diff --git a/examples/companion_radio/MyMesh.cpp b/examples/companion_radio/MyMesh.cpp index 10578511..c514b04c 100644 --- a/examples/companion_radio/MyMesh.cpp +++ b/examples/companion_radio/MyMesh.cpp @@ -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); diff --git a/examples/simple_repeater/MyMesh.cpp b/examples/simple_repeater/MyMesh.cpp index 68cc3685..fdd93be7 100644 --- a/examples/simple_repeater/MyMesh.cpp +++ b/examples/simple_repeater/MyMesh.cpp @@ -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); diff --git a/examples/simple_repeater/MyMesh.h b/examples/simple_repeater/MyMesh.h index c2d662a2..a37c8f74 100644 --- a/examples/simple_repeater/MyMesh.h +++ b/examples/simple_repeater/MyMesh.h @@ -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; diff --git a/examples/simple_room_server/MyMesh.cpp b/examples/simple_room_server/MyMesh.cpp index c311c941..9cd14647 100644 --- a/examples/simple_room_server/MyMesh.cpp +++ b/examples/simple_room_server/MyMesh.cpp @@ -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, ""); diff --git a/examples/simple_room_server/MyMesh.h b/examples/simple_room_server/MyMesh.h index 380e54da..6205f31a 100644 --- a/examples/simple_room_server/MyMesh.h +++ b/examples/simple_room_server/MyMesh.h @@ -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"); diff --git a/examples/simple_sensor/SensorMesh.cpp b/examples/simple_sensor/SensorMesh.cpp index 055c579d..7dc256cb 100644 --- a/examples/simple_sensor/SensorMesh.cpp +++ b/examples/simple_sensor/SensorMesh.cpp @@ -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); } diff --git a/examples/simple_sensor/SensorMesh.h b/examples/simple_sensor/SensorMesh.h index 1d65b877..e110eaca 100644 --- a/examples/simple_sensor/SensorMesh.h +++ b/examples/simple_sensor/SensorMesh.h @@ -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"); } diff --git a/src/Dispatcher.h b/src/Dispatcher.h index aad6cba3..3adf283e 100644 --- a/src/Dispatcher.h +++ b/src/Dispatcher.h @@ -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. */ diff --git a/src/helpers/CommonCLI.cpp b/src/helpers/CommonCLI.cpp index 23efe7fa..9a505ded 100644 --- a/src/helpers/CommonCLI.cpp +++ b/src/helpers/CommonCLI.cpp @@ -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>| "); + 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)); diff --git a/src/helpers/CommonCLI.h b/src/helpers/CommonCLI.h index 695d2053..6c1e839e 100644 --- a/src/helpers/CommonCLI.h +++ b/src/helpers/CommonCLI.h @@ -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 { diff --git a/src/helpers/radiolib/CustomLR1110.h b/src/helpers/radiolib/CustomLR1110.h index 4061c6b1..5d1618cb 100644 --- a/src/helpers/radiolib/CustomLR1110.h +++ b/src/helpers/radiolib/CustomLR1110.h @@ -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; } -}; \ No newline at end of file +}; diff --git a/src/helpers/radiolib/CustomLR1110Wrapper.h b/src/helpers/radiolib/CustomLR1110Wrapper.h index 13efd25b..9f459d8a 100644 --- a/src/helpers/radiolib/CustomLR1110Wrapper.h +++ b/src/helpers/radiolib/CustomLR1110Wrapper.h @@ -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(); } diff --git a/src/helpers/radiolib/CustomSX1262Wrapper.h b/src/helpers/radiolib/CustomSX1262Wrapper.h index cc7bb223..22d5a2f2 100644 --- a/src/helpers/radiolib/CustomSX1262Wrapper.h +++ b/src/helpers/radiolib/CustomSX1262Wrapper.h @@ -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 { diff --git a/src/helpers/radiolib/RadioLibWrappers.cpp b/src/helpers/radiolib/RadioLibWrappers.cpp index 66606362..22df970d 100644 --- a/src/helpers/radiolib/RadioLibWrappers.cpp +++ b/src/helpers/radiolib/RadioLibWrappers.cpp @@ -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; } diff --git a/src/helpers/radiolib/RadioLibWrappers.h b/src/helpers/radiolib/RadioLibWrappers.h index 9943bcab..af3609f2 100644 --- a/src/helpers/radiolib/RadioLibWrappers.h +++ b/src/helpers/radiolib/RadioLibWrappers.h @@ -3,6 +3,13 @@ #include #include +// 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 {