/*
 =============================================================================
 FireFly Morse Throwie
 - a light controlled (LED as Sensor) morse blinker throwie with ATTiny85
 =============================================================================

 Project definitions, sources
 -----------------------------------------------------------------------------
 Version: 0.3 - ATTiny85, 1 MHz, BOD fuse disabled
 gitea  : https://gitea.togo-lab.io/tgohle/0001-FireFly
 Date   : 2026-06-14

Key changes Version 0.3:

- Removed unnecessary ADC enable/disable from loop()
- Disabled ADC and analog comparator in setup()
- Added INPUT_PULLUP for unused Arduino pins 0, 1, 2 will save power
- Added safer watchdog setup with interrupt protection to save run near brownout
- Added wdt_reset() before sleep for cleaner 8 s timing
- Added lowercase-to-uppercase handling
- Unsupported characters are now ignored without adding fake timing gaps

-----------------------------------------------------------------------------

Inspired by Karl Lunt's FireFly project:
   http://www.seanet.com/~karllunt/fireflyLED.html

 Morse code reference:
     'A', ".-"      'B', "-..."    'C', "-.-."
     'D', "-.."     'E', "."       'F', "..-."
     'G', "--."     'H', "...."    'I', ".."
     'J', ".---"    'K', "-.-"     'L', ".-.."
     'M', "--"      'N', "-."      'O', "---"
     'P', ".--."    'Q', "--.-"    'R', ".-."
     'S', "..."     'T', "-"       'U', "..-"
     'V', "...-"    'W', ".--"     'X', "-..-"
     'Y', "-.--"    'Z', "--.."

     '1', ".----"   '2', "..---"   '3', "...--"
     '4', "....-"   '5', "....."   '6', "-...."
     '7', "--..."   '8', "---.."   '9', "----."
     '0', "-----"

     '.', ".-.-.-"  ',', "--..--"  '?', "..--.."
     '!', "-.-.--"  ':', "---..."  ';', "-.-.-."
     '(', "-.--."   ')', "-.--.-"  '"', ".-..-."
     '@', ".--.-."  '&', ".-..."

-----------------------------------------------------------------------------

 Legal stuff / Copyright:
   License_-_CC_BY-NC_4.0
   https://creativecommons.org/licenses/by-nc/4.0/
-----------------------------------------------------------------------------
*/

#include <avr/sleep.h>
#include <avr/wdt.h>
#include <avr/pgmspace.h>
#include <avr/interrupt.h>
#include <string.h>

#ifndef cbi
#define cbi(sfr, bit) (_SFR_BYTE(sfr) &= ~_BV(bit))
#endif
#ifndef sbi
#define sbi(sfr, bit) (_SFR_BYTE(sfr) |= _BV(bit))
#endif

// ---------------------------------------------------------------------------
// Timing: unit length in ms.
// At 1 MHz, delay() is accurate when F_CPU=1000000L is set in boards.txt.
// 100 ms gives readable optical Morse; raise to 150 ms if readability is poor.
#define unitLength 100

// ---------------------------------------------------------------------------
// Sleep interval.
// Watchdog setup uses 8 s. For final use, change sleepCycles from 4 to 6
// for about 32s - 48s between sensing/blinking activations. Goal ~ 1 blink/min
const uint8_t sleepCycles = 6;

// ---------------------------------------------------------------------------
// LED pin definitions (N-side = cathode, P-side = anode for sensing/driving).
#define LED1_N_SIDE 3
#define LED1_P_SIDE 4

// ---------------------------------------------------------------------------
// ATTiny85 Arduino pins 0, 1, 2 are unused in actual design and physically open.
// Keep them in INPUT_PULLUP so they do not float and waste current.
#define UNUSED_PIN_0 0
#define UNUSED_PIN_1 1
#define UNUSED_PIN_2 2

// ---------------------------------------------------------------------------
// ATTiny85 has 512 bytes SRAM; this keeps the Morse text in flash.
// Only uppercase letters, digits, spaces, and the special chars in the switch
// below are emitted. Lowercase letters are converted to uppercase.
// Test setup: 20 x "0" because 0 = "-----" gives maximum LED ON time for test.
// shorter text will better, more text will cost more power.
const char morseText[] PROGMEM = "TGO 26";
// Ruler:                         0....0....1...1....2
// Ruler:                         0....5....0...5....0
// ---------------------------------------------------------------------------
// Darkness threshold.
// Higher value = triggers in brighter conditions.
// Best calibrated at actual dusk/dawn with the chosen LED type.
const unsigned int darknessThreshold = 17000;

