Skip to content

Power & sleep

Goal: a solar/battery client node spends most of its life with the CPU halted, waking only for its scheduled lane windows and for radio interrupts. The design borrows Meshtastic’s proven mechanics (their PowerFSM.cpp + sleep.cpp) and simplifies the state space because chIRpChat’s lane schedule gives us something Meshtastic doesn’t have: the node knows in advance when traffic can possibly arrive.

ACTIVE ──idle 15 s──► DOZE ──idle 5 min──► NAP ◄──► (scheduled wakes)
▲ ◄──any event── ▲ ◄──IRC/radio── │
└── USB/WiFi session active pins ACTIVE └─ battery < cutoff ──► HIBERNATE
  • ACTIVE — USB host attached / WiFi session open / TUI in use / traffic in flight. Full clocks. (USB-ECM means a laptop session pins ACTIVE; USB supplies power then anyway.)
  • DOZE — radio in RX on the current lane, CPU in light sleep between events. ESP32: esp_light_sleep_start() with GPIO wake on DIO1 (RX-done) and timer wake for the next lane-window edge — identical wiring to Meshtastic’s enableLoraInterrupt() (DIO1 high-level wake, GPIO 39 on the XIAO Wio). Wake latency ~1 ms; the SX1262 receives autonomously while the CPU sleeps, so nothing is missed.
  • NAP — between scheduled windows the radio itself sleeps too (SX126x sleep, ~1.6 µA, warm start). The node wakes by timer for: its router’s lane windows (uplink/downlink), beacon slots, and a slow housekeeping tick. Clock drift is re-disciplined by each heard beacon (BEACON.time); the wake margin widens automatically with measured drift (±(drift_ppm × seconds_asleep) guard band).
  • HIBERNATE — deep sleep, radio off, RTC timer + button wake only. Voltage thresholds and hysteresis copied from Meshtastic field practice.

Routers ship with sleep disabled (they are the schedule); solar routers may enable NAP between their own advertised windows — their published lane plan already tells every client when the router is deaf, so sleeping is free of protocol consequences. That’s the design’s quiet win: the lane plan is simultaneously the radio schedule and the power schedule.

No light-sleep equivalent needed: the nRF52’s idle current with SoftDevice and sd_app_evt_wait() is already ~µA-class between events; DIO1 is a GPIOTE wake. NAP/HIBERNATE map to System ON idle / System OFF. The core’s lrc::Clock/lrc::PowerGov interfaces hide the difference; the firmware HALs implement them per-platform (see firmware/).

  1. No busy-waiting anywhere — every wait is an event or a timer.
  2. The radio is never left in TX-capable limbo: RF switch state is re-asserted on every wake (HARDWARE.md §RF switch safety).
  3. Flash flushes (STORAGE.md) run only in ACTIVE/DOZE on the idle hook.
  4. Every state transition increments a telemetry counter; unexplained wake storms must be visible in /msg *lrc stats power (field debugging beats lab guessing).