diff --git a/docs/WiFi.md b/docs/WiFi.md index 998f3522c..3e66461d0 100644 --- a/docs/WiFi.md +++ b/docs/WiFi.md @@ -250,9 +250,10 @@ a trusted LAN. The WebConfig **Advanced** card exposes device power saving on WiFi Companion, repeater, and room-server builds. It also exposes RX power saving on radio chips that support receive duty cycling. RXPS can select continuous receive, levels -1-10, automatic or explicit 16/32-symbol preambles, or manual receive/sleep -windows. The WebConfig **WiFi** card exposes WiFi modem power saving. All three -settings are persisted across reboots. +1-10, automatic or explicit 16/32-symbol timing assumptions, or manual +receive/sleep windows. The timing assumption does not change the radio's actual +wire preamble. The WebConfig **WiFi** card exposes WiFi modem power saving. All +three settings are persisted across reboots. The three settings are independent. A WiFi Companion keeps its transports available while device power saving reduces CPU and GPS idle power. An diff --git a/docs/cli_commands.md b/docs/cli_commands.md index 6617664c2..2bf697a2b 100644 --- a/docs/cli_commands.md +++ b/docs/cli_commands.md @@ -742,7 +742,18 @@ On SX1262+TCXO boards, the tested fast-setting boundaries are: 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. +preamble 32 in the normal build. + +The table's effective preamble is the length RXPS uses for its timing window. +The radio's actual transmitted preamble is a firmware property, not a +`radio.rxps` preference. Normal firmware transmits 32 symbols at SF5-SF8 and +16 symbols at SF9-SF12. A controlled build can set +`-D LORA_FAST_PREAMBLE=64`; this changes the actual SF5-SF8 wire preamble and +lets SF5/BW250 duty-cycle at effective level 8 (`1252 / 6424 us`). That profile +passed a 16/16 on-air qualification at 909.950 MHz. SF5/BW500 remains too fast. +Use the 64-symbol option only when every possible sender to an RXPS receiver +uses the longer preamble; treating a 32-symbol sender as if it sent 64 would +create a receive gap. --- @@ -798,7 +809,8 @@ Station G2/G3 targets default to `off`. **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`. +- `preamble`: RXPS timing assumption in symbols; `16` or `32`. This does not + change the radio's actual transmitted preamble. - `state`: `on` or `off`. **Notes:** @@ -809,6 +821,9 @@ Station G2/G3 targets default to `off`. - `on` and `conservative` select level `1` with a 16-symbol preamble; `balanced` selects level `5` with a 16-symbol preamble. - Fresh Cascade-profile builds start with RXPS on at level `8` and a 16-symbol preamble. Saved operator settings still take precedence after an upgrade. - Level-based settings automatically recalculate their timings when the spreading factor or bandwidth changes. Custom ` ` timings remain fixed. +- `get radio.rxps` reports both the saved timing assumption and any effective + preamble/level selected for the current tuple. A 64-symbol effective preamble + appears only in a firmware build whose actual fast preamble is 64. - The selected mode is applied immediately, persisted, and restored after reboot. --- diff --git a/docs/terminal_chat_cli.md b/docs/terminal_chat_cli.md index a43429f84..2f678ead3 100644 --- a/docs/terminal_chat_cli.md +++ b/docs/terminal_chat_cli.md @@ -107,14 +107,16 @@ 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. 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. +builds select level 8 with a 16-symbol timing assumption. The `preamble` +argument controls the RXPS calculation; it does not change the physical wire +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 the normal build 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 @@ -135,8 +137,13 @@ RXPS boundary profiles (CR does not change the RXPS preamble timing): | 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`. +SF5/BW250 and SF5/BW500 remain too fast for a true TCXO duty cycle in the +normal 32-symbol build, so they use `continuous-fast`. A controlled build with +`-D LORA_FAST_PREAMBLE=64` changes the actual SF5-SF8 wire preamble. SF5/BW250 +then uses effective level 8 and `1252 / 6424 us`; that combination passed a +16/16 on-air qualification at 909.950 MHz. SF5/BW500 still uses +`continuous-fast`. Deploy a longer physical preamble consistently across all +senders that may reach an RXPS receiver. ``` get wifi.powersave diff --git a/src/helpers/radiolib/RXPowerSaving.h b/src/helpers/radiolib/RXPowerSaving.h index a3d0a3907..a4327ab70 100644 --- a/src/helpers/radiolib/RXPowerSaving.h +++ b/src/helpers/radiolib/RXPowerSaving.h @@ -6,6 +6,17 @@ #define RX_POWERSAVING_MIN_PERIOD_US 1000UL #define RX_POWERSAVING_MAX_PERIOD_US 30000000UL +// Actual transmit preamble used by MeshCore at SF5-SF8. Keep 32 as the wire +// compatible default; controlled qualification builds can select 64 to test +// faster RX duty-cycle profiles without pretending the sender transmits a +// longer preamble than it really does. +#ifndef LORA_FAST_PREAMBLE +#define LORA_FAST_PREAMBLE 32 +#endif +#if LORA_FAST_PREAMBLE != 32 && LORA_FAST_PREAMBLE != 64 +#error "LORA_FAST_PREAMBLE must be 32 or 64" +#endif + // 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 @@ -95,7 +106,11 @@ inline bool canStartRxPowerSavingDutyCycle(uint32_t rx_us, uint32_t sleep_us, // Must stay in sync with RadioLibWrapper::preambleLengthForSF() (the value the // radio actually transmits); kept local here because CommonCLI is radio-agnostic. inline uint8_t rxPowerSavingPreambleForSF(uint8_t sf) { - return sf <= 8 ? 32 : 16; + return sf <= 8 ? LORA_FAST_PREAMBLE : 16; +} + +inline bool isRxPowerSavingPreamble(uint8_t preamble) { + return preamble == 16 || preamble == 32 || preamble == 64; } inline bool isNumeric(const char *sp) { @@ -115,7 +130,7 @@ 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 (!rx_us || !sleep_us || level < 1 || level > 10 || sf < 5 || sf > 12 - || bw <= 0.0f || (preamble != 16 && preamble != 32)) { + || bw <= 0.0f || !isRxPowerSavingPreamble(preamble)) { return false; } @@ -162,19 +177,19 @@ inline bool calcRxPowerSavingLevelAtOrAbove( 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)) { + || !isRxPowerSavingPreamble(preamble)) { 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; + // it. A second pass may use the radio's real, longer transmit preamble when + // the configured assumption cannot cover the transition at a faster tuple. + const uint8_t transmitted_preamble = rxPowerSavingPreambleForSF(sf); + const bool can_try_transmitted = transmitted_preamble > preamble; + const uint8_t pass_count = can_try_transmitted ? 2 : 1; for (uint8_t pass = 0; pass < pass_count; pass++) { - const uint8_t candidate_preamble = pass == 0 ? preamble : 32; + const uint8_t candidate_preamble = pass == 0 + ? preamble : transmitted_preamble; 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; diff --git a/src/helpers/radiolib/RadioLibWrappers.h b/src/helpers/radiolib/RadioLibWrappers.h index 4a0d7362b..87c59554a 100644 --- a/src/helpers/radiolib/RadioLibWrappers.h +++ b/src/helpers/radiolib/RadioLibWrappers.h @@ -172,7 +172,9 @@ public: virtual float getCurrentRSSI() =0; virtual uint8_t getSpreadingFactor() const { return LORA_SF; } - static uint16_t preambleLengthForSF(uint8_t sf) { return sf <= 8 ? 32 : 16; } + static uint16_t preambleLengthForSF(uint8_t sf) { + return rxPowerSavingPreambleForSF(sf); + } bool updatePreamble(uint8_t sf) { if (_radio->setPreambleLength(preambleLengthForSF(sf)) != RADIOLIB_ERR_NONE) return false; _preamble_sf = sf; diff --git a/test/README.md b/test/README.md index 06fcfaaad..dbd8b0a65 100644 --- a/test/README.md +++ b/test/README.md @@ -63,7 +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_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, normal-preamble SF5/BW250/BW500 continuous-RX fallback, opt-in 64-symbol SF5/BW250 level-8 timing, 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 index 89ce25644..1db24d6cf 100644 --- a/test/test_rx_power_saving/test_rx_power_saving.cpp +++ b/test/test_rx_power_saving/test_rx_power_saving.cpp @@ -64,8 +64,8 @@ TEST(RxPowerSaving, RequestedLevelIsRaisedOnlyToRequiredMinimum) { 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_EQ(LORA_FAST_PREAMBLE == 64 ? 3U : 7U, effective_level); + EXPECT_EQ((uint8_t)LORA_FAST_PREAMBLE, effective_preamble); EXPECT_TRUE(canStartRxPowerSavingDutyCycle( rx_us, sleep_us, sx1262_tcxo_transition_us)); @@ -73,7 +73,7 @@ TEST(RxPowerSaving, RequestedLevelIsRaisedOnlyToRequiredMinimum) { 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_EQ((uint8_t)LORA_FAST_PREAMBLE, effective_preamble); EXPECT_TRUE(canStartRxPowerSavingDutyCycle( rx_us, sleep_us, sx1262_tcxo_transition_us)); } @@ -93,7 +93,7 @@ TEST(RxPowerSaving, Sf5Bw500StillFallsBackWithPreamble32) { EXPECT_TRUE(rxPowerSavingUsesContinuousFallback(rx_us, sleep_us)); } -TEST(RxPowerSaving, Sf5Bw250StillFallsBackWithPreamble32OnTcxoRadio) { +TEST(RxPowerSaving, Sf5Bw250NeedsLongerTransmittedPreambleOnTcxoRadio) { constexpr uint32_t sx1262_tcxo_transition_us = 6000; uint32_t rx_us = 0; uint32_t sleep_us = 0; @@ -112,11 +112,55 @@ TEST(RxPowerSaving, Sf5Bw250StillFallsBackWithPreamble32OnTcxoRadio) { ASSERT_TRUE(calcRxPowerSavingLevelAtOrAbove( 1, 5, 250.0f, 16, sx1262_tcxo_transition_us, &rx_us, &sleep_us, &effective_level, &effective_preamble)); +#if LORA_FAST_PREAMBLE == 64 + EXPECT_EQ(8U, effective_level); + EXPECT_EQ(64U, effective_preamble); + EXPECT_EQ(1252U, rx_us); + EXPECT_EQ(6424U, sleep_us); + EXPECT_FALSE(rxPowerSavingUsesContinuousFallback(rx_us, sleep_us)); +#else EXPECT_EQ(0U, effective_level); EXPECT_EQ(0U, effective_preamble); EXPECT_TRUE(rxPowerSavingUsesContinuousFallback(rx_us, sleep_us)); +#endif } +TEST(RxPowerSaving, Sf5Bw250UsesPreamble64AtEffectiveLevel8) { + 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, 250.0f, 64, sx1262_tcxo_transition_us, + &rx_us, &sleep_us, &effective_level, &effective_preamble)); + EXPECT_EQ(8U, effective_level); + EXPECT_EQ(64U, effective_preamble); + EXPECT_EQ(1252U, rx_us); + EXPECT_EQ(6424U, sleep_us); + EXPECT_TRUE(canStartRxPowerSavingDutyCycle( + rx_us, sleep_us, sx1262_tcxo_transition_us)); +} + +#if LORA_FAST_PREAMBLE == 64 +TEST(RxPowerSaving, FastPreamble64BuildSelectsItAutomatically) { + 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, 250.0f, 16, sx1262_tcxo_transition_us, + &rx_us, &sleep_us, &effective_level, &effective_preamble)); + EXPECT_EQ(8U, effective_level); + EXPECT_EQ(64U, effective_preamble); + EXPECT_EQ(1252U, rx_us); + EXPECT_EQ(6424U, sleep_us); +} +#endif + TEST(RxPowerSaving, Sf5Bw125UsesPreamble32AtEffectiveLevel7) { constexpr uint32_t sx1262_tcxo_transition_us = 6000; uint32_t rx_us = 0; @@ -127,10 +171,15 @@ TEST(RxPowerSaving, Sf5Bw125UsesPreamble32AtEffectiveLevel7) { 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(LORA_FAST_PREAMBLE == 64 ? 3U : 7U, effective_level); + EXPECT_EQ((uint8_t)LORA_FAST_PREAMBLE, effective_preamble); +#if LORA_FAST_PREAMBLE == 64 + EXPECT_EQ(3641U, rx_us); + EXPECT_EQ(6414U, sleep_us); +#else EXPECT_EQ(2731U, rx_us); EXPECT_EQ(6101U, sleep_us); +#endif EXPECT_TRUE(canStartRxPowerSavingDutyCycle( rx_us, sleep_us, sx1262_tcxo_transition_us)); } @@ -145,10 +194,15 @@ TEST(RxPowerSaving, Sf6Bw250UsesPreamble32AtEffectiveLevel7) { 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(LORA_FAST_PREAMBLE == 64 ? 3U : 7U, effective_level); + EXPECT_EQ((uint8_t)LORA_FAST_PREAMBLE, effective_preamble); +#if LORA_FAST_PREAMBLE == 64 + EXPECT_EQ(3641U, rx_us); + EXPECT_EQ(6414U, sleep_us); +#else EXPECT_EQ(2731U, rx_us); EXPECT_EQ(6101U, sleep_us); +#endif } TEST(RxPowerSaving, Sf5Bw62Point5UsesPreamble16AtEffectiveLevel10) {