// ---------------------------------------------------------------------------
// Watchdog interrupt flag — volatile because it is written in an ISR.
volatile bool f_wdt = true;

// ---------------------------------------------------------------------------
// Forward declarations.
void setup_watchdog(uint8_t ii);
void system_sleep();
void configureUnusedPins();
void releaseLedPins(int LED_N, int LED_P);
unsigned int sensDarkness(int LED_N, int LED_P);
void morse(int LED_N, int LED_P);
void dit(int LED_P);
void dah(int LED_P);

// ===========================================================================
// Setup
// ===========================================================================
void setup()
{
  // Disable unused peripherals immediately to save power.
  // PRTIM1, PRUSI: unused, safe to gate off.
  // PRADC: ADC is not used; LED sensing is done with digitalRead().
  // PRTIM0: must stay ON while awake because delay() depends on Timer0.
  PRR = (1 << PRTIM1) | (1 << PRUSI) | (1 << PRADC);

  // ADC off. It is not needed for digitalRead()-based LED sensing.
  cbi(ADCSRA, ADEN);

  // Analog comparator off. Saves a little sleep current.
  ACSR |= (1 << ACD);

  configureUnusedPins();
  releaseLedPins(LED1_N_SIDE, LED1_P_SIDE);

  setup_watchdog(9);      // 8-second watchdog interval.
}

// ===========================================================================
// Main loop
// ===========================================================================
void loop()
{
  if (f_wdt) {
    f_wdt = false;

    if (sensDarkness(LED1_N_SIDE, LED1_P_SIDE) > darknessThreshold) {
      morse(LED1_N_SIDE, LED1_P_SIDE);
    }

    // Return LED pins to high-Z before sleeping.
    // Internal pullups stay disabled for the LED pins.
    releaseLedPins(LED1_N_SIDE, LED1_P_SIDE);
  }

  // Test setup: 1 x 8 s sleep.
  // Final setup: set sleepCycles = 4 for about 32 s.
  for (uint8_t i = 0; i < sleepCycles; i++) {
    system_sleep();
  }
}

// ===========================================================================
// Pin helpers
// ===========================================================================
void configureUnusedPins()
{
  pinMode(UNUSED_PIN_0, INPUT_PULLUP);
  pinMode(UNUSED_PIN_1, INPUT_PULLUP);
  pinMode(UNUSED_PIN_2, INPUT_PULLUP);
}

void releaseLedPins(int LED_N, int LED_P)
{
  // digitalWrite LOW before INPUT disables the internal pullup on Arduino cores.
  digitalWrite(LED_N, LOW);
  digitalWrite(LED_P, LOW);
  pinMode(LED_N, INPUT);
  pinMode(LED_P, INPUT);
}

// ===========================================================================
// Sleep helpers
// ===========================================================================
void system_sleep()
{
  // Reset watchdog counter so each sleep cycle starts with a fresh interval.
  wdt_reset();

  set_sleep_mode(SLEEP_MODE_PWR_DOWN);

  // Atomic sleep entry pattern.
  // After sei(), AVR executes the next instruction before servicing interrupts,
  // so sleep_cpu() is not skipped by a just-pending interrupt.
  cli();
  sleep_enable();
  sei();
  sleep_cpu();

  // WDT interrupt wakes the CPU here.
  sleep_disable();
}

// Watchdog setup - ii selects timeout:
//   0=16ms  1=32ms  2=64ms  3=128ms  4=250ms  5=500ms
//   6=1s    7=2s    8=4s    9=8s
void setup_watchdog(uint8_t ii)
{
  if (ii > 9) ii = 9;

  uint8_t bb = ii & 7;
  if (ii > 7) bb |= (1 << WDP3);

  uint8_t oldSREG = SREG;
  cli();

  wdt_reset();
  MCUSR &= ~(1 << WDRF);

  // Timed sequence: enable configuration change, then set interrupt-only WDT.
  WDTCR |= (1 << WDCE) | (1 << WDE);
  WDTCR = bb | (1 << WDIE);

  SREG = oldSREG;
}

ISR(WDT_vect)
{
  f_wdt = true;
}

