Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
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
113 changes: 96 additions & 17 deletions examples/companion_radio/MyMesh.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -62,6 +62,7 @@
#define CMD_SET_DEFAULT_FLOOD_SCOPE 63
#define CMD_GET_DEFAULT_FLOOD_SCOPE 64
#define CMD_SEND_RAW_PACKET 65
#define CMD_RUN_CLI_COMMAND 66 // v14+

// Stats sub-types for CMD_GET_STATS
#define STATS_TYPE_CORE 0
Expand Down Expand Up @@ -97,6 +98,7 @@
#define RESP_ALLOWED_REPEAT_FREQ 26
#define RESP_CODE_CHANNEL_DATA_RECV 27
#define RESP_CODE_DEFAULT_FLOOD_SCOPE 28
#define RESP_CODE_CLI_REPLY 29 // v14+, a reply to CMD_RUN_CLI_COMMAND

#define MAX_CHANNEL_DATA_LENGTH (MAX_FRAME_SIZE - 9)

Expand Down Expand Up @@ -262,7 +264,7 @@ int MyMesh::getInterferenceThreshold() const {
return 0; // disabled for now, until currentRSSI() problem is resolved
}
bool MyMesh::getCADEnabled() const {
return false; // hardware CAD before TX (disabled by default, until configurable)
return _prefs.cad_enabled;
}

