diff --git a/src/include/RxScanOrder.h b/src/include/RxScanOrder.h new file mode 100644 index 0000000000..98b230f117 --- /dev/null +++ b/src/include/RxScanOrder.h @@ -0,0 +1,62 @@ +#pragma once + +#include + +// One complete hopping cycle plus the existing 10% synchronization margin. +inline uint32_t rxScanWindowMs(uint32_t channels, uint8_t hopInterval, uint32_t packetIntervalUs) +{ + return 11U * channels * hopInterval * packetIntervalUs / 10000U; +} + +template +class RxScanOrder +{ +public: + // A zero window excludes an unsupported mode. Ties retain RF table order. + template + uint8_t begin(uint8_t preferred, Window window) + { + count = 0; + for (uint8_t rate = 0; rate < Capacity; ++rate) + { + const uint32_t duration = window(rate); + if (!duration) continue; + uint8_t pos = count; + while (pos && durations[pos - 1] > duration) + { + rates[pos] = rates[pos - 1]; + durations[pos] = durations[pos - 1]; + --pos; + } + rates[pos] = rate; + durations[pos] = duration; + ++count; + } + // Keep a valid saved rate first, then visit every other mode by cost. + for (uint8_t pos = 0; pos < count; ++pos) + { + if (rates[pos] != preferred) continue; + for (; pos; --pos) rates[pos] = rates[pos - 1]; + rates[0] = preferred; + break; + } + cursor = 0; + return count ? rates[0] : 0; + } + + uint8_t next() + { + if (!count) return 0; + cursor = (cursor + 1) % count; + return rates[cursor]; + } + + // After binding, the next scan starts with the first supported mode. + void restart() { cursor = count ? count - 1 : 0; } + +private: + uint8_t rates[Capacity] = {}; + uint32_t durations[Capacity] = {}; + uint8_t count = 0; + uint8_t cursor = 0; +}; diff --git a/src/src/rx_main.cpp b/src/src/rx_main.cpp index 76e6e90a65..63ed83d1bd 100644 --- a/src/src/rx_main.cpp +++ b/src/src/rx_main.cpp @@ -1,5 +1,6 @@ #include "rxtx_common.h" #include "LowPassFilter.h" +#include "RxScanOrder.h" #include "crc.h" #include "telemetry_protocol.h" @@ -200,7 +201,7 @@ LPF LPF_UplinkRSSI0(5); // track rssi per antenna LPF LPF_UplinkRSSI1(5); MeanAccumulator SnrMean; -static uint8_t scanIndex; +static RxScanOrder scanOrder; uint8_t ExpressLRS_nextAirRateIndex; int8_t SwitchModePending; @@ -403,7 +404,7 @@ void SetRFLinkRate(uint8_t index, bool bindMode) // Set speed of RF link TelemetrySender.setMaxPackageIndex(OtaIsFullRes ? ELRS8_TELEMETRY_MAX_PACKAGES : ELRS4_TELEMETRY_MAX_PACKAGES); // Wait for (11/10) 110% of time it takes to cycle through all freqs in FHSS table (in ms) - cycleInterval = ((uint32_t)11U * FHSSgetChannelCount() * ModParams->FHSShopInterval * interval) / (10U * 1000U); + cycleInterval = rxScanWindowMs(FHSSgetChannelCount(), ModParams->FHSShopInterval, interval); ExpressLRS_currAirRate_Modparams = ModParams; ExpressLRS_currAirRate_RFperfParams = RFperf; @@ -1693,15 +1694,14 @@ static void setupRadio() Radio.RXdoneCallback = &RXdoneISR; Radio.TXdoneCallback = &TXdoneISR; - scanIndex = config.GetRateInitialIdx(); - for (int i=0 ; iradio_type == RADIO_TYPE_LR1121_LORA_2G4 || + params->radio_type == RADIO_TYPE_LR1121_GFSK_2G4; + const uint32_t channels = secondary ? FHSSconfigDualBand->freq_count : FHSSconfig->freq_count; + return rxScanWindowMs(channels, params->FHSShopInterval, params->interval); + }); SetRFLinkRate(scanIndex, false); // Start slow on the selected rate to give it the best chance // to connect before beginning rate cycling @@ -1735,21 +1735,14 @@ static void cycleRfMode(unsigned long now) RFmodeLastCycled = now; LastSyncPacket = now; // reset this variable SendLinkStatstoFCForcedSends = 2; - SetRFLinkRate(scanIndex % RATE_MAX, false); // switch between rates + // The startup rate has already had its listening window; advance once. + SetRFLinkRate(scanOrder.next(), false); LQCalc.reset100(); LQCalcDVDA.reset100(); // Display the current air rate to the user as an indicator something is happening - scanIndex++; Radio.RXnb(); DBGLN("%u", ExpressLRS_currAirRate_Modparams->interval); - // Skip unsupported modes for hardware with only a single LR1121 or with a single RF path - while (!isSupportedRFRate(scanIndex % RATE_MAX)) - { - DBGLN("Skip %u", get_elrs_airRateConfig(scanIndex % RATE_MAX)->interval); - scanIndex++; - } - // Switch to FAST_SYNC if not already in it (won't be if was just connected) RFmodeCycleMultiplier = 1; } // if time to switch RF mode @@ -1810,7 +1803,7 @@ static void ExitBindingMode() #endif // Force RF cycling to start at the beginning immediately - scanIndex = RATE_MAX; + scanOrder.restart(); RFmodeLastCycled = 0; LockRFmode = false; LostConnection(false); diff --git a/src/test/test_rx_scan/test_rx_scan.cpp b/src/test/test_rx_scan/test_rx_scan.cpp new file mode 100644 index 0000000000..13dda95c51 --- /dev/null +++ b/src/test/test_rx_scan/test_rx_scan.cpp @@ -0,0 +1,85 @@ +#include "RxScanOrder.h" +#include + +// SX128 RF table: F1000, F500, D500, D250, 500, 333 Full, 250, 150, 100 Full, 50. +static const uint32_t intervals[] = {1000, 2000, 1000, 1000, 2000, 3003, 4000, 6666, 10000, 20000}; +static uint32_t window(uint8_t rate) +{ + const uint8_t hop = rate < 4 || rate == 9 ? 2 : 4; + return rxScanWindowMs(80, hop, intervals[rate]); +} + +void setUp() {} +void tearDown() {} + +static void fresh_scan_orders_by_window_and_never_repeats_startup() +{ + RxScanOrder<10> scan; + TEST_ASSERT_EQUAL(0, scan.begin(0, window)); + const uint8_t expected[] = {2, 3, 1, 4, 5, 6, 7, 8, 9, 0}; + for (auto rate : expected) TEST_ASSERT_EQUAL(rate, scan.next()); +} + +static void saved_rate_is_first_then_all_other_modes_by_window() +{ + const uint8_t sorted[] = {0, 2, 3, 1, 4, 5, 6, 7, 8, 9}; + for (uint8_t saved = 0; saved < 10; ++saved) + { + RxScanOrder<10> scan; + TEST_ASSERT_EQUAL(saved, scan.begin(saved, window)); + for (auto rate : sorted) + if (rate != saved) TEST_ASSERT_EQUAL(rate, scan.next()); + TEST_ASSERT_EQUAL(saved, scan.next()); + } +} + +static void excludes_unsupported_and_handles_invalid_saved_index() +{ + RxScanOrder<10> scan; + auto supported = [](uint8_t rate) { return rate == 0 || rate == 2 || rate == 7 ? uint32_t(0) : window(rate); }; + TEST_ASSERT_EQUAL(3, scan.begin(255, supported)); + const uint8_t expected[] = {1, 4, 5, 6, 8, 9, 3}; + for (auto rate : expected) TEST_ASSERT_EQUAL(rate, scan.next()); + TEST_ASSERT_EQUAL(3, scan.begin(2, supported)); +} + +static void binding_restart_and_single_mode_do_not_skip_the_first_rate() +{ + RxScanOrder<10> scan; + TEST_ASSERT_EQUAL(6, scan.begin(6, window)); + scan.next(); + scan.restart(); + TEST_ASSERT_EQUAL(6, scan.next()); + TEST_ASSERT_EQUAL(0, scan.next()); + TEST_ASSERT_EQUAL(4, scan.begin(0, [](uint8_t rate) { return rate == 4 ? window(rate) : uint32_t(0); })); + for (int i = 0; i < 30; ++i) TEST_ASSERT_EQUAL(4, scan.next()); + scan.restart(); + TEST_ASSERT_EQUAL(4, scan.next()); +} + +static void windows_preserve_the_existing_hopping_margin() +{ + TEST_ASSERT_EQUAL(176, window(0)); + TEST_ASSERT_EQUAL(352, window(1)); + TEST_ASSERT_EQUAL(176, window(2)); + TEST_ASSERT_EQUAL(176, window(3)); + TEST_ASSERT_EQUAL(704, window(4)); + TEST_ASSERT_EQUAL(1057, window(5)); + TEST_ASSERT_EQUAL(1408, window(6)); + TEST_ASSERT_EQUAL(3520, window(8)); + TEST_ASSERT_EQUAL(3520, window(9)); + // A different band's channel count must affect ordering, not just Hz. + TEST_ASSERT_EQUAL(228, rxScanWindowMs(13, 4, 4000)); + TEST_ASSERT_EQUAL(1408, rxScanWindowMs(80, 4, 4000)); +} + +int main() +{ + UNITY_BEGIN(); + RUN_TEST(fresh_scan_orders_by_window_and_never_repeats_startup); + RUN_TEST(saved_rate_is_first_then_all_other_modes_by_window); + RUN_TEST(excludes_unsupported_and_handles_invalid_saved_index); + RUN_TEST(binding_restart_and_single_mode_do_not_skip_the_first_rate); + RUN_TEST(windows_preserve_the_existing_hopping_margin); + return UNITY_END(); +}