Added RX duty-cycle power saving support

This commit is contained in:
Jarosław Domański committed 2026-07-08 08:49:37 +02:00
1 parent 46c729b2e9
commit e97f0fecc6
15 files changed
+419 -14

No files matched your search

+50
View File
@@ -267,6 +267,50 @@ bool MyMesh::getCADEnabled() const {
return true; // hardware CAD before TX (no CLI toggle on companion; enabled by default)
}
#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);
@@ -972,6 +1016,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");
}
@@ -1397,6 +1444,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);
+13
View File
@@ -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
@@ -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();
@@ -1064,6 +1067,16 @@ 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;
}
#if defined(USE_SX1262) || defined(USE_SX1268)
void MyMesh::setRxBoostedGain(bool enable) {
radio_driver.setRxBoostedGainMode(enable);
+1
View File
@@ -212,6 +212,7 @@ public:
void dumpLogFile() override;
void setTxPower(int8_t power_dbm) override;
bool setRxPowerSaving(bool enable, uint32_t rx_us, uint32_t sleep_us) override;
void formatNeighborsReply(char *reply) override;
void removeNeighbor(const uint8_t* pubkey, int key_len) override;
void formatStatsReply(char *reply) override;
+7
View File
@@ -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
@@ -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();
@@ -808,6 +811,10 @@ 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::saveIdentity(const mesh::LocalIdentity &new_id) {
#if defined(NRF52_PLATFORM) || defined(STM32_PLATFORM)
IdentityStore store(*_fs, "");
+1
View File
@@ -206,6 +206,7 @@ public:
void dumpLogFile() override;
void setTxPower(int8_t power_dbm) override;
bool setRxPowerSaving(bool enable, uint32_t rx_us, uint32_t sleep_us) override;
void formatNeighborsReply(char *reply) override {
strcpy(reply, "not supported");
+7
View File
@@ -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;
@@ -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();
@@ -849,6 +852,10 @@ 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::formatStatsReply(char *reply) {
StatsFormatHelper::formatCoreStats(reply, board, *_ms, _err_flags, _mgr);
}
+1
View File
@@ -68,6 +68,7 @@ public:
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 formatNeighborsReply(char *reply) override {
strcpy(reply, "not supported");
}
+3
View File
@@ -71,6 +71,9 @@ public:
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.
*/
+195 -3
View File
@@ -29,6 +29,80 @@ static bool isValidName(const char *n) {
return true;
}
static bool isValidRxPowerSavingPeriod(uint32_t us) {
return us >= RX_POWERSAVING_MIN_PERIOD_US && us <= RX_POWERSAVING_MAX_PERIOD_US;
}
// MeshCore preamble convention used for the RX powersaving timing calculation.
// Must stay in sync with RadioLibWrapper::preambleLengthForSF() (the value the
// radio actually transmits); kept local here because CommonCLI is radio-agnostic.
static uint16_t rxPowerSavingPreambleForSF(uint8_t sf) {
return sf <= 8 ? 32 : 16;
}
static bool isNumeric(const char* sp) {
if (!sp || !*sp) return false;
while (*sp) {
if (*sp < '0' || *sp > '9') return false;
sp++;
}
return true;
}
static uint32_t ceilPositiveFloat(float value) {
uint32_t rounded = (uint32_t)value;
return value > (float)rounded ? rounded + 1 : rounded;
}
static bool calcRxPowerSavingLevel(uint32_t level, uint8_t sf, float bw, uint32_t preamble,
uint32_t* rx_us, uint32_t* sleep_us) {
if (level < 1 || level > 10 || sf < 5 || sf > 12 || bw <= 0.0f || (preamble != 16 && preamble != 32)) {
return false;
}
const float symbol_us = (1000.0f * (float)(1UL << sf)) / bw;
const float amount = (float)(level - 1) / 9.0f;
const float rx_start_symbols = preamble == 16 ? 12.0f : 16.0f;
const float sleep_start_symbols = preamble == 16 ? 2.0f : 15.0f;
const float rx_edge_symbols = 8.0f;
const float sleep_edge_symbols = (float)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 ensureRxPowerSavingDefaults(NodePrefs* prefs) {
if (!isValidRxPowerSavingPeriod(prefs->rx_ps_rx_us)) {
prefs->rx_ps_rx_us = RX_POWERSAVING_DEFAULT_RX_US;
}
if (!isValidRxPowerSavingPeriod(prefs->rx_ps_sleep_us)) {
prefs->rx_ps_sleep_us = RX_POWERSAVING_DEFAULT_SLEEP_US;
}
}
// Recomputes rx_ps_rx_us/rx_ps_sleep_us from the stored level and the current
// radio SF/BW. No-op (returns false) for manual timings (rx_ps_level == 0).
// Lets level-based RX powersaving auto-retune when SF/BW change.
static bool recalcRxPowerSavingFromLevel(NodePrefs* prefs) {
if (prefs->rx_ps_level < 1 || prefs->rx_ps_level > 10) return false; // manual: nothing to recompute
uint32_t preamble = prefs->rx_ps_preamble ? prefs->rx_ps_preamble
: rxPowerSavingPreambleForSF(prefs->sf);
uint32_t rx_us, sleep_us;
if (!calcRxPowerSavingLevel(prefs->rx_ps_level, prefs->sf, prefs->bw, preamble, &rx_us, &sleep_us)) {
return false;
}
if (!isValidRxPowerSavingPeriod(rx_us) || !isValidRxPowerSavingPeriod(sleep_us)) {
return false;
}
prefs->rx_ps_rx_us = rx_us;
prefs->rx_ps_sleep_us = sleep_us;
return true;
}
void CommonCLI::loadPrefs(FILESYSTEM* fs) {
if (fs->exists("/com_prefs")) {
loadPrefsInt(fs, "/com_prefs"); // new filename
@@ -96,7 +170,12 @@ void CommonCLI::loadPrefsInt(FILESYSTEM* fs, const char* filename) {
file.read((uint8_t *)&_prefs->flood_max_advert, sizeof(_prefs->flood_max_advert)); // 292
file.read((uint8_t *)&_prefs->radio_fem_rxgain, sizeof(_prefs->radio_fem_rxgain)); // 293
file.read((uint8_t *)&_prefs->cad_enabled, sizeof(_prefs->cad_enabled)); // 294
// next: 295
file.read((uint8_t *)&_prefs->rx_powersaving_enabled, sizeof(_prefs->rx_powersaving_enabled)); // 295
file.read((uint8_t *)&_prefs->rx_ps_rx_us, sizeof(_prefs->rx_ps_rx_us)); // 296
file.read((uint8_t *)&_prefs->rx_ps_sleep_us, sizeof(_prefs->rx_ps_sleep_us)); // 300
file.read((uint8_t *)&_prefs->rx_ps_level, sizeof(_prefs->rx_ps_level)); // 304
file.read((uint8_t *)&_prefs->rx_ps_preamble, sizeof(_prefs->rx_ps_preamble)); // 305
// next: 306
// sanitise bad pref values
_prefs->rx_delay_base = constrain(_prefs->rx_delay_base, 0, 20.0f);
@@ -129,6 +208,13 @@ void CommonCLI::loadPrefsInt(FILESYSTEM* fs, const char* filename) {
_prefs->rx_boosted_gain = constrain(_prefs->rx_boosted_gain, 0, 1); // boolean
_prefs->radio_fem_rxgain = constrain(_prefs->radio_fem_rxgain, 0, 1); // boolean
_prefs->cad_enabled = constrain(_prefs->cad_enabled, 0, 1); // boolean
_prefs->rx_powersaving_enabled = constrain(_prefs->rx_powersaving_enabled, 0, 1);
_prefs->rx_ps_level = constrain(_prefs->rx_ps_level, 0, 10);
if (_prefs->rx_ps_preamble != 16 && _prefs->rx_ps_preamble != 32) {
_prefs->rx_ps_preamble = 0; // 0 = auto (derive from SF)
}
ensureRxPowerSavingDefaults(_prefs);
recalcRxPowerSavingFromLevel(_prefs); // retune level-based timings to the loaded SF/BW
file.close();
}
@@ -195,7 +281,12 @@ void CommonCLI::savePrefs(FILESYSTEM* fs) {
file.write((uint8_t *)&_prefs->flood_max_advert, sizeof(_prefs->flood_max_advert)); // 292
file.write((uint8_t *)&_prefs->radio_fem_rxgain, sizeof(_prefs->radio_fem_rxgain)); // 293
file.write((uint8_t *)&_prefs->cad_enabled, sizeof(_prefs->cad_enabled)); // 294
// next: 295
file.write((uint8_t *)&_prefs->rx_powersaving_enabled, sizeof(_prefs->rx_powersaving_enabled)); // 295
file.write((uint8_t *)&_prefs->rx_ps_rx_us, sizeof(_prefs->rx_ps_rx_us)); // 296
file.write((uint8_t *)&_prefs->rx_ps_sleep_us, sizeof(_prefs->rx_ps_sleep_us)); // 300
file.write((uint8_t *)&_prefs->rx_ps_level, sizeof(_prefs->rx_ps_level)); // 304
file.write((uint8_t *)&_prefs->rx_ps_preamble, sizeof(_prefs->rx_ps_preamble)); // 305
// next: 306
file.close();
}
@@ -635,6 +726,100 @@ void CommonCLI::handleSetCmd(uint32_t sender_timestamp, char* command, char* rep
} else {
strcpy(reply, "Error: state must be on or off");
}
} else if (memcmp(config, "radio.rxps ", 11) == 0) {
const char* value = &config[11];
uint8_t enable = _prefs->rx_powersaving_enabled;
uint32_t rx_us = _prefs->rx_ps_rx_us;
uint32_t sleep_us = _prefs->rx_ps_sleep_us;
uint32_t level = 0;
uint32_t preamble = rxPowerSavingPreambleForSF(_prefs->sf);
bool level_requested = false;
bool preamble_overridden = false;
ensureRxPowerSavingDefaults(_prefs);
rx_us = _prefs->rx_ps_rx_us;
sleep_us = _prefs->rx_ps_sleep_us;
if (strcmp(value, "off") == 0) {
enable = 0;
} else if (strcmp(value, "on") == 0 || strcmp(value, "conservative") == 0) {
enable = 1;
rx_us = RX_POWERSAVING_DEFAULT_RX_US;
sleep_us = RX_POWERSAVING_DEFAULT_SLEEP_US;
} else if (strcmp(value, "balanced") == 0) {
enable = 1;
rx_us = RX_POWERSAVING_DEFAULT_RX_US;
sleep_us = RX_POWERSAVING_BALANCED_SLEEP_US;
} else {
StrHelper::strncpy(tmp, value, sizeof(tmp));
const char *parts[4];
int num = mesh::Utils::parseTextParts(tmp, parts, 4, ' ');
if (num == 1 && isNumeric(parts[0])) {
level = _atoi(parts[0]);
level_requested = true;
enable = 1;
} else if (num == 2 && strcmp(parts[0], "level") == 0 && isNumeric(parts[1])) {
level = _atoi(parts[1]);
level_requested = true;
enable = 1;
} else if (num == 4 && strcmp(parts[0], "level") == 0 && isNumeric(parts[1]) &&
strcmp(parts[2], "preamble") == 0 && isNumeric(parts[3])) {
level = _atoi(parts[1]);
preamble = _atoi(parts[3]);
level_requested = true;
preamble_overridden = true;
enable = 1;
} else if (num == 2 && isNumeric(parts[0]) && isNumeric(parts[1])) {
rx_us = _atoi(parts[0]);
sleep_us = _atoi(parts[1]);
enable = 1;
} else {
strcpy(reply, "ERROR: use off|on|conservative|balanced|level <1-10>|<rx_us> <sleep_us>");
return;
}
if (level_requested && !calcRxPowerSavingLevel(level, _prefs->sf, _prefs->bw, preamble, &rx_us, &sleep_us)) {
strcpy(reply, "ERROR: level range is 1-10; preamble is 16 or 32");
return;
}
}
if (!isValidRxPowerSavingPeriod(rx_us) || !isValidRxPowerSavingPeriod(sleep_us)) {
sprintf(reply, "ERROR: range is %lu-%lu us",
(unsigned long)RX_POWERSAVING_MIN_PERIOD_US,
(unsigned long)RX_POWERSAVING_MAX_PERIOD_US);
return;
}
if (!_callbacks->setRxPowerSaving(enable, rx_us, sleep_us)) {
strcpy(reply, "ERROR: RX powersaving unsupported");
return;
}
_prefs->rx_powersaving_enabled = enable;
_prefs->rx_ps_rx_us = rx_us;
_prefs->rx_ps_sleep_us = sleep_us;
if (level_requested) {
// Remember the intent so the timings can auto-retune when SF/BW change.
_prefs->rx_ps_level = level;
_prefs->rx_ps_preamble = preamble_overridden ? preamble : 0; // 0 = auto (derive from SF)
} else if (strcmp(value, "off") != 0) {
// on/conservative/balanced/manual timings are fixed, not level-derived.
_prefs->rx_ps_level = 0;
_prefs->rx_ps_preamble = 0;
}
savePrefs();
if (level_requested) {
sprintf(reply, "OK - level %lu,%s,%lu,%lu,preamble=%lu",
(unsigned long)level,
enable ? "on" : "off",
(unsigned long)rx_us,
(unsigned long)sleep_us,
(unsigned long)preamble);
} else {
sprintf(reply, "OK - %s,%lu,%lu", enable ? "on" : "off",
(unsigned long)rx_us, (unsigned long)sleep_us);
}
} else if (memcmp(config, "radio ", 6) == 0) {
strcpy(tmp, &config[6]);
const char *parts[4];
@@ -648,8 +833,11 @@ void CommonCLI::handleSetCmd(uint32_t sender_timestamp, char* command, char* rep
_prefs->cr = cr;
_prefs->freq = freq;
_prefs->bw = bw;
// Retune level-based RX powersaving to the new SF/BW. Persist only; the
// radio itself is "reboot to apply", and begin() re-arms the timings then.
bool rxps_retuned = recalcRxPowerSavingFromLevel(_prefs);
_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");
}
@@ -893,6 +1081,10 @@ void CommonCLI::handleGetCmd(uint32_t sender_timestamp, char* command, char* rep
} else {
sprintf(reply, "> %s", _board->isLoRaFemLnaEnabled() ? "on" : "off");
}
} else if (memcmp(config, "radio.rxps", 10) == 0) {
ensureRxPowerSavingDefaults(_prefs);
sprintf(reply, "> %s,%lu,%lu", _prefs->rx_powersaving_enabled ? "on" : "off",
(unsigned long)_prefs->rx_ps_rx_us, (unsigned long)_prefs->rx_ps_sleep_us);
} else if (memcmp(config, "radio", 5) == 0) {
char freq[16], bw[16];
strcpy(freq, StrHelper::ftoa(_prefs->freq));
+15
View File
@@ -19,6 +19,12 @@
#define LOOP_DETECT_MODERATE 2
#define LOOP_DETECT_STRICT 3
#define RX_POWERSAVING_DEFAULT_RX_US 65625UL
#define RX_POWERSAVING_DEFAULT_SLEEP_US 60000UL
#define RX_POWERSAVING_BALANCED_SLEEP_US 80000UL
#define RX_POWERSAVING_MIN_PERIOD_US 1000UL
#define RX_POWERSAVING_MAX_PERIOD_US 30000000UL
struct NodePrefs { // persisted to file
float airtime_factor;
char node_name[32];
@@ -66,6 +72,11 @@ struct NodePrefs { // persisted to file
uint8_t path_hash_mode; // which path mode to use when sending
uint8_t loop_detect;
uint8_t cad_enabled; // hardware Channel Activity Detection before TX (boolean)
uint8_t rx_powersaving_enabled; // boolean
uint32_t rx_ps_rx_us;
uint32_t rx_ps_sleep_us;
uint8_t rx_ps_level; // 0 = manual/explicit us timings; 1..10 = level-derived (auto-retunes on SF/BW change)
uint8_t rx_ps_preamble; // 0 = auto (derive from SF); else 16 or 32 = explicit override for level calc
};
class CommonCLICallbacks {
@@ -115,6 +126,10 @@ public:
virtual void setRxBoostedGain(bool enable) {
// no op by default
};
virtual bool setRxPowerSaving(bool enable, uint32_t rx_us, uint32_t sleep_us) {
return !enable;
};
};
class CommonCLI {
+35 -1
View File
@@ -24,6 +24,40 @@ class CustomLR1110 : public LR1110 {
float getFreqMHz() const { return freqMHz; }
int16_t startReceiveDutyCycle(uint32_t rxPeriod, uint32_t sleepPeriod,
RadioLibIrqFlags_t irqFlags = RADIOLIB_IRQ_RX_DEFAULT_FLAGS,
RadioLibIrqFlags_t irqMask = RADIOLIB_IRQ_RX_DEFAULT_MASK) {
// RadioLib's LR11x0 duty-cycle path stages RX but does not call
// launchMode(), where the software RF switch is normally set to RX.
uint32_t transitionTime = this->tcxoDelay + 1000;
sleepPeriod -= transitionTime;
uint32_t rxPeriodRaw = (rxPeriod * 32768UL) / 1000000UL;
uint32_t sleepPeriodRaw = (sleepPeriod * 32768UL) / 1000000UL;
if ((rxPeriodRaw & 0xFF000000) || (rxPeriodRaw == 0)) {
return RADIOLIB_ERR_INVALID_RX_PERIOD;
}
if ((sleepPeriodRaw & 0xFF000000) || (sleepPeriodRaw == 0)) {
return RADIOLIB_ERR_INVALID_SLEEP_PERIOD;
}
RadioModeConfig_t cfg = {
.receive = {
.timeout = RADIOLIB_LR11X0_RX_TIMEOUT_INF,
.irqFlags = irqFlags,
.irqMask = irqMask,
.len = 0,
}
};
int16_t state = this->stageMode(RADIOLIB_RADIO_MODE_RX, &cfg);
RADIOLIB_ASSERT(state);
this->mod->setRfSwitchState(Module::MODE_RX);
return this->setRxDutyCycle(rxPeriodRaw, sleepPeriodRaw, RADIOLIB_LR11X0_RX_DUTY_CYCLE_MODE_RX);
}
int16_t setRxBoostedGainMode(bool en) {
_rx_boosted = en;
return LR1110::setRxBoostedGainMode(en);
@@ -38,4 +72,4 @@ class CustomLR1110 : public LR1110 {
}
uint8_t getSpreadingFactor() const { return spreadingFactor; }
};
};
@@ -31,6 +31,27 @@ public:
_radio->setPreambleLength(preambleLengthForSF(getSpreadingFactor())); // overcomes weird issues with small and big pkts
}
bool supportsRxPowerSaving() const override { return true; }
protected:
int startReceiveMode() override {
if (!_rx_ps_enabled) {
return _radio->startReceive();
}
const RadioLibIrqFlags_t irqFlags = RADIOLIB_IRQ_RX_DEFAULT_FLAGS;
const RadioLibIrqFlags_t irqMask = RADIOLIB_IRQ_RX_DEFAULT_MASK;
int err = ((CustomLR1110 *)_radio)->startReceiveDutyCycle(_rx_ps_rx_us, _rx_ps_sleep_us, irqFlags, irqMask);
if (err == RADIOLIB_ERR_NONE) {
return err;
}
MESH_DEBUG_PRINTLN("CustomLR1110Wrapper: error: startReceiveDutyCycle(%d), falling back to continuous RX", err);
return _radio->startReceive();
}
public:
float getLastRSSI() const override { return ((CustomLR1110 *)_radio)->getRSSI(); }
float getLastSNR() const override { return ((CustomLR1110 *)_radio)->getSNR(); }
@@ -38,6 +38,31 @@ public:
((CustomSX1262 *)_radio)->sleep(false);
}
bool supportsRxPowerSaving() const override { return true; }
protected:
int startReceiveMode() override {
if (!_rx_ps_enabled) {
return _radio->startReceive();
}
const RadioLibIrqFlags_t irqFlags = RADIOLIB_IRQ_RX_DEFAULT_FLAGS;
const RadioLibIrqFlags_t irqMask =
(1UL << RADIOLIB_IRQ_RX_DONE) |
(1UL << RADIOLIB_IRQ_TIMEOUT) |
(1UL << RADIOLIB_IRQ_CRC_ERR) |
(1UL << RADIOLIB_IRQ_HEADER_ERR);
int err = ((CustomSX1262 *)_radio)->startReceiveDutyCycle(_rx_ps_rx_us, _rx_ps_sleep_us, irqFlags, irqMask);
if (err == RADIOLIB_ERR_NONE) {
return err;
}
MESH_DEBUG_PRINTLN("CustomSX1262Wrapper: error: startReceiveDutyCycle(%d), falling back to continuous RX", err);
return _radio->startReceive();
}
public:
void doResetAGC() override { sx126xResetAGC((SX126x *)_radio); }
void setRxBoostedGainMode(bool en) override {
+30 -9
View File
@@ -85,7 +85,11 @@ void RadioLibWrapper::resetAGC() {
}
void RadioLibWrapper::loop() {
if (state == STATE_RX && _num_floor_samples < NUM_NOISE_FLOOR_SAMPLES) {
// In RX duty-cycle (powersaving) mode the radio auto-sleeps between listen
// windows, so getCurrentRSSI() would read a sleeping frontend and corrupt the
// noise floor. Skip adaptive sampling; note this also degrades interference
// detection (isChannelActive) while powersaving is enabled.
if (!_rx_ps_enabled && state == STATE_RX && _num_floor_samples < NUM_NOISE_FLOOR_SAMPLES) {
if (!isReceivingPacket()) {
int rssi = getCurrentRSSI();
if (rssi < _noise_floor + SAMPLING_THRESHOLD) { // only consider samples below current floor + sampling THRESHOLD
@@ -105,18 +109,40 @@ void RadioLibWrapper::loop() {
}
void RadioLibWrapper::startRecv() {
int err = _radio->startReceive();
int err = startReceiveMode();
if (err == RADIOLIB_ERR_NONE) {
state = STATE_RX;
} else {
MESH_DEBUG_PRINTLN("RadioLibWrapper: error: startReceive(%d)", err);
MESH_DEBUG_PRINTLN("RadioLibWrapper: error: startReceiveMode(%d)", err);
}
}
int RadioLibWrapper::startReceiveMode() {
return _radio->startReceive();
}
bool RadioLibWrapper::isInRecvMode() const {
return (state & ~STATE_INT_READY) == STATE_RX;
}
bool RadioLibWrapper::setRxPowerSaving(bool enabled, uint32_t rx_us, uint32_t sleep_us) {
if (enabled && !supportsRxPowerSaving()) {
return false;
}
_rx_ps_enabled = enabled;
_rx_ps_rx_us = rx_us;
_rx_ps_sleep_us = sleep_us;
// Force the next recvRaw() to arm the requested RX mode, but don't clobber a
// completed-but-unread packet (STATE_INT_READY): recvRaw() will consume it and
// then re-arm with the new mode. Also leave an in-flight TX alone. (Same
// non-atomic guard style as resetAGC().)
if ((state & STATE_INT_READY) == 0 && (state & ~STATE_INT_READY) != STATE_TX_WAIT) {
state = STATE_IDLE;
}
return true;
}
int RadioLibWrapper::recvRaw(uint8_t* bytes, int sz) {
int len = 0;
if (state & STATE_INT_READY) {
@@ -137,12 +163,7 @@ int RadioLibWrapper::recvRaw(uint8_t* bytes, int sz) {
}
if (state != STATE_RX) {
int err = _radio->startReceive();
if (err == RADIOLIB_ERR_NONE) {
state = STATE_RX;
} else {
MESH_DEBUG_PRINTLN("RadioLibWrapper: error: startReceive(%d)", err);
}
startRecv();
}
return len;
}
+15 -1
View File
@@ -3,6 +3,13 @@
#include <Mesh.h>
#include <RadioLib.h>
// Fallback RX powersaving timings, only used until setRxPowerSaving() is called
// (begin() always applies the persisted values). The authoritative defaults live
// in CommonCLI.h as RX_POWERSAVING_DEFAULT_RX_US / _SLEEP_US and are delivered
// via NodePrefs; keep these mirrored.
#define RX_PS_FALLBACK_RX_US 65625UL
#define RX_PS_FALLBACK_SLEEP_US 60000UL
class RadioLibWrapper : public mesh::Radio {
protected:
PhysicalLayer* _radio;
@@ -13,15 +20,21 @@ protected:
uint16_t _num_floor_samples;
int32_t _floor_sample_sum;
uint8_t _preamble_sf;
bool _rx_ps_enabled;
uint32_t _rx_ps_rx_us;
uint32_t _rx_ps_sleep_us;
void idle();
void startRecv();
virtual int startReceiveMode();
float packetScoreInt(float snr, int sf, int packet_len);
virtual bool isReceivingPacket() =0;
virtual void doResetAGC();
public:
RadioLibWrapper(PhysicalLayer& radio, mesh::MainBoard& board) : _radio(&radio), _board(&board), _preamble_sf(0) { n_recv = n_sent = 0; }
RadioLibWrapper(PhysicalLayer& radio, mesh::MainBoard& board)
: _radio(&radio), _board(&board), _preamble_sf(0), _rx_ps_enabled(false),
_rx_ps_rx_us(RX_PS_FALLBACK_RX_US), _rx_ps_sleep_us(RX_PS_FALLBACK_SLEEP_US) { n_recv = n_sent = n_recv_errors = 0; }
void begin() override;
virtual void powerOff() { _radio->sleep(); }
@@ -31,6 +44,7 @@ public:
bool isSendComplete() override;
void onSendFinished() override;
bool isInRecvMode() const override;
bool setRxPowerSaving(bool enabled, uint32_t rx_us, uint32_t sleep_us) override;
bool isChannelActive();
bool isReceiving() override {