Make repeater RX watchdog MeshCore-aware and tune temporary radio
Use RX-only radio recovery and feed the repeater reboot watchdog after MeshCore parsing. Default to 24 hours, apply temporary watchdog and advert intervals in RAM, and report temporary durations as days, hours, and minutes. Add regression coverage and an ESP-NOW bridge setup guide.
This commit is contained in:
1 parent
91d636b8ff
commit
2b4a35adb7
19 files changed
+1265
-140
No files matched your search
+52
-64
@@ -49,9 +49,8 @@ void Dispatcher::setRadioAvailable(bool available) {
|
||||
agc_reset_armed = false;
|
||||
_radio->begin();
|
||||
prev_isrecv_mode = _radio->isInRecvMode();
|
||||
last_observed_radio_irq = _radio->getLastRadioInterruptMillis();
|
||||
#ifdef RADIO_LIVENESS_SOFT_ONLY
|
||||
last_radio_activity_ms = now;
|
||||
rx_watchdog_window_start = now;
|
||||
#else
|
||||
radio_liveness.begin(now);
|
||||
nonrx_soft_recovery_attempted = false;
|
||||
@@ -62,6 +61,7 @@ void Dispatcher::setRadioAvailable(bool available) {
|
||||
void Dispatcher::begin() {
|
||||
n_sent_flood = n_sent_direct = 0;
|
||||
n_recv_flood = n_recv_direct = 0;
|
||||
last_meshcore_recv_millis = 0;
|
||||
_err_flags = 0;
|
||||
outbound_radio_retry_at = 0;
|
||||
outbound_radio_retry_pending = false;
|
||||
@@ -80,14 +80,12 @@ void Dispatcher::begin() {
|
||||
if (radio_available) {
|
||||
_radio->begin();
|
||||
prev_isrecv_mode = _radio->isInRecvMode();
|
||||
last_observed_radio_irq = _radio->getLastRadioInterruptMillis();
|
||||
} else {
|
||||
prev_isrecv_mode = false;
|
||||
last_observed_radio_irq = 0;
|
||||
MESH_DEBUG_PRINTLN("Dispatcher: starting without a radio transport");
|
||||
}
|
||||
#ifdef RADIO_LIVENESS_SOFT_ONLY
|
||||
last_radio_activity_ms = now;
|
||||
rx_watchdog_window_start = now;
|
||||
#else
|
||||
radio_liveness.begin(now);
|
||||
nonrx_soft_recovery_attempted = false;
|
||||
@@ -309,16 +307,6 @@ void Dispatcher::loop() {
|
||||
_radio->loop();
|
||||
|
||||
const unsigned long now = _ms->getMillis();
|
||||
const unsigned long latest_irq = _radio->getLastRadioInterruptMillis();
|
||||
if (latest_irq != 0 && latest_irq != last_observed_radio_irq) {
|
||||
last_observed_radio_irq = latest_irq;
|
||||
#ifdef RADIO_LIVENESS_SOFT_ONLY
|
||||
last_radio_activity_ms = now;
|
||||
#else
|
||||
radio_liveness.noteActivity(now);
|
||||
#endif
|
||||
}
|
||||
|
||||
// check for radio 'stuck' in mode other than Rx
|
||||
bool is_recv = _radio->isInRecvMode();
|
||||
if (is_recv != prev_isrecv_mode) {
|
||||
@@ -339,7 +327,7 @@ void Dispatcher::loop() {
|
||||
MESH_DEBUG_PRINTLN("Radio watchdog: radio outside RX for %lu ms; soft recovery",
|
||||
now - radio_nonrx_start);
|
||||
if (_radio->recoverRadio(false)) {
|
||||
last_radio_activity_ms = now;
|
||||
rx_watchdog_window_start = now;
|
||||
recovered_this_loop = true;
|
||||
}
|
||||
#else
|
||||
@@ -352,47 +340,6 @@ void Dispatcher::loop() {
|
||||
radio_nonrx_start = now; // bounded retry cadence if recovery is unsupported
|
||||
}
|
||||
|
||||
// Continuous-RX can fail while the MCU and main loop remain healthy, so a
|
||||
// CPU watchdog cannot detect it. Use packet/IRQ/TX activity as proof of
|
||||
// radio life and stage recovery from a harmless re-arm to a radio-only
|
||||
// hardware reset when the driver supports one. Observer configuration may
|
||||
// request a faster soft check.
|
||||
uint32_t soft_liveness_ms = RADIO_LIVENESS_SOFT_MS;
|
||||
#ifdef WITH_MQTT_BRIDGE
|
||||
const uint32_t configured_watchdog_ms = getRadioWatchdogMillis();
|
||||
if (configured_watchdog_ms > 0) soft_liveness_ms = configured_watchdog_ms;
|
||||
#endif
|
||||
if (is_recv && outbound == NULL && !recovered_this_loop) {
|
||||
#ifdef RADIO_LIVENESS_SOFT_ONLY
|
||||
if (soft_liveness_ms > 0 && now - last_radio_activity_ms >= soft_liveness_ms) {
|
||||
_err_flags |= ERR_EVENT_RADIO_WATCHDOG;
|
||||
MESH_DEBUG_PRINTLN("Radio watchdog: no hardware activity for %lu ms, state=%d, soft recovery",
|
||||
now - last_radio_activity_ms, _radio->getRadioState());
|
||||
_radio->recoverRadio(false);
|
||||
// This radio has no independently resettable RF peripheral. Retry its
|
||||
// only safe recovery at the soft interval until IRQ/TX/RX proves life.
|
||||
last_radio_activity_ms = now;
|
||||
}
|
||||
#else
|
||||
uint32_t hard_liveness_ms = RADIO_LIVENESS_HARD_MS;
|
||||
if (soft_liveness_ms > hard_liveness_ms / 2) {
|
||||
hard_liveness_ms = soft_liveness_ms <= 0x7FFFFFFFUL
|
||||
? soft_liveness_ms * 2 : 0xFFFFFFFFUL;
|
||||
}
|
||||
const RadioRecoveryAction action = radio_liveness.poll(
|
||||
now, soft_liveness_ms, hard_liveness_ms);
|
||||
if (action != RadioRecoveryAction::NONE) {
|
||||
const bool hard = action == RadioRecoveryAction::HARD;
|
||||
_err_flags |= ERR_EVENT_RADIO_WATCHDOG;
|
||||
MESH_DEBUG_PRINTLN("Radio watchdog: no hardware activity for %lu ms, state=%d, %s recovery",
|
||||
now - radio_liveness.lastActivity(), _radio->getRadioState(),
|
||||
hard ? "hard" : "soft");
|
||||
const bool recovered = _radio->recoverRadio(hard);
|
||||
if (hard) radio_liveness.noteHardRecoveryResult(now, recovered);
|
||||
}
|
||||
#endif
|
||||
}
|
||||
|
||||
if (outbound) { // waiting for outbound send to complete, or for its one radio retry
|
||||
if (outbound_radio_retry_pending) {
|
||||
if (!millisHasNowPassed(outbound_radio_retry_at)) return;
|
||||
@@ -434,11 +381,6 @@ void Dispatcher::loop() {
|
||||
// otherwise a later CAD pause can trip the eight-second watchdog using
|
||||
// the start time of the whole healthy burst.
|
||||
radio_nonrx_start = _ms->getMillis();
|
||||
#ifdef RADIO_LIVENESS_SOFT_ONLY
|
||||
last_radio_activity_ms = radio_nonrx_start; // TX success -> radio is alive
|
||||
#else
|
||||
radio_liveness.noteActivity(radio_nonrx_start); // TX success -> radio is alive
|
||||
#endif
|
||||
restoreOutboundTxOverrides();
|
||||
logTx(outbound, 2 + outbound->getPathByteLen() + outbound->payload_len);
|
||||
onSendComplete(outbound);
|
||||
@@ -458,6 +400,7 @@ void Dispatcher::loop() {
|
||||
restoreOutboundTxOverrides();
|
||||
_err_flags |= ERR_EVENT_RADIO_WATCHDOG;
|
||||
const bool recovered = _radio->recoverRadio(true);
|
||||
recovered_this_loop = true;
|
||||
if (!recovered) {
|
||||
MESH_DEBUG_PRINTLN("%s Dispatcher::loop(): WARNING: hard radio recovery after TX timeout failed!",
|
||||
getLogDateTime());
|
||||
@@ -516,6 +459,48 @@ void Dispatcher::loop() {
|
||||
}
|
||||
}
|
||||
checkRecv();
|
||||
|
||||
// Count only successful recvRaw() delivery as RX activity. Check after
|
||||
// draining RX and completing TX, before starting the next queued transmit:
|
||||
// a packet arriving at the deadline prevents unnecessary recovery, and a
|
||||
// continuous TX queue cannot hide a deaf receiver. The driver defers any
|
||||
// recovery that would interrupt a pending packet or an active reception.
|
||||
if (_radio->isInRecvMode() && outbound == NULL && !recovered_this_loop) {
|
||||
const unsigned long now = _ms->getMillis();
|
||||
uint32_t soft_liveness_ms = RADIO_LIVENESS_SOFT_MS;
|
||||
#ifdef WITH_MQTT_BRIDGE
|
||||
const uint32_t configured_watchdog_ms = getRadioWatchdogMillis();
|
||||
if (configured_watchdog_ms > 0) soft_liveness_ms = configured_watchdog_ms;
|
||||
#endif
|
||||
#ifdef RADIO_LIVENESS_SOFT_ONLY
|
||||
if (soft_liveness_ms > 0 && (uint32_t)(now - rx_watchdog_window_start) >= soft_liveness_ms) {
|
||||
_err_flags |= ERR_EVENT_RADIO_WATCHDOG;
|
||||
MESH_DEBUG_PRINTLN("Radio watchdog: no packet received in observation window for %lu ms, state=%d, soft recovery",
|
||||
(unsigned long)(uint32_t)(now - rx_watchdog_window_start), _radio->getRadioState());
|
||||
_radio->recoverRadio(false);
|
||||
// This radio has no independently resettable RF peripheral. Allow a
|
||||
// full observation interval between attempts on a quiet mesh.
|
||||
rx_watchdog_window_start = now;
|
||||
}
|
||||
#else
|
||||
uint32_t hard_liveness_ms = RADIO_LIVENESS_HARD_MS;
|
||||
if (soft_liveness_ms > hard_liveness_ms / 2) {
|
||||
hard_liveness_ms = soft_liveness_ms <= 0x7FFFFFFFUL
|
||||
? soft_liveness_ms * 2 : 0xFFFFFFFFUL;
|
||||
}
|
||||
const RadioRecoveryAction action = radio_liveness.poll(
|
||||
now, soft_liveness_ms, hard_liveness_ms);
|
||||
if (action != RadioRecoveryAction::NONE) {
|
||||
const bool hard = action == RadioRecoveryAction::HARD;
|
||||
_err_flags |= ERR_EVENT_RADIO_WATCHDOG;
|
||||
MESH_DEBUG_PRINTLN("Radio watchdog: no packet received in observation window for %lu ms, state=%d, %s recovery",
|
||||
(unsigned long)(uint32_t)(now - radio_liveness.windowStart()), _radio->getRadioState(),
|
||||
hard ? "hard" : "soft");
|
||||
const bool recovered = _radio->recoverRadio(hard);
|
||||
if (hard) radio_liveness.noteHardRecoveryResult(now, recovered);
|
||||
}
|
||||
#endif
|
||||
}
|
||||
checkSend();
|
||||
releaseDroppedOutbound();
|
||||
}
|
||||
@@ -595,10 +580,12 @@ void Dispatcher::checkRecv() {
|
||||
uint8_t raw[MAX_TRANS_UNIT+1];
|
||||
int len = _radio->recvRaw(raw, MAX_TRANS_UNIT);
|
||||
if (len > 0) {
|
||||
// A successful driver read proves RX works even if application parsing
|
||||
// rejects the packet or the packet pool has no space. IRQs alone do not.
|
||||
#ifdef RADIO_LIVENESS_SOFT_ONLY
|
||||
last_radio_activity_ms = _ms->getMillis();
|
||||
rx_watchdog_window_start = _ms->getMillis();
|
||||
#else
|
||||
radio_liveness.noteActivity(_ms->getMillis());
|
||||
radio_liveness.noteReceive(_ms->getMillis());
|
||||
#endif
|
||||
snr = _radio->getLastSNR();
|
||||
rssi = _radio->getLastRSSI();
|
||||
@@ -609,6 +596,7 @@ void Dispatcher::checkRecv() {
|
||||
MESH_DEBUG_PRINTLN("%s Dispatcher::checkRecv(): WARNING: received data, no unused packets available!", getLogDateTime());
|
||||
} else {
|
||||
if (tryParsePacket(pkt, raw, len)) {
|
||||
last_meshcore_recv_millis = _ms->getMillis();
|
||||
pkt->_snr = snr * 4.0f;
|
||||
pkt->_rssi = (int16_t)rssi;
|
||||
score = _radio->packetScore(snr, len);
|
||||
|
||||
Reference in new issue
Block a user