Skip to content
Open
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
26 changes: 26 additions & 0 deletions docs/cli_commands.md
Original file line number Diff line number Diff line change
Expand Up @@ -277,6 +277,32 @@ This document provides an overview of CLI commands that can be sent to MeshCore

---

#### View or change RX duty-cycle power saving
**Usage:**
- `get radio.rxps`
- `get rxps.wd`
- `set radio.rxps off`
- `set radio.rxps on`
- `set radio.rxps conservative`
- `set radio.rxps balanced`
- `set radio.rxps <1-10>`
- `set radio.rxps level <1-10>`
- `set radio.rxps level <1-10> preamble <16|32>`
- `set radio.rxps <rx_us> <sleep_us>`

**Parameters:**
- `rx_us`, `sleep_us`: Receive and sleep durations in microseconds (`1000`-`30000000`).
- `level`: A power-saving level from `1` (most conservative) to `10` (least power saving).
- `preamble`: LoRa preamble length in symbols; `16` or `32`.

**Notes:**
- `get rxps.wd` reports the RXPS watchdog's soft and hard recovery counts.
- `on` and `conservative` select level `1` with a 16-symbol preamble; `balanced` selects level `5` with a 16-symbol preamble.
- Level-based settings automatically recalculate their timings when the spreading factor or bandwidth changes. Custom `<rx_us> <sleep_us>` timings remain fixed.
- The selected mode is applied immediately, persisted, and restored after reboot.

---

#### View or change the LoRa FEM receive-path gain state on supported boards
**Usage:**
- `get radio.fem.rxgain`
Expand Down
62 changes: 62 additions & 0 deletions examples/companion_radio/MyMesh.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -267,6 +267,62 @@ bool MyMesh::getCADEnabled() const {
return true; // hardware CAD before TX (no CLI toggle on companion; enabled by default)
}

#if defined(RXPS_FIXED_ENABLED) || defined(RXPS_FIXED_LEVEL) || defined(RXPS_FIXED_PREAMBLE)
#if !defined(RXPS_FIXED_ENABLED) || !defined(RXPS_FIXED_LEVEL) || !defined(RXPS_FIXED_PREAMBLE)
#error "RXPS_FIXED_ENABLED, RXPS_FIXED_LEVEL, and RXPS_FIXED_PREAMBLE must be defined together"
#endif
#if RXPS_FIXED_LEVEL < 1 || RXPS_FIXED_LEVEL > 10
#error "RXPS_FIXED_LEVEL must be between 1 and 10"
#endif
#if RXPS_FIXED_PREAMBLE != 16 && RXPS_FIXED_PREAMBLE != 32
#error "RXPS_FIXED_PREAMBLE must be 16 or 32"
#endif
#endif

#ifdef RXPS_FIXED_ENABLED
static uint32_t ceilPositiveFloat(float value) {
uint32_t rounded = (uint32_t)value;
return value > (float)rounded ? rounded + 1 : rounded;
}

static bool calcFixedRxPowerSaving(uint8_t sf, float bw, uint32_t* rx_us, uint32_t* sleep_us) {
if (RXPS_FIXED_LEVEL < 1 || RXPS_FIXED_LEVEL > 10 || sf < 5 || sf > 12 ||
bw <= 0.0f || (RXPS_FIXED_PREAMBLE != 16 && RXPS_FIXED_PREAMBLE != 32)) {
return false;
}

const float symbol_us = (1000.0f * (float)(1UL << sf)) / bw;
const float amount = (float)(RXPS_FIXED_LEVEL - 1) / 9.0f;
const float rx_start_symbols = RXPS_FIXED_PREAMBLE == 16 ? 12.0f : 16.0f;
const float sleep_start_symbols = RXPS_FIXED_PREAMBLE == 16 ? 2.0f : 15.0f;
const float rx_edge_symbols = 8.0f;
const float sleep_edge_symbols = (float)RXPS_FIXED_PREAMBLE + 4.25f - 8.0f;

const float rx_symbols = rx_start_symbols + amount * (rx_edge_symbols - rx_start_symbols);
const float sleep_symbols = sleep_start_symbols + amount * (sleep_edge_symbols - sleep_start_symbols);

*rx_us = ceilPositiveFloat(rx_symbols * symbol_us);
*sleep_us = (uint32_t)(sleep_symbols * symbol_us);
return true;
}

