Skip to content
Open
12 changes: 12 additions & 0 deletions examples/simple_repeater/MyMesh.cpp
Original file line numberDiff line numberDiff line change
Expand Up@@ -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);
}
Expand Down
12 changes: 12 additions & 0 deletions examples/simple_room_server/MyMesh.cpp
Original file line numberDiff line numberDiff line change
Expand Up@@ -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);
}
Expand Down
16 changes: 14 additions & 2 deletions examples/simple_sensor/SensorMesh.cpp
Original file line numberDiff line numberDiff 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
1 change: 1 addition & 0 deletions src/Dispatcher.cpp
Original file line numberDiff line numberDiff line change
Expand Up@@ -60,6 +60,7 @@ uint32_t Dispatcher::getCADFailRetryDelay() const {
return 200;
}
uint32_t Dispatcher::getCADFailMaxDuration() const {
if (_radio->isAS923_1_JP()) return UINT32_MAX; // ARIB STD-T108: never force TX during LBT
return 4000; // 4 seconds
}

Expand Down
5 changes: 5 additions & 0 deletions src/Dispatcher.h
Original file line numberDiff line numberDiff line change
Expand Up@@ -78,6 +78,11 @@ class Radio {

virtual float getLastRSSI() const { return 0; }
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
};

/**
Expand Down
11 changes: 7 additions & 4 deletions src/helpers/BaseChatMesh.cpp
Original file line numberDiff line numberDiff line change
Expand Up@@ -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, &timestamp, 4); // mostly an extra blob to help make packet_hash unique
Expand DownExpand Up@@ -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, &timestamp, 4); // mostly an extra blob to help make packet_hash unique
Expand DownExpand Up@@ -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

Expand Down
1 change: 1 addition & 0 deletions src/helpers/radiolib/CustomLR1110.h
Original file line numberDiff line numberDiff line change
Expand Up@@ -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
};
2 changes: 2 additions & 0 deletions src/helpers/radiolib/CustomLR1110Wrapper.h
Original file line numberDiff line numberDiff line change
Expand Up@@ -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;
Expand Down
2 changes: 2 additions & 0 deletions src/helpers/radiolib/CustomSX1262Wrapper.h
Original file line numberDiff line numberDiff line change
Expand Up@@ -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);
}
Expand Down
55 changes: 52 additions & 3 deletions src/helpers/radiolib/RadioLibWrappers.cpp
Original file line numberDiff line numberDiff line change
Expand Up@@ -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
Expand DownExpand Up@@ -183,6 +189,9 @@ bool RadioLibWrapper::isSendComplete() {
void RadioLibWrapper::onSendFinished() {
_radio->finishTransmit();
_board->onAfterTransmit();
if (isAS923_1_JP()) {
delay(50); // ARIB STD-T108 §3.4.1: >= 50ms between transmissions
}
state = STATE_IDLE;
}

Expand All@@ -191,10 +200,50 @@ int16_t RadioLibWrapper::performChannelScan() {
}

bool RadioLibWrapper::isChannelActive() {
// int.thresh: RSSI-based interference detection (relative to noise floor)
if (_threshold != 0 && getCurrentRSSI() > _noise_floor + _threshold) return true;
if (isAS923_1_JP()) {
// Non-blocking backoff: if a prior busy detection armed a backoff wait,
// report busy and let Dispatcher::checkSend() re-poll on the next loop()
// pass instead of spin-waiting here (which used to block the Dispatcher
// loop for up to 16s).
if (_lbt_backoff_active) {
if ((int32_t)(millis() - _lbt_deadline) < 0) {
return true; // still waiting; re-checked next call
}
_lbt_backoff_active = false;
return isChannelActive(); // backoff elapsed -- re-sense (bounded, single-level recursion)
}

// ARIB STD-T108: 5ms continuous RSSI sensing, -80dBm absolute threshold
uint32_t sense_start = millis();
while (millis() - sense_start < 5) {
if (getCurrentRSSI() > -80.0f) {
_busy_count++;
uint32_t base_ms = 2000;
uint32_t max_backoff = min(base_ms * (1u << _busy_count), (uint32_t)16000);
_lbt_deadline = millis() + random(max_backoff / 2, max_backoff);
_lbt_backoff_active = true;
return true;
}
YIELD_TASK();
}
// Channel free: reset busy counter and add airtime-scaled jitter.
// JP_LBT_JITTER_DIVISOR controls jitter upper bound:
// /8 -> SF12/BW125 ~975ms, SF7/BW62.5 ~50ms
// /16 -> SF12/BW125 ~490ms, SF7/BW62.5 ~25ms
// /32 -> SF12/BW125 ~245ms, SF7/BW62.5 ~12ms (default)
_busy_count = 0;
uint32_t airtime_ms = getEstAirtimeFor(MAX_TRANS_UNIT);
uint32_t jitter_until = millis() + random(0, airtime_ms / JP_LBT_JITTER_DIVISOR);
while (millis() < jitter_until) {
YIELD_TASK();
}
// JP RSSI sensing passed; fall through to CAD if enabled
} else {
// Non-JP: RSSI-based interference detection (relative to noise floor)
if (_threshold != 0 && getCurrentRSSI() > _noise_floor + _threshold) return true;
}

// cad: hardware channel activity detection
// hardware channel activity detection (JP and non-JP)
if (_cad_enabled) {
int16_t result = performChannelScan();
// scanChannel() triggers DIO interrupt (CAD done) which sets STATE_INT_READY
Expand Down
36 changes: 35 additions & 1 deletion src/helpers/radiolib/RadioLibWrappers.h
Original file line numberDiff line numberDiff line change
Expand Up@@ -18,18 +18,22 @@ class RadioLibWrapper : public mesh::Radio {
uint32_t n_recv, n_sent, n_recv_errors;
int16_t _noise_floor, _threshold;
bool _cad_enabled;
uint8_t _busy_count;
uint16_t _num_floor_samples;
int32_t _floor_sample_sum;
uint8_t _preamble_sf;

bool _lbt_backoff_active; // non-blocking wait after RSSI busy detection (up to 16s)
uint32_t _lbt_deadline;

void idle();
void startRecv();
float packetScoreInt(float snr, int sf, int packet_len);
virtual bool isReceivingPacket() =0;
virtual void doResetAGC();

public:
RadioLibWrapper(PhysicalLayer& radio, mesh::MainBoard& board) : _radio(&radio), _board(&board), _preamble_sf(0) { n_recv = n_sent = 0; }
RadioLibWrapper(PhysicalLayer& radio, mesh::MainBoard& board) : _radio(&radio), _board(&board), _busy_count(0), _preamble_sf(0), _lbt_backoff_active(false), _lbt_deadline(0) { n_recv = n_sent = 0; }

void begin() override;
virtual void powerOff() { _radio->sleep(); }
Expand All@@ -56,6 +60,36 @@ class RadioLibWrapper : public mesh::Radio {
static uint16_t preambleLengthForSF(uint8_t sf) { return sf <= 8 ? 32 : 16; }
void updatePreamble(uint8_t sf) { _preamble_sf = sf; _radio->setPreambleLength(preambleLengthForSF(sf)); }
PacketMillis calcMaxPacketMillis(uint8_t sf, float bw, uint8_t cr, uint8_t preambleSymbols);
virtual uint8_t getCodingRate() const { return 8; } // default CR4/8, override in subclass
virtual float getFreqMHz() const { return 0.0f; } // default unknown, override in subclass

static constexpr uint8_t JP_LBT_JITTER_DIVISOR = 32;

bool isAS923_1_JP() const override {
float freq = getFreqMHz();
return (fabsf(freq - 920.800f) < 0.05f ||
fabsf(freq - 921.000f) < 0.05f ||
fabsf(freq - 921.200f) < 0.05f);
}

int getMaxTextLen() const override {
if (!isAS923_1_JP()) return 10 * 16; // default 160 bytes
uint8_t cr = getCodingRate();
if (cr <= 5) return 64; // 3874ms @ SF12/BW125/CR4-5
if (cr == 6) return 48; // 3874ms @ SF12/BW125/CR4-6
if (cr == 7) return 32; // 3678ms @ SF12/BW125/CR4-7
return 24; // 3547ms @ SF12/BW125/CR4-8
}

int getMaxGroupTextLen() const override {
if (!isAS923_1_JP()) return 10 * 16; // default 160 bytes
uint8_t cr = getCodingRate();
if (cr <= 5) return 64; // 3710ms @ SF12/BW125/CR4-5
if (cr == 6) return 48; // 3678ms @ SF12/BW125/CR4-6
if (cr == 7) return 39; // 3907ms @ SF12/BW125/CR4-7
return 29; // 3809ms @ SF12/BW125/CR4-8
}

virtual int16_t performChannelScan();

int getNoiseFloor() const override { return _noise_floor; }
Expand Down