Skip to content
104 changes: 77 additions & 27 deletions src/helpers/radiolib/RadioLibWrappers.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -40,7 +51,9 @@ void RadioLibWrapper::begin() {

// start average out some samples
_num_floor_samples = 0;
_floor_sample_sum = 0;
_floor_block_ready = false;
_last_floor_sample_at = 0;
_held_block_count = 0;
}

uint32_t RadioLibWrapper::getRngSeed() {
Expand All @@ -61,9 +74,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;
}
}

Expand All @@ -78,34 +91,71 @@ 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;
_held_block_count = 0; // contamination context is stale after an AFE reset
}

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;
}
uint32_t now = millis();
if (!isReceivingPacket() && now - _last_floor_sample_at >= NOISE_FLOOR_SAMPLE_INTERVAL_MS) {
// Accept every idle sample, spaced NOISE_FLOOR_SAMPLE_INTERVAL_MS apart so the block spans a real
// ~3.2 s window and the median rejects transient transmissions (not a few-ms snapshot). The old
// "rssi < floor + threshold" filter was a one-way ratchet: it only accepted samples below the
// current floor, so the block average drifted 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();
_last_floor_sample_at = now;
}
} else if (_num_floor_samples >= NUM_NOISE_FLOOR_SAMPLES && _floor_sample_sum != 0) {
_noise_floor = _floor_sample_sum / NUM_NOISE_FLOOR_SAMPLES;
if (_noise_floor < -120) {
_noise_floor = -120; // clamp to lower bound of -120dBi
} 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);
// One-sided hold: a median jumping far ABOVE the published floor is activity-contaminated
// (inter-packet energy slips past the !isReceivingPacket() idle guard). Hold the old value so
// the RSSI-margin LBT stays meaningful under load; near-stable/quieter blocks publish at once.
// First block always publishes (_noise_floor=0 from begin()), so the hold binds only post-boot.
//
// Bounded: after NOISE_FLOOR_MAX_HELD_BLOCKS consecutive held blocks accept the median, else a real
// permanent rise is held forever (stuck-floor bug from the other direction). Count-based so the hold
// rides out load bursts (slow blocks) while a quiet rise releases in a few blocks.
if (median > _noise_floor + NOISE_FLOOR_MAX_RISE_DB) {
_held_block_count++;
if (_held_block_count >= NOISE_FLOOR_MAX_HELD_BLOCKS) {
_noise_floor = median;
if (_noise_floor < -120) {
_noise_floor = -120; // clamp to lower bound of -120dBi
}
_held_block_count = 0;
#ifdef MESH_DEBUG_NOISE_FLOOR
MESH_DEBUG_PRINTLN("RadioLibWrapper: noise_floor = %d (accepted after %d held blocks, persistent rise)",
(int)_noise_floor, NOISE_FLOOR_MAX_HELD_BLOCKS);
#endif
} else {
MESH_DEBUG_PRINTLN("RadioLibWrapper: noise_floor held at %d (block median %d contaminated, held %d/%d)",
(int)_noise_floor, (int)median, _held_block_count, NOISE_FLOOR_MAX_HELD_BLOCKS);
}
} else {
_held_block_count = 0;
_noise_floor = median;
if (_noise_floor < -120) {
_noise_floor = -120; // clamp to lower bound of -120dBi
}
#ifdef MESH_DEBUG_NOISE_FLOOR
MESH_DEBUG_PRINTLN("RadioLibWrapper: noise_floor = %d (median)", (int)_noise_floor);
#endif
}
_floor_sample_sum = 0;

#ifdef MESH_DEBUG_NOISE_FLOOR
MESH_DEBUG_PRINTLN("RadioLibWrapper: noise_floor = %d", (int)_noise_floor);
#endif
_floor_block_ready = true;
}
}

Expand Down
15 changes: 14 additions & 1 deletion src/helpers/radiolib/RadioLibWrappers.h
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,16 @@
#include <Mesh.h>
#include <RadioLib.h>

#define NUM_NOISE_FLOOR_SAMPLES 64 // RSSI samples reduced to a median per noise-floor calibration block
#define NOISE_FLOOR_MAX_RISE_DB 15 // block median jumping this far ABOVE the published floor is treated as
// activity-contaminated and held, so the RSSI-margin LBT keeps a meaningful
// (idle) reference while the channel is occupied
#define NOISE_FLOOR_SAMPLE_INTERVAL_MS 50 // min spacing between RSSI samples so a 64-sample block spans a real
// ~3.2 s window, giving the median temporal interference rejection
// instead of collapsing to a few ms of near-simultaneous readings
#define NOISE_FLOOR_MAX_HELD_BLOCKS 3 // after this many consecutive held blocks the median is accepted, so a
// permanent floor rise can't keep it stuck low. Count-based: rides out load
// bursts, a true rise releases in a few blocks.
#ifdef USE_CC310_HW_CRYPTO
#include <Adafruit_nRFCrypto.h>
#endif
Expand All @@ -19,7 +29,10 @@ 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)
uint32_t _last_floor_sample_at; // millis() of the last accepted RSSI sample (rate-limits block sampling)
uint8_t _held_block_count; // consecutive held blocks since the last published noise-floor value
uint8_t _preamble_sf;

void idle();
Expand Down