diff --git a/docs/cli_commands.md b/docs/cli_commands.md index 06d119a2e4..9a19dcc27e 100644 --- a/docs/cli_commands.md +++ b/docs/cli_commands.md @@ -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 ` + +**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 ` ` 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` diff --git a/examples/companion_radio/MyMesh.cpp b/examples/companion_radio/MyMesh.cpp index 10578511cb..972755bc63 100644 --- a/examples/companion_radio/MyMesh.cpp +++ b/examples/companion_radio/MyMesh.cpp @@ -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); @@ -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"); } @@ -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); diff --git a/examples/simple_repeater/MyMesh.cpp b/examples/simple_repeater/MyMesh.cpp index 68cc3685e8..8502834c70 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,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); @@ -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; } diff --git a/examples/simple_repeater/MyMesh.h b/examples/simple_repeater/MyMesh.h index 5ee859801e..25e53795b9 100644 --- a/examples/simple_repeater/MyMesh.h +++ b/examples/simple_repeater/MyMesh.h @@ -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; diff --git a/examples/simple_room_server/MyMesh.cpp b/examples/simple_room_server/MyMesh.cpp index c311c94194..779628636a 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,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, ""); @@ -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; } diff --git a/examples/simple_room_server/MyMesh.h b/examples/simple_room_server/MyMesh.h index 380e54da94..ff4b951a0e 100644 --- a/examples/simple_room_server/MyMesh.h +++ b/examples/simple_room_server/MyMesh.h @@ -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"); diff --git a/examples/simple_sensor/SensorMesh.cpp b/examples/simple_sensor/SensorMesh.cpp index 055c579dc1..2cb120d092 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,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); } diff --git a/examples/simple_sensor/SensorMesh.h b/examples/simple_sensor/SensorMesh.h index 1d65b8772b..e9c9f4859f 100644 --- a/examples/simple_sensor/SensorMesh.h +++ b/examples/simple_sensor/SensorMesh.h @@ -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"); } diff --git a/platformio.ini b/platformio.ini index e16f7b8304..734d805912 100644 --- a/platformio.ini +++ b/platformio.ini @@ -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 diff --git a/src/Dispatcher.cpp b/src/Dispatcher.cpp index c0610b7f8a..ef5358b8b8 100644 --- a/src/Dispatcher.cpp +++ b/src/Dispatcher.cpp @@ -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 { @@ -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); @@ -256,6 +260,7 @@ void Dispatcher::checkRecv() { processRecvPacket(pkt); } } + _radio->onReceiveProcessed(); } void Dispatcher::processRecvPacket(Packet* pkt) { @@ -387,4 +392,4 @@ unsigned long Dispatcher::futureMillis(int millis_from_now) const { return _ms->getMillis() + millis_from_now; } -} \ No newline at end of file +} diff --git a/src/Dispatcher.h b/src/Dispatcher.h index aad6cba3ec..de949b0430 100644 --- a/src/Dispatcher.h +++ b/src/Dispatcher.h @@ -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. */ @@ -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. */ diff --git a/src/helpers/CommonCLI.cpp b/src/helpers/CommonCLI.cpp index 77f7a13d39..a32051e380 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(); } @@ -670,6 +761,105 @@ 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; + level = RX_POWERSAVING_CONSERVATIVE_LEVEL; + preamble = RX_POWERSAVING_PROFILE_PREAMBLE; + level_requested = true; + preamble_overridden = true; + } else if (strcmp(value, "balanced") == 0) { + enable = 1; + level = RX_POWERSAVING_BALANCED_LEVEL; + preamble = RX_POWERSAVING_PROFILE_PREAMBLE; + level_requested = true; + preamble_overridden = true; + } 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) { + // manual timings are fixed, not level-derived + // (the named profiles set level_requested and are handled above) + _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]; @@ -683,8 +873,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"); } @@ -928,6 +1121,14 @@ 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, "rxps.wd", 7) == 0) { + uint32_t wd_soft, wd_hard; + _callbacks->getRxPsWatchdogCounts(&wd_soft, &wd_hard); + sprintf(reply, "> soft=%lu,hard=%lu", (unsigned long)wd_soft, (unsigned long)wd_hard); } 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 695d205398..6ec5aa0133 100644 --- a/src/helpers/CommonCLI.h +++ b/src/helpers/CommonCLI.h @@ -19,6 +19,18 @@ #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_MIN_PERIOD_US 1000UL +#define RX_POWERSAVING_MAX_PERIOD_US 30000000UL + +// The named profiles are level presets pinned to a 16-symbol preamble: most +// deployed senders still transmit 16-symbol preambles regardless of the newer +// SF-based rule (32 for SF <= 8). Revisit once the field has largely migrated. +#define RX_POWERSAVING_CONSERVATIVE_LEVEL 1UL +#define RX_POWERSAVING_BALANCED_LEVEL 5UL +#define RX_POWERSAVING_PROFILE_PREAMBLE 16UL + struct NodePrefs { // persisted to file float airtime_factor; char node_name[32]; @@ -66,6 +78,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 +132,14 @@ class CommonCLICallbacks { virtual void setRxBoostedGain(bool enable) { // no op by default }; + + virtual bool setRxPowerSaving(bool enable, uint32_t rx_us, uint32_t sleep_us) { + return !enable; + }; + + virtual void getRxPsWatchdogCounts(uint32_t* soft, uint32_t* hard) { + *soft = 0; *hard = 0; + }; }; class CommonCLI { diff --git a/src/helpers/radiolib/CustomLLCC68Wrapper.h b/src/helpers/radiolib/CustomLLCC68Wrapper.h index 8861f76d24..e61d8a9737 100644 --- a/src/helpers/radiolib/CustomLLCC68Wrapper.h +++ b/src/helpers/radiolib/CustomLLCC68Wrapper.h @@ -22,9 +22,6 @@ class CustomLLCC68Wrapper : public RadioLibWrapper { float getCurrentRSSI() override { return ((CustomLLCC68 *)_radio)->getRSSI(false); } - float getLastRSSI() const override { return ((CustomLLCC68 *)_radio)->getRSSI(); } - float getLastSNR() const override { return ((CustomLLCC68 *)_radio)->getSNR(); } - float packetScore(float snr, int packet_len) override { int sf = ((CustomLLCC68 *)_radio)->spreadingFactor; return packetScoreInt(snr, sf, packet_len); diff --git a/src/helpers/radiolib/CustomLR1110.h b/src/helpers/radiolib/CustomLR1110.h index 4061c6b1a6..c1e6fb524a 100644 --- a/src/helpers/radiolib/CustomLR1110.h +++ b/src/helpers/radiolib/CustomLR1110.h @@ -24,6 +24,61 @@ 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) { + uint32_t symbolPeriod = (uint32_t)(((1000.0f * (float)(1UL << this->spreadingFactor)) / + this->bandwidthKhz) + 0.999f); + uint32_t transitionTime = this->tcxoDelay + 1000; + if (sleepPeriod <= transitionTime) { + return RADIOLIB_ERR_INVALID_SLEEP_PERIOD; + } + uint32_t programmedSleepPeriod = sleepPeriod - transitionTime; + + // PreambleDetected restarts the timeout at 2*rx + sleep. LR1110 testing + // established 78 ms RX / 26.851 ms sleep as the production minimum at + // SF8, BW 62.5 kHz. Preamble + 11 symbols plus 1 ms preserves that margin. + uint64_t requiredExtendedPeriod = + ((uint64_t)this->preambleLengthLoRa + 11ULL) * symbolPeriod + 1000ULL; + uint64_t extendedPeriod = 2ULL * rxPeriod + programmedSleepPeriod; + if (extendedPeriod < requiredExtendedPeriod) { + rxPeriod = (uint32_t)((requiredExtendedPeriod - programmedSleepPeriod + 1ULL) / 2ULL); + } + + uint32_t rxPeriodRaw = (uint32_t)(((uint64_t)rxPeriod * 32768UL) / 1000000UL); + uint32_t sleepPeriodRaw = (uint32_t)(((uint64_t)programmedSleepPeriod * 32768UL) / 1000000UL); + + if ((rxPeriodRaw & 0xFF000000) || (rxPeriodRaw == 0)) { + return RADIOLIB_ERR_INVALID_RX_PERIOD; + } + + if ((sleepPeriodRaw & 0xFF000000) || (sleepPeriodRaw == 0)) { + return RADIOLIB_ERR_INVALID_SLEEP_PERIOD; + } + + // Semtech requires Standby RC and an explicitly configured RTC source + // before SetRxDutyCycle. RadioLib does neither in its LoRa RXPS path. + int16_t state = standby(RADIOLIB_LR11X0_STANDBY_RC); + RADIOLIB_ASSERT(state); + state = configLfClock(RADIOLIB_LR11X0_LF_CLK_RC | RADIOLIB_LR11X0_LF_BUSY_RELEASE_ENABLED); + RADIOLIB_ASSERT(state); + + RadioModeConfig_t cfg = { + .receive = { + .timeout = RADIOLIB_LR11X0_RX_TIMEOUT_INF, + .irqFlags = irqFlags, + .irqMask = irqMask, + .len = 0, + } + }; + state = this->stageMode(RADIOLIB_RADIO_MODE_RX, &cfg); + RADIOLIB_ASSERT(state); + + // Send the already converted values. RadioLib 7.7.1 converts them again + // with 32-bit arithmetic, which overflows for periods above about 131 ms. + return this->setRxDutyCycle(rxPeriodRaw, sleepPeriodRaw, RADIOLIB_LR11X0_RX_DUTY_CYCLE_MODE_RX); + } + int16_t setRxBoostedGainMode(bool en) { _rx_boosted = en; return LR1110::setRxBoostedGainMode(en); @@ -31,11 +86,20 @@ class CustomLR1110 : public LR1110 { bool getRxBoostedGainMode() const { return _rx_boosted; } + // BUSY high means the chip is asleep (RX duty-cycle sleep window) or mid + // command; any SPI access would stall until the chip's next listen window. + bool isChipBusy() { + uint32_t busy = this->mod->getGpio(); + return busy != RADIOLIB_NC && this->mod->hal->digitalRead(busy); + } + bool isReceiving() { + if (isChipBusy()) return false; // asleep, cannot be mid-receive + uint16_t irq = getIrqStatus(); bool detected = ((irq & RADIOLIB_LR11X0_IRQ_SYNC_WORD_HEADER_VALID) || (irq & RADIOLIB_LR11X0_IRQ_PREAMBLE_DETECTED)); return detected; } 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 13efd25b57..73e31e8aae 100644 --- a/src/helpers/radiolib/CustomLR1110Wrapper.h +++ b/src/helpers/radiolib/CustomLR1110Wrapper.h @@ -20,6 +20,9 @@ class CustomLR1110Wrapper : public RadioLibWrapper { bool isReceivingPacket() override { return ((CustomLR1110 *)_radio)->isReceiving(); } + bool isChipBusy() override { + return ((CustomLR1110 *)_radio)->isChipBusy(); + } float getCurrentRSSI() override { float rssi = -110; ((CustomLR1110 *)_radio)->getRssiInst(&rssi); @@ -31,9 +34,41 @@ class CustomLR1110Wrapper : public RadioLibWrapper { _radio->setPreambleLength(preambleLengthForSF(getSpreadingFactor())); // overcomes weird issues with small and big pkts } - float getLastRSSI() const override { return ((CustomLR1110 *)_radio)->getRSSI(); } - float getLastSNR() const override { return ((CustomLR1110 *)_radio)->getSNR(); } + bool supportsRxPowerSaving() const override { return true; } + +protected: + int startReceiveMode() override { + if (_rx_ps_armed) { + // stop the previous duty-cycle sequence with an explicit standby before + // reconfiguring the radio + stopReceiveDutyCycle(); + } + if (!_rx_ps_enabled || _nf_calib_active) { + // plain continuous RX: powersaving off, or a periodic noise-floor + // calibration window is in progress + return _radio->startReceive(); + } + + const RadioLibIrqFlags_t irqFlags = RADIOLIB_IRQ_RX_DEFAULT_FLAGS; + // route RX timeout (false preamble detect) and error IRQs to the IRQ pin + // as well, so the wrapper re-arms RX instead of waiting forever + 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 = ((CustomLR1110 *)_radio)->startReceiveDutyCycle(_rx_ps_rx_us, _rx_ps_sleep_us, irqFlags, irqMask); + if (err == RADIOLIB_ERR_NONE) { + _rx_ps_armed = true; + return err; + } + MESH_DEBUG_PRINTLN("CustomLR1110Wrapper: error: startReceiveDutyCycle(%d), falling back to continuous RX", err); + return _radio->startReceive(); + } + +public: uint8_t getSpreadingFactor() const override { return ((CustomLR1110 *)_radio)->getSpreadingFactor(); } void setRxBoostedGainMode(bool en) override { diff --git a/src/helpers/radiolib/CustomSTM32WLxWrapper.h b/src/helpers/radiolib/CustomSTM32WLxWrapper.h index 97bf6820d6..a5f5152004 100644 --- a/src/helpers/radiolib/CustomSTM32WLxWrapper.h +++ b/src/helpers/radiolib/CustomSTM32WLxWrapper.h @@ -23,9 +23,6 @@ class CustomSTM32WLxWrapper : public RadioLibWrapper { float getCurrentRSSI() override { return ((CustomSTM32WLx *)_radio)->getRSSI(false); } - float getLastRSSI() const override { return ((CustomSTM32WLx *)_radio)->getRSSI(); } - float getLastSNR() const override { return ((CustomSTM32WLx *)_radio)->getSNR(); } - float packetScore(float snr, int packet_len) override { int sf = ((CustomSTM32WLx *)_radio)->spreadingFactor; return packetScoreInt(snr, sf, packet_len); diff --git a/src/helpers/radiolib/CustomSX1262.h b/src/helpers/radiolib/CustomSX1262.h index ad20122902..f24604e858 100644 --- a/src/helpers/radiolib/CustomSX1262.h +++ b/src/helpers/radiolib/CustomSX1262.h @@ -86,12 +86,39 @@ class CustomSX1262 : public SX1262 { return true; // success } + // BUSY high means the chip is asleep (RX duty-cycle sleep window) or mid + // command; any SPI access would stall until the chip's next listen window. + bool isChipBusy() { + uint32_t busy = this->mod->getGpio(); + return busy != RADIOLIB_NC && this->mod->hal->digitalRead(busy); + } + bool isReceiving() { + if (isChipBusy()) return false; // asleep, cannot be mid-receive + uint16_t irq = getIrqFlags(); bool detected = (irq & SX126X_IRQ_HEADER_VALID) || (irq & SX126X_IRQ_PREAMBLE_DETECTED); return detected; } + // Port of Semtech's sx126x_stop_rtc() (same registers as RadioLib's + // fixImplicitTimeout / datasheet errata 15.3): after duty-cycle RX ends via + // RxDone or SetStandby, the internal RTC keeps running and its pending + // event can silently knock a subsequently started RX back to standby with + // no IRQ, leaving the node deaf. Must be called before re-arming RX. + int16_t stopRTC() { + uint8_t rtcStop = 0x00; + int16_t state = writeRegister(RADIOLIB_SX126X_REG_RTC_CTRL, &rtcStop, 1); + RADIOLIB_ASSERT(state); + + uint8_t rtcEvent = 0; + state = readRegister(RADIOLIB_SX126X_REG_EVENT_MASK, &rtcEvent, 1); + RADIOLIB_ASSERT(state); + + rtcEvent |= 0x02; // clear the RX timeout event + return writeRegister(RADIOLIB_SX126X_REG_EVENT_MASK, &rtcEvent, 1); + } + bool getRxBoostedGainMode() { uint8_t rxGain = 0; readRegister(RADIOLIB_SX126X_REG_RX_GAIN, &rxGain, 1); diff --git a/src/helpers/radiolib/CustomSX1262Wrapper.h b/src/helpers/radiolib/CustomSX1262Wrapper.h index cc7bb2238b..1c9c0ab683 100644 --- a/src/helpers/radiolib/CustomSX1262Wrapper.h +++ b/src/helpers/radiolib/CustomSX1262Wrapper.h @@ -13,6 +13,7 @@ class CustomSX1262Wrapper : public RadioLibWrapper { CustomSX1262Wrapper(CustomSX1262& radio, mesh::MainBoard& board) : RadioLibWrapper(radio, board) { } void setParams(float freq, float bw, uint8_t sf, uint8_t cr) override { + cacheParams(freq, bw, sf, cr); ((CustomSX1262 *)_radio)->setFrequency(freq); ((CustomSX1262 *)_radio)->setSpreadingFactor(sf); ((CustomSX1262 *)_radio)->setBandwidth(bw); @@ -20,15 +21,15 @@ class CustomSX1262Wrapper : public RadioLibWrapper { updatePreamble(sf); } - bool isReceivingPacket() override { + bool isReceivingPacket() override { return ((CustomSX1262 *)_radio)->isReceiving(); } + bool isChipBusy() override { + return ((CustomSX1262 *)_radio)->isChipBusy(); + } float getCurrentRSSI() override { return ((CustomSX1262 *)_radio)->getRSSI(false); } - float getLastRSSI() const override { return ((CustomSX1262 *)_radio)->getRSSI(); } - float getLastSNR() const override { return ((CustomSX1262 *)_radio)->getSNR(); } - float packetScore(float snr, int packet_len) override { int sf = ((CustomSX1262 *)_radio)->spreadingFactor; return packetScoreInt(snr, sf, packet_len); @@ -38,6 +39,52 @@ class CustomSX1262Wrapper : public RadioLibWrapper { ((CustomSX1262 *)_radio)->sleep(false); } + bool supportsRxPowerSaving() const override { return true; } + +protected: + int startReceiveMode() override { + if (_rx_ps_armed) { + // leaving duty-cycle mode (after RxDone or a reconfig): stop the + // sequencer and the still-running RTC, or its pending event can + // silently abort the RX we are about to start + stopReceiveDutyCycle(); + } + if (!_rx_ps_enabled || _nf_calib_active) { + // plain continuous RX: powersaving off, or a periodic noise-floor + // calibration window is in progress + 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) { + _rx_ps_armed = true; + return err; + } + + MESH_DEBUG_PRINTLN("CustomSX1262Wrapper: error: startReceiveDutyCycle(%d), falling back to continuous RX", err); + return _radio->startReceive(); + } + + void stopReceiveDutyCycle() override { + _radio->standby(); // also wakes the chip if it is in the sleep window + ((CustomSX1262 *)_radio)->stopRTC(); + _rx_ps_armed = false; + } + + bool radioDeepInit() override { + // std_init() re-runs RadioLib begin(), which starts with a hardware reset + // via NRST - the only way out of a hard-locked chip (BUSY stuck high). + return ((CustomSX1262 *)_radio)->std_init(); + } + +public: void doResetAGC() override { sx126xResetAGC((SX126x *)_radio); } void setRxBoostedGainMode(bool en) override { diff --git a/src/helpers/radiolib/CustomSX1268Wrapper.h b/src/helpers/radiolib/CustomSX1268Wrapper.h index 9ddea78f3f..bf8426283d 100644 --- a/src/helpers/radiolib/CustomSX1268Wrapper.h +++ b/src/helpers/radiolib/CustomSX1268Wrapper.h @@ -26,9 +26,6 @@ class CustomSX1268Wrapper : public RadioLibWrapper { float getCurrentRSSI() override { return ((CustomSX1268 *)_radio)->getRSSI(false); } - float getLastRSSI() const override { return ((CustomSX1268 *)_radio)->getRSSI(); } - float getLastSNR() const override { return ((CustomSX1268 *)_radio)->getSNR(); } - float packetScore(float snr, int packet_len) override { int sf = ((CustomSX1268 *)_radio)->spreadingFactor; return packetScoreInt(snr, sf, packet_len); diff --git a/src/helpers/radiolib/CustomSX1276Wrapper.h b/src/helpers/radiolib/CustomSX1276Wrapper.h index 9d75ce12a1..bba8412f17 100644 --- a/src/helpers/radiolib/CustomSX1276Wrapper.h +++ b/src/helpers/radiolib/CustomSX1276Wrapper.h @@ -25,9 +25,6 @@ class CustomSX1276Wrapper : public RadioLibWrapper { float getCurrentRSSI() override { return ((CustomSX1276 *)_radio)->getRSSI(false); } - float getLastRSSI() const override { return ((CustomSX1276 *)_radio)->getRSSI(); } - float getLastSNR() const override { return ((CustomSX1276 *)_radio)->getSNR(); } - float packetScore(float snr, int packet_len) override { int sf = ((CustomSX1276 *)_radio)->spreadingFactor; return packetScoreInt(snr, sf, packet_len); diff --git a/src/helpers/radiolib/RadioLibWrappers.cpp b/src/helpers/radiolib/RadioLibWrappers.cpp index 66606362d0..b44b428e71 100644 --- a/src/helpers/radiolib/RadioLibWrappers.cpp +++ b/src/helpers/radiolib/RadioLibWrappers.cpp @@ -11,6 +11,11 @@ #define NUM_NOISE_FLOOR_SAMPLES 64 #define SAMPLING_THRESHOLD 14 +// periodic noise-floor calibration windows (RX duty-cycle powersaving only) +#define NF_CALIB_INTERVAL_MS 60000UL // at least once a minute +#define NF_CALIB_TIMEOUT_MS 5000UL // give up on the batch (busy channel) +#define NF_CALIB_SETTLE_MS 20UL // frontend/AGC settle after RX entry + static volatile uint8_t state = STATE_IDLE; // this function is called when a complete packet @@ -48,6 +53,8 @@ uint32_t RadioLibWrapper::getRngSeed() { } void RadioLibWrapper::setTxPower(int8_t dbm) { + _cur_dbm = dbm; + _dbm_valid = true; _radio->setOutputPower(dbm); } @@ -84,9 +91,132 @@ void RadioLibWrapper::resetAGC() { _floor_sample_sum = 0; } +void RadioLibWrapper::rxPsWatchdogCheck() { + // don't interfere mid-transmit or with a completed-but-unread packet + // (a pending DIO1 event is itself proof the radio is alive; recvRaw() will + // re-arm and re-base the watchdog) + if ((state & STATE_INT_READY) != 0 || (state & ~STATE_INT_READY) == STATE_TX_WAIT) { + _wd_observe_until = 0; + return; + } + + unsigned long now = millis(); + bool tripped = false; + + if (_rx_ps_armed && state == STATE_RX && _wd_stuck_thresh > 0) { + bool busy = isChipBusy(); + if (busy != _wd_last_busy) { + // the sleep/listen wave is present -> radio healthy + _wd_last_busy = busy; + _wd_last_transition = now; + _wd_stage = 0; + _wd_strikes = 0; + _wd_observe_until = 0; + } else if (_wd_observe_until != 0) { + // active observation window in progress (MCU kept awake via + // isWatchdogObserving()); a healthy chip must toggle BUSY within it + if ((long)(now - _wd_observe_until) >= 0) { + _wd_observe_until = 0; + if (!busy && isReceivingPacket()) { + // BUSY held low by an ongoing reception (extended RX) - alive + _wd_last_transition = now; + _wd_strikes = 0; + } else if (++_wd_strikes >= 2) { + _wd_strikes = 0; + tripped = true; + } else { + _wd_last_transition = now; // full threshold before the next window + } + } + } else if (now - _wd_last_transition > _wd_stuck_thresh) { + // no proof of life for too long: actively watch one full cycle + _wd_observe_until = now + _wd_observe_ms; + if (_wd_observe_until == 0) _wd_observe_until = 1; // 0 means "off" + } + } else { + _wd_observe_until = 0; + } + if (_startrx_fails >= 3) tripped = true; // can't even re-arm receive mode + + if (!tripped) return; + + _wd_last_transition = now; // grace period before the next escalation + _startrx_fails = 0; + _wd_observe_until = 0; + + if (_wd_stage == 0) { + _wd_stage = 1; + n_wd_soft++; + MESH_DEBUG_PRINTLN("RadioLibWrapper: watchdog: RX duty-cycle stuck, soft re-arm"); + state = STATE_IDLE; // next recvRaw() re-arms receive mode + } else { + _wd_stage = 2; + n_wd_hard++; + MESH_DEBUG_PRINTLN("RadioLibWrapper: watchdog: still stuck, hard radio reset"); + if (radioDeepInit()) { + _rx_ps_armed = false; // chip is factory-fresh after NRST + _radio->setPacketReceivedAction(setFlag); + if (_params_valid) setParams(_cur_freq, _cur_bw, _cur_sf, _cur_cr); + if (_dbm_valid) _radio->setOutputPower(_cur_dbm); + } + state = STATE_IDLE; // re-arm (rx powersaving settings are kept in members) + } +} + +// Initial and periodic noise-floor calibration, active only with RX duty-cycle powersaving: +// a duty-cycled receiver can't be sampled reliably (the frontend is off in the +// sleep windows and settling right after each wake), so at least once a minute +// the receive mode is dropped to plain continuous RX, a fresh sample batch is +// collected exactly like the non-powersaving path does, and the duty cycle is +// re-armed. The published average stays in _noise_floor as usual. +void RadioLibWrapper::noiseFloorCalibCheck() { + unsigned long now = millis(); + if (_nf_calib_active) { + if (!_rx_ps_enabled || (long)(now - _nf_calib_deadline) >= 0) { + // powersaving turned off mid-window, or the batch couldn't complete + // (busy channel / stuck filter) - keep the previous floor + endNoiseFloorCalib(now); + } + } else if (_rx_ps_enabled && _rx_ps_armed && state == STATE_RX + && (_nf_last_calib == 0 || now - _nf_last_calib >= NF_CALIB_INTERVAL_MS) + && !isReceivingPacket()) { + // never interrupt an ongoing reception to calibrate (a TX in flight is + // already excluded by state == STATE_RX); retries next loop iteration + _nf_calib_active = true; + _nf_calib_deadline = now + NF_CALIB_TIMEOUT_MS; + _nf_sample_from = now + NF_CALIB_SETTLE_MS; + _num_floor_samples = 0; // start a fresh batch for this window + _floor_sample_sum = 0; + state = STATE_IDLE; // recvRaw() re-arms; startReceiveMode() sees the + // active flag and starts continuous RX, not duty-cycle + } +} + +void RadioLibWrapper::endNoiseFloorCalib(unsigned long now) { + _nf_calib_active = false; + _nf_last_calib = now; + // force a receive re-arm back into duty-cycle mode, but don't clobber a + // completed-but-unread packet or an in-flight TX (recvRaw()/onSendFinished() + // will re-arm right after those anyway; same guard style as setRxPowerSaving) + if ((state & STATE_INT_READY) == 0 && (state & ~STATE_INT_READY) != STATE_TX_WAIT) { + state = STATE_IDLE; + } +} + void RadioLibWrapper::loop() { + if (_rx_ps_enabled) { + rxPsWatchdogCheck(); + } + noiseFloorCalibCheck(); + if (state == STATE_RX && _num_floor_samples < NUM_NOISE_FLOOR_SAMPLES) { - if (!isReceivingPacket()) { + // Noise floor is only sampled outside RX duty-cycle mode: continuously in + // plain RX (powersaving off), or inside the periodic calibration window + // (powersaving on), skipping the first moments after RX entry there while + // the frontend/AGC settles (unsettled GetRssiInst reads ~-127 dBm garbage). + if (!_rx_ps_armed + && !(_nf_calib_active && (long)(millis() - _nf_sample_from) < 0) + && !isReceivingPacket()) { int rssi = getCurrentRSSI(); if (rssi < _noise_floor + SAMPLING_THRESHOLD) { // only consider samples below current floor + sampling THRESHOLD _num_floor_samples++; @@ -101,57 +231,159 @@ void RadioLibWrapper::loop() { _floor_sample_sum = 0; MESH_DEBUG_PRINTLN("RadioLibWrapper: noise_floor = %d", (int)_noise_floor); + + if (_nf_calib_active) { + endNoiseFloorCalib(millis()); // fresh floor published - back to duty cycle + } } } void RadioLibWrapper::startRecv() { - int err = _radio->startReceive(); + int err = startReceiveMode(); if (err == RADIOLIB_ERR_NONE) { state = STATE_RX; + _startrx_fails = 0; + if (_rx_ps_armed) { + // (re)base the duty-cycle watchdog on the freshly armed cycle + _wd_last_busy = isChipBusy(); + _wd_last_transition = millis(); + // Longest legitimate silence on the BUSY pin: one full cycle, plus the + // extended RX after a (possibly false) preamble detect (2*rx + sleep), + // plus a worst-case packet airtime, plus margin for TCXO/transitions. + // Floored at 60s so a light-sleeping MCU (ESP32 wakes every ~30s) opens + // an observation window every couple of wakeups instead of on each one. + uint32_t rx_ms = _rx_ps_rx_us / 1000, sleep_ms = _rx_ps_sleep_us / 1000; + _wd_stuck_thresh = (rx_ms + sleep_ms) + 2 * (2 * rx_ms + sleep_ms) + + getEstAirtimeFor(MAX_TRANS_UNIT) + 1000; + if (_wd_stuck_thresh < 60000) _wd_stuck_thresh = 60000; + // active observation window must cover one full duty cycle + _wd_observe_ms = rx_ms + sleep_ms + 50; + if (_wd_observe_ms > 1500) _wd_observe_ms = 1500; + } } else { - MESH_DEBUG_PRINTLN("RadioLibWrapper: error: startReceive(%d)", err); + if (_startrx_fails < 255) _startrx_fails++; + MESH_DEBUG_PRINTLN("RadioLibWrapper: error: startReceiveMode(%d)", err); } } +int RadioLibWrapper::startReceiveMode() { + return _radio->startReceive(); +} + +void RadioLibWrapper::stopReceiveDutyCycle() { + // The duty-cycle sequencer only stops on RxDone or an explicit standby; + // issuing other mode commands while it runs leads to undefined behaviour. + _radio->standby(); + _rx_ps_armed = false; +} + +bool RadioLibWrapper::isPacketReady() { + if (!_rx_ps_armed) return true; // continuous RX: DIO1 only fires for RxDone/TxDone here + + // In duty-cycle RX the DIO1 interrupt also fires for RX timeout (false + // preamble detect) and header errors. GetRxBufferStatus still reports the + // *previous* packet's length then, so reading the buffer would re-deliver + // stale bytes as a ghost packet. Only read when the radio reports RxDone. + // (checkIrq errors are treated as ready, falling back to old behaviour.) + return _radio->checkIrq(RADIOLIB_IRQ_RX_DONE) != 0; +} + 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) { - len = _radio->getPacketLength(); - if (len > 0) { - if (len > sz) { len = sz; } - int err = _radio->readData(bytes, len); - if (err != RADIOLIB_ERR_NONE) { - MESH_DEBUG_PRINTLN("RadioLibWrapper: error: readData(%d)", err); - len = 0; - n_recv_errors++; - } else { - // Serial.print(" readData() -> "); Serial.println(len); - n_recv++; + if (isPacketReady()) { + if (_rx_ps_armed) { + stopReceiveDutyCycle(); + } + len = _radio->getPacketLength(); + if (len > 0) { + if (len > sz) { len = sz; } + _last_snr = _radio->getSNR(); + _last_rssi = _radio->getRSSI(); + int err = _radio->readData(bytes, len); + if (err != RADIOLIB_ERR_NONE) { + MESH_DEBUG_PRINTLN("RadioLibWrapper: error: readData(%d)", err); + len = 0; + n_recv_errors++; + } else { + // Serial.print(" readData() -> "); Serial.println(len); + n_recv++; + } } } state = STATE_IDLE; // need another startReceive() } - if (state != STATE_RX) { + if (len > 0 && _rx_ps_enabled) { + _rx_hold_continuous = true; int err = _radio->startReceive(); if (err == RADIOLIB_ERR_NONE) { state = STATE_RX; + if (_nf_calib_active) { + _nf_sample_from = millis() + NF_CALIB_SETTLE_MS; + } } else { - MESH_DEBUG_PRINTLN("RadioLibWrapper: error: startReceive(%d)", err); + MESH_DEBUG_PRINTLN("RadioLibWrapper: error: startReceive after packet (%d)", err); } + return len; + } + + if (state != STATE_RX) { + startRecv(); } return len; } +void RadioLibWrapper::onReceiveProcessed() { + if (!_rx_hold_continuous) return; + + if ((state & ~STATE_INT_READY) == STATE_TX_WAIT) { + _rx_hold_continuous = false; + return; + } + if ((state & STATE_INT_READY) != 0 || isReceivingPacket()) { + return; + } + + _rx_hold_continuous = false; + if (!_rx_ps_enabled || _nf_calib_active) return; + + state = STATE_IDLE; + startRecv(); +} + uint32_t RadioLibWrapper::getEstAirtimeFor(int len_bytes) { return _radio->getTimeOnAir(len_bytes) / 1000; } bool RadioLibWrapper::startSendRaw(const uint8_t* bytes, int len) { + if (_rx_ps_armed) { + // stop the duty-cycle sequencer before SetTx, otherwise its next RTC + // event can fire mid-transmission and abort the TX + stopReceiveDutyCycle(); + } _board->onBeforeTransmit(); int err = _radio->startTransmit((uint8_t *) bytes, len); if (err == RADIOLIB_ERR_NONE) { @@ -184,11 +416,25 @@ int16_t RadioLibWrapper::performChannelScan() { } bool RadioLibWrapper::isChannelActive() { - // int.thresh: RSSI-based interference detection (relative to noise floor) - if (_threshold != 0 && getCurrentRSSI() > _noise_floor + _threshold) return true; + // int.thresh: RSSI-based interference detection (relative to noise floor). + // In RX duty-cycle mode only checked while the chip is in a listen window + // (during the sleep window the frontend is off and the read would stall). + if (_threshold != 0 && !(_rx_ps_armed && isChipBusy()) + && getCurrentRSSI() > _noise_floor + _threshold) return true; // cad: hardware channel activity detection if (_cad_enabled) { + if (_rx_ps_armed) { + // CAD must not be issued on top of a running duty-cycle sequencer. The + // sequencer's RTC keeps running across the SetStandby that the scan does + // first (same errata as stopRTC() documents for RX), and its pending event + // can knock the chip back to standby mid-CAD without raising an IRQ. The + // CAD-done then never arrives and RadioLib's scanChannel() waits for it + // forever, hanging the whole main loop. Stop the sequencer and its RTC + // first, exactly like startSendRaw() and startReceiveMode() do; the + // startRecv() below re-arms the duty cycle. + stopReceiveDutyCycle(); + } int16_t result = performChannelScan(); // scanChannel() triggers DIO interrupt (CAD done) which sets STATE_INT_READY // via setFlag() ISR. Clear it before restarting RX so recvRaw() doesn't @@ -205,10 +451,10 @@ bool RadioLibWrapper::isChannelActive() { } float RadioLibWrapper::getLastRSSI() const { - return _radio->getRSSI(); + return _last_rssi; } float RadioLibWrapper::getLastSNR() const { - return _radio->getSNR(); + return _last_snr; } // Approximate SNR threshold per SF for successful reception (based on Semtech datasheets) diff --git a/src/helpers/radiolib/RadioLibWrappers.h b/src/helpers/radiolib/RadioLibWrappers.h index 9943bcab77..24f6da711e 100644 --- a/src/helpers/radiolib/RadioLibWrappers.h +++ b/src/helpers/radiolib/RadioLibWrappers.h @@ -3,34 +3,104 @@ #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; mesh::MainBoard* _board; uint32_t n_recv, n_sent, n_recv_errors; int16_t _noise_floor, _threshold; + float _last_rssi, _last_snr; bool _cad_enabled; uint16_t _num_floor_samples; int32_t _floor_sample_sum; uint8_t _preamble_sf; + bool _rx_ps_enabled; + bool _rx_ps_armed; // radio is currently in RX duty-cycle mode + bool _rx_hold_continuous; + uint32_t _rx_ps_rx_us; + uint32_t _rx_ps_sleep_us; + + // RX duty-cycle watchdog: a healthy duty cycle shows a square wave on the + // BUSY pin (high in the sleep window / TCXO warmup, low while listening). + // If the wave stops, the chip fell out of the cycle without an IRQ. + // Passive sampling only works while the main loop spins; on MCUs that light + // sleep between wakeups the watchdog instead opens an "active observation" + // window (isWatchdogObserving() keeps the MCU awake) spanning one full radio + // cycle - a healthy chip must toggle BUSY within it. + bool _wd_last_busy; + uint8_t _wd_stage; // 0 = healthy, 1 = soft re-arm done, 2 = hard reset done + uint8_t _wd_strikes; // consecutive failed observation windows + uint8_t _startrx_fails; // consecutive startReceiveMode() failures + unsigned long _wd_last_transition; // millis of last BUSY level change (proof of life) + unsigned long _wd_stuck_thresh; // ms without proof of life before observing + unsigned long _wd_observe_until; // 0 = not observing, else millis deadline + uint32_t _wd_observe_ms; // observation window: one full cycle + margin + uint32_t n_wd_soft, n_wd_hard; + + // last applied radio settings, reapplied after a hard radio reset + float _cur_freq, _cur_bw; + uint8_t _cur_sf, _cur_cr; + int8_t _cur_dbm; + bool _params_valid, _dbm_valid; + + // Initial and periodic noise-floor calibration (only while RX duty-cycle + // powersaving is armed): a duty-cycled receiver can't be sampled reliably, + // so at least once a minute the wrapper drops to plain continuous RX, + // collects a fresh sample batch, publishes it and re-arms the duty cycle. + bool _nf_calib_active; + unsigned long _nf_last_calib; // millis of last completed/attempted window + unsigned long _nf_calib_deadline; // abort window if the batch can't complete + unsigned long _nf_sample_from; // no samples before this (RX entry settle) void idle(); void startRecv(); + void rxPsWatchdogCheck(); + void noiseFloorCalibCheck(); + void endNoiseFloorCalib(unsigned long now); + void cacheParams(float freq, float bw, uint8_t sf, uint8_t cr) { + _cur_freq = freq; _cur_bw = bw; _cur_sf = sf; _cur_cr = cr; _params_valid = true; + } + virtual int startReceiveMode(); + virtual void stopReceiveDutyCycle(); + virtual bool isPacketReady(); + // true while the chip cannot service SPI (RX duty-cycle sleep window, or + // briefly while processing a command); radios expose this via the BUSY pin + virtual bool isChipBusy() { return false; } + // full radio recovery: hardware reset (NRST) + re-init to boot defaults; + // returns false if unsupported. Caller reapplies cached runtime params. + virtual bool radioDeepInit() { return false; } 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_armed(false), + _rx_hold_continuous(false), + _rx_ps_rx_us(RX_PS_FALLBACK_RX_US), _rx_ps_sleep_us(RX_PS_FALLBACK_SLEEP_US), + _wd_last_busy(false), _wd_stage(0), _wd_strikes(0), _startrx_fails(0), _wd_last_transition(0), + _wd_stuck_thresh(0), _wd_observe_until(0), _wd_observe_ms(0), + _params_valid(false), _dbm_valid(false), + _nf_calib_active(false), _nf_last_calib(0), _nf_calib_deadline(0), _nf_sample_from(0) + { n_recv = n_sent = n_recv_errors = n_wd_soft = n_wd_hard = 0; _last_rssi = _last_snr = 0; } void begin() override; virtual void powerOff() { _radio->sleep(); } int recvRaw(uint8_t* bytes, int sz) override; + void onReceiveProcessed() override; uint32_t getEstAirtimeFor(int len_bytes) override; bool startSendRaw(const uint8_t* bytes, int len) override; 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 { @@ -59,10 +129,19 @@ class RadioLibWrapper : public mesh::Radio { uint32_t getPacketsRecv() const { return n_recv; } uint32_t getPacketsRecvErrors() const { return n_recv_errors; } uint32_t getPacketsSent() const { return n_sent; } + uint32_t getRxPsWatchdogSoftCount() const { return n_wd_soft; } + uint32_t getRxPsWatchdogHardCount() const { return n_wd_hard; } + // true while the watchdog is actively watching for BUSY transitions; used by + // the app's hasPendingWork() to keep the MCU out of light sleep for the window + bool isWatchdogObserving() const { return _wd_observe_until != 0; } + // true while a periodic noise-floor calibration window is in progress; the + // app's hasPendingWork() must keep the MCU awake so the sample batch and the + // return to duty-cycle complete promptly + bool isCalibratingNoiseFloor() const { return _nf_calib_active; } void resetStats() { n_recv = n_sent = n_recv_errors = 0; } - virtual float getLastRSSI() const override; - virtual float getLastSNR() const override; + float getLastRSSI() const override final; + float getLastSNR() const override final; float packetScore(float snr, int packet_len) override { return packetScoreInt(snr, 10, packet_len); } // assume sf=10 diff --git a/variants/heltec_t096/platformio.ini b/variants/heltec_t096/platformio.ini index 5f17436ad6..dccb9d0be5 100644 --- a/variants/heltec_t096/platformio.ini +++ b/variants/heltec_t096/platformio.ini @@ -126,6 +126,7 @@ board_build.ldscript = boards/nrf52840_s140_v6_extrafs.ld board_upload.maximum_size = 712704 build_flags = ${Heltec_t096.build_flags} + ${companion_rxps.build_flags} -I examples/companion_radio/ui-new -D MAX_CONTACTS=350 -D MAX_GROUP_CHANNELS=40 @@ -155,6 +156,7 @@ board_build.ldscript = boards/nrf52840_s140_v6_extrafs.ld board_upload.maximum_size = 712704 build_flags = ${Heltec_t096.build_flags} + ${companion_rxps.build_flags} -I examples/companion_radio/ui-new -D MAX_CONTACTS=350 -D MAX_GROUP_CHANNELS=40 diff --git a/variants/heltec_t114/platformio.ini b/variants/heltec_t114/platformio.ini index e808d6c287..91caa4a6c7 100644 --- a/variants/heltec_t114/platformio.ini +++ b/variants/heltec_t114/platformio.ini @@ -99,6 +99,7 @@ board_build.ldscript = boards/nrf52840_s140_v6_extrafs.ld board_upload.maximum_size = 712704 build_flags = ${Heltec_t114.build_flags} + ${companion_rxps.build_flags} -I examples/companion_radio/ui-new -D DISPLAY_CLASS=NullDisplayDriver -D MAX_CONTACTS=350 @@ -122,6 +123,7 @@ board_build.ldscript = boards/nrf52840_s140_v6_extrafs.ld board_upload.maximum_size = 712704 build_flags = ${Heltec_t114.build_flags} + ${companion_rxps.build_flags} -I examples/companion_radio/ui-new -D DISPLAY_CLASS=NullDisplayDriver -D MAX_CONTACTS=350 @@ -210,6 +212,7 @@ board_build.ldscript = boards/nrf52840_s140_v6_extrafs.ld board_upload.maximum_size = 712704 build_flags = ${Heltec_t114_with_display.build_flags} + ${companion_rxps.build_flags} -I examples/companion_radio/ui-new -D MAX_CONTACTS=350 -D MAX_GROUP_CHANNELS=40 @@ -233,6 +236,7 @@ board_build.ldscript = boards/nrf52840_s140_v6_extrafs.ld board_upload.maximum_size = 712704 build_flags = ${Heltec_t114_with_display.build_flags} + ${companion_rxps.build_flags} -I examples/companion_radio/ui-new -D MAX_CONTACTS=350 -D MAX_GROUP_CHANNELS=40 diff --git a/variants/heltec_tracker_v2/platformio.ini b/variants/heltec_tracker_v2/platformio.ini index d914ce6a1b..ff5c02c260 100644 --- a/variants/heltec_tracker_v2/platformio.ini +++ b/variants/heltec_tracker_v2/platformio.ini @@ -136,6 +136,7 @@ lib_deps = extends = Heltec_tracker_v2 build_flags = ${Heltec_tracker_v2.build_flags} + ${companion_rxps.build_flags} -I examples/companion_radio/ui-new -D MAX_CONTACTS=350 -D MAX_GROUP_CHANNELS=40 @@ -155,6 +156,7 @@ lib_deps = extends = Heltec_tracker_v2 build_flags = ${Heltec_tracker_v2.build_flags} + ${companion_rxps.build_flags} -I examples/companion_radio/ui-new -D MAX_CONTACTS=350 -D MAX_GROUP_CHANNELS=40 @@ -184,6 +186,7 @@ platform_packages = framework-arduinoespressif32 @ symlink://D:/esp32-2.0.17 extends = Heltec_tracker_v2 build_flags = ${Heltec_tracker_v2.build_flags} + ${companion_rxps.build_flags} -I examples/companion_radio/ui-new -D MAX_CONTACTS=350 -D MAX_GROUP_CHANNELS=40 diff --git a/variants/heltec_v4/platformio.ini b/variants/heltec_v4/platformio.ini index 113ba24a04..ec90e72389 100644 --- a/variants/heltec_v4/platformio.ini +++ b/variants/heltec_v4/platformio.ini @@ -184,6 +184,7 @@ lib_deps = extends = heltec_v4_oled build_flags = ${heltec_v4_oled.build_flags} + ${companion_rxps.build_flags} -I examples/companion_radio/ui-new -D MAX_CONTACTS=350 -D MAX_GROUP_CHANNELS=40 @@ -203,6 +204,7 @@ lib_deps = extends = heltec_v4_oled build_flags = ${heltec_v4_oled.build_flags} + ${companion_rxps.build_flags} -I examples/companion_radio/ui-new -D MAX_CONTACTS=350 -D MAX_GROUP_CHANNELS=40 @@ -237,6 +239,7 @@ build_flags = extends = heltec_v4_oled build_flags = ${heltec_v4_oled.build_flags} + ${companion_rxps.build_flags} -I examples/companion_radio/ui-new -D MAX_CONTACTS=350 -D MAX_GROUP_CHANNELS=40 diff --git a/variants/rak4631/platformio.ini b/variants/rak4631/platformio.ini index 8f72999af4..75d4502d77 100644 --- a/variants/rak4631/platformio.ini +++ b/variants/rak4631/platformio.ini @@ -122,6 +122,7 @@ board_build.ldscript = boards/nrf52840_s140_v6_extrafs.ld board_upload.maximum_size = 712704 build_flags = ${rak4631.build_flags} + ${companion_rxps.build_flags} -I examples/companion_radio/ui-new -D PIN_USER_BTN=9 -D PIN_USER_BTN_ANA=31 @@ -143,6 +144,7 @@ board_build.ldscript = boards/nrf52840_s140_v6_extrafs.ld board_upload.maximum_size = 712704 build_flags = ${rak4631.build_flags} + ${companion_rxps.build_flags} -I examples/companion_radio/ui-new -D PIN_USER_BTN=9 -D PIN_USER_BTN_ANA=31 diff --git a/variants/t1000-e/platformio.ini b/variants/t1000-e/platformio.ini index 43a3d93f61..7e662fbc00 100644 --- a/variants/t1000-e/platformio.ini +++ b/variants/t1000-e/platformio.ini @@ -73,6 +73,7 @@ extends = t1000-e board_build.ldscript = boards/nrf52840_s140_v7_extrafs.ld board_upload.maximum_size = 708608 build_flags = ${t1000-e.build_flags} + ${companion_rxps.build_flags} -I examples/companion_radio/ui-orig -D MAX_CONTACTS=350 -D MAX_GROUP_CHANNELS=40 @@ -96,6 +97,7 @@ extends = t1000-e board_build.ldscript = boards/nrf52840_s140_v7_extrafs.ld board_upload.maximum_size = 708608 build_flags = ${t1000-e.build_flags} + ${companion_rxps.build_flags} -I examples/companion_radio/ui-orig -D MAX_CONTACTS=350 -D MAX_GROUP_CHANNELS=40 diff --git a/variants/thinknode_m3/platformio.ini b/variants/thinknode_m3/platformio.ini index 0a3d4eda92..df6806f743 100644 --- a/variants/thinknode_m3/platformio.ini +++ b/variants/thinknode_m3/platformio.ini @@ -75,6 +75,7 @@ extends = ThinkNode_M3 board_build.ldscript = boards/nrf52840_s140_v6_extrafs.ld board_upload.maximum_size = 708608 build_flags = ${ThinkNode_M3.build_flags} + ${companion_rxps.build_flags} -I examples/companion_radio/ui-orig -D MAX_CONTACTS=350 -D MAX_GROUP_CHANNELS=40 @@ -98,6 +99,7 @@ extends = ThinkNode_M3 board_build.ldscript = boards/nrf52840_s140_v6_extrafs.ld board_upload.maximum_size = 708608 build_flags = ${ThinkNode_M3.build_flags} + ${companion_rxps.build_flags} -I examples/companion_radio/ui-orig -D MAX_CONTACTS=350 -D MAX_GROUP_CHANNELS=40 diff --git a/variants/xiao_nrf52/platformio.ini b/variants/xiao_nrf52/platformio.ini index f4d1b93e6b..44fcc56f36 100644 --- a/variants/xiao_nrf52/platformio.ini +++ b/variants/xiao_nrf52/platformio.ini @@ -46,6 +46,7 @@ board_build.ldscript = boards/nrf52840_s140_v7_extrafs.ld board_upload.maximum_size = 708608 build_flags = ${Xiao_nrf52.build_flags} + ${companion_rxps.build_flags} -I examples/companion_radio/ui-orig -D MAX_CONTACTS=350 -D MAX_GROUP_CHANNELS=40 @@ -69,6 +70,7 @@ board_build.ldscript = boards/nrf52840_s140_v7_extrafs.ld board_upload.maximum_size = 708608 build_flags = ${Xiao_nrf52.build_flags} + ${companion_rxps.build_flags} -I examples/companion_radio/ui-orig -D MAX_CONTACTS=350 -D MAX_GROUP_CHANNELS=40 diff --git a/variants/xiao_s3/platformio.ini b/variants/xiao_s3/platformio.ini index b59e69ae43..47315b5ffe 100644 --- a/variants/xiao_s3/platformio.ini +++ b/variants/xiao_s3/platformio.ini @@ -94,6 +94,7 @@ lib_deps = extends = Xiao_S3 build_flags = ${Xiao_S3.build_flags} + ${companion_rxps.build_flags} -I examples/companion_radio/ui-new -D MAX_CONTACTS=350 -D MAX_GROUP_CHANNELS=40 @@ -122,6 +123,7 @@ platform_packages = framework-arduinoespressif32 @ symlink://D:/esp32-2.0.17 extends = Xiao_S3 build_flags = ${Xiao_S3.build_flags} + ${companion_rxps.build_flags} -I examples/companion_radio/ui-new -D MAX_CONTACTS=350 -D MAX_GROUP_CHANNELS=40 diff --git a/variants/xiao_s3_wio/platformio.ini b/variants/xiao_s3_wio/platformio.ini index 6686de55d2..ca28a28593 100644 --- a/variants/xiao_s3_wio/platformio.ini +++ b/variants/xiao_s3_wio/platformio.ini @@ -132,6 +132,7 @@ lib_deps = extends = Xiao_S3_WIO build_flags = ${Xiao_S3_WIO.build_flags} + ${companion_rxps.build_flags} -I examples/companion_radio/ui-new -D MAX_CONTACTS=350 -D MAX_GROUP_CHANNELS=40 @@ -154,6 +155,7 @@ lib_deps = extends = Xiao_S3_WIO build_flags = ${Xiao_S3_WIO.build_flags} + ${companion_rxps.build_flags} -I examples/companion_radio/ui-new -D MAX_CONTACTS=350 -D MAX_GROUP_CHANNELS=40 @@ -182,6 +184,7 @@ platform_packages = framework-arduinoespressif32 @ symlink://D:/esp32-2.0.17 extends = Xiao_S3_WIO build_flags = ${Xiao_S3_WIO.build_flags} + ${companion_rxps.build_flags} -I examples/companion_radio/ui-new -D MAX_CONTACTS=350 -D MAX_GROUP_CHANNELS=40 @@ -210,6 +213,7 @@ build_src_filter = ${Xiao_S3_WIO.build_src_filter} +<../examples/companion_radio/*.cpp> build_flags = ${Xiao_S3_WIO.build_flags} + ${companion_rxps.build_flags} -I examples/companion_radio/ui-new -D MAX_CONTACTS=350 -D MAX_GROUP_CHANNELS=40