Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
32 commits
Select commit Hold shift + click to select a range
8435464
Add v4 FEM LNA CLI control commands.
Quency-D Mar 24, 2026
65752fe
Fix the memory leak issue in the strdup function.
Quency-D Mar 24, 2026
2442e9a
Adapt LNA CLI control commands for heltec_tracker_v2.
Quency-D Mar 24, 2026
9664305
Adapt LNA CLI control commands for heltec_t096.
Quency-D Mar 24, 2026
ddedb3c
Merge branch 'dev' into cli-lna-command
Quency-D Apr 25, 2026
ca047fe
Merge branch 'meshcore-dev:main' into cli-lna-command
Quency-D Apr 25, 2026
22f07b3
Revert "Merge branch 'meshcore-dev:main' into cli-lna-command"
Quency-D Apr 25, 2026
c42f6db
Revise according to the review comments.
Quency-D Apr 25, 2026
9d26953
Remove unnecessary blank line in MyMesh.cpp
Quency-D Apr 25, 2026
62b0d82
Restore WiFi operation command scope comments
Quency-D Apr 25, 2026
444dcfb
Change write to read for radio_fem_rxgain
Quency-D Apr 27, 2026
12e6899
Merge branch 'dev' into cli-lna-command
Quency-D May 9, 2026
250f448
Merge branch 'dev' into cli-lna-command
Quency-D May 20, 2026
07bfe90
free packet on parse failure
liamcottle Jun 8, 2026
ae0bb7e
use releasePacket instead of _mgr->free
liamcottle Jun 8, 2026
5f3b7f2
Merge pull request #2722 from liamcottle/fix/free-packet-on-parse-fai…
ripplebiz Jun 8, 2026
9100a58
Merge branch 'dev' into cli-lna-command
Quency-D Jun 10, 2026
d5f74e9
* PAYLOAD_TYPE_PATH bad path_len now rejected
ripplebiz Jun 13, 2026
3ee58fd
Remove companion FEM RX gain command IDs
Quency-D Jun 13, 2026
5300fa1
Restore companion NodePrefs file ending
Quency-D Jun 13, 2026
a5cb0c2
Merge pull request #2140 from Quency-D/cli-lna-command
ripplebiz Jun 13, 2026
07648e3
* fix for anon contacts when full
ripplebiz Jun 14, 2026
c2d223f
* now handle the case where onAdvertRecv() should _replace_ the anon …
ripplebiz Jun 14, 2026
538ac38
* fix for counter in RESP_CODE_CONTACTS_START
ripplebiz Jun 14, 2026
2e73fe9
* fix for anon lastmod
ripplebiz Jun 14, 2026
29f0a7f
Merge pull request #2763 from meshcore-dev/anon-contacts-fix
oltaco Jun 14, 2026
4f9a091
Use hardware channel activity detection for checking interference
weebl2000 Feb 18, 2026
813d108
Also return busy if preamble detected
weebl2000 Feb 22, 2026
04a6c70
Just check for not channel free
weebl2000 Feb 22, 2026
099ef67
Prevent packet errors from growing
weebl2000 Mar 6, 2026
7c8e092
Have CAD be a separate toggle (set cad on/off)
weebl2000 Jun 3, 2026
60ea4a9
Merge pull request #1727 from weebl2000/use-hardware-channel-activity…
ripplebiz Jun 15, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
28 changes: 28 additions & 0 deletions docs/cli_commands.md
Original file line number Diff line number Diff line change
Expand Up @@ -263,6 +263,20 @@ This document provides an overview of CLI commands that can be sent to MeshCore

---

#### View or change the LoRa FEM receive-path gain state on supported boards
**Usage:**
- `get radio.fem.rxgain`
- `set radio.fem.rxgain <state>`

**Parameters:**
- `state`: `on`|`off`

**Notes:**
- This controls the external LoRa FEM receive-path LNA where the board supports it.
- This is separate from `radio.rxgain`, which controls the radio chip receive gain mode.

---

### System

