feat(power): CAD-windowed receive (battery saver) — phase 1

Add an optional low-power RX mode: instead of listening continuously, the
radio periodically runs a Channel Activity Detection scan and sits in standby
between scans; on detected activity it drops into a full receive to catch the
packet. Cuts average RX current several-fold (the dominant draw on this node).

- RadioLibWrapper: CAD state machine (scan → sleep window → rx) driven from
  loop(), which runs before recvRaw each cycle. `state` stays STATE_RX through
  all phases so the dispatcher's not-in-RX watchdog never trips. Re-arm routes
  through armRecv() in both startRecv() and recvRaw(); toggling on/off switches
  mode safely at the next idle. Falls back to continuous RX if CAD is
  unsupported. setPowerSaving()/getPowerSaving() control it.
- Software CSMA is already disabled on this firmware (interference threshold 0),
  so duty-cycling doesn't affect listen-before-talk; airtime budget still gates
  TX, and isReceivingPacket() still defers TX while a packet is mid-air.
- Companion: new rx_powersave pref (schema 0xC0DE0008), Settings › Radio ›
  "Pwr save" toggle, applied at boot (MyMesh) and on change (UITask).

CAD window = 45 ms (< the 16-symbol preamble at SF8/BW62.5) and CAD params use
RadioLib defaults — both will want on-hardware tuning + a before/after current
measurement to confirm reception is intact.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
Jakub
2026-06-10 17:21:24 +02:00
co-authored by Claude Opus 4.8
parent baaac3179c
commit 90ca21432c
8 changed files with 141 additions and 9 deletions
@@ -54,6 +54,7 @@ class SettingsScreen : public UIScreen {
// Radio section
SECTION_RADIO,
TX_POWER,
POWER_SAVE,
// System section
SECTION_SYSTEM,
TIMEZONE,
@@ -479,6 +480,10 @@ class SettingsScreen : public UIScreen {
snprintf(buf, sizeof(buf),"%ddBm", p ? p->tx_power_dbm : 0);
display.setCursor(display.valCol(), y);
display.print(buf);
} else if (item == POWER_SAVE) {
display.print("Pwr save");
display.setCursor(display.valCol(), y);
display.print((p && p->rx_powersave) ? "ON" : "OFF");
#if AUTO_OFF_MILLIS > 0
} else if (item == AUTO_OFF) {
display.print("AutoOff");
@@ -736,6 +741,12 @@ public:
if (right && p->tx_power_dbm < 22) { p->tx_power_dbm++; _task->applyTxPower(); _dirty = true; return true; }
if (left && p->tx_power_dbm > 2) { p->tx_power_dbm--; _task->applyTxPower(); _dirty = true; return true; }
}
if (_selected == POWER_SAVE && p && (left || right || enter)) {
p->rx_powersave ^= 1;
_task->applyPowerSave();
_dirty = true;
return true;
}
#if AUTO_OFF_MILLIS > 0
if (_selected == AUTO_OFF && p) {
int idx = autoOffIndex();
@@ -2066,6 +2066,11 @@ void UITask::applyTxPower() {
radio_driver.setTxPower(_node_prefs->tx_power_dbm);
}
void UITask::applyPowerSave() {
if (_node_prefs == NULL) return;
radio_driver.setPowerSaving(_node_prefs->rx_powersave);
}
void UITask::applyBrightness() {
if (_display != NULL && _node_prefs != NULL) {
_display->setBrightness(_node_prefs->display_brightness);
+1
View File
@@ -249,6 +249,7 @@ public:
void setBuzzerVolumeLevel(uint8_t level);
uint8_t getBuzzerVolume() const { return _node_prefs ? _node_prefs->buzzer_volume : 4; }
void applyTxPower();
void applyPowerSave(); // CAD-windowed RX on/off from prefs
void applyFont();
void applyRotation();
void applyFullRefreshInterval();