mirror of
https://github.com/MarekZegare4/MeshCore-Solo.git
synced 2026-10-07 02:06:40 +00:00
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:
+50
-10
@@ -344,7 +344,7 @@ if pursued.
|
|||||||
**On branch `feat/power-saving`** — two independent toggles under Settings ›
|
**On branch `feat/power-saving`** — two independent toggles under Settings ›
|
||||||
Radio, both **default OFF**. Under field testing; not yet merged.
|
Radio, both **default OFF**. Under field testing; not yet merged.
|
||||||
|
|
||||||
**✅ Done — hardware duty-cycle RX ("Pwr save")**
|
**⛔ Disabled (2026-09-22) — hardware duty-cycle RX ("Pwr save")**
|
||||||
- Uses the SX126x's own **RX duty-cycle** (`SetRxDutyCycle`, datasheet 13.1.7) via
|
- Uses the SX126x's own **RX duty-cycle** (`SetRxDutyCycle`, datasheet 13.1.7) via
|
||||||
RadioLib `startReceiveDutyCycleAuto(preamble, 8)`: the chip's sequencer cycles
|
RadioLib `startReceiveDutyCycleAuto(preamble, 8)`: the chip's sequencer cycles
|
||||||
RX↔sleep, latches a preamble and stays in RX to receive the packet (RX_DONE on
|
RX↔sleep, latches a preamble and stays in RX to receive the packet (RX_DONE on
|
||||||
@@ -352,20 +352,60 @@ Radio, both **default OFF**. Under field testing; not yet merged.
|
|||||||
continuous RX. `armRecv()` arms duty-cycle when power-save is on, else a normal
|
continuous RX. `armRecv()` arms duty-cycle when power-save is on, else a normal
|
||||||
`startReceive()`; `loop()` re-arms only on a toggle. Falls back to continuous RX
|
`startReceive()`; `loop()` re-arms only on a toggle. Falls back to continuous RX
|
||||||
if the modem doesn't support duty-cycle (non-SX126x). `state` stays `STATE_RX`
|
if the modem doesn't support duty-cycle (non-SX126x). `state` stays `STATE_RX`
|
||||||
so the dispatcher's not-in-RX watchdog never trips.
|
so the dispatcher's not-in-RX watchdog never trips. Duty-cycle engages when the
|
||||||
- Duty-cycle engages when the configured preamble ≥ 2·8+1 symbols. At SF≤8 the
|
*assumed sender* preamble ≥ 2·8+1 symbols; using our own outgoing preamble
|
||||||
preamble is 32 → full duty-cycle; at SF9–12 it is 16 → RadioLib transparently
|
(`preambleLengthForSF(sf)`: 32 at SF≤8, 16 at SF9-12) as that assumption meant
|
||||||
stays on continuous RX (no power saving on the slow SFs).
|
duty-cycle only ever actually engaged at SF≤8.
|
||||||
- Companion: `rx_powersave` pref (schema `0xC0DE0009`), **Settings › Radio ›
|
- **Field report + investigation:** a user on the stock "EU/UK (Narrow)" preset
|
||||||
"Pwr save"**, applied at boot (MyMesh) and on change (UITask). Noise-floor
|
(SF8) saw reception drop from ~1-5 msg/min to ~1/3h with Pwr save on, unaffected
|
||||||
sampling is skipped while on (chip is asleep most of the time) — the radio page
|
by a better antenna. Root cause, confirmed against the SX1262 datasheet and
|
||||||
shows "Noise floor: n/a".
|
RadioLib's own maintainers ([jgromes/RadioLib#1597](https://github.com/jgromes/RadioLib/issues/1597),
|
||||||
|
closed as inherent chip behaviour, not a library bug): the SX126x's duty-cycle
|
||||||
|
preamble-detection state machine restarts every sleep/wake cycle and needs the
|
||||||
|
*actual transmitted* preamble to closely match what we've configured our
|
||||||
|
receiver to expect — tolerance in that issue's own testing was only 1-2 symbols
|
||||||
|
either way, well short of covering e.g. a repeater still on pre-v1.16 firmware
|
||||||
|
(16 symbols vs. our 32). A mismatch isn't a gradual sensitivity hit, it
|
||||||
|
deterministically drops every packet from that sender no matter the signal
|
||||||
|
strength — and a lone node mostly hears repeater rebroadcasts, exactly the
|
||||||
|
nodes least likely to be freshly updated. There is no software workaround:
|
||||||
|
RadioLib's own parameters only trade which senders you're blind to (shortening
|
||||||
|
`minSymbols` to tolerate a shorter assumed preamble directly shortens the
|
||||||
|
wake-window's correlator dwell time, trading the preamble-mismatch failure mode
|
||||||
|
for a marginal-signal one instead). A network-wide capability negotiation (e.g.
|
||||||
|
via the still-unused `ADV_FEAT1_MASK`/`ADV_FEAT2_MASK` fields already reserved
|
||||||
|
in `AdvertDataHelpers.h`) could plausibly gate this safely, but is a real
|
||||||
|
feature, not a quick fix — rejected for now as out of scope. Checked whether
|
||||||
|
IoTThinks' MeshCore fork (github.com/IoTThinks/MeshCore) had solved this: it
|
||||||
|
hasn't, and doesn't hit the problem at all, because its "power saving" never
|
||||||
|
touches the radio — `ESP32Board::sleep()`/NRF52 `board.sleep(0)` only light-sleep
|
||||||
|
the **MCU**, waking on the radio's own DIO1 GPIO interrupt while the radio itself
|
||||||
|
stays in plain continuous RX the whole time. That's the same MCU-idle mechanism
|
||||||
|
already noted below (native NRF52 companion power-saving from the v1.16
|
||||||
|
upstream merge) — safe, but doesn't touch the dominant power draw (the radio in
|
||||||
|
continuous RX), unlike a real duty-cycle.
|
||||||
|
- **Resolution:** `examples/companion_radio/MyMesh.h` now defines
|
||||||
|
`FEAT_RX_POWERSAVE 0`, gating out the Settings row (`SettingsScreen.h`), the
|
||||||
|
Diagnostics RXPS watchdog row (`DiagnosticsScreen.h`), and every call site that
|
||||||
|
would apply `_prefs.rx_powersave` to the radio or to CAD auto-enable
|
||||||
|
(`MyMesh.cpp`, `UITask::applyPowerSave()`) — including forcing
|
||||||
|
`setPowerSaving(false)` unconditionally so a *stale* `rx_powersave=1` byte left
|
||||||
|
over in an existing prefs file from before this change can't do anything either.
|
||||||
|
The real duty-cycle implementation itself
|
||||||
|
(`RadioLibWrapper`/`CustomSX1262Wrapper::startPowerSaveRecv()`) is left in place,
|
||||||
|
unneutered, matching our own preamble convention — it's simply unreachable now.
|
||||||
|
Flipping `FEAT_RX_POWERSAVE` back on requires solving the network-compatibility
|
||||||
|
problem above first, not just re-adding the toggle.
|
||||||
|
- Companion: `rx_powersave` pref (schema `0xC0DE0009`) still exists in
|
||||||
|
`NodePrefs`/`DataStore` purely for file-format stability; nothing reads it
|
||||||
|
anywhere behavior-relevant while `FEAT_RX_POWERSAVE` is 0.
|
||||||
|
|
||||||
> **History:** an earlier attempt used a *software* CAD state machine (scan →
|
> **History:** an earlier attempt used a *software* CAD state machine (scan →
|
||||||
> warm-sleep window → on-detect full RX, with `standbyXOSC`/burst windows). It
|
> warm-sleep window → on-detect full RX, with `standbyXOSC`/burst windows). It
|
||||||
> fought the hardware — querying a warm-sleeping chip from `checkSend()` gave a
|
> fought the hardware — querying a warm-sleeping chip from `checkSend()` gave a
|
||||||
> phantom-busy channel that stalled TX for ~4 s, and ACKs dropped in the scan
|
> phantom-busy channel that stalled TX for ~4 s, and ACKs dropped in the scan
|
||||||
> gaps. Replaced wholesale by the hardware duty-cycle above, which fixed both.
|
> gaps. Replaced wholesale by the hardware duty-cycle above, which turned out to
|
||||||
|
> have its own, deeper problem (see above).
|
||||||
|
|
||||||
**✅ Done — Adaptive Power Control ("Auto pwr")**
|
**✅ Done — Adaptive Power Control ("Auto pwr")**
|
||||||
- `tx_power_dbm` becomes a *ceiling*; APC drives the radio's actual power within
|
- `tx_power_dbm` becomes a *ceiling*; APC drives the radio's actual power within
|
||||||
|
|||||||
@@ -71,8 +71,9 @@ Lists all available home screen pages. For each entry:
|
|||||||
| SF | 5–12 | LEFT/RIGHT. Spreading factor. |
|
| SF | 5–12 | LEFT/RIGHT. Spreading factor. |
|
||||||
| BW | 7.8–500 kHz | LEFT/RIGHT cycles the standard LoRa bandwidths. |
|
| BW | 7.8–500 kHz | LEFT/RIGHT cycles the standard LoRa bandwidths. |
|
||||||
| CR | 5–8 | LEFT/RIGHT. Coding rate (4/5–4/8). |
|
| CR | 5–8 | LEFT/RIGHT. Coding rate (4/5–4/8). |
|
||||||
| Pwr save | ON / OFF | **Battery saver.** Hardware duty-cycle receive (SX126x only): cycles RX↔sleep, wakes on preamble, cuts average RX current at the cost of some latency. **Forced off (`--`) while the repeater is on** — restored once it's switched off. A background watchdog auto-recovers if the sequencer gets stuck (soft re-arm, then a full reset) — see Tools › Diagnostics for the counts. |
|
|
||||||
| Auto pwr | ON / OFF | **Adaptive Power Control.** Lowers TX power on strong links, ramps back to the **TX Pwr** ceiling on weak/lost ones. Link quality from DM ACK SNR, or — for channels (no ACK) — a repeater's rebroadcast. Live power shown on the radio page/name bar. Default OFF. **Suppressed (`--`) while the repeater is on** — restored once it's switched off. |
|
| Auto pwr | ON / OFF | **Adaptive Power Control.** Lowers TX power on strong links, ramps back to the **TX Pwr** ceiling on weak/lost ones. Link quality from DM ACK SNR, or — for channels (no ACK) — a repeater's rebroadcast. Live power shown on the radio page/name bar. Default OFF. **Suppressed (`--`) while the repeater is on** — restored once it's switched off. |
|
||||||
|
|
||||||
|
There is no "Pwr save" row: hardware RX duty-cycle receive was tried and disabled (`FEAT_RX_POWERSAVE 0` in `examples/companion_radio/MyMesh.h`) — the SX126x's duty-cycle preamble detection needs the sender's actual preamble to exactly match what we configure, which a mixed-firmware mesh can't guarantee (silently drops every packet from a mismatched sender, no matter the signal strength). See `docs/development/roadmap.md` for the full writeup.
|
||||||
| Scope | list | Shows the list's **default** scope. **Enter** opens the **SCOPE** list: `*` (wildcard = unscoped, always first, can't be renamed or deleted) plus up to 8 named scopes of your own (e.g. `pl`). Typing a name derives a shared key the same way on every device, so any device that types the same name lands on the same scope automatically, no key exchange needed. **Enter** on a row opens **Set as default** / **Rename** / **Delete** (delete confirms first); **+ Add scope** at the bottom opens the keyboard. The **default** scope (marked `[default]`) governs **DMs** and the **repeater's own relay slot**; each **channel** carries its own pick — set from the channel's context menu (see [Message Screen](../message_screen/message_screen.md)) — and a channel left on `*` sends unscoped. Scopes tag flood traffic so repeaters can tell your community's messages apart from others sharing the same frequency; paired with **Tools › Repeater › Scope only** it's also what this device relays for in repeater mode. The default also syncs both ways with the phone app's default-scope setting. Not encryption — message content is unaffected either way. Upgrading from a build with the old single Scope field carries it over as the default entry and seeds every existing channel with it, so nothing changes on the air. |
|
| Scope | list | Shows the list's **default** scope. **Enter** opens the **SCOPE** list: `*` (wildcard = unscoped, always first, can't be renamed or deleted) plus up to 8 named scopes of your own (e.g. `pl`). Typing a name derives a shared key the same way on every device, so any device that types the same name lands on the same scope automatically, no key exchange needed. **Enter** on a row opens **Set as default** / **Rename** / **Delete** (delete confirms first); **+ Add scope** at the bottom opens the keyboard. The **default** scope (marked `[default]`) governs **DMs** and the **repeater's own relay slot**; each **channel** carries its own pick — set from the channel's context menu (see [Message Screen](../message_screen/message_screen.md)) — and a channel left on `*` sends unscoped. Scopes tag flood traffic so repeaters can tell your community's messages apart from others sharing the same frequency; paired with **Tools › Repeater › Scope only** it's also what this device relays for in repeater mode. The default also syncs both ways with the phone app's default-scope setting. Not encryption — message content is unaffected either way. Upgrading from a build with the old single Scope field carries it over as the default entry and seeds every existing channel with it, so nothing changes on the air. |
|
||||||
|
|
||||||
| OLED | E-Ink |
|
| OLED | E-Ink |
|
||||||
|
|||||||
@@ -498,9 +498,10 @@ A circular tab carousel of live device and mesh stats, refreshed once a second (
|
|||||||
| Pool free | Free entries in the packet pool |
|
| Pool free | Free entries in the packet pool |
|
||||||
| Queue | Packets waiting in the outbound queue |
|
| Queue | Packets waiting in the outbound queue |
|
||||||
| Errors | Radio error flags since boot/reset — `OK`, or tokens `F` (queue full), `C` (CAD timeout), `R` (RX-start timeout) |
|
| Errors | Radio error flags since boot/reset — `OK`, or tokens `F` (queue full), `C` (CAD timeout), `R` (RX-start timeout) |
|
||||||
| RXPS wd s/h | RX duty-cycle watchdog recovery count, `soft/hard` — how many times the background watchdog has re-armed (soft) or fully reset (hard) a stuck duty-cycle sequencer. Stays `0/0` unless Settings › Radio › **Pwr save** is on and something actually went wrong. |
|
|
||||||
|
|
||||||
The packet counters, **Forwarded**, **Errors** and **RXPS wd s/h** are cumulative since boot. On the **Live** tab, **Hold Enter** opens a *Reset counters?* confirm (defaults to Cancel); the live readings (noise, RSSI/SNR, pool, queue, uptime) are not affected. **Cancel/Back** returns to the Tools list.
|
The packet counters, **Forwarded** and **Errors**, are cumulative since boot. On the **Live** tab, **Hold Enter** opens a *Reset counters?* confirm (defaults to Cancel); the live readings (noise, RSSI/SNR, pool, queue, uptime) are not affected. **Cancel/Back** returns to the Tools list.
|
||||||
|
|
||||||
|
There is no "RXPS wd s/h" row: it belongs to hardware RX duty-cycle receive, which is currently disabled (see the Settings screen doc and `docs/development/roadmap.md`) — nothing to watchdog.
|
||||||
|
|
||||||
The counters make the repeater behaviour observable: **Forwarded** confirms the node is actually relaying (not just configured to), and **Pool free** / **Queue** show whether forwarding is exhausting the packet pool. See **Tools › Repeater** for the relaying options.
|
The counters make the repeater behaviour observable: **Forwarded** confirms the node is actually relaying (not just configured to), and **Pool free** / **Queue** show whether forwarding is exhausting the packet pool. See **Tools › Repeater** for the relaying options.
|
||||||
|
|
||||||
@@ -551,7 +552,7 @@ The flood filters (**Skip advert** through **Scope only**) are **opt-in** (defau
|
|||||||
|
|
||||||
**Same network vs. separate network.** With **Network = Current** (or a Custom profile matching your companion settings) the repeater stays on your own network — you keep messaging while relaying. A *different* Custom profile moves the device entirely onto that network while relaying (one radio can't be on two at once), returning to your own network when switched off. The profile re-applies after a reboot if the repeater was left on.
|
**Same network vs. separate network.** With **Network = Current** (or a Custom profile matching your companion settings) the repeater stays on your own network — you keep messaging while relaying. A *different* Custom profile moves the device entirely onto that network while relaying (one radio can't be on two at once), returning to your own network when switched off. The profile re-applies after a reboot if the repeater was left on.
|
||||||
|
|
||||||
While on, a **»** indicator appears in the status bar (same blink convention as auto-advert/trail markers). Two radio settings are overridden and restored afterwards: **Pwr save** forced off (must listen continuously) and **Auto pwr** forced off (full TX power for relay reach). Both show `--` in Settings while active.
|
While on, a **»** indicator appears in the status bar (same blink convention as auto-advert/trail markers). **Auto pwr** is overridden off and restored afterwards (full TX power for relay reach) — shows `--` in Settings while active. (Pwr save, hardware RX duty-cycle, is currently disabled outright — see above — so there's nothing left for repeater mode to override there.)
|
||||||
|
|
||||||
Live forwarding stats — **Forwarded**, **Pool free**, **Queue** — are shown on **Tools › Diagnostics** (this screen is config-only).
|
Live forwarding stats — **Forwarded**, **Pool free**, **Queue** — are shown on **Tools › Diagnostics** (this screen is config-only).
|
||||||
|
|
||||||
|
|||||||
@@ -287,7 +287,14 @@ bool MyMesh::getCADEnabled() const {
|
|||||||
// _prefs.cad_enabled itself has no UI/CLI exposure yet on companion_radio
|
// _prefs.cad_enabled itself has no UI/CLI exposure yet on companion_radio
|
||||||
// (unlike simple_repeater's CommonCLI `cad` command) — it's wired and
|
// (unlike simple_repeater's CommonCLI `cad` command) — it's wired and
|
||||||
// persisted for a future manual override, but always 0 today.
|
// persisted for a future manual override, but always 0 today.
|
||||||
|
// rx_powersave is never actually applied to the radio while FEAT_RX_POWERSAVE
|
||||||
|
// is 0 (see MyMesh.h) -- don't let a stale persisted byte from before that
|
||||||
|
// still auto-enable CAD here for a duty-cycle mode that isn't running.
|
||||||
|
#if FEAT_RX_POWERSAVE
|
||||||
return _prefs.cad_enabled || (_prefs.rx_powersave && !_prefs.client_repeat);
|
return _prefs.cad_enabled || (_prefs.rx_powersave && !_prefs.client_repeat);
|
||||||
|
#else
|
||||||
|
return _prefs.cad_enabled;
|
||||||
|
#endif
|
||||||
}
|
}
|
||||||
|
|
||||||
int MyMesh::calcRxDelay(float score, uint32_t air_time) const {
|
int MyMesh::calcRxDelay(float score, uint32_t air_time) const {
|
||||||
@@ -1991,7 +1998,11 @@ void MyMesh::begin(bool has_display) {
|
|||||||
applyRepeaterRadio(); // companion params, or the repeater profile if relaying with one set
|
applyRepeaterRadio(); // companion params, or the repeater profile if relaying with one set
|
||||||
applyApc(); // sets TX power to the ceiling and arms APC if enabled
|
applyApc(); // sets TX power to the ceiling and arms APC if enabled
|
||||||
radio_driver.setRxBoostedGainMode(_prefs.rx_boosted_gain);
|
radio_driver.setRxBoostedGainMode(_prefs.rx_boosted_gain);
|
||||||
|
#if FEAT_RX_POWERSAVE
|
||||||
radio_driver.setPowerSaving(_prefs.rx_powersave && !_prefs.client_repeat); // duty-cycle RX off while repeating (must hear all traffic)
|
radio_driver.setPowerSaving(_prefs.rx_powersave && !_prefs.client_repeat); // duty-cycle RX off while repeating (must hear all traffic)
|
||||||
|
#else
|
||||||
|
radio_driver.setPowerSaving(false); // see MyMesh.h FEAT_RX_POWERSAVE -- ignore any stale persisted rx_powersave byte
|
||||||
|
#endif
|
||||||
board.setLoRaFemLnaEnabled(_prefs.radio_fem_rxgain);
|
board.setLoRaFemLnaEnabled(_prefs.radio_fem_rxgain);
|
||||||
board.setLoRaFemPaGainEnabled(_prefs.radio_fem_txgain);
|
board.setLoRaFemPaGainEnabled(_prefs.radio_fem_txgain);
|
||||||
MESH_DEBUG_PRINTLN("RX Boosted Gain Mode: %s",
|
MESH_DEBUG_PRINTLN("RX Boosted Gain Mode: %s",
|
||||||
@@ -2482,7 +2493,11 @@ void MyMesh::handleCmdFrame(size_t len) {
|
|||||||
// Keep the "repeating ⇒ continuous RX, full TX power" invariants when repeat
|
// Keep the "repeating ⇒ continuous RX, full TX power" invariants when repeat
|
||||||
// is toggled via the app, mirroring the on-device path (a repeater must hear
|
// is toggled via the app, mirroring the on-device path (a repeater must hear
|
||||||
// all traffic and relay at consistent power).
|
// all traffic and relay at consistent power).
|
||||||
|
#if FEAT_RX_POWERSAVE
|
||||||
radio_driver.setPowerSaving(_prefs.rx_powersave && !_prefs.client_repeat);
|
radio_driver.setPowerSaving(_prefs.rx_powersave && !_prefs.client_repeat);
|
||||||
|
#else
|
||||||
|
radio_driver.setPowerSaving(false); // see MyMesh.h FEAT_RX_POWERSAVE -- ignore any stale persisted rx_powersave byte
|
||||||
|
#endif
|
||||||
applyApc(); // pins power to the ceiling; apcActive() keeps it there while repeating
|
applyApc(); // pins power to the ceiling; apcActive() keeps it there while repeating
|
||||||
MESH_DEBUG_PRINTLN("OK: CMD_SET_RADIO_PARAMS: f=%d, bw=%d, sf=%d, cr=%d", freq, bw, (uint32_t)sf,
|
MESH_DEBUG_PRINTLN("OK: CMD_SET_RADIO_PARAMS: f=%d, bw=%d, sf=%d, cr=%d", freq, bw, (uint32_t)sf,
|
||||||
(uint32_t)cr);
|
(uint32_t)cr);
|
||||||
|
|||||||
@@ -11,6 +11,20 @@ class UITask;
|
|||||||
/*------------ Frame Protocol --------------*/
|
/*------------ Frame Protocol --------------*/
|
||||||
#define FIRMWARE_VER_CODE 13
|
#define FIRMWARE_VER_CODE 13
|
||||||
|
|
||||||
|
// Hardware RX duty-cycle ("Pwr save") is disabled: the SX126x's duty-cycle
|
||||||
|
// preamble detection needs the *actual transmitted* preamble to exactly match
|
||||||
|
// what we're configured to expect (confirmed hardware behaviour, see SX1262
|
||||||
|
// datasheet 6.1.3 and https://github.com/jgromes/RadioLib/issues/1597) --
|
||||||
|
// something we can't guarantee across a mesh with mixed firmware. A mismatch
|
||||||
|
// doesn't cost a little sensitivity, it silently drops every packet from that
|
||||||
|
// sender regardless of signal strength (2026-09-22 field report: near-total
|
||||||
|
// reception loss, unaffected by antenna). The real duty-cycle implementation
|
||||||
|
// (RadioLibWrapper::armRecv()/CustomSX1262Wrapper::startPowerSaveRecv()) is
|
||||||
|
// left in place for if a network-wide compatibility mechanism ever lands --
|
||||||
|
// flipping this back to 1 requires solving that first, not just re-adding the
|
||||||
|
// Settings toggle. See docs/development/roadmap.md for the full writeup.
|
||||||
|
#define FEAT_RX_POWERSAVE 0
|
||||||
|
|
||||||
// Fallback only -- every real build (local or CI) goes through build.sh, which
|
// Fallback only -- every real build (local or CI) goes through build.sh, which
|
||||||
// always injects its own FIRMWARE_BUILD_DATE (today's date at build time).
|
// always injects its own FIRMWARE_BUILD_DATE (today's date at build time).
|
||||||
// __DATE__ is the compiler's own "Mmm dd yyyy" build-date macro, so a
|
// __DATE__ is the compiler's own "Mmm dd yyyy" build-date macro, so a
|
||||||
|
|||||||
@@ -144,12 +144,14 @@ class DiagnosticsScreen : public UIScreen {
|
|||||||
}
|
}
|
||||||
addRow("Errors", buf);
|
addRow("Errors", buf);
|
||||||
|
|
||||||
|
#if FEAT_RX_POWERSAVE
|
||||||
// RX duty-cycle watchdog recovery counts since boot/reset (soft re-arm /
|
// RX duty-cycle watchdog recovery counts since boot/reset (soft re-arm /
|
||||||
// hard chip reset). Always 0/0 on radios or profiles that never arm
|
// hard chip reset). Always 0/0 on radios or profiles that never arm
|
||||||
// duty-cycle power-save (repeaters force it off — see applyPowerSave()).
|
// duty-cycle power-save (repeaters force it off — see applyPowerSave()).
|
||||||
snprintf(buf, sizeof(buf), "%lu/%lu", (unsigned long)radio_driver.getRxPsWatchdogSoftCount(),
|
snprintf(buf, sizeof(buf), "%lu/%lu", (unsigned long)radio_driver.getRxPsWatchdogSoftCount(),
|
||||||
(unsigned long)radio_driver.getRxPsWatchdogHardCount());
|
(unsigned long)radio_driver.getRxPsWatchdogHardCount());
|
||||||
addRow("RXPS wd s/h", buf);
|
addRow("RXPS wd s/h", buf);
|
||||||
|
#endif
|
||||||
}
|
}
|
||||||
|
|
||||||
void buildSystemLines() {
|
void buildSystemLines() {
|
||||||
|
|||||||
@@ -61,7 +61,9 @@ class SettingsScreen : public UIScreen {
|
|||||||
TX_POWER,
|
TX_POWER,
|
||||||
RADIO_PRESET,
|
RADIO_PRESET,
|
||||||
CUSTOM_FREQ, CUSTOM_SF, CUSTOM_BW, CUSTOM_CR,
|
CUSTOM_FREQ, CUSTOM_SF, CUSTOM_BW, CUSTOM_CR,
|
||||||
|
#if FEAT_RX_POWERSAVE
|
||||||
POWER_SAVE,
|
POWER_SAVE,
|
||||||
|
#endif
|
||||||
TX_APC,
|
TX_APC,
|
||||||
SCOPE_NAME,
|
SCOPE_NAME,
|
||||||
// System section
|
// System section
|
||||||
@@ -556,12 +558,14 @@ class SettingsScreen : public UIScreen {
|
|||||||
snprintf(buf, sizeof(buf), "%d", p ? (int)p->cr : 0);
|
snprintf(buf, sizeof(buf), "%d", p ? (int)p->cr : 0);
|
||||||
display.setCursor(valCol(display), y);
|
display.setCursor(valCol(display), y);
|
||||||
display.print(buf);
|
display.print(buf);
|
||||||
|
#if FEAT_RX_POWERSAVE
|
||||||
} else if (item == POWER_SAVE) {
|
} else if (item == POWER_SAVE) {
|
||||||
display.print("Pwr save");
|
display.print("Pwr save");
|
||||||
display.setCursor(valCol(display), y);
|
display.setCursor(valCol(display), y);
|
||||||
// Forced off (and locked) while the repeater is on — it must hear all traffic.
|
// Forced off (and locked) while the repeater is on — it must hear all traffic.
|
||||||
if (p && p->client_repeat) display.print("--");
|
if (p && p->client_repeat) display.print("--");
|
||||||
else display.print((p && p->rx_powersave) ? "ON" : "OFF");
|
else display.print((p && p->rx_powersave) ? "ON" : "OFF");
|
||||||
|
#endif
|
||||||
} else if (item == TX_APC) {
|
} else if (item == TX_APC) {
|
||||||
display.print("Auto pwr");
|
display.print("Auto pwr");
|
||||||
display.setCursor(valCol(display), y);
|
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_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_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 (_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 (_selected == POWER_SAVE && p && (left || right || enter)) {
|
||||||
if (p->client_repeat) { _task->showAlert("Off while repeating", 900); return true; }
|
if (p->client_repeat) { _task->showAlert("Off while repeating", 900); return true; }
|
||||||
p->rx_powersave ^= 1;
|
p->rx_powersave ^= 1;
|
||||||
@@ -1085,6 +1090,7 @@ public:
|
|||||||
_dirty = true;
|
_dirty = true;
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
#endif
|
||||||
if (_selected == TX_APC && p && (left || right || enter)) {
|
if (_selected == TX_APC && p && (left || right || enter)) {
|
||||||
if (p->client_repeat) { _task->showAlert("Off while repeating", 900); return true; }
|
if (p->client_repeat) { _task->showAlert("Off while repeating", 900); return true; }
|
||||||
p->tx_apc ^= 1;
|
p->tx_apc ^= 1;
|
||||||
|
|||||||
@@ -3805,10 +3805,14 @@ void UITask::applyTxPower() {
|
|||||||
|
|
||||||
void UITask::applyPowerSave() {
|
void UITask::applyPowerSave() {
|
||||||
if (_node_prefs == NULL) return;
|
if (_node_prefs == NULL) return;
|
||||||
|
#if FEAT_RX_POWERSAVE
|
||||||
// A repeater must hear every packet to relay it, so duty-cycle RX (which sleeps
|
// A repeater must hear every packet to relay it, so duty-cycle RX (which sleeps
|
||||||
// between preamble checks) is forced off while repeating — the user's pref is
|
// between preamble checks) is forced off while repeating — the user's pref is
|
||||||
// kept and restored when the repeater is switched off.
|
// kept and restored when the repeater is switched off.
|
||||||
radio_driver.setPowerSaving(_node_prefs->rx_powersave && !_node_prefs->client_repeat);
|
radio_driver.setPowerSaving(_node_prefs->rx_powersave && !_node_prefs->client_repeat);
|
||||||
|
#else
|
||||||
|
radio_driver.setPowerSaving(false); // see MyMesh.h FEAT_RX_POWERSAVE -- ignore any stale persisted rx_powersave byte
|
||||||
|
#endif
|
||||||
}
|
}
|
||||||
|
|
||||||
void UITask::applyApc() {
|
void UITask::applyApc() {
|
||||||
|
|||||||
@@ -61,6 +61,21 @@ public:
|
|||||||
// minSymbols=8 is the reliable preamble-latch count for SF7-12. If the
|
// minSymbols=8 is the reliable preamble-latch count for SF7-12. If the
|
||||||
// configured preamble is too short for a real duty-cycle (senderPreamble <
|
// configured preamble is too short for a real duty-cycle (senderPreamble <
|
||||||
// 2*minSymbols+1), RadioLib transparently falls back to a continuous receive.
|
// 2*minSymbols+1), RadioLib transparently falls back to a continuous receive.
|
||||||
|
//
|
||||||
|
// NOT CURRENTLY REACHABLE: FEAT_RX_POWERSAVE (MyMesh.h) is 0, so nothing ever
|
||||||
|
// calls setPowerSaving(true) and this never runs. Left implemented (matching
|
||||||
|
// our own preambleLengthForSF(sf) convention, so it's correct for nodes on
|
||||||
|
// this same convention) rather than removed, because the reason it's disabled
|
||||||
|
// isn't a bug here: the SX126x's duty-cycle preamble detection needs the
|
||||||
|
// *actual transmitted* preamble to exactly match what we assume here, a
|
||||||
|
// confirmed hardware behaviour (SX1262 datasheet 6.1.3; RadioLib issue
|
||||||
|
// https://github.com/jgromes/RadioLib/issues/1597) that no local parameter
|
||||||
|
// choice can work around -- it requires a network-wide compatibility
|
||||||
|
// guarantee we can't currently make. A mismatch doesn't cost a little
|
||||||
|
// sensitivity, it silently drops every packet from that sender regardless of
|
||||||
|
// signal strength (2026-09-22 field report: near-total reception loss on a
|
||||||
|
// stock SF8 preset, unaffected by antenna gain). See MyMesh.h and
|
||||||
|
// docs/development/roadmap.md before ever flipping FEAT_RX_POWERSAVE back on.
|
||||||
int16_t startPowerSaveRecv() override {
|
int16_t startPowerSaveRecv() override {
|
||||||
return ((SX126x *)_radio)->startReceiveDutyCycleAuto(preambleLengthForSF(_preamble_sf), 8);
|
return ((SX126x *)_radio)->startReceiveDutyCycleAuto(preambleLengthForSF(_preamble_sf), 8);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -46,6 +46,13 @@ protected:
|
|||||||
// stays in RX to receive the packet (RX_DONE on DIO1) — no MCU state machine,
|
// stays in RX to receive the packet (RX_DONE on DIO1) — no MCU state machine,
|
||||||
// average RX current cut several-fold. Driven from armRecv()/loop(); falls back
|
// average RX current cut several-fold. Driven from armRecv()/loop(); falls back
|
||||||
// to continuous RX if the modem doesn't support it.
|
// to continuous RX if the modem doesn't support it.
|
||||||
|
// Companion-side gating: FEAT_RX_POWERSAVE (examples/companion_radio/MyMesh.h)
|
||||||
|
// is 0, so nothing on that target ever calls setPowerSaving(true) and this
|
||||||
|
// path never actually runs there -- the SX126x duty-cycle's preamble
|
||||||
|
// detection needs the sender's actual preamble to exactly match what we
|
||||||
|
// configure (confirmed hardware behaviour, not a bug here), which can't be
|
||||||
|
// guaranteed across a mesh with mixed firmware. See that file before
|
||||||
|
// re-enabling it anywhere.
|
||||||
bool _power_save = false;
|
bool _power_save = false;
|
||||||
bool _ps_active = false; // is the radio currently armed in duty-cycle mode
|
bool _ps_active = false; // is the radio currently armed in duty-cycle mode
|
||||||
int8_t _tx_dbm = 0; // last TX power applied (tracks APC's live value)
|
int8_t _tx_dbm = 0; // last TX power applied (tracks APC's live value)
|
||||||
|
|||||||
Reference in New Issue
Block a user