#### View or change this node's name
Expand Down Expand Up @@ -564,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 <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`
Expand Down
17 changes: 7 additions & 10 deletions examples/companion_radio/MyMesh.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -261,6 +261,9 @@ float MyMesh::getAirtimeBudgetFactor() const {
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;
Expand Down Expand Up @@ -854,7 +857,7 @@ void MyMesh::onSendTimeout() {}

MyMesh::MyMesh(mesh::Radio &radio, mesh::RNG &rng, mesh::RTCClock &rtc, SimpleMeshTables &tables, DataStore& store, AbstractUITask* ui)
: BaseChatMesh(radio, *new ArduinoMillis(), rng, rtc, *new StaticPoolPacketManager(16), tables),
_serial(NULL), telemetry(MAX_PACKET_PAYLOAD - 4), _store(&store), _ui(ui) {
_serial(NULL), telemetry(MAX_PACKET_PAYLOAD - 4), _store(&store), _ui(ui), _iter(0) {
_iter_started = false;
_cli_rescue = false;
offline_queue_len = 0;
Expand Down Expand Up @@ -1542,6 +1545,7 @@ void MyMesh::handleCmdFrame(size_t len) {
memcpy(anon.id.pub_key, pub_key, PUB_KEY_SIZE);
anon.out_path_len = 0; // default to zero-hop direct
anon.type = ADV_TYPE_NONE; // unknown
anon.lastmod = getRTCClock()->getCurrentTime();

if (addContact(anon)) recipient = &anon;
}
Expand Down Expand Up @@ -1981,6 +1985,7 @@ void MyMesh::handleCmdFrame(size_t len) {
sendPacket(pkt, priority, 0);
writeOKFrame();
} else {
releasePacket(pkt);
writeErrFrame(ERR_CODE_ILLEGAL_ARG);
}
} else {
Expand Down Expand Up @@ -2186,15 +2191,7 @@ void MyMesh::checkSerialInterface() {
&& !_serial->isWriteBusy() // don't spam the Serial Interface too quickly!
) {
ContactInfo contact;
bool found = false;
while (_iter.hasNext(this, contact)) {
if (contact.type != ADV_TYPE_NONE) {
found = true;
break;
}
}

if (found) {
if (_iter.hasNext(this, contact)) {
if (contact.lastmod > _iter_filter_since) { // apply the 'since' filter
writeContactRespFrame(RESP_CODE_CONTACT, contact);
if (contact.lastmod > _most_recent_lastmod) {
Expand Down
1 change: 1 addition & 0 deletions examples/companion_radio/MyMesh.h
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down
3 changes: 3 additions & 0 deletions examples/simple_repeater/MyMesh.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -893,6 +893,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
Expand All @@ -917,6 +918,7 @@ MyMesh::MyMesh(mesh::MainBoard &board, mesh::Radio &radio, mesh::MillisecondCloc
_prefs.rx_boosted_gain = 1; // enabled by default;
#endif
#endif
_prefs.radio_fem_rxgain = 1;

pending_discover_tag = 0;
pending_discover_until = 0;
Expand Down Expand Up @@ -965,6 +967,7 @@ void MyMesh::begin(FILESYSTEM *fs) {
radio_driver.setRxBoostedGainMode(_prefs.rx_boosted_gain);
MESH_DEBUG_PRINTLN("RX Boosted Gain Mode: %s",
radio_driver.getRxBoostedGainMode() ? "Enabled" : "Disabled");
board.setLoRaFemLnaEnabled(_prefs.radio_fem_rxgain);

updateAdvertTimer();
updateFloodAdvertTimer();
Expand Down
3 changes: 3 additions & 0 deletions examples/simple_repeater/MyMesh.h
Original file line number Diff line number Diff line change
Expand Up @@ -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
}
Expand Down
3 changes: 2 additions & 1 deletion examples/simple_repeater/UITask.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -41,7 +41,8 @@ void UITask::begin(NodePrefs* node_prefs, const char* build_date, const char* fi
}

// v1.2.3 (1 Jan 2025)
sprintf(_version_info, "%s (%s)", version, build_date);
snprintf(_version_info, sizeof(_version_info), "%s (%s)", version, build_date);
free(version);
}

void UITask::renderCurrScreen() {
Expand Down
3 changes: 3 additions & 0 deletions examples/simple_room_server/MyMesh.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -658,6 +659,7 @@ MyMesh::MyMesh(mesh::MainBoard &board, mesh::Radio &radio, mesh::MillisecondCloc
_prefs.gps_enabled = 0;
_prefs.gps_interval = 0;
_prefs.advert_loc_policy = ADVERT_LOC_PREFS;
_prefs.radio_fem_rxgain = 1;

next_post_idx = 0;
next_client_idx = 0;
Expand Down Expand Up @@ -699,6 +701,7 @@ void MyMesh::begin(FILESYSTEM *fs) {

radio_driver.setParams(_prefs.freq, _prefs.bw, _prefs.sf, _prefs.cr);
radio_driver.setTxPower(_prefs.tx_power_dbm);
board.setLoRaFemLnaEnabled(_prefs.radio_fem_rxgain);

updateAdvertTimer();
updateFloodAdvertTimer();
Expand Down
3 changes: 3 additions & 0 deletions examples/simple_room_server/MyMesh.h
Original file line number Diff line number Diff line change
Expand Up @@ -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
}
Expand Down
3 changes: 2 additions & 1 deletion examples/simple_room_server/UITask.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -41,7 +41,8 @@ void UITask::begin(NodePrefs* node_prefs, const char* build_date, const char* fi
}

// v1.2.3 (1 Jan 2025)
sprintf(_version_info, "%s (%s)", version, build_date);
snprintf(_version_info, sizeof(_version_info), "%s (%s)", version, build_date);
free(version);
}

void UITask::renderCurrScreen() {
Expand Down
2 changes: 1 addition & 1 deletion examples/simple_secure_chat/main.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -135,7 +135,7 @@ class MyMesh : public BaseChatMesh, ContactVisitor {
File file = _fs->open("/contacts", "w", true);
#endif
if (file) {
ContactsIterator iter;
ContactsIterator iter = startContactsIterator();
ContactInfo c;
uint8_t unused = 0;
uint32_t reserved = 0;
Expand Down
6 changes: 6 additions & 0 deletions examples/simple_sensor/SensorMesh.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -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
}
Expand Down Expand Up @@ -726,11 +729,13 @@ 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;
_prefs.gps_interval = 0;
_prefs.advert_loc_policy = ADVERT_LOC_PREFS;
_prefs.radio_fem_rxgain = 1;

memset(default_scope.key, 0, sizeof(default_scope.key));
}
Expand Down Expand Up @@ -766,6 +771,7 @@ void SensorMesh::begin(FILESYSTEM* fs) {

radio_driver.setParams(_prefs.freq, _prefs.bw, _prefs.sf, _prefs.cr);
radio_driver.setTxPower(_prefs.tx_power_dbm);
board.setLoRaFemLnaEnabled(_prefs.radio_fem_rxgain);

updateAdvertTimer();
updateFloodAdvertTimer();
Expand Down
1 change: 1 addition & 0 deletions examples/simple_sensor/SensorMesh.h
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down
3 changes: 2 additions & 1 deletion examples/simple_sensor/UITask.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -41,7 +41,8 @@ void UITask::begin(NodePrefs* node_prefs, const char* build_date, const char* fi
}

// v1.2.3 (1 Jan 2025)
sprintf(_version_info, "%s (%s)", version, build_date);
snprintf(_version_info, sizeof(_version_info), "%s (%s)", version, build_date);
free(version);
}

void UITask::renderCurrScreen() {
Expand Down
1 change: 1 addition & 0 deletions src/Dispatcher.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -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();
Expand Down
3 changes: 3 additions & 0 deletions src/Dispatcher.h
Original file line number Diff line number Diff line change
Expand Up @@ -65,6 +65,8 @@ class Radio {

virtual void triggerNoiseFloorCalibrate(int threshold) { }

virtual void setCADEnabled(bool enable) { }

virtual void resetAGC() { }

virtual bool isInRecvMode() const = 0;
Expand Down Expand Up @@ -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; }

Expand Down
4 changes: 4 additions & 0 deletions src/Mesh.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -155,6 +155,10 @@ DispatcherAction Mesh::onRecvPacket(Packet* pkt) {
if (pkt->getPayloadType() == PAYLOAD_TYPE_PATH) {
int k = 0;
uint8_t path_len = data[k++];
if (!Packet::isValidPathLen(path_len)) {
MESH_DEBUG_PRINTLN("%s PAYLOAD_TYPE_PATH, bad path_len: %u", getLogDateTime(), (uint32_t)path_len);
break; // reject bad encoding
}
uint8_t hash_size = (path_len >> 6) + 1;
uint8_t hash_count = path_len & 63;
uint8_t* path = &data[k]; k += hash_size*hash_count;
Expand Down
3 changes: 3 additions & 0 deletions src/MeshCore.h
Original file line number Diff line number Diff line change
Expand Up @@ -64,6 +64,9 @@ class MainBoard {
virtual uint8_t getStartupReason() const = 0;
virtual bool getBootloaderVersion(char* version, size_t max_len) { return false; }
virtual bool startOTAUpdate(const char* id, char reply[]) { return false; } // not supported
virtual bool setLoRaFemLnaEnabled(bool enable) { return false; }
virtual bool canControlLoRaFemLna() const { return false; }
virtual bool isLoRaFemLnaEnabled() const { return false; }

// Power management interface (boards with power management override these)
virtual bool isExternalPowered() { return false; }
Expand Down
52 changes: 32 additions & 20 deletions src/helpers/BaseChatMesh.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -68,29 +68,36 @@ void BaseChatMesh::bootstrapRTCfromContacts() {
}

ContactInfo* BaseChatMesh::allocateContactSlot(bool transient_only) {
if (num_contacts < MAX_CONTACTS) {
return &contacts[num_contacts++];
} else if (transient_only || shouldOverwriteWhenFull()) {
// Find oldest non-favourite contact by oldest lastmod timestamp
int oldest_idx = -1;
uint32_t oldest_lastmod = 0xFFFFFFFF;
for (int i = 0; i < num_contacts; i++) {
if (transient_only) {
if (contacts[i].type == ADV_TYPE_NONE && contacts[i].lastmod < oldest_lastmod) {
oldest_lastmod = contacts[i].lastmod;
oldest_idx = i;
}
} else {
int oldest_idx = -1;
uint32_t oldest_lastmod = 0xFFFFFFFF;
if (transient_only) {
// only allocate from first N
for (int i = 0; i < MAX_ANON_CONTACTS; i++) {
if (contacts[i].type == ADV_TYPE_NONE && contacts[i].lastmod < oldest_lastmod) {
oldest_lastmod = contacts[i].lastmod;
oldest_idx = i;
}
}
if (oldest_idx >= 0) {
// NOTE: do NOT call onContactOverwrite()
return &contacts[oldest_idx];
}
} else {
if (num_contacts < MAX_ANON_CONTACTS+MAX_CONTACTS) {
return &contacts[num_contacts++];
} else if (shouldOverwriteWhenFull()) {
// Find oldest non-favourite contact by oldest lastmod timestamp
for (int i = MAX_ANON_CONTACTS; i < num_contacts; i++) {
bool is_favourite = (contacts[i].flags & 0x01) != 0;
if (!is_favourite && contacts[i].lastmod < oldest_lastmod && contacts[i].type != ADV_TYPE_NONE) {
if (!is_favourite && contacts[i].lastmod < oldest_lastmod) {
oldest_lastmod = contacts[i].lastmod;
oldest_idx = i;
}
}
}
if (oldest_idx >= 0) {
onContactOverwrite(contacts[oldest_idx].id.pub_key);
return &contacts[oldest_idx];
if (oldest_idx >= 0) {
onContactOverwrite(contacts[oldest_idx].id.pub_key);
return &contacts[oldest_idx];
}
}
}
return NULL; // no space, no overwrite or all contacts are all favourites
Expand Down Expand Up @@ -139,6 +146,11 @@ void BaseChatMesh::onAdvertRecv(mesh::Packet* packet, const mesh::Identity& id,
packet->header = save;
}

if (from && from->type == ADV_TYPE_NONE) { // already in contacts, but from a temporary ANON_REQ ?
memset(from, 0, sizeof(*from)); // clear the anon/temp slot
from = NULL; // do normal 'add' flow
}

bool is_new = false; // true = not in contacts[], false = exists in contacts[]
if (from == NULL) {
if (!shouldAutoAddContactType(parser.getType())) {
Expand Down Expand Up @@ -930,11 +942,11 @@ bool BaseChatMesh::getContactByIdx(uint32_t idx, ContactInfo& contact) {
}

ContactsIterator BaseChatMesh::startContactsIterator() {
return ContactsIterator();
return ContactsIterator(MAX_ANON_CONTACTS); // start at offset, skip the anon entries
}

bool ContactsIterator::hasNext(const BaseChatMesh* mesh, ContactInfo& dest) {
if (next_idx >= mesh->getNumContacts()) return false;
if (next_idx >= mesh->getTotalContactSlots()) return false;

dest = mesh->contacts[next_idx++];
return true;
Expand Down
Loading
Loading