Fixed RX duty-cycle rearming and profile timings

This commit is contained in:
Jarosław Domański committed 2026-07-08 14:25:14 +02:00
1 parent e97f0fecc6
commit 38034d2ee2
8 files changed
+122 -25

No files matched your search

+14 -9
View File
@@ -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>|<rx_us> <sleep_us>");
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 <rx_us> <sleep_us> 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;
}
+7 -1
View File
@@ -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];
+6
View File
@@ -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;
+13 -1
View File
@@ -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;
}
+24
View File
@@ -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);
@@ -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); }
+41 -13
View File
@@ -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) {
+4 -1
View File
@@ -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;