From 4f07061f9b91f29a9c96c6c5ca73d6630dc3d069 Mon Sep 17 00:00:00 2001 From: mmmorks Date: Tue, 8 Sep 2026 00:10:52 -0700 Subject: [PATCH] Fix the unsigned underflow in calcMaxPacketMillis()'s airtime fallback The fallback said "4 secs" and computed 4000 - preamble_us, i.e. 4 ms minus a preamble that exceeds it at every setting there is. The subtraction is unsigned, so the result is not a mis-sized deadline but the absence of one: at SF7/BW250 preamble_us is 22656, 4000 - 22656 wraps to 4294948640 us, and the later (payload_us * 8) / cr overflows again, leaving a payload watchdog of roughly 49 days. CustomSX1262::isReceiving() uses that deadline to clear a latched HEADER_VALID, so a header IRQ that never completes into a packet would hold the channel "busy" for the life of the node. Make the fallback a flat 4 s of payload (MAX_PACKET_FALLBACK_PAYLOAD_US, overridable per platform), with no subtraction left to underflow. This hardens a latent path rather than fixing an observed failure: the branch is only reached when getTimeOnAir(MAX_TRANS_UNIT) <= preamble_us, which happens on an unconfigured modem, and every caller in the tree (Custom{SX1262,SX1268,LLCC68,LR1110,LR2021,STM32WLx}Wrapper::setParams) configures freq/bw/sf/cr first. It is one setParams ordering change away from being live, and the failure mode is silent and permanent. --- src/helpers/radiolib/RadioLibWrappers.cpp | 25 ++++++++++++++++++++++- 1 file changed, 24 insertions(+), 1 deletion(-) diff --git a/src/helpers/radiolib/RadioLibWrappers.cpp b/src/helpers/radiolib/RadioLibWrappers.cpp index e4d2ba1c27..0eefada734 100644 --- a/src/helpers/radiolib/RadioLibWrappers.cpp +++ b/src/helpers/radiolib/RadioLibWrappers.cpp @@ -11,6 +11,14 @@ #define NUM_NOISE_FLOOR_SAMPLES 64 #define SAMPLING_THRESHOLD 14 +// Payload budget calcMaxPacketMillis() assumes when the modem cannot tell it +// how long a packet takes. Long on purpose: this deadline exists to break a +// stuck header IRQ, and one that expires early would clear the flags of a +// packet still arriving. +#ifndef MAX_PACKET_FALLBACK_PAYLOAD_US + #define MAX_PACKET_FALLBACK_PAYLOAD_US 4000000UL +#endif + static volatile uint8_t state = STATE_IDLE; // this function is called when a complete packet @@ -252,7 +260,22 @@ PacketMillis RadioLibWrapper::calcMaxPacketMillis(uint8_t sf, float bw, uint8_t // airtime for max packet at current radio settings uint32_t total_us = _radio->getTimeOnAir(MAX_TRANS_UNIT); // airtime for payload only (no preamble, header or SOF) - uint32_t payload_us = total_us > preamble_us ? total_us - preamble_us : 4000 - preamble_us; // fallback to 4 secs at worst case + uint32_t payload_us; + if (total_us > preamble_us) { + payload_us = total_us - preamble_us; + } else { + // getTimeOnAir() gave nothing usable (it returns 0 on an unconfigured + // modem). Fall back to the 4 s this has always claimed -- as 4 s of + // *payload*, not as 4 s of total airtime minus the preamble. + // + // The value used to be 4000, i.e. 4 ms, and the subtraction underflowed for + // any setting whose preamble exceeds that: every one of them. An underflow + // here is not a mis-sized deadline but the absence of one, because the + // result becomes a ~49-day payload watchdog, so CustomSX1262::isReceiving() + // would hold a latched HEADER_VALID true forever and isReceiving() would + // never again report the channel idle. + payload_us = MAX_PACKET_FALLBACK_PAYLOAD_US; + } // rescale payload_us for max possible CR if (cr >= 5 && cr < 8) { payload_us = (payload_us * 8) / cr; }