feat(power): battery saving — hardware duty-cycle RX + adaptive TX power

Two independent, default-off toggles under Settings › Radio.

Pwr save: hardware RX duty-cycle (SX126x SetRxDutyCycle via
startReceiveDutyCycleAuto). The chip cycles RX↔sleep and wakes on a preamble —
no MCU state machine; recvRaw reads the packet exactly as in continuous RX.
Falls back to continuous RX on non-SX126x. (Replaces an earlier software-CAD
state machine that fought the hardware: polling a warm-sleeping chip gave a
phantom-busy channel that stalled TX ~4 s and dropped ACKs in the scan gaps.)

Auto pwr: Adaptive Power Control. tx_power_dbm becomes a ceiling; actual TX
power tracks the reverse-link SNR margin (measured above the per-SF demod floor,
EWMA-smoothed, proportional step with a deadband). Feedback comes from direct /
room-server ACKs and, for channels (no ACK), from hearing a repeater rebroadcast
our own flood; a lost confirmation ramps power back up so channel sends can't get
stranded below what the repeaters can hear.

Prefs schema 0xC0DE0009 (rx_powersave, tx_apc). Radio page / name bar show the
live TX power; noise floor reads n/a while duty-cycling.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
Jakub
2026-06-11 08:15:42 +02:00
co-authored by Claude Opus 4.8
parent baaac3179c
commit 5b58049139
13 changed files with 346 additions and 42 deletions
+11 -1
View File
@@ -120,12 +120,22 @@ struct NodePrefs { // persisted to file
// Trail Summary readout: 0=speed (km/h or mph), 1=pace (min/km or min/mi).
// The km-vs-mi choice now comes from units_imperial, so this is just the mode.
uint8_t trail_show_pace;
// Hardware duty-cycle receive (battery saver): 0=continuous RX (default), 1=on.
// The SX126x cycles RX↔sleep on its own and wakes on a preamble — cuts average
// RX current at the cost of a little receive latency. See RadioLibWrapper
// power-save (startReceiveDutyCycleAuto).
uint8_t rx_powersave;
// Adaptive Power Control: 0=off (fixed tx_power_dbm, default), 1=on. When on,
// tx_power_dbm is treated as a ceiling and the radio's actual power is lowered
// at runtime on strong links (good ACK SNR), saving TX energy. Never persisted
// below the ceiling, so disabling restores the user's configured power.
uint8_t tx_apc;
// Tail sentinel written at the end of /new_prefs. Bump the low byte when
// adding/removing/reordering fields in DataStore::savePrefs/loadPrefsInt so
// older saves are detected on load and skipped (zero-init defaults kept).
// High 24 bits identify the file format; low byte is the schema revision.
static const uint32_t SCHEMA_SENTINEL = 0xC0DE0008;
static const uint32_t SCHEMA_SENTINEL = 0xC0DE0009;
// Bit-index for each home page. Used by page_order (entries store bit+1) and
// by home_pages_mask. Single source of truth — both HomeScreen::pageBit/bitToPage