/* ============================================================================= FireFly Morse Throwie - a light controlled (LED as Sensor) morse blinker throwie with ATTiny85 ============================================================================= Project definitions, sources ----------------------------------------------------------------------------- Version: 0.2 - ATTiny85, 1 MHz, BOD fuse disabled gitea : https://gitea.togo-lab.io/tgohle/0001-FireFly Date : 2026-06-13 ----------------------------------------------------------------------------- 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 // FIX 3: needed for PROGMEM / pgm_read_byte #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 if readability is poor. #define unitLength 100 // --------------------------------------------------------------------------- // LED pin definitions (N-side = cathode, P-side = anode for sensing/driving) #define LED1_N_SIDE 3 #define LED1_P_SIDE 4 // --------------------------------------------------------------------------- // ATTiny85 has 512 bytes SRAM; this frees all of it for the stack. // Only uppercase letters, digits, and the special chars in the switch below. // Test only: 20 x "0" due 0 = "-----" most energy draining const char morseText[] PROGMEM = "0000000000000000000"; // Ruler: 0...0....1...1....2 Attention: more Text // Ruler: 1...5....0...5....0 will cost more power! // --------------------------------------------------------------------------- // Darkness threshold. // Higher value = triggers in brighter conditions. // Best calibrated at actual dusk/dawn with the chosen LED type. unsigned int darknessThreshold = 17000; // --------------------------------------------------------------------------- // Watchdog interrupt flag — volatile because it is written in an ISR volatile boolean f_wdt = 1; // --------------------------------------------------------------------------- // Forward declarations void setup_watchdog(int ii); void system_sleep(); 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 some µ/mA. // PRTIM1, PRUSI: genuinely unused, safe to gate off // PRADC: controlled manually around sensDarkness() // PRTIM0: must stay ON — delay() and millis() depend on Timer0 PRR = (1 << PRTIM1) | (1 << PRUSI) | (1 << PRADC); // ADC is gated via PRR above; also clear ADEN just in case cbi(ADCSRA, ADEN); setup_watchdog(9); // 8-second watchdog interval // simple repeat if more delay is need, eg 4 } // =========================================================================== // Main loop // =========================================================================== void loop() { if (f_wdt == 1) { f_wdt = 0; // Enable ADC only for the sensing window, then shut it off again. cbi(PRR, PRADC); // un-gate ADC clock sbi(ADCSRA, ADEN); // power ADC on if (sensDarkness(LED1_N_SIDE, LED1_P_SIDE) > darknessThreshold) { morse(LED1_N_SIDE, LED1_P_SIDE); } cbi(ADCSRA, ADEN); // ADC off sbi(PRR, PRADC); // re-gate ADC clock // Return LED pins to input (high-Z) before sleeping pinMode(LED1_N_SIDE, INPUT); pinMode(LED1_P_SIDE, INPUT); } // sleep 4 x 8 s = ~32 s between activations for (uint8_t i = 0; i < 1; i++) { system_sleep(); } // f_wdt is set to 1 by the WDT ISR when the 8 s expire; no manual clear needed here. } // =========================================================================== // Sleep helpers // =========================================================================== void system_sleep() { set_sleep_mode(SLEEP_MODE_PWR_DOWN); sleep_enable(); sleep_mode(); // CPU halts here until WDT fires sleep_disable(); // ADC stays off - re-enable it in loop() only when sensing } // 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(int ii) { byte bb; if (ii > 9) ii = 9; bb = ii & 7; if (ii > 7) bb |= (1 << 5); bb |= (1 << WDCE); MCUSR &= ~(1 << WDRF); WDTCR |= (1 << WDCE) | (1 << WDE); // timed sequence - must not be split WDTCR = bb; WDTCR |= _BV(WDIE); } ISR(WDT_vect) { f_wdt = 1; } // =========================================================================== // Light sensor // =========================================================================== // Returns a "darkness level": higher = darker. // Charges the LED junction capacitor, then times how long it takes to bleed // back through the reverse-biased diode to a logic LOW. // ~30000 = pitch black, ~0 = bright light // return type is unsigned int (counter can reach 30000, fits in 16 bits) unsigned int sensDarkness(int LED_N, int LED_P) { unsigned int i; // Charge the LED (forward-bias momentarily) pinMode(LED_N, OUTPUT); pinMode(LED_P, OUTPUT); digitalWrite(LED_N, HIGH); digitalWrite(LED_P, LOW); // Let the N-end float and measure bleed-down time pinMode(LED_N, INPUT); digitalWrite(LED_N, LOW); // disable internal pull-up for (i = 0; i < 30000; i++) { if (digitalRead(LED_N) == 0) break; } // clean up after sensing 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); // read each character from flash with pgm_read_byte() // use < morseText length, not <= (avoids reading past the null terminator) uint8_t len = strlen_P(morseText); for (uint8_t i = 0; i < len; i++) { char c = (char)pgm_read_byte(&morseText[i]); switch (c) { // ----- Letters ----- case 'A': dit(LED_P); dah(LED_P); break; // .- case 'B': dah(LED_P); dit(LED_P); dit(LED_P); dit(LED_P); break; // -... case 'C': dah(LED_P); dit(LED_P); dah(LED_P); dit(LED_P); break; // -.-. case 'D': dah(LED_P); dit(LED_P); dit(LED_P); break; // -.. case 'E': dit(LED_P); break; // . case 'F': dit(LED_P); dit(LED_P); dah(LED_P); dit(LED_P); break; // ..-. case 'G': dah(LED_P); dah(LED_P); dit(LED_P); break; // --. case 'H': dit(LED_P); dit(LED_P); dit(LED_P); dit(LED_P); break; // .... case 'I': dit(LED_P); dit(LED_P); break; // .. case 'J': dit(LED_P); dah(LED_P); dah(LED_P); dah(LED_P); break; // .--- case 'K': dah(LED_P); dit(LED_P); dah(LED_P); break; // -.- case 'L': dit(LED_P); dah(LED_P); dit(LED_P); dit(LED_P); break; // .-.. case 'M': dah(LED_P); dah(LED_P); break; // -- case 'N': dah(LED_P); dit(LED_P); break; // -. case 'O': dah(LED_P); dah(LED_P); dah(LED_P); break; // --- case 'P': dit(LED_P); dah(LED_P); dah(LED_P); dit(LED_P); break; // .--. case 'Q': dah(LED_P); dah(LED_P); dit(LED_P); dah(LED_P); break; // --.- case 'R': dit(LED_P); dah(LED_P); dit(LED_P); break; // .-. case 'S': dit(LED_P); dit(LED_P); dit(LED_P); break; // ... case 'T': dah(LED_P); break; // - case 'U': dit(LED_P); dit(LED_P); dah(LED_P); break; // ..- case 'V': dit(LED_P); dit(LED_P); dit(LED_P); dah(LED_P); break; // ...- case 'W': dit(LED_P); dah(LED_P); dah(LED_P); break; // .-- case 'X': dah(LED_P); dit(LED_P); dit(LED_P); dah(LED_P); break; // -..- case 'Y': dah(LED_P); dit(LED_P); dah(LED_P); dah(LED_P); break; // -.-- case 'Z': dah(LED_P); dah(LED_P); dit(LED_P); dit(LED_P); break; // --.. // ----- Digits ----- case '1': dit(LED_P); dah(LED_P); dah(LED_P); dah(LED_P); dah(LED_P); break; // .---- case '2': dit(LED_P); dit(LED_P); dah(LED_P); dah(LED_P); dah(LED_P); break; // ..--- case '3': dit(LED_P); dit(LED_P); dit(LED_P); dah(LED_P); dah(LED_P); break; // ...-- case '4': dit(LED_P); dit(LED_P); dit(LED_P); dit(LED_P); dah(LED_P); break; // ....- case '5': dit(LED_P); dit(LED_P); dit(LED_P); dit(LED_P); dit(LED_P); break; // ..... case '6': dah(LED_P); dit(LED_P); dit(LED_P); dit(LED_P); dit(LED_P); break; // -.... case '7': dah(LED_P); dah(LED_P); dit(LED_P); dit(LED_P); dit(LED_P); break; // --... case '8': dah(LED_P); dah(LED_P); dah(LED_P); dit(LED_P); dit(LED_P); break; // ---. case '9': dah(LED_P); dah(LED_P); dah(LED_P); dah(LED_P); dit(LED_P); break; // ----. case '0': dah(LED_P); dah(LED_P); dah(LED_P); dah(LED_P); dah(LED_P); break; // ----- // ----- Punctuation ----- case ' ': delay(unitLength * 5); break; // word gap (5 + 3 = 8 units total) case '.': dit(LED_P); dah(LED_P); dit(LED_P); dah(LED_P); dit(LED_P); dah(LED_P); break; // .-.-.- case ',': dah(LED_P); dah(LED_P); dit(LED_P); dit(LED_P); dah(LED_P); dah(LED_P); break; // --..-- case '?': dit(LED_P); dit(LED_P); dah(LED_P); dah(LED_P); dit(LED_P); dit(LED_P); break; // ..--.. case '!': dah(LED_P); dit(LED_P); dah(LED_P); dit(LED_P); dah(LED_P); dah(LED_P); break; // -.-.-- case ':': dah(LED_P); dah(LED_P); dah(LED_P); dit(LED_P); dit(LED_P); dit(LED_P); break; // ---... case ';': dah(LED_P); dit(LED_P); dah(LED_P); dit(LED_P); dah(LED_P); dit(LED_P); break; // -.-.-. case '(': dah(LED_P); dit(LED_P); dah(LED_P); dah(LED_P); dit(LED_P); break; // -.--. case ')': dah(LED_P); dit(LED_P); dah(LED_P); dah(LED_P); dit(LED_P); dah(LED_P); break; // -.--.- case '"': dit(LED_P); dah(LED_P); dit(LED_P); dit(LED_P); dah(LED_P); dit(LED_P); break; // .-..-. case '@': dit(LED_P); dah(LED_P); dah(LED_P); dit(LED_P); dah(LED_P); dit(LED_P); break; // .--.-. case '&': dit(LED_P); dah(LED_P); dit(LED_P); dit(LED_P); dit(LED_P); break; // .-... } // Inter-character gap: 3 units (the dit/dah functions already trail 1 unit, // so this adds the remaining 2 to reach the standard 3-unit gap). // Space case already handles word gap - no extra delay needed there. if (c != ' ') { delay(unitLength * 2); } } }