Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
66 changes: 66 additions & 0 deletions docs/cli_commands.md
Original file line number Diff line number Diff line change
Expand Up @@ -307,6 +307,72 @@ This document provides an overview of CLI commands that can be sent to MeshCore

---

#### View or change RX power saving
**Usage:**
- `get radio.rxps`
- `set radio.rxps off`
- `set radio.rxps on`
- `set radio.rxps conservative`
- `set radio.rxps balanced`
- `set radio.rxps <level>`
- `set radio.rxps level <level>`
- `set radio.rxps level <level> preamble <symbols>`
- `set radio.rxps <rx_us> <sleep_us>`

**Parameters:**
- `level`: `1-10`; higher levels use shorter receive windows and longer sleep windows.
- `symbols`: `16` or `32` preamble symbols.
- `rx_us`: receive-window duration in microseconds, `1000-30000000`.
- `sleep_us`: radio sleep duration in microseconds, `1000-30000000`.

**Repeater default:** `off`

**Profiles:**
- `on` and `conservative`: level 1 with a 16-symbol preamble.
- `balanced`: level 5 with a 16-symbol preamble.
- A numeric level, or `level <level>`, automatically uses 32 preamble symbols for SF5-SF8 and 16 for SF9-SF12.
- `level <level> preamble <symbols>` explicitly fixes the preamble used in the calculation.
- Explicit `rx_us sleep_us` values select manual timing (`level=0`).

Level-based settings are recalculated after SF or bandwidth changes. Manual timings are not recalculated. Settings are persisted in `/prefs.json`. Companion firmware does not expose this text command and applies its fixed level 5 / preamble 16 profile at startup and after radio-parameter changes.

`get radio.rxps` reports:

```text
desired=<on|off>,effective=<armed|continuous>,supported=<yes|no>,
level=<0-10>,preamble=<0|16|32>,rx=<us>,sleep=<us>,
err=<RadioLib error>,fail=<count>[,erx=<us>,eslp=<us>]
```

- `desired` is the saved user setting.
- `effective=armed` means receive duty-cycle is active.
- `effective=continuous` means RXPS is disabled, unsupported, or the last arm attempt fell back to continuous RX.
- `fail` counts failed arm operations; each one falls back to continuous RX. `clear stats` resets both this total and the consecutive-failure backoff, granting three fresh arm attempts.
- `erx` and `eslp` appear only when the driver had to clamp the requested periods, and report the effective periods after driver clamping. On LR1110 the RX window is stretched when `2*rx + sleep` would not cover the extended period Semtech requires, so the real duty cycle can be less economical than `rx`/`sleep` suggest.
- RXPS is currently supported by the SX1262 and LR1110 wrappers. Other radios remain in continuous RX and reject attempts to enable RXPS.
- There is intentionally no RXPS watchdog, watchdog command, or periodic recovery. Recovery is limited to the immediate continuous-RX fallback after an arm error. After 3 consecutive arm failures the node stops retrying on every RX restart and stays in continuous RX until the RXPS configuration is set again or `clear stats` grants a fresh set of attempts.
- On boards with a host-controlled RXEN pin, the RF switch is held in receive mode for the whole duty cycle (otherwise the node would be deaf). An external LNA on that pin therefore stays biased during the sleep windows, so the real power saving is smaller than the `rx`/`sleep` ratio implies.

---

#### Disable the host-controlled RF receive switch during RX power saving
**Usage:**
- `get radio.rxps.rfrx_disabled`
- `set radio.rxps.rfrx_disabled <state>`

**Parameters:**
- `state`: `on`|`off`

**Default:** `off`

**Notes:**
- This is a runtime-only diagnostic setting and resets to `off` after reboot.
- `on` reproduces the missing RF_RX assertion during SX1262 receive duty-cycle mode.
- Supported only on SX1262 targets with a host-controlled RX enable pin.
- Enabling it can significantly reduce receive sensitivity and make remote commands harder to receive.

---

### System

#### View or change this node's name
Expand Down
37 changes: 36 additions & 1 deletion examples/companion_radio/MyMesh.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@

#include <Arduino.h> // needed for PlatformIO
#include <Mesh.h>
#include "helpers/radiolib/RXPowerSaving.h"

#define CMD_APP_START 1
#define CMD_SEND_TXT_MSG 2
Expand Down Expand Up @@ -265,6 +266,33 @@ bool MyMesh::getCADEnabled() const {
return false; // hardware CAD before TX (disabled by default, until configurable)
}

