/* ============================================================================= FireFly Morse Throwie - a light controlled (LED as Sensor) morse blinker throwie with ATTiny85 ============================================================================= Project definitions, sources ----------------------------------------------------------------------------- Version: 0.3 - ATTiny85, 1 MHz, BOD fuse disabled gitea : https://gitea.togo-lab.io/tgohle/0001-FireFly Date : 2026-06-14 Key changes Version 0.3: - Removed unnecessary ADC enable/disable from loop() - Disabled ADC and analog comparator in setup() - Added INPUT_PULLUP for unused Arduino pins 0, 1, 2 will save power - Added safer watchdog setup with interrupt protection to save run near brownout - Added wdt_reset() before sleep for cleaner 8 s timing - Added lowercase-to-uppercase handling - Unsupported characters are now ignored without adding fake timing gaps ----------------------------------------------------------------------------- Inspired by Karl Lunt's FireFly project: http://www.seanet.com/~karllunt/fireflyLED.html Morse code reference: 'A', ".-" 'B', "-..." 'C', "-.-." 'D', "-.." 'E', "." 'F', "..-." 'G', "--." 'H', "...." 'I', ".." 'J', ".---" 'K', "-.-" 'L', ".-.." 'M', "--" 'N', "-." 'O', "---" 'P', ".--." 'Q', "--.-" 'R', ".-." 'S', "..." 'T', "-" 'U', "..-" 'V', "...-" 'W', ".--" 'X', "-..-" 'Y', "-.--" 'Z', "--.." '1', ".----" '2', "..---" '3', "...--" '4', "....-" '5', "....." '6', "-...." '7', "--..." '8', "---.." '9', "----." '0', "-----" '.', ".-.-.-" ',', "--..--" '?', "..--.." '!', "-.-.--" ':', "---..." ';', "-.-.-." '(', "-.--." ')', "-.--.-" '"', ".-..-." '@', ".--.-." '&', ".-..." ----------------------------------------------------------------------------- Legal stuff / Copyright: License_-_CC_BY-NC_4.0 https://creativecommons.org/licenses/by-nc/4.0/ ----------------------------------------------------------------------------- */ #include #include #include #include #include #ifndef cbi #define cbi(sfr, bit) (_SFR_BYTE(sfr) &= ~_BV(bit)) #endif #ifndef sbi #define sbi(sfr, bit) (_SFR_BYTE(sfr) |= _BV(bit)) #endif // --------------------------------------------------------------------------- // Timing: unit length in ms. // At 1 MHz, delay() is accurate when F_CPU=1000000L is set in boards.txt. // 100 ms gives readable optical Morse; raise to 150 ms if readability is poor. #define unitLength 100 // --------------------------------------------------------------------------- // Sleep interval. // Watchdog setup uses 8 s. For final use, change sleepCycles from 4 to 6 // for about 32s - 48s between sensing/blinking activations. Goal ~ 1 blink/min const uint8_t sleepCycles = 6; // --------------------------------------------------------------------------- // LED pin definitions (N-side = cathode, P-side = anode for sensing/driving). #define LED1_N_SIDE 3 #define LED1_P_SIDE 4 // --------------------------------------------------------------------------- // ATTiny85 Arduino pins 0, 1, 2 are unused in actual design and physically open. // Keep them in INPUT_PULLUP so they do not float and waste current. #define UNUSED_PIN_0 0 #define UNUSED_PIN_1 1 #define UNUSED_PIN_2 2 // --------------------------------------------------------------------------- // ATTiny85 has 512 bytes SRAM; this keeps the Morse text in flash. // Only uppercase letters, digits, spaces, and the special chars in the switch // below are emitted. Lowercase letters are converted to uppercase. // Test setup: 20 x "0" because 0 = "-----" gives maximum LED ON time for test. // shorter text will better, more text will cost more power. const char morseText[] PROGMEM = "TGO 26"; // Ruler: 0....0....1...1....2 // Ruler: 0....5....0...5....0 // --------------------------------------------------------------------------- // Darkness threshold. // Higher value = triggers in brighter conditions. // Best calibrated at actual dusk/dawn with the chosen LED type. const unsigned int darknessThreshold = 17000; // --------------------------------------------------------------------------- // Watchdog interrupt flag — volatile because it is written in an ISR. volatile bool f_wdt = true; // --------------------------------------------------------------------------- // Forward declarations. void setup_watchdog(uint8_t ii); void system_sleep(); void configureUnusedPins(); void releaseLedPins(int LED_N, int LED_P); unsigned int sensDarkness(int LED_N, int LED_P); void morse(int LED_N, int LED_P); void dit(int LED_P); void dah(int LED_P); // =========================================================================== // Setup // =========================================================================== void setup() { // Disable unused peripherals immediately to save power. // PRTIM1, PRUSI: unused, safe to gate off. // PRADC: ADC is not used; LED sensing is done with digitalRead(). // PRTIM0: must stay ON while awake because delay() depends on Timer0. PRR = (1 << PRTIM1) | (1 << PRUSI) | (1 << PRADC); // ADC off. It is not needed for digitalRead()-based LED sensing. cbi(ADCSRA, ADEN); // Analog comparator off. Saves a little sleep current. ACSR |= (1 << ACD); configureUnusedPins(); releaseLedPins(LED1_N_SIDE, LED1_P_SIDE); setup_watchdog(9); // 8-second watchdog interval. } // =========================================================================== // Main loop // =========================================================================== void loop() { if (f_wdt) { f_wdt = false; if (sensDarkness(LED1_N_SIDE, LED1_P_SIDE) > darknessThreshold) { morse(LED1_N_SIDE, LED1_P_SIDE); } // Return LED pins to high-Z before sleeping. // Internal pullups stay disabled for the LED pins. releaseLedPins(LED1_N_SIDE, LED1_P_SIDE); } // Test setup: 1 x 8 s sleep. // Final setup: set sleepCycles = 4 for about 32 s. for (uint8_t i = 0; i < sleepCycles; i++) { system_sleep(); } } // =========================================================================== // Pin helpers // =========================================================================== void configureUnusedPins() { pinMode(UNUSED_PIN_0, INPUT_PULLUP); pinMode(UNUSED_PIN_1, INPUT_PULLUP); pinMode(UNUSED_PIN_2, INPUT_PULLUP); } void releaseLedPins(int LED_N, int LED_P) { // digitalWrite LOW before INPUT disables the internal pullup on Arduino cores. digitalWrite(LED_N, LOW); digitalWrite(LED_P, LOW); pinMode(LED_N, INPUT); pinMode(LED_P, INPUT); } // =========================================================================== // Sleep helpers // =========================================================================== void system_sleep() { // Reset watchdog counter so each sleep cycle starts with a fresh interval. wdt_reset(); set_sleep_mode(SLEEP_MODE_PWR_DOWN); // Atomic sleep entry pattern. // After sei(), AVR executes the next instruction before servicing interrupts, // so sleep_cpu() is not skipped by a just-pending interrupt. cli(); sleep_enable(); sei(); sleep_cpu(); // WDT interrupt wakes the CPU here. sleep_disable(); } // Watchdog setup - ii selects timeout: // 0=16ms 1=32ms 2=64ms 3=128ms 4=250ms 5=500ms // 6=1s 7=2s 8=4s 9=8s void setup_watchdog(uint8_t ii) { if (ii > 9) ii = 9; uint8_t bb = ii & 7; if (ii > 7) bb |= (1 << WDP3); uint8_t oldSREG = SREG; cli(); wdt_reset(); MCUSR &= ~(1 << WDRF); // Timed sequence: enable configuration change, then set interrupt-only WDT. WDTCR |= (1 << WDCE) | (1 << WDE); WDTCR = bb | (1 << WDIE); SREG = oldSREG; } ISR(WDT_vect) { f_wdt = true; } // =========================================================================== // Light sensor // =========================================================================== // Returns a "darkness level": higher = darker. // Charges the LED junction capacitance, then times how long it takes to bleed // back through the reverse-biased LED until the input reads LOW. // ~30000 = pitch black, ~0 = bright light unsigned int sensDarkness(int LED_N, int LED_P) { unsigned int i; // Charge the LED junction capacitance. pinMode(LED_N, OUTPUT); pinMode(LED_P, OUTPUT); digitalWrite(LED_N, HIGH); digitalWrite(LED_P, LOW); // Let the N-side float and measure bleed-down time. pinMode(LED_N, INPUT); digitalWrite(LED_N, LOW); // disable internal pullup for (i = 0; i < 30000; i++) { if (digitalRead(LED_N) == LOW) break; } // Clean up after sensing: both LED pins low, then caller releases them. pinMode(LED_N, OUTPUT); digitalWrite(LED_N, LOW); pinMode(LED_P, OUTPUT); digitalWrite(LED_P, LOW); return i; } // =========================================================================== // Morse helpers // =========================================================================== void dit(int LED_P) { digitalWrite(LED_P, HIGH); delay(unitLength); digitalWrite(LED_P, LOW); delay(unitLength); } void dah(int LED_P) { digitalWrite(LED_P, HIGH); delay(unitLength * 3); digitalWrite(LED_P, LOW); delay(unitLength); } // =========================================================================== // Morse sender // =========================================================================== void morse(int LED_N, int LED_P) { pinMode(LED_N, OUTPUT); pinMode(LED_P, OUTPUT); digitalWrite(LED_N, LOW); digitalWrite(LED_P, LOW); const size_t len = strlen_P(morseText); for (size_t i = 0; i < len; i++) { char c = (char)pgm_read_byte(&morseText[i]); // Allow lowercase source text without silently breaking timing. if (c >= 'a' && c <= 'z') { c = c - 'a' + 'A'; } bool symbolSent = false; switch (c) { // ----- Letters ----- case 'A': dit(LED_P); dah(LED_P); symbolSent = true; break; // .- case 'B': dah(LED_P); dit(LED_P); dit(LED_P); dit(LED_P); symbolSent = true; break; // -... case 'C': dah(LED_P); dit(LED_P); dah(LED_P); dit(LED_P); symbolSent = true; break; // -.-. case 'D': dah(LED_P); dit(LED_P); dit(LED_P); symbolSent = true; break; // -.. case 'E': dit(LED_P); symbolSent = true; break; // . case 'F': dit(LED_P); dit(LED_P); dah(LED_P); dit(LED_P); symbolSent = true; break; // ..-. case 'G': dah(LED_P); dah(LED_P); dit(LED_P); symbolSent = true; break; // --. case 'H': dit(LED_P); dit(LED_P); dit(LED_P); dit(LED_P); symbolSent = true; break; // .... case 'I': dit(LED_P); dit(LED_P); symbolSent = true; break; // .. case 'J': dit(LED_P); dah(LED_P); dah(LED_P); dah(LED_P); symbolSent = true; break; // .--- case 'K': dah(LED_P); dit(LED_P); dah(LED_P); symbolSent = true; break; // -.- case 'L': dit(LED_P); dah(LED_P); dit(LED_P); dit(LED_P); symbolSent = true; break; // .-.. case 'M': dah(LED_P); dah(LED_P); symbolSent = true; break; // -- case 'N': dah(LED_P); dit(LED_P); symbolSent = true; break; // -. case 'O': dah(LED_P); dah(LED_P); dah(LED_P); symbolSent = true; break; // --- case 'P': dit(LED_P); dah(LED_P); dah(LED_P); dit(LED_P); symbolSent = true; break; // .--. case 'Q': dah(LED_P); dah(LED_P); dit(LED_P); dah(LED_P); symbolSent = true; break; // --.- case 'R': dit(LED_P); dah(LED_P); dit(LED_P); symbolSent = true; break; // .-. case 'S': dit(LED_P); dit(LED_P); dit(LED_P); symbolSent = true; break; // ... case 'T': dah(LED_P); symbolSent = true; break; // - case 'U': dit(LED_P); dit(LED_P); dah(LED_P); symbolSent = true; break; // ..- case 'V': dit(LED_P); dit(LED_P); dit(LED_P); dah(LED_P); symbolSent = true; break; // ...- case 'W': dit(LED_P); dah(LED_P); dah(LED_P); symbolSent = true; break; // .-- case 'X': dah(LED_P); dit(LED_P); dit(LED_P); dah(LED_P); symbolSent = true; break; // -..- case 'Y': dah(LED_P); dit(LED_P); dah(LED_P); dah(LED_P); symbolSent = true; break; // -.-- case 'Z': dah(LED_P); dah(LED_P); dit(LED_P); dit(LED_P); symbolSent = true; break; // --.. // ----- Digits ----- case '1': dit(LED_P); dah(LED_P); dah(LED_P); dah(LED_P); dah(LED_P); symbolSent = true; break; // .---- case '2': dit(LED_P); dit(LED_P); dah(LED_P); dah(LED_P); dah(LED_P); symbolSent = true; break; // ..--- case '3': dit(LED_P); dit(LED_P); dit(LED_P); dah(LED_P); dah(LED_P); symbolSent = true; break; // ...-- case '4': dit(LED_P); dit(LED_P); dit(LED_P); dit(LED_P); dah(LED_P); symbolSent = true; break; // ....- case '5': dit(LED_P); dit(LED_P); dit(LED_P); dit(LED_P); dit(LED_P); symbolSent = true; break; // ..... case '6': dah(LED_P); dit(LED_P); dit(LED_P); dit(LED_P); dit(LED_P); symbolSent = true; break; // -.... case '7': dah(LED_P); dah(LED_P); dit(LED_P); dit(LED_P); dit(LED_P); symbolSent = true; break; // --... case '8': dah(LED_P); dah(LED_P); dah(LED_P); dit(LED_P); dit(LED_P); symbolSent = true; break; // ---.. case '9': dah(LED_P); dah(LED_P); dah(LED_P); dah(LED_P); dit(LED_P); symbolSent = true; break; // ----. case '0': dah(LED_P); dah(LED_P); dah(LED_P); dah(LED_P); dah(LED_P); symbolSent = true; break; // ----- // ----- Punctuation ----- case '.': dit(LED_P); dah(LED_P); dit(LED_P); dah(LED_P); dit(LED_P); dah(LED_P); symbolSent = true; break; // .-.-.- case ',': dah(LED_P); dah(LED_P); dit(LED_P); dit(LED_P); dah(LED_P); dah(LED_P); symbolSent = true; break; // --..-- case '?': dit(LED_P); dit(LED_P); dah(LED_P); dah(LED_P); dit(LED_P); dit(LED_P); symbolSent = true; break; // ..--.. case '!': dah(LED_P); dit(LED_P); dah(LED_P); dit(LED_P); dah(LED_P); dah(LED_P); symbolSent = true; break; // -.-.-- case ':': dah(LED_P); dah(LED_P); dah(LED_P); dit(LED_P); dit(LED_P); dit(LED_P); symbolSent = true; break; // ---... case ';': dah(LED_P); dit(LED_P); dah(LED_P); dit(LED_P); dah(LED_P); dit(LED_P); symbolSent = true; break; // -.-.-. case '(': dah(LED_P); dit(LED_P); dah(LED_P); dah(LED_P); dit(LED_P); symbolSent = true; break; // -.--. case ')': dah(LED_P); dit(LED_P); dah(LED_P); dah(LED_P); dit(LED_P); dah(LED_P); symbolSent = true; break; // -.--.- case '"': dit(LED_P); dah(LED_P); dit(LED_P); dit(LED_P); dah(LED_P); dit(LED_P); symbolSent = true; break; // .-..-. case '@': dit(LED_P); dah(LED_P); dah(LED_P); dit(LED_P); dah(LED_P); dit(LED_P); symbolSent = true; break; // .--.-. case '&': dit(LED_P); dah(LED_P); dit(LED_P); dit(LED_P); dit(LED_P); symbolSent = true; break; // .-... // Space handling: // Previous symbol already ended with 1 unit OFF and then received the // normal 2 unit character gap. Add 4 units here -> total word gap 7 units. case ' ': delay(unitLength * 4); break; // Unsupported chars are ignored without adding an artificial gap. default: break; } // Inter-character gap: 3 units total. // dit()/dah() already trail 1 unit OFF, so add the remaining 2 units. if (symbolSent) { delay(unitLength * 2); } } digitalWrite(LED_P, LOW); digitalWrite(LED_N, LOW); }