2 Commits
2 changed files with 294 additions and 127 deletions

No files matched your search

+49 -3
View File
@@ -14,7 +14,16 @@ FireFly Solar is a small outdoor LED blinker inspired by the idea of an LED thro
During the day, the solar cell charges the energy storage.
At night, the circuit wakes up and sends a simple Morse-style light signal.
The project is partly a small garden object, partly a low-power electronics experiment, and partly a design exercise for future DIY kits.
## About this Project
FireFly Solar is my **technology playground**. It is a small garden object, but its real purpose is to learn by building something complete. With it I learn about:
- **Circuit design at the edge**: in this case, as low power as possible
- **PCB design**
- **Weatherproof casing**
- **ATtiny programming**
Possible next technology step: everything about DIY SMD design and soldering.
## Project Goals
@@ -24,6 +33,40 @@ The project is partly a small garden object, partly a low-power electronics expe
- Test mechanical concepts for outdoor 3D-printed parts
- Learn what is needed to turn a small electronics project into a possible DIY kit
## Software
The firmware runs on an **ATtiny85** at 1 MHz (internal clock, ATTinyCore: `clock=1internal`, `millis=disabled`). The LED itself is used as the light sensor. When it is dark, the firefly sends a short Morse text ("TGO 26") about once a minute.
### Current Firmware version: v0.4 (2026-11-08)
Goal: lowest possible average current.
Changes vs v0.3:
- No `delay()` / `millis()`; Timer0 and Timer1 are gated off in PRR.
- The watchdog (interrupt mode) is the only timebase: Morse units (~96 ms), the sensing window and the 8 s sleeps. The CPU sleeps in power-down in between.
- Darkness sensing: the CPU sleeps in 16 ms steps while the LED junction discharges, instead of a busy `digitalRead()` loop. Bright light exits after the first step. `darknessThreshold` is replaced by `darkSteps`.
- The ADC is disabled before it is gated off in PRR; pins are set up before PRR.
- After a power-on or external reset, no blinking for `RECOVER_CYCLES` (about 16 min), so the supercap can recharge.
- `DEBUG_TEST` switch for bench testing: 8 s cycle, no recovery wait, short flash when the sketch decides "bright". Set it to 0 for the field.
- Optional `BOD_FUSE_ENABLED` (BOD off during sleep); unused with the BOD fuse off.
Tested: compiles with ATTinyCore and runs on hardware. Current draw is not yet measured.
## PCB
- Finished: a batch of 10 is assembled, tested and working.
- Latest adjustment: fine-tuned the resistors for the LED, and the 3.9 V Zener diode is confirmed safe avoid overcharging 4V supercap.
- Next version: the PCB should be a little shorter. The wiring space is a bit too tight now.
## Casing
Works basically as intended. Lessons for the next version:
- Better locking of the solar modules.
- The rope structure works, but building it is too complicated and takes a lot of nerves.
- Using material tension to secure the modules via rope was basically a good idea.
## Naming
The English project name remains:
@@ -39,9 +82,12 @@ The name roughly means **sun-disc firefly**. It keeps the solar theme, adds a sl
---
## Contributing
PRs and issues welcome. Keep it simple.
PRs and issues welcome. Keep it simple.
Photos of your build you send me go in `Documentation/gallery/` with a short caption and license notice.
---
## Default license**: [CC-BY-NC-4.0](https://creativecommons.org/licenses/by-nc/4.0/)
## License
Default license: [CC-BY-NC-4.0](https://creativecommons.org/licenses/by-nc/4.0/)
@@ -1,24 +1,46 @@
/*
=============================================================================
FireFly Morse Throwie
- a light controlled (LED as Sensor) morse blinker throwie with ATTiny85
FireFly Solar / 日輪蛍 – Nichirin Hotaru / a Solar Morse Throwie
太陽を宿し、夜にモールスを灯す蛍。
Taiyō o yadoshi, yoru ni Mōrusu o tomosu hotaru.
A firefly carrying the sun, lighting Morse in the night.
=============================================================================
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
Version : 0.5 - ATTiny85, 1 MHz, BOD fuse disabled
used Flash: 1284 bytes
gitea : https://gitea.togo-lab.io/tgohle/0001-FireFly
Date : 2026-11-10
Key changes Version 0.5 (goal: lowest possible average current):
Key changes Version 0.3:
- No delay(), no millis(): Timer0 and Timer1 are not needed any more and
are switched off in PRR.
- ALL timing (Morse units, sensing window, 8 s sleep) is done by the watchdog
in interrupt mode while the CPU is in POWER-DOWN. The CPU runs only for a
few microseconds per wake-up. LED pins keep their state in power-down.
- Darkness sensing by LED no longer polls digitalRead() in a busy loop (that was
~0.3-1 s awake per cycle). The LED junction is charged, then the CPU sleeps
in 16 ms steps and checks the pin: bright light exits after the first step.
- ADC is disabled BEFORE it is gated off in PRR (order matters).
- After a power-on, external, brown-out or watchdog reset the sketch waits
RECOVER_CYCLES cycles before blinking, allowing the supercap to recharge.
Default ~13 min 20 s (nominal); the debug option bypasses recovery.
- Startup code saves MCUSR and disables the watchdog early (.init3), avoiding
a watchdog-reset loop before setup() can execute.
- Optional: BOD disable during sleep (only useful if BOD fuse is enabled).
But found out that also BOD Fuse for 1.8V shorten unnecessary blinking time,
will still work ~1V, meaning ~20 min longer blinking time.
- Field Test: After a sunny day in Winter, outdoor -1 ... 8°C, with a payload
of 10 signs) it blinks for >> 12h.
- 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
Timing note: the watchdog oscillator is not precise (changes with Vcc and
temperature). All Morse timing comes from the same clock, so dit/dah/gap
ratios stay correct; only the absolute speed drifts a little.
The HW actual allowes 5V and 3.3V, standard is 3.3V. So don't expec exact timing.
-----------------------------------------------------------------------------
@@ -44,7 +66,9 @@ Inspired by Karl Lunt's FireFly project:
'.', ".-.-.-" ',', "--..--" '?', "..--.."
'!', "-.-.--" ':', "---..." ';', "-.-.-."
'(', "-.--." ')', "-.--.-" '"', ".-..-."
'@', ".--.-." '&', ".-..."
'@', ".--.-." '&', ".-..." '-', "-....-"
'/', "-..-." ''', ".----." '+', ".-.-."
'=', "-...-" '_', "..--.-"
-----------------------------------------------------------------------------
@@ -60,6 +84,12 @@ Inspired by Karl Lunt's FireFly project:
#include <avr/interrupt.h>
#include <string.h>
// ---------------------------------------------------------------------------
// DEBUG_TEST 1 = bench testing: no reset-recovery wait, a blink attempt every
// 8 s, and a short flash when the sketch thinks it is BRIGHT.
// SET TO 0 FOR THE FIELD (40 s idle sleep, ~13m20s reset recovery).
#define DEBUG_TEST 0
#ifndef cbi
#define cbi(sfr, bit) (_SFR_BYTE(sfr) &= ~_BV(bit))
#endif
@@ -68,16 +98,55 @@ Inspired by Karl Lunt's FireFly project:
#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
// Watchdog timeout codes (nominal, at ~5 V; real values vary with Vcc/temp):
// actual HW uses 3.3 (standard) or 5V (for blue LED experimental setup)
// so don't expect exact timing)
// 0=16ms 1=32ms 2=64ms 3=128ms 4=250ms 5=500ms
// 6=1s 7=2s 8=4s 9=8s
//
// One Morse unit = UNIT_WDT_A + UNIT_WDT_B -> 64 ms + 32 ms = ~96 ms
// A=3, B=0 -> ~144 ms (easier to read, more LED time)
// A=3, B=0xFF -> ~128 ms
// A=2, B=0xFF -> ~64 ms (fast, less LED time, harder to read by eye)
// Use 0xFF for B to skip the second sleep.
#define UNIT_WDT_A 2
#define UNIT_WDT_B 1
// ---------------------------------------------------------------------------
// 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;
// Sleep interval between two sensing/blink attempts: sleepCycles x 8 s.
// 5 x 8 s = 40 s. Goal ~ 1 blink/min.
#if DEBUG_TEST
const uint8_t sleepCycles = 1;
#else
const uint8_t sleepCycles = 5;
#endif
// ---------------------------------------------------------------------------
// Darkness window in 16 ms steps (nominal).
// After charging the LED junction the CPU sleeps step by step. If the pin
// reads LOW within the window the LED discharged quickly = bright = no blink.
// Still HIGH after the full window = dark = blink.
// Longer window = triggers in darker conditions
// The window costs almost no energy (CPU sleeps), so it is only a threshold.
// Calibrate at real dusk. Old threshold 17000 loops: measure the old
// sensDarkness() time with micros() at your dusk level and divide it by 16 ms.
const uint8_t darkSteps = 40; // ~640 ms
// ---------------------------------------------------------------------------
// After a power-on / brown-out / external / watchdog reset, do not blink for
// this many cycles (each has 40 s of sleep) so the supercap can recharge.
// 20 -> ~13 min 20 s nominal. Set to 0 for bench testing/uploading.
#if DEBUG_TEST
#define RECOVER_CYCLES 0
#else
#define RECOVER_CYCLES 20
#endif
// ---------------------------------------------------------------------------
// Set to 1 only if the BOD fuse is enabled (e.g. hfuse 0xDE = 1.8 V).
// Then BOD is switched off during power-down to save its ~20 uA.
// With BOD fuse disabled this has no effect, leave it 0.
#define BOD_FUSE_ENABLED 0
// ---------------------------------------------------------------------------
// LED pin definitions (N-side = cathode, P-side = anode for sensing/driving).
@@ -95,28 +164,38 @@ const uint8_t sleepCycles = 6;
// 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;
// Shorter text = less LED time = less energy.
const char morseText[] PROGMEM = "TOGO-LAB.IO FIREFLY";
// Ruler: 0....0....1....1....2
// Ruler: 01...5....0....5....0
// ---------------------------------------------------------------------------
// Watchdog interrupt flag — volatile because it is written in an ISR.
volatile bool f_wdt = true;
// Watchdog interrupt flag - volatile because it is written in an ISR.
volatile bool f_wdt = false;
// Remaining cycles without blinking after a reset.
static uint8_t recoverCycles = 0;
// Preserve the reset flags before Arduino startup and stop a watchdog left
// running by a previous reset. AVR-libc's recommended .init3 pattern.
// .noinit is not cleared by C runtime initialization.
static uint8_t resetCauseEarly __attribute__((section(".noinit")));
void earlyWatchdogInit(void) __attribute__((used, naked, section(".init3")));
void earlyWatchdogInit(void)
{
resetCauseEarly = MCUSR;
MCUSR = 0; // Clear WDRF before disabling WDT.
wdt_disable(); // Prevent repeated ~15 ms watchdog resets.
}
// ---------------------------------------------------------------------------
// Forward declarations.
void setup_watchdog(uint8_t ii);
void system_sleep();
static void sleepWdt(uint8_t ii);
static void waitUnits(uint8_t n);
void configureUnusedPins();
void releaseLedPins(int LED_N, int LED_P);
unsigned int sensDarkness(int LED_N, int LED_P);
bool isDark(int LED_N, int LED_P);
void morse(int LED_N, int LED_P);
void dit(int LED_P);
void dah(int LED_P);
@@ -126,22 +205,28 @@ void dah(int LED_P);
// ===========================================================================
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);
// Reset cause was captured before C/Arduino initialization in .init3.
// Include WDRF: a watchdog reset also needs supercap recovery time.
if (resetCauseEarly & ((1 << PORF) | (1 << BORF) |
(1 << EXTRF) | (1 << WDRF))) {
recoverCycles = RECOVER_CYCLES;
}
// Pins first (while the Arduino core can still reach all timer registers
// for its turnOffPWM() handling in digitalWrite()).
configureUnusedPins();
releaseLedPins(LED1_N_SIDE, LED1_P_SIDE);
setup_watchdog(9); // 8-second watchdog interval.
// ADC off FIRST, then gate its clock. With PRADC set, ADC register
// writes are ignored, so the order matters.
cbi(ADCSRA, ADEN);
// Analog comparator off.
ACSR |= (1 << ACD);
// Power reduction: ADC, USI, Timer0, Timer1 all off.
// Nothing in this sketch uses them: no delay(), no millis(), no analogWrite().
PRR = (1 << PRADC) | (1 << PRUSI) | (1 << PRTIM0) | (1 << PRTIM1);
}
// ===========================================================================
@@ -149,22 +234,29 @@ void setup()
// ===========================================================================
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);
if (recoverCycles > 0) {
recoverCycles--;
} else if (isDark(LED1_N_SIDE, LED1_P_SIDE)) {
morse(LED1_N_SIDE, LED1_P_SIDE);
}
#if DEBUG_TEST
else {
// Sketch decided "bright": one short ~128 ms flash so you can see it.
pinMode(LED1_N_SIDE, OUTPUT);
pinMode(LED1_P_SIDE, OUTPUT);
digitalWrite(LED1_N_SIDE, LOW);
digitalWrite(LED1_P_SIDE, HIGH);
sleepWdt(3);
digitalWrite(LED1_P_SIDE, LOW);
}
#endif
// 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();
sleepWdt(9); // 8 s each
}
}
@@ -188,87 +280,110 @@ void releaseLedPins(int LED_N, int LED_P)
}
// ===========================================================================
// Sleep helpers
// Sleep helpers (watchdog = only timebase)
// ===========================================================================
void system_sleep()
ISR(WDT_vect)
{
// 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();
f_wdt = true;
}
// 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)
// Sleep in POWER-DOWN until the watchdog interrupt fires after timeout code ii
// (0=16 ms ... 9=8 s). Reconfigures the watchdog on every call, so the full
// interval always starts fresh.
static void sleepWdt(uint8_t ii)
{
if (ii > 9) ii = 9;
uint8_t bb = ii & 7;
if (ii > 7) bb |= (1 << WDP3);
uint8_t oldSREG = SREG;
// Interrupt-only mode (WDE = 0). WDIF written as 1 clears a stale flag.
const uint8_t cfg = bb | (1 << WDIE) | (1 << WDIF);
f_wdt = false;
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);
// WDCE/WDE unlock and configuration: two consecutive OUT instructions.
// The second write is guaranteed to occur within the required four cycles.
const uint8_t unlock = _BV(WDCE) | _BV(WDE);
__asm__ __volatile__(
"out %[wdt], %[unlock] \n\t"
"out %[wdt], %[config] \n\t"
:
: [wdt] "I" (_SFR_IO_ADDR(WDTCR)),
[unlock] "r" (unlock),
[config] "r" (cfg)
: "memory"
);
SREG = oldSREG;
set_sleep_mode(SLEEP_MODE_PWR_DOWN);
// Atomic sleep entry: after sei() the next instruction (sleep_cpu) still
// executes before any interrupt is serviced, so the wake-up cannot be missed.
// Loop again if something else woke the CPU.
while (!f_wdt) {
sleep_enable();
#if BOD_FUSE_ENABLED
sleep_bod_disable();
#endif
sei();
sleep_cpu();
sleep_disable();
cli();
}
sei();
}
ISR(WDT_vect)
// Wait n Morse units in power-down.
static void waitUnits(uint8_t n)
{
f_wdt = true;
while (n--) {
sleepWdt(UNIT_WDT_A);
#if UNIT_WDT_B != 0xFF
sleepWdt(UNIT_WDT_B);
#endif
}
}
// ===========================================================================
// 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)
// Returns true if it is dark.
// Charges the LED junction capacitance, then lets it bleed through the
// reverse-biased LED while the CPU sleeps. Checks the pin every ~16 ms.
// LOW within the window -> bright (discharged fast)
// still HIGH at the end -> dark
// In power-down the input buffer of the floating pin is disabled by hardware,
// so the slowly falling voltage causes no shoot-through current.
bool isDark(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.
// Let the N-side float (no pullup) and measure bleed-down time.
pinMode(LED_N, INPUT);
digitalWrite(LED_N, LOW); // disable internal pullup
digitalWrite(LED_N, LOW);
for (i = 0; i < 30000; i++) {
if (digitalRead(LED_N) == LOW) break;
bool dark = true;
for (uint8_t k = 0; k < darkSteps; k++) {
sleepWdt(0); // ~16 ms
if (digitalRead(LED_N) == LOW) {
dark = false; // bright: stop early
break;
}
}
// Clean up after sensing: both LED pins low, then caller releases them.
// Clean up after sensing: both LED pins low, caller releases them.
pinMode(LED_N, OUTPUT);
digitalWrite(LED_N, LOW);
pinMode(LED_P, OUTPUT);
digitalWrite(LED_P, LOW);
return i;
return dark;
}
// ===========================================================================
@@ -277,17 +392,17 @@ unsigned int sensDarkness(int LED_N, int LED_P)
void dit(int LED_P)
{
digitalWrite(LED_P, HIGH);
delay(unitLength);
waitUnits(1);
digitalWrite(LED_P, LOW);
delay(unitLength);
waitUnits(1);
}
void dah(int LED_P)
{
digitalWrite(LED_P, HIGH);
delay(unitLength * 3);
waitUnits(3);
digitalWrite(LED_P, LOW);
delay(unitLength);
waitUnits(1);
}
// ===========================================================================
@@ -354,24 +469,30 @@ void morse(int LED_N, int LED_P)
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; // .-...
// ----- Punctuation & Specials -----
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; // .-...
case '-': dah(LED_P); dit(LED_P); dit(LED_P); dit(LED_P); dit(LED_P); dah(LED_P); symbolSent = true; break; // -....-
case '/': 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);dah(LED_P); dah(LED_P); dit(LED_P); symbolSent = true; break; // .----.
case '+': 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); dit(LED_P); dit(LED_P); dah(LED_P); symbolSent = true; break; // -...-
case '_': dit(LED_P); dit(LED_P); dah(LED_P); dah(LED_P); dit(LED_P); dah(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);
waitUnits(4);
break;
// Unsupported chars are ignored without adding an artificial gap.
@@ -382,7 +503,7 @@ void morse(int LED_N, int LED_P)
// Inter-character gap: 3 units total.
// dit()/dah() already trail 1 unit OFF, so add the remaining 2 units.
if (symbolSent) {
delay(unitLength * 2);
waitUnits(2);
}
}