From 38034d2ee2fb35abb3ad8396d6683650566385bf Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jaros=C5=82aw=20Doma=C5=84ski?= Date: Wed, 8 Jul 2026 14:25:14 +0200 Subject: [PATCH] Fixed RX duty-cycle rearming and profile timings --- src/helpers/CommonCLI.cpp | 23 +++++---- src/helpers/CommonCLI.h | 8 +++- src/helpers/radiolib/CustomLR1110.h | 6 +++ src/helpers/radiolib/CustomLR1110Wrapper.h | 14 +++++- src/helpers/radiolib/CustomSX1262.h | 24 ++++++++++ src/helpers/radiolib/CustomSX1262Wrapper.h | 13 ++++++ src/helpers/radiolib/RadioLibWrappers.cpp | 54 ++++++++++++++++------ src/helpers/radiolib/RadioLibWrappers.h | 5 +- 8 files changed, 122 insertions(+), 25 deletions(-) diff --git a/src/helpers/CommonCLI.cpp b/src/helpers/CommonCLI.cpp index 9a505ded..83eb2066 100644 --- a/src/helpers/CommonCLI.cpp +++ b/src/helpers/CommonCLI.cpp @@ -744,12 +744,16 @@ void CommonCLI::handleSetCmd(uint32_t sender_timestamp, char* command, char* rep enable = 0; } else if (strcmp(value, "on") == 0 || strcmp(value, "conservative") == 0) { enable = 1; - rx_us = RX_POWERSAVING_DEFAULT_RX_US; - sleep_us = RX_POWERSAVING_DEFAULT_SLEEP_US; + level = RX_POWERSAVING_CONSERVATIVE_LEVEL; + preamble = RX_POWERSAVING_PROFILE_PREAMBLE; + level_requested = true; + preamble_overridden = true; } else if (strcmp(value, "balanced") == 0) { enable = 1; - rx_us = RX_POWERSAVING_DEFAULT_RX_US; - sleep_us = RX_POWERSAVING_BALANCED_SLEEP_US; + level = RX_POWERSAVING_BALANCED_LEVEL; + preamble = RX_POWERSAVING_PROFILE_PREAMBLE; + level_requested = true; + preamble_overridden = true; } else { StrHelper::strncpy(tmp, value, sizeof(tmp)); const char *parts[4]; @@ -777,11 +781,11 @@ void CommonCLI::handleSetCmd(uint32_t sender_timestamp, char* command, char* rep strcpy(reply, "ERROR: use off|on|conservative|balanced|level <1-10>| "); return; } + } - if (level_requested && !calcRxPowerSavingLevel(level, _prefs->sf, _prefs->bw, preamble, &rx_us, &sleep_us)) { - strcpy(reply, "ERROR: level range is 1-10; preamble is 16 or 32"); - return; - } + if (level_requested && !calcRxPowerSavingLevel(level, _prefs->sf, _prefs->bw, preamble, &rx_us, &sleep_us)) { + strcpy(reply, "ERROR: level range is 1-10; preamble is 16 or 32"); + return; } if (!isValidRxPowerSavingPeriod(rx_us) || !isValidRxPowerSavingPeriod(sleep_us)) { @@ -804,7 +808,8 @@ void CommonCLI::handleSetCmd(uint32_t sender_timestamp, char* command, char* rep _prefs->rx_ps_level = level; _prefs->rx_ps_preamble = preamble_overridden ? preamble : 0; // 0 = auto (derive from SF) } else if (strcmp(value, "off") != 0) { - // on/conservative/balanced/manual timings are fixed, not level-derived. + // manual timings are fixed, not level-derived + // (the named profiles set level_requested and are handled above) _prefs->rx_ps_level = 0; _prefs->rx_ps_preamble = 0; } diff --git a/src/helpers/CommonCLI.h b/src/helpers/CommonCLI.h index 6c1e839e..46d55ccc 100644 --- a/src/helpers/CommonCLI.h +++ b/src/helpers/CommonCLI.h @@ -21,10 +21,16 @@ #define RX_POWERSAVING_DEFAULT_RX_US 65625UL #define RX_POWERSAVING_DEFAULT_SLEEP_US 60000UL -#define RX_POWERSAVING_BALANCED_SLEEP_US 80000UL #define RX_POWERSAVING_MIN_PERIOD_US 1000UL #define RX_POWERSAVING_MAX_PERIOD_US 30000000UL +// The named profiles are level presets pinned to a 16-symbol preamble: most +// deployed senders still transmit 16-symbol preambles regardless of the newer +// SF-based rule (32 for SF <= 8). Revisit once the field has largely migrated. +#define RX_POWERSAVING_CONSERVATIVE_LEVEL 1UL +#define RX_POWERSAVING_BALANCED_LEVEL 5UL +#define RX_POWERSAVING_PROFILE_PREAMBLE 16UL + struct NodePrefs { // persisted to file float airtime_factor; char node_name[32]; diff --git a/src/helpers/radiolib/CustomLR1110.h b/src/helpers/radiolib/CustomLR1110.h index 5d1618cb..95e51ba4 100644 --- a/src/helpers/radiolib/CustomLR1110.h +++ b/src/helpers/radiolib/CustomLR1110.h @@ -66,6 +66,12 @@ class CustomLR1110 : public LR1110 { bool getRxBoostedGainMode() const { return _rx_boosted; } bool isReceiving() { + // BUSY high means the chip is asleep (RX duty-cycle sleep window) or mid + // command, so it cannot be mid-receive - and the SPI read below would + // stall until the chip's next listen window. + uint32_t busy = this->mod->getGpio(); + if (busy != RADIOLIB_NC && this->mod->hal->digitalRead(busy)) return false; + uint16_t irq = getIrqStatus(); bool detected = ((irq & RADIOLIB_LR11X0_IRQ_SYNC_WORD_HEADER_VALID) || (irq & RADIOLIB_LR11X0_IRQ_PREAMBLE_DETECTED)); return detected; diff --git a/src/helpers/radiolib/CustomLR1110Wrapper.h b/src/helpers/radiolib/CustomLR1110Wrapper.h index 9f459d8a..f5e7dbff 100644 --- a/src/helpers/radiolib/CustomLR1110Wrapper.h +++ b/src/helpers/radiolib/CustomLR1110Wrapper.h @@ -35,15 +35,27 @@ public: protected: int startReceiveMode() override { + if (_rx_ps_armed) { + // stop the previous duty-cycle sequence with an explicit standby before + // reconfiguring the radio + stopReceiveDutyCycle(); + } if (!_rx_ps_enabled) { return _radio->startReceive(); } const RadioLibIrqFlags_t irqFlags = RADIOLIB_IRQ_RX_DEFAULT_FLAGS; - const RadioLibIrqFlags_t irqMask = RADIOLIB_IRQ_RX_DEFAULT_MASK; + // route RX timeout (false preamble detect) and error IRQs to the IRQ pin + // as well, so the wrapper re-arms RX instead of waiting forever + const RadioLibIrqFlags_t irqMask = + (1UL << RADIOLIB_IRQ_RX_DONE) | + (1UL << RADIOLIB_IRQ_TIMEOUT) | + (1UL << RADIOLIB_IRQ_CRC_ERR) | + (1UL << RADIOLIB_IRQ_HEADER_ERR); int err = ((CustomLR1110 *)_radio)->startReceiveDutyCycle(_rx_ps_rx_us, _rx_ps_sleep_us, irqFlags, irqMask); if (err == RADIOLIB_ERR_NONE) { + _rx_ps_armed = true; return err; } diff --git a/src/helpers/radiolib/CustomSX1262.h b/src/helpers/radiolib/CustomSX1262.h index ad201229..87b6b548 100644 --- a/src/helpers/radiolib/CustomSX1262.h +++ b/src/helpers/radiolib/CustomSX1262.h @@ -87,11 +87,35 @@ class CustomSX1262 : public SX1262 { } bool isReceiving() { + // BUSY high means the chip is asleep (RX duty-cycle sleep window) or mid + // command, so it cannot be mid-receive - and the SPI read below would + // stall until the chip's next listen window. + uint32_t busy = this->mod->getGpio(); + if (busy != RADIOLIB_NC && this->mod->hal->digitalRead(busy)) return false; + uint16_t irq = getIrqFlags(); bool detected = (irq & SX126X_IRQ_HEADER_VALID) || (irq & SX126X_IRQ_PREAMBLE_DETECTED); return detected; } + // Port of Semtech's sx126x_stop_rtc() (same registers as RadioLib's + // fixImplicitTimeout / datasheet errata 15.3): after duty-cycle RX ends via + // RxDone or SetStandby, the internal RTC keeps running and its pending + // event can silently knock a subsequently started RX back to standby with + // no IRQ, leaving the node deaf. Must be called before re-arming RX. + int16_t stopRTC() { + uint8_t rtcStop = 0x00; + int16_t state = writeRegister(RADIOLIB_SX126X_REG_RTC_CTRL, &rtcStop, 1); + RADIOLIB_ASSERT(state); + + uint8_t rtcEvent = 0; + state = readRegister(RADIOLIB_SX126X_REG_EVENT_MASK, &rtcEvent, 1); + RADIOLIB_ASSERT(state); + + rtcEvent |= 0x02; // clear the RX timeout event + return writeRegister(RADIOLIB_SX126X_REG_EVENT_MASK, &rtcEvent, 1); + } + bool getRxBoostedGainMode() { uint8_t rxGain = 0; readRegister(RADIOLIB_SX126X_REG_RX_GAIN, &rxGain, 1); diff --git a/src/helpers/radiolib/CustomSX1262Wrapper.h b/src/helpers/radiolib/CustomSX1262Wrapper.h index 22d5a2f2..6e880c0e 100644 --- a/src/helpers/radiolib/CustomSX1262Wrapper.h +++ b/src/helpers/radiolib/CustomSX1262Wrapper.h @@ -42,6 +42,12 @@ public: protected: int startReceiveMode() override { + if (_rx_ps_armed) { + // leaving duty-cycle mode (after RxDone or a reconfig): stop the + // sequencer and the still-running RTC, or its pending event can + // silently abort the RX we are about to start + stopReceiveDutyCycle(); + } if (!_rx_ps_enabled) { return _radio->startReceive(); } @@ -55,6 +61,7 @@ protected: int err = ((CustomSX1262 *)_radio)->startReceiveDutyCycle(_rx_ps_rx_us, _rx_ps_sleep_us, irqFlags, irqMask); if (err == RADIOLIB_ERR_NONE) { + _rx_ps_armed = true; return err; } @@ -62,6 +69,12 @@ protected: return _radio->startReceive(); } + void stopReceiveDutyCycle() override { + _radio->standby(); // also wakes the chip if it is in the sleep window + ((CustomSX1262 *)_radio)->stopRTC(); + _rx_ps_armed = false; + } + public: void doResetAGC() override { sx126xResetAGC((SX126x *)_radio); } diff --git a/src/helpers/radiolib/RadioLibWrappers.cpp b/src/helpers/radiolib/RadioLibWrappers.cpp index 22df970d..66001102 100644 --- a/src/helpers/radiolib/RadioLibWrappers.cpp +++ b/src/helpers/radiolib/RadioLibWrappers.cpp @@ -121,6 +121,24 @@ int RadioLibWrapper::startReceiveMode() { return _radio->startReceive(); } +void RadioLibWrapper::stopReceiveDutyCycle() { + // The duty-cycle sequencer only stops on RxDone or an explicit standby; + // issuing other mode commands while it runs leads to undefined behaviour. + _radio->standby(); + _rx_ps_armed = false; +} + +bool RadioLibWrapper::isPacketReady() { + if (!_rx_ps_armed) return true; // continuous RX: DIO1 only fires for RxDone/TxDone here + + // In duty-cycle RX the DIO1 interrupt also fires for RX timeout (false + // preamble detect) and header errors. GetRxBufferStatus still reports the + // *previous* packet's length then, so reading the buffer would re-deliver + // stale bytes as a ghost packet. Only read when the radio reports RxDone. + // (checkIrq errors are treated as ready, falling back to old behaviour.) + return _radio->checkIrq(RADIOLIB_IRQ_RX_DONE) != 0; +} + bool RadioLibWrapper::isInRecvMode() const { return (state & ~STATE_INT_READY) == STATE_RX; } @@ -146,17 +164,19 @@ bool RadioLibWrapper::setRxPowerSaving(bool enabled, uint32_t rx_us, uint32_t sl int RadioLibWrapper::recvRaw(uint8_t* bytes, int sz) { int len = 0; if (state & STATE_INT_READY) { - len = _radio->getPacketLength(); - if (len > 0) { - if (len > sz) { len = sz; } - int err = _radio->readData(bytes, len); - if (err != RADIOLIB_ERR_NONE) { - MESH_DEBUG_PRINTLN("RadioLibWrapper: error: readData(%d)", err); - len = 0; - n_recv_errors++; - } else { - // Serial.print(" readData() -> "); Serial.println(len); - n_recv++; + if (isPacketReady()) { + len = _radio->getPacketLength(); + if (len > 0) { + if (len > sz) { len = sz; } + int err = _radio->readData(bytes, len); + if (err != RADIOLIB_ERR_NONE) { + MESH_DEBUG_PRINTLN("RadioLibWrapper: error: readData(%d)", err); + len = 0; + n_recv_errors++; + } else { + // Serial.print(" readData() -> "); Serial.println(len); + n_recv++; + } } } state = STATE_IDLE; // need another startReceive() @@ -173,6 +193,11 @@ uint32_t RadioLibWrapper::getEstAirtimeFor(int len_bytes) { } bool RadioLibWrapper::startSendRaw(const uint8_t* bytes, int len) { + if (_rx_ps_armed) { + // stop the duty-cycle sequencer before SetTx, otherwise its next RTC + // event can fire mid-transmission and abort the TX + stopReceiveDutyCycle(); + } _board->onBeforeTransmit(); int err = _radio->startTransmit((uint8_t *) bytes, len); if (err == RADIOLIB_ERR_NONE) { @@ -205,8 +230,11 @@ int16_t RadioLibWrapper::performChannelScan() { } bool RadioLibWrapper::isChannelActive() { - // int.thresh: RSSI-based interference detection (relative to noise floor) - if (_threshold != 0 && getCurrentRSSI() > _noise_floor + _threshold) return true; + // int.thresh: RSSI-based interference detection (relative to noise floor). + // Skipped in RX duty-cycle mode: the frontend sleeps part of the time, so an + // instantaneous RSSI is meaningless and the SPI read would block until the + // chip's next listen window. + if (!_rx_ps_enabled && _threshold != 0 && getCurrentRSSI() > _noise_floor + _threshold) return true; // cad: hardware channel activity detection if (_cad_enabled) { diff --git a/src/helpers/radiolib/RadioLibWrappers.h b/src/helpers/radiolib/RadioLibWrappers.h index af3609f2..12f3e4e4 100644 --- a/src/helpers/radiolib/RadioLibWrappers.h +++ b/src/helpers/radiolib/RadioLibWrappers.h @@ -21,19 +21,22 @@ protected: int32_t _floor_sample_sum; uint8_t _preamble_sf; bool _rx_ps_enabled; + bool _rx_ps_armed; // radio is currently in RX duty-cycle mode uint32_t _rx_ps_rx_us; uint32_t _rx_ps_sleep_us; void idle(); void startRecv(); virtual int startReceiveMode(); + virtual void stopReceiveDutyCycle(); + virtual bool isPacketReady(); float packetScoreInt(float snr, int sf, int packet_len); virtual bool isReceivingPacket() =0; virtual void doResetAGC(); public: RadioLibWrapper(PhysicalLayer& radio, mesh::MainBoard& board) - : _radio(&radio), _board(&board), _preamble_sf(0), _rx_ps_enabled(false), + : _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) { n_recv = n_sent = n_recv_errors = 0; } void begin() override;