Bound failed RX retries without blocking power-saving sleep

This commit is contained in:
mikecarper committed 2026-10-16 01:27:24 -07:00
1 parent 862260de17
commit 339079c439
14 files changed
+463 -12

No files matched your search

+18 -1
View File
@@ -584,10 +584,18 @@ bool Dispatcher::getNextQueueWakeDelay(uint32_t& delay_millis) const {
}
}
uint32_t receive_retry_delay = 0;
const bool receive_retry = radio_available
&& _radio->getReceiveRetryDelay(now, receive_retry_delay);
if (outbound != NULL) {
if (outbound_radio_retry_pending) {
int32_t signed_retry_delay = (int32_t)(outbound_radio_retry_at - now);
delay_millis = signed_retry_delay > 0 ? (uint32_t)signed_retry_delay : 0;
// This backoff branch services checkRecv(), but returns before AGC.
// Wake for a failed RX retry without making overdue AGC a hot poll.
if (receive_retry && receive_retry_delay < delay_millis) {
delay_millis = receive_retry_delay;
}
} else {
// TX completion/timeout still needs the normal fast lifecycle path.
delay_millis = 0;
@@ -595,6 +603,11 @@ bool Dispatcher::getNextQueueWakeDelay(uint32_t& delay_millis) const {
return true;
}
if (receive_retry) {
if (!found || receive_retry_delay < shortest_delay) shortest_delay = receive_retry_delay;
found = true;
}
if (outbound_restore_cr) {
const int32_t restore_delay = (int32_t)(outbound_restore_retry_at - now);
const uint32_t delay = restore_delay > 0 ? (uint32_t)restore_delay : 0;
@@ -854,7 +867,11 @@ void Dispatcher::loop() {
agc_reset.noteAttempt(_ms->getMillis());
} else {
// Ownership/calibration deferral is not a reset or health evidence.
agc_reset.noteDeferred(_ms->getMillis());
const uint32_t deferred_at = _ms->getMillis();
uint32_t retry_delay = 50, receive_delay;
if (adaptive && _radio->getReceiveRetryDelay(deferred_at, receive_delay)
&& receive_delay > retry_delay) retry_delay = receive_delay;
agc_reset.noteDeferred(deferred_at, retry_delay);
}
}
+5
View File
@@ -215,6 +215,11 @@ public:
return getNoiseFloorCalibrationSecondsRemaining();
}
virtual bool isCalibratingNoiseFloor() const { return false; }
// A failed receive start may need a later retry without keeping the MCU
// awake. Transports without a deferred receive retry retain their cadence.
virtual bool getReceiveRetryDelay(uint32_t now, uint32_t& delay_ms) const {
(void)now; (void)delay_ms; return false;
}
virtual void triggerNoiseFloorCalibrate(int threshold) { }
+3 -2
View File
@@ -54,8 +54,9 @@ public:
// A busy operation changes only its retry deadline, never the reset cadence
// or adaptive stage. This permits sleep instead of spinning on a due reset.
void noteDeferred(uint32_t now) {
if (_state) _next = now + 50;
void noteDeferred(uint32_t now, uint32_t retry_delay_ms = 50) {
if (retry_delay_ms == 0 || retry_delay_ms > INT32_MAX) retry_delay_ms = 50;
if (_state) _next = now + retry_delay_ms;
}
// Call only after an actual accepted reset sequence, not a busy/deferred one.
+46 -1
View File
@@ -207,6 +207,9 @@ void RadioLibWrapper::begin() {
}
#endif
state = STATE_IDLE;
_rx_retry_at = 0;
_rx_retry_delay_ms = 0;
_rx_retry_pending = false;
if (_board->getStartupReason() == BD_STARTUP_RX_PACKET) { // received a LoRa packet (while in deep sleep)
_loop_event_pending = true; // Main-owned wake event; ISR remains sole publisher
@@ -1877,6 +1880,8 @@ void RadioLibWrapper::startRecv() {
// Keep the partial block, but wait for the frontend before its next sample.
_nf_sample_from = millis() + NF_CALIB_SETTLE_MS;
_startrx_fails = 0;
_rx_retry_pending = false;
_rx_retry_delay_ms = 0;
if (_rx_ps_armed) {
// (re)base the duty-cycle watchdog on the freshly armed cycle
_wd_last_busy = isChipBusy();
@@ -1895,6 +1900,15 @@ void RadioLibWrapper::startRecv() {
if (_wd_observe_ms > 1500) _wd_observe_ms = 1500;
}
} else {
state = STATE_IDLE;
_rx_ps_armed = false;
// Recover a transient failure promptly, then permit whole-second timed
// sleep under persistent failure. Direct retune/CAD/packet rearms remain
// immediate; only recvRaw's repeated idle fallback observes this deadline.
_rx_retry_delay_ms = _rx_retry_delay_ms == 0 ? 20
: (_rx_retry_delay_ms < 1000 ? _rx_retry_delay_ms * 2 : 2000);
_rx_retry_at = millis() + _rx_retry_delay_ms;
_rx_retry_pending = true;
if (_startrx_fails < 255) _startrx_fails++;
MESH_DEBUG_PRINTLN("RadioLibWrapper: error: startReceiveMode(%d)", err);
}
@@ -1933,6 +1947,15 @@ bool RadioLibWrapper::isInRecvMode() const {
return state == STATE_RX;
}
bool RadioLibWrapper::getReceiveRetryDelay(uint32_t now, uint32_t& delay_ms) const {
#if MESH_ENABLE_TX_BEACONS
if (_beacon_owned) return false;
#endif
if (!_rx_retry_pending || state != STATE_IDLE || _cw_active) return false;
delay_ms = static_cast<int32_t>(now - _rx_retry_at) >= 0 ? 0 : _rx_retry_at - now;
return true;
}
// RX PowerSaving
bool RadioLibWrapper::setRxPowerSaving(bool enabled, uint32_t rx_us, uint32_t sleep_us) {
#if MESH_ENABLE_TX_BEACONS
@@ -1965,7 +1988,7 @@ int RadioLibWrapper::recvRaw(uint8_t* bytes, int sz) {
#if MESH_ENABLE_TX_BEACONS
if (_beacon_owned) return 0;
#endif
if (_cw_active) return 0;
if (_cw_active || state == STATE_TX_WAIT) return 0;
int len = 0;
#if defined(USE_LR2021)
bool receiver_stopped = false;
@@ -2005,6 +2028,8 @@ int RadioLibWrapper::recvRaw(uint8_t* bytes, int sz) {
// Serial.print(" readData() -> "); Serial.println(len);
n_recv++;
last_recv_millis = millis();
_rx_retry_pending = false;
_rx_retry_delay_ms = 0;
}
}
}
@@ -2022,6 +2047,9 @@ int RadioLibWrapper::recvRaw(uint8_t* bytes, int sz) {
return 0;
}
#endif
// Serviced receive events retain the normal immediate rearm, even if an
// earlier idle start failed. A pending IRQ probe above still owns its FIFO.
_rx_retry_pending = false;
state = STATE_IDLE; // need another startReceive()
}
@@ -2033,16 +2061,32 @@ int RadioLibWrapper::recvRaw(uint8_t* bytes, int sz) {
int err = _radio->startReceive();
if (err == RADIOLIB_ERR_NONE) {
state = STATE_RX;
_rx_retry_pending = false;
_rx_retry_delay_ms = 0;
if (_nf_calib_active) {
_nf_sample_from = millis() + NF_CALIB_SETTLE_MS;
}
} else {
_rx_retry_delay_ms = 20; // the delivered packet just proved RX was working
_rx_retry_at = millis() + _rx_retry_delay_ms;
_rx_retry_pending = true;
MESH_DEBUG_PRINTLN("RadioLibWrapper: error: startReceive after packet (%d)", err);
}
return len;
}
if (state != STATE_RX) {
if (_rx_retry_pending) {
const uint32_t now = millis();
if (static_cast<int32_t>(now - _rx_retry_at) < 0) return len;
// A command error can leave hardware receiving despite the idle software
// state. Preserve that owner and move the retry deadline forward instead
// of keeping a permanently due wake or issuing SPI while BUSY is high.
if (isChipBusy() || isPacketPendingOrReceiving() || hasPendingRadioInterrupt()) {
_rx_retry_at = now + 2000;
return len;
}
}
startRecv();
}
return len;
@@ -2164,6 +2208,7 @@ void RadioLibWrapper::onSendFinished() {
claimRadioInterrupt();
_irq_probe_failures = 0;
_board->onAfterTransmit();
_rx_retry_pending = false; // TX retirement gets its usual immediate RX rearm
state = STATE_IDLE;
}
+8 -2
View File
@@ -58,6 +58,11 @@ protected:
NoiseFloorEstimator _secondary_floor_estimator;
uint32_t _rx_mode_checked_at = 0;
uint8_t _rx_mode_failures = 0;
// Independent of calibration and watchdog counters: repeated failed starts
// must retain their backoff through an AGC reset or radio recovery attempt.
uint32_t _rx_retry_at = 0;
uint16_t _rx_retry_delay_ms = 0;
bool _rx_retry_pending = false;
unsigned long last_recv_millis;
unsigned long last_radio_interrupt_millis; // updated on any ISR event, even CRC errors
bool _rx_ps_enabled;
@@ -530,10 +535,11 @@ public:
// true while a noise-floor batch needs prompt loop service; the app's
// hasPendingWork() keeps the MCU awake for the bounded spaced-sample window.
bool isCalibratingNoiseFloor() const override {
return _nf_calib_active
return isInRecvMode() && !_rx_ps_armed && (_nf_calib_active
|| (_nf_refresh_requested
&& (!_rx_ps_enabled || _rx_ps_continuous_fallback));
&& (!_rx_ps_enabled || _rx_ps_continuous_fallback)));
}
bool getReceiveRetryDelay(uint32_t now, uint32_t& delay_ms) const override;
void resetStats() { n_recv = n_sent = n_recv_errors = 0; }
uint8_t getRadioState() const override;