From a9edacdd79b406c8b74b1015e82ca80f2084237d Mon Sep 17 00:00:00 2001 From: Wessel Nieboer Date: Wed, 18 Feb 2026 01:16:52 +0100 Subject: [PATCH 01/14] Use hardware channel activity detection for checking interference --- examples/companion_radio/MyMesh.cpp | 2 +- examples/simple_repeater/MyMesh.cpp | 2 +- examples/simple_room_server/MyMesh.cpp | 2 +- examples/simple_sensor/SensorMesh.cpp | 2 +- src/helpers/radiolib/RadioLibWrappers.cpp | 33 ++++++++++++++++++++--- src/helpers/radiolib/RadioLibWrappers.h | 2 +- 6 files changed, 35 insertions(+), 8 deletions(-) diff --git a/examples/companion_radio/MyMesh.cpp b/examples/companion_radio/MyMesh.cpp index e8c1914bad..7d621d53b4 100644 --- a/examples/companion_radio/MyMesh.cpp +++ b/examples/companion_radio/MyMesh.cpp @@ -258,7 +258,7 @@ float MyMesh::getAirtimeBudgetFactor() const { } int MyMesh::getInterferenceThreshold() const { - return 0; // disabled for now, until currentRSSI() problem is resolved + return 1; // non-zero enables hardware CAD (Channel Activity Detection) before TX } int MyMesh::calcRxDelay(float score, uint32_t air_time) const { diff --git a/examples/simple_repeater/MyMesh.cpp b/examples/simple_repeater/MyMesh.cpp index 666f79fc5c..13dd916892 100644 --- a/examples/simple_repeater/MyMesh.cpp +++ b/examples/simple_repeater/MyMesh.cpp @@ -886,7 +886,7 @@ MyMesh::MyMesh(mesh::MainBoard &board, mesh::Radio &radio, mesh::MillisecondCloc _prefs.advert_interval = 1; // default to 2 minutes for NEW installs _prefs.flood_advert_interval = 12; // 12 hours _prefs.flood_max = 64; - _prefs.interference_threshold = 0; // disabled + _prefs.interference_threshold = 1; // non-zero enables hardware CAD before TX // bridge defaults _prefs.bridge_enabled = 1; // enabled diff --git a/examples/simple_room_server/MyMesh.cpp b/examples/simple_room_server/MyMesh.cpp index 145fb0fd9f..78488bf569 100644 --- a/examples/simple_room_server/MyMesh.cpp +++ b/examples/simple_room_server/MyMesh.cpp @@ -643,7 +643,7 @@ MyMesh::MyMesh(mesh::MainBoard &board, mesh::Radio &radio, mesh::MillisecondCloc _prefs.advert_interval = 1; // default to 2 minutes for NEW installs _prefs.flood_advert_interval = 12; // 12 hours _prefs.flood_max = 64; - _prefs.interference_threshold = 0; // disabled + _prefs.interference_threshold = 1; // non-zero enables hardware CAD before TX #ifdef ROOM_PASSWORD StrHelper::strncpy(_prefs.guest_password, ROOM_PASSWORD, sizeof(_prefs.guest_password)); #endif diff --git a/examples/simple_sensor/SensorMesh.cpp b/examples/simple_sensor/SensorMesh.cpp index b8fe1e579c..05b2952657 100644 --- a/examples/simple_sensor/SensorMesh.cpp +++ b/examples/simple_sensor/SensorMesh.cpp @@ -725,7 +725,7 @@ SensorMesh::SensorMesh(mesh::MainBoard& board, mesh::Radio& radio, mesh::Millise _prefs.flood_advert_interval = 0; // disabled _prefs.disable_fwd = true; _prefs.flood_max = 64; - _prefs.interference_threshold = 0; // disabled + _prefs.interference_threshold = 1; // non-zero enables hardware CAD before TX // GPS defaults _prefs.gps_enabled = 0; diff --git a/src/helpers/radiolib/RadioLibWrappers.cpp b/src/helpers/radiolib/RadioLibWrappers.cpp index 85f3a935db..fbac7076f3 100644 --- a/src/helpers/radiolib/RadioLibWrappers.cpp +++ b/src/helpers/radiolib/RadioLibWrappers.cpp @@ -170,10 +170,37 @@ void RadioLibWrapper::onSendFinished() { state = STATE_IDLE; } +int16_t RadioLibWrapper::performChannelScan() { + return _radio->scanChannel(); +} + bool RadioLibWrapper::isChannelActive() { - return _threshold == 0 - ? false // interference check is disabled - : getCurrentRSSI() > _noise_floor + _threshold; + if (_threshold == 0) return false; // interference check is disabled + + int16_t result = performChannelScan(); + // scanChannel() triggers DIO interrupt (CAD done) which sets STATE_INT_READY + // via setFlag() ISR. Clear it before restarting RX so recvRaw() doesn't + // try to read a non-existent packet and count a spurious recv error. + state = STATE_IDLE; + startRecv(); + + if (result != RADIOLIB_CHANNEL_FREE) { + // Random backoff to desynchronize retries between competing nodes + uint32_t backoff_until = millis() + random(8000, 22000); + while (millis() < backoff_until) { + vTaskDelay(1); // yield CPU to FreeRTOS tasks including BLE + } + return true; + } + + // Small jitter even when channel is free to prevent simultaneous TX + // from two nodes that both detect a free channel at the same time + uint32_t jitter_until = millis() + random(0, 500); + while (millis() < jitter_until) { + vTaskDelay(1); // yield CPU to FreeRTOS tasks including BLE + } + + return false; } float RadioLibWrapper::getLastRSSI() const { diff --git a/src/helpers/radiolib/RadioLibWrappers.h b/src/helpers/radiolib/RadioLibWrappers.h index ff951aaa47..ad85315b11 100644 --- a/src/helpers/radiolib/RadioLibWrappers.h +++ b/src/helpers/radiolib/RadioLibWrappers.h @@ -32,7 +32,7 @@ class RadioLibWrapper : public mesh::Radio { bool isInRecvMode() const override; bool isChannelActive(); - bool isReceiving() override { + bool isReceiving() override { if (isReceivingPacket()) return true; return isChannelActive(); From 6598f9116eea540b3ea8cedf77413f00179a8c68 Mon Sep 17 00:00:00 2001 From: jirogit Date: Tue, 24 Mar 2026 22:06:14 -0700 Subject: [PATCH 02/14] Add JP_STRICT mode: enforce MAX_TEXT_LEN for ARIB STD-T108 compliance JP_STRICT limits MAX_TEXT_LEN to 1*CIPHER_BLOCK_SIZE (16 bytes) to keep TX time under 4 seconds on SF12/BW125/CR4-8 (Japan LoRa settings). SF12/BW125/CR4-8 airtime: 60 bytes total packet = ~3809ms (within 4s limit) packet overhead ~44 bytes, leaving 16 bytes for text payload Enabled only for WioTrackerL1 and RAK_WisMesh_Tag builds. --- src/helpers/BaseChatMesh.h | 9 ++++++++- variants/rak_wismesh_tag/platformio.ini | 1 + variants/wio-tracker-l1/platformio.ini | 1 + 3 files changed, 10 insertions(+), 1 deletion(-) diff --git a/src/helpers/BaseChatMesh.h b/src/helpers/BaseChatMesh.h index b39e736388..9382fd7cff 100644 --- a/src/helpers/BaseChatMesh.h +++ b/src/helpers/BaseChatMesh.h @@ -5,7 +5,14 @@ #include #include -#define MAX_TEXT_LEN (10*CIPHER_BLOCK_SIZE) // must be LESS than (MAX_PACKET_PAYLOAD - 4 - CIPHER_MAC_SIZE - 1) +// JP_STRICT: limit MAX_TEXT_LEN to keep TX time under 4s (ARIB STD-T108) +// SF12/BW125/CR4-8: 60 bytes total packet = ~3809ms, overhead ~44 bytes +// leaving 16 bytes (1*CIPHER_BLOCK_SIZE) for text payload +#ifdef JP_STRICT + #define MAX_TEXT_LEN (1*CIPHER_BLOCK_SIZE) // ~16 chars, TX <= 4s for JP +#else + #define MAX_TEXT_LEN (10*CIPHER_BLOCK_SIZE) // must be LESS than (MAX_PACKET_PAYLOAD - 4 - CIPHER_MAC_SIZE - 1) +#endif #include "ContactInfo.h" diff --git a/variants/rak_wismesh_tag/platformio.ini b/variants/rak_wismesh_tag/platformio.ini index 29124daf49..720baf847a 100644 --- a/variants/rak_wismesh_tag/platformio.ini +++ b/variants/rak_wismesh_tag/platformio.ini @@ -25,6 +25,7 @@ build_flags = ${nrf52_base.build_flags} -D PIN_BUZZER=21 -D PIN_BOARD_SDA=PIN_WIRE_SDA -D PIN_BOARD_SCL=PIN_WIRE_SCL + -D JP_STRICT build_src_filter = ${nrf52_base.build_src_filter} +<../variants/rak_wismesh_tag> + diff --git a/variants/wio-tracker-l1/platformio.ini b/variants/wio-tracker-l1/platformio.ini index 7bb175bb9a..65e0a59b50 100644 --- a/variants/wio-tracker-l1/platformio.ini +++ b/variants/wio-tracker-l1/platformio.ini @@ -16,6 +16,7 @@ build_flags = ${nrf52_base.build_flags} -D SX126X_RX_BOOSTED_GAIN=1 -D PIN_OLED_RESET=-1 -D GPS_BAUD_RATE=9600 + -D JP_STRICT build_src_filter = ${nrf52_base.build_src_filter} + +<../variants/wio-tracker-l1> From 5081126008f9de48f1fb6bca5c89b872a78996f1 Mon Sep 17 00:00:00 2001 From: jirogit Date: Tue, 24 Mar 2026 23:11:29 -0700 Subject: [PATCH 03/14] Add JP_STRICT 5ms continuous RSSI sensing before TX (ARIB STD-T108) Under JP_STRICT mode, add energy-based carrier sensing loop before CAD: - Sample RSSI continuously for >= 5ms before each TX attempt - If RSSI exceeds threshold at any point, trigger random backoff - ARIB STD-T108 requires energy-based sensing; LoRa CAD alone is insufficient as it only detects LoRa preambles This satisfies the minimum 5ms continuous sensing requirement for the 920.6-922.2 MHz zone (specified low power radio, LBT mode). Test results (JP LoRa SF12/BW125/CR4-8, simultaneous DM): - 16-char: 2/2 success, delivered within 1:03-1:50 - 1-char: 3/4 success, delivered within 0:46-2:11 (shorter airtime reduces RSSI detection window) --- src/helpers/radiolib/RadioLibWrappers.cpp | 21 +++++++++++++++++++++ 1 file changed, 21 insertions(+) diff --git a/src/helpers/radiolib/RadioLibWrappers.cpp b/src/helpers/radiolib/RadioLibWrappers.cpp index fbac7076f3..883047b6d5 100644 --- a/src/helpers/radiolib/RadioLibWrappers.cpp +++ b/src/helpers/radiolib/RadioLibWrappers.cpp @@ -144,6 +144,7 @@ uint32_t RadioLibWrapper::getEstAirtimeFor(int len_bytes) { bool RadioLibWrapper::startSendRaw(const uint8_t* bytes, int len) { _board->onBeforeTransmit(); + _tx_start_ms = millis(); // recording TX time int err = _radio->startTransmit((uint8_t *) bytes, len); if (err == RADIOLIB_ERR_NONE) { state = STATE_TX_WAIT; @@ -159,6 +160,8 @@ bool RadioLibWrapper::isSendComplete() { if (state & STATE_INT_READY) { state = STATE_IDLE; n_sent++; + uint32_t tx_duration = millis() - _tx_start_ms; + MESH_DEBUG_PRINTLN("TX duration: %lu ms (len=%d)", tx_duration); return true; } return false; @@ -177,6 +180,24 @@ int16_t RadioLibWrapper::performChannelScan() { bool RadioLibWrapper::isChannelActive() { if (_threshold == 0) return false; // interference check is disabled +#ifdef JP_STRICT + // ARIB STD-T108 compliant LBT: continuous RSSI sensing for >= 5ms + // Energy-based sensing required; LoRa CAD alone is not sufficient + uint32_t sense_start = millis(); + uint32_t sense_duration_ms = 5; + while (millis() - sense_start < sense_duration_ms) { + if (getCurrentRSSI() > _noise_floor + _threshold) { + // Channel busy detected during 5ms sensing window + uint32_t backoff_until = millis() + random(8000, 22000); + while (millis() < backoff_until) { + vTaskDelay(1); // yield CPU to FreeRTOS tasks including BLE + } + return true; + } + vTaskDelay(1); // yield CPU between RSSI samples + } +#endif + int16_t result = performChannelScan(); // scanChannel() triggers DIO interrupt (CAD done) which sets STATE_INT_READY // via setFlag() ISR. Clear it before restarting RX so recvRaw() doesn't From d4644c167693033321684bd73a3da6052a2d78f9 Mon Sep 17 00:00:00 2001 From: jirogit Date: Wed, 25 Mar 2026 00:41:21 -0700 Subject: [PATCH 04/14] JP_STRICT: use absolute -80dBm RSSI threshold for LBT (ARIB STD-T108) Replace relative threshold (_noise_floor + _threshold) with absolute -80dBm as specified by ARIB STD-T108 for carrier sense detection. Previous relative threshold (~-99dBm) caused false busy detection from environmental noise. -80dBm matches the legal requirement and reduces spurious backoff. --- src/helpers/radiolib/RadioLibWrappers.cpp | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/src/helpers/radiolib/RadioLibWrappers.cpp b/src/helpers/radiolib/RadioLibWrappers.cpp index 883047b6d5..68b1be5ccf 100644 --- a/src/helpers/radiolib/RadioLibWrappers.cpp +++ b/src/helpers/radiolib/RadioLibWrappers.cpp @@ -170,6 +170,10 @@ bool RadioLibWrapper::isSendComplete() { void RadioLibWrapper::onSendFinished() { _radio->finishTransmit(); _board->onAfterTransmit(); +#ifdef JP_STRICT + // ARIB STD-T108: wait >= 50ms after TX before next transmission + delay(50); +#endif state = STATE_IDLE; } @@ -186,7 +190,7 @@ bool RadioLibWrapper::isChannelActive() { uint32_t sense_start = millis(); uint32_t sense_duration_ms = 5; while (millis() - sense_start < sense_duration_ms) { - if (getCurrentRSSI() > _noise_floor + _threshold) { + if (getCurrentRSSI() > -80.0f) { // Channel busy detected during 5ms sensing window uint32_t backoff_until = millis() + random(8000, 22000); while (millis() < backoff_until) { From 96f435dad076e32cd50b2cba9ebc318957ebcc89 Mon Sep 17 00:00:00 2001 From: jirogit Date: Wed, 25 Mar 2026 19:39:46 -0700 Subject: [PATCH 05/14] JP_STRICT: adjust MAX_TEXT_LEN based on LORA_CR setting MAX_TEXT_LEN is automatically selected based on the LORA_CR build flag defined in platformio.ini (e.g. -D LORA_CR=5). CR4/5: 48 bytes (~16 JP chars, TX ~3808ms) CR4/6: 32 bytes (~10 JP chars) CR4/7: 24 bytes (~8 JP chars) CR4/8: 16 bytes (~5 JP chars, default) To enable CR4/5 for Japan, add -D LORA_CR=5 to your board's build_flags in platformio.ini. Set LORA_CR=5 for WioTrackerL1 and RAK WisMesh Tag. --- src/helpers/BaseChatMesh.h | 20 ++++++++++++++++---- variants/rak_wismesh_tag/platformio.ini | 1 + variants/wio-tracker-l1/platformio.ini | 1 + 3 files changed, 18 insertions(+), 4 deletions(-) diff --git a/src/helpers/BaseChatMesh.h b/src/helpers/BaseChatMesh.h index 9382fd7cff..65a5e558d1 100644 --- a/src/helpers/BaseChatMesh.h +++ b/src/helpers/BaseChatMesh.h @@ -6,12 +6,24 @@ #include // JP_STRICT: limit MAX_TEXT_LEN to keep TX time under 4s (ARIB STD-T108) -// SF12/BW125/CR4-8: 60 bytes total packet = ~3809ms, overhead ~44 bytes -// leaving 16 bytes (1*CIPHER_BLOCK_SIZE) for text payload +// SF12/BW125, packet overhead ~44 bytes +// CR4/5: 100 bytes total = ~3808ms → 56 bytes text → ~18 JP chars +// CR4/6: 80 bytes total = ~3530ms → 36 bytes text → ~12 JP chars +// CR4/7: 70 bytes total = ~3530ms → 26 bytes text → ~8 JP chars +// CR4/8: 60 bytes total = ~3809ms → 16 bytes text → ~5 JP chars #ifdef JP_STRICT - #define MAX_TEXT_LEN (1*CIPHER_BLOCK_SIZE) // ~16 chars, TX <= 4s for JP + #if defined(LORA_CR) && (LORA_CR == 5) + #define MAX_TEXT_LEN (3*CIPHER_BLOCK_SIZE) // 48 bytes ~18 JP chars + #elif defined(LORA_CR) && (LORA_CR == 6) + #define MAX_TEXT_LEN (2*CIPHER_BLOCK_SIZE) // 32 bytes ~10 JP chars + #elif defined(LORA_CR) && (LORA_CR == 7) + #define MAX_TEXT_LEN (1*CIPHER_BLOCK_SIZE+8) // 24 bytes ~8 JP chars + #else + #define MAX_TEXT_LEN (1*CIPHER_BLOCK_SIZE) // 16 bytes ~5 JP chars + #endif #else - #define MAX_TEXT_LEN (10*CIPHER_BLOCK_SIZE) // must be LESS than (MAX_PACKET_PAYLOAD - 4 - CIPHER_MAC_SIZE - 1) + #define MAX_TEXT_LEN (10*CIPHER_BLOCK_SIZE) +// must be LESS than (MAX_PACKET_PAYLOAD - 4 - CIPHER_MAC_SIZE - 1) #endif #include "ContactInfo.h" diff --git a/variants/rak_wismesh_tag/platformio.ini b/variants/rak_wismesh_tag/platformio.ini index 720baf847a..5f45343fe6 100644 --- a/variants/rak_wismesh_tag/platformio.ini +++ b/variants/rak_wismesh_tag/platformio.ini @@ -26,6 +26,7 @@ build_flags = ${nrf52_base.build_flags} -D PIN_BOARD_SDA=PIN_WIRE_SDA -D PIN_BOARD_SCL=PIN_WIRE_SCL -D JP_STRICT + -D LORA_CR=5 build_src_filter = ${nrf52_base.build_src_filter} +<../variants/rak_wismesh_tag> + diff --git a/variants/wio-tracker-l1/platformio.ini b/variants/wio-tracker-l1/platformio.ini index 65e0a59b50..8365ba5a15 100644 --- a/variants/wio-tracker-l1/platformio.ini +++ b/variants/wio-tracker-l1/platformio.ini @@ -17,6 +17,7 @@ build_flags = ${nrf52_base.build_flags} -D PIN_OLED_RESET=-1 -D GPS_BAUD_RATE=9600 -D JP_STRICT + -D LORA_CR=5 build_src_filter = ${nrf52_base.build_src_filter} + +<../variants/wio-tracker-l1> From 3158dea001a55a4a69c5ee9e44f330fcb44f16f1 Mon Sep 17 00:00:00 2001 From: jirogit Date: Thu, 26 Mar 2026 00:29:36 -0700 Subject: [PATCH 06/14] JP_STRICT: dynamic MAX_TEXT_LEN based on runtime CR value Instead of compile-time LORA_CR define, MAX_TEXT_LEN is now determined at runtime by reading the actual coding rate from the radio hardware. Added getMaxTextLen() to RadioLibWrapper and Dispatcher: - CR4/5: 48 bytes (~16 JP chars, TX ~3808ms) - CR4/6: 32 bytes (~10 JP chars) - CR4/7: 24 bytes (~8 JP chars) - CR4/8: 16 bytes (~5 JP chars, default) getCodingRate() added to CustomSX1262Wrapper to read codingRate from RadioLib PhysicalLayer at runtime. Tested: 48-byte limit with LORA_CR=5, 16-byte limit with LORA_CR=8. --- src/Dispatcher.h | 4 ++++ src/helpers/BaseChatMesh.cpp | 7 +++++++ src/helpers/BaseChatMesh.h | 20 +------------------- src/helpers/radiolib/CustomSX1262Wrapper.h | 4 ++++ src/helpers/radiolib/RadioLibWrappers.h | 15 ++++++++++++++- 5 files changed, 30 insertions(+), 20 deletions(-) diff --git a/src/Dispatcher.h b/src/Dispatcher.h index 2a99d0682b..7d4e2abf29 100644 --- a/src/Dispatcher.h +++ b/src/Dispatcher.h @@ -38,6 +38,10 @@ class Radio { virtual float packetScore(float snr, int packet_len) = 0; +#ifdef JP_STRICT + virtual int getMaxTextLen() const { return 1 * 16; } // default: CR4/8 +#endif + /** * \brief starts the raw packet send. (no wait) * \param bytes the raw packet data diff --git a/src/helpers/BaseChatMesh.cpp b/src/helpers/BaseChatMesh.cpp index 7ddc461d29..d414c7b73f 100644 --- a/src/helpers/BaseChatMesh.cpp +++ b/src/helpers/BaseChatMesh.cpp @@ -395,8 +395,15 @@ 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); + +#ifdef JP_STRICT + int max_len = _radio->getMaxTextLen(); + if (text_len > max_len) return NULL; + if (attempt > 3 && text_len > max_len - 2) return NULL; +#else if (text_len > MAX_TEXT_LEN) return NULL; if (attempt > 3 && text_len > MAX_TEXT_LEN-2) return NULL; +#endif uint8_t temp[5+MAX_TEXT_LEN+1]; memcpy(temp, ×tamp, 4); // mostly an extra blob to help make packet_hash unique diff --git a/src/helpers/BaseChatMesh.h b/src/helpers/BaseChatMesh.h index 65a5e558d1..a778323f72 100644 --- a/src/helpers/BaseChatMesh.h +++ b/src/helpers/BaseChatMesh.h @@ -5,26 +5,8 @@ #include #include -// JP_STRICT: limit MAX_TEXT_LEN to keep TX time under 4s (ARIB STD-T108) -// SF12/BW125, packet overhead ~44 bytes -// CR4/5: 100 bytes total = ~3808ms → 56 bytes text → ~18 JP chars -// CR4/6: 80 bytes total = ~3530ms → 36 bytes text → ~12 JP chars -// CR4/7: 70 bytes total = ~3530ms → 26 bytes text → ~8 JP chars -// CR4/8: 60 bytes total = ~3809ms → 16 bytes text → ~5 JP chars -#ifdef JP_STRICT - #if defined(LORA_CR) && (LORA_CR == 5) - #define MAX_TEXT_LEN (3*CIPHER_BLOCK_SIZE) // 48 bytes ~18 JP chars - #elif defined(LORA_CR) && (LORA_CR == 6) - #define MAX_TEXT_LEN (2*CIPHER_BLOCK_SIZE) // 32 bytes ~10 JP chars - #elif defined(LORA_CR) && (LORA_CR == 7) - #define MAX_TEXT_LEN (1*CIPHER_BLOCK_SIZE+8) // 24 bytes ~8 JP chars - #else - #define MAX_TEXT_LEN (1*CIPHER_BLOCK_SIZE) // 16 bytes ~5 JP chars - #endif -#else - #define MAX_TEXT_LEN (10*CIPHER_BLOCK_SIZE) +#define MAX_TEXT_LEN (10*CIPHER_BLOCK_SIZE) // must be LESS than (MAX_PACKET_PAYLOAD - 4 - CIPHER_MAC_SIZE - 1) -#endif #include "ContactInfo.h" diff --git a/src/helpers/radiolib/CustomSX1262Wrapper.h b/src/helpers/radiolib/CustomSX1262Wrapper.h index e13561a4a9..3fcd95d44a 100644 --- a/src/helpers/radiolib/CustomSX1262Wrapper.h +++ b/src/helpers/radiolib/CustomSX1262Wrapper.h @@ -37,4 +37,8 @@ class CustomSX1262Wrapper : public RadioLibWrapper { bool getRxBoostedGainMode() const override { return ((CustomSX1262 *)_radio)->getRxBoostedGainMode(); } + + uint8_t getCodingRate() const override { + return ((CustomSX1262 *)_radio)->codingRate + 4; // RadioLib stores 1-4, return 5-8 + } }; diff --git a/src/helpers/radiolib/RadioLibWrappers.h b/src/helpers/radiolib/RadioLibWrappers.h index ad85315b11..17140a0c4b 100644 --- a/src/helpers/radiolib/RadioLibWrappers.h +++ b/src/helpers/radiolib/RadioLibWrappers.h @@ -7,7 +7,7 @@ class RadioLibWrapper : public mesh::Radio { protected: PhysicalLayer* _radio; mesh::MainBoard* _board; - uint32_t n_recv, n_sent, n_recv_errors; + uint32_t n_recv, n_sent, n_recv_errors, _tx_start_ms; int16_t _noise_floor, _threshold; uint16_t _num_floor_samples; int32_t _floor_sample_sum; @@ -42,7 +42,20 @@ class RadioLibWrapper : public mesh::Radio { virtual uint8_t getSpreadingFactor() const { return LORA_SF; } static uint16_t preambleLengthForSF(uint8_t sf) { return sf <= 8 ? 32 : 16; } void updatePreamble(uint8_t sf) { _preamble_sf = sf; _radio->setPreambleLength(preambleLengthForSF(sf)); } + virtual uint8_t getCodingRate() const = 0; + +#ifdef JP_STRICT + int getMaxTextLen() const { + uint8_t cr = getCodingRate(); + if (cr <= 5) return 3 * 16; // 48 bytes ~16 JP chars + if (cr == 6) return 2 * 16; // 32 bytes ~10 JP chars + if (cr == 7) return 1 * 16 + 8; // 24 bytes ~8 JP chars + return 1 * 16; // 16 bytes ~5 JP chars + } +#endif + virtual int16_t performChannelScan(); + int getNoiseFloor() const override { return _noise_floor; } void triggerNoiseFloorCalibrate(int threshold) override; void resetAGC() override; From 48080d27e269f485830086c715df0a52c102f3ad Mon Sep 17 00:00:00 2001 From: jirogit Date: Thu, 26 Mar 2026 12:29:35 -0700 Subject: [PATCH 07/14] Add exponential backoff for channel busy detection Replace fixed random(8000-22000ms) backoff with exponential backoff: - 1st busy: 3-6s - 2nd busy: 6-12s - 3rd+ busy: 12-20s (capped) - Reset counter on channel free Results (48-byte simultaneous DM, JP SF12/BW125/CR4-5): - 3/3 success, delivered within 0:23-0:45 - Previous fixed backoff: 1:03-3:55 --- src/helpers/radiolib/RadioLibWrappers.cpp | 33 +++++++++++++---------- src/helpers/radiolib/RadioLibWrappers.h | 1 + 2 files changed, 20 insertions(+), 14 deletions(-) diff --git a/src/helpers/radiolib/RadioLibWrappers.cpp b/src/helpers/radiolib/RadioLibWrappers.cpp index 68b1be5ccf..861d1e4dd4 100644 --- a/src/helpers/radiolib/RadioLibWrappers.cpp +++ b/src/helpers/radiolib/RadioLibWrappers.cpp @@ -36,6 +36,7 @@ void RadioLibWrapper::begin() { _noise_floor = 0; _threshold = 0; + _busy_count = 0; // initialize exponential backoff counter // start average out some samples _num_floor_samples = 0; @@ -185,39 +186,43 @@ bool RadioLibWrapper::isChannelActive() { if (_threshold == 0) return false; // interference check is disabled #ifdef JP_STRICT - // ARIB STD-T108 compliant LBT: continuous RSSI sensing for >= 5ms - // Energy-based sensing required; LoRa CAD alone is not sufficient + // 5ms continuous RSSI sensing uint32_t sense_start = millis(); - uint32_t sense_duration_ms = 5; - while (millis() - sense_start < sense_duration_ms) { + while (millis() - sense_start < 5) { if (getCurrentRSSI() > -80.0f) { - // Channel busy detected during 5ms sensing window - uint32_t backoff_until = millis() + random(8000, 22000); + // RSSI busy: backoff and return without CAD + _busy_count++; + uint32_t base_ms = 3000; + uint32_t max_backoff = min(base_ms * (1u << _busy_count), (uint32_t)20000); + uint32_t backoff_until = millis() + random(max_backoff / 2, max_backoff); while (millis() < backoff_until) { - vTaskDelay(1); // yield CPU to FreeRTOS tasks including BLE + vTaskDelay(1); } return true; } - vTaskDelay(1); // yield CPU between RSSI samples + vTaskDelay(1); } #endif + // CAD int16_t result = performChannelScan(); - // scanChannel() triggers DIO interrupt (CAD done) which sets STATE_INT_READY - // via setFlag() ISR. Clear it before restarting RX so recvRaw() doesn't - // try to read a non-existent packet and count a spurious recv error. state = STATE_IDLE; startRecv(); if (result != RADIOLIB_CHANNEL_FREE) { - // Random backoff to desynchronize retries between competing nodes - uint32_t backoff_until = millis() + random(8000, 22000); + // CAD busy: backoff + _busy_count++; + uint32_t base_ms = 3000; + uint32_t max_backoff = min(base_ms * (1u << _busy_count), (uint32_t)20000); + uint32_t backoff_until = millis() + random(max_backoff / 2, max_backoff); while (millis() < backoff_until) { - vTaskDelay(1); // yield CPU to FreeRTOS tasks including BLE + vTaskDelay(1); } return true; } + _busy_count = 0; + // Small jitter even when channel is free to prevent simultaneous TX // from two nodes that both detect a free channel at the same time uint32_t jitter_until = millis() + random(0, 500); diff --git a/src/helpers/radiolib/RadioLibWrappers.h b/src/helpers/radiolib/RadioLibWrappers.h index 17140a0c4b..900c51dedc 100644 --- a/src/helpers/radiolib/RadioLibWrappers.h +++ b/src/helpers/radiolib/RadioLibWrappers.h @@ -9,6 +9,7 @@ class RadioLibWrapper : public mesh::Radio { mesh::MainBoard* _board; uint32_t n_recv, n_sent, n_recv_errors, _tx_start_ms; int16_t _noise_floor, _threshold; + uint8_t _busy_count; // consecutive busy detections for exponential backoff uint16_t _num_floor_samples; int32_t _floor_sample_sum; uint8_t _preamble_sf; From 5295ca74360802c7a9c9bc647502f4e2aa7698f5 Mon Sep 17 00:00:00 2001 From: jirogit Date: Thu, 26 Mar 2026 20:21:37 -0700 Subject: [PATCH 08/14] Fix missing newline at end of file in CustomSX1262.h --- src/helpers/radiolib/CustomSX1262.h | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/helpers/radiolib/CustomSX1262.h b/src/helpers/radiolib/CustomSX1262.h index ad20122902..ab8f2dc942 100644 --- a/src/helpers/radiolib/CustomSX1262.h +++ b/src/helpers/radiolib/CustomSX1262.h @@ -97,4 +97,4 @@ class CustomSX1262 : public SX1262 { readRegister(RADIOLIB_SX126X_REG_RX_GAIN, &rxGain, 1); return (rxGain == RADIOLIB_SX126X_RX_GAIN_BOOSTED); } -}; \ No newline at end of file +}; From 3c0ed55f36524ff5dd380dac433debfd0c735bb8 Mon Sep 17 00:00:00 2001 From: jirogit Date: Thu, 26 Mar 2026 22:51:01 -0700 Subject: [PATCH 09/14] Add getCodingRate() to CustomLR1110 and CustomLR1110Wrapper LR1110 has codingRate as protected field in LR_common.h, so getCodingRate() is implemented in CustomLR1110 class directly, and exposed via CustomLR1110Wrapper override. This enables dynamic MAX_TEXT_LEN calculation for T1000-E under JP_STRICT mode. --- src/helpers/radiolib/CustomLR1110.h | 6 ++- src/helpers/radiolib/CustomLR1110Wrapper.h | 4 ++ src/helpers/radiolib/RadioLibWrappers.cpp | 44 ++++++++-------------- 3 files changed, 24 insertions(+), 30 deletions(-) diff --git a/src/helpers/radiolib/CustomLR1110.h b/src/helpers/radiolib/CustomLR1110.h index 4061c6b1a6..7494ccb476 100644 --- a/src/helpers/radiolib/CustomLR1110.h +++ b/src/helpers/radiolib/CustomLR1110.h @@ -38,4 +38,8 @@ class CustomLR1110 : public LR1110 { } uint8_t getSpreadingFactor() const { return spreadingFactor; } -}; \ No newline at end of file +}; + 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 0445262b57..595b03b53f 100644 --- a/src/helpers/radiolib/CustomLR1110Wrapper.h +++ b/src/helpers/radiolib/CustomLR1110Wrapper.h @@ -33,4 +33,8 @@ class CustomLR1110Wrapper : public RadioLibWrapper { bool getRxBoostedGainMode() const override { return ((CustomLR1110 *)_radio)->getRxBoostedGainMode(); } + + uint8_t getCodingRate() const override { + return ((CustomLR1110 *)_radio)->getCodingRate(); + } }; diff --git a/src/helpers/radiolib/RadioLibWrappers.cpp b/src/helpers/radiolib/RadioLibWrappers.cpp index 861d1e4dd4..d22bea3c9d 100644 --- a/src/helpers/radiolib/RadioLibWrappers.cpp +++ b/src/helpers/radiolib/RadioLibWrappers.cpp @@ -186,53 +186,39 @@ bool RadioLibWrapper::isChannelActive() { if (_threshold == 0) return false; // interference check is disabled #ifdef JP_STRICT - // 5ms continuous RSSI sensing + // ARIB STD-T108 compliant LBT: continuous RSSI sensing for >= 5ms + // Energy-based sensing required; LoRa CAD not used here — + // CAD detects only LoRa preambles and is not required by ARIB STD-T108 uint32_t sense_start = millis(); while (millis() - sense_start < 5) { if (getCurrentRSSI() > -80.0f) { - // RSSI busy: backoff and return without CAD + // Channel busy: exponential backoff (tuned for JP 4s airtime) _busy_count++; - uint32_t base_ms = 3000; - uint32_t max_backoff = min(base_ms * (1u << _busy_count), (uint32_t)20000); + 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) { - vTaskDelay(1); + vTaskDelay(1); // yield CPU to FreeRTOS tasks including BLE } return true; } - vTaskDelay(1); + vTaskDelay(1); // yield CPU between RSSI samples } -#endif - - // CAD - int16_t result = performChannelScan(); - state = STATE_IDLE; - startRecv(); - - if (result != RADIOLIB_CHANNEL_FREE) { - // CAD busy: backoff - _busy_count++; - uint32_t base_ms = 3000; - uint32_t max_backoff = min(base_ms * (1u << _busy_count), (uint32_t)20000); - uint32_t backoff_until = millis() + random(max_backoff / 2, max_backoff); - while (millis() < backoff_until) { - vTaskDelay(1); - } - return true; - } - + // Channel free: reset busy counter and add small jitter _busy_count = 0; - - // Small jitter even when channel is free to prevent simultaneous TX - // from two nodes that both detect a free channel at the same time uint32_t jitter_until = millis() + random(0, 500); while (millis() < jitter_until) { vTaskDelay(1); // yield CPU to FreeRTOS tasks including BLE } - return false; + +#else + // Non-JP: original behavior (RSSI threshold only) + return getCurrentRSSI() > _noise_floor + _threshold; +#endif } + float RadioLibWrapper::getLastRSSI() const { return _radio->getRSSI(); } From 4867d36f1dcb52054e3dfbcb318f73ad0be81c4e Mon Sep 17 00:00:00 2001 From: jirogit Date: Fri, 27 Mar 2026 23:42:37 -0700 Subject: [PATCH 10/14] Replace JP_STRICT build flag with runtime frequency detection JP LBT mode now activates automatically based on operating frequency: - CH25: 920.800MHz - CH26: 921.000MHz - CH27: 921.200MHz (ARIB STD-T108, 200kHz grid) Changes: - Add isJapanMode() to RadioLibWrapper using getFreqMHz() - Add getFreqMHz() to CustomSX1262Wrapper and CustomLR1110Wrapper - Remove #ifdef JP_STRICT throughout, replaced by isJapanMode() - Remove -D JP_STRICT build flag from platformio.ini - MAX_TEXT_LEN dynamically determined by CR at runtime via getMaxTextLen() No build flags required: JP compliance activates automatically when device is configured to Japan 3 frequencies. --- src/Dispatcher.h | 4 +- src/helpers/BaseChatMesh.cpp | 7 --- src/helpers/radiolib/CustomLR1110Wrapper.h | 3 ++ src/helpers/radiolib/CustomSX1262Wrapper.h | 3 ++ src/helpers/radiolib/RadioLibWrappers.cpp | 59 +++++++++++----------- src/helpers/radiolib/RadioLibWrappers.h | 17 +++++-- variants/rak_wismesh_tag/platformio.ini | 2 - variants/wio-tracker-l1/platformio.ini | 2 - 8 files changed, 48 insertions(+), 49 deletions(-) diff --git a/src/Dispatcher.h b/src/Dispatcher.h index 7d4e2abf29..0abfa9ab79 100644 --- a/src/Dispatcher.h +++ b/src/Dispatcher.h @@ -38,9 +38,7 @@ class Radio { virtual float packetScore(float snr, int packet_len) = 0; -#ifdef JP_STRICT - virtual int getMaxTextLen() const { return 1 * 16; } // default: CR4/8 -#endif + virtual int getMaxTextLen() const { return 10 * 16; } // default: non-JP /** * \brief starts the raw packet send. (no wait) diff --git a/src/helpers/BaseChatMesh.cpp b/src/helpers/BaseChatMesh.cpp index d414c7b73f..2003db4468 100644 --- a/src/helpers/BaseChatMesh.cpp +++ b/src/helpers/BaseChatMesh.cpp @@ -395,16 +395,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); - -#ifdef JP_STRICT int max_len = _radio->getMaxTextLen(); if (text_len > max_len) return NULL; if (attempt > 3 && text_len > max_len - 2) return NULL; -#else - if (text_len > MAX_TEXT_LEN) return NULL; - if (attempt > 3 && text_len > MAX_TEXT_LEN-2) return NULL; -#endif - uint8_t temp[5+MAX_TEXT_LEN+1]; memcpy(temp, ×tamp, 4); // mostly an extra blob to help make packet_hash unique temp[4] = (attempt & 3); diff --git a/src/helpers/radiolib/CustomLR1110Wrapper.h b/src/helpers/radiolib/CustomLR1110Wrapper.h index 595b03b53f..a4ed3dbf68 100644 --- a/src/helpers/radiolib/CustomLR1110Wrapper.h +++ b/src/helpers/radiolib/CustomLR1110Wrapper.h @@ -37,4 +37,7 @@ class CustomLR1110Wrapper : public RadioLibWrapper { uint8_t getCodingRate() const override { return ((CustomLR1110 *)_radio)->getCodingRate(); } + float getFreqMHz() const override { + return ((CustomLR1110 *)_radio)->getFreqMHz(); + } }; diff --git a/src/helpers/radiolib/CustomSX1262Wrapper.h b/src/helpers/radiolib/CustomSX1262Wrapper.h index 3fcd95d44a..b2d9432c4e 100644 --- a/src/helpers/radiolib/CustomSX1262Wrapper.h +++ b/src/helpers/radiolib/CustomSX1262Wrapper.h @@ -41,4 +41,7 @@ class CustomSX1262Wrapper : public RadioLibWrapper { 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; + } }; diff --git a/src/helpers/radiolib/RadioLibWrappers.cpp b/src/helpers/radiolib/RadioLibWrappers.cpp index d22bea3c9d..1f0990a145 100644 --- a/src/helpers/radiolib/RadioLibWrappers.cpp +++ b/src/helpers/radiolib/RadioLibWrappers.cpp @@ -171,10 +171,10 @@ bool RadioLibWrapper::isSendComplete() { void RadioLibWrapper::onSendFinished() { _radio->finishTransmit(); _board->onAfterTransmit(); -#ifdef JP_STRICT - // ARIB STD-T108: wait >= 50ms after TX before next transmission - delay(50); -#endif + if (isJapanMode()) { + // ARIB STD-T108: wait >= 50ms after TX before next transmission + delay(50); + } state = STATE_IDLE; } @@ -185,40 +185,39 @@ int16_t RadioLibWrapper::performChannelScan() { bool RadioLibWrapper::isChannelActive() { if (_threshold == 0) return false; // interference check is disabled -#ifdef JP_STRICT - // ARIB STD-T108 compliant LBT: continuous RSSI sensing for >= 5ms - // Energy-based sensing required; LoRa CAD not used here — - // CAD detects only LoRa preambles and is not required by ARIB STD-T108 - uint32_t sense_start = millis(); - while (millis() - sense_start < 5) { - if (getCurrentRSSI() > -80.0f) { - // Channel busy: exponential backoff (tuned for JP 4s airtime) - _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) { - vTaskDelay(1); // yield CPU to FreeRTOS tasks including BLE + // Activate JP_STRICT LBT on Japan 920MHz band 3 channels only + // CH25=920.800MHz, CH26=921.000MHz, CH27=921.200MHz (ARIB STD-T108) + if (isJapanMode()) { + // ARIB STD-T108 compliant LBT: continuous RSSI sensing for >= 5ms + // Energy-based sensing required; LoRa CAD not used + uint32_t sense_start = millis(); + while (millis() - sense_start < 5) { + if (getCurrentRSSI() > -80.0f) { + // Channel busy: exponential backoff (tuned for JP 4s airtime) + _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) { + vTaskDelay(1); + } + return true; } - return true; + vTaskDelay(1); } - vTaskDelay(1); // yield CPU between RSSI samples - } - // Channel free: reset busy counter and add small jitter - _busy_count = 0; - uint32_t jitter_until = millis() + random(0, 500); - while (millis() < jitter_until) { - vTaskDelay(1); // yield CPU to FreeRTOS tasks including BLE + // Channel free: reset busy counter and add small jitter + _busy_count = 0; + uint32_t jitter_until = millis() + random(0, 500); + while (millis() < jitter_until) { + vTaskDelay(1); + } + return false; } - return false; -#else // Non-JP: original behavior (RSSI threshold only) return getCurrentRSSI() > _noise_floor + _threshold; -#endif } - float RadioLibWrapper::getLastRSSI() const { return _radio->getRSSI(); } diff --git a/src/helpers/radiolib/RadioLibWrappers.h b/src/helpers/radiolib/RadioLibWrappers.h index 900c51dedc..d94a6e5dd6 100644 --- a/src/helpers/radiolib/RadioLibWrappers.h +++ b/src/helpers/radiolib/RadioLibWrappers.h @@ -44,16 +44,23 @@ 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)); } virtual uint8_t getCodingRate() const = 0; + virtual float getFreqMHz() const = 0; + + 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); + } -#ifdef JP_STRICT int getMaxTextLen() const { + if (!isJapanMode()) return 10 * 16; // default uint8_t cr = getCodingRate(); - if (cr <= 5) return 3 * 16; // 48 bytes ~16 JP chars - if (cr == 6) return 2 * 16; // 32 bytes ~10 JP chars + if (cr <= 5) return 3 * 16; // 48 bytes ~16 JP chars + if (cr == 6) return 2 * 16; // 32 bytes ~10 JP chars if (cr == 7) return 1 * 16 + 8; // 24 bytes ~8 JP chars - return 1 * 16; // 16 bytes ~5 JP chars + return 1 * 16; // 16 bytes ~5 JP chars } -#endif virtual int16_t performChannelScan(); diff --git a/variants/rak_wismesh_tag/platformio.ini b/variants/rak_wismesh_tag/platformio.ini index 5f45343fe6..29124daf49 100644 --- a/variants/rak_wismesh_tag/platformio.ini +++ b/variants/rak_wismesh_tag/platformio.ini @@ -25,8 +25,6 @@ build_flags = ${nrf52_base.build_flags} -D PIN_BUZZER=21 -D PIN_BOARD_SDA=PIN_WIRE_SDA -D PIN_BOARD_SCL=PIN_WIRE_SCL - -D JP_STRICT - -D LORA_CR=5 build_src_filter = ${nrf52_base.build_src_filter} +<../variants/rak_wismesh_tag> + diff --git a/variants/wio-tracker-l1/platformio.ini b/variants/wio-tracker-l1/platformio.ini index 8365ba5a15..7bb175bb9a 100644 --- a/variants/wio-tracker-l1/platformio.ini +++ b/variants/wio-tracker-l1/platformio.ini @@ -16,8 +16,6 @@ build_flags = ${nrf52_base.build_flags} -D SX126X_RX_BOOSTED_GAIN=1 -D PIN_OLED_RESET=-1 -D GPS_BAUD_RATE=9600 - -D JP_STRICT - -D LORA_CR=5 build_src_filter = ${nrf52_base.build_src_filter} + +<../variants/wio-tracker-l1> From db23aa0bd99fadcee6c4ce178998802ad8c5a890 Mon Sep 17 00:00:00 2001 From: jirogit Date: Sat, 28 Mar 2026 00:02:00 -0700 Subject: [PATCH 11/14] Remove TX duration debug logging _tx_start_ms and TX duration logging were added for 4-second airtime verification and are no longer needed. --- src/helpers/radiolib/RadioLibWrappers.cpp | 3 --- src/helpers/radiolib/RadioLibWrappers.h | 2 +- 2 files changed, 1 insertion(+), 4 deletions(-) diff --git a/src/helpers/radiolib/RadioLibWrappers.cpp b/src/helpers/radiolib/RadioLibWrappers.cpp index 1f0990a145..5605cdbe60 100644 --- a/src/helpers/radiolib/RadioLibWrappers.cpp +++ b/src/helpers/radiolib/RadioLibWrappers.cpp @@ -145,7 +145,6 @@ uint32_t RadioLibWrapper::getEstAirtimeFor(int len_bytes) { bool RadioLibWrapper::startSendRaw(const uint8_t* bytes, int len) { _board->onBeforeTransmit(); - _tx_start_ms = millis(); // recording TX time int err = _radio->startTransmit((uint8_t *) bytes, len); if (err == RADIOLIB_ERR_NONE) { state = STATE_TX_WAIT; @@ -161,8 +160,6 @@ bool RadioLibWrapper::isSendComplete() { if (state & STATE_INT_READY) { state = STATE_IDLE; n_sent++; - uint32_t tx_duration = millis() - _tx_start_ms; - MESH_DEBUG_PRINTLN("TX duration: %lu ms (len=%d)", tx_duration); return true; } return false; diff --git a/src/helpers/radiolib/RadioLibWrappers.h b/src/helpers/radiolib/RadioLibWrappers.h index d94a6e5dd6..ec326a2c87 100644 --- a/src/helpers/radiolib/RadioLibWrappers.h +++ b/src/helpers/radiolib/RadioLibWrappers.h @@ -7,7 +7,7 @@ class RadioLibWrapper : public mesh::Radio { protected: PhysicalLayer* _radio; mesh::MainBoard* _board; - uint32_t n_recv, n_sent, n_recv_errors, _tx_start_ms; + uint32_t n_recv, n_sent, n_recv_errors; int16_t _noise_floor, _threshold; uint8_t _busy_count; // consecutive busy detections for exponential backoff uint16_t _num_floor_samples; From 52f1d69921a81d8329c0973147fb08dd4f69ed21 Mon Sep 17 00:00:00 2001 From: jirogit Date: Sat, 28 Mar 2026 02:50:06 -0700 Subject: [PATCH 12/14] Fix build for non-FreeRTOS and non-SX1262 platforms - Replace vTaskDelay(1) with YIELD_TASK() macro for platform compatibility (FreeRTOS: vTaskDelay, others: delay) - Make getCodingRate() and getFreqMHz() non-pure virtual with defaults (default CR4/8, default freq 0.0f for unknown platforms) - Add getCodingRate() and getFreqMHz() to CustomSTM32WLxWrapper All environments now build successfully. --- src/helpers/radiolib/CustomSTM32WLxWrapper.h | 7 +++++++ src/helpers/radiolib/RadioLibWrappers.cpp | 13 ++++++++++--- src/helpers/radiolib/RadioLibWrappers.h | 6 +++--- 3 files changed, 20 insertions(+), 6 deletions(-) diff --git a/src/helpers/radiolib/CustomSTM32WLxWrapper.h b/src/helpers/radiolib/CustomSTM32WLxWrapper.h index cccdc0437c..ec42b324b8 100644 --- a/src/helpers/radiolib/CustomSTM32WLxWrapper.h +++ b/src/helpers/radiolib/CustomSTM32WLxWrapper.h @@ -24,4 +24,11 @@ class CustomSTM32WLxWrapper : public RadioLibWrapper { uint8_t getSpreadingFactor() const override { return ((CustomSTM32WLx *)_radio)->spreadingFactor; } void doResetAGC() override { sx126xResetAGC((SX126x *)_radio); } + + uint8_t getCodingRate() const override { + return ((CustomSTM32WLx *)_radio)->codingRate + 4; + } + float getFreqMHz() const override { + return ((CustomSTM32WLx *)_radio)->freqMHz; + } }; diff --git a/src/helpers/radiolib/RadioLibWrappers.cpp b/src/helpers/radiolib/RadioLibWrappers.cpp index 5605cdbe60..5cad696a14 100644 --- a/src/helpers/radiolib/RadioLibWrappers.cpp +++ b/src/helpers/radiolib/RadioLibWrappers.cpp @@ -2,6 +2,13 @@ #define RADIOLIB_STATIC_ONLY 1 #include "RadioLibWrappers.h" +// Platform-safe yield for use in busy-wait loops +#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 @@ -196,17 +203,17 @@ bool RadioLibWrapper::isChannelActive() { 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) { - vTaskDelay(1); + YIELD_TASK(); } return true; } - vTaskDelay(1); + YIELD_TASK(); } // Channel free: reset busy counter and add small jitter _busy_count = 0; uint32_t jitter_until = millis() + random(0, 500); while (millis() < jitter_until) { - vTaskDelay(1); + YIELD_TASK(); } return false; } diff --git a/src/helpers/radiolib/RadioLibWrappers.h b/src/helpers/radiolib/RadioLibWrappers.h index ec326a2c87..ba6a6b7b69 100644 --- a/src/helpers/radiolib/RadioLibWrappers.h +++ b/src/helpers/radiolib/RadioLibWrappers.h @@ -43,9 +43,9 @@ class RadioLibWrapper : public mesh::Radio { virtual uint8_t getSpreadingFactor() const { return LORA_SF; } static uint16_t preambleLengthForSF(uint8_t sf) { return sf <= 8 ? 32 : 16; } void updatePreamble(uint8_t sf) { _preamble_sf = sf; _radio->setPreambleLength(preambleLengthForSF(sf)); } - virtual uint8_t getCodingRate() const = 0; - virtual float getFreqMHz() const = 0; - + 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 || From c8628b6f23a1be56135a59e0d97fd9e2a43df12c Mon Sep 17 00:00:00 2001 From: jirogit Date: Wed, 1 Apr 2026 00:39:19 -0700 Subject: [PATCH 13/14] JP: update MAX_TEXT_LEN limits based on measured airtimes, add getMaxGroupTextLen() Measured actual TimeOnAir via RadioLib getTimeOnAir() on RAK WisMesh Tag (SF12/BW125, CR4/5-8) to verify ARIB STD-T108 4-second TX limit compliance. DM limits (getMaxTextLen): CR4/5: 64 bytes (3874ms) CR4/6: 48 bytes (3874ms) CR4/7: 32 bytes (3678ms) CR4/8: 24 bytes (3547ms) Channel message limits (getMaxGroupTextLen): CR4/5: 64 bytes (3710ms) CR4/6: 48 bytes (3678ms) CR4/7: 39 bytes (3907ms) CR4/8: 29 bytes (3809ms) DM and channel packets differ by 1 byte overhead, warranting separate limits for CR4/7 and CR4/8. Non-JP returns default 160 bytes unchanged. Also apply dynamic limit to sendCommandData() and sendGroupMessage() via getMaxTextLen()/getMaxGroupTextLen() instead of hardcoded MAX_TEXT_LEN. --- src/Dispatcher.h | 1 + src/helpers/BaseChatMesh.cpp | 7 +++++-- src/helpers/radiolib/RadioLibWrappers.h | 19 ++++++++++++++----- 3 files changed, 20 insertions(+), 7 deletions(-) diff --git a/src/Dispatcher.h b/src/Dispatcher.h index 0abfa9ab79..d32633f1b9 100644 --- a/src/Dispatcher.h +++ b/src/Dispatcher.h @@ -39,6 +39,7 @@ class Radio { virtual float packetScore(float snr, int packet_len) = 0; virtual int getMaxTextLen() const { return 10 * 16; } // default: non-JP + virtual int getMaxGroupTextLen() const { return 10 * 16; } // default: non-JP /** * \brief starts the raw packet send. (no wait) diff --git a/src/helpers/BaseChatMesh.cpp b/src/helpers/BaseChatMesh.cpp index 2003db4468..220a375b3c 100644 --- a/src/helpers/BaseChatMesh.cpp +++ b/src/helpers/BaseChatMesh.cpp @@ -436,7 +436,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 @@ -469,7 +470,9 @@ 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 ba6a6b7b69..4df6be0c81 100644 --- a/src/helpers/radiolib/RadioLibWrappers.h +++ b/src/helpers/radiolib/RadioLibWrappers.h @@ -54,12 +54,21 @@ class RadioLibWrapper : public mesh::Radio { } int getMaxTextLen() const { - if (!isJapanMode()) return 10 * 16; // default + if (!isJapanMode()) return 10 * 16; // default 160 bytes uint8_t cr = getCodingRate(); - if (cr <= 5) return 3 * 16; // 48 bytes ~16 JP chars - if (cr == 6) return 2 * 16; // 32 bytes ~10 JP chars - if (cr == 7) return 1 * 16 + 8; // 24 bytes ~8 JP chars - return 1 * 16; // 16 bytes ~5 JP chars + 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(); From 5448ecf67580dcb6afb229b6faf3466583f39680 Mon Sep 17 00:00:00 2001 From: jirogit Date: Fri, 17 Apr 2026 01:02:05 -0700 Subject: [PATCH 14/14] JP LBT: prevent forced TX during backoff, no CAD timeout in Japan mode MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Dispatcher's default 4-second getCADFailMaxDuration() would trigger forced transmission before JP LBT backoff completes (max 16s), violating ARIB STD-T108 which prohibits TX while channel is busy. - Add isJapanMode() virtual method to Radio base class (Dispatcher.h) - Override getCADFailMaxDuration() in all Mesh subclasses to return UINT32_MAX in Japan mode — no forced TX, channel must be free - Default non-JP behavior unchanged (4000ms) If ambient noise exceeds -80dBm, TX is blocked indefinitely. Users can monitor noise floor via companion app: menu (⋮) -> Tools -> Noise Floor. --- examples/companion_radio/MyMesh.h | 4 ++++ examples/simple_repeater/MyMesh.h | 4 ++++ examples/simple_room_server/MyMesh.h | 5 +++++ examples/simple_sensor/SensorMesh.h | 4 ++++ src/Dispatcher.h | 1 + 5 files changed, 18 insertions(+) diff --git a/examples/companion_radio/MyMesh.h b/examples/companion_radio/MyMesh.h index aeff591cf4..d3a9bf5f52 100644 --- a/examples/companion_radio/MyMesh.h +++ b/examples/companion_radio/MyMesh.h @@ -151,6 +151,10 @@ class MyMesh : public BaseChatMesh, public DataStoreHost { uint32_t calcFloodTimeoutMillisFor(uint32_t pkt_airtime_millis) const override; uint32_t calcDirectTimeoutMillisFor(uint32_t pkt_airtime_millis, uint8_t path_len) const override; + uint32_t getCADFailMaxDuration() const override { + if (_radio->isJapanMode()) return UINT32_MAX; // JP LBT: no forced TX — channel must be free per ARIB STD-T108 + return Dispatcher::getCADFailMaxDuration(); + } void onSendTimeout() override; // DataStoreHost methods diff --git a/examples/simple_repeater/MyMesh.h b/examples/simple_repeater/MyMesh.h index 8ed0317e69..cf1ff1a4f1 100644 --- a/examples/simple_repeater/MyMesh.h +++ b/examples/simple_repeater/MyMesh.h @@ -143,6 +143,10 @@ class MyMesh : public mesh::Mesh, public CommonCLICallbacks { void logTx(mesh::Packet* pkt, int len) override; void logTxFail(mesh::Packet* pkt, int len) override; int calcRxDelay(float score, uint32_t air_time) const override; + uint32_t getCADFailMaxDuration() const override { + if (_radio->isJapanMode()) return UINT32_MAX; // JP LBT: no forced TX — channel must be free per ARIB STD-T108 + return Dispatcher::getCADFailMaxDuration(); + } uint32_t getRetransmitDelay(const mesh::Packet* packet) override; uint32_t getDirectRetransmitDelay(const mesh::Packet* packet) override; diff --git a/examples/simple_room_server/MyMesh.h b/examples/simple_room_server/MyMesh.h index 1b35ae95a1..680ce57685 100644 --- a/examples/simple_room_server/MyMesh.h +++ b/examples/simple_room_server/MyMesh.h @@ -137,6 +137,11 @@ class MyMesh : public mesh::Mesh, public CommonCLICallbacks { void logTxFail(mesh::Packet* pkt, int len) override; int calcRxDelay(float score, uint32_t air_time) const override; + uint32_t getCADFailMaxDuration() const override { + if (_radio->isJapanMode()) return UINT32_MAX; // JP LBT: no forced TX — channel must be free per ARIB STD-T108 + return Dispatcher::getCADFailMaxDuration(); + } + const char* getLogDateTime() override; uint32_t getRetransmitDelay(const mesh::Packet* packet) override; uint32_t getDirectRetransmitDelay(const mesh::Packet* packet) override; diff --git a/examples/simple_sensor/SensorMesh.h b/examples/simple_sensor/SensorMesh.h index 424b16c175..d99ad662f9 100644 --- a/examples/simple_sensor/SensorMesh.h +++ b/examples/simple_sensor/SensorMesh.h @@ -117,6 +117,10 @@ class SensorMesh : public mesh::Mesh, public CommonCLICallbacks { float getAirtimeBudgetFactor() const override; bool allowPacketForward(const mesh::Packet* packet) override; int calcRxDelay(float score, uint32_t air_time) const override; + uint32_t getCADFailMaxDuration() const override { + if (_radio->isJapanMode()) return UINT32_MAX; // JP LBT: no forced TX — channel must be free per ARIB STD-T108 + return Dispatcher::getCADFailMaxDuration(); + } uint32_t getRetransmitDelay(const mesh::Packet* packet) override; uint32_t getDirectRetransmitDelay(const mesh::Packet* packet) override; int getInterferenceThreshold() const override; diff --git a/src/Dispatcher.h b/src/Dispatcher.h index d32633f1b9..5c362c2d3b 100644 --- a/src/Dispatcher.h +++ b/src/Dispatcher.h @@ -40,6 +40,7 @@ class Radio { virtual int getMaxTextLen() const { return 10 * 16; } // default: non-JP virtual int getMaxGroupTextLen() const { return 10 * 16; } // default: non-JP + virtual bool isJapanMode() const { return false; } // default: non-JP /** * \brief starts the raw packet send. (no wait)