Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
62 changes: 62 additions & 0 deletions src/include/RxScanOrder.h
Original file line number Diff line number Diff line change
@@ -0,0 +1,62 @@
#pragma once

#include <stdint.h>

// 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 <uint8_t Capacity>
class RxScanOrder
{
public:
// A zero window excludes an unsupported mode. Ties retain RF table order.
template <typename Window>
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;
};
35 changes: 14 additions & 21 deletions src/src/rx_main.cpp
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
#include "rxtx_common.h"
#include "LowPassFilter.h"
#include "RxScanOrder.h"

#include "crc.h"
#include "telemetry_protocol.h"
Expand Down Expand Up @@ -200,7 +201,7 @@ LPF LPF_UplinkRSSI0(5); // track rssi per antenna
LPF LPF_UplinkRSSI1(5);
MeanAccumulator<int32_t, int8_t, -16> SnrMean;

static uint8_t scanIndex;
static RxScanOrder<RATE_MAX> scanOrder;
uint8_t ExpressLRS_nextAirRateIndex;
int8_t SwitchModePending;

Expand Down Expand Up @@ -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;
Expand Down Expand Up @@ -1693,15 +1694,14 @@ static void setupRadio()
Radio.RXdoneCallback = &RXdoneISR;
Radio.TXdoneCallback = &TXdoneISR;

scanIndex = config.GetRateInitialIdx();
for (int i=0 ; i<RATE_MAX ; i++)
{
if (isSupportedRFRate(scanIndex))
{
break;
}
scanIndex = (scanIndex + 1) % RATE_MAX;
}
const uint8_t scanIndex = scanOrder.begin(config.GetRateInitialIdx(), [](uint8_t index) {
if (!isSupportedRFRate(index)) return uint32_t(0);
const auto *params = get_elrs_airRateConfig(index);
const bool secondary = params->radio_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
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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);
Expand Down
85 changes: 85 additions & 0 deletions src/test/test_rx_scan/test_rx_scan.cpp
Original file line number Diff line number Diff line change
@@ -0,0 +1,85 @@
#include "RxScanOrder.h"
#include <unity.h>

// 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();
}
Loading