// ===========================================================================
// Light sensor
// ===========================================================================
// Returns a "darkness level": higher = darker.
// Charges the LED junction capacitance, then times how long it takes to bleed
// back through the reverse-biased LED until the input reads LOW.
//   ~30000 = pitch black,  ~0 = bright light
unsigned int sensDarkness(int LED_N, int LED_P)
{
  unsigned int i;

  // Charge the LED junction capacitance.
  pinMode(LED_N, OUTPUT);
  pinMode(LED_P, OUTPUT);
  digitalWrite(LED_N, HIGH);
  digitalWrite(LED_P, LOW);

  // Let the N-side float and measure bleed-down time.
  pinMode(LED_N, INPUT);
  digitalWrite(LED_N, LOW);   // disable internal pullup

  for (i = 0; i < 30000; i++) {
    if (digitalRead(LED_N) == LOW) break;
  }

  // Clean up after sensing: both LED pins low, then caller releases them.
  pinMode(LED_N, OUTPUT);
  digitalWrite(LED_N, LOW);
  pinMode(LED_P, OUTPUT);
  digitalWrite(LED_P, LOW);

  return i;
}

// ===========================================================================
// Morse helpers
// ===========================================================================
void dit(int LED_P)
{
  digitalWrite(LED_P, HIGH);
  delay(unitLength);
  digitalWrite(LED_P, LOW);
  delay(unitLength);
}

void dah(int LED_P)
{
  digitalWrite(LED_P, HIGH);
  delay(unitLength * 3);
  digitalWrite(LED_P, LOW);
  delay(unitLength);
}

