From 806dcf88e428a2aaf9bd3c70cc179730f328957c Mon Sep 17 00:00:00 2001
From: Wessel Nieboer <wessel@weebl.me>
Date: Wed, 18 Feb 2026 01:16:52 +0100
Subject: [PATCH 1/5] Use hardware channel activity detection for checking
 interference

---
 examples/companion_radio/MyMesh.cpp       |  2 +-
 examples/simple_repeater/MyMesh.cpp       |  2 +-
 examples/simple_room_server/MyMesh.cpp    |  2 +-
 examples/simple_sensor/SensorMesh.cpp     |  2 +-
 src/helpers/radiolib/RadioLibWrappers.cpp | 13 ++++++++++---
 src/helpers/radiolib/RadioLibWrappers.h   |  3 ++-
 6 files changed, 16 insertions(+), 8 deletions(-)

diff --git a/examples/companion_radio/MyMesh.cpp b/examples/companion_radio/MyMesh.cpp
index 6fbb0f7428..a3d7e01d85 100644
--- a/examples/companion_radio/MyMesh.cpp
+++ b/examples/companion_radio/MyMesh.cpp
@@ -259,7 +259,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/simple_repeater/MyMesh.cpp b/examples/simple_repeater/MyMesh.cpp
index 096907494b..0b2966d4b0 100644
--- a/examples/simple_repeater/MyMesh.cpp
+++ b/examples/simple_repeater/MyMesh.cpp
@@ -892,7 +892,7 @@ MyMesh::MyMesh(mesh::MainBoard &board, mesh::Radio &radio, mesh::MillisecondCloc
   _prefs.flood_max = 64;
   _prefs.flood_max_unscoped = 64;
   _prefs.flood_max_advert = 8;
-  _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_room_server/MyMesh.cpp b/examples/simple_room_server/MyMesh.cpp
index 98b22fdb72..77bb0cfb54 100644
--- a/examples/simple_room_server/MyMesh.cpp
+++ b/examples/simple_room_server/MyMesh.cpp
@@ -649,7 +649,7 @@ MyMesh::MyMesh(mesh::MainBoard &board, mesh::Radio &radio, mesh::MillisecondCloc
   _prefs.flood_max = 64;
   _prefs.flood_max_unscoped = 64;
   _prefs.flood_max_advert = 8;
-  _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_sensor/SensorMesh.cpp b/examples/simple_sensor/SensorMesh.cpp
index 879fcbf026..38ae3cf5de 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/src/helpers/radiolib/RadioLibWrappers.cpp b/src/helpers/radiolib/RadioLibWrappers.cpp
index b6519aefa7..c67ab768c5 100644
--- a/src/helpers/radiolib/RadioLibWrappers.cpp
+++ b/src/helpers/radiolib/RadioLibWrappers.cpp
@@ -178,10 +178,17 @@ 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;
+  if (_threshold == 0) return false;    // interference check is disabled
+
+  int16_t result = performChannelScan();
+  // scanChannel() leaves radio in standby — restart RX regardless of result
+  startRecv();
+  return (result == RADIOLIB_LORA_DETECTED);
 }
 
 float RadioLibWrapper::getLastRSSI() const {
diff --git a/src/helpers/radiolib/RadioLibWrappers.h b/src/helpers/radiolib/RadioLibWrappers.h
index efd3e17931..c1ece6448b 100644
--- a/src/helpers/radiolib/RadioLibWrappers.h
+++ b/src/helpers/radiolib/RadioLibWrappers.h
@@ -32,7 +32,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,6 +46,7 @@ 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;

From e93a51418cfd9f2f496d0e4a0b81f218021f5bc7 Mon Sep 17 00:00:00 2001
From: Wessel Nieboer <github@weebl.me>
Date: Sun, 22 Feb 2026 16:05:18 +0100
Subject: [PATCH 2/5] Also return busy if preamble detected

---
 src/helpers/radiolib/RadioLibWrappers.cpp | 2 +-
 1 file changed, 1 insertion(+), 1 deletion(-)

diff --git a/src/helpers/radiolib/RadioLibWrappers.cpp b/src/helpers/radiolib/RadioLibWrappers.cpp
index c67ab768c5..9b00b176bb 100644
--- a/src/helpers/radiolib/RadioLibWrappers.cpp
+++ b/src/helpers/radiolib/RadioLibWrappers.cpp
@@ -188,7 +188,7 @@ bool RadioLibWrapper::isChannelActive() {
   int16_t result = performChannelScan();
   // scanChannel() leaves radio in standby — restart RX regardless of result
   startRecv();
-  return (result == RADIOLIB_LORA_DETECTED);
+  return (result == RADIOLIB_LORA_DETECTED || result == RADIOLIB_PREAMBLE_DETECTED);
 }
 
 float RadioLibWrapper::getLastRSSI() const {

From 4272e80ce905911f8df47bdc8696a0e912884ea9 Mon Sep 17 00:00:00 2001
From: Wessel Nieboer <github@weebl.me>
Date: Sun, 22 Feb 2026 16:08:04 +0100
Subject: [PATCH 3/5] Just check for not channel free

---
 src/helpers/radiolib/RadioLibWrappers.cpp | 2 +-
 1 file changed, 1 insertion(+), 1 deletion(-)

diff --git a/src/helpers/radiolib/RadioLibWrappers.cpp b/src/helpers/radiolib/RadioLibWrappers.cpp
index 9b00b176bb..d50c2bace7 100644
--- a/src/helpers/radiolib/RadioLibWrappers.cpp
+++ b/src/helpers/radiolib/RadioLibWrappers.cpp
@@ -188,7 +188,7 @@ bool RadioLibWrapper::isChannelActive() {
   int16_t result = performChannelScan();
   // scanChannel() leaves radio in standby — restart RX regardless of result
   startRecv();
-  return (result == RADIOLIB_LORA_DETECTED || result == RADIOLIB_PREAMBLE_DETECTED);
+  return result != RADIOLIB_CHANNEL_FREE;
 }
 
 float RadioLibWrapper::getLastRSSI() const {

From 67b7205d37fd6f02bb8991b9567425b8cfe61757 Mon Sep 17 00:00:00 2001
From: Wessel Nieboer <github@weebl.me>
Date: Fri, 6 Mar 2026 04:09:05 +0100
Subject: [PATCH 4/5] Prevent packet errors from growing

---
 src/helpers/radiolib/RadioLibWrappers.cpp | 5 ++++-
 1 file changed, 4 insertions(+), 1 deletion(-)

diff --git a/src/helpers/radiolib/RadioLibWrappers.cpp b/src/helpers/radiolib/RadioLibWrappers.cpp
index d50c2bace7..5d68d7e90e 100644
--- a/src/helpers/radiolib/RadioLibWrappers.cpp
+++ b/src/helpers/radiolib/RadioLibWrappers.cpp
@@ -186,7 +186,10 @@ bool RadioLibWrapper::isChannelActive() {
   if (_threshold == 0) return false;    // interference check is disabled
 
   int16_t result = performChannelScan();
-  // scanChannel() leaves radio in standby — restart RX regardless of result
+  // 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();
   return result != RADIOLIB_CHANNEL_FREE;
 }

From 681b4eb9b513a8fa5379bffc0fa60ee85881baf9 Mon Sep 17 00:00:00 2001
From: Wessel Nieboer <wessel@weebl.me>
Date: Wed, 3 Jun 2026 10:13:10 +0200
Subject: [PATCH 5/5] Have CAD be a separate toggle (set cad on/off)

---
 docs/cli_commands.md                      | 14 ++++++++++++++
 examples/companion_radio/MyMesh.cpp       |  5 ++++-
 examples/companion_radio/MyMesh.h         |  1 +
 examples/simple_repeater/MyMesh.cpp       |  3 ++-
 examples/simple_repeater/MyMesh.h         |  3 +++
 examples/simple_room_server/MyMesh.cpp    |  3 ++-
 examples/simple_room_server/MyMesh.h      |  3 +++
 examples/simple_sensor/SensorMesh.cpp     |  6 +++++-
 examples/simple_sensor/SensorMesh.h       |  1 +
 src/Dispatcher.cpp                        |  1 +
 src/Dispatcher.h                          |  3 +++
 src/helpers/CommonCLI.cpp                 | 13 +++++++++++--
 src/helpers/CommonCLI.h                   |  1 +
 src/helpers/radiolib/RadioLibWrappers.cpp | 23 +++++++++++++++--------
 src/helpers/radiolib/RadioLibWrappers.h   |  2 ++
 15 files changed, 68 insertions(+), 14 deletions(-)

diff --git a/docs/cli_commands.md b/docs/cli_commands.md
index 4a203f160a..4ea969f90e 100644
--- a/docs/cli_commands.md
+++ b/docs/cli_commands.md
@@ -559,6 +559,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 <on|off>`
+
+**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 a3d7e01d85..a0e368587d 100644
--- a/examples/companion_radio/MyMesh.cpp
+++ b/examples/companion_radio/MyMesh.cpp
@@ -259,7 +259,10 @@ float MyMesh::getAirtimeBudgetFactor() const {
 }
 
 int MyMesh::getInterferenceThreshold() const {
-  return 1; // non-zero enables hardware CAD (Channel Activity Detection) before TX
+  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 {
diff --git a/examples/companion_radio/MyMesh.h b/examples/companion_radio/MyMesh.h
index 43d3950beb..f4190f30ac 100644
--- a/examples/companion_radio/MyMesh.h
+++ b/examples/companion_radio/MyMesh.h
@@ -105,6 +105,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 0b2966d4b0..e2d32dba2f 100644
--- a/examples/simple_repeater/MyMesh.cpp
+++ b/examples/simple_repeater/MyMesh.cpp
@@ -892,7 +892,8 @@ MyMesh::MyMesh(mesh::MainBoard &board, mesh::Radio &radio, mesh::MillisecondCloc
   _prefs.flood_max = 64;
   _prefs.flood_max_unscoped = 64;
   _prefs.flood_max_advert = 8;
-  _prefs.interference_threshold = 1; // non-zero enables hardware CAD before TX
+  _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 7597c6c6f6..24c4b1f2a7 100644
--- a/examples/simple_repeater/MyMesh.h
+++ b/examples/simple_repeater/MyMesh.h
@@ -150,6 +150,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_room_server/MyMesh.cpp b/examples/simple_room_server/MyMesh.cpp
index 77bb0cfb54..0e13b5483b 100644
--- a/examples/simple_room_server/MyMesh.cpp
+++ b/examples/simple_room_server/MyMesh.cpp
@@ -649,7 +649,8 @@ MyMesh::MyMesh(mesh::MainBoard &board, mesh::Radio &radio, mesh::MillisecondCloc
   _prefs.flood_max = 64;
   _prefs.flood_max_unscoped = 64;
   _prefs.flood_max_advert = 8;
-  _prefs.interference_threshold = 1; // non-zero enables hardware CAD before TX
+  _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 5277ddad61..e9e53ec919 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 38ae3cf5de..055c579dc1 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
 }
@@ -725,7 +728,8 @@ 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 = 1;  // non-zero enables hardware CAD before TX
+  _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 c9f135f65e..1d65b8772b 100644
--- a/examples/simple_sensor/SensorMesh.h
+++ b/examples/simple_sensor/SensorMesh.h
@@ -120,6 +120,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 9d7a11131d..c0610b7f8a 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 dd032f130d..aad6cba3ec 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 b78ad6ebd6..be1014c84a 100644
--- a/src/helpers/CommonCLI.cpp
+++ b/src/helpers/CommonCLI.cpp
@@ -91,7 +91,8 @@ void CommonCLI::loadPrefsInt(FILESYSTEM* fs, const char* filename) {
     file.read((uint8_t *)&_prefs->rx_boosted_gain, sizeof(_prefs->rx_boosted_gain));              // 290
     file.read((uint8_t *)&_prefs->flood_max_unscoped, sizeof(_prefs->flood_max_unscoped));   // 291
     file.read((uint8_t *)&_prefs->flood_max_advert, sizeof(_prefs->flood_max_advert));       // 292
-    // next: 293
+    file.read((uint8_t *)&_prefs->cad_enabled, sizeof(_prefs->cad_enabled));                  // 293
+    // next: 294
 
     // sanitise bad pref values
     _prefs->rx_delay_base = constrain(_prefs->rx_delay_base, 0, 20.0f);
@@ -121,6 +122,7 @@ void CommonCLI::loadPrefsInt(FILESYSTEM* fs, const char* filename) {
 
     // sanitise settings
     _prefs->rx_boosted_gain = constrain(_prefs->rx_boosted_gain, 0, 1); // boolean
+    _prefs->cad_enabled = constrain(_prefs->cad_enabled, 0, 1); // boolean
 
     file.close();
   }
@@ -184,7 +186,8 @@ void CommonCLI::savePrefs(FILESYSTEM* fs) {
     file.write((uint8_t *)&_prefs->rx_boosted_gain, sizeof(_prefs->rx_boosted_gain));              // 290
     file.write((uint8_t *)&_prefs->flood_max_unscoped, sizeof(_prefs->flood_max_unscoped));   // 291
     file.write((uint8_t *)&_prefs->flood_max_advert, sizeof(_prefs->flood_max_advert));       // 292
-    // next: 293
+    file.write((uint8_t *)&_prefs->cad_enabled, sizeof(_prefs->cad_enabled));                 // 293
+    // next: 294
 
     file.close();
   }
@@ -500,6 +503,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();
@@ -776,6 +783,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 b509c2b31a..64311e4a78 100644
--- a/src/helpers/CommonCLI.h
+++ b/src/helpers/CommonCLI.h
@@ -63,6 +63,7 @@ struct NodePrefs { // persisted to file
   uint8_t rx_boosted_gain; // power settings
   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)
 };
 
 class CommonCLICallbacks {
diff --git a/src/helpers/radiolib/RadioLibWrappers.cpp b/src/helpers/radiolib/RadioLibWrappers.cpp
index 5d68d7e90e..5e72336c05 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;
@@ -183,15 +184,21 @@ int16_t RadioLibWrapper::performChannelScan() {
 }
 
 bool RadioLibWrapper::isChannelActive() {
-  if (_threshold == 0) return false;    // interference check is disabled
+  // 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;
+  }
 
-  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();
-  return result != RADIOLIB_CHANNEL_FREE;
+  return false;
 }
 
 float RadioLibWrapper::getLastRSSI() const {
diff --git a/src/helpers/radiolib/RadioLibWrappers.h b/src/helpers/radiolib/RadioLibWrappers.h
index c1ece6448b..9943bcab77 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;
@@ -50,6 +51,7 @@ 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 resetAGC() override;
 
   void loop() override;
