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/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.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_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.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_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.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/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 2a99d0682b..5c362c2d3b 100644 --- a/src/Dispatcher.h +++ b/src/Dispatcher.h @@ -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 isJapanMode() const { return false; } // default: non-JP + /** * \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..220a375b3c 100644 --- a/src/helpers/BaseChatMesh.cpp +++ b/src/helpers/BaseChatMesh.cpp @@ -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, ×tamp, 4); // mostly an extra blob to help make packet_hash unique temp[4] = (attempt & 3); @@ -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/BaseChatMesh.h b/src/helpers/BaseChatMesh.h index b39e736388..a778323f72 100644 --- a/src/helpers/BaseChatMesh.h +++ b/src/helpers/BaseChatMesh.h @@ -5,7 +5,8 @@ #include #include -#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" 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..a4ed3dbf68 100644 --- a/src/helpers/radiolib/CustomLR1110Wrapper.h +++ b/src/helpers/radiolib/CustomLR1110Wrapper.h @@ -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(); + } }; 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/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 +}; diff --git a/src/helpers/radiolib/CustomSX1262Wrapper.h b/src/helpers/radiolib/CustomSX1262Wrapper.h index e13561a4a9..b2d9432c4e 100644 --- a/src/helpers/radiolib/CustomSX1262Wrapper.h +++ b/src/helpers/radiolib/CustomSX1262Wrapper.h @@ -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; + } }; diff --git a/src/helpers/radiolib/RadioLibWrappers.cpp b/src/helpers/radiolib/RadioLibWrappers.cpp index 85f3a935db..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 @@ -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; @@ -167,13 +175,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 { diff --git a/src/helpers/radiolib/RadioLibWrappers.h b/src/helpers/radiolib/RadioLibWrappers.h index ff951aaa47..4df6be0c81 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; 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; @@ -32,7 +33,7 @@ class RadioLibWrapper : public mesh::Radio { bool isInRecvMode() const override; bool isChannelActive(); - bool isReceiving() override { + bool isReceiving() override { if (isReceivingPacket()) return true; return isChannelActive(); @@ -42,7 +43,36 @@ 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 { return 8; } // default CR4/8, override in subclass + virtual float getFreqMHz() const { return 0.0f; } // default unknown, override in subclass + // + bool isJapanMode() const { + float freq = getFreqMHz(); + return (fabsf(freq - 920.800f) < 0.05f || + fabsf(freq - 921.000f) < 0.05f || + fabsf(freq - 921.200f) < 0.05f); + } + + int getMaxTextLen() const { + if (!isJapanMode()) return 10 * 16; // default 160 bytes + uint8_t cr = getCodingRate(); + if (cr <= 5) return 64; // 3874ms @ SF12/BW125/CR4-5 + if (cr == 6) return 48; // 3874ms @ SF12/BW125/CR4-6 + if (cr == 7) return 32; // 3678ms @ SF12/BW125/CR4-7 + return 24; // 3547ms @ SF12/BW125/CR4-8 + } + + int getMaxGroupTextLen() const { + if (!isJapanMode()) return 10 * 16; // default 160 bytes + uint8_t cr = getCodingRate(); + if (cr <= 5) return 64; // 3710ms @ SF12/BW125/CR4-5 + if (cr == 6) return 48; // 3678ms @ SF12/BW125/CR4-6 + if (cr == 7) return 39; // 3907ms @ SF12/BW125/CR4-7 + return 29; // 3809ms @ SF12/BW125/CR4-8 + } + virtual int16_t performChannelScan(); + int getNoiseFloor() const override { return _noise_floor; } void triggerNoiseFloorCalibrate(int threshold) override; void resetAGC() override;