From 0ed8167d02ce6c0c0a4e1840247340af7fb1a0b6 Mon Sep 17 00:00:00 2001 From: mikecarper Date: Thu, 10 Sep 2026 14:17:51 -0700 Subject: [PATCH] Reject standby RSSI during noise calibration --- docs/radio_receive_calibration.md | 6 ++++ src/helpers/radiolib/RadioLibWrappers.cpp | 16 +++++++-- test/test_radio_receive_contract.py | 41 +++++++++++++++++++++-- 3 files changed, 59 insertions(+), 4 deletions(-) diff --git a/docs/radio_receive_calibration.md b/docs/radio_receive_calibration.md index f6f09bc9..94fe91e7 100644 --- a/docs/radio_receive_calibration.md +++ b/docs/radio_receive_calibration.md @@ -34,6 +34,12 @@ without publishing. Longer collection spends more time in continuous RX when RXPS is enabled. A quiet minimum is about 3.2 seconds; real completion time also depends on packet activity and main-loop scheduling. +On radios with a mode probe, each eligible RSSI sample first checks that the +chip has not silently left RX. Confirmed standby/non-RX discards the partial +block without changing the published floor. Rejected attempts are also spaced +50 ms apart. BUSY and packet ownership block sampling; families without a mode +probe retain their existing RSSI path. + CAD has a bounded wait, stops RX duty cycling before scanning, and re-arms the configured receive mode on success, busy detection, or error. A completed or currently receiving packet owns the radio and blocks CAD. A packet interrupt diff --git a/src/helpers/radiolib/RadioLibWrappers.cpp b/src/helpers/radiolib/RadioLibWrappers.cpp index 3ae82bf6..1336dbf6 100644 --- a/src/helpers/radiolib/RadioLibWrappers.cpp +++ b/src/helpers/radiolib/RadioLibWrappers.cpp @@ -528,10 +528,22 @@ void RadioLibWrapper::loop() { if (!_rx_ps_armed && _floor_estimator.ready(static_cast(now)) && !(_nf_sample_from != 0 && (long)(now - _nf_sample_from) < 0) - && !isReceivingPacket()) { + && !isChipBusy() && !isPacketPendingOrReceiving()) { + // Silent RX loss can leave software in STATE_RX while standby RSSI is + // meaningless. Reject that observation before it can lower the floor. + // Space rejected attempts too; an empty block must not hammer SPI. + _nf_sample_from = now + NoiseFloorEstimator::SAMPLE_INTERVAL_MS; + if (readReceiveMode() == 0) { + _floor_estimator.reset(true); + return; + } // Admission is independent of the old floor, allowing upward recovery. + // A mode probe is unavailable on some radio families; keep their existing + // RSSI path. Preserve a packet IRQ arriving during the status transaction. // The estimator enforces 50 ms spacing and rejects invalid radio readings. - _floor_estimator.add(getCurrentRSSI(), static_cast(now)); + if (!isPacketPendingOrReceiving()) { + _floor_estimator.add(getCurrentRSSI(), static_cast(now)); + } } } } diff --git a/test/test_radio_receive_contract.py b/test/test_radio_receive_contract.py index 402d9e4c..b6d5ba05 100644 --- a/test/test_radio_receive_contract.py +++ b/test/test_radio_receive_contract.py @@ -71,13 +71,18 @@ struct RadioLibWrapper { int32_t _noise_floor_centi_dbm = -10500; int arm_result = 0, chip_mode = 1; float rssi = -100; - unsigned arms = 0, stops = 0, soft = 0, hard = 0, reads = 0; + unsigned arms = 0, stops = 0, soft = 0, hard = 0, reads = 0, mode_reads = 0; + bool inject_mode_irq = false; NoiseFloorEstimator _floor_estimator; RadioLibWrapper() { state = STATE_RX; now_ms = 100; } bool isChipBusy() { return busy; } bool isReceivingPacket() { return packet; } float getCurrentRSSI() { ++reads; return rssi; } - int8_t readReceiveMode() { return chip_mode; } + int8_t readReceiveMode() { + ++mode_reads; + if (inject_mode_irq) state |= STATE_INT_READY; + return chip_mode; + } bool recoverRadio(bool use_hard) { use_hard ? ++hard : ++soft; return true; } void rxPsWatchdogCheck() {} unsigned long getEstAirtimeFor(int) { return 10; } @@ -188,6 +193,38 @@ int main() { } assert(w._noise_floor_centi_dbm == -10125); } + // Hardware standby RSSI is not background noise, even if software says RX. + { + RadioLibWrapper w; + w._floor_estimator.add(-100, 0); + w.chip_mode = 0; + w.rssi = -127; + for (; now_ms < 4100; ++now_ms) w.loop(); + assert(w._noise_floor_centi_dbm == -10500); + assert(w._floor_estimator.count() == 0 && w.reads == 0); + assert(w.mode_reads == 80); // rejected samples are also limited to 20 Hz + w.chip_mode = 1; + w.rssi = -100; + for (; now_ms < 8100; ++now_ms) w.loop(); + assert(w.reads == 64 && !w._nf_refresh_requested); + assert(w._noise_floor_centi_dbm == -10125); + } + // A BUSY chip or a packet arriving during the mode probe must block RSSI I/O. + for (bool busy : {false, true}) { + RadioLibWrapper w; + w.busy = busy; + w.inject_mode_irq = !busy; + w.loop(); + assert(w.reads == 0); + assert(w.mode_reads == (busy ? 0U : 1U)); + } + // Families without a mode probe retain the existing RSSI sampling path. + { + RadioLibWrapper w; + w.chip_mode = -1; + w.loop(); + assert(w.reads == 1); + } // Busy continuous RX times out without changing the published floor. { RadioLibWrapper w;