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
+15
View File
@@ -194,6 +194,13 @@ protected:
uint32_t calcFloodTimeoutMillisFor(uint32_t pkt_airtime_millis) const override;
uint32_t calcDirectTimeoutMillisFor(uint32_t pkt_airtime_millis, uint8_t path_len) const override;
void onSendTimeout() override;
void onAckRecv(mesh::Packet* packet, uint32_t ack_crc) override; // APC: ACK SNR sample
// APC internals (see MyMesh.cpp): one reverse-link SNR sample, a lost-confirmation
// ramp-up, and tracking an originated flood so its echo (or absence) can be scored.
// The heard-echo sampling itself lives in filterRecvFloodPacket() (declared below).
void apcSampleSnr(float snr);
void apcOnFailure();
void apcTrackFloodSend(const mesh::Packet* pkt);
// DataStoreHost methods
bool onContactLoaded(const ContactInfo& contact) override { return addContact(contact); }
@@ -209,6 +216,7 @@ public:
void savePrefs() { _store->savePrefs(_prefs, sensors.node_lat, sensors.node_lon); }
void saveRTCTime() { _store->saveRTCTime(); }
DataStore* getDataStore() const { return _store; }
void applyApc(); // (re)initialise Adaptive Power Control from prefs
bool isAckPending(uint32_t expected_ack) const {
for (int i = 0; i < EXPECTED_ACK_TABLE_SIZE; i++)
@@ -276,6 +284,13 @@ private:
uint32_t _active_ble_pin;
bool _iter_started;
bool _cli_rescue;
int8_t _apc_cur_dbm; // APC current TX power (≤ tx_power_dbm ceiling) when tx_apc on
float _apc_margin_ewma; // APC smoothed reverse-link SNR margin above the SF demod floor
uint8_t _apc_fail_count; // APC consecutive lost-confirmation count (graduated ramp-up)
uint8_t _apc_flood_hash[MAX_HASH_SIZE]; // hash of the channel/flood send awaiting a repeater echo
uint16_t _apc_flood_len; // its payload length — cheap pre-filter before hashing
uint32_t _apc_flood_deadline; // echo-wait deadline for that send
bool _apc_flood_pending; // a tracked flood send is awaiting its echo
bool send_unscoped; // force un-scoped flood (instead of using send_scope)
char cli_command[80];
uint8_t app_target_ver;