From 0ad3fc11d95cdfd0af4534da67ac1331761320cf Mon Sep 17 00:00:00 2001 From: jirogit Date: Wed, 9 Sep 2026 00:20:45 -0700 Subject: [PATCH 1/4] Add RSSI-based listen-before-talk to RadioLibWrapper Hardware CAD only detects a LoRa preamble, so it is blind to any other modulation sharing the band. int.thresh does sample RSSI, but relative to the tracked noise floor and only once, which cannot express an absolute threshold or a sensing window. Add a third, independent channel-busy check to isChannelActive(): while enabled, sample getCurrentRSSI() continuously for sense_ms and report the channel busy if any sample exceeds an absolute dBm threshold. The loop always takes at least one sample, so sense_ms of 0 degrades to a single instantaneous reading rather than to no reading at all. The two existing checks are untouched and the return value stays a plain bool, so isReceiving() remains the single gate that Dispatcher::checkSend() consults. CAD and this check are fully independent; either, both, or neither may be active. Also enforce an optional quiet period after each transmit in onSendFinished(). delay() is used rather than a bare spin so the pause yields to the scheduler. Nothing calls setRssiLbtParams() yet, so this commit is inert on its own: the mechanism stays disabled until the prefs are wired up. --- src/Dispatcher.h | 2 ++ src/helpers/radiolib/RadioLibWrappers.cpp | 18 ++++++++++++++++++ src/helpers/radiolib/RadioLibWrappers.h | 10 ++++++++++ 3 files changed, 30 insertions(+) diff --git a/src/Dispatcher.h b/src/Dispatcher.h index aad6cba3ec..52d44541dc 100644 --- a/src/Dispatcher.h +++ b/src/Dispatcher.h @@ -67,6 +67,8 @@ class Radio { virtual void setCADEnabled(bool enable) { } + virtual void setRssiLbtParams(bool enabled, int8_t thr_dbm, uint16_t sense_ms, uint16_t pause_ms) { } + virtual void resetAGC() { } virtual bool isInRecvMode() const = 0; diff --git a/src/helpers/radiolib/RadioLibWrappers.cpp b/src/helpers/radiolib/RadioLibWrappers.cpp index e4d2ba1c27..c70e429af1 100644 --- a/src/helpers/radiolib/RadioLibWrappers.cpp +++ b/src/helpers/radiolib/RadioLibWrappers.cpp @@ -37,6 +37,10 @@ void RadioLibWrapper::begin() { _noise_floor = 0; _threshold = 0; _cad_enabled = false; + _rssi_lbt_enabled = false; + _rssi_lbt_thr_dbm = 0; + _rssi_lbt_sense_ms = 0; + _rssi_lbt_pause_ms = 0; // start average out some samples _num_floor_samples = 0; @@ -189,6 +193,11 @@ void RadioLibWrapper::onSendFinished() { _radio->finishTransmit(); _board->onAfterTransmit(); state = STATE_IDLE; + + // rssi.lbt: quiet period after every transmit + if (_rssi_lbt_enabled && _rssi_lbt_pause_ms > 0) { + delay(_rssi_lbt_pause_ms); + } } int16_t RadioLibWrapper::performChannelScan() { @@ -199,6 +208,15 @@ bool RadioLibWrapper::isChannelActive() { // int.thresh: RSSI-based interference detection (relative to noise floor) if (_threshold != 0 && getCurrentRSSI() > _noise_floor + _threshold) return true; + // rssi.lbt: energy detection against an absolute threshold, sampled continuously + // over a sensing window. Unlike CAD this is independent of the modulation on air. + if (_rssi_lbt_enabled) { + uint32_t start = millis(); + do { + if (getCurrentRSSI() > _rssi_lbt_thr_dbm) return true; + } while (millis() - start < _rssi_lbt_sense_ms); + } + // cad: hardware channel activity detection if (_cad_enabled) { int16_t result = performChannelScan(); diff --git a/src/helpers/radiolib/RadioLibWrappers.h b/src/helpers/radiolib/RadioLibWrappers.h index 77dd93116b..15fa13f872 100644 --- a/src/helpers/radiolib/RadioLibWrappers.h +++ b/src/helpers/radiolib/RadioLibWrappers.h @@ -18,6 +18,10 @@ class RadioLibWrapper : public mesh::Radio { uint32_t n_recv, n_sent, n_recv_errors; int16_t _noise_floor, _threshold; bool _cad_enabled; + bool _rssi_lbt_enabled; + int8_t _rssi_lbt_thr_dbm; + uint16_t _rssi_lbt_sense_ms; + uint16_t _rssi_lbt_pause_ms; uint16_t _num_floor_samples; int32_t _floor_sample_sum; uint8_t _preamble_sf; @@ -61,6 +65,12 @@ class RadioLibWrapper : public mesh::Radio { int getNoiseFloor() const override { return _noise_floor; } void triggerNoiseFloorCalibrate(int threshold) override; void setCADEnabled(bool enable) override { _cad_enabled = enable; } + void setRssiLbtParams(bool enabled, int8_t thr_dbm, uint16_t sense_ms, uint16_t pause_ms) override { + _rssi_lbt_enabled = enabled; + _rssi_lbt_thr_dbm = thr_dbm; + _rssi_lbt_sense_ms = sense_ms; + _rssi_lbt_pause_ms = pause_ms; + } void resetAGC() override; void loop() override; From cfe7aafe15181cd39deb4792eaef6a1aaca466ae Mon Sep 17 00:00:00 2001 From: jirogit Date: Wed, 9 Sep 2026 00:20:45 -0700 Subject: [PATCH 2/4] Wire the RSSI listen-before-talk prefs and CLI Add the six parameters to CommonRadioPrefs, which both NodePrefs implementations derive from, so companion, repeater, room server and sensor are all covered by one implementation: set rssi.lbt on|off set rssi.lbt.params ,,,, get rssi.lbt get rssi.lbt.params -> "> -80,5,0,4000,50" The params are comma separated in both directions, following `set radio` and `get radio`, so the output of the getter can be pasted straight back into the setter. Defaults are chosen so that a single `set rssi.lbt on` brings up a usable configuration. Dispatcher pushes the sensing parameters down to the radio from the same place it already refreshes the noise floor threshold and the CAD flag. Only thr_dbm, sense_ms and pause_ms are the radio's concern; maxwait_ms and txmax_ms are acted on by the dispatcher itself. The legacy binary prefs loader is deliberately left alone: these fields cannot exist in a file written by a build that predates them, so such a file simply leaves them at their defaults. --- examples/companion_radio/MyMesh.cpp | 18 +++++++++++++ examples/companion_radio/MyMesh.h | 6 +++++ examples/companion_radio/NodePrefs.h | 24 +++++++++++++++++ examples/simple_repeater/MyMesh.h | 18 +++++++++++++ examples/simple_room_server/MyMesh.h | 18 +++++++++++++ examples/simple_sensor/SensorMesh.cpp | 18 +++++++++++++ examples/simple_sensor/SensorMesh.h | 6 +++++ src/Dispatcher.cpp | 1 + src/Dispatcher.h | 6 +++++ src/helpers/CommonCLI.h | 24 +++++++++++++++++ src/helpers/CommonRadioPrefs.cpp | 39 +++++++++++++++++++++++++++ src/helpers/CommonRadioPrefs.h | 18 +++++++++++++ 12 files changed, 196 insertions(+) diff --git a/examples/companion_radio/MyMesh.cpp b/examples/companion_radio/MyMesh.cpp index 46c8e2f60c..72629a2159 100644 --- a/examples/companion_radio/MyMesh.cpp +++ b/examples/companion_radio/MyMesh.cpp @@ -270,6 +270,24 @@ int MyMesh::getInterferenceThreshold() const { bool MyMesh::getCADEnabled() const { return _prefs.cad_enabled; } +bool MyMesh::getRssiLbtEnabled() const { + return _prefs.rssi_lbt_enabled; +} +int8_t MyMesh::getRssiLbtThrDbm() const { + return _prefs.rssi_lbt_thr_dbm; +} +uint16_t MyMesh::getRssiLbtSenseMs() const { + return _prefs.rssi_lbt_sense_ms; +} +uint16_t MyMesh::getRssiLbtMaxwaitMs() const { + return _prefs.rssi_lbt_maxwait_ms; +} +uint16_t MyMesh::getRssiLbtTxmaxMs() const { + return _prefs.rssi_lbt_txmax_ms; +} +uint16_t MyMesh::getRssiLbtPauseMs() const { + return _prefs.rssi_lbt_pause_ms; +} int MyMesh::calcRxDelay(float score, uint32_t air_time) const { if (_prefs.rx_delay_base <= 0.0f) return 0; diff --git a/examples/companion_radio/MyMesh.h b/examples/companion_radio/MyMesh.h index 3b98a4f674..bf272653d3 100644 --- a/examples/companion_radio/MyMesh.h +++ b/examples/companion_radio/MyMesh.h @@ -121,6 +121,12 @@ class MyMesh : public BaseChatMesh, public DataStoreHost { float getAirtimeBudgetFactor() const override; int getInterferenceThreshold() const override; bool getCADEnabled() const override; + bool getRssiLbtEnabled() const override; + int8_t getRssiLbtThrDbm() const override; + uint16_t getRssiLbtSenseMs() const override; + uint16_t getRssiLbtMaxwaitMs() const override; + uint16_t getRssiLbtTxmaxMs() const override; + uint16_t getRssiLbtPauseMs() const override; int getAGCResetInterval() const override { return ((int)_prefs.agc_reset_interval) * 4000; // milliseconds } diff --git a/examples/companion_radio/NodePrefs.h b/examples/companion_radio/NodePrefs.h index 846ff16989..3c36aa407f 100644 --- a/examples/companion_radio/NodePrefs.h +++ b/examples/companion_radio/NodePrefs.h @@ -43,6 +43,12 @@ class NodePrefs : public ConfigSerializer { // persisted to file uint8_t path_hash_mode = 0; // which path mode to use when sending uint8_t autoadd_max_hops = 0; // 0 = no limit, 1 = direct (0 hops), N = up to N-1 hops (max 64) uint8_t cad_enabled = 0; + uint8_t rssi_lbt_enabled = 0; // RSSI listen-before-talk (boolean) + int8_t rssi_lbt_thr_dbm = -80; // absolute RSSI threshold, above which the channel counts as busy + uint16_t rssi_lbt_sense_ms = 5; // how long to sample RSSI for + uint16_t rssi_lbt_maxwait_ms = 0; // max time to wait out a busy channel before forcing TX (0 = unlimited) + uint16_t rssi_lbt_txmax_ms = 4000; // max airtime of a single TX (0 = unlimited) + uint16_t rssi_lbt_pause_ms = 50; // quiet period after each TX uint8_t interference_threshold = 0; uint8_t agc_reset_interval = 0; // secs / 4 char default_scope_name[31]; @@ -59,6 +65,12 @@ class NodePrefs : public ConfigSerializer { // persisted to file def("sf", _parent->sf); def("cr", _parent->cr); def("cad", _parent->cad_enabled); + def("lbt_en", _parent->rssi_lbt_enabled); + def("lbt_thr", _parent->rssi_lbt_thr_dbm); + def("lbt_sense", _parent->rssi_lbt_sense_ms); + def("lbt_maxwait", _parent->rssi_lbt_maxwait_ms); + def("lbt_txmax", _parent->rssi_lbt_txmax_ms); + def("lbt_pause", _parent->rssi_lbt_pause_ms); def("int_thr", _parent->interference_threshold); def("rxgain", _parent->rx_boosted_gain); def("fem_rxgain", _parent->radio_fem_rxgain); // fem_rxgain WAS mapped to wrong JSON property previously @@ -88,6 +100,18 @@ class NodePrefs : public ConfigSerializer { // persisted to file void setAirtimeFactor(float af) override { _parent->airtime_factor = af; markDirty(); } bool isCadEnabled() const override { return _parent->cad_enabled; } void setCadEnabled(bool en) override { _parent->cad_enabled = en; markDirty(); } + bool isRssiLbtEnabled() const override { return _parent->rssi_lbt_enabled; } + void setRssiLbtEnabled(bool en) override { _parent->rssi_lbt_enabled = en; markDirty(); } + int8_t getRssiLbtThrDbm() const override { return _parent->rssi_lbt_thr_dbm; } + void setRssiLbtThrDbm(int8_t dbm) override { _parent->rssi_lbt_thr_dbm = dbm; markDirty(); } + uint16_t getRssiLbtSenseMs() const override { return _parent->rssi_lbt_sense_ms; } + void setRssiLbtSenseMs(uint16_t ms) override { _parent->rssi_lbt_sense_ms = ms; markDirty(); } + uint16_t getRssiLbtMaxwaitMs() const override { return _parent->rssi_lbt_maxwait_ms; } + void setRssiLbtMaxwaitMs(uint16_t ms) override { _parent->rssi_lbt_maxwait_ms = ms; markDirty(); } + uint16_t getRssiLbtTxmaxMs() const override { return _parent->rssi_lbt_txmax_ms; } + void setRssiLbtTxmaxMs(uint16_t ms) override { _parent->rssi_lbt_txmax_ms = ms; markDirty(); } + uint16_t getRssiLbtPauseMs() const override { return _parent->rssi_lbt_pause_ms; } + void setRssiLbtPauseMs(uint16_t ms) override { _parent->rssi_lbt_pause_ms = ms; markDirty(); } uint8_t getIntThresh() const override { return _parent->interference_threshold; } void setIntThresh(uint8_t t) override { _parent->interference_threshold = t; markDirty(); } uint8_t getRxGain() const override { return _parent->rx_boosted_gain; } diff --git a/examples/simple_repeater/MyMesh.h b/examples/simple_repeater/MyMesh.h index cac6c4a281..247a2acf33 100644 --- a/examples/simple_repeater/MyMesh.h +++ b/examples/simple_repeater/MyMesh.h @@ -150,6 +150,24 @@ class MyMesh : public mesh::Mesh, public CommonCLICallbacks { bool getCADEnabled() const override { return _prefs.cad_enabled; } + bool getRssiLbtEnabled() const override { + return _prefs.rssi_lbt_enabled; + } + int8_t getRssiLbtThrDbm() const override { + return _prefs.rssi_lbt_thr_dbm; + } + uint16_t getRssiLbtSenseMs() const override { + return _prefs.rssi_lbt_sense_ms; + } + uint16_t getRssiLbtMaxwaitMs() const override { + return _prefs.rssi_lbt_maxwait_ms; + } + uint16_t getRssiLbtTxmaxMs() const override { + return _prefs.rssi_lbt_txmax_ms; + } + uint16_t getRssiLbtPauseMs() const override { + return _prefs.rssi_lbt_pause_ms; + } int getAGCResetInterval() const override { return ((int)_prefs.agc_reset_interval) * 4000; // milliseconds } diff --git a/examples/simple_room_server/MyMesh.h b/examples/simple_room_server/MyMesh.h index 5cf949c6bd..e4f41d8a02 100644 --- a/examples/simple_room_server/MyMesh.h +++ b/examples/simple_room_server/MyMesh.h @@ -149,6 +149,24 @@ class MyMesh : public mesh::Mesh, public CommonCLICallbacks { bool getCADEnabled() const override { return _prefs.cad_enabled; } + bool getRssiLbtEnabled() const override { + return _prefs.rssi_lbt_enabled; + } + int8_t getRssiLbtThrDbm() const override { + return _prefs.rssi_lbt_thr_dbm; + } + uint16_t getRssiLbtSenseMs() const override { + return _prefs.rssi_lbt_sense_ms; + } + uint16_t getRssiLbtMaxwaitMs() const override { + return _prefs.rssi_lbt_maxwait_ms; + } + uint16_t getRssiLbtTxmaxMs() const override { + return _prefs.rssi_lbt_txmax_ms; + } + uint16_t getRssiLbtPauseMs() const override { + return _prefs.rssi_lbt_pause_ms; + } int getAGCResetInterval() const override { return ((int)_prefs.agc_reset_interval) * 4000; // milliseconds } diff --git a/examples/simple_sensor/SensorMesh.cpp b/examples/simple_sensor/SensorMesh.cpp index 23d0cdc353..5b2195137a 100644 --- a/examples/simple_sensor/SensorMesh.cpp +++ b/examples/simple_sensor/SensorMesh.cpp @@ -326,6 +326,24 @@ int SensorMesh::getInterferenceThreshold() const { bool SensorMesh::getCADEnabled() const { return _prefs.cad_enabled; } +bool SensorMesh::getRssiLbtEnabled() const { + return _prefs.rssi_lbt_enabled; +} +int8_t SensorMesh::getRssiLbtThrDbm() const { + return _prefs.rssi_lbt_thr_dbm; +} +uint16_t SensorMesh::getRssiLbtSenseMs() const { + return _prefs.rssi_lbt_sense_ms; +} +uint16_t SensorMesh::getRssiLbtMaxwaitMs() const { + return _prefs.rssi_lbt_maxwait_ms; +} +uint16_t SensorMesh::getRssiLbtTxmaxMs() const { + return _prefs.rssi_lbt_txmax_ms; +} +uint16_t SensorMesh::getRssiLbtPauseMs() const { + return _prefs.rssi_lbt_pause_ms; +} int SensorMesh::getAGCResetInterval() const { return ((int)_prefs.agc_reset_interval) * 4000; // milliseconds } diff --git a/examples/simple_sensor/SensorMesh.h b/examples/simple_sensor/SensorMesh.h index b5e96d5cc7..50af6a46cf 100644 --- a/examples/simple_sensor/SensorMesh.h +++ b/examples/simple_sensor/SensorMesh.h @@ -121,6 +121,12 @@ class SensorMesh : public mesh::Mesh, public CommonCLICallbacks { uint32_t getDirectRetransmitDelay(const mesh::Packet* packet) override; int getInterferenceThreshold() const override; bool getCADEnabled() const override; + bool getRssiLbtEnabled() const override; + int8_t getRssiLbtThrDbm() const override; + uint16_t getRssiLbtSenseMs() const override; + uint16_t getRssiLbtMaxwaitMs() const override; + uint16_t getRssiLbtTxmaxMs() const override; + uint16_t getRssiLbtPauseMs() const override; int getAGCResetInterval() const override; void onAnonDataRecv(mesh::Packet* packet, const uint8_t* secret, const mesh::Identity& sender, uint8_t* data, size_t len) override; int searchPeersByHash(const uint8_t* hash) override; diff --git a/src/Dispatcher.cpp b/src/Dispatcher.cpp index c0610b7f8a..39a395d8b9 100644 --- a/src/Dispatcher.cpp +++ b/src/Dispatcher.cpp @@ -67,6 +67,7 @@ void Dispatcher::loop() { if (millisHasNowPassed(next_floor_calib_time)) { _radio->triggerNoiseFloorCalibrate(getInterferenceThreshold()); _radio->setCADEnabled(getCADEnabled()); + _radio->setRssiLbtParams(getRssiLbtEnabled(), getRssiLbtThrDbm(), getRssiLbtSenseMs(), getRssiLbtPauseMs()); next_floor_calib_time = futureMillis(NOISE_FLOOR_CALIB_INTERVAL); } _radio->loop(); diff --git a/src/Dispatcher.h b/src/Dispatcher.h index 52d44541dc..1f2d17f718 100644 --- a/src/Dispatcher.h +++ b/src/Dispatcher.h @@ -171,6 +171,12 @@ class Dispatcher { virtual uint32_t getCADFailMaxDuration() const; virtual int getInterferenceThreshold() const { return 0; } // disabled by default virtual bool getCADEnabled() const { return false; } // hardware CAD disabled by default + virtual bool getRssiLbtEnabled() const { return false; } // RSSI listen-before-talk disabled by default + virtual int8_t getRssiLbtThrDbm() const { return 0; } + virtual uint16_t getRssiLbtSenseMs() const { return 0; } + virtual uint16_t getRssiLbtMaxwaitMs() const { return 0; } + virtual uint16_t getRssiLbtTxmaxMs() const { return 0; } + virtual uint16_t getRssiLbtPauseMs() const { return 0; } virtual int getAGCResetInterval() const { return 0; } // disabled by default virtual unsigned long getDutyCycleWindowMs() const { return 3600000; } diff --git a/src/helpers/CommonCLI.h b/src/helpers/CommonCLI.h index 8591cdc140..65f32db0be 100644 --- a/src/helpers/CommonCLI.h +++ b/src/helpers/CommonCLI.h @@ -71,6 +71,12 @@ class NodePrefs : public ConfigSerializer { uint8_t path_hash_mode = 0; // which path mode to use when sending uint8_t loop_detect = 0; uint8_t cad_enabled = 0; // hardware Channel Activity Detection before TX (boolean) + uint8_t rssi_lbt_enabled = 0; // RSSI listen-before-talk (boolean) + int8_t rssi_lbt_thr_dbm = -80; // absolute RSSI threshold, above which the channel counts as busy + uint16_t rssi_lbt_sense_ms = 5; // how long to sample RSSI for + uint16_t rssi_lbt_maxwait_ms = 0; // max time to wait out a busy channel before forcing TX (0 = unlimited) + uint16_t rssi_lbt_txmax_ms = 4000; // max airtime of a single TX (0 = unlimited) + uint16_t rssi_lbt_pause_ms = 50; // quiet period after each TX uint8_t extra_sf[4]; private: @@ -83,6 +89,12 @@ class NodePrefs : public ConfigSerializer { def("sf", _parent->sf); def("cr", _parent->cr); def("cad", _parent->cad_enabled); + def("lbt_en", _parent->rssi_lbt_enabled); + def("lbt_thr", _parent->rssi_lbt_thr_dbm); + def("lbt_sense", _parent->rssi_lbt_sense_ms); + def("lbt_maxwait", _parent->rssi_lbt_maxwait_ms); + def("lbt_txmax", _parent->rssi_lbt_txmax_ms); + def("lbt_pause", _parent->rssi_lbt_pause_ms); def("int_thr", _parent->interference_threshold); def("rxgain", _parent->rx_boosted_gain); def("fem_rxgain", _parent->radio_fem_rxgain); @@ -111,6 +123,18 @@ class NodePrefs : public ConfigSerializer { void setAirtimeFactor(float af) override { _parent->airtime_factor = af; markDirty(); } bool isCadEnabled() const override { return _parent->cad_enabled; } void setCadEnabled(bool en) override { _parent->cad_enabled = en; markDirty(); } + bool isRssiLbtEnabled() const override { return _parent->rssi_lbt_enabled; } + void setRssiLbtEnabled(bool en) override { _parent->rssi_lbt_enabled = en; markDirty(); } + int8_t getRssiLbtThrDbm() const override { return _parent->rssi_lbt_thr_dbm; } + void setRssiLbtThrDbm(int8_t dbm) override { _parent->rssi_lbt_thr_dbm = dbm; markDirty(); } + uint16_t getRssiLbtSenseMs() const override { return _parent->rssi_lbt_sense_ms; } + void setRssiLbtSenseMs(uint16_t ms) override { _parent->rssi_lbt_sense_ms = ms; markDirty(); } + uint16_t getRssiLbtMaxwaitMs() const override { return _parent->rssi_lbt_maxwait_ms; } + void setRssiLbtMaxwaitMs(uint16_t ms) override { _parent->rssi_lbt_maxwait_ms = ms; markDirty(); } + uint16_t getRssiLbtTxmaxMs() const override { return _parent->rssi_lbt_txmax_ms; } + void setRssiLbtTxmaxMs(uint16_t ms) override { _parent->rssi_lbt_txmax_ms = ms; markDirty(); } + uint16_t getRssiLbtPauseMs() const override { return _parent->rssi_lbt_pause_ms; } + void setRssiLbtPauseMs(uint16_t ms) override { _parent->rssi_lbt_pause_ms = ms; markDirty(); } uint8_t getIntThresh() const override { return _parent->interference_threshold; } void setIntThresh(uint8_t t) override { _parent->interference_threshold = t; markDirty(); } uint8_t getRxGain() const override { return _parent->rx_boosted_gain; } diff --git a/src/helpers/CommonRadioPrefs.cpp b/src/helpers/CommonRadioPrefs.cpp index a25df88069..e87fb6ca3e 100644 --- a/src/helpers/CommonRadioPrefs.cpp +++ b/src/helpers/CommonRadioPrefs.cpp @@ -113,6 +113,45 @@ bool CommonRadioPrefs::handleCommand(const char* command, uint32_t sender_timest return true; } + if (strcmp(command, "get rssi.lbt") == 0) { + sprintf(reply, "> %s", isRssiLbtEnabled() ? "on" : "off"); + return true; + } + if (memcmp(command, "set rssi.lbt ", 13) == 0) { + setRssiLbtEnabled(memcmp(&command[13], "on", 2) == 0); + strcpy(reply, "OK"); + return true; + } + + if (strcmp(command, "get rssi.lbt.params") == 0) { + sprintf(reply, "> %d,%d,%d,%d,%d", (int32_t) getRssiLbtThrDbm(), (uint32_t) getRssiLbtSenseMs(), + (uint32_t) getRssiLbtMaxwaitMs(), (uint32_t) getRssiLbtTxmaxMs(), (uint32_t) getRssiLbtPauseMs()); + return true; + } + if (memcmp(command, "set rssi.lbt.params ", 20) == 0) { + char tmp[132]; + strcpy(tmp, &command[20]); + const char *parts[5]; + int num = mesh::Utils::parseTextParts(tmp, parts, 5); + int32_t thr = num > 0 ? atol(parts[0]) : 1; // out of range, so a short command is rejected below + int32_t sense = num > 1 ? atol(parts[1]) : -1; + int32_t maxwait = num > 2 ? atol(parts[2]) : -1; + int32_t txmax = num > 3 ? atol(parts[3]) : -1; + int32_t pause = num > 4 ? atol(parts[4]) : -1; + if (thr >= -128 && thr <= 0 && sense >= 0 && sense <= 65535 && maxwait >= 0 && maxwait <= 65535 + && txmax >= 0 && txmax <= 65535 && pause >= 0 && pause <= 65535) { + setRssiLbtThrDbm((int8_t) thr); + setRssiLbtSenseMs((uint16_t) sense); + setRssiLbtMaxwaitMs((uint16_t) maxwait); + setRssiLbtTxmaxMs((uint16_t) txmax); + setRssiLbtPauseMs((uint16_t) pause); + strcpy(reply, "OK"); + } else { + strcpy(reply, "Error, invalid rssi.lbt params"); + } + return true; + } + if (strcmp(command, "get radio.rxgain") == 0) { sprintf(reply, "> %s", getRxGain() != 0 ? "on" : "off"); return true; diff --git a/src/helpers/CommonRadioPrefs.h b/src/helpers/CommonRadioPrefs.h index 96895bb2eb..81ccf3fbd9 100644 --- a/src/helpers/CommonRadioPrefs.h +++ b/src/helpers/CommonRadioPrefs.h @@ -29,6 +29,24 @@ class CommonRadioPrefs : public ConfigSerializer, public KeyValueStore { virtual bool isCadEnabled() const = 0; virtual void setCadEnabled(bool en) = 0; + virtual bool isRssiLbtEnabled() const = 0; + virtual void setRssiLbtEnabled(bool en) = 0; + + virtual int8_t getRssiLbtThrDbm() const = 0; + virtual void setRssiLbtThrDbm(int8_t dbm) = 0; + + virtual uint16_t getRssiLbtSenseMs() const = 0; + virtual void setRssiLbtSenseMs(uint16_t ms) = 0; + + virtual uint16_t getRssiLbtMaxwaitMs() const = 0; + virtual void setRssiLbtMaxwaitMs(uint16_t ms) = 0; + + virtual uint16_t getRssiLbtTxmaxMs() const = 0; + virtual void setRssiLbtTxmaxMs(uint16_t ms) = 0; + + virtual uint16_t getRssiLbtPauseMs() const = 0; + virtual void setRssiLbtPauseMs(uint16_t ms) = 0; + virtual uint8_t getIntThresh() const = 0; virtual void setIntThresh(uint8_t t) = 0; From b8b236dad2f98c4e00ee75ef1bb412599a2f277b Mon Sep 17 00:00:00 2001 From: jirogit Date: Wed, 9 Sep 2026 00:20:45 -0700 Subject: [PATCH 3/4] Enforce the RSSI listen-before-talk transmit limits Two dispatcher-side limits, both gated on rssi.lbt being enabled so that the default-off path is byte-for-byte the previous behaviour: txmax_ms caps the airtime of a single transmit. A packet whose estimated airtime exceeds the cap is dropped just before startSendRaw(), reusing the existing send-failure cleanup. There is no counter for this; the drop is only visible through MESH_DEBUG_PRINTLN, so release builds stay silent. maxwait_ms feeds getCADFailMaxDuration(), which gates the same busy check in checkSend() as CAD's own fixed 4000ms wait. With rssi.lbt off, the function is untouched -- still a hard-coded 4000ms. With rssi.lbt on, its max-wait applies; if CAD is also enabled, the stricter (longer) of the two wins, since CAD's fixed wait must never shorten a wait rssi.lbt asked to be longer. A max-wait of zero means "never force the transmit", which is stricter than any finite value, hence the normalisation to the largest representable duration before comparing. The effective value is recomputed on every call rather than stored, so a derived number can never end up in prefs and disagree with what the CLI reports. --- src/Dispatcher.cpp | 26 +++++++++++++++++++++++++- 1 file changed, 25 insertions(+), 1 deletion(-) diff --git a/src/Dispatcher.cpp b/src/Dispatcher.cpp index 39a395d8b9..c5f2a25352 100644 --- a/src/Dispatcher.cpp +++ b/src/Dispatcher.cpp @@ -59,8 +59,21 @@ int Dispatcher::calcRxDelay(float score, uint32_t air_time) const { uint32_t Dispatcher::getCADFailRetryDelay() const { return 200; } +// a max-wait of zero means 'unlimited', which is the strictest setting of all, so normalise it +// to the largest representable duration before any comparison. +static uint32_t normMaxWait(uint16_t ms) { + return ms == 0 ? 0xFFFFFFFFUL : (uint32_t) ms; +} + uint32_t Dispatcher::getCADFailMaxDuration() const { - return 4000; // 4 seconds + // rssi.lbt is the only mechanism with a configurable max-wait; CAD's is the fixed upstream + // 4000ms. With rssi.lbt off this is byte-for-byte the original hard-coded behaviour. With + // both active, the stricter (longer) of the two wins -- CAD's fixed wait never gets to + // shorten a max-wait that rssi.lbt asked to be longer. + if (!getRssiLbtEnabled()) return 4000; // 4 seconds, upstream default, untouched + uint32_t rssi = normMaxWait(getRssiLbtMaxwaitMs()); + if (!getCADEnabled()) return rssi; + return rssi > 4000 ? rssi : 4000; } void Dispatcher::loop() { @@ -325,6 +338,17 @@ void Dispatcher::checkSend() { } else { memcpy(&raw[len], outbound->payload, outbound->payload_len); len += outbound->payload_len; + uint16_t txmax = getRssiLbtTxmaxMs(); + if (getRssiLbtEnabled() && txmax != 0 && _radio->getEstAirtimeFor(len) > txmax) { + MESH_DEBUG_PRINTLN("%s Dispatcher::checkSend(): packet airtime exceeds rssi.lbt txmax, dropping, len=%d", getLogDateTime(), len); + + logTxFail(outbound, outbound->getRawLength()); + + releasePacket(outbound); // return to pool + outbound = NULL; + return; + } + uint32_t max_airtime = _radio->getEstAirtimeFor(len)*3/2; outbound_start = _ms->getMillis(); bool success = _radio->startSendRaw(raw, len); From 3abf378406208760d11ead472b64987aa910e9ff Mon Sep 17 00:00:00 2001 From: jirogit Date: Wed, 9 Sep 2026 00:20:45 -0700 Subject: [PATCH 4/4] Document the RSSI listen-before-talk commands Follows the layout of the existing cad section. Spells out that rssi.lbt is independent of cad and int.thresh, that all of rssi.lbt.params is ignored while rssi.lbt is off, the 0-means-unlimited convention on each field that has it, and how its max-wait interacts with CAD's fixed 4000ms wait in getCADFailMaxDuration(). --- docs/cli_commands.md | 38 +++++++++++++++++++++++++++++++++++++- 1 file changed, 37 insertions(+), 1 deletion(-) diff --git a/docs/cli_commands.md b/docs/cli_commands.md index 8772b929fe..4ea744c65b 100644 --- a/docs/cli_commands.md +++ b/docs/cli_commands.md @@ -613,7 +613,7 @@ This document provides an overview of CLI commands that can be sent to MeshCore - `get cad` - `set cad ` -**Description:** When enabled, the radio performs a hardware Channel Activity Detection scan before transmitting and defers if the channel is busy. Runs independently of `int.thresh` — either, both, or none may be active. +**Description:** When enabled, the radio performs a hardware Channel Activity Detection scan before transmitting and defers if the channel is busy. Runs independently of `int.thresh` and `rssi.lbt` — any combination may be active. **Parameters:** - `on|off`: Enable or disable hardware CAD @@ -622,6 +622,42 @@ This document provides an overview of CLI commands that can be sent to MeshCore --- +#### Enable or disable RSSI listen-before-talk +**Usage:** +- `get rssi.lbt` +- `set rssi.lbt ` + +**Description:** When enabled, the radio samples RSSI continuously for a short window before transmitting and defers if any sample exceeds an absolute threshold. Unlike `cad`, which detects a LoRa preamble, this is energy detection and so is independent of whatever modulation is on air; unlike `int.thresh`, the threshold is absolute rather than relative to the tracked noise floor. + +Runs independently of `cad` and `int.thresh` — any combination may be active. When both `cad` and `rssi.lbt` are enabled, the stricter of CAD's fixed 4000ms wait and the `rssi.lbt` `maxwait_ms` applies. + +While this is `off`, every value in `rssi.lbt.params` is ignored and the node transmits exactly as it would with this feature absent. + +**Parameters:** +- `on|off`: Enable or disable RSSI listen-before-talk + +**Default:** `off` + +--- + +#### View or change the RSSI listen-before-talk parameters +**Usage:** +- `get rssi.lbt.params` +- `set rssi.lbt.params ,,,,` + +**Description:** All five values are set together and reported together, in the same order, so the output of `get rssi.lbt.params` can be pasted straight back into `set rssi.lbt.params`. None of them have any effect while `rssi.lbt` is `off`. + +**Parameters:** +- `thr_dBm`: Absolute RSSI threshold, `-128`-`0`. A sample above this marks the channel busy. +- `sense_ms`: How long to sample RSSI for, `0`-`65535`. `0` takes a single instantaneous reading. +- `maxwait_ms`: How long a busy channel may hold off a pending transmit before it is sent anyway, `0`-`65535`. `0` means unlimited — the transmit is never forced. +- `txmax_ms`: Maximum airtime of a single transmit, `0`-`65535`. A packet whose estimated airtime exceeds this is dropped rather than sent. `0` means unlimited. +- `pause_ms`: Quiet period enforced after each transmit, `0`-`65535`. + +**Default:** `-80,5,0,4000,50` + +--- + #### View or change the AGC Reset Interval **Usage:** - `get agc.reset.interval`