3 Commits

2 changed files with 628 additions and 470 deletions

View File

@ -1,103 +1,35 @@
/*
=============================================================================
FireFly Morse Throwie
- a light controled (LED as Sensor) morseblinker throwie with ATTiny45/85
- a light controlled (LED as Sensor) morse blinker throwie with ATTiny85
=============================================================================
Project definitions, sources
-----------------------------------------------------------------------------
Version: 0.1 - Attiny85 Version
Date : 25.05.2026
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:
Inspired by Karl Lunt's FireFly project I wrote some code to make this
Throwie lasting longer, blinking only at low light levels and morse also
some text.
- 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
How it works (only the Morsethrowy part):
LED detects light level, using bleeding out time of LED pn-capacitor.
If dark enough blink a text in morsecode (ATTiny45 = 22 Chars, text will
be defined in line 191/192 of this file)
To save energy go to powersave mode after blinking or light level is
over threshold. Tests this every 8s (maximum time for watchdog timer
possible for ATTiny). Repeat this endless
The wiring, it's very simple, see also HW part of the Project.
+---+ +------\/------+
| | ATTINY45 /85 |
> | -+1=PB5 VCC=8+-> to SuperCap / DC-DC 3V/5V
xxx Ohm < | | |
> +------+2=PB3 PB2=7+-
| | |
----- +------+3=PB4 PB1=6+-
LED / \ | | |
----- | +--+4=GND PB0=5+-
| | | | |
+---+ | +--------------+
|
+---------------------> to battery GND
You can use other pins if necessary, theoretical up to 3 LEDs, as example I
tried out to connect another one at PB2&PB1. working fine. Basic wiring:
+ LED_N_Side), wired to a digital pin, NOT Vcc
|
<
> 100 - 460 ohm resistor depending Voltage
<
|
|
-----
/ \ LED, maybe a 5mm, clear plastic is good
-----
|
|
+ LED_P_Side), wired to a digital pin, NOT Gnd
-----------------------------------------------------------------------------
Im just a newbe in ATTiny / Arduino programming, so many thanks to:
Programming ATTiny with Arduino IDE:
http://highlowtech.org/?p=1695
LED Lightsensor, Arduino Playground:
http://playground.arduino.cc/Learning/LEDSensor
Powersaving Mode for Attiny45/85:
A good description by Martin Nawrath nawrath@khm.de and the folks at
http://www.insidegadgets.com/2011/02/05/reduce-attiny-power-consumption-by-sleeping-with-the-watchdog-timer/
With this description I got ~5uA during sleep mode.
in Morse-Mode with LED OFF: 1.7mA
LED ON: 6.9mA
--> I hope to run this throwies ~40days with one CR2032.
Morsecode
To translate a text into morsecode, there are several ways. You can use
this fine online translator:
http://morsecode.scphillips.com/jtranslator.html
There is also a great morse coder / encoder from Matthias Esterl aka madc.
Works fine for Arduino, but not for ATTiny because of RAM limitations. But
maybe this is helpful if your project works with an Arduino.
https://gist.github.com/madc/4474559
For this project I use a simple case structur. Looks arkward, but the
ATTiny45 has only 256byte RAM but 4kFlash. A data structure is better programming
but needs a lot of RAM we not have. Therefore this ugly case structure.
This will waste the flash memory but we have enough and saving RAM for
our payload - the ASCII-string with the morsetext.
BTW - Morsecode itself:
Morse code reference:
'A', ".-" 'B', "-..." 'C', "-.-."
'D', "-.." 'E', "." 'F', "..-."
'G', "--." 'H', "...." 'I', ".."
'J', ".---" 'K', ".-.-" 'L', ".-.."
'J', ".---" 'K', "-.-" 'L', ".-.."
'M', "--" 'N', "-." 'O', "---"
'P', ".--." 'Q', "--.-" 'R', ".-."
'S', "..." 'T', "-" 'U', "..-"
@ -114,25 +46,20 @@
'(', "-.--." ')', "-.--.-" '"', ".-..-."
'@', ".--.-." '&', ".-..."
-----------------------------------------------------------------------------
-----------------------------------------------------------------------------
Legal stuff / Copyright:
Creative Commons Attribution ShareAlike 3.0.:
http://creativecommons.org/licenses/by-sa/3.0/legalcode
-----------------------------------------------------------------------------
License_-_CC_BY-NC_4.0
https://creativecommons.org/licenses/by-nc/4.0/
-----------------------------------------------------------------------------
*/
// ===========================================================================
// Ok folks, let's start
// ===========================================================================
// for sleep Mode / Powersave we need some additional stuff. Its alredy there,
// if you installed the Attiny extension for Arduino IDE, no additional
// installing needed.
#include <avr/sleep.h>
#include <avr/wdt.h>
#include <avr/pgmspace.h>
#include <avr/interrupt.h>
#include <string.h>
// ===========================================================================
// Some definitions for powersave depending of ATTiny type.
#ifndef cbi
#define cbi(sfr, bit) (_SFR_BYTE(sfr) &= ~_BV(bit))
#endif
@ -140,405 +67,325 @@
#define sbi(sfr, bit) (_SFR_BYTE(sfr) |= _BV(bit))
#endif
// ===========================================================================
// for ATTiny85
// Unit length < 150 will hard to be read for optical morse code
// > 300 are too slow in my feeling for longer text
//
// for ATTiny85 at 8MHz (for 1MHz see ATTiny45)
// Unit length < 5 will hard to be read for optical morse code
// > 10 are too slow in my feeling for longer text
//
//
// ---------------------------------------------------------------------------
// 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
// ---------------------------------------------------------------------------
// LED Pin definitions,
// theoretical you can use up to 3 LED. For example you can use it for a
// landmark beacon. So if you point this LEDs to different directions you can
// detect the position in dependence of your location to the bacon.
// 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
// ===========================================================================
// Variables
// ===========================================================================
// ###########################################################################
// # and now the text, but be aware, only 256Byte RAM for Attiny45 #
// # so there are only 22 Chars left for your message. #
// # BTW with an Attiny85 you will have additional 256byte #
// # => more than for twitter :-) #
// # => for FireFly with SuperCap: less ist better, charge last longer #
// ---------------------------------------------------------------------------
// 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
String morseText =
"TOGO LAB";
// # "....5....1....5....2.."; just a ruler, remember 22 Chars for ATTiny #
// IT IS USELESS
// HAIL GLOW CLOUD
// ###########################################################################
// ---------------------------------------------------------------------------
// 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;
// Define light trigger threshold. best way to set it on dusk / dawn level
// diffuse red ones are less sensitive than clear green ones...
int darknessThreshold = 17000;
// ---------------------------------------------------------------------------
// Watchdog interrupt flag — volatile because it is written in an ISR.
volatile bool f_wdt = true;
// Interrupt Flag, should be volatile, means: read from RAM, not register
// because registers are used for interrupt handling, it have to be volatile.
volatile boolean f_wdt = 1;
// ---------------------------------------------------------------------------
// 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 watchdog
// 0=16ms, 1=32ms,2=64ms,3=128ms,4=250ms,5=500ms
// 6=1 sec,7=2 sec, 8=4 sec, 9= 8sec
// Setup
// ===========================================================================
void setup()
{
setup_watchdog(9);
// 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
// Main loop
// ===========================================================================
void loop()
{
if (f_wdt) {
f_wdt = false;
void loop(){
if (f_wdt==1) { // wait for timed out watchdog
// flag is set when a watchdog timeout occurs
f_wdt=0; // reset flag
// if it's dark enough [>darknesThreshold] morse
if (sensDarkness(LED1_N_SIDE, LED1_P_SIDE) > darknessThreshold){
// looks like it's dark, so do your job
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);
}
// set all used port to intput to save power
pinMode(LED1_N_SIDE,INPUT);
pinMode(LED1_P_SIDE,INPUT);
system_sleep();
f_wdt=0; // 2nd time sleep
// 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();
}
}
// ===========================================================================
// Subroutines for Sleeping
// Pin helpers
// ===========================================================================
// set system into the sleep state
// system wakes up when wtchdog is timed out
void system_sleep() {
cbi(ADCSRA,ADEN); // switch Analog to
// Digitalconverter OFF
void configureUnusedPins()
{
pinMode(UNUSED_PIN_0, INPUT_PULLUP);
pinMode(UNUSED_PIN_1, INPUT_PULLUP);
pinMode(UNUSED_PIN_2, INPUT_PULLUP);
}
set_sleep_mode(SLEEP_MODE_PWR_DOWN); // sleep mode is set here
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();
sleep_mode(); // System sleeps here
sleep_disable(); // System continues execution here when
// watchdog timed out
sbi(ADCSRA,ADEN); // switch Analog to Digitalconverter ON
// WDT interrupt wakes the CPU here.
sleep_disable();
}
// ----------------------------------------------------------------------------
// Setup for watchdog. Parameter ii for sleeping time:
// 0 = 16ms, 1 = 32ms, 2 = 64ms,
// 3 = 128ms, 4 = 250ms, 5 = 500ms
// 6 = 1 sec, 7 = 2 sec, 8 = 4 sec,
// 9 = 8 sec
void setup_watchdog(int ii) {
// 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;
byte bb;
int ww;
if (ii > 9 ) ii=9;
bb=ii & 7;
if (ii > 7) bb|= (1<<5);
bb|= (1<<WDCE);
ww=bb;
uint8_t bb = ii & 7;
if (ii > 7) bb |= (1 << WDP3);
MCUSR &= ~(1<<WDRF);
uint8_t oldSREG = SREG;
cli();
WDTCR |= (1<<WDCE) | (1<<WDE); // start timed sequence
wdt_reset();
MCUSR &= ~(1 << WDRF);
WDTCR = bb; // set new watchdog timeout value
WDTCR |= _BV(WDIE);
}
// -----------------------------------------------------------------------------
// Watchdog Interrupt Service / is executed when watchdog timed out
ISR(WDT_vect) {
f_wdt=1; // set global flag
// Timed sequence: enable configuration change, then set interrupt-only WDT.
WDTCR |= (1 << WDCE) | (1 << WDE);
WDTCR = bb | (1 << WDIE);
SREG = oldSREG;
}
// =============================================================================
// Subroutines for light sensor
// =============================================================================
// Function sensDarkness
// Usage: sensDarkness(Pin-No. N-Side of LED, Pin-No. P-Side of LED):
// will result "darkness-level" - the higher the darker:
// 30000 = pitch black
// 0 = sunshine
int sensDarkness(int LED_N, int LED_P){
ISR(WDT_vect)
{
f_wdt = true;
}
unsigned int i; //Parameter bleed-out LED capacitor
// ===========================================================================
// 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 capacitor of LED
pinMode(LED_N,OUTPUT);
pinMode(LED_P,OUTPUT);
digitalWrite(LED_N,HIGH);
digitalWrite(LED_P,LOW);
// Charge the LED junction capacitance.
pinMode(LED_N, OUTPUT);
pinMode(LED_P, OUTPUT);
digitalWrite(LED_N, HIGH);
digitalWrite(LED_P, LOW);
// Isolate the N end of the diode and turn off internal pull-up resistor
pinMode(LED_N,INPUT);
digitalWrite(LED_N,LOW);
// Let the N-side float and measure bleed-down time.
pinMode(LED_N, INPUT);
digitalWrite(LED_N, LOW); // disable internal pullup
// Count how long it takes the diode to bleed back down to a logic zero
for ( i = 0; i < 30000; i++) {
if ( digitalRead(LED_N)==0) break;
for (i = 0; i < 30000; i++) {
if (digitalRead(LED_N) == LOW) break;
}
// thats it, return result
// 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;
}
// =============================================================================
// Subroutines for morse text
// =============================================================================
// -----------------------------------------------------------------------------
// Function dit = ".", a dit is as long as a unitLength, used in morse()
//
void dit(int LED_P){
digitalWrite( LED_P, HIGH ); delay( unitLength );
digitalWrite( LED_P, LOW ); delay( unitLength );
// ===========================================================================
// Morse helpers
// ===========================================================================
void dit(int LED_P)
{
digitalWrite(LED_P, HIGH);
delay(unitLength);
digitalWrite(LED_P, LOW);
delay(unitLength);
}
// -----------------------------------------------------------------------------
// Function dah = "-", a dah is as long as a 3*unitLength
//
void dah(int LED_P){
digitalWrite( LED_P, HIGH ); delay( unitLength*3 );
digitalWrite( LED_P, LOW ); delay( unitLength );
void dah(int LED_P)
{
digitalWrite(LED_P, HIGH);
delay(unitLength * 3);
digitalWrite(LED_P, LOW);
delay(unitLength);
}
// -----------------------------------------------------------------------------
// Function morse:
// hand over text and LED Pins
// because you want to use more than one LED hand over also LED Pins
// ===========================================================================
// 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);
void morse(int LED_N, int LED_P){
const size_t len = strlen_P(morseText);
// Because I have more Flash than RAM I decided to use a case structure.
// Its not so pretty, but I can use the RAM to hold my morseText.
//
// set N-Side of LED Low, so you can set P-side high to let them flash
for (size_t i = 0; i < len; i++) {
char c = (char)pgm_read_byte(&morseText[i]);
pinMode( LED_N, OUTPUT);
pinMode( LED_P, OUTPUT);
digitalWrite( LED_N, LOW);
// send it to defined LED
for(int i=0; i<=morseText.length(); i++)
{
switch( morseText[i] )
{
// Chars A-Z
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': // .-.-
dit(LED_P); 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;
// Numbers
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;
// Special Signs
case ' ': // Gap
delay( unitLength*5 );
break;
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);
// Allow lowercase source text without silently breaking timing.
if (c >= 'a' && c <= 'z') {
c = c - 'a' + 'A';
}
// wait at end of sign
delay( unitLength*3 );
}
}
// -----------------------------------------------------------------------------
// That's all, folks :-)
// -----------------------------------------------------------------------------
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);
}

View File

@ -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 <avr/sleep.h>
#include <avr/wdt.h>
#include <avr/pgmspace.h> // 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);
}
}
}