Compare commits

..

32 Commits

Author SHA1 Message Date
ripplebiz 60ea4a91bf Merge pull request #1727 from weebl2000/use-hardware-channel-activity-detection
Use hardware channel activity detection for checking interference
2026-06-15 19:51:59 +10:00
Wessel Nieboer 7c8e092457 Have CAD be a separate toggle (set cad on/off) 2026-06-14 15:20:58 +02:00
Wessel Nieboer 099ef67348 Prevent packet errors from growing 2026-06-14 15:19:20 +02:00
Wessel Nieboer 04a6c7009a Just check for not channel free 2026-06-14 15:19:19 +02:00
Wessel Nieboer 813d108c7a Also return busy if preamble detected 2026-06-14 15:19:19 +02:00
Wessel Nieboer 4f9a091671 Use hardware channel activity detection for checking interference 2026-06-14 15:19:18 +02:00
Huw Duddy 29f0a7fc4f Merge pull request #2763 from meshcore-dev/anon-contacts-fix
* fix for anon contacts when full
2026-06-14 19:11:38 +10:00
Scott Powell 2e73fe948e * fix for anon lastmod 2026-06-14 18:54:51 +10:00
Scott Powell 538ac38e18 * fix for counter in RESP_CODE_CONTACTS_START 2026-06-14 18:39:34 +10:00
Scott Powell c2d223ff55 * now handle the case where onAdvertRecv() should _replace_ the anon contact slot 2026-06-14 18:12:08 +10:00
Scott Powell 07648e3344 * fix for anon contacts when full 2026-06-14 17:50:41 +10:00
ripplebiz a5cb0c25ca Merge pull request #2140 from Quency-D/cli-lna-command
Add CLI control to LoRa's fem LNA.
2026-06-13 22:00:08 +10:00
Quency-D 5300fa18c7 Restore companion NodePrefs file ending 2026-06-13 17:40:11 +08:00
Quency-D 3ee58fd2e3 Remove companion FEM RX gain command IDs 2026-06-13 17:31:54 +08:00
Scott Powell d5f74e93c5 * PAYLOAD_TYPE_PATH bad path_len now rejected 2026-06-13 18:19:41 +10:00
Quency-D 9100a58671 Merge branch 'dev' into cli-lna-command 2026-06-10 11:30:04 +08:00
ripplebiz 5f3b7f25d0 Merge pull request #2722 from liamcottle/fix/free-packet-on-parse-failure
Fix: CMD_SEND_RAW_PACKET not freeing packet on parse failure
2026-06-08 22:47:06 +10:00
liamcottle ae0bb7ee95 use releasePacket instead of _mgr->free 2026-06-09 00:42:58 +12:00
liamcottle 07bfe90695 free packet on parse failure 2026-06-09 00:31:48 +12:00
Quency-D 250f448c3f Merge branch 'dev' into cli-lna-command 2026-05-20 10:13:28 +08:00
Quency-D 12e6899580 Merge branch 'dev' into cli-lna-command 2026-05-09 18:19:31 +08:00
Quency-D 444dcfb8fd Change write to read for radio_fem_rxgain 2026-04-27 14:06:21 +08:00
Quency-D 62b0d82682 Restore WiFi operation command scope comments 2026-04-25 16:12:40 +08:00
Quency-D 9d26953398 Remove unnecessary blank line in MyMesh.cpp 2026-04-25 16:10:57 +08:00
Quency-D c42f6db0eb Revise according to the review comments.
Co-authored-by: Copilot <copilot@github.com>
2026-04-25 16:01:26 +08:00
Quency-D 22f07b3e48 Revert "Merge branch 'meshcore-dev:main' into cli-lna-command"
This reverts commit ca047fe0c0, reversing
changes made to ddedb3c7a7.
2026-04-25 15:33:59 +08:00
Quency-D ca047fe0c0 Merge branch 'meshcore-dev:main' into cli-lna-command 2026-04-25 15:29:00 +08:00
Quency-D ddedb3c7a7 Merge branch 'dev' into cli-lna-command 2026-04-25 15:28:07 +08:00
Quency-D 9664305a87 Adapt LNA CLI control commands for heltec_t096. 2026-03-24 16:13:13 +08:00
Quency-D 2442e9a5bd Adapt LNA CLI control commands for heltec_tracker_v2. 2026-03-24 16:05:28 +08:00
Quency-D 65752fef72 Fix the memory leak issue in the strdup function. 2026-03-24 13:57:11 +08:00
Quency-D 8435464c84 Add v4 FEM LNA CLI control commands. 2026-03-24 10:48:15 +08:00
32 changed files with 264 additions and 58 deletions
+28
View File
@@ -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
@@ -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`
+7 -10
View File
@@ -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;
@@ -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;
@@ -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;
}
@@ -1981,6 +1985,7 @@ void MyMesh::handleCmdFrame(size_t len) {
sendPacket(pkt, priority, 0);
writeOKFrame();
} else {
releasePacket(pkt);
writeErrFrame(ERR_CODE_ILLEGAL_ARG);
}
} else {
@@ -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) {
+1
View File
@@ -105,6 +105,7 @@ public:
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;
+3
View File
@@ -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
@@ -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;
@@ -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();
+3
View File
@@ -150,6 +150,9 @@ protected:
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
}
+2 -1
View File
@@ -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() {
+3
View File
@@ -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
@@ -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;
@@ -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();
+3
View File
@@ -144,6 +144,9 @@ protected:
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
}
+2 -1
View File
@@ -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() {
+1 -1
View File
@@ -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;
+6
View File
@@ -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,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));
}
@@ -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();
+1
View File
@@ -120,6 +120,7 @@ protected:
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;
+2 -1
View File
@@ -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() {
+1
View File
@@ -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();
+3
View File
@@ -65,6 +65,8 @@ public:
virtual void triggerNoiseFloorCalibrate(int threshold) { }
virtual void setCADEnabled(bool enable) { }
virtual void resetAGC() { }
virtual bool isInRecvMode() const = 0;
@@ -166,6 +168,7 @@ protected:
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; }
+4
View File
@@ -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;
+3
View File
@@ -64,6 +64,9 @@ public:
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; }
+32 -20
View File
@@ -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
@@ -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())) {
@@ -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;
+12 -5
View File
@@ -28,8 +28,9 @@ public:
class BaseChatMesh;
class ContactsIterator {
int next_idx = 0;
int next_idx;
public:
ContactsIterator(int start) { next_idx = start; }
bool hasNext(const BaseChatMesh* mesh, ContactInfo& dest);
};
@@ -79,8 +80,9 @@ class BaseChatMesh : public mesh::Mesh {
protected:
BaseChatMesh(mesh::Radio& radio, mesh::MillisecondClock& ms, mesh::RNG& rng, mesh::RTCClock& rtc, mesh::PacketManager& mgr, mesh::MeshTables& tables)
: mesh::Mesh(radio, ms, rng, rtc, mgr, tables)
{
num_contacts = 0;
{
resetContacts();
#ifdef MAX_GROUP_CHANNELS
memset(channels, 0, sizeof(channels));
num_channels = 0;
@@ -91,7 +93,11 @@ protected:
}
void bootstrapRTCfromContacts();
void resetContacts() { num_contacts = 0; }
void resetContacts() {
memset(contacts, 0, sizeof(contacts[0])*MAX_ANON_CONTACTS); // set all to have type = ADV_TYPE_NONE(0)
num_contacts = MAX_ANON_CONTACTS; // seed the first contacts for anon requests
}
void populateContactFromAdvert(ContactInfo& ci, const mesh::Identity& id, const AdvertDataParser& parser, uint32_t timestamp);
ContactInfo* allocateContactSlot(bool transient_only=false); // helper to find slot for new contact
@@ -166,7 +172,8 @@ public:
ContactInfo* lookupContactByPubKey(const uint8_t* pub_key, int prefix_len);
bool removeContact(ContactInfo& contact);
bool addContact(const ContactInfo& contact);
int getNumContacts() const { return num_contacts; }
int getTotalContactSlots() const { return num_contacts; }
int getNumContacts() const { return num_contacts - MAX_ANON_CONTACTS; } // don't include the reserved slots at start
bool getContactByIdx(uint32_t idx, ContactInfo& contact);
ContactsIterator startContactsIterator();
ChannelDetails* addChannel(const char* name, const char* psk_base64);
+51 -11
View File
@@ -88,10 +88,12 @@ void CommonCLI::loadPrefsInt(FILESYSTEM* fs, const char* filename) {
file.read((uint8_t *)&_prefs->discovery_mod_timestamp, sizeof(_prefs->discovery_mod_timestamp)); // 162
file.read((uint8_t *)&_prefs->adc_multiplier, sizeof(_prefs->adc_multiplier)); // 166
file.read((uint8_t *)_prefs->owner_info, sizeof(_prefs->owner_info)); // 170
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->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
file.read((uint8_t *)&_prefs->radio_fem_rxgain, sizeof(_prefs->radio_fem_rxgain)); // 293
file.read((uint8_t *)&_prefs->cad_enabled, sizeof(_prefs->cad_enabled)); // 294
// next: 295
// sanitise bad pref values
_prefs->rx_delay_base = constrain(_prefs->rx_delay_base, 0, 20.0f);
@@ -121,6 +123,8 @@ 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
file.close();
}
@@ -181,10 +185,12 @@ void CommonCLI::savePrefs(FILESYSTEM* fs) {
file.write((uint8_t *)&_prefs->discovery_mod_timestamp, sizeof(_prefs->discovery_mod_timestamp)); // 162
file.write((uint8_t *)&_prefs->adc_multiplier, sizeof(_prefs->adc_multiplier)); // 166
file.write((uint8_t *)_prefs->owner_info, sizeof(_prefs->owner_info)); // 170
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->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
file.write((uint8_t *)&_prefs->radio_fem_rxgain, sizeof(_prefs->radio_fem_rxgain)); // 293
file.write((uint8_t *)&_prefs->cad_enabled, sizeof(_prefs->cad_enabled)); // 294
// next: 295
file.close();
}
@@ -500,6 +506,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();
@@ -568,6 +578,28 @@ void CommonCLI::handleSetCmd(uint32_t sender_timestamp, char* command, char* rep
savePrefs();
_callbacks->setRxBoostedGain(_prefs->rx_boosted_gain);
#endif
} else if (memcmp(config, "radio.fem.rxgain ", 17) == 0) {
if (!_board->canControlLoRaFemLna()) {
strcpy(reply, "Error: unsupported");
} else if (memcmp(&config[17], "on", 2) == 0) {
if (_board->setLoRaFemLnaEnabled(true)) {
_prefs->radio_fem_rxgain = 1;
savePrefs();
strcpy(reply, "OK - LoRa FEM RX gain on");
} else {
strcpy(reply, "Error: failed to apply LoRa FEM RX gain");
}
} else if (memcmp(&config[17], "off", 3) == 0) {
if (_board->setLoRaFemLnaEnabled(false)) {
_prefs->radio_fem_rxgain = 0;
savePrefs();
strcpy(reply, "OK - LoRa FEM RX gain off");
} else {
strcpy(reply, "Error: failed to apply LoRa FEM RX gain");
}
} else {
strcpy(reply, "Error: state must be on or off");
}
} else if (memcmp(config, "radio ", 6) == 0) {
strcpy(tmp, &config[6]);
const char *parts[4];
@@ -757,7 +789,7 @@ void CommonCLI::handleSetCmd(uint32_t sender_timestamp, char* command, char* rep
}
} else {
_prefs->adc_multiplier = 0.0f;
strcpy(reply, "Error: unsupported by this board");
strcpy(reply, "Error: unsupported");
};
} else {
strcpy(reply, "unknown config: ");
@@ -776,6 +808,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) {
@@ -805,6 +839,12 @@ void CommonCLI::handleGetCmd(uint32_t sender_timestamp, char* command, char* rep
} else if (memcmp(config, "radio.rxgain", 12) == 0) {
sprintf(reply, "> %s", _prefs->rx_boosted_gain ? "on" : "off");
#endif
} else if (memcmp(config, "radio.fem.rxgain", 16) == 0) {
if (!_board->canControlLoRaFemLna()) {
strcpy(reply, "Error: unsupported");
} else {
sprintf(reply, "> %s", _board->isLoRaFemLnaEnabled() ? "on" : "off");
}
} else if (memcmp(config, "radio", 5) == 0) {
char freq[16], bw[16];
strcpy(freq, StrHelper::ftoa(_prefs->freq));
@@ -890,12 +930,12 @@ void CommonCLI::handleGetCmd(uint32_t sender_timestamp, char* command, char* rep
strcpy(reply, "> unknown");
}
#else
strcpy(reply, "ERROR: unsupported");
strcpy(reply, "Error: unsupported");
#endif
} else if (memcmp(config, "adc.multiplier", 14) == 0) {
float adc_mult = _board->getAdcMultiplier();
if (adc_mult == 0.0f) {
strcpy(reply, "Error: unsupported by this board");
strcpy(reply, "Error: unsupported");
} else {
sprintf(reply, "> %.3f", adc_mult);
}
+2
View File
@@ -61,8 +61,10 @@ struct NodePrefs { // persisted to file
float adc_multiplier;
char owner_info[120];
uint8_t rx_boosted_gain; // power settings
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)
};
class CommonCLICallbacks {
+20 -3
View File
@@ -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 {
+4 -1
View File
@@ -9,6 +9,7 @@ protected:
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 @@ public:
bool isInRecvMode() const override;
bool isChannelActive();
bool isReceiving() override {
bool isReceiving() override {
if (isReceivingPacket()) return true;
return isChannelActive();
@@ -46,9 +47,11 @@ public:
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;
+2 -1
View File
@@ -12,8 +12,9 @@ class LoRaFEMControl
void setRxModeEnable(void);
void setRxModeEnableWhenMCUSleep(void);
void setLNAEnable(bool enabled);
bool isLnaCanControl(void) { return lna_can_control; }
bool isLnaCanControl(void) const { return lna_can_control; }
void setLnaCanControl(bool can_control) { lna_can_control = can_control; }
bool isLNAEnabled(void) const { return lna_enabled; }
private:
bool lna_enabled = false;
+19 -1
View File
@@ -123,4 +123,22 @@ void T096Board::powerOff() {
const char* T096Board::getManufacturerName() const {
return "Heltec T096";
}
}
bool T096Board::setLoRaFemLnaEnabled(bool enable) {
if (!loRaFEMControl.isLnaCanControl()) {
return false;
}
loRaFEMControl.setLNAEnable(enable);
loRaFEMControl.setRxModeEnable();
return true;
}
bool T096Board::canControlLoRaFemLna() const {
return loRaFEMControl.isLnaCanControl();
}
bool T096Board::isLoRaFemLnaEnabled() const {
return loRaFEMControl.isLNAEnabled();
}
+3
View File
@@ -25,4 +25,7 @@ public:
uint16_t getBattMilliVolts() override;
const char* getManufacturerName() const override ;
void powerOff() override;
bool setLoRaFemLnaEnabled(bool enable) override;
bool canControlLoRaFemLna() const override;
bool isLoRaFemLnaEnabled() const override;
};
@@ -82,3 +82,21 @@ void HeltecTrackerV2Board::begin() {
const char* HeltecTrackerV2Board::getManufacturerName() const {
return "Heltec Tracker V2";
}
bool HeltecTrackerV2Board::setLoRaFemLnaEnabled(bool enable) {
if (!loRaFEMControl.isLnaCanControl()) {
return false;
}
loRaFEMControl.setLNAEnable(enable);
loRaFEMControl.setRxModeEnable();
return true;
}
bool HeltecTrackerV2Board::canControlLoRaFemLna() const {
return loRaFEMControl.isLnaCanControl();
}
bool HeltecTrackerV2Board::isLoRaFemLnaEnabled() const {
return loRaFEMControl.isLNAEnabled();
}
@@ -21,5 +21,8 @@ public:
void powerOff() override;
uint16_t getBattMilliVolts() override;
const char* getManufacturerName() const override ;
bool setLoRaFemLnaEnabled(bool enable) override;
bool canControlLoRaFemLna() const override;
bool isLoRaFemLnaEnabled() const override;
};
+2 -1
View File
@@ -12,8 +12,9 @@ class LoRaFEMControl
void setRxModeEnable(void);
void setRxModeEnableWhenMCUSleep(void);
void setLNAEnable(bool enabled);
bool isLnaCanControl(void) { return lna_can_control; }
bool isLnaCanControl(void) const { return lna_can_control; }
void setLnaCanControl(bool can_control) { lna_can_control = can_control; }
bool isLNAEnabled(void) const { return lna_enabled; }
private:
bool lna_enabled = false;
+18
View File
@@ -83,3 +83,21 @@ void HeltecV4Board::begin() {
return loRaFEMControl.getFEMType() == KCT8103L_PA ? "Heltec V4.3 OLED" : "Heltec V4 OLED";
#endif
}
bool HeltecV4Board::setLoRaFemLnaEnabled(bool enable) {
if (!loRaFEMControl.isLnaCanControl()) {
return false;
}
loRaFEMControl.setLNAEnable(enable);
loRaFEMControl.setRxModeEnable();
return true;
}
bool HeltecV4Board::canControlLoRaFemLna() const {
return loRaFEMControl.isLnaCanControl();
}
bool HeltecV4Board::isLoRaFemLnaEnabled() const {
return loRaFEMControl.isLNAEnabled();
}
+3
View File
@@ -25,6 +25,9 @@ public:
void onAfterTransmit(void) override;
void enterDeepSleep(uint32_t secs, int pin_wake_btn = -1);
void powerOff() override;
bool setLoRaFemLnaEnabled(bool enable) override;
bool canControlLoRaFemLna() const override;
bool isLoRaFemLnaEnabled() const override;
uint16_t getBattMilliVolts() override;
bool setAdcMultiplier(float multiplier) override {
if (multiplier == 0.0f) {
+2 -1
View File
@@ -18,8 +18,9 @@ class LoRaFEMControl
void setRxModeEnable(void);
void setRxModeEnableWhenMCUSleep(void);
void setLNAEnable(bool enabled);
bool isLnaCanControl(void) { return lna_can_control; }
bool isLnaCanControl(void) const { return lna_can_control; }
void setLnaCanControl(bool can_control) { lna_can_control = can_control; }
bool isLNAEnabled(void) const { return lna_enabled; }
LoRaFEMType getFEMType(void) const { return fem_type; }
private:
LoRaFEMType fem_type=OTHER_FEM_TYPES;