diff --git a/examples/simple_repeater/MyMesh.cpp b/examples/simple_repeater/MyMesh.cpp index f57de659e..8502834c7 100644 --- a/examples/simple_repeater/MyMesh.cpp +++ b/examples/simple_repeater/MyMesh.cpp @@ -1465,5 +1465,6 @@ bool MyMesh::hasPendingWork() const { if (bridge.isRunning()) return true; // bridge needs WiFi radio, can't sleep #endif if (radio_driver.isWatchdogObserving()) return true; // keep MCU awake for one radio duty cycle + if (radio_driver.isCalibratingNoiseFloor()) return true; // keep MCU awake for the noise-floor window return _mgr->getOutboundTotal() > 0; } diff --git a/examples/simple_room_server/MyMesh.cpp b/examples/simple_room_server/MyMesh.cpp index 43f479a00..779628636 100644 --- a/examples/simple_room_server/MyMesh.cpp +++ b/examples/simple_room_server/MyMesh.cpp @@ -1050,5 +1050,6 @@ bool MyMesh::hasPendingWork() const { if (bridge.isRunning()) return true; // bridge needs WiFi radio, can't sleep #endif if (radio_driver.isWatchdogObserving()) return true; // keep MCU awake for one radio duty cycle + if (radio_driver.isCalibratingNoiseFloor()) return true; // keep MCU awake for the noise-floor window return _mgr->getOutboundTotal() > 0; } diff --git a/src/helpers/radiolib/CustomLR1110Wrapper.h b/src/helpers/radiolib/CustomLR1110Wrapper.h index 56aec8be6..5482d7df0 100644 --- a/src/helpers/radiolib/CustomLR1110Wrapper.h +++ b/src/helpers/radiolib/CustomLR1110Wrapper.h @@ -43,7 +43,9 @@ protected: // reconfiguring the radio stopReceiveDutyCycle(); } - if (!_rx_ps_enabled) { + if (!_rx_ps_enabled || _nf_calib_active) { + // plain continuous RX: powersaving off, or a periodic noise-floor + // calibration window is in progress return _radio->startReceive(); } diff --git a/src/helpers/radiolib/CustomSX1262Wrapper.h b/src/helpers/radiolib/CustomSX1262Wrapper.h index b1b9ce577..eb8c3980d 100644 --- a/src/helpers/radiolib/CustomSX1262Wrapper.h +++ b/src/helpers/radiolib/CustomSX1262Wrapper.h @@ -52,7 +52,9 @@ protected: // silently abort the RX we are about to start stopReceiveDutyCycle(); } - if (!_rx_ps_enabled) { + if (!_rx_ps_enabled || _nf_calib_active) { + // plain continuous RX: powersaving off, or a periodic noise-floor + // calibration window is in progress return _radio->startReceive(); } diff --git a/src/helpers/radiolib/RadioLibWrappers.cpp b/src/helpers/radiolib/RadioLibWrappers.cpp index 082be0073..86c71086c 100644 --- a/src/helpers/radiolib/RadioLibWrappers.cpp +++ b/src/helpers/radiolib/RadioLibWrappers.cpp @@ -11,6 +11,11 @@ #define NUM_NOISE_FLOOR_SAMPLES 64 #define SAMPLING_THRESHOLD 14 +// periodic noise-floor calibration windows (RX duty-cycle powersaving only) +#define NF_CALIB_INTERVAL_MS 60000UL // at least once a minute +#define NF_CALIB_TIMEOUT_MS 5000UL // give up on the batch (busy channel) +#define NF_CALIB_SETTLE_MS 20UL // frontend/AGC settle after RX entry + static volatile uint8_t state = STATE_IDLE; // this function is called when a complete packet @@ -158,16 +163,60 @@ void RadioLibWrapper::rxPsWatchdogCheck() { } } +// Periodic noise-floor calibration, active only with RX duty-cycle powersaving: +// a duty-cycled receiver can't be sampled reliably (the frontend is off in the +// sleep windows and settling right after each wake), so at least once a minute +// the receive mode is dropped to plain continuous RX, a fresh sample batch is +// collected exactly like the non-powersaving path does, and the duty cycle is +// re-armed. The published average stays in _noise_floor as usual. +void RadioLibWrapper::noiseFloorCalibCheck() { + unsigned long now = millis(); + if (_nf_calib_active) { + if (!_rx_ps_enabled || (long)(now - _nf_calib_deadline) >= 0) { + // powersaving turned off mid-window, or the batch couldn't complete + // (busy channel / stuck filter) - keep the previous floor + endNoiseFloorCalib(now); + } + } else if (_rx_ps_enabled && _rx_ps_armed && state == STATE_RX + && now - _nf_last_calib >= NF_CALIB_INTERVAL_MS + && !isReceivingPacket()) { + // never interrupt an ongoing reception to calibrate (a TX in flight is + // already excluded by state == STATE_RX); retries next loop iteration + _nf_calib_active = true; + _nf_calib_deadline = now + NF_CALIB_TIMEOUT_MS; + _nf_sample_from = now + NF_CALIB_SETTLE_MS; + _num_floor_samples = 0; // start a fresh batch for this window + _floor_sample_sum = 0; + state = STATE_IDLE; // recvRaw() re-arms; startReceiveMode() sees the + // active flag and starts continuous RX, not duty-cycle + } +} + +void RadioLibWrapper::endNoiseFloorCalib(unsigned long now) { + _nf_calib_active = false; + _nf_last_calib = now; + // force a receive re-arm back into duty-cycle mode, but don't clobber a + // completed-but-unread packet or an in-flight TX (recvRaw()/onSendFinished() + // will re-arm right after those anyway; same guard style as setRxPowerSaving) + if ((state & STATE_INT_READY) == 0 && (state & ~STATE_INT_READY) != STATE_TX_WAIT) { + state = STATE_IDLE; + } +} + void RadioLibWrapper::loop() { if (_rx_ps_enabled) { rxPsWatchdogCheck(); } + noiseFloorCalibCheck(); if (state == STATE_RX && _num_floor_samples < NUM_NOISE_FLOOR_SAMPLES) { - // In RX duty-cycle (powersaving) mode only sample while the chip is in a - // listen window: during the sleep window the frontend is off, so the RSSI - // is meaningless and the SPI read would stall until the next window. - if (!(_rx_ps_armed && isChipBusy()) && !isReceivingPacket()) { + // Noise floor is only sampled outside RX duty-cycle mode: continuously in + // plain RX (powersaving off), or inside the periodic calibration window + // (powersaving on), skipping the first moments after RX entry there while + // the frontend/AGC settles (unsettled GetRssiInst reads ~-127 dBm garbage). + if (!_rx_ps_armed + && !(_nf_calib_active && (long)(millis() - _nf_sample_from) < 0) + && !isReceivingPacket()) { int rssi = getCurrentRSSI(); if (rssi < _noise_floor + SAMPLING_THRESHOLD) { // only consider samples below current floor + sampling THRESHOLD _num_floor_samples++; @@ -182,6 +231,10 @@ void RadioLibWrapper::loop() { _floor_sample_sum = 0; MESH_DEBUG_PRINTLN("RadioLibWrapper: noise_floor = %d", (int)_noise_floor); + + if (_nf_calib_active) { + endNoiseFloorCalib(millis()); // fresh floor published - back to duty cycle + } } } diff --git a/src/helpers/radiolib/RadioLibWrappers.h b/src/helpers/radiolib/RadioLibWrappers.h index 3f226ebb4..54b824795 100644 --- a/src/helpers/radiolib/RadioLibWrappers.h +++ b/src/helpers/radiolib/RadioLibWrappers.h @@ -48,9 +48,21 @@ protected: int8_t _cur_dbm; bool _params_valid, _dbm_valid; + // Periodic noise-floor calibration (only while RX duty-cycle powersaving is + // armed): a duty-cycled receiver can't be sampled reliably, so at least once + // a minute the wrapper drops to plain continuous RX, collects a fresh sample + // batch exactly like the non-powersaving path does, publishes the average + // into _noise_floor and re-arms the duty cycle. + bool _nf_calib_active; + unsigned long _nf_last_calib; // millis of last completed/attempted window + unsigned long _nf_calib_deadline; // abort window if the batch can't complete + unsigned long _nf_sample_from; // no samples before this (RX entry settle) + void idle(); void startRecv(); void rxPsWatchdogCheck(); + void noiseFloorCalibCheck(); + void endNoiseFloorCalib(unsigned long now); void cacheParams(float freq, float bw, uint8_t sf, uint8_t cr) { _cur_freq = freq; _cur_bw = bw; _cur_sf = sf; _cur_cr = cr; _params_valid = true; } @@ -73,7 +85,9 @@ public: _rx_ps_rx_us(RX_PS_FALLBACK_RX_US), _rx_ps_sleep_us(RX_PS_FALLBACK_SLEEP_US), _wd_last_busy(false), _wd_stage(0), _wd_strikes(0), _startrx_fails(0), _wd_last_transition(0), _wd_stuck_thresh(0), _wd_observe_until(0), _wd_observe_ms(0), - _params_valid(false), _dbm_valid(false) { n_recv = n_sent = n_recv_errors = n_wd_soft = n_wd_hard = 0; } + _params_valid(false), _dbm_valid(false), + _nf_calib_active(false), _nf_last_calib(0), _nf_calib_deadline(0), _nf_sample_from(0) + { n_recv = n_sent = n_recv_errors = n_wd_soft = n_wd_hard = 0; } void begin() override; virtual void powerOff() { _radio->sleep(); } @@ -117,6 +131,10 @@ public: // true while the watchdog is actively watching for BUSY transitions; used by // the app's hasPendingWork() to keep the MCU out of light sleep for the window bool isWatchdogObserving() const { return _wd_observe_until != 0; } + // true while a periodic noise-floor calibration window is in progress; the + // app's hasPendingWork() must keep the MCU awake so the sample batch and the + // return to duty-cycle complete promptly + bool isCalibratingNoiseFloor() const { return _nf_calib_active; } void resetStats() { n_recv = n_sent = n_recv_errors = 0; } virtual float getLastRSSI() const override;