Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
18 commits
Select commit Hold shift + click to select a range
a9edacd
Use hardware channel activity detection for checking interference
weebl2000 Feb 18, 2026
6598f91
Add JP_STRICT mode: enforce MAX_TEXT_LEN for ARIB STD-T108 compliance
jirogit Mar 25, 2026
5081126
Add JP_STRICT 5ms continuous RSSI sensing before TX (ARIB STD-T108)
jirogit Mar 25, 2026
d4644c1
JP_STRICT: use absolute -80dBm RSSI threshold for LBT (ARIB STD-T108)
jirogit Mar 25, 2026
96f435d
JP_STRICT: adjust MAX_TEXT_LEN based on LORA_CR setting
jirogit Mar 26, 2026
3158dea
JP_STRICT: dynamic MAX_TEXT_LEN based on runtime CR value
jirogit Mar 26, 2026
48080d2
Add exponential backoff for channel busy detection
jirogit Mar 26, 2026
5295ca7
Fix missing newline at end of file in CustomSX1262.h
jirogit Mar 27, 2026
3c0ed55
Add getCodingRate() to CustomLR1110 and CustomLR1110Wrapper
jirogit Mar 27, 2026
4867d36
Replace JP_STRICT build flag with runtime frequency detection
jirogit Mar 28, 2026
db23aa0
Remove TX duration debug logging
jirogit Mar 28, 2026
52f1d69
Fix build for non-FreeRTOS and non-SX1262 platforms
jirogit Mar 28, 2026
c8628b6
JP: update MAX_TEXT_LEN limits based on measured airtimes, add getMax…
jirogit Apr 1, 2026
5448ecf
JP LBT: prevent forced TX during backoff, no CAD timeout in Japan mode
jirogit Apr 17, 2026
f2aad64
JP LBT: scale jitter by airtime instead of fixed 500ms
jirogit May 3, 2026
c70e066
JP LBT: fix comment, /32 is default not /16
jirogit May 3, 2026
3a88765
rename isJapanMode() to isAS923_1_JP() for precision
jirogit May 5, 2026
4e09f6a
JP LBT: suppress txdelay to jitter-scale at SF12/BW125
jirogit May 5, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion examples/companion_radio/MyMesh.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand Down
4 changes: 4 additions & 0 deletions examples/companion_radio/MyMesh.h
Original file line number Diff line number Diff line change
Expand Up @@ -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->isAS923_1_JP()) return UINT32_MAX; // JP LBT: no forced TX — channel must be free per ARIB STD-T108
return Dispatcher::getCADFailMaxDuration();
}
void onSendTimeout() override;

// DataStoreHost methods
Expand Down
14 changes: 13 additions & 1 deletion examples/simple_repeater/MyMesh.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -537,10 +537,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);
}
Expand Down Expand Up @@ -886,7 +898,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
Expand Down
4 changes: 4 additions & 0 deletions examples/simple_repeater/MyMesh.h
Original file line number Diff line number Diff line change
Expand Up @@ -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->isAS923_1_JP()) 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;
Expand Down
14 changes: 13 additions & 1 deletion examples/simple_room_server/MyMesh.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -272,10 +272,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);
}
Expand Down Expand Up @@ -643,7 +655,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
Expand Down
5 changes: 5 additions & 0 deletions examples/simple_room_server/MyMesh.h
Original file line number Diff line number Diff line change
Expand Up @@ -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->isAS923_1_JP()) 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;
Expand Down
18 changes: 15 additions & 3 deletions examples/simple_sensor/SensorMesh.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -725,7 +737,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;
Expand Down
4 changes: 4 additions & 0 deletions examples/simple_sensor/SensorMesh.h
Original file line number Diff line number Diff line change
Expand Up @@ -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->isAS923_1_JP()) 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;
Expand Down
4 changes: 4 additions & 0 deletions src/Dispatcher.h
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,10 @@ 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
virtual bool isAS923_1_JP() const { return false; } // default: non-JP

/**
* \brief starts the raw packet send. (no wait)
* \param bytes the raw packet data
Expand Down
13 changes: 8 additions & 5 deletions src/helpers/BaseChatMesh.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -395,9 +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);
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, &timestamp, 4); // mostly an extra blob to help make packet_hash unique
temp[4] = (attempt & 3);
Expand Down Expand Up @@ -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, &timestamp, 4); // mostly an extra blob to help make packet_hash unique
Expand Down Expand Up @@ -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

Expand Down
3 changes: 2 additions & 1 deletion src/helpers/BaseChatMesh.h
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,8 @@
#include <helpers/AdvertDataHelpers.h>
#include <helpers/TxtDataHelpers.h>

#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)

#include "ContactInfo.h"

Expand Down
6 changes: 5 additions & 1 deletion src/helpers/radiolib/CustomLR1110.h
Original file line number Diff line number Diff line change
Expand Up @@ -38,4 +38,8 @@ 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
}
};
7 changes: 7 additions & 0 deletions src/helpers/radiolib/CustomLR1110Wrapper.h
Original file line number Diff line number Diff line change
Expand Up @@ -33,4 +33,11 @@ class CustomLR1110Wrapper : public RadioLibWrapper {
bool getRxBoostedGainMode() const override {
return ((CustomLR1110 *)_radio)->getRxBoostedGainMode();
}

uint8_t getCodingRate() const override {
return ((CustomLR1110 *)_radio)->getCodingRate();
}
float getFreqMHz() const override {
return ((CustomLR1110 *)_radio)->getFreqMHz();
}
};
7 changes: 7 additions & 0 deletions src/helpers/radiolib/CustomSTM32WLxWrapper.h
Original file line number Diff line number Diff line change
Expand Up @@ -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;
}
};
2 changes: 1 addition & 1 deletion src/helpers/radiolib/CustomSX1262.h
Original file line number Diff line number Diff line change
Expand Up @@ -97,4 +97,4 @@ class CustomSX1262 : public SX1262 {
readRegister(RADIOLIB_SX126X_REG_RX_GAIN, &rxGain, 1);
return (rxGain == RADIOLIB_SX126X_RX_GAIN_BOOSTED);
}
};
};
7 changes: 7 additions & 0 deletions src/helpers/radiolib/CustomSX1262Wrapper.h
Original file line number Diff line number Diff line change
Expand Up @@ -37,4 +37,11 @@ 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
}
float getFreqMHz() const override {
return ((CustomSX1262 *)_radio)->freqMHz;
}
};
57 changes: 54 additions & 3 deletions src/helpers/radiolib/RadioLibWrappers.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -36,6 +43,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;
Expand Down Expand Up @@ -167,13 +175,56 @@ bool RadioLibWrapper::isSendComplete() {
void RadioLibWrapper::onSendFinished() {
_radio->finishTransmit();
_board->onAfterTransmit();
if (isAS923_1_JP()) {
// ARIB STD-T108: wait >= 50ms after TX before next transmission
delay(50);
}
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

// Activate JP_STRICT LBT on Japan 920MHz band 3 channels only
// CH25=920.800MHz, CH26=921.000MHz, CH27=921.200MHz (ARIB STD-T108)
if (isAS923_1_JP()) {
// 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) {
YIELD_TASK();
}
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();
}
return false;
}

// Non-JP: original behavior (RSSI threshold only)
return getCurrentRSSI() > _noise_floor + _threshold;
}

float RadioLibWrapper::getLastRSSI() const {
Expand Down
Loading