From b984fdcb4c836472d9975266502020af57383c4a Mon Sep 17 00:00:00 2001 From: mikecarper Date: Mon, 24 Aug 2026 15:10:54 -0700 Subject: [PATCH] Harden fast RXPS timing and board behavior --- docs/cli_commands.md | 24 ++- docs/terminal_chat_cli.md | 31 ++- examples/companion_radio/MyMesh.cpp | 84 +++++++- examples/companion_radio/MyMesh.h | 2 + examples/companion_radio/ui-new/UITask.cpp | 33 +++- examples/companion_radio/ui-new/icons.h | 12 +- examples/simple_repeater/MyMesh.h | 4 - src/Dispatcher.h | 1 + src/helpers/BaseChatMesh.cpp | 3 + src/helpers/ClientACL.h | 2 +- src/helpers/CommonCLI.cpp | 59 +++++- src/helpers/radiolib/CustomLR1110Wrapper.h | 9 + src/helpers/radiolib/CustomSX1262.h | 14 ++ src/helpers/radiolib/CustomSX1262Wrapper.h | 10 + src/helpers/radiolib/RXPowerSaving.h | 123 +++++++++++- src/helpers/radiolib/RadioLibWrappers.cpp | 18 +- src/helpers/radiolib/RadioLibWrappers.h | 11 +- .../sensors/EnvironmentSensorManager.cpp | 7 + test/README.md | 1 + .../test_rx_power_saving.cpp | 187 ++++++++++++++++++ variants/wio-tracker-l1/platformio.ini | 2 + 21 files changed, 607 insertions(+), 30 deletions(-) create mode 100644 test/test_rx_power_saving/test_rx_power_saving.cpp diff --git a/docs/cli_commands.md b/docs/cli_commands.md index 1e63fdb6..6617664c 100644 --- a/docs/cli_commands.md +++ b/docs/cli_commands.md @@ -658,7 +658,11 @@ capacity. Remote use follows the existing administrator-command permissions. **Default:** `869.525,250,11,5` -**Note:** Requires reboot to apply +**Note:** Requires reboot to apply. If RXPS is enabled and the saved minimum +level/preamble cannot safely cover the new radio timing, the command reply +reports the effective level and preamble, or `RXPS continuous-fast` when no +level through 10 is safe. Slower settings recalculate from the saved RXPS +minimum and return to it exactly when it is safe. --- @@ -721,6 +725,24 @@ capacity. Remote use follows the existing administrator-command permissions. - `del radioat` and `del tempradioat` delete all entries when `n` is omitted. - Each queue supports 3 entries. Scheduled entries are not saved across reboot. - `radioat` saves the new radio preferences when it fires. `tempradioat` applies temporarily, then reverts to the saved radio preferences. +- A successful scheduling reply notes any RXPS effective-level/preamble change + required by the scheduled tuple. `RXPS continuous-fast` means the tuple is + accepted but will use continuous receive because no safe duty-cycle level is + available. Returning to slower settings recalculates from the saved RXPS + minimum. + +On SX1262+TCXO boards, the tested fast-setting boundaries are: + +| SF | BW (kHz) | Effective preamble | Minimum effective RXPS level | Result | +|---:|---------:|-------------------:|-----------------------------:|:-------| +| 7 | 500 | 32 | 7 | RXPS duty cycling | +| 6 | 250 | 32 | 7 | RXPS duty cycling | +| 5 | 125 | 32 | 7 | RXPS duty cycling | +| 5 | 62.5 | 16 | 10 | RXPS duty cycling | + +SF7/BW500, SF6/BW250, and SF5/BW125 are timing-equivalent because each has a +256 us LoRa symbol. SF5/BW250 and SF5/BW500 use `continuous-fast` even with +preamble 32. --- diff --git a/docs/terminal_chat_cli.md b/docs/terminal_chat_cli.md index 745e89f8..a43429f8 100644 --- a/docs/terminal_chat_cli.md +++ b/docs/terminal_chat_cli.md @@ -107,7 +107,36 @@ get radio.rxps set radio.rxps {off|on|level 1-10 [preamble 16|32]|rx_us sleep_us} ``` Shows or changes LoRa receive duty cycling on supported radios. Fresh Cascade -builds select level 8 with a 16-symbol preamble. +builds select level 8 with a 16-symbol preamble. A configured level is the +minimum: when faster SF/BW settings shorten the timing window, firmware raises +the effective level only as far as needed, up to level 10. For SF5-SF8 it can +then use the radio's actual 32-symbol transmit preamble if no level is safe +under the legacy 16-symbol assumption. The configured values remain unchanged, +so every radio change recalculates from the saved minimum. A slower tuple +returns to that exact level when it is safe; otherwise firmware keeps using the +smallest safe effective level. + +If neither adjustment leaves enough time for the radio to wake, RXPS stays +logically enabled but receives continuously instead of rejecting the radio +setting or starting an invalid duty cycle. `get radio.rxps` reports this as +`mode=continuous-fast` and reports any `effective-level` or +`effective-preamble` adjustment. On SX1262 boards with a TCXO, SF5/BW125 works +at effective level 7 with preamble 32; SF5/BW250 and SF5/BW500 use the +continuous fallback. A later compatible SF/BW change resumes duty cycling +without another RXPS command. + +For the SX1262+TCXO boards tested here, these fast combinations are the useful +RXPS boundary profiles (CR does not change the RXPS preamble timing): + +| SF | BW (kHz) | Effective preamble | Minimum effective level | RX / sleep | +|---:|---------:|-------------------:|------------------------:|-----------:| +| 7 | 500 | 32 | 7 | 2731 / 6101 us | +| 6 | 250 | 32 | 7 | 2731 / 6101 us | +| 5 | 125 | 32 | 7 | 2731 / 6101 us | +| 5 | 62.5 | 16 | 10 | 4096 / 6272 us | + +SF5/BW250 and SF5/BW500 remain too fast for a true TCXO duty cycle even with +preamble 32, so they use `continuous-fast`. ``` get wifi.powersave diff --git a/examples/companion_radio/MyMesh.cpp b/examples/companion_radio/MyMesh.cpp index d3826343..ae206f49 100644 --- a/examples/companion_radio/MyMesh.cpp +++ b/examples/companion_radio/MyMesh.cpp @@ -1790,6 +1790,7 @@ bool MyMesh::scheduleTempRadio(float freq, float bw, uint8_t sf, uint8_t cr, _temp_radio_failures = 0; snprintf(reply, reply_size, "OK - temp params for %lu mins", (unsigned long)timeout_mins); + appendRxPowerSavingAdjustmentNote(reply, reply_size, sf, bw); return true; } @@ -4108,6 +4109,46 @@ bool MyMesh::applyAndSaveRxPowerSaving(const char* value, char* reply) { return true; } +void MyMesh::appendRxPowerSavingAdjustmentNote(char* reply, size_t reply_size, + uint8_t sf, float bw) const { + if (!reply || reply_size == 0 || !_prefs.rx_powersaving_enabled + || _prefs.rx_ps_level < 1 || _prefs.rx_ps_level > 10) { + return; + } + + uint32_t rx_us = 0; + uint32_t sleep_us = 0; + uint8_t effective_level = 0; + uint8_t effective_preamble = 0; + if (!recalcRxPowerSavingFromLevel( + _prefs.rx_ps_level, sf, bw, _prefs.rx_ps_preamble, + &rx_us, &sleep_us, &effective_level, &effective_preamble)) { + return; + } + + const size_t used = strlen(reply); + if (used >= reply_size - 1) return; + + if (rxPowerSavingUsesContinuousFallback(rx_us, sleep_us)) { + snprintf(reply + used, reply_size - used, + "; RXPS continuous-fast (no safe level %u-10)", + (unsigned)_prefs.rx_ps_level); + return; + } + + const uint8_t requested_preamble = _prefs.rx_ps_preamble == 0 + ? rxPowerSavingPreambleForSF(sf) : _prefs.rx_ps_preamble; + if (effective_level == _prefs.rx_ps_level + && effective_preamble == requested_preamble) { + return; + } + + snprintf(reply + used, reply_size - used, + "; RXPS effective level %u, preamble %u (saved minimum %u)", + (unsigned)effective_level, (unsigned)effective_preamble, + (unsigned)_prefs.rx_ps_level); +} + #ifdef ENABLE_USB_INTERFACE Stream& MyMesh::terminalOutput() { return _terminal_output != NULL ? *_terminal_output : Serial; @@ -4953,12 +4994,49 @@ void MyMesh::handleTerminalCommand(char* command) { if (!radio_driver.supportsRxPowerSaving()) { terminalOutput().print(" ERROR: RX power saving is unsupported on this radio\r\n"); } else { - terminalOutput().printf(" radio.rxps %s,level=%u,preamble=%u,rx=%lu,sleep=%lu\r\n", + uint8_t effective_level = 0; + uint8_t effective_preamble = 0; + uint32_t effective_rx_us = _prefs.rx_ps_rx_us; + uint32_t effective_sleep_us = _prefs.rx_ps_sleep_us; + uint8_t active_sf = _prefs.sf; + float active_bw = _prefs.bw; +#if defined(COMPANION_RADIO_FULL) + if (isTempRadioActive()) { + active_sf = _temp_radio_sf; + active_bw = _temp_radio_bw; + } +#endif + if (_prefs.rx_ps_level != 0) { + recalcRxPowerSavingFromLevel( + _prefs.rx_ps_level, active_sf, active_bw, + _prefs.rx_ps_preamble, &effective_rx_us, &effective_sleep_us, + &effective_level, &effective_preamble); + } + char effective_suffix[48] = {}; + const bool adjusted_level = effective_level != 0 + && effective_level != _prefs.rx_ps_level; + const bool adjusted_preamble = effective_preamble != 0 + && effective_preamble != _prefs.rx_ps_preamble; + if (adjusted_level && adjusted_preamble) { + snprintf(effective_suffix, sizeof(effective_suffix), + ",effective-level=%u,effective-preamble=%u", + (unsigned)effective_level, (unsigned)effective_preamble); + } else if (adjusted_level) { + snprintf(effective_suffix, sizeof(effective_suffix), + ",effective-level=%u", (unsigned)effective_level); + } else if (adjusted_preamble) { + snprintf(effective_suffix, sizeof(effective_suffix), + ",effective-preamble=%u", (unsigned)effective_preamble); + } + terminalOutput().printf(" radio.rxps %s,level=%u,preamble=%u,rx=%lu,sleep=%lu%s%s\r\n", _prefs.rx_powersaving_enabled ? "on" : "off", (unsigned)_prefs.rx_ps_level, (unsigned)_prefs.rx_ps_preamble, - (unsigned long)_prefs.rx_ps_rx_us, - (unsigned long)_prefs.rx_ps_sleep_us); + (unsigned long)effective_rx_us, + (unsigned long)effective_sleep_us, + radio_driver.isRxPowerSavingContinuousFallback() + ? ",mode=continuous-fast" : "", + effective_suffix); } #if defined(ESP32) && defined(WIFI_SSID) } else if (strcmp(command, "get wifi.powersave") == 0) { diff --git a/examples/companion_radio/MyMesh.h b/examples/companion_radio/MyMesh.h index 5a1c357b..7a98e91d 100644 --- a/examples/companion_radio/MyMesh.h +++ b/examples/companion_radio/MyMesh.h @@ -302,6 +302,8 @@ private: bool applyAndSaveRxBoostedGain(bool enabled); bool applyAndSavePowerSaving(const char* value, char* reply); bool applyAndSaveRxPowerSaving(const char* value, char* reply); + void appendRxPowerSavingAdjustmentNote(char* reply, size_t reply_size, + uint8_t sf, float bw) const; #if defined(ESP32) && defined(WIFI_SSID) bool applyAndSaveWiFiPowerSaving(const char* value, char* reply, size_t reply_size); diff --git a/examples/companion_radio/ui-new/UITask.cpp b/examples/companion_radio/ui-new/UITask.cpp index eefa14f8..bf78fbbf 100644 --- a/examples/companion_radio/ui-new/UITask.cpp +++ b/examples/companion_radio/ui-new/UITask.cpp @@ -109,7 +109,9 @@ class HomeScreen : public UIScreen { #if UI_SENSORS_PAGE == 1 SENSORS, #endif +#ifndef UI_NO_HIBERNATE SHUTDOWN, +#endif Count // keep as last }; @@ -165,9 +167,18 @@ class HomeScreen : public UIScreen { } display.setTextSize(1); + bool charging = board.isExternalPowered(); +#ifdef PIN_BUZZER + bool muted = _task->isBuzzerQuiet(); +#endif + int uptimeWidth = display.getTextWidth(uptime); int spaceWidth = display.getTextWidth(" "); - display.setCursor(iconX - uptimeWidth - spaceWidth, iconY); + int statusWidth = charging ? 9 : 0; +#ifdef PIN_BUZZER + if (muted) statusWidth += 9; +#endif + display.setCursor(iconX - statusWidth - uptimeWidth - spaceWidth, iconY); display.print(uptime); // battery outline @@ -180,11 +191,21 @@ class HomeScreen : public UIScreen { int fillWidth = (batteryPercentage * (iconWidth - 4)) / 100; display.fillRect(iconX + 2, iconY + 2, fillWidth, iconHeight - 4); - // show muted icon if buzzer is muted + // Show external power beside the battery. Most boards have no + // charge-complete signal, so use a high percentage band for the plug. + if (charging) { + static constexpr int BATT_FULL_PCT = 95; + const uint8_t* symbol = + batteryPercentage >= BATT_FULL_PCT ? plug_icon : charging_icon; + display.setColor(UIColor::title_txt); + display.drawXbm(iconX - 9, iconY + 1, symbol, 8, 8); + } + + // Keep the mute icon left of the charging icon when both are present. #ifdef PIN_BUZZER - if (_task->isBuzzerQuiet()) { + if (muted) { display.setColor(UIColor::warning_txt); - display.drawXbm(iconX - 9, iconY + 1, muted_icon, 8, 8); + display.drawXbm(iconX - (charging ? 18 : 9), iconY + 1, muted_icon, 8, 8); } #endif } @@ -487,6 +508,7 @@ public: if (sensors_scroll) sensors_scroll_offset = (sensors_scroll_offset+1)%sensors_nb; else sensors_scroll_offset = 0; #endif +#ifndef UI_NO_HIBERNATE } else if (_page == HomePage::SHUTDOWN) { display.setColor(UIColor::corp_blue); display.setTextSize(1); @@ -498,6 +520,7 @@ public: display.drawXbm((display.width() - 32) / 2, 18, power_icon, 32, 32); display.drawTextCentered(display.width() / 2, 64 - 11, "hibernate:" PRESS_LABEL); } +#endif } return 5000; // next render after 5000 ms } @@ -544,10 +567,12 @@ public: return true; } #endif +#ifndef UI_NO_HIBERNATE if (c == KEY_ENTER && _page == HomePage::SHUTDOWN) { _shutdown_init = true; // need to wait for button to be released return true; } +#endif return false; } }; diff --git a/examples/companion_radio/ui-new/icons.h b/examples/companion_radio/ui-new/icons.h index cbe23790..7eefc5b4 100644 --- a/examples/companion_radio/ui-new/icons.h +++ b/examples/companion_radio/ui-new/icons.h @@ -119,4 +119,14 @@ static const uint8_t advert_icon[] = { static const uint8_t muted_icon[] = { 0x20, 0x6a, 0xea, 0xe4, 0xe4, 0xea, 0x6a, 0x20 -}; \ No newline at end of file +}; + +// Small lightning bolt, 8x8px, shown while externally powered and charging. +static const uint8_t charging_icon[] = { + 0x18, 0x30, 0x60, 0xFC, 0x18, 0x30, 0x60, 0xC0 +}; + +// Small power plug, 8x8px, shown once the battery reaches the full band. +static const uint8_t plug_icon[] = { + 0x24, 0x24, 0x7E, 0x7E, 0x7E, 0x3C, 0x18, 0x18 +}; diff --git a/examples/simple_repeater/MyMesh.h b/examples/simple_repeater/MyMesh.h index 2acd4269..31bafbb9 100644 --- a/examples/simple_repeater/MyMesh.h +++ b/examples/simple_repeater/MyMesh.h @@ -125,10 +125,6 @@ struct RepeaterStats { uint32_t n_recv_errors; }; -#ifndef MAX_CLIENTS - #define MAX_CLIENTS 32 -#endif - struct NeighbourInfo { mesh::Identity id; uint32_t advert_timestamp; diff --git a/src/Dispatcher.h b/src/Dispatcher.h index 0c5ec830..4feaf43e 100644 --- a/src/Dispatcher.h +++ b/src/Dispatcher.h @@ -116,6 +116,7 @@ public: virtual bool supportsRxPowerSaving() const { return false; } virtual bool setRxPowerSaving(bool enabled, uint32_t rx_us, uint32_t sleep_us) { return !enabled; } + virtual bool isRxPowerSavingContinuousFallback() const { return false; } virtual bool supportsRxPowerSavingRfRxDisable() const { return false; } virtual bool setRxPowerSavingRfRxDisabled(bool disabled) { (void)disabled; diff --git a/src/helpers/BaseChatMesh.cpp b/src/helpers/BaseChatMesh.cpp index f9af2b17..899c4c37 100644 --- a/src/helpers/BaseChatMesh.cpp +++ b/src/helpers/BaseChatMesh.cpp @@ -415,6 +415,9 @@ void BaseChatMesh::handleReturnPathRetry(const ContactInfo& contact, const uint8 int BaseChatMesh::searchChannelsByHash(const uint8_t* hash, mesh::GroupChannel dest[], int max_matches) { int n = 0; for (int i = 0; i < MAX_GROUP_CHANNELS && n < max_matches; i++) { + // Empty slots have an all-zero secret and can otherwise validate null-key + // traffic as if it belonged to a configured local channel. + if (channels[i].name[0] == 0) continue; if (channels[i].channel.hash[0] == hash[0]) { dest[n++] = channels[i].channel; } diff --git a/src/helpers/ClientACL.h b/src/helpers/ClientACL.h index 34f214f8..555b7a0d 100644 --- a/src/helpers/ClientACL.h +++ b/src/helpers/ClientACL.h @@ -43,7 +43,7 @@ struct ClientInfo { }; #ifndef MAX_CLIENTS - #define MAX_CLIENTS 20 + #define MAX_CLIENTS 32 #endif class ClientACL { diff --git a/src/helpers/CommonCLI.cpp b/src/helpers/CommonCLI.cpp index 735da909..f3f444e4 100644 --- a/src/helpers/CommonCLI.cpp +++ b/src/helpers/CommonCLI.cpp @@ -453,7 +453,50 @@ bool CommonCLI::calculateRxPowerSavingLevel(uint32_t level, uint8_t sf, float bw if (level < 1 || level > 10 || (preamble != 16 && preamble != 32)) { return false; } - return calcRxPowerSavingLevel((uint8_t)level, sf, bw, (uint8_t)preamble, rx_us, sleep_us); + return calcRxPowerSavingLevelAtOrAbove( + (uint8_t)level, sf, bw, (uint8_t)preamble, + rxPowerSavingDefaultTransitionUs(), rx_us, sleep_us); +} + +static void appendRxPowerSavingAdjustmentNote(char* reply, const NodePrefs* prefs, + uint8_t sf, float bw) { + if (!reply || !prefs || !prefs->rx_powersaving_enabled + || prefs->rx_ps_level < 1 || prefs->rx_ps_level > 10) { + return; + } + + uint32_t rx_us = 0; + uint32_t sleep_us = 0; + uint8_t effective_level = 0; + uint8_t effective_preamble = 0; + if (!recalcRxPowerSavingFromLevel( + prefs->rx_ps_level, sf, bw, prefs->rx_ps_preamble, + &rx_us, &sleep_us, &effective_level, &effective_preamble)) { + return; + } + + char note[96] = {}; + if (rxPowerSavingUsesContinuousFallback(rx_us, sleep_us)) { + snprintf(note, sizeof(note), + "; RXPS continuous-fast (no safe level %u-10)", + (unsigned)prefs->rx_ps_level); + } else { + const uint8_t requested_preamble = prefs->rx_ps_preamble == 0 + ? rxPowerSavingPreambleForSF(sf) : prefs->rx_ps_preamble; + if (effective_level == prefs->rx_ps_level + && effective_preamble == requested_preamble) { + return; + } + snprintf(note, sizeof(note), + "; RXPS effective level %u, preamble %u (saved minimum %u)", + (unsigned)effective_level, (unsigned)effective_preamble, + (unsigned)prefs->rx_ps_level); + } + + const size_t used = strlen(reply); + if (used < 159) { + snprintf(reply + used, 160 - used, "%s", note); + } } // Recomputes rx_ps_rx_us/rx_ps_sleep_us from the stored level and the current @@ -2409,6 +2452,7 @@ void CommonCLI::handleCommand(uint32_t sender_timestamp, char* command, char* re && isValidLoRaBandwidth(bw) && temp_timeout_mins > 0) { _callbacks->applyTempRadioParams(freq, bw, sf, cr, temp_timeout_mins); sprintf(reply, "OK - temp params for %d mins", temp_timeout_mins); + appendRxPowerSavingAdjustmentNote(reply, _prefs, sf, bw); } else { strcpy(reply, "Error, invalid params"); } @@ -3464,6 +3508,8 @@ void CommonCLI::handleSetCmd(uint32_t sender_timestamp, char* command, char* rep (unsigned long)rx_us, (unsigned long)sleep_us, (unsigned long)preamble); + appendRxPowerSavingAdjustmentNote( + reply, _prefs, _prefs->sf, _prefs->bw); } else { sprintf(reply, "OK - %s,%lu,%lu", enable ? "on" : "off", (unsigned long)rx_us, (unsigned long)sleep_us); @@ -3495,11 +3541,12 @@ void CommonCLI::handleSetCmd(uint32_t sender_timestamp, char* command, char* rep _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( + recalcRxPowerSavingFromLevel( _prefs->rx_ps_level, _prefs->sf, _prefs->bw, _prefs->rx_ps_preamble, &_prefs->rx_ps_rx_us, &_prefs->rx_ps_sleep_us); // retune level-based timings to the loaded SF/BW _callbacks->savePrefs(); - strcpy(reply, rxps_retuned ? "OK - reboot to apply (rxps retuned)" : "OK - reboot to apply"); + strcpy(reply, "OK - reboot to apply"); + appendRxPowerSavingAdjustmentNote(reply, _prefs, sf, bw); } else { strcpy(reply, "Error, invalid radio params"); } @@ -3513,6 +3560,9 @@ void CommonCLI::handleSetCmd(uint32_t sender_timestamp, char* command, char* rep strcpy(reply, "Error, invalid radio params"); } else { _callbacks->addScheduledRadioParams(false, freq, bw, sf, cr, start_time, end_time, reply); + if (strncmp(reply, "OK", 2) == 0) { + appendRxPowerSavingAdjustmentNote(reply, _prefs, sf, bw); + } } } else if (memcmp(config, "tempradioat ", 12) == 0) { float freq, bw; @@ -3524,6 +3574,9 @@ void CommonCLI::handleSetCmd(uint32_t sender_timestamp, char* command, char* rep strcpy(reply, "Error, invalid radio params"); } else { _callbacks->addScheduledRadioParams(true, freq, bw, sf, cr, start_time, end_time, reply); + if (strncmp(reply, "OK", 2) == 0) { + appendRxPowerSavingAdjustmentNote(reply, _prefs, sf, bw); + } } } else if (memcmp(config, "lat ", 4) == 0) { float latitude = 0.0f; diff --git a/src/helpers/radiolib/CustomLR1110Wrapper.h b/src/helpers/radiolib/CustomLR1110Wrapper.h index dc1fb408..79527cf5 100644 --- a/src/helpers/radiolib/CustomLR1110Wrapper.h +++ b/src/helpers/radiolib/CustomLR1110Wrapper.h @@ -99,9 +99,17 @@ protected: if (!_rx_ps_enabled || _nf_calib_active) { // plain continuous RX: powersaving off, or a periodic noise-floor // calibration window is in progress + if (!_rx_ps_enabled) _rx_ps_continuous_fallback = false; return _radio->startReceive(); } + if (rxPowerSavingUsesContinuousFallback(_rx_ps_rx_us, _rx_ps_sleep_us)) { + _rx_ps_continuous_fallback = true; + return _radio->startReceive(); + } + + _rx_ps_continuous_fallback = false; + 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 @@ -117,6 +125,7 @@ protected: return err; } + _rx_ps_continuous_fallback = true; MESH_DEBUG_PRINTLN("CustomLR1110Wrapper: error: startReceiveDutyCycle(%d), falling back to continuous RX", err); return _radio->startReceive(); } diff --git a/src/helpers/radiolib/CustomSX1262.h b/src/helpers/radiolib/CustomSX1262.h index 18918a48..e96073d2 100644 --- a/src/helpers/radiolib/CustomSX1262.h +++ b/src/helpers/radiolib/CustomSX1262.h @@ -2,6 +2,7 @@ #include #include "MeshCore.h" +#include "RXPowerSaving.h" #ifndef SX126X_TX_BUSY_TIMEOUT_MS #define SX126X_TX_BUSY_TIMEOUT_MS 1000UL @@ -13,6 +14,7 @@ class CustomSX1262 : public SX1262 { uint32_t _activityAt = 0; bool _headerSeen = false; bool _rx_ps_rf_rx_disabled = false; + uint32_t _rxDutyCycleTransitionUs = 1000; public: CustomSX1262(Module *mod) : SX1262(mod) { } @@ -100,6 +102,11 @@ class CustomSX1262 : public SX1262 { mesh::usbLoggingPort().println(status); return false; // fail } + + // RadioLib's default DIO3 TCXO delay is 5 ms and its RX duty-cycle + // command adds another 1 ms for sleep/wake transitions. If begin() + // fell back to a crystal, only the fixed 1 ms transition remains. + _rxDutyCycleTransitionUs = tcxo > 0.0f ? 6000UL : 1000UL; setCRC(1); @@ -268,6 +275,7 @@ class CustomSX1262 : public SX1262 { } return true; } + if (preamble) { if (_activityAt == 0) _activityAt = now; if (now - _activityAt > _preambleMillis) { @@ -283,6 +291,12 @@ class CustomSX1262 : public SX1262 { return false; } + bool canUseRxPowerSavingDutyCycle(uint32_t rx_us, uint32_t sleep_us) const { + return !rxPowerSavingUsesContinuousFallback(rx_us, sleep_us) + && canStartRxPowerSavingDutyCycle( + rx_us, sleep_us, _rxDutyCycleTransitionUs); + } + void setPreambleMillis(uint32_t preambleMillis) { _preambleMillis = preambleMillis; MESH_DEBUG_PRINTLN("Set _preambleMillis=%u", _preambleMillis); diff --git a/src/helpers/radiolib/CustomSX1262Wrapper.h b/src/helpers/radiolib/CustomSX1262Wrapper.h index 7264f23f..9fc9b71e 100644 --- a/src/helpers/radiolib/CustomSX1262Wrapper.h +++ b/src/helpers/radiolib/CustomSX1262Wrapper.h @@ -113,9 +113,18 @@ protected: if (!_rx_ps_enabled || _nf_calib_active) { // plain continuous RX: powersaving off, or a periodic noise-floor // calibration window is in progress + if (!_rx_ps_enabled) _rx_ps_continuous_fallback = false; return _radio->startReceive(); } + if (!((CustomSX1262 *)_radio)->canUseRxPowerSavingDutyCycle( + _rx_ps_rx_us, _rx_ps_sleep_us)) { + _rx_ps_continuous_fallback = true; + return _radio->startReceive(); + } + + _rx_ps_continuous_fallback = false; + const RadioLibIrqFlags_t irqFlags = RADIOLIB_IRQ_RX_DEFAULT_FLAGS; const RadioLibIrqFlags_t irqMask = (1UL << RADIOLIB_IRQ_RX_DONE) | @@ -129,6 +138,7 @@ protected: return err; } + _rx_ps_continuous_fallback = true; MESH_DEBUG_PRINTLN("CustomSX1262Wrapper: error: startReceiveDutyCycle(%d), falling back to continuous RX", err); return _radio->startReceive(); } diff --git a/src/helpers/radiolib/RXPowerSaving.h b/src/helpers/radiolib/RXPowerSaving.h index a7559a02..a3d0a390 100644 --- a/src/helpers/radiolib/RXPowerSaving.h +++ b/src/helpers/radiolib/RXPowerSaving.h @@ -6,6 +6,13 @@ #define RX_POWERSAVING_MIN_PERIOD_US 1000UL #define RX_POWERSAVING_MAX_PERIOD_US 30000000UL +// A level-derived timing pair equal to this value means the requested LoRa +// tuple is too fast for a safe duty cycle. Keep the operator's RXPS preference +// enabled, but receive continuously until a later SF/BW change produces usable +// timings. Using an in-range pair lets the existing radio-parameter API carry +// the fallback without persisting invalid sub-millisecond periods. +#define RX_POWERSAVING_CONTINUOUS_FALLBACK_US RX_POWERSAVING_MIN_PERIOD_US + // 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. @@ -59,6 +66,31 @@ inline bool isValidRxPowerSavingPeriod(uint32_t us) { return us >= RX_POWERSAVING_MIN_PERIOD_US && us <= RX_POWERSAVING_MAX_PERIOD_US; } +inline bool rxPowerSavingUsesContinuousFallback(uint32_t rx_us, uint32_t sleep_us) { + return rx_us == RX_POWERSAVING_CONTINUOUS_FALLBACK_US + && sleep_us == RX_POWERSAVING_CONTINUOUS_FALLBACK_US; +} + +inline uint32_t rxPowerSavingDefaultTransitionUs() { +#if defined(SX126X_DIO3_TCXO_VOLTAGE) + // RadioLib uses a 5 ms DIO3 TCXO delay plus 1 ms for sleep/wake. + return 6000UL; +#else + // Crystal and non-SX126x implementations still need transition headroom. + return 1000UL; +#endif +} + +// Mirrors the SX126x/RF duty-cycle timer conversion: both encoded periods must +// contain at least one 15.625 us tick after the radio/TCXO transition time has +// been removed from the sleep period. +inline bool canStartRxPowerSavingDutyCycle(uint32_t rx_us, uint32_t sleep_us, + uint32_t transition_us) { + if (sleep_us <= transition_us) return false; + return ((rx_us * 8UL) / 125UL) != 0 + && (((sleep_us - transition_us) * 8UL) / 125UL) != 0; +} + // 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. @@ -82,7 +114,8 @@ inline uint32_t ceilPositiveFloat(float value) { inline bool calcRxPowerSavingLevel(uint8_t level, uint8_t sf, float bw, uint8_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)) { + if (!rx_us || !sleep_us || level < 1 || level > 10 || sf < 5 || sf > 12 + || bw <= 0.0f || (preamble != 16 && preamble != 32)) { return false; } @@ -96,8 +129,83 @@ inline bool calcRxPowerSavingLevel(uint8_t level, uint8_t sf, float bw, uint8_t 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); + uint32_t calculated_rx_us = ceilPositiveFloat(rx_symbols * symbol_us); + uint32_t calculated_sleep_us = (uint32_t)(sleep_symbols * symbol_us); + + // At high-rate settings (notably SF5/BW500), the entire preamble can be + // shorter than the radio's sleep/wake transition. Scaling either period up + // would break the preamble-overlap guarantee and silently lose packets. + // Canonicalize that case to continuous RX instead; the wrapper will resume + // a real duty cycle automatically when slower parameters are restored. + if (calculated_rx_us < RX_POWERSAVING_MIN_PERIOD_US + || calculated_sleep_us < RX_POWERSAVING_MIN_PERIOD_US) { + calculated_rx_us = RX_POWERSAVING_CONTINUOUS_FALLBACK_US; + calculated_sleep_us = RX_POWERSAVING_CONTINUOUS_FALLBACK_US; + } + + *rx_us = calculated_rx_us; + *sleep_us = calculated_sleep_us; + return true; +} + +// Treat requested_level as the operator's minimum. Faster tuples shorten each +// LoRa symbol, so progressively more aggressive levels are tried until the +// sleep window is long enough to cover the radio/TCXO transition. The +// configured level itself is not changed; callers persist only the effective +// timings, allowing slower tuples to return to the requested level later. +// effective_level and effective_preamble are 0 when no combination can safely +// duty-cycle and continuous RX is required for this tuple. +inline bool calcRxPowerSavingLevelAtOrAbove( + uint8_t requested_level, uint8_t sf, float bw, uint8_t preamble, + uint32_t transition_us, uint32_t *rx_us, uint32_t *sleep_us, + uint8_t *effective_level = nullptr, + uint8_t *effective_preamble = nullptr) { + if (!rx_us || !sleep_us || requested_level < 1 || requested_level > 10 + || sf < 5 || sf > 12 || bw <= 0.0f + || (preamble != 16 && preamble != 32)) { + return false; + } + + // Keep the configured/legacy preamble assumption if any level can satisfy + // it. For SF5-SF8 the radio already transmits 32 symbols, so a second pass + // may use that real preamble length when 16 symbols cannot cover the TCXO + // transition at a faster tuple. + const bool can_try_32 = preamble == 16 + && rxPowerSavingPreambleForSF(sf) == 32; + const uint8_t pass_count = can_try_32 ? 2 : 1; + for (uint8_t pass = 0; pass < pass_count; pass++) { + const uint8_t candidate_preamble = pass == 0 ? preamble : 32; + for (uint8_t level = requested_level; level <= 10; level++) { + const float symbol_us = (1000.0f * (float)(1UL << sf)) / bw; + const float amount = (float)(level - 1) / 9.0f; + const float rx_start_symbols = candidate_preamble == 16 ? 12.0f : 16.0f; + const float sleep_start_symbols = candidate_preamble == 16 ? 2.0f : 15.0f; + const float rx_edge_symbols = 8.0f; + const float sleep_edge_symbols = (float)candidate_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); + const uint32_t candidate_rx_us = ceilPositiveFloat(rx_symbols * symbol_us); + const uint32_t candidate_sleep_us = (uint32_t)(sleep_symbols * symbol_us); + + if (isValidRxPowerSavingPeriod(candidate_rx_us) + && isValidRxPowerSavingPeriod(candidate_sleep_us) + && canStartRxPowerSavingDutyCycle( + candidate_rx_us, candidate_sleep_us, transition_us)) { + *rx_us = candidate_rx_us; + *sleep_us = candidate_sleep_us; + if (effective_level) *effective_level = level; + if (effective_preamble) *effective_preamble = candidate_preamble; + return true; + } + } + } + + *rx_us = RX_POWERSAVING_CONTINUOUS_FALLBACK_US; + *sleep_us = RX_POWERSAVING_CONTINUOUS_FALLBACK_US; + if (effective_level) *effective_level = 0; + if (effective_preamble) *effective_preamble = 0; return true; } @@ -115,7 +223,10 @@ inline void ensureRxPowerSavingDefaults(uint32_t *rx_ps_rx_us, uint32_t *rx_ps_s // 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. inline bool recalcRxPowerSavingFromLevel(uint8_t level, uint8_t sf, float bw, uint8_t preamble, - uint32_t *rx_ps_rx_us, uint32_t *rx_ps_sleep_us) { + uint32_t *rx_ps_rx_us, uint32_t *rx_ps_sleep_us, + uint8_t *effective_level = nullptr, + uint8_t *effective_preamble = nullptr, + uint32_t transition_us = rxPowerSavingDefaultTransitionUs()) { if (level < 1 || level > 10) { return false; // manual: nothing to recompute } @@ -125,7 +236,9 @@ inline bool recalcRxPowerSavingFromLevel(uint8_t level, uint8_t sf, float bw, ui } uint32_t rx_us, sleep_us; - if (!calcRxPowerSavingLevel(level, sf, bw, preamble, &rx_us, &sleep_us)) { + if (!calcRxPowerSavingLevelAtOrAbove( + level, sf, bw, preamble, transition_us, &rx_us, &sleep_us, + effective_level, effective_preamble)) { return false; } diff --git a/src/helpers/radiolib/RadioLibWrappers.cpp b/src/helpers/radiolib/RadioLibWrappers.cpp index 423aec77..d3c891f6 100644 --- a/src/helpers/radiolib/RadioLibWrappers.cpp +++ b/src/helpers/radiolib/RadioLibWrappers.cpp @@ -150,6 +150,8 @@ mesh::RadioParamApplyResult RadioLibWrapper::trySetParams(float freq, float bw, _rx_ps_enabled = true; _rx_ps_rx_us = rx_ps_timings[0]; _rx_ps_sleep_us = rx_ps_timings[1]; + _rx_ps_continuous_fallback = rxPowerSavingUsesContinuousFallback( + _rx_ps_rx_us, _rx_ps_sleep_us); } } else { bool restored = had_previous_params @@ -424,14 +426,16 @@ void RadioLibWrapper::endNoiseFloorCalib(unsigned long now) { } void RadioLibWrapper::loop() { - if (_rx_ps_enabled) { + if (_rx_ps_enabled && !_rx_ps_continuous_fallback) { rxPsWatchdogCheck(); } unsigned long now = millis(); if (_nf_calib_active || _nf_refresh_requested) { noiseFloorCalibCheck(now); } - if (_nf_refresh_requested && !_rx_ps_enabled && _nf_calib_deadline == 0 + if (_nf_refresh_requested + && (!_rx_ps_enabled || _rx_ps_continuous_fallback) + && _nf_calib_deadline == 0 && state == STATE_RX) { // Measure the awake-time bound from actual continuous RX, not from begin() // or a request made while the radio is idle, transmitting, or starting up. @@ -467,7 +471,9 @@ void RadioLibWrapper::loop() { return; } - if (_nf_refresh_requested && !_rx_ps_enabled && _nf_calib_deadline != 0 + if (_nf_refresh_requested + && (!_rx_ps_enabled || _rx_ps_continuous_fallback) + && _nf_calib_deadline != 0 && (long)(now - _nf_calib_deadline) >= 0) { // A continuously busy channel can reject every candidate sample. Do not // keep the MCU awake indefinitely; retain the previous floor and let the @@ -567,6 +573,8 @@ bool RadioLibWrapper::setRxPowerSaving(bool enabled, uint32_t rx_us, uint32_t sl _rx_ps_enabled = enabled; _rx_ps_rx_us = rx_us; _rx_ps_sleep_us = sleep_us; + _rx_ps_continuous_fallback = enabled + && rxPowerSavingUsesContinuousFallback(rx_us, sleep_us); endReconfigure(resume_rx); return true; } @@ -607,7 +615,7 @@ int RadioLibWrapper::recvRaw(uint8_t* bytes, int sz) { #endif } - if (len > 0 && _rx_ps_enabled) { + if (len > 0 && _rx_ps_enabled && !_rx_ps_continuous_fallback) { // Dispatcher still needs the just-cached RSSI/SNR. Keep the receiver in // ordinary continuous mode until it has parsed and scored this packet, // then onReceiveProcessed() restores the duty cycle. @@ -644,7 +652,7 @@ void RadioLibWrapper::finishReceiveProcessing() { } _rx_hold_continuous = false; - if (!_rx_ps_enabled || _nf_calib_active) return; + if (!_rx_ps_enabled || _rx_ps_continuous_fallback || _nf_calib_active) return; state = STATE_IDLE; startRecv(); diff --git a/src/helpers/radiolib/RadioLibWrappers.h b/src/helpers/radiolib/RadioLibWrappers.h index e0d0c6af..4a0d7362 100644 --- a/src/helpers/radiolib/RadioLibWrappers.h +++ b/src/helpers/radiolib/RadioLibWrappers.h @@ -4,6 +4,7 @@ #include #include "CadTiming.h" #include "RadioAirtime.h" +#include "RXPowerSaving.h" // Fallback RX powersaving timings, only used until setRxPowerSaving() is called // (begin() always applies the persisted values). The authoritative defaults live @@ -40,6 +41,7 @@ protected: uint8_t _preamble_sf; bool _rx_ps_enabled; bool _rx_ps_armed; // radio is currently in RX duty-cycle mode + bool _rx_ps_continuous_fallback; // requested RXPS is receiving continuously for this tuple bool _rx_hold_continuous; // keep plain RX active until Dispatcher consumes cached metadata uint32_t _rx_ps_rx_us; uint32_t _rx_ps_sleep_us; @@ -119,7 +121,7 @@ public: RadioLibWrapper(PhysicalLayer& radio, mesh::MainBoard& board) : _radio(&radio), _board(&board), _noise_floor_valid(false), _nf_refresh_requested(true), _preamble_sf(0), _rx_ps_enabled(false), _rx_ps_armed(false), - _rx_hold_continuous(false), + _rx_ps_continuous_fallback(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), @@ -142,6 +144,9 @@ public: void onSendFinished() override; bool isInRecvMode() const override; bool setRxPowerSaving(bool enabled, uint32_t rx_us, uint32_t sleep_us) override; + bool isRxPowerSavingContinuousFallback() const override { + return _rx_ps_enabled && _rx_ps_continuous_fallback; + } virtual bool supportsRxPowerSavingRfRxDisable() const { return false; } virtual bool setRxPowerSavingRfRxDisabled(bool) { return false; } virtual bool isRxPowerSavingRfRxDisabled() const { return false; } @@ -196,7 +201,9 @@ public: // true while a noise-floor batch needs prompt loop service; the app's // hasPendingWork() keeps the MCU awake only for this short sample burst. bool isCalibratingNoiseFloor() const { - return _nf_calib_active || (_nf_refresh_requested && !_rx_ps_enabled); + return _nf_calib_active + || (_nf_refresh_requested + && (!_rx_ps_enabled || _rx_ps_continuous_fallback)); } void resetStats() { n_recv = n_sent = n_recv_errors = 0; } diff --git a/src/helpers/sensors/EnvironmentSensorManager.cpp b/src/helpers/sensors/EnvironmentSensorManager.cpp index 4f51a8de..98fca471 100644 --- a/src/helpers/sensors/EnvironmentSensorManager.cpp +++ b/src/helpers/sensors/EnvironmentSensorManager.cpp @@ -924,11 +924,16 @@ bool EnvironmentSensorManager::gpsIsAwake(uint8_t ioPin) { } #endif + // RAK3401 shares WB_IO2 with the radio PA supply. Probing a GPS must never + // pulse that rail low or turn the pin back into an input. +#ifndef RAK_3401 // set initial waking state pinMode(ioPin, OUTPUT); digitalWrite(ioPin, LOW); delay(500); digitalWrite(ioPin, HIGH); +#endif + // give the receiver time to become responsive delay(500); // Try to init RAK12500 on I2C @@ -961,7 +966,9 @@ bool EnvironmentSensorManager::gpsIsAwake(uint8_t ioPin) { return true; } +#ifndef RAK_3401 pinMode(ioPin, INPUT); +#endif MESH_DEBUG_PRINTLN("GPS did not init with this IO pin... try the next"); return false; } diff --git a/test/README.md b/test/README.md index 3354c6ac..06fcfaaa 100644 --- a/test/README.md +++ b/test/README.md @@ -63,6 +63,7 @@ does not reflect the GoogleTest count -- run the built binary directly | `test_packet_manager` | `src/Packet.cpp`, `src/Dispatcher.cpp`, `src/helpers/StaticPoolPacketManager.cpp` | truncated-packet rejection, unavailable-radio behavior, scoped RX-delay replacement, queue/CAD scheduling, and staged radio/TX recovery | | `test_persistent_store_format` | `src/helpers/PersistentStoreFormat.h` | contact-page headers and CRCs, dirty-page state, stable slot allocation, and bounded resumable legacy migration across power loss | | `test_power_management` | `src/helpers/PowerManagementUtils.h` | median filtering of a brownout outlier and valid-reading requirements for the boot lock | +| `test_rx_power_saving` | `src/helpers/radiolib/RXPowerSaving.h` | level-derived timing, automatic level/preamble selection, equivalent SF7/BW500, SF6/BW250, and SF5/BW125 profiles, SF5/BW62.5 with preamble 16, SF5/BW250/BW500 continuous-RX fallback, automatic duty-cycle resume, and SX1262 TCXO timing thresholds | | `test_region_names` | `src/helpers/RegionNameUtils.h` | canonical public-region markers while preserving distinct private and differently named regions | | `test_routing_policy` | `src/helpers/RoutingPolicy.h` | scoped/unscoped flood hop limits and selection of direct, path-return, mirrored-scope, default-scope, or unscoped replies | | `test_rs232_uart` | `src/helpers/bridges/RS232UartUtils.h` | stopping the active UART peripheral before reassigning its pins | diff --git a/test/test_rx_power_saving/test_rx_power_saving.cpp b/test/test_rx_power_saving/test_rx_power_saving.cpp new file mode 100644 index 00000000..89ce2564 --- /dev/null +++ b/test/test_rx_power_saving/test_rx_power_saving.cpp @@ -0,0 +1,187 @@ +#include + +#include + +TEST(RxPowerSaving, BalancedNormalProfileKeepsDerivedDutyCycle) { + uint32_t rx_us = 0; + uint32_t sleep_us = 0; + + ASSERT_TRUE(calcRxPowerSavingLevel(5, 7, 62.5f, 16, &rx_us, &sleep_us)); + EXPECT_EQ(20936U, rx_us); + EXPECT_EQ(13425U, sleep_us); + EXPECT_FALSE(rxPowerSavingUsesContinuousFallback(rx_us, sleep_us)); +} + +TEST(RxPowerSaving, Sf5Bw500FallsBackToContinuousReceive) { + uint32_t rx_us = 0; + uint32_t sleep_us = 0; + + ASSERT_TRUE(recalcRxPowerSavingFromLevel(5, 5, 500.0f, 16, + &rx_us, &sleep_us)); + EXPECT_EQ(RX_POWERSAVING_CONTINUOUS_FALLBACK_US, rx_us); + EXPECT_EQ(RX_POWERSAVING_CONTINUOUS_FALLBACK_US, sleep_us); + EXPECT_TRUE(rxPowerSavingUsesContinuousFallback(rx_us, sleep_us)); +} + +TEST(RxPowerSaving, ReturningToSlowerProfileResumesDutyCycle) { + uint32_t rx_us = 0; + uint32_t sleep_us = 0; + + ASSERT_TRUE(recalcRxPowerSavingFromLevel(5, 5, 500.0f, 16, + &rx_us, &sleep_us)); + ASSERT_TRUE(rxPowerSavingUsesContinuousFallback(rx_us, sleep_us)); + + ASSERT_TRUE(recalcRxPowerSavingFromLevel(5, 9, 500.0f, 16, + &rx_us, &sleep_us)); + EXPECT_EQ(10468U, rx_us); + EXPECT_EQ(6712U, sleep_us); + EXPECT_FALSE(rxPowerSavingUsesContinuousFallback(rx_us, sleep_us)); +} + +TEST(RxPowerSaving, TcxoThresholdSeparatesFastestUsableProfiles) { + constexpr uint32_t sx1262_tcxo_transition_us = 6000; + uint32_t rx_us = 0; + uint32_t sleep_us = 0; + + ASSERT_TRUE(calcRxPowerSavingLevel(6, 7, 500.0f, 32, + &rx_us, &sleep_us)); + EXPECT_FALSE(canStartRxPowerSavingDutyCycle( + rx_us, sleep_us, sx1262_tcxo_transition_us)); + + ASSERT_TRUE(calcRxPowerSavingLevel(7, 7, 500.0f, 32, + &rx_us, &sleep_us)); + EXPECT_TRUE(canStartRxPowerSavingDutyCycle( + rx_us, sleep_us, sx1262_tcxo_transition_us)); +} + +TEST(RxPowerSaving, RequestedLevelIsRaisedOnlyToRequiredMinimum) { + constexpr uint32_t sx1262_tcxo_transition_us = 6000; + uint32_t rx_us = 0; + uint32_t sleep_us = 0; + uint8_t effective_level = 0; + uint8_t effective_preamble = 0; + + ASSERT_TRUE(calcRxPowerSavingLevelAtOrAbove( + 1, 7, 500.0f, 16, sx1262_tcxo_transition_us, + &rx_us, &sleep_us, &effective_level, &effective_preamble)); + EXPECT_EQ(7U, effective_level); + EXPECT_EQ(32U, effective_preamble); + EXPECT_TRUE(canStartRxPowerSavingDutyCycle( + rx_us, sleep_us, sx1262_tcxo_transition_us)); + + ASSERT_TRUE(calcRxPowerSavingLevelAtOrAbove( + 8, 7, 500.0f, 16, sx1262_tcxo_transition_us, + &rx_us, &sleep_us, &effective_level, &effective_preamble)); + EXPECT_EQ(8U, effective_level); + EXPECT_EQ(32U, effective_preamble); + EXPECT_TRUE(canStartRxPowerSavingDutyCycle( + rx_us, sleep_us, sx1262_tcxo_transition_us)); +} + +TEST(RxPowerSaving, Sf5Bw500StillFallsBackWithPreamble32) { + constexpr uint32_t sx1262_tcxo_transition_us = 6000; + uint32_t rx_us = 0; + uint32_t sleep_us = 0; + uint8_t effective_level = 99; + uint8_t effective_preamble = 99; + + ASSERT_TRUE(calcRxPowerSavingLevelAtOrAbove( + 1, 5, 500.0f, 16, sx1262_tcxo_transition_us, + &rx_us, &sleep_us, &effective_level, &effective_preamble)); + EXPECT_EQ(0U, effective_level); + EXPECT_EQ(0U, effective_preamble); + EXPECT_TRUE(rxPowerSavingUsesContinuousFallback(rx_us, sleep_us)); +} + +TEST(RxPowerSaving, Sf5Bw250StillFallsBackWithPreamble32OnTcxoRadio) { + constexpr uint32_t sx1262_tcxo_transition_us = 6000; + uint32_t rx_us = 0; + uint32_t sleep_us = 0; + uint8_t effective_level = 99; + uint8_t effective_preamble = 99; + + // Even the maximum level only provides a 3.616 ms sleep window. RadioLib + // must reserve 6 ms for the SX1262 TCXO and sleep/wake transitions. + ASSERT_TRUE(calcRxPowerSavingLevel(10, 5, 250.0f, 32, + &rx_us, &sleep_us)); + EXPECT_EQ(1024U, rx_us); + EXPECT_EQ(3616U, sleep_us); + EXPECT_FALSE(canStartRxPowerSavingDutyCycle( + rx_us, sleep_us, sx1262_tcxo_transition_us)); + + ASSERT_TRUE(calcRxPowerSavingLevelAtOrAbove( + 1, 5, 250.0f, 16, sx1262_tcxo_transition_us, + &rx_us, &sleep_us, &effective_level, &effective_preamble)); + EXPECT_EQ(0U, effective_level); + EXPECT_EQ(0U, effective_preamble); + EXPECT_TRUE(rxPowerSavingUsesContinuousFallback(rx_us, sleep_us)); +} + +TEST(RxPowerSaving, Sf5Bw125UsesPreamble32AtEffectiveLevel7) { + constexpr uint32_t sx1262_tcxo_transition_us = 6000; + uint32_t rx_us = 0; + uint32_t sleep_us = 0; + uint8_t effective_level = 0; + uint8_t effective_preamble = 0; + + ASSERT_TRUE(calcRxPowerSavingLevelAtOrAbove( + 1, 5, 125.0f, 16, sx1262_tcxo_transition_us, + &rx_us, &sleep_us, &effective_level, &effective_preamble)); + EXPECT_EQ(7U, effective_level); + EXPECT_EQ(32U, effective_preamble); + EXPECT_EQ(2731U, rx_us); + EXPECT_EQ(6101U, sleep_us); + EXPECT_TRUE(canStartRxPowerSavingDutyCycle( + rx_us, sleep_us, sx1262_tcxo_transition_us)); +} + +TEST(RxPowerSaving, Sf6Bw250UsesPreamble32AtEffectiveLevel7) { + constexpr uint32_t sx1262_tcxo_transition_us = 6000; + uint32_t rx_us = 0; + uint32_t sleep_us = 0; + uint8_t effective_level = 0; + uint8_t effective_preamble = 0; + + ASSERT_TRUE(calcRxPowerSavingLevelAtOrAbove( + 1, 6, 250.0f, 16, sx1262_tcxo_transition_us, + &rx_us, &sleep_us, &effective_level, &effective_preamble)); + EXPECT_EQ(7U, effective_level); + EXPECT_EQ(32U, effective_preamble); + EXPECT_EQ(2731U, rx_us); + EXPECT_EQ(6101U, sleep_us); +} + +TEST(RxPowerSaving, Sf5Bw62Point5UsesPreamble16AtEffectiveLevel10) { + constexpr uint32_t sx1262_tcxo_transition_us = 6000; + uint32_t rx_us = 0; + uint32_t sleep_us = 0; + uint8_t effective_level = 0; + uint8_t effective_preamble = 0; + + ASSERT_TRUE(calcRxPowerSavingLevelAtOrAbove( + 1, 5, 62.5f, 16, sx1262_tcxo_transition_us, + &rx_us, &sleep_us, &effective_level, &effective_preamble)); + EXPECT_EQ(10U, effective_level); + EXPECT_EQ(16U, effective_preamble); + EXPECT_EQ(4096U, rx_us); + EXPECT_EQ(6272U, sleep_us); +} + +TEST(RxPowerSaving, RejectsInvalidInputs) { + uint32_t rx_us = 123; + uint32_t sleep_us = 456; + + EXPECT_FALSE(calcRxPowerSavingLevel(0, 7, 500.0f, 32, + &rx_us, &sleep_us)); + EXPECT_FALSE(calcRxPowerSavingLevel(5, 4, 500.0f, 32, + &rx_us, &sleep_us)); + EXPECT_FALSE(calcRxPowerSavingLevel(5, 7, 0.0f, 32, + &rx_us, &sleep_us)); + EXPECT_FALSE(calcRxPowerSavingLevel(5, 7, 500.0f, 24, + &rx_us, &sleep_us)); +} + +int main(int argc, char** argv) { + ::testing::InitGoogleTest(&argc, argv); + return RUN_ALL_TESTS(); +} diff --git a/variants/wio-tracker-l1/platformio.ini b/variants/wio-tracker-l1/platformio.ini index f6d85c9e..ff2b6954 100644 --- a/variants/wio-tracker-l1/platformio.ini +++ b/variants/wio-tracker-l1/platformio.ini @@ -93,6 +93,7 @@ build_flags = ${WioTrackerL1.build_flags} -D MAX_GROUP_CHANNELS=40 -D DISPLAY_CLASS=SH1106Display -D UI_HAS_JOYSTICK=1 + -D UI_NO_HIBERNATE -D OFFLINE_QUEUE_SIZE=256 -D PIN_BUZZER=12 -D QSPIFLASH=1 @@ -125,6 +126,7 @@ build_flags = ${WioTrackerL1.build_flags} -D OFFLINE_QUEUE_SIZE=256 -D DISPLAY_CLASS=SH1106Display -D UI_HAS_JOYSTICK=1 + -D UI_NO_HIBERNATE -D PIN_BUZZER=12 -D QSPIFLASH=1 -D ADVERT_NAME='"@@MAC"'