static void applyCompanionRxPowerSaving(uint8_t sf, float bw) {
#ifdef WRAPPER_CLASS
RxPowerSavingControl* control = &radio_driver;

// setRxPowerSaving() rejects out-of-range periods without touching the
// wrapper's state, so on any failure we must explicitly stand the duty cycle
// down. Otherwise it would stay armed with the *previous* SF/BW timings -
// e.g. SF5/BW500 yields rx=655us (below the 1ms minimum), and the radio would
// keep sleeping in windows sized for SF11, missing every preamble.
uint32_t rx_us = 0;
uint32_t sleep_us = 0;
bool ok = calcRxPowerSavingLevel(RX_POWERSAVING_BALANCED_LEVEL, sf, bw,
RX_POWERSAVING_PROFILE_PREAMBLE, &rx_us, &sleep_us) &&
control->setRxPowerSaving(true, rx_us, sleep_us);
if (!ok) {
control->setRxPowerSaving(false, RX_POWERSAVING_DEFAULT_RX_US,
RX_POWERSAVING_DEFAULT_SLEEP_US);
}
MESH_DEBUG_PRINTLN("RX Power Saving: companion level=5,preamble=16,rx=%lu,sleep=%lu,%s",
(unsigned long)rx_us, (unsigned long)sleep_us,
ok ? "accepted" : "unavailable - continuous RX");
#else
(void)sf;
(void)bw;
#endif
}

