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-04 18:54:52 +02:00
parent baaac3179c
commit 90ca21432c
8 changed files with 141 additions and 9 deletions

View File

@@ -323,14 +323,15 @@ void DataStore::loadPrefsInt(const char *filename, NodePrefs& _prefs, double& no
rd(&_prefs.units_imperial, sizeof(_prefs.units_imperial));
rd(&_prefs.trail_show_pace, sizeof(_prefs.trail_show_pace));
rd(&_prefs.advert_sound_scope, sizeof(_prefs.advert_sound_scope));
// These fields were appended after ch_fav_only; an older file can leave
// stray bytes here (the old tail sentinel gets partly consumed), so clamp
// out-of-range values back to defaults.
rd(&_prefs.rx_powersave, sizeof(_prefs.rx_powersave));
// These fields were appended over successive schema bumps; an older file
// can leave stray bytes here, so clamp out-of-range values back to defaults.
// Values for notif_melody_ad: 0=built-in, 1=melody1, 2=melody2, 3=none.
if (_prefs.notif_melody_ad > 3) _prefs.notif_melody_ad = 0;
if (_prefs.units_imperial > 1) _prefs.units_imperial = 0;
if (_prefs.trail_show_pace > 1) _prefs.trail_show_pace = 0;
if (_prefs.advert_sound_scope > 1) _prefs.advert_sound_scope = ADVERT_SOUND_SCOPE_ALL;
if (_prefs.rx_powersave > 1) _prefs.rx_powersave = 0;
// Schema sentinel: bumped on layout changes. Mismatch means an older file
// (or a different schema); rd() already zero-inits any fields not present,
@@ -450,6 +451,7 @@ void DataStore::savePrefs(const NodePrefs& _prefs, double node_lat, double node_
file.write((uint8_t *)&_prefs.units_imperial, sizeof(_prefs.units_imperial));
file.write((uint8_t *)&_prefs.trail_show_pace, sizeof(_prefs.trail_show_pace));
file.write((uint8_t *)&_prefs.advert_sound_scope, sizeof(_prefs.advert_sound_scope));
file.write((uint8_t *)&_prefs.rx_powersave, sizeof(_prefs.rx_powersave));
// Tail sentinel — must be last. See NodePrefs::SCHEMA_SENTINEL.
uint32_t sentinel = NodePrefs::SCHEMA_SENTINEL;

View File

@@ -1203,6 +1203,7 @@ void MyMesh::begin(bool has_display) {
radio_driver.setParams(_prefs.freq, _prefs.bw, _prefs.sf, _prefs.cr);
radio_driver.setTxPower(_prefs.tx_power_dbm);
radio_driver.setRxBoostedGainMode(_prefs.rx_boosted_gain);
radio_driver.setPowerSaving(_prefs.rx_powersave); // CAD-windowed RX (battery saver)
MESH_DEBUG_PRINTLN("RX Boosted Gain Mode: %s",
radio_driver.getRxBoostedGainMode() ? "Enabled" : "Disabled");
}

View File

@@ -120,6 +120,11 @@ 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;
// CAD-windowed receive (battery saver): 0=continuous RX (default), 1=on.
// Periodically scans for activity and idles between scans instead of
// listening continuously — cuts average RX current at the cost of a little
// receive latency. See RadioLibWrapper power-save.
uint8_t rx_powersave;
// Tail sentinel written at the end of /new_prefs. Bump the low byte when
// adding/removing/reordering fields in DataStore::savePrefs/loadPrefsInt so

View File

@@ -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();

View File

@@ -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);

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();