Merge branch 'dev' into wio-tracker-L1-1W

This commit is contained in:
Scott Powell
2026-09-15 18:24:52 +10:00
99 changed files with 2528 additions and 491 deletions
+243 -25
View File
@@ -1,6 +1,9 @@
#include "MyMesh.h"
#include <Arduino.h> // needed for PlatformIO
#ifdef ENABLE_WIFI_INTERFACE
#include <WiFi.h>
#endif
#include <Mesh.h>
#define CMD_APP_START 1
@@ -62,6 +65,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
@@ -97,6 +101,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)
@@ -134,6 +139,10 @@
#define ERR_CODE_FILE_IO_ERROR 5
#define ERR_CODE_ILLEGAL_ARG 6
// Copied from simple_repeater (could probably be shared)
#define CTL_TYPE_NODE_DISCOVER_REQ 0x80
#define CTL_TYPE_NODE_DISCOVER_RESP 0x90
#define MAX_SIGN_DATA_LEN (8 * 1024) // 8K
// Auto-add config bitmask
@@ -259,10 +268,10 @@ float MyMesh::getAirtimeBudgetFactor() const {
}
int MyMesh::getInterferenceThreshold() const {
return 0; // disabled for now, until currentRSSI() problem is resolved
return _prefs.interference_threshold;
}
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 {
@@ -271,11 +280,11 @@ int MyMesh::calcRxDelay(float score, uint32_t air_time) const {
}
uint32_t MyMesh::getRetransmitDelay(const mesh::Packet *packet) {
uint32_t t = (_radio->getEstAirtimeFor(packet->getPathByteLen() + packet->payload_len + 2) * 0.5f);
uint32_t t = (_radio->getEstAirtimeFor(packet->getPathByteLen() + packet->payload_len + 2) * _prefs.tx_delay_factor);
return getRNG()->nextInt(0, 5*t + 1);
}
uint32_t MyMesh::getDirectRetransmitDelay(const mesh::Packet *packet) {
uint32_t t = (_radio->getEstAirtimeFor(packet->getPathByteLen() + packet->payload_len + 2) * 0.2f);
uint32_t t = (_radio->getEstAirtimeFor(packet->getPathByteLen() + packet->payload_len + 2) * _prefs.direct_tx_delay_factor);
return getRNG()->nextInt(0, 5*t + 1);
}
@@ -403,6 +412,53 @@ int MyMesh::getRecentlyHeard(AdvertPath dest[], int max_num) {
return max_num;
}
#if defined(DISPLAY_CLASS) && !(defined(UI_NO_DISCOVER_SCREEN) && (UI_NO_DISCOVER_SCREEN + 0 != 0))
int MyMesh::getDiscoveredNodes(DiscoveredNode nodes[], int max_num) {
if (max_num > DISCOVERED_NODES_TABLE_SIZE) max_num = DISCOVERED_NODES_TABLE_SIZE;
if (max_num > disc_nodes_count) max_num = disc_nodes_count;
for (int i = 0; i < max_num; i++) {
nodes[i] = discovered_nodes[i];
}
return max_num;
}
bool MyMesh::requestRepeatersDiscovery() {
uint8_t cmd_bytes[6];
cmd_bytes[0] = CTL_TYPE_NODE_DISCOVER_REQ | 1; // DISCOVER_REQ | prefix only
cmd_bytes[1] = 0xFF; // Repeaters
getRNG()->random(&cmd_bytes[2], 4); // tag
disc_nodes_count = 0;
disc_node_req_tag = *((uint32_t*)&cmd_bytes[2]);
mesh::Packet* req = createControlData(cmd_bytes, sizeof(cmd_bytes));
if (req) {
sendZeroHop(req);
return true;
}
return false;
}
void MyMesh::checkControlDataForPendingDiscovery(uint8_t payload[], size_t p_len) {
if ((p_len < 12)
|| (payload[0] & 0xF0 != CTL_TYPE_NODE_DISCOVER_RESP)
|| (disc_nodes_count >= DISCOVERED_NODES_TABLE_SIZE)
|| (memcmp(&payload[2], &disc_node_req_tag, 4))) {
return;
}
memcpy(&discovered_nodes[disc_nodes_count].pubkey_prefix, &payload[6], 8);
discovered_nodes[disc_nodes_count].type = payload[0] & 0xF;
discovered_nodes[disc_nodes_count].snr_out = ((int8_t)payload[1]) / 4.0;
discovered_nodes[disc_nodes_count].snr_in = _radio->getLastSNR();
ContactInfo* c = lookupContactByPubKey(&payload[6], 8);
if (c != NULL) {
strncpy(discovered_nodes[disc_nodes_count].name, c->name, 32);
} else {
discovered_nodes[disc_nodes_count].name[0] = 0;
}
disc_nodes_count ++;
}
#endif
void MyMesh::onContactPathUpdated(const ContactInfo &contact) {
out_frame[0] = PUSH_CODE_PATH_UPDATED;
memcpy(&out_frame[1], contact.id.pub_key, PUB_KEY_SIZE);
@@ -528,12 +584,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);
@@ -783,6 +850,9 @@ void MyMesh::onControlDataRecv(mesh::Packet *packet) {
MESH_DEBUG_PRINTLN("onControlDataRecv(), payload_len too long: %d", packet->payload_len);
return;
}
#if defined(DISPLAY_CLASS) && !(defined(UI_NO_DISCOVER_SCREEN) && (UI_NO_DISCOVER_SCREEN + 0 != 0))
checkControlDataForPendingDiscovery(packet->payload, packet->payload_len);
#endif
int i = 0;
out_frame[i++] = PUSH_CODE_CONTROL_DATA;
out_frame[i++] = (int8_t)(_radio->getLastSNR() * 4);
@@ -865,6 +935,7 @@ MyMesh::MyMesh(mesh::Radio &radio, mesh::RNG &rng, mesh::RTCClock &rtc, SimpleMe
_serial(NULL), telemetry(MAX_PACKET_PAYLOAD - 4), _store(&store), _ui(ui), _iter(0) {
_iter_started = false;
_cli_rescue = false;
cli_command[0] = 0;
offline_queue_len = 0;
app_target_ver = 0;
clearPendingReqs();
@@ -877,6 +948,8 @@ MyMesh::MyMesh(mesh::Radio &radio, mesh::RNG &rng, mesh::RTCClock &rtc, SimpleMe
// defaults
_prefs.airtime_factor = 1.0;
_prefs.tx_delay_factor = 0.5f;
_prefs.direct_tx_delay_factor = 0.2f;
strcpy(_prefs.node_name, "NONAME");
_prefs.freq = LORA_FREQ;
_prefs.sf = LORA_SF;
@@ -943,6 +1016,8 @@ void MyMesh::begin(bool has_display) {
// sanitise bad pref values
_prefs.rx_delay_base = constrain(_prefs.rx_delay_base, 0, 20.0f);
_prefs.tx_delay_factor = constrain(_prefs.tx_delay_factor, 0, 2.0f);
_prefs.direct_tx_delay_factor = constrain(_prefs.direct_tx_delay_factor, 0, 2.0f);
_prefs.airtime_factor = constrain(_prefs.airtime_factor, 0, 9.0f);
_prefs.freq = constrain(_prefs.freq, 150.0f, 2500.0f);
_prefs.bw = constrain(_prefs.bw, 7.8f, 500.0f);
@@ -980,8 +1055,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");
}
@@ -1086,6 +1162,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++];
@@ -1096,16 +1186,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);
@@ -1184,8 +1274,8 @@ void MyMesh::handleCmdFrame(size_t len) {
writeErrFrame(ERR_CODE_NOT_FOUND); // bad channel_idx
} else if (data_type == DATA_TYPE_RESERVED) {
writeErrFrame(ERR_CODE_ILLEGAL_ARG);
} else if (payload_len > MAX_CHANNEL_DATA_LENGTH) {
MESH_DEBUG_PRINTLN("CMD_SEND_CHANNEL_DATA payload too long: %d > %d", payload_len, MAX_CHANNEL_DATA_LENGTH);
} else if (payload_len > MAX_GROUP_DATA_LENGTH) {
MESH_DEBUG_PRINTLN("CMD_SEND_CHANNEL_DATA payload too long: %d > %d", payload_len, MAX_GROUP_DATA_LENGTH);
writeErrFrame(ERR_CODE_ILLEGAL_ARG);
} else if (sendGroupData(channel.channel, path, path_len, data_type, payload, payload_len)) {
writeOKFrame();
@@ -1515,7 +1605,7 @@ void MyMesh::handleCmdFrame(size_t len) {
#endif
} else if (cmd_frame[0] == CMD_SEND_RAW_DATA && len >= 6) {
int i = 1;
int8_t path_len = cmd_frame[i++];
uint8_t path_len = cmd_frame[i++];
if (path_len >= 0 && mesh::Packet::isValidPathLen(path_len)) {
uint8_t path[MAX_PATH_SIZE];
i += mesh::Packet::writePath(path, &cmd_frame[i], path_len);
@@ -2035,8 +2125,135 @@ 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;
}
#ifdef ENABLE_WIFI_INTERFACE
if (memcmp(command, "set wifi.ssid ", 14) == 0) {
StrHelper::strncpy(_prefs.wifi_ssid, &command[14], sizeof(_prefs.wifi_ssid));
savePrefs();
sprintf(reply, "> wifi.ssid is now %s (set wifi.pwd too, then reboot)", _prefs.wifi_ssid);
return true;
}
if (memcmp(command, "set wifi.pwd ", 13) == 0) {
StrHelper::strncpy(_prefs.wifi_pwd, &command[13], sizeof(_prefs.wifi_pwd));
savePrefs();
strcpy(reply, "> wifi.pwd updated (reboot to apply)");
return true;
}
if (strcmp(command, "set wifi.clear") == 0) {
_prefs.wifi_ssid[0] = 0;
_prefs.wifi_pwd[0] = 0;
savePrefs();
strcpy(reply, "> wifi config cleared (reboot to apply)");
return true;
}
if (strcmp(command, "get wifi.ssid") == 0) { // no 'get wifi.pwd', by design
sprintf(reply, "> %s", _prefs.getWifiSSID()[0] ? _prefs.getWifiSSID() : "(not set)");
return true;
}
if (memcmp(command, "set wifi.enabled ", 17) == 0) {
_prefs.wifi_enabled = atoi(&command[17]) ? 1 : 0;
savePrefs();
sprintf(reply, "> wifi.enabled is now %d (reboot to apply)", _prefs.wifi_enabled);
return true;
}
if (strcmp(command, "get wifi.enabled") == 0) {
sprintf(reply, "> %d", _prefs.wifi_enabled);
return true;
}
if (strcmp(command, "get wifi.status") == 0) {
strcpy(reply, WiFi.status() == WL_CONNECTED ? "> connected" : "> disconnected");
return true;
}
if (strcmp(command, "get wifi.ip") == 0) {
if (WiFi.status() == WL_CONNECTED) {
sprintf(reply, "> %s", WiFi.localIP().toString().c_str());
} else {
strcpy(reply, "> (not connected)");
}
return true;
}
#endif
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;
}
if (strcmp(command, "get tz.offset") == 0) {
sprintf(reply, "> %d", _prefs.tz_offset);
return true;
}
if (memcmp(command, "set tz.offset ", 14) == 0) {
int8_t tz = atof(&command[14]);
if (tz < -12 || tz > 14) {
strcpy(reply, "Error, must be from -12 to +14");
} else {
_prefs.tz_offset = tz;
savePrefs();
strcpy(reply, "OK");
}
return true;
}
return false; // not handled
}
void MyMesh::checkCLIRescueCmd() {
int len = strlen(cli_command);
// `cli_command` must stay NUL-terminated within its bounds. If it ever isn't,
// strlen() above can return >= sizeof(cli_command) and the loop below would
// then index past the buffer, so clamp defensively.
if (len >= (int)sizeof(cli_command)) {
cli_command[0] = 0;
len = 0;
}
while (Serial.available() && len < sizeof(cli_command)-1) {
char c = Serial.read();
if (c != '\n') {
@@ -2045,22 +2262,18 @@ void MyMesh::checkCLIRescueCmd() {
}
Serial.print(c); // echo
}
if (len == sizeof(cli_command)-1) { // command buffer full
cli_command[sizeof(cli_command)-1] = '\r';
if (len == sizeof(cli_command)-1) { // buffer full: treat as a completed line
cli_command[sizeof(cli_command)-2] = '\r'; // place end-of-line marker inside the buffer
cli_command[sizeof(cli_command)-1] = 0; // keep the buffer NUL-terminated
}
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) {
@@ -2241,6 +2454,11 @@ void MyMesh::loop() {
checkCLIRescueCmd();
} else {
checkSerialInterface();
#if defined(ENABLE_WIFI_INTERFACE) && defined(RP2040_PLATFORM) && !defined(ENABLE_USB_INTERFACE)
// RP2040 WiFi builds are headless and have no way into the rescue CLI (that needs a
// display + long-press), so serve config commands on the otherwise unused USB serial
checkCLIRescueCmd();
#endif
}
// is there are pending dirty contacts write needed?