int MyMesh::calcRxDelay(float score, uint32_t air_time) const {
if (_prefs.rx_delay_base <= 0.0f) return 0;
return (int)((pow(_prefs.rx_delay_base, 0.85f - score) - 1.0) * air_time);
Expand Down Expand Up @@ -980,6 +1008,7 @@ void MyMesh::begin(bool has_display) {
board.setLoRaFemPaGainEnabled(_prefs.radio_fem_txgain);
MESH_DEBUG_PRINTLN("RX Boosted Gain Mode: %s",
radio_driver.getRxBoostedGainMode() ? "Enabled" : "Disabled");
applyCompanionRxPowerSaving(_prefs.sf, _prefs.bw);
}

const char *MyMesh::getNodeName() {
Expand Down Expand Up @@ -1403,6 +1432,7 @@ void MyMesh::handleCmdFrame(size_t len) {
savePrefs();

radio_driver.setParams(_prefs.freq, _prefs.bw, _prefs.sf, _prefs.cr);
applyCompanionRxPowerSaving(_prefs.sf, _prefs.bw);
MESH_DEBUG_PRINTLN("OK: CMD_SET_RADIO_PARAMS: f=%d, bw=%d, sf=%d, cr=%d", freq, bw, (uint32_t)sf,
(uint32_t)cr);

Expand Down Expand Up @@ -2263,5 +2293,10 @@ bool MyMesh::advert() {

// To check if there is pending work
bool MyMesh::hasPendingWork() const {
return _mgr->getOutboundTotal() > 0 || dirty_contacts_expiry != 0;
bool calibration_active = false;
#ifdef WRAPPER_CLASS
const RxPowerSavingControl* rxps_control = &radio_driver;
calibration_active = rxps_control->isRxPowerSavingCalibrationActive();
#endif
return _mgr->getOutboundTotal() > 0 || dirty_contacts_expiry != 0 || calibration_active;
}
30 changes: 29 additions & 1 deletion examples/simple_repeater/MyMesh.cpp
Original file line number Diff line number Diff line change
@@ -1,5 +1,28 @@
#include "MyMesh.h"
#include <algorithm>
#include "helpers/radiolib/RXPowerSaving.h"

static RxPowerSavingControl* getRxPowerSavingControl() {
#ifdef WRAPPER_CLASS
return &radio_driver;
#else
return nullptr;
#endif
}

static void applyRxPowerSavingConfig(NodePrefs& prefs, uint8_t sf, float bw) {
normalizeRxPowerSavingConfig(&prefs.rxps, sf, bw);
RxPowerSavingControl* control = getRxPowerSavingControl();
bool ok = control != nullptr
? control->setRxPowerSaving(
prefs.rxps.enabled != 0, prefs.rxps.rx_us, prefs.rxps.sleep_us)
: prefs.rxps.enabled == 0;
MESH_DEBUG_PRINTLN("RX Power Saving: desired=%s, rx=%lu, sleep=%lu, %s",
prefs.rxps.enabled ? "on" : "off",
(unsigned long)prefs.rxps.rx_us,
(unsigned long)prefs.rxps.sleep_us,
ok ? "accepted" : "unsupported");
}

/* ------------------------------ Config -------------------------------- */

Expand Down Expand Up @@ -862,7 +885,7 @@ MyMesh::MyMesh(mesh::MainBoard &board, mesh::Radio &radio, mesh::MillisecondCloc
mesh::RTCClock &rtc, mesh::MeshTables &tables)
: mesh::Mesh(radio, ms, rng, rtc, *new StaticPoolPacketManager(32), tables),
region_map(key_store), temp_map(key_store),
_cli(board, rtc, sensors, region_map, acl, &_prefs, this),
_cli(board, rtc, sensors, region_map, acl, &_prefs, this, getRxPowerSavingControl()),
telemetry(MAX_PACKET_PAYLOAD - 4),
discover_limiter(4, 120), // max 4 every 2 minutes
anon_limiter(4, 180) // max 4 every 3 minutes
Expand Down Expand Up @@ -983,6 +1006,7 @@ void MyMesh::begin(FILESYSTEM *fs) {
radio_driver.getRxBoostedGainMode() ? "Enabled" : "Disabled");
board.setLoRaFemLnaEnabled(_prefs.radio_fem_rxgain);
board.setLoRaFemPaGainEnabled(_prefs.radio_fem_txgain);
applyRxPowerSavingConfig(_prefs, _prefs.sf, _prefs.bw);

updateAdvertTimer();
updateFloodAdvertTimer();
Expand Down Expand Up @@ -1308,12 +1332,14 @@ void MyMesh::loop() {
if (set_radio_at && millisHasNowPassed(set_radio_at)) { // apply pending (temporary) radio params
set_radio_at = 0; // clear timer
radio_driver.setParams(pending_freq, pending_bw, pending_sf, pending_cr);
applyRxPowerSavingConfig(_prefs, pending_sf, pending_bw);
MESH_DEBUG_PRINTLN("Temp radio params");
}

if (revert_radio_at && millisHasNowPassed(revert_radio_at)) { // revert radio params to orig
revert_radio_at = 0; // clear timer
radio_driver.setParams(_prefs.freq, _prefs.bw, _prefs.sf, _prefs.cr);
applyRxPowerSavingConfig(_prefs, _prefs.sf, _prefs.bw);
MESH_DEBUG_PRINTLN("Radio params restored");
}

Expand All @@ -1334,5 +1360,7 @@ bool MyMesh::hasPendingWork() const {
#if defined(WITH_BRIDGE)
if (bridge.isRunning()) return true; // bridge needs WiFi radio, can't sleep
#endif
const RxPowerSavingControl* control = getRxPowerSavingControl();
if (control != nullptr && control->isRxPowerSavingCalibrationActive()) return true;
return _mgr->getOutboundTotal() > 0;
}
2 changes: 2 additions & 0 deletions platformio.ini
Original file line number Diff line number Diff line change
Expand Up @@ -170,6 +170,8 @@ build_src_filter =
+<../src/Utils.cpp>
+<../src/Packet.cpp>
+<../src/helpers/ConfigSerializer.cpp>
+<../src/helpers/radiolib/RXPowerSaving.cpp>
+<../src/helpers/radiolib/RXPowerSavingCLI.cpp>
lib_deps =
google/googletest @ 1.17.0

Expand Down
17 changes: 16 additions & 1 deletion src/helpers/CommonCLI.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@
#include "TxtDataHelpers.h"
#include "AdvertDataHelpers.h"
#include "TxtDataHelpers.h"
#include "radiolib/RXPowerSavingCLI.h"
#include <RTClib.h>

#ifndef BRIDGE_MAX_BAUD
Expand Down Expand Up @@ -585,6 +586,13 @@ void CommonCLI::handleSetCmd(uint32_t sender_timestamp, char* command, char* rep
} else {
strcpy(reply, "Error: state must be on or off");
}
} else if (strncmp(config, "radio.rxps.rfrx_disabled ", 25) == 0) {
RXPowerSavingCLI::setRfRxDisabled(&config[25], _rxps_control, reply, 160);
} else if (memcmp(config, "radio.rxps ", 11) == 0) {
if (RXPowerSavingCLI::set(&config[11], _prefs->sf, _prefs->bw, &_prefs->rxps,
_rxps_control, reply, 160)) {
savePrefs();
}
} else if (memcmp(config, "radio ", 6) == 0) {
strcpy(tmp, &config[6]);
const char *parts[4];
Expand All @@ -598,8 +606,11 @@ void CommonCLI::handleSetCmd(uint32_t sender_timestamp, char* command, char* rep
_prefs->cr = cr;
_prefs->freq = freq;
_prefs->bw = bw;
bool rxps_retuned = recalcRxPowerSavingFromLevel(
_prefs->rxps.level, _prefs->sf, _prefs->bw, _prefs->rxps.preamble,
&_prefs->rxps.rx_us, &_prefs->rxps.sleep_us);
_callbacks->savePrefs();
strcpy(reply, "OK - reboot to apply");
strcpy(reply, rxps_retuned ? "OK - reboot to apply (rxps retuned)" : "OK - reboot to apply");
} else {
strcpy(reply, "Error, invalid radio params");
}
Expand Down Expand Up @@ -856,6 +867,10 @@ void CommonCLI::handleGetCmd(uint32_t sender_timestamp, char* command, char* rep
} else {
sprintf(reply, "> %s", _board->isLoRaFemPaGainEnabled() ? "on" : "off");
}
} else if (strcmp(config, "radio.rxps.rfrx_disabled") == 0) {
RXPowerSavingCLI::getRfRxDisabled(_rxps_control, reply, 160);
} else if (strcmp(config, "radio.rxps") == 0) {
RXPowerSavingCLI::get(&_prefs->rxps, _rxps_control, reply, 160);
} else if (memcmp(config, "radio", 5) == 0) {
char freq[16], bw[16];
strcpy(freq, StrHelper::ftoa(_prefs->freq));
Expand Down
15 changes: 13 additions & 2 deletions src/helpers/CommonCLI.h
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@
#include <helpers/ClientACL.h>
#include <helpers/RegionMap.h>
#include <helpers/ConfigSerializer.h>
#include <helpers/radiolib/RXPowerSaving.h>

#if defined(WITH_RS232_BRIDGE) || defined(WITH_ESPNOW_BRIDGE)
#define WITH_BRIDGE
Expand Down Expand Up @@ -70,6 +71,7 @@ class NodePrefs : public ConfigSerializer {
uint8_t loop_detect = 0;
uint8_t cad_enabled = 0; // hardware Channel Activity Detection before TX (boolean)
uint8_t extra_sf[4];
RxPowerSavingConfig rxps;

private:
class RadioPrefs : public ConfigSerializer {
Expand All @@ -93,6 +95,11 @@ class NodePrefs : public ConfigSerializer {
def("agc_int", _parent->agc_reset_interval);
def("hash_mode", _parent->path_hash_mode);
def("multi_ack", _parent->multi_acks);
def("rxps_en", _parent->rxps.enabled);
def("rxps_rx_us", _parent->rxps.rx_us);
def("rxps_sleep_us", _parent->rxps.sleep_us);
def("rxps_level", _parent->rxps.level);
def("rxps_preamble", _parent->rxps.preamble);
}
public:
RadioPrefs(NodePrefs* parent) : _parent(parent) { }
Expand Down Expand Up @@ -253,6 +260,7 @@ class CommonCLI {
mesh::RTCClock* _rtc;
NodePrefs* _prefs;
CommonCLICallbacks* _callbacks;
RxPowerSavingControl* _rxps_control;
mesh::MainBoard* _board;
SensorManager* _sensors;
RegionMap* _region_map;
Expand All @@ -268,8 +276,11 @@ class CommonCLI {
void handleSetCmd(uint32_t sender_timestamp, char* command, char* reply);

public:
CommonCLI(mesh::MainBoard& board, mesh::RTCClock& rtc, SensorManager& sensors, RegionMap& region_map, ClientACL& acl, NodePrefs* prefs, CommonCLICallbacks* callbacks)
: _board(&board), _rtc(&rtc), _sensors(&sensors), _region_map(&region_map), _acl(&acl), _prefs(prefs), _callbacks(callbacks) { }
CommonCLI(mesh::MainBoard& board, mesh::RTCClock& rtc, SensorManager& sensors,
RegionMap& region_map, ClientACL& acl, NodePrefs* prefs,
CommonCLICallbacks* callbacks, RxPowerSavingControl* rxps_control = nullptr)
: _rtc(&rtc), _prefs(prefs), _callbacks(callbacks), _rxps_control(rxps_control),
_board(&board), _sensors(&sensors), _region_map(&region_map), _acl(&acl) { }

void loadPrefs(FILESYSTEM* _fs);
bool savePrefs(FILESYSTEM* _fs);
Expand Down
Loading
Loading