Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
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 @@ -255,7 +255,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
2 changes: 1 addition & 1 deletion examples/simple_repeater/MyMesh.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -871,7 +871,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
2 changes: 1 addition & 1 deletion examples/simple_room_server/MyMesh.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -622,7 +622,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
2 changes: 1 addition & 1 deletion examples/simple_sensor/SensorMesh.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -723,7 +723,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
3 changes: 3 additions & 0 deletions src/Dispatcher.h
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,9 @@ 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)
* \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 @@ -36,4 +36,8 @@ class CustomLR1110 : public LR1110 {
bool detected = ((irq & RADIOLIB_LR11X0_IRQ_SYNC_WORD_HEADER_VALID) || (irq & RADIOLIB_LR11X0_IRQ_PREAMBLE_DETECTED));
return detected;
}
};

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 @@ -31,4 +31,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 @@ -23,4 +23,11 @@ class CustomSTM32WLxWrapper : public RadioLibWrapper {
}

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 @@ -36,4 +36,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;
}
};
52 changes: 49 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 @@ -34,6 +41,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 @@ -165,13 +173,51 @@ bool RadioLibWrapper::isSendComplete() {
void RadioLibWrapper::onSendFinished() {
_radio->finishTransmit();
_board->onAfterTransmit();
if (isJapanMode()) {
// 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 (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) {
YIELD_TASK();
}
return true;
}
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) {
YIELD_TASK();
}
return false;
}

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

float RadioLibWrapper::getLastRSSI() const {
Expand Down
32 changes: 31 additions & 1 deletion src/helpers/radiolib/RadioLibWrappers.h
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ class RadioLibWrapper : public mesh::Radio {
mesh::MainBoard* _board;
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;
int32_t _floor_sample_sum;

Expand All @@ -31,14 +32,43 @@ class RadioLibWrapper : public mesh::Radio {
bool isInRecvMode() const override;
bool isChannelActive();

bool isReceiving() override {
bool isReceiving() override {
if (isReceivingPacket()) return true;

return isChannelActive();
}

virtual float getCurrentRSSI() =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 {
Copy link
Copy Markdown

@4np 4np Apr 2, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The function name doesn't match the frequencies being checked. 920.8, 921.0, and 921.2 MHz are the default channels for AS923-2, which is the plan for Indonesia and Vietnam, not Japan. Japan uses AS923-1 with default channels at 923.2 and 923.4 MHz. This means the function will return false for most Japanese devices and true for Indonesian/Vietnamese ones, a false positive for an entirely different region. Suggest renaming to isAS923_2Mode() or correcting the frequencies to 923.2/923.4 MHz depending on the intended behavior.

Copy link
Copy Markdown
Author

@jirogit jirogit Apr 2, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The function name doesn't match the frequencies being checked. 920.8, 921.0, and 921.2 MHz are the default channels for AS923-2, which is the plan for Indonesia and Vietnam, not Japan. Japan uses AS923-1 with default channels at 923.2 and 923.4 MHz. This means the function will return false for most Japanese devices and true for Indonesian/Vietnamese ones, a false positive for an entirely different region. Suggest renaming to isAS923_2Mode() or correcting the frequencies to 923.2/923.4 MHz depending on the intended behavior.

Thank you for the review. You are correct that 920.8/921.0/921.2 MHz overlap with AS923-2 (Indonesia/Vietnam in LoRaWAN terms). However, these frequencies are legal under Japanese radio law (ARIB STD-T108, 920.6–928.0 MHz) and are within the band permitted for use in Japan.

Regarding the false positive concern for Indonesian/Vietnamese users: MeshCore does not currently have a defined default frequency for Indonesia. Vietnam's current default is 920.250 MHz, which does not match any of the three frequencies checked by isJapanMode() (920.800 / 921.000 / 921.200 MHz). In practice, the risk of unintended activation on non-Japanese nodes is low.

That said, the concern is valid in principle and points toward a broader architectural question: as MeshCore expands into regions with different regulatory requirements — duty cycle limits in the EU, LBT variants in other Asian countries, TX power caps, airtime limits — a more general regional compliance framework would be valuable.

A future "set radio_law / get radio_law" command (distinct from the existing "set region") could allow users to explicitly select their regulatory domain (e.g. JP, EU, FCC), enabling the firmware to apply the appropriate constraints automatically.

The current frequency-based isJapanMode() is intentionally minimal and pragmatic given the near-zero Japanese node count today, but we recognize it is not a scalable long-term solution. We are open to adding such a mechanism if the maintainer prefers a more structured approach, and would welcome guidance on the preferred direction for regional compliance in MeshCore going forward.

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; }
void triggerNoiseFloorCalibrate(int threshold) override;
void resetAGC() override;
Expand Down
2 changes: 1 addition & 1 deletion variants/rak_wismesh_tag/platformio.ini
Original file line number Diff line number Diff line change
Expand Up @@ -118,4 +118,4 @@ build_flags =
; -D MESH_PACKET_LOGGING=1
-D MESH_DEBUG=1
build_src_filter = ${rak4631.build_src_filter}
+<../examples/simple_sensor>
+<../examples/simple_sensor>