From c723da2cabca54ed6f26350ebd76b96304f1e26b Mon Sep 17 00:00:00 2001 From: nogood Date: Fri, 3 Jul 2026 03:31:44 +0300 Subject: [PATCH 1/3] Merged CAD support from original FW --- docs/cli_commands.md | 14 ++++++++++++++ examples/companion_radio/MyMesh.cpp | 4 ++++ examples/companion_radio/MyMesh.h | 1 + examples/simple_repeater/MyMesh.cpp | 1 + examples/simple_repeater/MyMesh.h | 3 +++ examples/simple_room_server/MyMesh.cpp | 1 + examples/simple_room_server/MyMesh.h | 3 +++ examples/simple_sensor/SensorMesh.cpp | 4 ++++ examples/simple_sensor/SensorMesh.h | 1 + src/Dispatcher.cpp | 1 + src/Dispatcher.h | 3 +++ src/helpers/CommonCLI.cpp | 11 +++++++++++ src/helpers/CommonCLI.h | 1 + src/helpers/radiolib/RadioLibWrappers.cpp | 23 ++++++++++++++++++++--- src/helpers/radiolib/RadioLibWrappers.h | 5 ++++- 15 files changed, 72 insertions(+), 4 deletions(-) diff --git a/docs/cli_commands.md b/docs/cli_commands.md index ce0e7da1..c06f5e12 100644 --- a/docs/cli_commands.md +++ b/docs/cli_commands.md @@ -578,6 +578,20 @@ This document provides an overview of CLI commands that can be sent to MeshCore --- +#### Enable or disable hardware Channel Activity Detection (CAD) +**Usage:** +- `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. + +**Parameters:** +- `on|off`: Enable or disable hardware CAD + +**Default:** `off` + +--- + #### View or change the AGC Reset Interval **Usage:** - `get agc.reset.interval` diff --git a/examples/companion_radio/MyMesh.cpp b/examples/companion_radio/MyMesh.cpp index 3e3ba811..2e18953d 100644 --- a/examples/companion_radio/MyMesh.cpp +++ b/examples/companion_radio/MyMesh.cpp @@ -331,6 +331,10 @@ int MyMesh::getInterferenceThreshold() const { return 0; // disabled for now, until currentRSSI() problem is resolved } +bool MyMesh::getCADEnabled() const { + return true; // hardware CAD before TX (no CLI toggle on companion; enabled by default) +} + int MyMesh::calcRxDelay(float score, uint32_t air_time) const { if (_prefs.rx_delay_base <= 0.0f) return 0; return (int)((pow(_prefs.rx_delay_base, 0.85f - score) - 1.0) * air_time); diff --git a/examples/companion_radio/MyMesh.h b/examples/companion_radio/MyMesh.h index b20d6fc5..01340b43 100644 --- a/examples/companion_radio/MyMesh.h +++ b/examples/companion_radio/MyMesh.h @@ -110,6 +110,7 @@ class MyMesh : public BaseChatMesh, public DataStoreHost { protected: float getAirtimeBudgetFactor() const override; int getInterferenceThreshold() const override; + bool getCADEnabled() const override; int calcRxDelay(float score, uint32_t air_time) const override; uint32_t getRetransmitDelay(const mesh::Packet *packet) override; uint32_t getDirectRetransmitDelay(const mesh::Packet *packet) override; diff --git a/examples/simple_repeater/MyMesh.cpp b/examples/simple_repeater/MyMesh.cpp index 4713dab4..452d3a15 100644 --- a/examples/simple_repeater/MyMesh.cpp +++ b/examples/simple_repeater/MyMesh.cpp @@ -1239,6 +1239,7 @@ MyMesh::MyMesh(mesh::MainBoard &board, mesh::Radio &radio, mesh::MillisecondCloc _prefs.flood_max_unscoped = 64; _prefs.flood_max_advert = 8; _prefs.interference_threshold = 0; // disabled + _prefs.cad_enabled = 0; // hardware CAD before TX (off by default; 'set cad on') // bridge defaults _prefs.bridge_enabled = 1; // enabled diff --git a/examples/simple_repeater/MyMesh.h b/examples/simple_repeater/MyMesh.h index 3cfea6dd..116864cf 100644 --- a/examples/simple_repeater/MyMesh.h +++ b/examples/simple_repeater/MyMesh.h @@ -233,6 +233,9 @@ class MyMesh : public mesh::Mesh, public CommonCLICallbacks, public WebPanelComm int getInterferenceThreshold() const override { return _prefs.interference_threshold; } + bool getCADEnabled() const override { + return _prefs.cad_enabled; + } int getAGCResetInterval() const override { return ((int)_prefs.agc_reset_interval) * 4000; // milliseconds } diff --git a/examples/simple_room_server/MyMesh.cpp b/examples/simple_room_server/MyMesh.cpp index 14694506..b409ae6e 100644 --- a/examples/simple_room_server/MyMesh.cpp +++ b/examples/simple_room_server/MyMesh.cpp @@ -650,6 +650,7 @@ MyMesh::MyMesh(mesh::MainBoard &board, mesh::Radio &radio, mesh::MillisecondCloc _prefs.flood_max_unscoped = 64; _prefs.flood_max_advert = 8; _prefs.interference_threshold = 0; // disabled + _prefs.cad_enabled = 0; // hardware CAD before TX (off by default; 'set cad on') #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 97e00065..51d79179 100644 --- a/examples/simple_room_server/MyMesh.h +++ b/examples/simple_room_server/MyMesh.h @@ -144,6 +144,9 @@ class MyMesh : public mesh::Mesh, public CommonCLICallbacks { int getInterferenceThreshold() const override { return _prefs.interference_threshold; } + bool getCADEnabled() const override { + return _prefs.cad_enabled; + } 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 5862441e..966a9ba5 100644 --- a/examples/simple_sensor/SensorMesh.cpp +++ b/examples/simple_sensor/SensorMesh.cpp @@ -323,6 +323,9 @@ uint32_t SensorMesh::getDirectRetransmitDelay(const mesh::Packet* packet) { int SensorMesh::getInterferenceThreshold() const { return _prefs.interference_threshold; } +bool SensorMesh::getCADEnabled() const { + return _prefs.cad_enabled; +} int SensorMesh::getAGCResetInterval() const { return ((int)_prefs.agc_reset_interval) * 4000; // milliseconds } @@ -726,6 +729,7 @@ SensorMesh::SensorMesh(mesh::MainBoard& board, mesh::Radio& radio, mesh::Millise _prefs.disable_fwd = true; _prefs.flood_max = 64; _prefs.interference_threshold = 0; // disabled + _prefs.cad_enabled = 0; // hardware CAD before TX (off by default; 'set cad on') // GPS defaults _prefs.gps_enabled = 0; diff --git a/examples/simple_sensor/SensorMesh.h b/examples/simple_sensor/SensorMesh.h index b9af6fa4..0402f71e 100644 --- a/examples/simple_sensor/SensorMesh.h +++ b/examples/simple_sensor/SensorMesh.h @@ -121,6 +121,7 @@ class SensorMesh : public mesh::Mesh, public CommonCLICallbacks { uint32_t getRetransmitDelay(const mesh::Packet* packet) override; uint32_t getDirectRetransmitDelay(const mesh::Packet* packet) override; int getInterferenceThreshold() const override; + bool getCADEnabled() 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 0e46ad96..98cb5d41 100644 --- a/src/Dispatcher.cpp +++ b/src/Dispatcher.cpp @@ -66,6 +66,7 @@ uint32_t Dispatcher::getCADFailMaxDuration() const { void Dispatcher::loop() { if (millisHasNowPassed(next_floor_calib_time)) { _radio->triggerNoiseFloorCalibrate(getInterferenceThreshold()); + _radio->setCADEnabled(getCADEnabled()); next_floor_calib_time = futureMillis(NOISE_FLOOR_CALIB_INTERVAL); } _radio->loop(); diff --git a/src/Dispatcher.h b/src/Dispatcher.h index dd032f13..aad6cba3 100644 --- a/src/Dispatcher.h +++ b/src/Dispatcher.h @@ -65,6 +65,8 @@ class Radio { virtual void triggerNoiseFloorCalibrate(int threshold) { } + virtual void setCADEnabled(bool enable) { } + virtual void resetAGC() { } virtual bool isInRecvMode() const = 0; @@ -166,6 +168,7 @@ class Dispatcher { virtual uint32_t getCADFailRetryDelay() const; 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 int getAGCResetInterval() const { return 0; } // disabled by default virtual unsigned long getDutyCycleWindowMs() const { return 3600000; } diff --git a/src/helpers/CommonCLI.cpp b/src/helpers/CommonCLI.cpp index 9e011864..766f576c 100644 --- a/src/helpers/CommonCLI.cpp +++ b/src/helpers/CommonCLI.cpp @@ -100,6 +100,9 @@ void CommonCLI::loadPrefsInt(FILESYSTEM* fs, const char* filename) { if (file.available() >= (int)sizeof(_prefs->fan_timeout_secs)) { file.read((uint8_t *)&_prefs->fan_timeout_secs, sizeof(_prefs->fan_timeout_secs)); // 293 } + if (file.available() >= (int)sizeof(_prefs->cad_enabled)) { + file.read((uint8_t *)&_prefs->cad_enabled, sizeof(_prefs->cad_enabled)); // 294 + } if (file.available() >= (int)sizeof(_prefs->flood_max_unscoped)) { file.read((uint8_t *)&_prefs->flood_max_unscoped, sizeof(_prefs->flood_max_unscoped)); // 295 } @@ -140,6 +143,7 @@ void CommonCLI::loadPrefsInt(FILESYSTEM* fs, const char* filename) { // sanitise settings _prefs->rx_boosted_gain = constrain(_prefs->rx_boosted_gain, 0, 1); // boolean _prefs->radio_fem_rxgain = constrain(_prefs->radio_fem_rxgain, 0, 1); // boolean + _prefs->cad_enabled = constrain(_prefs->cad_enabled, 0, 1); // boolean _prefs->fan_mode = constrain(_prefs->fan_mode, 0, 2); _prefs->fan_timeout_secs = constrain(_prefs->fan_timeout_secs, 0, 600); @@ -207,6 +211,7 @@ void CommonCLI::savePrefs(FILESYSTEM* fs) { file.write((uint8_t *)&_prefs->rx_boosted_gain, sizeof(_prefs->rx_boosted_gain)); // 291 file.write((uint8_t *)&_prefs->fan_mode, sizeof(_prefs->fan_mode)); // 292 file.write((uint8_t *)&_prefs->fan_timeout_secs, sizeof(_prefs->fan_timeout_secs)); // 293 + file.write((uint8_t *)&_prefs->cad_enabled, sizeof(_prefs->cad_enabled)); // 294 file.write((uint8_t *)&_prefs->flood_max_unscoped, sizeof(_prefs->flood_max_unscoped)); // 295 file.write((uint8_t *)&_prefs->flood_max_advert, sizeof(_prefs->flood_max_advert)); // 296 file.write((uint8_t *)&_prefs->radio_fem_rxgain, sizeof(_prefs->radio_fem_rxgain)); // 297 @@ -535,6 +540,10 @@ void CommonCLI::handleSetCmd(uint32_t sender_timestamp, char* command, char* rep _prefs->interference_threshold = atoi(&config[11]); savePrefs(); strcpy(reply, "OK"); + } else if (memcmp(config, "cad ", 4) == 0) { + _prefs->cad_enabled = memcmp(&config[4], "on", 2) == 0; + savePrefs(); + strcpy(reply, "OK"); } else if (memcmp(config, "agc.reset.interval ", 19) == 0) { _prefs->agc_reset_interval = atoi(&config[19]) / 4; savePrefs(); @@ -833,6 +842,8 @@ void CommonCLI::handleGetCmd(uint32_t sender_timestamp, char* command, char* rep sprintf(reply, "> %s", StrHelper::ftoa(_prefs->airtime_factor)); } else if (memcmp(config, "int.thresh", 10) == 0) { sprintf(reply, "> %d", (uint32_t) _prefs->interference_threshold); + } else if (memcmp(config, "cad", 3) == 0) { + sprintf(reply, "> %s", _prefs->cad_enabled ? "on" : "off"); } else if (memcmp(config, "agc.reset.interval", 18) == 0) { sprintf(reply, "> %d", ((uint32_t) _prefs->agc_reset_interval) * 4); } else if (memcmp(config, "multi.acks", 10) == 0) { diff --git a/src/helpers/CommonCLI.h b/src/helpers/CommonCLI.h index edda1fa4..d65fa03b 100644 --- a/src/helpers/CommonCLI.h +++ b/src/helpers/CommonCLI.h @@ -65,6 +65,7 @@ struct NodePrefs { // persisted to file uint8_t radio_fem_rxgain; // LoRa FEM RX gain setting uint8_t path_hash_mode; // which path mode to use when sending uint8_t loop_detect; + uint8_t cad_enabled; // hardware Channel Activity Detection before TX (boolean) uint8_t fan_mode; uint16_t fan_timeout_secs; }; diff --git a/src/helpers/radiolib/RadioLibWrappers.cpp b/src/helpers/radiolib/RadioLibWrappers.cpp index b6519aef..5e72336c 100644 --- a/src/helpers/radiolib/RadioLibWrappers.cpp +++ b/src/helpers/radiolib/RadioLibWrappers.cpp @@ -36,6 +36,7 @@ void RadioLibWrapper::begin() { _noise_floor = 0; _threshold = 0; + _cad_enabled = false; // start average out some samples _num_floor_samples = 0; @@ -178,10 +179,26 @@ void RadioLibWrapper::onSendFinished() { 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; + // int.thresh: RSSI-based interference detection (relative to noise floor) + if (_threshold != 0 && getCurrentRSSI() > _noise_floor + _threshold) return true; + + // cad: hardware channel activity detection + if (_cad_enabled) { + int16_t result = performChannelScan(); + // scanChannel() triggers DIO interrupt (CAD done) which sets STATE_INT_READY + // via setFlag() ISR. Clear it before restarting RX so recvRaw() doesn't + // try to read a non-existent packet and count a spurious recv error. + state = STATE_IDLE; + startRecv(); + if (result != RADIOLIB_CHANNEL_FREE) return true; + } + + return false; } float RadioLibWrapper::getLastRSSI() const { diff --git a/src/helpers/radiolib/RadioLibWrappers.h b/src/helpers/radiolib/RadioLibWrappers.h index efd3e179..9943bcab 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; + bool _cad_enabled; 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(); @@ -46,9 +47,11 @@ 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 int16_t performChannelScan(); int getNoiseFloor() const override { return _noise_floor; } void triggerNoiseFloorCalibrate(int threshold) override; + void setCADEnabled(bool enable) override { _cad_enabled = enable; } void resetAGC() override; void loop() override; From d05071f4f49c2985d9e138d1084dc086b1c75584 Mon Sep 17 00:00:00 2001 From: nogood Date: Tue, 7 Jul 2026 18:17:01 +0300 Subject: [PATCH 2/3] fixed_merge_cad_support_from_original_fw --- src/helpers/CommonCLI.cpp | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/helpers/CommonCLI.cpp b/src/helpers/CommonCLI.cpp index 766f576c..fc7c3903 100644 --- a/src/helpers/CommonCLI.cpp +++ b/src/helpers/CommonCLI.cpp @@ -211,11 +211,11 @@ void CommonCLI::savePrefs(FILESYSTEM* fs) { file.write((uint8_t *)&_prefs->rx_boosted_gain, sizeof(_prefs->rx_boosted_gain)); // 291 file.write((uint8_t *)&_prefs->fan_mode, sizeof(_prefs->fan_mode)); // 292 file.write((uint8_t *)&_prefs->fan_timeout_secs, sizeof(_prefs->fan_timeout_secs)); // 293 - file.write((uint8_t *)&_prefs->cad_enabled, sizeof(_prefs->cad_enabled)); // 294 file.write((uint8_t *)&_prefs->flood_max_unscoped, sizeof(_prefs->flood_max_unscoped)); // 295 file.write((uint8_t *)&_prefs->flood_max_advert, sizeof(_prefs->flood_max_advert)); // 296 file.write((uint8_t *)&_prefs->radio_fem_rxgain, sizeof(_prefs->radio_fem_rxgain)); // 297 - // next: 298 + file.write((uint8_t *)&_prefs->cad_enabled, sizeof(_prefs->cad_enabled)); // 298 + // next: 299 file.close(); } From 4f2f9bc8e9a809317c8479a97190e85fb5c6a9eb Mon Sep 17 00:00:00 2001 From: nogood Date: Wed, 8 Jul 2026 18:51:51 +0300 Subject: [PATCH 3/3] fixed_read_cad_enabled --- src/helpers/CommonCLI.cpp | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/src/helpers/CommonCLI.cpp b/src/helpers/CommonCLI.cpp index fc7c3903..cfac3463 100644 --- a/src/helpers/CommonCLI.cpp +++ b/src/helpers/CommonCLI.cpp @@ -100,9 +100,6 @@ void CommonCLI::loadPrefsInt(FILESYSTEM* fs, const char* filename) { if (file.available() >= (int)sizeof(_prefs->fan_timeout_secs)) { file.read((uint8_t *)&_prefs->fan_timeout_secs, sizeof(_prefs->fan_timeout_secs)); // 293 } - if (file.available() >= (int)sizeof(_prefs->cad_enabled)) { - file.read((uint8_t *)&_prefs->cad_enabled, sizeof(_prefs->cad_enabled)); // 294 - } if (file.available() >= (int)sizeof(_prefs->flood_max_unscoped)) { file.read((uint8_t *)&_prefs->flood_max_unscoped, sizeof(_prefs->flood_max_unscoped)); // 295 } @@ -112,7 +109,10 @@ void CommonCLI::loadPrefsInt(FILESYSTEM* fs, const char* filename) { if (file.available() >= (int)sizeof(_prefs->radio_fem_rxgain)) { file.read((uint8_t *)&_prefs->radio_fem_rxgain, sizeof(_prefs->radio_fem_rxgain)); // 297 } - // next: 298 + if (file.available() >= (int)sizeof(_prefs->cad_enabled)) { + file.read((uint8_t *)&_prefs->cad_enabled, sizeof(_prefs->cad_enabled)); // 298 + } + // next: 299 // sanitise bad pref values _prefs->rx_delay_base = constrain(_prefs->rx_delay_base, 0, 20.0f);