Improved RX duty-cycle watchdog observation with esp32 sleep in mind

This commit is contained in:
Jarosław Domański
2026-07-09 14:39:20 +02:00
parent 9cbd944a29
commit a1330facff
4 changed files with 54 additions and 6 deletions
+1
View File
@@ -1464,5 +1464,6 @@ bool MyMesh::hasPendingWork() const {
#if defined(WITH_BRIDGE)
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
return _mgr->getOutboundTotal() > 0;
}
+1
View File
@@ -1049,5 +1049,6 @@ bool MyMesh::hasPendingWork() const {
#if defined(WITH_BRIDGE)
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
return _mgr->getOutboundTotal() > 0;
}
+38 -3
View File
@@ -88,7 +88,12 @@ void RadioLibWrapper::resetAGC() {
void RadioLibWrapper::rxPsWatchdogCheck() {
// don't interfere mid-transmit or with a completed-but-unread packet
if ((state & STATE_INT_READY) != 0 || (state & ~STATE_INT_READY) == STATE_TX_WAIT) return;
// (a pending DIO1 event is itself proof the radio is alive; recvRaw() will
// re-arm and re-base the watchdog)
if ((state & STATE_INT_READY) != 0 || (state & ~STATE_INT_READY) == STATE_TX_WAIT) {
_wd_observe_until = 0;
return;
}
unsigned long now = millis();
bool tripped = false;
@@ -96,12 +101,35 @@ void RadioLibWrapper::rxPsWatchdogCheck() {
if (_rx_ps_armed && state == STATE_RX && _wd_stuck_thresh > 0) {
bool busy = isChipBusy();
if (busy != _wd_last_busy) {
// the sleep/listen wave is present -> radio healthy
_wd_last_busy = busy;
_wd_last_transition = now;
_wd_stage = 0; // the sleep/listen wave is present -> radio healthy
_wd_stage = 0;
_wd_strikes = 0;
_wd_observe_until = 0;
} else if (_wd_observe_until != 0) {
// active observation window in progress (MCU kept awake via
// isWatchdogObserving()); a healthy chip must toggle BUSY within it
if ((long)(now - _wd_observe_until) >= 0) {
_wd_observe_until = 0;
if (!busy && isReceivingPacket()) {
// BUSY held low by an ongoing reception (extended RX) - alive
_wd_last_transition = now;
_wd_strikes = 0;
} else if (++_wd_strikes >= 2) {
_wd_strikes = 0;
tripped = true;
} else {
_wd_last_transition = now; // full threshold before the next window
}
}
} else if (now - _wd_last_transition > _wd_stuck_thresh) {
tripped = true; // BUSY frozen: chip fell out of the duty cycle with no IRQ
// no proof of life for too long: actively watch one full cycle
_wd_observe_until = now + _wd_observe_ms;
if (_wd_observe_until == 0) _wd_observe_until = 1; // 0 means "off"
}
} else {
_wd_observe_until = 0;
}
if (_startrx_fails >= 3) tripped = true; // can't even re-arm receive mode
@@ -109,6 +137,7 @@ void RadioLibWrapper::rxPsWatchdogCheck() {
_wd_last_transition = now; // grace period before the next escalation
_startrx_fails = 0;
_wd_observe_until = 0;
if (_wd_stage == 0) {
_wd_stage = 1;
@@ -168,9 +197,15 @@ void RadioLibWrapper::startRecv() {
// Longest legitimate silence on the BUSY pin: one full cycle, plus the
// extended RX after a (possibly false) preamble detect (2*rx + sleep),
// plus a worst-case packet airtime, plus margin for TCXO/transitions.
// Floored at 60s so a light-sleeping MCU (ESP32 wakes every ~30s) opens
// an observation window every couple of wakeups instead of on each one.
uint32_t rx_ms = _rx_ps_rx_us / 1000, sleep_ms = _rx_ps_sleep_us / 1000;
_wd_stuck_thresh = (rx_ms + sleep_ms) + 2 * (2 * rx_ms + sleep_ms)
+ getEstAirtimeFor(MAX_TRANS_UNIT) + 1000;
if (_wd_stuck_thresh < 60000) _wd_stuck_thresh = 60000;
// active observation window must cover one full duty cycle
_wd_observe_ms = rx_ms + sleep_ms + 50;
if (_wd_observe_ms > 1500) _wd_observe_ms = 1500;
}
} else {
if (_startrx_fails < 255) _startrx_fails++;
+14 -3
View File
@@ -28,11 +28,18 @@ protected:
// RX duty-cycle watchdog: a healthy duty cycle shows a square wave on the
// BUSY pin (high in the sleep window / TCXO warmup, low while listening).
// If the wave stops, the chip fell out of the cycle without an IRQ.
// Passive sampling only works while the main loop spins; on MCUs that light
// sleep between wakeups the watchdog instead opens an "active observation"
// window (isWatchdogObserving() keeps the MCU awake) spanning one full radio
// cycle - a healthy chip must toggle BUSY within it.
bool _wd_last_busy;
uint8_t _wd_stage; // 0 = healthy, 1 = soft re-arm done, 2 = hard reset done
uint8_t _wd_strikes; // consecutive failed observation windows
uint8_t _startrx_fails; // consecutive startReceiveMode() failures
unsigned long _wd_last_transition; // millis of last BUSY level change
unsigned long _wd_stuck_thresh; // ms without a transition considered stuck
unsigned long _wd_last_transition; // millis of last BUSY level change (proof of life)
unsigned long _wd_stuck_thresh; // ms without proof of life before observing
unsigned long _wd_observe_until; // 0 = not observing, else millis deadline
uint32_t _wd_observe_ms; // observation window: one full cycle + margin
uint32_t n_wd_soft, n_wd_hard;
// last applied radio settings, reapplied after a hard radio reset
@@ -64,7 +71,8 @@ public:
RadioLibWrapper(PhysicalLayer& radio, mesh::MainBoard& board)
: _radio(&radio), _board(&board), _preamble_sf(0), _rx_ps_enabled(false), _rx_ps_armed(false),
_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), _startrx_fails(0), _wd_last_transition(0), _wd_stuck_thresh(0),
_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; }
void begin() override;
@@ -106,6 +114,9 @@ public:
uint32_t getPacketsSent() const { return n_sent; }
uint32_t getRxPsWatchdogSoftCount() const { return n_wd_soft; }
uint32_t getRxPsWatchdogHardCount() const { return n_wd_hard; }
// 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; }
void resetStats() { n_recv = n_sent = n_recv_errors = 0; }
virtual float getLastRSSI() const override;