diff --git a/src/helpers/radiolib/RadioLibWrappers.cpp b/src/helpers/radiolib/RadioLibWrappers.cpp index 5e72336c05..7d3aff8c53 100644 --- a/src/helpers/radiolib/RadioLibWrappers.cpp +++ b/src/helpers/radiolib/RadioLibWrappers.cpp @@ -8,11 +8,22 @@ #define STATE_TX_DONE 4 #define STATE_INT_READY 16 -#define NUM_NOISE_FLOOR_SAMPLES 64 -#define SAMPLING_THRESHOLD 14 - static volatile uint8_t state = STATE_IDLE; +// In-place insertion sort of int16_t samples for the noise-floor median. Runs once per +// calibration block (64 elements, ~every 2 s of idle), so O(n^2) is irrelevant here. +static void sortInt16(int16_t* a, int n) { + for (int i = 1; i < n; i++) { + int16_t key = a[i]; + int j = i - 1; + while (j >= 0 && a[j] > key) { + a[j + 1] = a[j]; + j--; + } + a[j + 1] = key; + } +} + // this function is called when a complete packet // is transmitted by the module static @@ -40,7 +51,7 @@ void RadioLibWrapper::begin() { // start average out some samples _num_floor_samples = 0; - _floor_sample_sum = 0; + _floor_block_ready = false; } uint32_t RadioLibWrapper::getRngSeed() { @@ -58,9 +69,9 @@ void RadioLibWrapper::idle() { void RadioLibWrapper::triggerNoiseFloorCalibrate(int threshold) { _threshold = threshold; - if (_num_floor_samples >= NUM_NOISE_FLOOR_SAMPLES) { // ignore trigger if currently sampling + if (_num_floor_samples >= NUM_NOISE_FLOOR_SAMPLES) { // restart only once the current block is complete _num_floor_samples = 0; - _floor_sample_sum = 0; + _floor_block_ready = false; } } @@ -75,32 +86,39 @@ void RadioLibWrapper::resetAGC() { doResetAGC(); state = STATE_IDLE; // trigger a startReceive() - // Reset noise floor sampling so it reconverges from scratch. - // Without this, a stuck _noise_floor of -120 makes the sampling threshold - // too low (-106) to accept normal samples (~-105), self-reinforcing the - // stuck value even after the receiver has recovered. - _noise_floor = 0; + // Discard any in-progress noise-floor block: the analog frontend was just reset, so + // queued RSSI samples are stale. _noise_floor itself is left in place — the median + // estimator no longer drifts to -120 (the reason the old ratchet needed a hard + // _noise_floor = 0 reset), and forcing 0 here would create a brief permissive LBT + // window (margin = RSSI - 0) until the next block completes. _num_floor_samples = 0; - _floor_sample_sum = 0; + _floor_block_ready = false; } void RadioLibWrapper::loop() { if (state == STATE_RX && _num_floor_samples < NUM_NOISE_FLOOR_SAMPLES) { if (!isReceivingPacket()) { - int rssi = getCurrentRSSI(); - if (rssi < _noise_floor + SAMPLING_THRESHOLD) { // only consider samples below current floor + sampling THRESHOLD - _num_floor_samples++; - _floor_sample_sum += rssi; - } + // Accept every idle sample. The old "rssi < floor + threshold" filter was a one-way + // ratchet: it only ever accepted samples below the current floor, so the block average + // drifted downward to the -120 clamp and never recovered — leaving _noise_floor stuck + // low and the RSSI-margin LBT permanently over-sensitive. + _floor_samples[_num_floor_samples++] = (int16_t)getCurrentRSSI(); } - } else if (_num_floor_samples >= NUM_NOISE_FLOOR_SAMPLES && _floor_sample_sum != 0) { - _noise_floor = _floor_sample_sum / NUM_NOISE_FLOOR_SAMPLES; + } else if (_num_floor_samples >= NUM_NOISE_FLOOR_SAMPLES && !_floor_block_ready) { + // Block complete: reduce to the median. The median rejects transient interference + // spikes (high and low outliers) and recovers in BOTH directions, unlike the ratcheted + // mean. _noise_floor is written only here, so the previous value stays valid while the + // next block is sampled — no reset-to-0, no permissive LBT window during reconvergence. + sortInt16(_floor_samples, NUM_NOISE_FLOOR_SAMPLES); + int16_t median = (int16_t)(((int32_t)_floor_samples[NUM_NOISE_FLOOR_SAMPLES / 2 - 1] + + (int32_t)_floor_samples[NUM_NOISE_FLOOR_SAMPLES / 2]) / 2); + _noise_floor = median; if (_noise_floor < -120) { _noise_floor = -120; // clamp to lower bound of -120dBi } - _floor_sample_sum = 0; + _floor_block_ready = true; - MESH_DEBUG_PRINTLN("RadioLibWrapper: noise_floor = %d", (int)_noise_floor); + MESH_DEBUG_PRINTLN("RadioLibWrapper: noise_floor = %d (median)", (int)_noise_floor); } } diff --git a/src/helpers/radiolib/RadioLibWrappers.h b/src/helpers/radiolib/RadioLibWrappers.h index 3091832f11..a764065588 100644 --- a/src/helpers/radiolib/RadioLibWrappers.h +++ b/src/helpers/radiolib/RadioLibWrappers.h @@ -3,6 +3,8 @@ #include #include +#define NUM_NOISE_FLOOR_SAMPLES 64 // RSSI samples reduced to a median per noise-floor calibration block + class RadioLibWrapper : public mesh::Radio { protected: PhysicalLayer* _radio; @@ -11,7 +13,8 @@ class RadioLibWrapper : public mesh::Radio { int16_t _noise_floor, _threshold; bool _cad_enabled; uint16_t _num_floor_samples; - int32_t _floor_sample_sum; + int16_t _floor_samples[NUM_NOISE_FLOOR_SAMPLES]; + bool _floor_block_ready; // true once a full block has been reduced to a median (waits for trigger to restart) uint8_t _preamble_sf; void idle();