// ===========================================================================
// Morse sender
// ===========================================================================
void morse(int LED_N, int LED_P)
{
  pinMode(LED_N, OUTPUT);
  pinMode(LED_P, OUTPUT);
  digitalWrite(LED_N, LOW);
  digitalWrite(LED_P, LOW);

  const size_t len = strlen_P(morseText);

  for (size_t i = 0; i < len; i++) {
    char c = (char)pgm_read_byte(&morseText[i]);

    // Allow lowercase source text without silently breaking timing.
    if (c >= 'a' && c <= 'z') {
      c = c - 'a' + 'A';
    }

    bool symbolSent = false;

    switch (c)
    {
    // ----- Letters -----
    case 'A': dit(LED_P); dah(LED_P); symbolSent = true; break;                                      // .-
    case 'B': dah(LED_P); dit(LED_P); dit(LED_P); dit(LED_P); symbolSent = true; break;              // -...
    case 'C': dah(LED_P); dit(LED_P); dah(LED_P); dit(LED_P); symbolSent = true; break;              // -.-.
    case 'D': dah(LED_P); dit(LED_P); dit(LED_P); symbolSent = true; break;                          // -..
    case 'E': dit(LED_P); symbolSent = true; break;                                                  // .
    case 'F': dit(LED_P); dit(LED_P); dah(LED_P); dit(LED_P); symbolSent = true; break;              // ..-.
    case 'G': dah(LED_P); dah(LED_P); dit(LED_P); symbolSent = true; break;                          // --.
    case 'H': dit(LED_P); dit(LED_P); dit(LED_P); dit(LED_P); symbolSent = true; break;              // ....
    case 'I': dit(LED_P); dit(LED_P); symbolSent = true; break;                                      // ..
    case 'J': dit(LED_P); dah(LED_P); dah(LED_P); dah(LED_P); symbolSent = true; break;              // .---
    case 'K': dah(LED_P); dit(LED_P); dah(LED_P); symbolSent = true; break;                          // -.-
    case 'L': dit(LED_P); dah(LED_P); dit(LED_P); dit(LED_P); symbolSent = true; break;              // .-..
    case 'M': dah(LED_P); dah(LED_P); symbolSent = true; break;                                      // --
    case 'N': dah(LED_P); dit(LED_P); symbolSent = true; break;                                      // -.
    case 'O': dah(LED_P); dah(LED_P); dah(LED_P); symbolSent = true; break;                          // ---
    case 'P': dit(LED_P); dah(LED_P); dah(LED_P); dit(LED_P); symbolSent = true; break;              // .--.
    case 'Q': dah(LED_P); dah(LED_P); dit(LED_P); dah(LED_P); symbolSent = true; break;              // --.-
    case 'R': dit(LED_P); dah(LED_P); dit(LED_P); symbolSent = true; break;                          // .-.
    case 'S': dit(LED_P); dit(LED_P); dit(LED_P); symbolSent = true; break;                          // ...
    case 'T': dah(LED_P); symbolSent = true; break;                                                  // -
    case 'U': dit(LED_P); dit(LED_P); dah(LED_P); symbolSent = true; break;                          // ..-
    case 'V': dit(LED_P); dit(LED_P); dit(LED_P); dah(LED_P); symbolSent = true; break;              // ...-
    case 'W': dit(LED_P); dah(LED_P); dah(LED_P); symbolSent = true; break;                          // .--
    case 'X': dah(LED_P); dit(LED_P); dit(LED_P); dah(LED_P); symbolSent = true; break;              // -..-
    case 'Y': dah(LED_P); dit(LED_P); dah(LED_P); dah(LED_P); symbolSent = true; break;              // -.--
    case 'Z': dah(LED_P); dah(LED_P); dit(LED_P); dit(LED_P); symbolSent = true; break;              // --..

    // ----- Digits -----
    case '1': dit(LED_P); dah(LED_P); dah(LED_P); dah(LED_P); dah(LED_P); symbolSent = true; break;  // .----
    case '2': dit(LED_P); dit(LED_P); dah(LED_P); dah(LED_P); dah(LED_P); symbolSent = true; break;  // ..---
    case '3': dit(LED_P); dit(LED_P); dit(LED_P); dah(LED_P); dah(LED_P); symbolSent = true; break;  // ...--
    case '4': dit(LED_P); dit(LED_P); dit(LED_P); dit(LED_P); dah(LED_P); symbolSent = true; break;  // ....-
    case '5': dit(LED_P); dit(LED_P); dit(LED_P); dit(LED_P); dit(LED_P); symbolSent = true; break;  // .....
    case '6': dah(LED_P); dit(LED_P); dit(LED_P); dit(LED_P); dit(LED_P); symbolSent = true; break;  // -....
    case '7': dah(LED_P); dah(LED_P); dit(LED_P); dit(LED_P); dit(LED_P); symbolSent = true; break;  // --...
    case '8': dah(LED_P); dah(LED_P); dah(LED_P); dit(LED_P); dit(LED_P); symbolSent = true; break;  // ---..
    case '9': dah(LED_P); dah(LED_P); dah(LED_P); dah(LED_P); dit(LED_P); symbolSent = true; break;  // ----.
    case '0': dah(LED_P); dah(LED_P); dah(LED_P); dah(LED_P); dah(LED_P); symbolSent = true; break;  // -----

    // ----- Punctuation -----
    case '.': dit(LED_P); dah(LED_P); dit(LED_P); dah(LED_P); dit(LED_P); dah(LED_P); symbolSent = true; break;  // .-.-.-
    case ',': dah(LED_P); dah(LED_P); dit(LED_P); dit(LED_P); dah(LED_P); dah(LED_P); symbolSent = true; break;  // --..--
    case '?': dit(LED_P); dit(LED_P); dah(LED_P); dah(LED_P); dit(LED_P); dit(LED_P); symbolSent = true; break;  // ..--..
    case '!': dah(LED_P); dit(LED_P); dah(LED_P); dit(LED_P); dah(LED_P); dah(LED_P); symbolSent = true; break;  // -.-.--
    case ':': dah(LED_P); dah(LED_P); dah(LED_P); dit(LED_P); dit(LED_P); dit(LED_P); symbolSent = true; break;  // ---...
    case ';': dah(LED_P); dit(LED_P); dah(LED_P); dit(LED_P); dah(LED_P); dit(LED_P); symbolSent = true; break;  // -.-.-.
    case '(': dah(LED_P); dit(LED_P); dah(LED_P); dah(LED_P); dit(LED_P); symbolSent = true; break;              // -.--.
    case ')': dah(LED_P); dit(LED_P); dah(LED_P); dah(LED_P); dit(LED_P); dah(LED_P); symbolSent = true; break;  // -.--.-
    case '"': dit(LED_P); dah(LED_P); dit(LED_P); dit(LED_P); dah(LED_P); dit(LED_P); symbolSent = true; break;  // .-..-.
    case '@': dit(LED_P); dah(LED_P); dah(LED_P); dit(LED_P); dah(LED_P); dit(LED_P); symbolSent = true; break;  // .--.-.
    case '&': dit(LED_P); dah(LED_P); dit(LED_P); dit(LED_P); dit(LED_P); symbolSent = true; break;              // .-...

    // Space handling:
    // Previous symbol already ended with 1 unit OFF and then received the
    // normal 2 unit character gap. Add 4 units here -> total word gap 7 units.
    case ' ':
      delay(unitLength * 4);
      break;

    // Unsupported chars are ignored without adding an artificial gap.
    default:
      break;
    }

    // Inter-character gap: 3 units total.
    // dit()/dah() already trail 1 unit OFF, so add the remaining 2 units.
    if (symbolSent) {
      delay(unitLength * 2);
    }
  }

  digitalWrite(LED_P, LOW);
  digitalWrite(LED_N, LOW);
}