Reset RX recovery deadline after successful transmissions

This commit is contained in:
mikecarper committed 2026-09-08 12:35:37 -07:00
1 parent 6db10c3986
commit 4bd4a77989
2 files changed
+110 -2

No files matched your search

+7 -2
View File
@@ -429,10 +429,15 @@ void Dispatcher::loop() {
}
_radio->onSendFinished();
// RX can occur entirely between two loop observations during a TX
// burst. Start a fresh RX recovery allowance after each successful TX;
// 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 = _ms->getMillis(); // TX success -> radio is alive
last_radio_activity_ms = radio_nonrx_start; // TX success -> radio is alive
#else
radio_liveness.noteActivity(_ms->getMillis()); // TX success -> radio is alive
radio_liveness.noteActivity(radio_nonrx_start); // TX success -> radio is alive
#endif
restoreOutboundTxOverrides();
logTx(outbound, 2 + outbound->getPathByteLen() + outbound->payload_len);
@@ -3,6 +3,8 @@
#include <helpers/RadioLivenessTracker.h>
#include <helpers/radiolib/LR2021Band.h>
#include <helpers/radiolib/RxBoostedGainDefaults.h>
#include <Dispatcher.h>
#include <helpers/StaticPoolPacketManager.h>
using mesh::RadioLivenessTracker;
using mesh::RadioRecoveryAction;
@@ -63,6 +65,107 @@ TEST(LR2021Band, SelectsTheCorrectFrontEnd) {
EXPECT_TRUE(mesh::lr2021::isHighBand(2400.0f));
}
namespace {
class AdvancingClock : public mesh::MillisecondClock {
public:
unsigned long now = 1;
unsigned long getMillis() override { return now++; }
};
class BurstRadio : public mesh::Radio {
public:
bool receiving = true;
bool cad_busy = false;
bool broken_rx = false;
int completed = 0;
int recoveries = 0;
int recvRaw(uint8_t*, int) override {
if (!broken_rx) receiving = true;
return 0;
}
uint32_t getEstAirtimeFor(int) override { return 1000; }
float packetScore(float, int) override { return 1; }
bool startSendRaw(const uint8_t*, int) override {
receiving = false;
return true;
}
bool isSendComplete() override { ++completed; return true; }
void onSendFinished() override { receiving = false; }
bool isInRecvMode() const override { return receiving; }
bool isReceiving() override {
// Active CAD may leave the radio outside RX until the next recvRaw().
if (cad_busy) receiving = false;
return cad_busy;
}
bool recoverRadio(bool) override {
++recoveries;
receiving = !broken_rx;
return !broken_rx;
}
};
class WatchdogDispatcher : public mesh::Dispatcher {
public:
WatchdogDispatcher(BurstRadio& radio, AdvancingClock& clock,
StaticPoolPacketManager& packets)
: Dispatcher(radio, clock, packets) {}
mesh::DispatcherAction onRecvPacket(mesh::Packet*) override { return ACTION_RELEASE; }
uint16_t errors() const { return _err_flags; }
};
}
TEST(DispatcherRadioWatchdog, SuccessfulTxBurstDoesNotTriggerStuckRxRecovery) {
AdvancingClock clock;
BurstRadio radio;
StaticPoolPacketManager packets(16);
WatchdogDispatcher dispatcher(radio, clock, packets);
dispatcher.begin();
for (int i = 0; i < 12; ++i) {
auto* packet = packets.allocNew();
ASSERT_NE(packet, nullptr);
packet->header = ROUTE_TYPE_DIRECT | (PAYLOAD_TYPE_TXT_MSG << 2);
packet->path_len = 0;
packet->payload_len = 1;
packet->payload[0] = 0;
ASSERT_TRUE(dispatcher.sendPacket(packet, 0));
}
dispatcher.loop();
// RX happens inside each loop between completed TX and the next TX. The
// watchdog at the top of the loop sees only TX, for longer than 8 seconds.
for (int i = 0; i < 10; ++i) {
clock.now += 1000;
if (i == 9) radio.cad_busy = true;
dispatcher.loop();
}
ASSERT_EQ(radio.completed, 10);
clock.now += 100;
dispatcher.loop();
EXPECT_EQ(radio.recoveries, 0);
EXPECT_EQ(dispatcher.errors() & ERR_EVENT_STARTRX_TIMEOUT, 0);
// A subsequent genuine RX failure still gets its own recovery deadline.
radio.broken_rx = true;
clock.now += 8100;
dispatcher.loop();
EXPECT_EQ(radio.recoveries, 1);
EXPECT_NE(dispatcher.errors() & ERR_EVENT_STARTRX_TIMEOUT, 0);
}
TEST(DispatcherRadioWatchdog, StuckReceiverStillRecoversWithoutTransmitActivity) {
AdvancingClock clock;
BurstRadio radio;
StaticPoolPacketManager packets(2);
WatchdogDispatcher dispatcher(radio, clock, packets);
dispatcher.begin();
radio.broken_rx = true;
radio.receiving = false;
dispatcher.loop();
clock.now += 8100;
dispatcher.loop();
EXPECT_EQ(radio.recoveries, 1);
EXPECT_NE(dispatcher.errors() & ERR_EVENT_STARTRX_TIMEOUT, 0);
}
int main(int argc, char** argv) {
::testing::InitGoogleTest(&argc, argv);
return RUN_ALL_TESTS();