static void applyFixedRxPowerSaving(uint8_t sf, float bw) {
uint32_t rx_us, sleep_us;
if (!calcFixedRxPowerSaving(sf, bw, &rx_us, &sleep_us)) {
MESH_DEBUG_PRINTLN("RX Power Saving fixed profile invalid");
return;
}

bool ok = radio_driver.setRxPowerSaving(true, rx_us, sleep_us);
MESH_DEBUG_PRINTLN("RX Power Saving fixed level %d p%d: %s (%lu/%lu us)",
RXPS_FIXED_LEVEL,
RXPS_FIXED_PREAMBLE,
ok ? "Enabled" : "Unsupported",
(unsigned long)rx_us,
(unsigned long)sleep_us);
}
#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 @@ -972,6 +1028,9 @@ void MyMesh::begin(bool has_display) {
radio_driver.setTxPower(_prefs.tx_power_dbm);
radio_driver.setRxBoostedGainMode(_prefs.rx_boosted_gain);
board.setLoRaFemLnaEnabled(_prefs.radio_fem_rxgain);
#ifdef RXPS_FIXED_ENABLED
applyFixedRxPowerSaving(_prefs.sf, _prefs.bw);
#endif
MESH_DEBUG_PRINTLN("RX Boosted Gain Mode: %s",
radio_driver.getRxBoostedGainMode() ? "Enabled" : "Disabled");
}
Expand Down Expand Up @@ -1397,6 +1456,9 @@ void MyMesh::handleCmdFrame(size_t len) {
savePrefs();

radio_driver.setParams(_prefs.freq, _prefs.bw, _prefs.sf, _prefs.cr);
#ifdef RXPS_FIXED_ENABLED
applyFixedRxPowerSaving(_prefs.sf, _prefs.bw);
#endif
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
20 changes: 20 additions & 0 deletions examples/simple_repeater/MyMesh.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -896,6 +896,8 @@ MyMesh::MyMesh(mesh::MainBoard &board, mesh::Radio &radio, mesh::MillisecondCloc
_prefs.flood_max_advert = 8;
_prefs.interference_threshold = 0; // disabled
_prefs.cad_enabled = 0; // hardware CAD before TX (off by default; 'set cad on')
_prefs.rx_ps_rx_us = RX_POWERSAVING_DEFAULT_RX_US;
_prefs.rx_ps_sleep_us = RX_POWERSAVING_DEFAULT_SLEEP_US;

// bridge defaults
_prefs.bridge_enabled = 1; // enabled
Expand Down Expand Up @@ -970,6 +972,7 @@ void MyMesh::begin(FILESYSTEM *fs) {
MESH_DEBUG_PRINTLN("RX Boosted Gain Mode: %s",
radio_driver.getRxBoostedGainMode() ? "Enabled" : "Disabled");
board.setLoRaFemLnaEnabled(_prefs.radio_fem_rxgain);
setRxPowerSaving(_prefs.rx_powersaving_enabled, _prefs.rx_ps_rx_us, _prefs.rx_ps_sleep_us);

updateAdvertTimer();
updateFloodAdvertTimer();
Expand Down Expand Up @@ -1064,6 +1067,21 @@ void MyMesh::setTxPower(int8_t power_dbm) {
radio_driver.setTxPower(power_dbm);
}

bool MyMesh::setRxPowerSaving(bool enable, uint32_t rx_us, uint32_t sleep_us) {
bool ok = radio_driver.setRxPowerSaving(enable, rx_us, sleep_us);
MESH_DEBUG_PRINTLN("RX Power Saving: %s (%lu/%lu us)%s",
enable ? "Enabled" : "Disabled",
(unsigned long)rx_us,
(unsigned long)sleep_us,
ok ? "" : " unsupported");
return ok;
}
void MyMesh::getRxPsWatchdogCounts(uint32_t* soft, uint32_t* hard) {
*soft = radio_driver.getRxPsWatchdogSoftCount();
*hard = radio_driver.getRxPsWatchdogHardCount();
}


#if defined(USE_SX1262) || defined(USE_SX1268)
void MyMesh::setRxBoostedGain(bool enable) {
radio_driver.setRxBoostedGainMode(enable);
Expand Down Expand Up @@ -1446,5 +1464,7 @@ bool MyMesh::hasPendingWork() const {
#if defined(WITH_BRIDGE)
if (bridge.isRunning()) return true; // bridge needs WiFi radio, can't sleep
#endif
if (radio_driver.isWatchdogObserving()) return true; // keep MCU awake for one radio duty cycle
if (radio_driver.isCalibratingNoiseFloor()) return true; // keep MCU awake for the noise-floor window
return _mgr->getOutboundTotal() > 0;
}
2 changes: 2 additions & 0 deletions examples/simple_repeater/MyMesh.h
Original file line number Diff line number Diff line change
Expand Up @@ -219,6 +219,8 @@ class MyMesh : public mesh::Mesh, public CommonCLICallbacks {

void dumpLogFile() override;
void setTxPower(int8_t power_dbm) override;
bool setRxPowerSaving(bool enable, uint32_t rx_us, uint32_t sleep_us) override;
void getRxPsWatchdogCounts(uint32_t* soft, uint32_t* hard) override;
void formatNeighborsReply(char *reply) override;
void removeNeighbor(const uint8_t* pubkey, int key_len) override;
void formatStatsReply(char *reply) override;
Expand Down
14 changes: 14 additions & 0 deletions examples/simple_room_server/MyMesh.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -651,6 +651,8 @@ MyMesh::MyMesh(mesh::MainBoard &board, mesh::Radio &radio, mesh::MillisecondCloc
_prefs.flood_max_advert = 8;
_prefs.interference_threshold = 0; // disabled
_prefs.cad_enabled = 0; // hardware CAD before TX (off by default; 'set cad on')
_prefs.rx_ps_rx_us = RX_POWERSAVING_DEFAULT_RX_US;
_prefs.rx_ps_sleep_us = RX_POWERSAVING_DEFAULT_SLEEP_US;
#ifdef ROOM_PASSWORD
StrHelper::strncpy(_prefs.guest_password, ROOM_PASSWORD, sizeof(_prefs.guest_password));
#endif
Expand Down Expand Up @@ -702,6 +704,7 @@ void MyMesh::begin(FILESYSTEM *fs) {
radio_driver.setParams(_prefs.freq, _prefs.bw, _prefs.sf, _prefs.cr);
radio_driver.setTxPower(_prefs.tx_power_dbm);
board.setLoRaFemLnaEnabled(_prefs.radio_fem_rxgain);
setRxPowerSaving(_prefs.rx_powersaving_enabled, _prefs.rx_ps_rx_us, _prefs.rx_ps_sleep_us);

updateAdvertTimer();
updateFloodAdvertTimer();
Expand Down Expand Up @@ -808,6 +811,15 @@ void MyMesh::setTxPower(int8_t power_dbm) {
radio_driver.setTxPower(power_dbm);
}

bool MyMesh::setRxPowerSaving(bool enable, uint32_t rx_us, uint32_t sleep_us) {
return radio_driver.setRxPowerSaving(enable, rx_us, sleep_us);
}
void MyMesh::getRxPsWatchdogCounts(uint32_t* soft, uint32_t* hard) {
*soft = radio_driver.getRxPsWatchdogSoftCount();
*hard = radio_driver.getRxPsWatchdogHardCount();
}


void MyMesh::saveIdentity(const mesh::LocalIdentity &new_id) {
#if defined(NRF52_PLATFORM) || defined(STM32_PLATFORM)
IdentityStore store(*_fs, "");
Expand Down Expand Up @@ -1037,5 +1049,7 @@ bool MyMesh::hasPendingWork() const {
#if defined(WITH_BRIDGE)
if (bridge.isRunning()) return true; // bridge needs WiFi radio, can't sleep
#endif
if (radio_driver.isWatchdogObserving()) return true; // keep MCU awake for one radio duty cycle
if (radio_driver.isCalibratingNoiseFloor()) return true; // keep MCU awake for the noise-floor window
return _mgr->getOutboundTotal() > 0;
}
2 changes: 2 additions & 0 deletions examples/simple_room_server/MyMesh.h
Original file line number Diff line number Diff line change
Expand Up @@ -206,6 +206,8 @@ class MyMesh : public mesh::Mesh, public CommonCLICallbacks {

void dumpLogFile() override;
void setTxPower(int8_t power_dbm) override;
bool setRxPowerSaving(bool enable, uint32_t rx_us, uint32_t sleep_us) override;
void getRxPsWatchdogCounts(uint32_t* soft, uint32_t* hard) override;

void formatNeighborsReply(char *reply) override {
strcpy(reply, "not supported");
Expand Down
12 changes: 12 additions & 0 deletions examples/simple_sensor/SensorMesh.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -730,6 +730,8 @@ SensorMesh::SensorMesh(mesh::MainBoard& board, mesh::Radio& radio, mesh::Millise
_prefs.flood_max = 64;
_prefs.interference_threshold = 0; // disabled
_prefs.cad_enabled = 0; // hardware CAD before TX (off by default; 'set cad on')
_prefs.rx_ps_rx_us = RX_POWERSAVING_DEFAULT_RX_US;
_prefs.rx_ps_sleep_us = RX_POWERSAVING_DEFAULT_SLEEP_US;

// GPS defaults
_prefs.gps_enabled = 0;
Expand Down Expand Up @@ -770,6 +772,7 @@ void SensorMesh::begin(FILESYSTEM* fs) {

radio_driver.setParams(_prefs.freq, _prefs.bw, _prefs.sf, _prefs.cr);
radio_driver.setTxPower(_prefs.tx_power_dbm);
setRxPowerSaving(_prefs.rx_powersaving_enabled, _prefs.rx_ps_rx_us, _prefs.rx_ps_sleep_us);

updateAdvertTimer();
updateFloodAdvertTimer();
Expand Down Expand Up @@ -849,6 +852,15 @@ void SensorMesh::setTxPower(int8_t power_dbm) {
radio_driver.setTxPower(power_dbm);
}

bool SensorMesh::setRxPowerSaving(bool enable, uint32_t rx_us, uint32_t sleep_us) {
return radio_driver.setRxPowerSaving(enable, rx_us, sleep_us);
}
void SensorMesh::getRxPsWatchdogCounts(uint32_t* soft, uint32_t* hard) {
*soft = radio_driver.getRxPsWatchdogSoftCount();
*hard = radio_driver.getRxPsWatchdogHardCount();
}


void SensorMesh::formatStatsReply(char *reply) {
StatsFormatHelper::formatCoreStats(reply, board, *_ms, _err_flags, _mgr);
}
Expand Down
2 changes: 2 additions & 0 deletions examples/simple_sensor/SensorMesh.h
Original file line number Diff line number Diff line change
Expand Up @@ -68,6 +68,8 @@ class SensorMesh : public mesh::Mesh, public CommonCLICallbacks {
void eraseLogFile() override { }
void dumpLogFile() override { }
void setTxPower(int8_t power_dbm) override;
bool setRxPowerSaving(bool enable, uint32_t rx_us, uint32_t sleep_us) override;
void getRxPsWatchdogCounts(uint32_t* soft, uint32_t* hard) override;
void formatNeighborsReply(char *reply) override {
strcpy(reply, "not supported");
}
Expand Down
8 changes: 8 additions & 0 deletions platformio.ini
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,14 @@ extra_configs =
variants/*/platformio.ini
platformio.local.ini

[companion_rxps]
custom_rxps_level = 5
custom_rxps_preamble = 16
build_flags =
-D RXPS_FIXED_ENABLED=1
-D RXPS_FIXED_LEVEL=${companion_rxps.custom_rxps_level}
-D RXPS_FIXED_PREAMBLE=${companion_rxps.custom_rxps_preamble}

[arduino_base]
framework = arduino
monitor_speed = 115200
Expand Down
15 changes: 10 additions & 5 deletions src/Dispatcher.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -191,20 +191,24 @@ bool Dispatcher::tryParsePacket(Packet* pkt, const uint8_t* raw, int len) {
void Dispatcher::checkRecv() {
Packet* pkt;
float score;
float snr;
float rssi;
uint32_t air_time;
{
uint8_t raw[MAX_TRANS_UNIT+1];
int len = _radio->recvRaw(raw, MAX_TRANS_UNIT);
if (len > 0) {
logRxRaw(_radio->getLastSNR(), _radio->getLastRSSI(), raw, len);
snr = _radio->getLastSNR();
rssi = _radio->getLastRSSI();
logRxRaw(snr, rssi, raw, len);

pkt = _mgr->allocNew();
if (pkt == NULL) {
MESH_DEBUG_PRINTLN("%s Dispatcher::checkRecv(): WARNING: received data, no unused packets available!", getLogDateTime());
} else {
if (tryParsePacket(pkt, raw, len)) {
pkt->_snr = _radio->getLastSNR() * 4.0f;
score = _radio->packetScore(_radio->getLastSNR(), len);
pkt->_snr = snr * 4.0f;
score = _radio->packetScore(snr, len);
air_time = _radio->getEstAirtimeFor(len);
rx_air_time += air_time;
} else {
Expand All @@ -221,7 +225,7 @@ void Dispatcher::checkRecv() {
Serial.print(getLogDateTime());
Serial.printf(": RX, len=%d (type=%d, route=%s, payload_len=%d) SNR=%d RSSI=%d score=%d time=%d",
pkt->getRawLength(), pkt->getPayloadType(), pkt->isRouteDirect() ? "D" : "F", pkt->payload_len,
(int)pkt->getSNR(), (int)_radio->getLastRSSI(), (int)(score*1000), air_time);
(int)pkt->getSNR(), (int)rssi, (int)(score*1000), air_time);

static uint8_t packet_hash[MAX_HASH_SIZE];
pkt->calculatePacketHash(packet_hash);
Expand Down Expand Up @@ -256,6 +260,7 @@ void Dispatcher::checkRecv() {
processRecvPacket(pkt);
}
}
_radio->onReceiveProcessed();
}

void Dispatcher::processRecvPacket(Packet* pkt) {
Expand Down Expand Up @@ -387,4 +392,4 @@ unsigned long Dispatcher::futureMillis(int millis_from_now) const {
return _ms->getMillis() + millis_from_now;
}

}
}
6 changes: 6 additions & 0 deletions src/Dispatcher.h
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,9 @@ class Radio {
*/
virtual int recvRaw(uint8_t* bytes, int sz) = 0;

/** Called after the received packet and its radio metadata have been processed. */
virtual void onReceiveProcessed() { }

/**
* \returns estimated transmit air-time needed for packet of 'len_bytes', in milliseconds.
*/
Expand Down Expand Up @@ -71,6 +74,9 @@ class Radio {

virtual bool isInRecvMode() const = 0;

virtual bool supportsRxPowerSaving() const { return false; }
virtual bool setRxPowerSaving(bool enabled, uint32_t rx_us, uint32_t sleep_us) { return !enabled; }

/**
* \returns true if the radio is currently mid-receive of a packet.
*/
Expand Down
Loading