From 267f707a674788b578bc88979e94fa513d836e86 Mon Sep 17 00:00:00 2001 From: me Date: Sun, 2 Aug 2026 21:56:50 -0700 Subject: [PATCH] fix(jp-lbt): reduce backoff parameters for SF10 airtime SF12 (CR4:8) to SF10 (CR4:6) migration reduced airtime by ~4-5x. Backoff parameters adjusted proportionally. base_ms: 2000 -> 500 ms max_backoff: 16000 -> 4000 ms jitter is already airtime-scaled (airtime_ms / JP_LBT_JITTER_DIVISOR) and requires no change. Provisional values; to be refined with traffic statistics from deployed nodes. --- src/helpers/radiolib/RadioLibWrappers.cpp | 55 +++++++++++++++++++++-- src/helpers/radiolib/RadioLibWrappers.h | 46 ++++++++++++++----- 2 files changed, 87 insertions(+), 14 deletions(-) diff --git a/src/helpers/radiolib/RadioLibWrappers.cpp b/src/helpers/radiolib/RadioLibWrappers.cpp index 7146e47b6a..fa4ebb8eaa 100644 --- a/src/helpers/radiolib/RadioLibWrappers.cpp +++ b/src/helpers/radiolib/RadioLibWrappers.cpp @@ -2,6 +2,12 @@ #define RADIOLIB_STATIC_ONLY 1 #include "RadioLibWrappers.h" +#ifdef NRF52_PLATFORM + #define YIELD_TASK() vTaskDelay(1) +#else + #define YIELD_TASK() delay(1) +#endif + #define STATE_IDLE 0 #define STATE_RX 1 #define STATE_TX_WAIT 3 @@ -176,6 +182,9 @@ bool RadioLibWrapper::isSendComplete() { void RadioLibWrapper::onSendFinished() { _radio->finishTransmit(); _board->onAfterTransmit(); + if (isAS923_1_JP()) { + delay(50); // ARIB STD-T108 ยง3.4.1: >= 50ms between transmissions + } state = STATE_IDLE; } @@ -184,10 +193,50 @@ 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; + if (isAS923_1_JP()) { + // Non-blocking backoff: if a prior busy detection armed a backoff wait, + // report busy and let Dispatcher::checkSend() re-poll on the next loop() + // pass instead of spin-waiting here (which used to block the Dispatcher + // loop for up to 16s). + if (_lbt_backoff_active) { + if ((int32_t)(millis() - _lbt_deadline) < 0) { + return true; // still waiting; re-checked next call + } + _lbt_backoff_active = false; + return isChannelActive(); // backoff elapsed -- re-sense (bounded, single-level recursion) + } + + // ARIB STD-T108: 5ms continuous RSSI sensing, -80dBm absolute threshold + uint32_t sense_start = millis(); + while (millis() - sense_start < 5) { + if (getCurrentRSSI() > -80.0f) { + _busy_count++; + uint32_t base_ms = 500; + uint32_t max_backoff = min(base_ms * (1u << _busy_count), (uint32_t)4000); + _lbt_deadline = millis() + random(max_backoff / 2, max_backoff); + _lbt_backoff_active = true; + return true; + } + YIELD_TASK(); + } + // Channel free: reset busy counter and add airtime-scaled jitter. + // JP_LBT_JITTER_DIVISOR controls jitter upper bound: + // /8 -> SF12/BW125 ~975ms, SF7/BW62.5 ~50ms + // /16 -> SF12/BW125 ~490ms, SF7/BW62.5 ~25ms + // /32 -> SF12/BW125 ~245ms, SF7/BW62.5 ~12ms (default) + _busy_count = 0; + uint32_t airtime_ms = getEstAirtimeFor(MAX_TRANS_UNIT); + uint32_t jitter_until = millis() + random(0, airtime_ms / JP_LBT_JITTER_DIVISOR); + while (millis() < jitter_until) { + YIELD_TASK(); + } + // JP RSSI sensing passed; fall through to CAD if enabled + } else { + // Non-JP: RSSI-based interference detection (relative to noise floor) + if (_threshold != 0 && getCurrentRSSI() > _noise_floor + _threshold) return true; + } - // cad: hardware channel activity detection + // hardware channel activity detection (JP and non-JP) if (_cad_enabled) { int16_t result = performChannelScan(); // scanChannel() triggers DIO interrupt (CAD done) which sets STATE_INT_READY diff --git a/src/helpers/radiolib/RadioLibWrappers.h b/src/helpers/radiolib/RadioLibWrappers.h index e886c81b22..b58ac8ab02 100644 --- a/src/helpers/radiolib/RadioLibWrappers.h +++ b/src/helpers/radiolib/RadioLibWrappers.h @@ -3,9 +3,6 @@ #include #include -#ifdef USE_CC310_HW_CRYPTO -#include -#endif struct PacketMillis { uint32_t preambleMillis; // preamble-detect -> header-valid deadline uint32_t payloadMillis; // header-valid -> rx-done deadline @@ -18,10 +15,14 @@ class RadioLibWrapper : public mesh::Radio { uint32_t n_recv, n_sent, n_recv_errors; int16_t _noise_floor, _threshold; bool _cad_enabled; + uint8_t _busy_count; uint16_t _num_floor_samples; int32_t _floor_sample_sum; uint8_t _preamble_sf; + bool _lbt_backoff_active; // non-blocking wait after RSSI busy detection (up to 16s) + uint32_t _lbt_deadline; + void idle(); void startRecv(); float packetScoreInt(float snr, int sf, int packet_len); @@ -29,7 +30,7 @@ class RadioLibWrapper : public mesh::Radio { virtual void doResetAGC(); public: - RadioLibWrapper(PhysicalLayer& radio, mesh::MainBoard& board) : _radio(&radio), _board(&board), _preamble_sf(0) { n_recv = n_sent = 0; } + RadioLibWrapper(PhysicalLayer& radio, mesh::MainBoard& board) : _radio(&radio), _board(&board), _busy_count(0), _preamble_sf(0), _lbt_backoff_active(false), _lbt_deadline(0) { n_recv = n_sent = 0; } void begin() override; virtual void powerOff() { _radio->sleep(); } @@ -56,6 +57,36 @@ class RadioLibWrapper : public mesh::Radio { static uint16_t preambleLengthForSF(uint8_t sf) { return sf <= 8 ? 32 : 16; } void updatePreamble(uint8_t sf) { _preamble_sf = sf; _radio->setPreambleLength(preambleLengthForSF(sf)); } PacketMillis calcMaxPacketMillis(uint8_t sf, float bw, uint8_t cr, uint8_t preambleSymbols); + virtual uint8_t getCodingRate() const { return 8; } // default CR4/8, override in subclass + virtual float getFreqMHz() const { return 0.0f; } // default unknown, override in subclass + + static constexpr uint8_t JP_LBT_JITTER_DIVISOR = 32; + + bool isAS923_1_JP() const override { + float freq = getFreqMHz(); + return (fabsf(freq - 920.800f) < 0.05f || + fabsf(freq - 921.000f) < 0.05f || + fabsf(freq - 921.200f) < 0.05f); + } + + int getMaxTextLen() const override { + if (!isAS923_1_JP()) return 10 * 16; // default 160 bytes + uint8_t cr = getCodingRate(); + if (cr <= 5) return 64; // 3874ms @ SF12/BW125/CR4-5 + if (cr == 6) return 48; // 3874ms @ SF12/BW125/CR4-6 + if (cr == 7) return 32; // 3678ms @ SF12/BW125/CR4-7 + return 24; // 3547ms @ SF12/BW125/CR4-8 + } + + int getMaxGroupTextLen() const override { + if (!isAS923_1_JP()) return 10 * 16; // default 160 bytes + uint8_t cr = getCodingRate(); + if (cr <= 5) return 64; // 3710ms @ SF12/BW125/CR4-5 + if (cr == 6) return 48; // 3678ms @ SF12/BW125/CR4-6 + if (cr == 7) return 39; // 3907ms @ SF12/BW125/CR4-7 + return 29; // 3809ms @ SF12/BW125/CR4-8 + } + virtual int16_t performChannelScan(); int getNoiseFloor() const override { return _noise_floor; } @@ -89,15 +120,8 @@ class RadioNoiseListener : public mesh::RNG { RadioNoiseListener(PhysicalLayer& radio): _radio(&radio) { } void random(uint8_t* dest, size_t sz) override { -#ifdef USE_CC310_HW_CRYPTO - // CC310 TRNG is higher quality and environment-independent vs radio RSSI noise. - nRFCrypto.begin(); - nRFCrypto.Random.generate(dest, (uint16_t)sz); - nRFCrypto.end(); -#else for (int i = 0; i < sz; i++) { dest[i] = _radio->randomByte() ^ (::random(0, 256) & 0xFF); } -#endif } };