int MyMesh::calcRxDelay(float score, uint32_t air_time) const {
Expand Down Expand Up @@ -528,12 +530,23 @@ void MyMesh::onMessageRecv(const ContactInfo &from, mesh::Packet *pkt, uint32_t
queueMessage(from, TXT_TYPE_PLAIN, pkt, sender_timestamp, NULL, 0, text);
}

void MyMesh::onCommandDataRecv(const ContactInfo &from, mesh::Packet *pkt, uint32_t sender_timestamp,
const char *text) {
void MyMesh::onCommandDataRecv(const ContactInfo &from, mesh::Packet *pkt, uint32_t sender_timestamp, const char *text) {
markConnectionActive(from); // in case this is from a server, and we have a connection
queueMessage(from, TXT_TYPE_CLI_DATA, pkt, sender_timestamp, NULL, 0, text);
}

void MyMesh::onCLICommandRecv(const ContactInfo &from, mesh::Packet *pkt, uint32_t sender_timestamp,
const char *text, char* reply) {
markConnectionActive(from); // in case this is from a server, and we have a connection
if (from.isRemoteCLIAllowed()) {
if (!handleCommand(text, sender_timestamp, reply)) {
strcat(reply, "Unknown command"); // reply may have cmd prefix from 'text'
}
} else {
queueMessage(from, TXT_TYPE_CLI_COMMAND, pkt, sender_timestamp, NULL, 0, text);
}
}

void MyMesh::onSignedMessageRecv(const ContactInfo &from, mesh::Packet *pkt, uint32_t sender_timestamp,
const uint8_t *sender_prefix, const char *text) {
markConnectionActive(from);
Expand Down Expand Up @@ -976,8 +989,9 @@ void MyMesh::begin(bool has_display) {
radio_driver.setParams(_prefs.freq, _prefs.bw, _prefs.sf, _prefs.cr);
radio_driver.setTxPower(_prefs.tx_power_dbm);
radio_driver.setRxBoostedGainMode(_prefs.rx_boosted_gain);
board.setLoRaFemLnaEnabled(_prefs.radio_fem_rxgain);
board.setLoRaFemPaGainEnabled(_prefs.radio_fem_txgain);

board.attachDynamicPrefs(_prefs.getCustom());

MESH_DEBUG_PRINTLN("RX Boosted Gain Mode: %s",
radio_driver.getRxBoostedGainMode() ? "Enabled" : "Disabled");
}
Expand Down Expand Up @@ -1082,6 +1096,20 @@ void MyMesh::handleCmdFrame(size_t len) {
memcpy(&out_frame[i], _prefs.node_name, tlen);
i += tlen;
_serial->writeFrame(out_frame, i);
} else if (cmd_frame[0] == CMD_RUN_CLI_COMMAND && len >= 3) { // V14+
int i = 1;
char *text = (char *)&cmd_frame[i];
int tlen = len - i;
text[tlen] = 0; // ensure null

reply_buf[0] = 0;
if (!handleCommand(text, 0, reply_buf)) {
strcat(reply_buf, "Unknown command"); // reply_buf may have cmd prefix from 'text'
}
out_frame[0] = RESP_CODE_CLI_REPLY;
int rlen = strlen(reply_buf);
memcpy(&out_frame[1], reply_buf, rlen);
_serial->writeFrame(out_frame, 1 + rlen);
} else if (cmd_frame[0] == CMD_SEND_TXT_MSG && len >= 14) {
int i = 1;
uint8_t txt_type = cmd_frame[i++];
Expand All @@ -1092,16 +1120,16 @@ void MyMesh::handleCmdFrame(size_t len) {
uint8_t *pub_key_prefix = &cmd_frame[i];
i += 6;
ContactInfo *recipient = lookupContactByPubKey(pub_key_prefix, 6);
if (recipient && (txt_type == TXT_TYPE_PLAIN || txt_type == TXT_TYPE_CLI_DATA)) {
if (recipient && (txt_type == TXT_TYPE_PLAIN || txt_type == TXT_TYPE_CLI_DATA || txt_type == TXT_TYPE_CLI_COMMAND)) {
char *text = (char *)&cmd_frame[i];
int tlen = len - i;
uint32_t est_timeout;
text[tlen] = 0; // ensure null
int result;
uint32_t expected_ack;
if (txt_type == TXT_TYPE_CLI_DATA) {
if (txt_type == TXT_TYPE_CLI_DATA || txt_type == TXT_TYPE_CLI_COMMAND) {
msg_timestamp = getRTCClock()->getCurrentTimeUnique(); // Use node's RTC instead of app timestamp to avoid tripping replay protection
result = sendCommandData(*recipient, msg_timestamp, attempt, text, est_timeout);
result = sendCommandData(*recipient, msg_timestamp, attempt, txt_type, text, est_timeout);
expected_ack = 0; // no Ack expected
} else {
result = sendMessage(*recipient, msg_timestamp, attempt, text, expected_ack, est_timeout);
Expand Down Expand Up @@ -2031,6 +2059,62 @@ void MyMesh::enterCLIRescue() {
Serial.println("========= CLI Rescue =========");
}

bool MyMesh::handleCommand(const char* command, uint32_t sender_timestamp, char* reply) {
while (*command == ' ') command++; // skip leading spaces

if (strlen(command) > 4 && command[2] == '|') { // optional prefix (for companion radio CLI)
memcpy(reply, command, 3); // reflect the prefix back
reply += 3;
*reply = 0;
command += 3;
}

if (_prefs.getRadioPrefs()->handleCommand(command, sender_timestamp, reply)) { // is radio CLI command?
if (_prefs.getRadioPrefs()->isDirty()) { savePrefs(); }
return true;
}

// hook for variant-specific CLI processing
if (board.handleCommand(command, sender_timestamp, reply)) {
if (_prefs.isDirty()) { savePrefs(); }
return true;
}

if (memcmp(command, "set name ", 9) == 0) {
if (AdvertDataParser::isValidName(&command[9])) {
StrHelper::strncpy(_prefs.node_name, &command[9], sizeof(_prefs.node_name));
savePrefs();
strcpy(reply, "OK");
} else {
strcpy(reply, "Error, bad chars");
}
return true;
}
if (strcmp(command, "get name") == 0) {
sprintf(reply, "> %s", _prefs.node_name);
return true;
}

if (memcmp(command, "set pin ", 8) == 0) {
_prefs.ble_pin = atoi(&command[8]);
savePrefs();
sprintf(reply, "> pin is now %06d", _prefs.ble_pin);
return true;
}

if (strcmp(command, "board") == 0) {
strcpy(reply, board.getManufacturerName());
return true;
}

if (strcmp(command, "ver") == 0) {
sprintf(reply, "%s (Build: %s)", FIRMWARE_VERSION, FIRMWARE_BUILD_DATE);
return true;
}

return false; // not handled
}

void MyMesh::checkCLIRescueCmd() {
int len = strlen(cli_command);
while (Serial.available() && len < sizeof(cli_command)-1) {
Expand All @@ -2048,15 +2132,10 @@ void MyMesh::checkCLIRescueCmd() {
if (len > 0 && cli_command[len - 1] == '\r') { // received complete line
cli_command[len - 1] = 0; // replace newline with C string null terminator

if (memcmp(cli_command, "set ", 4) == 0) {
const char* config = &cli_command[4];
if (memcmp(config, "pin ", 4) == 0) {
_prefs.ble_pin = atoi(&config[4]);
savePrefs();
Serial.printf(" > pin is now %06d\n", _prefs.ble_pin);
} else {
Serial.printf(" Error: unknown config: %s\n", config);
}
reply_buf[0] = 0;
if (handleCommand(cli_command, 0, reply_buf)) {
// command was handled, print reply output
Serial.print(" "); Serial.print(reply_buf); Serial.println();
} else if (strcmp(cli_command, "rebuild") == 0) {
bool success = _store->formatFileSystem();
if (success) {
Expand Down
7 changes: 6 additions & 1 deletion examples/companion_radio/MyMesh.h
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@
#include "AbstractUITask.h"

/*------------ Frame Protocol --------------*/
#define FIRMWARE_VER_CODE 13
#define FIRMWARE_VER_CODE 14

#ifndef FIRMWARE_BUILD_DATE
#define FIRMWARE_BUILD_DATE "14 Aug 2026"
Expand Down Expand Up @@ -135,6 +135,8 @@ class MyMesh : public BaseChatMesh, public DataStoreHost {
const char *text) override;
void onCommandDataRecv(const ContactInfo &from, mesh::Packet *pkt, uint32_t sender_timestamp,
const char *text) override;
void onCLICommandRecv(const ContactInfo &from, mesh::Packet *pkt, uint32_t sender_timestamp,
const char *text, char* reply) override;
void onSignedMessageRecv(const ContactInfo &from, mesh::Packet *pkt, uint32_t sender_timestamp,
const uint8_t *sender_prefix, const char *text) override;
void onChannelMessageRecv(const mesh::GroupChannel &channel, mesh::Packet *pkt, uint32_t timestamp,
Expand Down Expand Up @@ -169,6 +171,7 @@ class MyMesh : public BaseChatMesh, public DataStoreHost {
_prefs.node_lat = sensors.node_lat;
_prefs.node_lon = sensors.node_lon;
_store->savePrefs(_prefs);
_prefs.clearDirty();
}

#if ENV_INCLUDE_GPS == 1
Expand Down Expand Up @@ -201,6 +204,7 @@ class MyMesh : public BaseChatMesh, public DataStoreHost {
}

void checkCLIRescueCmd();
bool handleCommand(const char* text, uint32_t sender_timestamp, char* reply);
void checkSerialInterface();
bool isValidClientRepeatFreq(uint32_t f) const;

Expand All @@ -225,6 +229,7 @@ class MyMesh : public BaseChatMesh, public DataStoreHost {
bool _cli_rescue;
bool send_unscoped; // force un-scoped flood (instead of using send_scope)
char cli_command[80];
char reply_buf[166];
uint8_t app_target_ver;
uint8_t *sign_data;
uint32_t sign_data_len;
Expand Down
60 changes: 52 additions & 8 deletions examples/companion_radio/NodePrefs.h
Original file line number Diff line number Diff line change
@@ -1,6 +1,8 @@
#pragma once
#include <cstdint> // For uint8_t, uint32_t
#include <helpers/ConfigSerializer.h>
#include <helpers/CommonRadioPrefs.h>
#include <helpers/DynamicConfigSerializer.h>

#define TELEM_MODE_DENY 0
#define TELEM_MODE_ALLOW_FLAGS 1 // use contact.flags
Expand Down Expand Up @@ -38,27 +40,24 @@ class NodePrefs : public ConfigSerializer { // persisted to file
uint8_t _client_repeat = 0; // DEPRECATED -> use repeat.disable_fwd
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;
char default_scope_name[31];
uint8_t default_scope_key[16];

private:
class RadioPrefs : public ConfigSerializer { // COPIED from CommonCLI (for now)
class RadioPrefs : public CommonRadioPrefs {
NodePrefs* _parent;
protected:
void structure() override {
def("freq", _parent->freq);
def("bw", _parent->bw);
def("sf", _parent->sf);
def("cr", _parent->cr);
//def("cad", _parent->cad_enabled);
def("cad", _parent->cad_enabled);
//def("int_thr", _parent->interference_threshold);
def("rxgain", _parent->rx_boosted_gain);
#if 0
// NOTE: these cannot be set (yet) so don't load/save until we can.
// also, fem_rxgain WAS mapped to wrong JSON property previously
def("fem_rxgain", _parent->radio_fem_rxgain);
def("fem_rxgain", _parent->radio_fem_rxgain); // fem_rxgain WAS mapped to wrong JSON property previously
def("fem_txgain", _parent->radio_fem_txgain);
#endif
def("tx", _parent->tx_power_dbm);
def("af", _parent->airtime_factor);
def("rxdelay", _parent->rx_delay_base);
Expand All @@ -70,6 +69,42 @@ class NodePrefs : public ConfigSerializer { // persisted to file
}
public:
RadioPrefs(NodePrefs* parent) : _parent(parent) { }

// CommonRadioPrefs interface
float getFreq() const override { return _parent->freq; }
void setFreq(float f) override { _parent->freq = f; markDirty(); }
float getBandwidth() const override { return _parent->bw; }
void setBandwidth(float bw) override { _parent->bw = bw; markDirty(); }
uint8_t getSpreadFactor() const override { return _parent->sf; }
void setSpreadFactor(uint8_t sf) override { _parent->sf = sf; markDirty(); }
uint8_t getCodingRate() const override { return _parent->cr; }
void setCodingRate(uint8_t cr) override { _parent->cr = cr; markDirty(); }
float getAirtimeFactor() const override { return _parent->airtime_factor; }
void setAirtimeFactor(float af) override { _parent->airtime_factor = af; markDirty(); }
bool isCadEnabled() const override { return false; }
void setCadEnabled(bool en) override { /* no-op */ }
uint8_t getIntThresh() const override { return 0; }
void setIntThresh(uint8_t t) override { /* no-op */ }
uint8_t getRxGain() const override { return _parent->rx_boosted_gain; }
void setRxGain(uint8_t g) override { _parent->rx_boosted_gain = g; markDirty(); }
uint8_t getTxPower() const override { return _parent->tx_power_dbm; }
void setTxPower(uint8_t dbm) override { _parent->tx_power_dbm = dbm; markDirty(); }
float getRxDelay() const override { return _parent->rx_delay_base; }
void setRxDelay(float d) override { _parent->rx_delay_base = d; markDirty(); }
uint8_t getAgcResetInt() const override { return 0; }
void setAgcResetInt(uint8_t secs) override { /* no-op */ }
uint8_t getHashMode() const override { return _parent->path_hash_mode; }
void setHashMode(uint8_t m) override { _parent->path_hash_mode = m; markDirty(); }
uint8_t getMultiAcks() const override { return _parent->multi_acks; }
void setMultiAcks(uint8_t m) override { _parent->multi_acks = m; markDirty(); }
float getFloodTxDelay() const override { return 0.5f; } // currently hard-coded
void setFloodTxDelay(float d) override { /* no-op */ }
float getDirectTxDelay() const override { return 0.2f; } // currently hard-coded
void setDirectTxDelay(float d) override { /* no-op */ }
uint8_t getFEMRxGain() const override { return _parent->radio_fem_rxgain; }
void setFEMRxGain(uint8_t g) override { _parent->radio_fem_rxgain = g; markDirty(); }
uint8_t getFEMTxGain() const override { return _parent->radio_fem_txgain; }
void setFEMTxGain(uint8_t g) override { _parent->radio_fem_txgain = g; markDirty(); }
};
RadioPrefs radio;

Expand Down Expand Up @@ -121,6 +156,8 @@ class NodePrefs : public ConfigSerializer { // persisted to file
};
CompanionPrefs companion;

DynamicConfigSerializer custom;

protected:
void structure() override {
def("name", node_name, sizeof(node_name));
Expand All @@ -132,14 +169,21 @@ class NodePrefs : public ConfigSerializer { // persisted to file
def("gps", gps);
def("repeat", repeat);
def("comp", companion);
def("custom", custom);
}
public:
NodePrefs() : radio(this), gps(this), companion(this) {
NodePrefs() : radio(this), gps(this), companion(this), custom(&radio) {
node_name[0] = 0;
default_scope_name[0] = 0;
memset(default_scope_key, 0, sizeof(default_scope_key));
}
// new accessor methods
bool isRepeatEn() const { return repeat.disable_fwd == 0; }
void setRepeatEn(bool en) { repeat.disable_fwd = en ? 0 : 1; }

CommonRadioPrefs* getRadioPrefs() { return &radio; }
KeyValueStore* getCustom() { return &custom; }

bool isDirty() const override { return ConfigSerializer::isDirty() || radio.isDirty() || custom.isDirty(); }
void clearDirty() override { ConfigSerializer::clearDirty(); radio.clearDirty(); custom.clearDirty(); }
};
6 changes: 3 additions & 3 deletions examples/simple_repeater/MyMesh.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -704,7 +704,7 @@ void MyMesh::onPeerDataRecv(mesh::Packet *packet, uint8_t type, int sender_idx,
memcpy(&sender_timestamp, data, 4); // timestamp (by sender's RTC clock - which could be wrong)
uint8_t flags = (data[4] >> 2); // message attempt number, and other flags

if (!(flags == TXT_TYPE_PLAIN || flags == TXT_TYPE_CLI_DATA)) {
if (!(flags == TXT_TYPE_PLAIN || flags == TXT_TYPE_CLI_DATA || flags == TXT_TYPE_CLI_COMMAND)) {
MESH_DEBUG_PRINTLN("onPeerDataRecv: unsupported text type received: flags=%02x", (uint32_t)flags);
} else if (sender_timestamp >= client->last_timestamp) { // prevent replay attacks
bool is_retry = (sender_timestamp == client->last_timestamp);
Expand Down Expand Up @@ -982,8 +982,8 @@ 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);
board.setLoRaFemPaGainEnabled(_prefs.radio_fem_txgain);

board.attachDynamicPrefs(_prefs.getCustom());

updateAdvertTimer();
updateFloodAdvertTimer();
Expand Down
8 changes: 4 additions & 4 deletions examples/simple_room_server/MyMesh.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -443,7 +443,7 @@ void MyMesh::onPeerDataRecv(mesh::Packet *packet, uint8_t type, int sender_idx,
memcpy(&sender_timestamp, data, 4); // timestamp (by sender's RTC clock - which could be wrong)
uint8_t flags = (data[4] >> 2); // message attempt number, and other flags

if (!(flags == TXT_TYPE_PLAIN || flags == TXT_TYPE_CLI_DATA)) {
if (!(flags == TXT_TYPE_PLAIN || flags == TXT_TYPE_CLI_DATA || flags == TXT_TYPE_CLI_COMMAND)) {
MESH_DEBUG_PRINTLN("onPeerDataRecv: unsupported command flags received: flags=%02x", (uint32_t)flags);
} else if (sender_timestamp >= client->last_timestamp) { // prevent replay attacks, but send Acks for retries
bool is_retry = (sender_timestamp == client->last_timestamp);
Expand All @@ -463,7 +463,7 @@ void MyMesh::onPeerDataRecv(mesh::Packet *packet, uint8_t type, int sender_idx,

uint8_t temp[166];
bool send_ack;
if (flags == TXT_TYPE_CLI_DATA) {
if (flags == TXT_TYPE_CLI_DATA || flags == TXT_TYPE_CLI_COMMAND) {
if (client->isAdmin()) {
if (is_retry) {
temp[5] = 0; // no reply
Expand Down Expand Up @@ -725,8 +725,8 @@ void MyMesh::begin(FILESYSTEM *fs) {
radio_driver.setParams(_prefs.freq, _prefs.bw, _prefs.sf, _prefs.cr);
radio_driver.setTxPower(_prefs.tx_power_dbm);
radio_driver.setRxBoostedGainMode(_prefs.rx_boosted_gain);
board.setLoRaFemLnaEnabled(_prefs.radio_fem_rxgain);
board.setLoRaFemPaGainEnabled(_prefs.radio_fem_txgain);

board.attachDynamicPrefs(_prefs.getCustom());

updateAdvertTimer();
updateFloodAdvertTimer();
Expand Down
Loading
Loading