// Mailbox mail-presence sensor - ATtiny402 firmware (megaTinyCore). 2026-10-05.
// Board: the MAILBOX SENSOR v1 PCB (files in ../pcb/).
//   PA6 LED_DRV  -> BC548 -> both IR LEDs (on = HIGH)
//   PA3 PT_PWR   -> powers both phototransistor loads only while measuring
//   PA1 PT1_SIG  (front beam)   PA2 PT2_SIG (rear beam)   - ADC, light pulls them DOWN
//   PA7 ZB_GATE  -> 2N7000 -> Zigbee contact input (J4 pin 3). HIGH = MOSFET on = "contact closed"
//   PA0 UPDI
// Mail present = PA7 HIGH (contact CLOSED). Empty = PA7 LOW (contact OPEN). HA inverts it.
//
// Power-up: the box must be EMPTY - the first 8 good readings set the "empty" baseline.
//
// BENCH build (#define BENCH 1): reads every second and prints the raw numbers as 4800-baud serial
// OUT OF J4 pin 3 (the Zigbee connector), bit-banged through the 2N7000. Bench wiring: Zigbee board
// UNPLUGGED, 10k from J4.3 to J4.1 (3V3), CH340 RX -> J4.3, CH340 GND -> J4.2.
//   line:  d1 l1 D1 | d2 l2 D2 | b1 b2 | state     (d = LEDs off, l = LEDs on, D = d - l, b = baseline)
#ifndef BENCH
#define BENCH 0
#endif
// SINGLE_BEAM (2026-10-08): only beam 1 (LED1 on J2.1-2 -> sensor 1 on J3.1-2, the CENTRE beam) is used.
// LED2 died; beam 1's readings are fed to the detector as BOTH beams, so detect.h and its tests are untouched.
// Set to 0 to go back to two beams once LED2 is replaced.
#ifndef SINGLE_BEAM
#define SINGLE_BEAM 1
#endif
#include <avr/sleep.h>
#include <avr/interrupt.h>
#include <util/delay.h>
#include "detect.h"

#define P_LED   PIN_PA6
#define P_PTPWR PIN_PA3
#define P_PT1   PIN_PA1
#define P_PT2   PIN_PA2
#define P_ZB    PIN_PA7

static Detector det;

static uint16_t adc_avg(uint8_t pin) {             // 4-sample average
  uint16_t s = 0;
  for (uint8_t i = 0; i < 4; i++) s += analogRead(pin);
  return s >> 2;
}

static void measure(uint16_t *d1, uint16_t *l1, uint16_t *d2, uint16_t *l2) {
  ADC0.CTRLA |= ADC_ENABLE_bm;
  digitalWrite(P_PTPWR, HIGH);
  delay(2);                                        // let the phototransistor loads settle
  *d1 = adc_avg(P_PT1); *d2 = adc_avg(P_PT2);       // ambient only
  digitalWrite(P_LED, HIGH);
  delay(1);                                        // phototransistor rise time with a 22k load
  *l1 = adc_avg(P_PT1); *l2 = adc_avg(P_PT2);       // ambient + LED
  digitalWrite(P_LED, LOW);
  digitalWrite(P_PTPWR, LOW);
  ADC0.CTRLA &= ~ADC_ENABLE_bm;
}

#if BENCH
// 4800 baud, inverted through the 2N7000: gate HIGH pulls the line LOW (a 0 bit).
static void tx_byte(uint8_t c) {
  digitalWrite(P_ZB, HIGH); _delay_us(208);                     // start bit
  for (uint8_t i = 0; i < 8; i++) { digitalWrite(P_ZB, (c & 1) ? LOW : HIGH); _delay_us(208); c >>= 1; }
  digitalWrite(P_ZB, LOW); _delay_us(208);                      // stop bit (line released = high)
}
static void tx_str(const char *s) { while (*s) tx_byte(*s++); }
static void tx_u(uint16_t v) { char b[6]; uint8_t i = 5; b[5] = 0; do { b[--i] = '0' + v % 10; v /= 10; } while (v); tx_str(&b[i]); tx_byte(' '); }
static void tx_i(int16_t v) { if (v < 0) { tx_byte('-'); v = -v; } tx_u((uint16_t)v); }
#endif

ISR(RTC_PIT_vect) { RTC.PITINTFLAGS = RTC_PI_bm; }

#if !BENCH
static void sleep_32s() {
  set_sleep_mode(SLEEP_MODE_PWR_DOWN);
  sleep_enable();
  sleep_cpu();                                      // RTC PIT wakes us
  sleep_disable();
}
#endif

void setup() {
  pinMode(P_LED, OUTPUT);   digitalWrite(P_LED, LOW);
  pinMode(P_PTPWR, OUTPUT); digitalWrite(P_PTPWR, LOW);
  pinMode(P_ZB, OUTPUT);    digitalWrite(P_ZB, LOW);
  PORTA.PIN1CTRL = PORT_ISC_INPUT_DISABLE_gc;       // analog pins: no digital input buffer (saves power)
  PORTA.PIN2CTRL = PORT_ISC_INPUT_DISABLE_gc;
  det_init(&det);
#if !BENCH
  RTC.CLKSEL = RTC_CLKSEL_INT1K_gc;                 // 1.024 kHz from the ultra-low-power oscillator
  while (RTC.PITSTATUS > 0) {}
  RTC.PITINTCTRL = RTC_PI_bm;
  RTC.PITCTRLA = RTC_PERIOD_CYC32768_gc | RTC_PITEN_bm;   // 32768 / 1024 Hz = 32 s
  sei();
#endif
}

void loop() {
  uint16_t d1, l1, d2, l2;
  measure(&d1, &l1, &d2, &l2);
#if SINGLE_BEAM
  d2 = d1; l2 = l1;                                 // beam 2 ignored (dead LED2)
#endif
  uint8_t state;
  if (!det_calibrate_done(&det)) {
    det_calibrate_sample(&det, d1, l1, d2, l2);
    state = 9;                                      // calibrating (output stays "empty")
  } else {
    state = det_update(&det, d1, l1, d2, l2);
    digitalWrite(P_ZB, state == MAIL_YES ? HIGH : LOW);
  }
#if BENCH
  tx_u(d1); tx_u(l1); tx_i((int16_t)d1 - (int16_t)l1); tx_str("| ");
  tx_u(d2); tx_u(l2); tx_i((int16_t)d2 - (int16_t)l2); tx_str("| ");
  tx_u(det.base[0]); tx_u(det.base[1]); tx_str("| ");
  tx_str(state == 9 ? "CAL" : state == MAIL_YES ? "MAIL" : "EMPTY");
  if (det.last_flooded) tx_str(" SUN");
  tx_str("\r\n");
  delay(1000);
#else
  sleep_32s();
#endif
}
