From b6c54b747d9fa7c58640ea3dfa91ab6e0c998ab8 Mon Sep 17 00:00:00 2001 From: me Date: Mon, 22 Jun 2026 12:34:58 -0700 Subject: [PATCH 01/11] Add isJapanMode() frequency detection for ARIB STD-T108 Add getCodingRate() and getFreqMHz() virtual methods to RadioLibWrapper base class, enabling subclasses to report their operating frequency. isJapanMode() detects JP 920MHz band (CH25-27) for ARIB STD-T108 compliance. getMaxTextLen() and getMaxGroupTextLen() enforce 4-second airtime limits per SF12/BW125 measurements. --- src/helpers/radiolib/RadioLibWrappers.h | 28 +++++++++++++++++++++++++ 1 file changed, 28 insertions(+) diff --git a/src/helpers/radiolib/RadioLibWrappers.h b/src/helpers/radiolib/RadioLibWrappers.h index 99f5ebbd8e..f37f7f48f4 100644 --- a/src/helpers/radiolib/RadioLibWrappers.h +++ b/src/helpers/radiolib/RadioLibWrappers.h @@ -56,6 +56,34 @@ 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 + + bool isJapanMode() const { + 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 { + if (!isJapanMode()) 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 { + if (!isJapanMode()) 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; } From 3ff8b2802847e91e7a4b86defc0b848acca78a18 Mon Sep 17 00:00:00 2001 From: me Date: Mon, 22 Jun 2026 12:46:23 -0700 Subject: [PATCH 02/11] Add ARIB STD-T108 LBT path to isChannelActive() In JP 920MHz band (CH25-27): 5ms continuous RSSI sensing at -80dBm absolute threshold, exponential backoff (2000-16000ms) on busy, jitter (0-500ms) on free, then falls through to CAD if enabled. Non-JP path: existing relative-RSSI threshold + CAD unchanged. Add YIELD_TASK() macro and _busy_count field for backoff tracking. --- src/helpers/radiolib/RadioLibWrappers.cpp | 36 +++++++++++++++++++++-- src/helpers/radiolib/RadioLibWrappers.h | 3 +- 2 files changed, 35 insertions(+), 4 deletions(-) diff --git a/src/helpers/radiolib/RadioLibWrappers.cpp b/src/helpers/radiolib/RadioLibWrappers.cpp index dc851e5cfe..192f44b1c4 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 @@ -191,10 +197,34 @@ 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 (isJapanMode()) { + // 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 = 2000; + uint32_t max_backoff = min(base_ms * (1u << _busy_count), (uint32_t)16000); + uint32_t backoff_until = millis() + random(max_backoff / 2, max_backoff); + while (millis() < backoff_until) { + YIELD_TASK(); + } + return true; + } + YIELD_TASK(); + } + _busy_count = 0; + uint32_t jitter_until = millis() + random(0, 500); + 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 f37f7f48f4..34f26d2f7b 100644 --- a/src/helpers/radiolib/RadioLibWrappers.h +++ b/src/helpers/radiolib/RadioLibWrappers.h @@ -18,6 +18,7 @@ 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; @@ -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) { n_recv = n_sent = 0; } void begin() override; virtual void powerOff() { _radio->sleep(); } From 7a171a2b93a66c7a782b72b81d5c1076067e3c7d Mon Sep 17 00:00:00 2001 From: me Date: Mon, 22 Jun 2026 12:58:44 -0700 Subject: [PATCH 03/11] Prevent forced TX during JP LBT backoff via getCADFailMaxDuration() Add isJapanMode() virtual to mesh::Radio (default false) so Dispatcher can query frequency context without depending on RadioLibWrapper. Dispatcher::getCADFailMaxDuration() returns UINT32_MAX for JP nodes, eliminating the 4-second forced-TX that would violate ARIB STD-T108. Non-JP nodes retain the original 4-second safety timeout unchanged. --- src/Dispatcher.cpp | 1 + src/Dispatcher.h | 2 ++ src/helpers/radiolib/RadioLibWrappers.h | 2 +- 3 files changed, 4 insertions(+), 1 deletion(-) diff --git a/src/Dispatcher.cpp b/src/Dispatcher.cpp index c0610b7f8a..d0cfc39660 100644 --- a/src/Dispatcher.cpp +++ b/src/Dispatcher.cpp @@ -60,6 +60,7 @@ uint32_t Dispatcher::getCADFailRetryDelay() const { return 200; } uint32_t Dispatcher::getCADFailMaxDuration() const { + if (_radio->isJapanMode()) return UINT32_MAX; // ARIB STD-T108: never force TX during LBT return 4000; // 4 seconds } diff --git a/src/Dispatcher.h b/src/Dispatcher.h index aad6cba3ec..e53b75aa01 100644 --- a/src/Dispatcher.h +++ b/src/Dispatcher.h @@ -78,6 +78,8 @@ class Radio { virtual float getLastRSSI() const { return 0; } virtual float getLastSNR() const { return 0; } + + virtual bool isJapanMode() const { return false; } }; /** diff --git a/src/helpers/radiolib/RadioLibWrappers.h b/src/helpers/radiolib/RadioLibWrappers.h index 34f26d2f7b..3d6b1f4ceb 100644 --- a/src/helpers/radiolib/RadioLibWrappers.h +++ b/src/helpers/radiolib/RadioLibWrappers.h @@ -60,7 +60,7 @@ class RadioLibWrapper : public mesh::Radio { 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 - bool isJapanMode() const { + bool isJapanMode() const override { float freq = getFreqMHz(); return (fabsf(freq - 920.800f) < 0.05f || fabsf(freq - 921.000f) < 0.05f || From 833350bff3a2104b62c3bbe7b5cdcce8d35ed848 Mon Sep 17 00:00:00 2001 From: me Date: Mon, 22 Jun 2026 13:07:18 -0700 Subject: [PATCH 04/11] Add getCodingRate() and getFreqMHz() to CustomLR1110/Wrapper getCodingRate() returns codingRate+4 (RadioLib stores 1-4 internally, expose as CR4/5-8 matching the rest of the codebase). CustomLR1110Wrapper overrides both getCodingRate() and getFreqMHz() so isJapanMode() and getMaxTextLen() work correctly on T1000-E. --- src/helpers/radiolib/CustomLR1110.h | 1 + src/helpers/radiolib/CustomLR1110Wrapper.h | 2 ++ 2 files changed, 3 insertions(+) diff --git a/src/helpers/radiolib/CustomLR1110.h b/src/helpers/radiolib/CustomLR1110.h index 75674a1dfd..ec57ed1923 100644 --- a/src/helpers/radiolib/CustomLR1110.h +++ b/src/helpers/radiolib/CustomLR1110.h @@ -92,4 +92,5 @@ class CustomLR1110 : public LR1110 { } uint8_t getSpreadingFactor() const { return spreadingFactor; } + uint8_t getCodingRate() const { return this->codingRate + 4; } // RadioLib stores 1-4, return 5-8 }; diff --git a/src/helpers/radiolib/CustomLR1110Wrapper.h b/src/helpers/radiolib/CustomLR1110Wrapper.h index 44230c61c3..ca86bc95f3 100644 --- a/src/helpers/radiolib/CustomLR1110Wrapper.h +++ b/src/helpers/radiolib/CustomLR1110Wrapper.h @@ -43,6 +43,8 @@ class CustomLR1110Wrapper : public RadioLibWrapper { float getLastSNR() const override { return ((CustomLR1110 *)_radio)->getSNR(); } uint8_t getSpreadingFactor() const override { return ((CustomLR1110 *)_radio)->getSpreadingFactor(); } + uint8_t getCodingRate() const override { return ((CustomLR1110 *)_radio)->getCodingRate(); } + float getFreqMHz() const override { return ((CustomLR1110 *)_radio)->getFreqMHz(); } bool setRxBoostedGainMode(bool en) override { return ((CustomLR1110 *)_radio)->setRxBoostedGainMode(en) == RADIOLIB_ERR_NONE; From e4a9b11ce1e8dc6fcdf4c4d50ec0f29e5c89049b Mon Sep 17 00:00:00 2001 From: me Date: Mon, 22 Jun 2026 13:12:55 -0700 Subject: [PATCH 05/11] Add getCodingRate() and getFreqMHz() to CustomSX1262Wrapper Enables isJapanMode() and getMaxTextLen() to work on SX1262-based targets (WisMesh Tag, T-Echo Lite, T114, XIAO nRF52, etc.). codingRate accessed as SX1262 base class member +4 (same pattern as LR1110). --- src/helpers/radiolib/CustomSX1262Wrapper.h | 2 ++ 1 file changed, 2 insertions(+) diff --git a/src/helpers/radiolib/CustomSX1262Wrapper.h b/src/helpers/radiolib/CustomSX1262Wrapper.h index bfea50ec9c..4d29e5f7e4 100644 --- a/src/helpers/radiolib/CustomSX1262Wrapper.h +++ b/src/helpers/radiolib/CustomSX1262Wrapper.h @@ -37,6 +37,8 @@ class CustomSX1262Wrapper : public RadioLibWrapper { return packetScoreInt(snr, sf, packet_len); } uint8_t getSpreadingFactor() const override { return ((CustomSX1262 *)_radio)->spreadingFactor; } + uint8_t getCodingRate() const override { return ((CustomSX1262 *)_radio)->codingRate + 4; } // RadioLib stores 1-4, return 5-8 + float getFreqMHz() const override { return ((CustomSX1262 *)_radio)->freqMHz; } virtual void powerOff() override { ((CustomSX1262 *)_radio)->sleep(false); } From 0e86274ac7ac93e70825dfe540f60bd93b61219e Mon Sep 17 00:00:00 2001 From: me Date: Mon, 22 Jun 2026 13:17:59 -0700 Subject: [PATCH 06/11] =?UTF-8?q?Add=20ARIB=20STD-T108=20=C2=A73.4.1=20pos?= =?UTF-8?q?t-TX=2050ms=20wait=20to=20onSendFinished()?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/helpers/radiolib/RadioLibWrappers.cpp | 3 +++ 1 file changed, 3 insertions(+) diff --git a/src/helpers/radiolib/RadioLibWrappers.cpp b/src/helpers/radiolib/RadioLibWrappers.cpp index 192f44b1c4..81dbbbaf88 100644 --- a/src/helpers/radiolib/RadioLibWrappers.cpp +++ b/src/helpers/radiolib/RadioLibWrappers.cpp @@ -189,6 +189,9 @@ bool RadioLibWrapper::isSendComplete() { void RadioLibWrapper::onSendFinished() { _radio->finishTransmit(); _board->onAfterTransmit(); + if (isJapanMode()) { + delay(50); // ARIB STD-T108 §3.4.1: >= 50ms between transmissions + } state = STATE_IDLE; } From 33b2bc3274e1f5b0f6506a9e1c2d4c61e5a28f98 Mon Sep 17 00:00:00 2001 From: me Date: Mon, 22 Jun 2026 13:33:04 -0700 Subject: [PATCH 07/11] Rename isJapanMode() to isAS923_1_JP() for clarity --- src/Dispatcher.cpp | 2 +- src/Dispatcher.h | 2 +- src/helpers/radiolib/RadioLibWrappers.cpp | 4 ++-- src/helpers/radiolib/RadioLibWrappers.h | 6 +++--- 4 files changed, 7 insertions(+), 7 deletions(-) diff --git a/src/Dispatcher.cpp b/src/Dispatcher.cpp index d0cfc39660..4ac78a1f5c 100644 --- a/src/Dispatcher.cpp +++ b/src/Dispatcher.cpp @@ -60,7 +60,7 @@ uint32_t Dispatcher::getCADFailRetryDelay() const { return 200; } uint32_t Dispatcher::getCADFailMaxDuration() const { - if (_radio->isJapanMode()) return UINT32_MAX; // ARIB STD-T108: never force TX during LBT + if (_radio->isAS923_1_JP()) return UINT32_MAX; // ARIB STD-T108: never force TX during LBT return 4000; // 4 seconds } diff --git a/src/Dispatcher.h b/src/Dispatcher.h index e53b75aa01..b64909fb1c 100644 --- a/src/Dispatcher.h +++ b/src/Dispatcher.h @@ -79,7 +79,7 @@ class Radio { virtual float getLastRSSI() const { return 0; } virtual float getLastSNR() const { return 0; } - virtual bool isJapanMode() const { return false; } + virtual bool isAS923_1_JP() const { return false; } }; /** diff --git a/src/helpers/radiolib/RadioLibWrappers.cpp b/src/helpers/radiolib/RadioLibWrappers.cpp index 81dbbbaf88..fda3b55828 100644 --- a/src/helpers/radiolib/RadioLibWrappers.cpp +++ b/src/helpers/radiolib/RadioLibWrappers.cpp @@ -189,7 +189,7 @@ bool RadioLibWrapper::isSendComplete() { void RadioLibWrapper::onSendFinished() { _radio->finishTransmit(); _board->onAfterTransmit(); - if (isJapanMode()) { + if (isAS923_1_JP()) { delay(50); // ARIB STD-T108 §3.4.1: >= 50ms between transmissions } state = STATE_IDLE; @@ -200,7 +200,7 @@ int16_t RadioLibWrapper::performChannelScan() { } bool RadioLibWrapper::isChannelActive() { - if (isJapanMode()) { + if (isAS923_1_JP()) { // ARIB STD-T108: 5ms continuous RSSI sensing, -80dBm absolute threshold uint32_t sense_start = millis(); while (millis() - sense_start < 5) { diff --git a/src/helpers/radiolib/RadioLibWrappers.h b/src/helpers/radiolib/RadioLibWrappers.h index 3d6b1f4ceb..bbcc91a0fe 100644 --- a/src/helpers/radiolib/RadioLibWrappers.h +++ b/src/helpers/radiolib/RadioLibWrappers.h @@ -60,7 +60,7 @@ class RadioLibWrapper : public mesh::Radio { 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 - bool isJapanMode() const override { + bool isAS923_1_JP() const override { float freq = getFreqMHz(); return (fabsf(freq - 920.800f) < 0.05f || fabsf(freq - 921.000f) < 0.05f || @@ -68,7 +68,7 @@ class RadioLibWrapper : public mesh::Radio { } int getMaxTextLen() const { - if (!isJapanMode()) return 10 * 16; // default 160 bytes + 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 @@ -77,7 +77,7 @@ class RadioLibWrapper : public mesh::Radio { } int getMaxGroupTextLen() const { - if (!isJapanMode()) return 10 * 16; // default 160 bytes + 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 From 9bece340eb02350f10adb987164f2e3ee0e3774e Mon Sep 17 00:00:00 2001 From: me Date: Mon, 22 Jun 2026 14:11:40 -0700 Subject: [PATCH 08/11] Enforce JP airtime limits via getMaxTextLen()/getMaxGroupTextLen() Add getMaxTextLen() and getMaxGroupTextLen() virtual to mesh::Radio (default 160 bytes). BaseChatMesh uses these instead of the hardcoded MAX_TEXT_LEN macro for limit checks in composeMsgPacket(), sendCommandData(), and sendGroupMessage(). Stack buffers remain sized to MAX_TEXT_LEN as a safe upper bound. --- src/Dispatcher.h | 3 +++ src/helpers/BaseChatMesh.cpp | 11 +++++++---- src/helpers/radiolib/RadioLibWrappers.h | 4 ++-- 3 files changed, 12 insertions(+), 6 deletions(-) diff --git a/src/Dispatcher.h b/src/Dispatcher.h index b64909fb1c..e4f2d4a537 100644 --- a/src/Dispatcher.h +++ b/src/Dispatcher.h @@ -80,6 +80,9 @@ class Radio { virtual float getLastSNR() const { return 0; } virtual bool isAS923_1_JP() const { return false; } + + virtual int getMaxTextLen() const { return 10 * 16; } // default 160 bytes + virtual int getMaxGroupTextLen() const { return 10 * 16; } // default 160 bytes }; /** diff --git a/src/helpers/BaseChatMesh.cpp b/src/helpers/BaseChatMesh.cpp index 972a97e9e6..75d27464ba 100644 --- a/src/helpers/BaseChatMesh.cpp +++ b/src/helpers/BaseChatMesh.cpp @@ -419,8 +419,9 @@ void BaseChatMesh::onGroupDataRecv(mesh::Packet* packet, uint8_t type, const mes mesh::Packet* BaseChatMesh::composeMsgPacket(const ContactInfo& recipient, uint32_t timestamp, uint8_t attempt, const char *text, uint32_t& expected_ack) { int text_len = strlen(text); - if (text_len > MAX_TEXT_LEN) return NULL; - if (attempt > 3 && text_len > MAX_TEXT_LEN-2) return NULL; + int max_len = _radio->getMaxTextLen(); + if (text_len > max_len) return NULL; + if (attempt > 3 && text_len > max_len - 2) return NULL; uint8_t temp[5+MAX_TEXT_LEN+1]; memcpy(temp, ×tamp, 4); // mostly an extra blob to help make packet_hash unique @@ -460,7 +461,8 @@ int BaseChatMesh::sendMessage(const ContactInfo& recipient, uint32_t timestamp, int BaseChatMesh::sendCommandData(const ContactInfo& recipient, uint32_t timestamp, uint8_t attempt, const char* text, uint32_t& est_timeout) { int text_len = strlen(text); - if (text_len > MAX_TEXT_LEN) return MSG_SEND_FAILED; + int max_len = _radio->getMaxTextLen(); + if (text_len > max_len) return MSG_SEND_FAILED; uint8_t temp[5+MAX_TEXT_LEN+1]; memcpy(temp, ×tamp, 4); // mostly an extra blob to help make packet_hash unique @@ -493,7 +495,8 @@ bool BaseChatMesh::sendGroupMessage(uint32_t timestamp, mesh::GroupChannel& chan char *ep = strchr((char *) &temp[5], 0); int prefix_len = ep - (char *) &temp[5]; - if (text_len + prefix_len > MAX_TEXT_LEN) text_len = MAX_TEXT_LEN - prefix_len; + int max_len = _radio->getMaxGroupTextLen(); + if (text_len + prefix_len > max_len) text_len = max_len - prefix_len; memcpy(ep, text, text_len); ep[text_len] = 0; // null terminator diff --git a/src/helpers/radiolib/RadioLibWrappers.h b/src/helpers/radiolib/RadioLibWrappers.h index bbcc91a0fe..125b0bea65 100644 --- a/src/helpers/radiolib/RadioLibWrappers.h +++ b/src/helpers/radiolib/RadioLibWrappers.h @@ -67,7 +67,7 @@ class RadioLibWrapper : public mesh::Radio { fabsf(freq - 921.200f) < 0.05f); } - int getMaxTextLen() const { + 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 @@ -76,7 +76,7 @@ class RadioLibWrapper : public mesh::Radio { return 24; // 3547ms @ SF12/BW125/CR4-8 } - int getMaxGroupTextLen() const { + 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 From b6c4521491f4bf10f627faeab7e51af2e6cffeae Mon Sep 17 00:00:00 2001 From: me Date: Tue, 23 Jun 2026 00:17:45 -0700 Subject: [PATCH 09/11] JP LBT: suppress txdelay to jitter-scale at SF12/BW125 --- examples/simple_repeater/MyMesh.cpp | 12 ++++++++++++ examples/simple_room_server/MyMesh.cpp | 12 ++++++++++++ examples/simple_sensor/SensorMesh.cpp | 16 ++++++++++++++-- src/helpers/radiolib/RadioLibWrappers.cpp | 8 +++++++- src/helpers/radiolib/RadioLibWrappers.h | 2 ++ 5 files changed, 47 insertions(+), 3 deletions(-) diff --git a/examples/simple_repeater/MyMesh.cpp b/examples/simple_repeater/MyMesh.cpp index c93ba1a4cd..28fe7b33a8 100644 --- a/examples/simple_repeater/MyMesh.cpp +++ b/examples/simple_repeater/MyMesh.cpp @@ -538,10 +538,22 @@ int MyMesh::calcRxDelay(float score, uint32_t air_time) const { } uint32_t MyMesh::getRetransmitDelay(const mesh::Packet *packet) { + if (_radio->isAS923_1_JP()) { + // JP LBT: suppress txdelay to jitter-scale to avoid adding unnecessary + // latency on top of LBT backoff. A window equal to jitter_max gives + // ~33% collision reduction vs zero, scales naturally with airtime as + // CR changes, and keeps average added delay to ~56ms at SF12/BW125. + uint32_t jitter_max = _radio->getEstAirtimeFor(MAX_TRANS_UNIT) / RadioLibWrapper::JP_LBT_JITTER_DIVISOR; + return getRNG()->nextInt(0, jitter_max + 1); + } uint32_t t = (_radio->getEstAirtimeFor(packet->getPathByteLen() + packet->payload_len + 2) * _prefs.tx_delay_factor); return getRNG()->nextInt(0, 5*t + 1); } uint32_t MyMesh::getDirectRetransmitDelay(const mesh::Packet *packet) { + if (_radio->isAS923_1_JP()) { + uint32_t jitter_max = _radio->getEstAirtimeFor(MAX_TRANS_UNIT) / RadioLibWrapper::JP_LBT_JITTER_DIVISOR; + return getRNG()->nextInt(0, jitter_max + 1); + } uint32_t t = (_radio->getEstAirtimeFor(packet->getPathByteLen() + packet->payload_len + 2) * _prefs.direct_tx_delay_factor); return getRNG()->nextInt(0, 5*t + 1); } diff --git a/examples/simple_room_server/MyMesh.cpp b/examples/simple_room_server/MyMesh.cpp index 0aff39cc1a..6210c9836d 100644 --- a/examples/simple_room_server/MyMesh.cpp +++ b/examples/simple_room_server/MyMesh.cpp @@ -289,10 +289,22 @@ const char *MyMesh::getLogDateTime() { } uint32_t MyMesh::getRetransmitDelay(const mesh::Packet *packet) { + if (_radio->isAS923_1_JP()) { + // JP LBT: suppress txdelay to jitter-scale to avoid adding unnecessary + // latency on top of LBT backoff. A window equal to jitter_max gives + // ~33% collision reduction vs zero, scales naturally with airtime as + // CR changes, and keeps average added delay to ~56ms at SF12/BW125. + uint32_t jitter_max = _radio->getEstAirtimeFor(MAX_TRANS_UNIT) / RadioLibWrapper::JP_LBT_JITTER_DIVISOR; + return getRNG()->nextInt(0, jitter_max + 1); + } uint32_t t = (_radio->getEstAirtimeFor(packet->getPathByteLen() + packet->payload_len + 2) * _prefs.tx_delay_factor); return getRNG()->nextInt(0, 5*t + 1); } uint32_t MyMesh::getDirectRetransmitDelay(const mesh::Packet *packet) { + if (_radio->isAS923_1_JP()) { + uint32_t jitter_max = _radio->getEstAirtimeFor(MAX_TRANS_UNIT) / RadioLibWrapper::JP_LBT_JITTER_DIVISOR; + return getRNG()->nextInt(0, jitter_max + 1); + } uint32_t t = (_radio->getEstAirtimeFor(packet->getPathByteLen() + packet->payload_len + 2) * _prefs.direct_tx_delay_factor); return getRNG()->nextInt(0, 5*t + 1); } diff --git a/examples/simple_sensor/SensorMesh.cpp b/examples/simple_sensor/SensorMesh.cpp index 17f8e323e5..1b31bd0b14 100644 --- a/examples/simple_sensor/SensorMesh.cpp +++ b/examples/simple_sensor/SensorMesh.cpp @@ -313,12 +313,24 @@ int SensorMesh::calcRxDelay(float score, uint32_t air_time) const { } uint32_t SensorMesh::getRetransmitDelay(const mesh::Packet* packet) { + if (_radio->isAS923_1_JP()) { + // JP LBT: suppress txdelay to jitter-scale to avoid adding unnecessary + // latency on top of LBT backoff. A window equal to jitter_max gives + // ~33% collision reduction vs zero, scales naturally with airtime as + // CR changes, and keeps average added delay to ~56ms at SF12/BW125. + uint32_t jitter_max = _radio->getEstAirtimeFor(MAX_TRANS_UNIT) / RadioLibWrapper::JP_LBT_JITTER_DIVISOR; + return getRNG()->nextInt(0, jitter_max + 1); + } uint32_t t = (_radio->getEstAirtimeFor(packet->getPathByteLen() + packet->payload_len + 2) * _prefs.tx_delay_factor); - return getRNG()->nextInt(0, 6)*t; + return getRNG()->nextInt(0, 5*t + 1); } uint32_t SensorMesh::getDirectRetransmitDelay(const mesh::Packet* packet) { + if (_radio->isAS923_1_JP()) { + uint32_t jitter_max = _radio->getEstAirtimeFor(MAX_TRANS_UNIT) / RadioLibWrapper::JP_LBT_JITTER_DIVISOR; + return getRNG()->nextInt(0, jitter_max + 1); + } uint32_t t = (_radio->getEstAirtimeFor(packet->getPathByteLen() + packet->payload_len + 2) * _prefs.direct_tx_delay_factor); - return getRNG()->nextInt(0, 6)*t; + return getRNG()->nextInt(0, 5*t + 1); } int SensorMesh::getInterferenceThreshold() const { return _prefs.interference_threshold; diff --git a/src/helpers/radiolib/RadioLibWrappers.cpp b/src/helpers/radiolib/RadioLibWrappers.cpp index fda3b55828..e4cdb20c95 100644 --- a/src/helpers/radiolib/RadioLibWrappers.cpp +++ b/src/helpers/radiolib/RadioLibWrappers.cpp @@ -216,8 +216,14 @@ bool RadioLibWrapper::isChannelActive() { } 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 jitter_until = millis() + random(0, 500); + 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(); } diff --git a/src/helpers/radiolib/RadioLibWrappers.h b/src/helpers/radiolib/RadioLibWrappers.h index 125b0bea65..5f120f5978 100644 --- a/src/helpers/radiolib/RadioLibWrappers.h +++ b/src/helpers/radiolib/RadioLibWrappers.h @@ -60,6 +60,8 @@ class RadioLibWrapper : public mesh::Radio { 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 || From 33318256f127edaf9c43e3e06164dcd92ea440ef Mon Sep 17 00:00:00 2001 From: jirogit Date: Fri, 17 Jul 2026 21:35:29 -0700 Subject: [PATCH 10/11] JP LBT: make busy-channel backoff non-blocking isChannelActive() previously spin-waited (with YIELD_TASK()) for up to 16s after detecting RSSI busy, blocking Dispatcher::loop() from servicing incoming packets for the duration of the backoff. Replace the blocking wait with a state machine (_lbt_backoff_active / _lbt_deadline): on busy detection, arm a deadline and return true (busy) immediately. Dispatcher::checkSend()'s existing retry mechanism (getCADFailRetryDelay()) re-polls isChannelActive() on the next loop() pass; once the deadline has elapsed, re-sense (bounded, single-level recursion) instead of assuming still-busy. The initial 5ms RSSI sense and the post-clear jitter wait remain blocking, as both are short (<=5ms and <=~1s respectively) relative to the up-to-16s backoff this change targets. Hardware-tested (LilyGo T3S3 sx1262 / Wio Tracker L1 Pro) in both quiet and electrically noisy environments; confirmed both bidirectional and unidirectional message delivery under legitimate busy-channel conditions. --- src/helpers/radiolib/RadioLibWrappers.cpp | 18 ++++++++++++++---- src/helpers/radiolib/RadioLibWrappers.h | 5 ++++- 2 files changed, 18 insertions(+), 5 deletions(-) diff --git a/src/helpers/radiolib/RadioLibWrappers.cpp b/src/helpers/radiolib/RadioLibWrappers.cpp index e4cdb20c95..760e5cf970 100644 --- a/src/helpers/radiolib/RadioLibWrappers.cpp +++ b/src/helpers/radiolib/RadioLibWrappers.cpp @@ -201,6 +201,18 @@ int16_t RadioLibWrapper::performChannelScan() { bool RadioLibWrapper::isChannelActive() { 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) { @@ -208,10 +220,8 @@ bool RadioLibWrapper::isChannelActive() { _busy_count++; uint32_t base_ms = 2000; uint32_t max_backoff = min(base_ms * (1u << _busy_count), (uint32_t)16000); - uint32_t backoff_until = millis() + random(max_backoff / 2, max_backoff); - while (millis() < backoff_until) { - YIELD_TASK(); - } + _lbt_deadline = millis() + random(max_backoff / 2, max_backoff); + _lbt_backoff_active = true; return true; } YIELD_TASK(); diff --git a/src/helpers/radiolib/RadioLibWrappers.h b/src/helpers/radiolib/RadioLibWrappers.h index 5f120f5978..9fb5432976 100644 --- a/src/helpers/radiolib/RadioLibWrappers.h +++ b/src/helpers/radiolib/RadioLibWrappers.h @@ -23,6 +23,9 @@ class RadioLibWrapper : public mesh::Radio { 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); @@ -30,7 +33,7 @@ class RadioLibWrapper : public mesh::Radio { virtual void doResetAGC(); public: - RadioLibWrapper(PhysicalLayer& radio, mesh::MainBoard& board) : _radio(&radio), _board(&board), _busy_count(0), _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(); } From 34bf7d141a804cd9df6e440b8efb504b091adbef Mon Sep 17 00:00:00 2001 From: me Date: Sun, 2 Aug 2026 21:56:50 -0700 Subject: [PATCH 11/11] 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 | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/helpers/radiolib/RadioLibWrappers.cpp b/src/helpers/radiolib/RadioLibWrappers.cpp index 760e5cf970..e33cf1ec41 100644 --- a/src/helpers/radiolib/RadioLibWrappers.cpp +++ b/src/helpers/radiolib/RadioLibWrappers.cpp @@ -218,8 +218,8 @@ bool RadioLibWrapper::isChannelActive() { while (millis() - sense_start < 5) { if (getCurrentRSSI() > -80.0f) { _busy_count++; - uint32_t base_ms = 2000; - uint32_t max_backoff = min(base_ms * (1u << _busy_count), (uint32_t)16000); + 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; @@ -301,4 +301,4 @@ PacketMillis RadioLibWrapper::calcMaxPacketMillis(uint8_t sf, float bw, uint8_t if (cr >= 5 && cr < 8) { payload_us = (payload_us * 8) / cr; } return PacketMillis {(preamble_us + 999) / 1000, (payload_us + 999) / 1000}; -} \ No newline at end of file +}