diff --git a/zephcore/adapters/radio/LoRaRadioBase.cpp b/zephcore/adapters/radio/LoRaRadioBase.cpp index b59e730..40bf76e 100644 --- a/zephcore/adapters/radio/LoRaRadioBase.cpp +++ b/zephcore/adapters/radio/LoRaRadioBase.cpp @@ -504,6 +504,11 @@ bool LoRaRadioBase::startSendRaw(const uint8_t *bytes, int len) return false; } + /* Defensive gate: callers should defer TX while radio is BUSY. */ + if (!isRadioReady()) { + return false; + } + /* Last-moment hardware check before killing active RX. * Closes the race between the Dispatcher's isReceiving() guard * and hwCancelReceive() — if a preamble arrived in that gap, @@ -561,6 +566,13 @@ float LoRaRadioBase::getLastSNR() const return _last_snr; } +bool LoRaRadioBase::isRadioReady() +{ + /* BUSY high means the radio cannot accept SPI commands now + * (e.g. duty-cycle sleep phase on SX126x/LR11xx). */ + return !hwIsChipBusy(); +} + /* ── Airtime + scoring ────────────────────────────────────────────────── */ uint32_t LoRaRadioBase::getEstAirtimeFor(int len_bytes) @@ -617,12 +629,9 @@ void LoRaRadioBase::triggerNoiseFloorCalibrate(int threshold) return; } - /* When duty cycle is active the chip alternates between short RX - * windows and sleep. GetRssiInst during the sleep phase hangs the - * SPI bus (BUSY stuck high for the full 3 s timeout). Check the - * BUSY pin directly (GPIO read, no SPI) — if the chip is sleeping, - * skip this cycle and try again next housekeeping tick. */ - if (_rx_duty_cycle_enabled && hwIsChipBusy()) { + /* Skip when the radio cannot accept commands right now + * (e.g. duty-cycle sleep BUSY window). */ + if (!isRadioReady()) { return; } diff --git a/zephcore/adapters/radio/LoRaRadioBase.h b/zephcore/adapters/radio/LoRaRadioBase.h index 398ed9b..bb37285 100644 --- a/zephcore/adapters/radio/LoRaRadioBase.h +++ b/zephcore/adapters/radio/LoRaRadioBase.h @@ -53,6 +53,7 @@ public: bool isInRecvMode() const override; float getLastRSSI() const override; float getLastSNR() const override; + bool isRadioReady() override; /* Packet statistics */ uint32_t getPacketsRecv() const override { return (uint32_t)atomic_get(&_packets_recv); } diff --git a/zephcore/include/mesh/Radio.h b/zephcore/include/mesh/Radio.h index f9a1203..6bba447 100644 --- a/zephcore/include/mesh/Radio.h +++ b/zephcore/include/mesh/Radio.h @@ -1,42 +1,43 @@ -/* - * SPDX-License-Identifier: Apache-2.0 - * ZephCore Radio interface - matches Dispatcher.h - */ - -#pragma once - -#include - -namespace mesh { - -class Radio { -public: - virtual void begin() {} - - virtual int recvRaw(uint8_t *bytes, int sz) = 0; - virtual uint32_t getEstAirtimeFor(int len_bytes) = 0; - virtual float packetScore(float snr, int packet_len) = 0; - virtual bool startSendRaw(const uint8_t *bytes, int len) = 0; - virtual bool isSendComplete() = 0; - virtual void onSendFinished() = 0; - - virtual int getNoiseFloor() const { return 0; } - virtual void triggerNoiseFloorCalibrate(int threshold) { (void)threshold; } - virtual void resetAGC() {} - - virtual bool isInRecvMode() const = 0; - virtual bool isReceiving() { return false; } - virtual float getLastRSSI() const { return 0; } - virtual float getLastSNR() const { return 0; } - - /* Adaptive Power Control */ - virtual void setTxPowerReduction(int8_t reduction_db) { (void)reduction_db; } - virtual int8_t getTxPowerReduction() const { return 0; } - - /* Packet statistics */ - virtual uint32_t getPacketsRecv() const { return 0; } - virtual uint32_t getPacketsSent() const { return 0; } - virtual uint32_t getPacketsRecvErrors() const { return 0; } -}; - -} /* namespace mesh */ +/* + * SPDX-License-Identifier: Apache-2.0 + * ZephCore Radio interface - matches Dispatcher.h + */ + +#pragma once + +#include + +namespace mesh { + +class Radio { +public: + virtual void begin() {} + + virtual int recvRaw(uint8_t *bytes, int sz) = 0; + virtual uint32_t getEstAirtimeFor(int len_bytes) = 0; + virtual float packetScore(float snr, int packet_len) = 0; + virtual bool startSendRaw(const uint8_t *bytes, int len) = 0; + virtual bool isSendComplete() = 0; + virtual void onSendFinished() = 0; + + virtual int getNoiseFloor() const { return 0; } + virtual void triggerNoiseFloorCalibrate(int threshold) { (void)threshold; } + virtual void resetAGC() {} + + virtual bool isInRecvMode() const = 0; + virtual bool isReceiving() { return false; } + virtual bool isRadioReady() { return true; } + virtual float getLastRSSI() const { return 0; } + virtual float getLastSNR() const { return 0; } + + /* Adaptive Power Control */ + virtual void setTxPowerReduction(int8_t reduction_db) { (void)reduction_db; } + virtual int8_t getTxPowerReduction() const { return 0; } + + /* Packet statistics */ + virtual uint32_t getPacketsRecv() const { return 0; } + virtual uint32_t getPacketsSent() const { return 0; } + virtual uint32_t getPacketsRecvErrors() const { return 0; } +}; + +} /* namespace mesh */ diff --git a/zephcore/src/Dispatcher.cpp b/zephcore/src/Dispatcher.cpp index 83c48a0..b0c28a5 100644 --- a/zephcore/src/Dispatcher.cpp +++ b/zephcore/src/Dispatcher.cpp @@ -376,8 +376,9 @@ void Dispatcher::checkSend() } } - if (_radio->isReceiving()) { - /* Channel busy — enforce retry timer so we don't hammer the check */ + if (_radio->isReceiving() || !_radio->isRadioReady()) { + /* Channel busy or radio not command-ready — enforce retry timer + * so we don't hammer checks during RX activity or BUSY windows. */ if (!millisHasNowPassed(next_tx_time)) { if (_tx_queued_cb) { uint32_t remaining = next_tx_time - now; @@ -443,10 +444,9 @@ void Dispatcher::checkSend() } #endif - /* Final LBT check — close the gap between initial - * isReceiving() and actual TX start (serialisation + - * logging can take 1-5 ms). */ - if (_radio->isReceiving()) { + /* Final gate — close the gap between initial checks and + * actual TX start (serialisation + logging can take 1-5 ms). */ + if (_radio->isReceiving() || !_radio->isRadioReady()) { uint32_t retry = getCADFailRetryDelay(); _mgr->queueOutbound(outbound, 0, futureMillis((int)retry)); outbound = nullptr;