diff --git a/firmware/ATTINY85_2026_MorseThrowie/ATTINY85_2026_MorseThrowie.ino b/firmware/ATTINY85_2026_MorseThrowie/ATTINY85_2026_MorseThrowie.ino index 6114029..04cc1da 100644 --- a/firmware/ATTINY85_2026_MorseThrowie/ATTINY85_2026_MorseThrowie.ino +++ b/firmware/ATTINY85_2026_MorseThrowie/ATTINY85_2026_MorseThrowie.ino @@ -1,18 +1,28 @@ /* ============================================================================= - FireFly Morse Throwie + 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: + 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: @@ -24,7 +34,7 @@ 'P', ".--." 'Q', "--.-" 'R', ".-." 'S', "..." 'T', "-" 'U', "..-" 'V', "...-" 'W', ".--" 'X', "-..-" - 'Y', "-.--" 'Z', "--.." + 'Y', "-.--" 'Z', "--.." '1', ".----" '2', "..---" '3', "...--" '4', "....-" '5', "....." '6', "-...." @@ -36,15 +46,19 @@ '(', "-.--." ')', "-.--.-" '"', ".-..-." '@', ".--.-." '&', ".-..." +----------------------------------------------------------------------------- + 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 +#include +#include +#include #ifndef cbi #define cbi(sfr, bit) (_SFR_BYTE(sfr) &= ~_BV(bit)) @@ -56,37 +70,52 @@ // --------------------------------------------------------------------------- // 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. +// 100 ms gives readable optical Morse; raise to 150 ms if readability is poor. #define unitLength 100 // --------------------------------------------------------------------------- -// LED pin definitions (N-side = cathode, P-side = anode for sensing/driving) +// Sleep interval. +// Watchdog setup uses 8 s. For final use, change sleepCycles from 1 to 4 +// for about 32 s between sensing/blinking activations. +const uint8_t sleepCycles = 4; + +// --------------------------------------------------------------------------- +// 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! - +// 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 = "TOGO LAB"; +// 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. -unsigned int darknessThreshold = 17000; +const unsigned int darknessThreshold = 17000; // --------------------------------------------------------------------------- -// Watchdog interrupt flag — volatile because it is written in an ISR -volatile boolean f_wdt = 1; +// Watchdog interrupt flag — volatile because it is written in an ISR. +volatile bool f_wdt = true; // --------------------------------------------------------------------------- -// Forward declarations -void setup_watchdog(int ii); +// 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); @@ -97,17 +126,22 @@ void dah(int LED_P); // =========================================================================== 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 + // 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 is gated via PRR above; also clear ADEN just in case + // ADC off. It is not needed for digitalRead()-based LED sensing. cbi(ADCSRA, ADEN); - setup_watchdog(9); // 8-second watchdog interval - // simple repeat if more delay is need, eg 4 + // 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. } // =========================================================================== @@ -115,117 +149,145 @@ void setup() // =========================================================================== 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 (f_wdt) { + f_wdt = false; 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); + // Return LED pins to high-Z before sleeping. + // Internal pullups stay disabled for the LED pins. + releaseLedPins(LED1_N_SIDE, LED1_P_SIDE); } - // sleep 4 x 8 s = ~32 s between activations - for (uint8_t i = 0; i < 1; i++) { + // 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(); } - // f_wdt is set to 1 by the WDT ISR when the 8 s expire; no manual clear needed here. +} + +// =========================================================================== +// 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(); - sleep_mode(); // CPU halts here until WDT fires + sei(); + sleep_cpu(); + + // WDT interrupt wakes the CPU here. 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) +void setup_watchdog(uint8_t ii) { - byte bb; if (ii > 9) ii = 9; - bb = ii & 7; - if (ii > 7) bb |= (1 << 5); - bb |= (1 << WDCE); + uint8_t bb = ii & 7; + if (ii > 7) bb |= (1 << WDP3); + + uint8_t oldSREG = SREG; + cli(); + + wdt_reset(); MCUSR &= ~(1 << WDRF); - WDTCR |= (1 << WDCE) | (1 << WDE); // timed sequence - must not be split - WDTCR = bb; - WDTCR |= _BV(WDIE); + + // 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 = 1; + f_wdt = true; } // =========================================================================== // 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. +// 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 - -// 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) + // Charge the LED junction capacitance. 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 + // Let the N-side float and measure bleed-down time. pinMode(LED_N, INPUT); - digitalWrite(LED_N, LOW); // disable internal pull-up + digitalWrite(LED_N, LOW); // disable internal pullup for (i = 0; i < 30000; i++) { - if (digitalRead(LED_N) == 0) break; + if (digitalRead(LED_N) == LOW) break; } - // clean up after sensing + // 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; + return i; } // =========================================================================== // Morse helpers // =========================================================================== - void dit(int LED_P) { - digitalWrite(LED_P, HIGH); delay(unitLength); - digitalWrite(LED_P, LOW); delay(unitLength); + 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); + digitalWrite(LED_P, HIGH); + delay(unitLength * 3); + digitalWrite(LED_P, LOW); + delay(unitLength); } // =========================================================================== @@ -236,76 +298,94 @@ void morse(int LED_N, int LED_P) pinMode(LED_N, OUTPUT); pinMode(LED_P, OUTPUT); digitalWrite(LED_N, LOW); + digitalWrite(LED_P, 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++) - { + 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); 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; // --.. + 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); 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; // ----- + 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 ' ': 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; // .-... + 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 (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 != ' ') { + // 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); } \ No newline at end of file diff --git a/firmware/archive/ATTINY85_2026_MorseThrowie 2.0/ATTINY85_2026_MorseThrowie 2.0.ino b/firmware/archive/ATTINY85_2026_MorseThrowie 2.0/ATTINY85_2026_MorseThrowie 2.0.ino new file mode 100644 index 0000000..6114029 --- /dev/null +++ b/firmware/archive/ATTINY85_2026_MorseThrowie 2.0/ATTINY85_2026_MorseThrowie 2.0.ino @@ -0,0 +1,311 @@ +/* + ============================================================================= + 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); + } + } +} \ No newline at end of file