Added RX duty-cycle noise floor calibration again (this time it works!)

This commit is contained in:
Jarosław Domański committed 2026-07-17 15:21:25 +02:00
1 parent 6582e8451f
commit 0af1bf14c5
6 files changed
+84 -7

No files matched your search

+1
View File
@@ -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;
}
+1
View File
@@ -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;
}
+3 -1
View File
@@ -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();
}
+3 -1
View File
@@ -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();
}
+57 -4
View File
@@ -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
}
}
}
+19 -1
View File
@@ -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;