Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
54 commits
Select commit Hold shift + click to select a range
3e82827
thinknode_m8: initial support
fdlamotte Aug 8, 2026
114093e
fix(lr2021): setTxPower() could leave the receiver down until reboot
fkallay1 Aug 15, 2026
0a595ae
gxepddisplay: better handling of full refresh (no faint partial after…
fdlamotte Aug 16, 2026
a8a6001
Duplicate MAX_CLIENTS definition
entr0p1 Aug 17, 2026
9387f55
trimmed comment
oltaco Aug 17, 2026
45c0c32
Merge pull request #3218 from fkallay1/fix/lr2021-set-tx-power-rx-stall
oltaco Aug 17, 2026
de0d503
M8: preview messages up to 164 cars
fdlamotte Aug 17, 2026
7f664f1
gxepd: do full refresh before screen off ... so wakes quicker ;)
fdlamotte Aug 17, 2026
2b0ed00
fix(sensors): claim an I2C address after a successful init
agessaman Aug 17, 2026
2a6eabe
fix(sensors): probe BMP/BME at both 0x76 and 0x77
agessaman Aug 17, 2026
0cce919
Fix for CMD_SEND_RAW_DATA and multi-byte paths
ripplebiz Aug 18, 2026
38f10cd
Fixes BLE exceeding flash size
DominikTyrala Aug 17, 2026
d65da5a
fix: move MeshadventurerBoard.h to correct location
recrof Aug 18, 2026
2db4b40
repeater: set `loop.detect` to `minimal` as default for new installs
recrof Aug 18, 2026
5a30456
Merge pull request #3239 from recrof/default-loop-detect-minimal
ripplebiz Aug 18, 2026
808994d
Merge pull request #3238 from recrof/meshadventurer-board-relocate
ripplebiz Aug 18, 2026
29b3346
Merge pull request #3180 from fdlamotte/thinknode_m8
fdlamotte Aug 18, 2026
612b217
updated gcc toolchain for nrf52 boards
liamcottle Aug 19, 2026
dd17105
Merge pull request #3246 from liamcottle/update/toolchain
ripplebiz Aug 19, 2026
dab6b63
Merge pull request #3235 from meshcore-dev/send-raw-direct-fix
oltaco Aug 19, 2026
67d6d42
fix for 3-byte paths passed to CMD_SEND_RAW_DATA
liamcottle Aug 19, 2026
b762420
Skip empty channel slots in searchChannelsByHash
dl9sau Aug 19, 2026
3a440ac
Add charging indicator to companion_radio battery icon
hansimgamr Jul 28, 2026
41c60da
Show plug at >=95% instead of exactly 100%
hansimgamr Jul 28, 2026
8d624b7
Merge pull request #3250 from hansimgamr/feat/t114-charging-indicator
ripplebiz Aug 20, 2026
4057095
Merge pull request #3249 from dl9sau/patch-1
ripplebiz Aug 20, 2026
a570911
Merge pull request #3236 from DominikTyrala/fix/c3_partition_setup
ripplebiz Aug 20, 2026
c0f30d7
Merge pull request #3234 from agessaman/fix/sensors-bosch-alt-i2c-add…
ripplebiz Aug 20, 2026
e003187
Merge pull request #3233 from entr0p1/fix/centralise-max-clients
ripplebiz Aug 20, 2026
afb969c
* initial wiring/routing of CLI commands to companion (either via app…
ripplebiz Aug 21, 2026
0b652b5
add support for rak12500 and rak12501 gps on rak3401
liamcottle Aug 22, 2026
9d7cee6
Merge pull request #3268 from liamcottle/rak/gps
ripplebiz Aug 22, 2026
ab4c338
don't toggle io pins on rak3401 when probing gps
liamcottle Aug 23, 2026
41588d8
* adding new CommonRadioPrefs
ripplebiz Aug 23, 2026
b4f7e94
* CommonRadioPrefs refactors done
ripplebiz Aug 23, 2026
2117976
"Unknown command" replies
ripplebiz Aug 23, 2026
f7c568e
onCommandDataRecv() use '>' prefix for CLI replies.
ripplebiz Aug 23, 2026
52306bf
legacy queueMessage() case, eg. repeater replies
ripplebiz Aug 23, 2026
2c0ace2
* new TXT_TYPE_CLI_COMMAND (3)
ripplebiz Aug 23, 2026
e485d01
support for CMD_SEND_TXT_MSG and new TXT_TYPE_CLI_COMMAND
ripplebiz Aug 23, 2026
47b0b7b
* T096, and Station G3: refactoring the 'FEM' prefs to variant-speci…
ripplebiz Aug 23, 2026
a1cf5bd
* refactored 'FEM' commands for HeltecTrackerV2 & HeltecV4
ripplebiz Aug 23, 2026
7dc2d54
add UI_NO_HIBERNATE build flag to disable hibernate screen on wio tra…
liamcottle Aug 24, 2026
b249990
Merge pull request #3290 from liamcottle/feature/ui-no-hibernate
oltaco Aug 24, 2026
5a162ff
* new DynamicConfigSerializer
ripplebiz Aug 24, 2026
845242f
* prefs, custom dirty state
ripplebiz Aug 24, 2026
12998cb
Merge pull request #3275 from liamcottle/gps/rak3401
liamcottle Aug 24, 2026
8ccc992
* DynamicConfigSerializer fixes, and unit tests
ripplebiz Aug 24, 2026
6dad3d5
* companion: fix for setSpreadFactor(). "set cad ..." and "board" no…
ripplebiz Aug 24, 2026
9ab1315
* fix for RPI Picow
ripplebiz Aug 25, 2026
1c32489
Merge pull request #3298 from meshcore-dev/companion-cli
liamcottle Aug 27, 2026
e3d0f9f
fix cad cli command on companion
liamcottle Aug 27, 2026
65650bb
New TXT_TYPE_CLI_COMMAND (3)
ripplebiz Aug 28, 2026
99a7e99
Merge upstream/dev into observer-firmware-dev
agessaman Aug 29, 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
53 changes: 53 additions & 0 deletions boards/thinknode_m8.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,53 @@
{
"build": {
"arduino": {
"ldscript": "nrf52840_s140_v6.ld"
},
"core": "nRF5",
"cpu": "cortex-m4",
"extra_flags": "-DARDUINO_NRF52840_ThinkNode_M8 -DNRF52840_XXAA",
"f_cpu": "64000000L",
"hwids": [
["0x239A", "0x4405"],
["0x239A", "0x0029"],
["0x239A", "0x002A"]
],
"usb_product": "elecrow_thinknode_m8",
"mcu": "nrf52840",
"variant": "ELECROW-ThinkNode-M8",
"variants_dir": "variants",
"bsp": {
"name": "adafruit"
},
"softdevice": {
"sd_flags": "-DS140",
"sd_name": "s140",
"sd_version": "6.1.1",
"sd_fwid": "0x00B6"
},
"bootloader": {
"settings_addr": "0xFF000"
}
},
"connectivity": ["bluetooth"],
"debug": {
"jlink_device": "nRF52840_xxAA",
"onboard_tools": ["jlink"],
"svd_path": "nrf52840.svd",
"openocd_target": "nrf52840-mdk-rs"
},
"frameworks": ["arduino"],
"name": "elecrow thinknode m8",
"upload": {
"maximum_ram_size": 248832,
"maximum_size": 815104,
"speed": 115200,
"protocol": "nrfutil",
"protocols": ["jlink", "nrfjprog", "nrfutil", "stlink"],
"use_1200bps_touch": true,
"require_upload_port": true,
"wait_for_upload_port": true
},
"url": "",
"vendor": "ELECROW"
}
3 changes: 2 additions & 1 deletion docs/payloads.md
Original file line number Diff line number Diff line change
Expand Up @@ -171,8 +171,9 @@ txt_type
| Value | Description | Message content |
|--------|---------------------------|--------------------------------------------------------------------------|
| `0x00` | plain text message | the plain text of the message |
| `0x01` | CLI command | the command text of the message |
| `0x01` | CLI data | CLI command OR reply text |
| `0x02` | signed plain text message | first four bytes is sender pubkey prefix, followed by plain text message |
| `0x03` | CLI command | (since v1.18+) CLI command text (explicit) |

## Anonymous request

Expand Down
135 changes: 109 additions & 26 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 @@ -259,7 +261,7 @@ float MyMesh::getAirtimeBudgetFactor() const {
}

bool MyMesh::getCADEnabled() const {
return false; // hardware CAD before TX (disabled by default, until configurable)
return _prefs.cad_enabled;
}

int MyMesh::getInterferenceThreshold() const {
Expand Down Expand Up @@ -529,12 +531,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 @@ -977,8 +990,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");
// NOTE: no FEM LNA wiring here — companion has its own NodePrefs without
Expand Down Expand Up @@ -1085,6 +1099,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 @@ -1095,16 +1123,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 @@ -1514,16 +1542,20 @@ 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++];
if (path_len >= 0 && i + path_len + 4 <= len) { // minimum 4 byte payload
uint8_t *path = &cmd_frame[i];
i += path_len;
auto pkt = createRawData(&cmd_frame[i], len - i);
if (pkt) {
sendDirect(pkt, path, path_len);
writeOKFrame();
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);
if (i + 4 > len) { // min payload 4 bytes
writeErrFrame(ERR_CODE_ILLEGAL_ARG);
} else {
writeErrFrame(ERR_CODE_TABLE_FULL);
auto pkt = createRawData(&cmd_frame[i], len - i);
if (pkt) {
sendDirect(pkt, path, path_len);
writeOKFrame();
} else {
writeErrFrame(ERR_CODE_TABLE_FULL);
}
}
} else {
writeErrFrame(ERR_CODE_UNSUPPORTED_CMD); // flood, not supported (yet)
Expand Down Expand Up @@ -2030,6 +2062,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 @@ -2047,15 +2135,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
Loading
Loading