fix(radio): disable Pwr save (RX duty-cycle) — unsafe across mixed firmware

Field report: a user on the stock SF8 "EU/UK (Narrow)" preset saw reception
drop from ~1-5 msg/min to ~1/3h with Pwr save on, unaffected by a better
antenna. Root cause, confirmed against the SX1262 datasheet and RadioLib's
own maintainers (jgromes/RadioLib#1597, closed as inherent chip behaviour):
the SX126x's duty-cycle preamble detection needs the sender's actual
preamble to closely match what we've configured our receiver to expect.
A mismatch — e.g. a repeater still on pre-v1.16 firmware sending 16 symbols
against our 32 — doesn't cost a little sensitivity, it silently drops every
packet from that sender regardless of signal strength. There's no local
parameter fix: shortening minSymbols to tolerate shorter preambles directly
shortens the wake-window's correlator dwell time, trading the preamble
mismatch failure mode for a marginal-signal one instead. Checked whether
IoTThinks' MeshCore fork solved this differently — it doesn't hit the
problem at all, because its "power saving" only light-sleeps the MCU on the
radio's own GPIO interrupt and never touches RX duty-cycle.

examples/companion_radio/MyMesh.h now defines FEAT_RX_POWERSAVE 0, gating
out the Settings row, the Diagnostics RXPS watchdog row, and every call site
that would apply rx_powersave to the radio or CAD auto-enable — including
forcing setPowerSaving(false) unconditionally so a stale rx_powersave=1 byte
from before this change can't do anything either. The real duty-cycle
implementation in RadioLibWrapper/CustomSX1262Wrapper is left in place,
unneutered, for if a network-wide compatibility mechanism (e.g. the
still-unused ADV_FEAT1/FEAT2 advert bits) is ever built to make it safe.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
This commit is contained in:
Jakub
2026-09-22 20:04:42 +02:00
co-authored by Claude Sonnet 5
parent f8cd24dd4d
commit 26e6bc877a
10 changed files with 119 additions and 14 deletions
@@ -61,7 +61,9 @@ class SettingsScreen : public UIScreen {
TX_POWER,
RADIO_PRESET,
CUSTOM_FREQ, CUSTOM_SF, CUSTOM_BW, CUSTOM_CR,
#if FEAT_RX_POWERSAVE
POWER_SAVE,
#endif
TX_APC,
SCOPE_NAME,
// System section
@@ -556,12 +558,14 @@ class SettingsScreen : public UIScreen {
snprintf(buf, sizeof(buf), "%d", p ? (int)p->cr : 0);
display.setCursor(valCol(display), y);
display.print(buf);
#if FEAT_RX_POWERSAVE
} else if (item == POWER_SAVE) {
display.print("Pwr save");
display.setCursor(valCol(display), y);
// Forced off (and locked) while the repeater is on — it must hear all traffic.
if (p && p->client_repeat) display.print("--");
else display.print((p && p->rx_powersave) ? "ON" : "OFF");
#endif
} else if (item == TX_APC) {
display.print("Auto pwr");
display.setCursor(valCol(display), y);
@@ -1078,6 +1082,7 @@ public:
if (_selected == CUSTOM_SF && p && dir && RadioParamsEditor::stepSF(p->sf, dir)) { _task->applyRadioParams(); _dirty = true; return true; }
if (_selected == CUSTOM_BW && p && dir && RadioParamsEditor::stepBW(p->bw, dir)) { _task->applyRadioParams(); _dirty = true; return true; }
if (_selected == CUSTOM_CR && p && dir && RadioParamsEditor::stepCR(p->cr, dir)) { _task->applyRadioParams(); _dirty = true; return true; }
#if FEAT_RX_POWERSAVE
if (_selected == POWER_SAVE && p && (left || right || enter)) {
if (p->client_repeat) { _task->showAlert("Off while repeating", 900); return true; }
p->rx_powersave ^= 1;
@@ -1085,6 +1090,7 @@ public:
_dirty = true;
return true;
}
#endif
if (_selected == TX_APC && p && (left || right || enter)) {
if (p->client_repeat) { _task->showAlert("Off while repeating", 900); return true; }
p->tx_apc ^= 1;