Skip to content

Commit 9f1a3ea

Browse files
author
Scott Powell
committed
Merge branch 'dev'
# Conflicts: # docs/cli_commands.md
2 parents eeae32b + 3fe2dd7 commit 9f1a3ea

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

64 files changed

+831
-283
lines changed
Lines changed: 43 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,43 @@
1+
name: PR Build Check
2+
3+
on:
4+
pull_request:
5+
branches: [main, dev]
6+
paths:
7+
- 'src/**'
8+
- 'examples/**'
9+
- 'variants/**'
10+
- 'platformio.ini'
11+
- '.github/workflows/pr-build-check.yml'
12+
13+
jobs:
14+
build:
15+
runs-on: ubuntu-latest
16+
strategy:
17+
fail-fast: false
18+
matrix:
19+
environment:
20+
# ESP32-S3 (most common platform)
21+
- Heltec_v3_companion_radio_ble
22+
- Heltec_v3_repeater
23+
- Heltec_v3_room_server
24+
# nRF52
25+
- RAK_4631_companion_radio_ble
26+
- RAK_4631_repeater
27+
- RAK_4631_room_server
28+
# RP2040
29+
- PicoW_repeater
30+
# STM32
31+
- wio-e5-mini_repeater
32+
# ESP32-C6
33+
- LilyGo_Tlora_C6_repeater_
34+
35+
steps:
36+
- name: Clone Repo
37+
uses: actions/checkout@v4
38+
39+
- name: Setup Build Environment
40+
uses: ./.github/actions/setup-build-environment
41+
42+
- name: Build ${{ matrix.environment }}
43+
run: pio run -e ${{ matrix.environment }}

boards/t_beams3_supreme.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -41,7 +41,7 @@
4141
"name": "LilyGo T-Beam supreme (8MB Flash 8MB PSRAM)",
4242
"upload": {
4343
"flash_size": "8MB",
44-
"maximum_ram_size": 327680,
44+
"maximum_ram_size": 8388608,
4545
"maximum_size": 8388608,
4646
"require_upload_port": true,
4747
"speed": 460800

docs/cli_commands.md

Lines changed: 8 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -53,7 +53,7 @@ This document provides an overview of CLI commands that can be sent to MeshCore
5353
- `time <epoch_seconds>`
5454

5555
**Parameters:**
56-
- `epoc_seconds`: Unix epoc time
56+
- `epoch_seconds`: Unix epoch time
5757

5858
---
5959

@@ -136,7 +136,7 @@ This document provides an overview of CLI commands that can be sent to MeshCore
136136

137137
---
138138

139-
### End capture of rx log to node sotrage
139+
### End capture of rx log to node storage
140140
**Usage:** `log stop`
141141

142142
---
@@ -200,7 +200,7 @@ This document provides an overview of CLI commands that can be sent to MeshCore
200200

201201
**Default:** Varies by board
202202

203-
**Notes:** This setting only controls the power level of the LoRa chip. Some nodes have an additional power amplifier stage which increases the total output. Referr to the node's manual for the correct setting to use. **Setting a value too high may violate the laws in your country.**
203+
**Notes:** This setting only controls the power level of the LoRa chip. Some nodes have an additional power amplifier stage which increases the total output. Refer to the node's manual for the correct setting to use. **Setting a value too high may violate the laws in your country.**
204204

205205
---
206206

@@ -230,6 +230,7 @@ This document provides an overview of CLI commands that can be sent to MeshCore
230230
**Default:** `869.525`
231231

232232
**Note:** Requires reboot to apply
233+
**Serial Only:** `set freq <frequency>`
233234

234235
### System
235236

@@ -295,16 +296,16 @@ This document provides an overview of CLI commands that can be sent to MeshCore
295296

296297
#### Change this node's admin password
297298
**Usage:**
298-
- `password <password>`
299+
- `password <new_password>`
299300

300301
**Parameters:**
301-
- `password`: Admin password
302+
- `new_password`: New admin password
302303

303304
**Set by build flag:** `ADMIN_PASSWORD`
304305

305306
**Default:** `password`
306307

307-
**Note:** Echoed back for confirmation
308+
**Note:** Command reply echoes the updated password for confirmation.
308309

309310
**Note:** Any node using this password will be added to the admin ACL list.
310311

@@ -769,7 +770,7 @@ region save
769770
- `gps advert <policy>`
770771

771772
**Parameters:**
772-
- `policy`: `none`|`shared`|`prefs`
773+
- `policy`: `none`|`share`|`prefs`
773774
- `none`: don't include location in adverts
774775
- `share`: share gps location (from SensorManager)
775776
- `prefs`: location stored in node's lat and lon settings

examples/companion_radio/DataStore.cpp

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -222,12 +222,14 @@ void DataStore::loadPrefsInt(const char *filename, NodePrefs& _prefs, double& no
222222
file.read((uint8_t *)&_prefs.rx_delay_base, sizeof(_prefs.rx_delay_base)); // 72
223223
file.read((uint8_t *)&_prefs.advert_loc_policy, sizeof(_prefs.advert_loc_policy)); // 76
224224
file.read((uint8_t *)&_prefs.multi_acks, sizeof(_prefs.multi_acks)); // 77
225-
file.read(pad, 2); // 78
225+
file.read((uint8_t *)&_prefs.path_hash_mode, sizeof(_prefs.path_hash_mode)); // 78
226+
file.read(pad, 1); // 79
226227
file.read((uint8_t *)&_prefs.ble_pin, sizeof(_prefs.ble_pin)); // 80
227228
file.read((uint8_t *)&_prefs.buzzer_quiet, sizeof(_prefs.buzzer_quiet)); // 84
228229
file.read((uint8_t *)&_prefs.gps_enabled, sizeof(_prefs.gps_enabled)); // 85
229230
file.read((uint8_t *)&_prefs.gps_interval, sizeof(_prefs.gps_interval)); // 86
230231
file.read((uint8_t *)&_prefs.autoadd_config, sizeof(_prefs.autoadd_config)); // 87
232+
file.read((uint8_t *)&_prefs.autoadd_max_hops, sizeof(_prefs.autoadd_max_hops)); // 88
231233

232234
file.close();
233235
}
@@ -257,12 +259,14 @@ void DataStore::savePrefs(const NodePrefs& _prefs, double node_lat, double node_
257259
file.write((uint8_t *)&_prefs.rx_delay_base, sizeof(_prefs.rx_delay_base)); // 72
258260
file.write((uint8_t *)&_prefs.advert_loc_policy, sizeof(_prefs.advert_loc_policy)); // 76
259261
file.write((uint8_t *)&_prefs.multi_acks, sizeof(_prefs.multi_acks)); // 77
260-
file.write(pad, 2); // 78
262+
file.write((uint8_t *)&_prefs.path_hash_mode, sizeof(_prefs.path_hash_mode)); // 78
263+
file.write(pad, 1); // 79
261264
file.write((uint8_t *)&_prefs.ble_pin, sizeof(_prefs.ble_pin)); // 80
262265
file.write((uint8_t *)&_prefs.buzzer_quiet, sizeof(_prefs.buzzer_quiet)); // 84
263266
file.write((uint8_t *)&_prefs.gps_enabled, sizeof(_prefs.gps_enabled)); // 85
264267
file.write((uint8_t *)&_prefs.gps_interval, sizeof(_prefs.gps_interval)); // 86
265268
file.write((uint8_t *)&_prefs.autoadd_config, sizeof(_prefs.autoadd_config)); // 87
269+
file.write((uint8_t *)&_prefs.autoadd_max_hops, sizeof(_prefs.autoadd_max_hops)); // 88
266270

267271
file.close();
268272
}

examples/companion_radio/MyMesh.cpp

Lines changed: 49 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -57,6 +57,7 @@
5757
#define CMD_SET_AUTOADD_CONFIG 58
5858
#define CMD_GET_AUTOADD_CONFIG 59
5959
#define CMD_GET_ALLOWED_REPEAT_FREQ 60
60+
#define CMD_SET_PATH_HASH_MODE 61
6061

6162
// Stats sub-types for CMD_GET_STATS
6263
#define STATS_TYPE_CORE 0
@@ -257,6 +258,15 @@ int MyMesh::calcRxDelay(float score, uint32_t air_time) const {
257258
return (int)((pow(_prefs.rx_delay_base, 0.85f - score) - 1.0) * air_time);
258259
}
259260

261+
uint32_t MyMesh::getRetransmitDelay(const mesh::Packet *packet) {
262+
uint32_t t = (_radio->getEstAirtimeFor(packet->getPathByteLen() + packet->payload_len + 2) * 0.5f);
263+
return getRNG()->nextInt(0, 5*t + 1);
264+
}
265+
uint32_t MyMesh::getDirectRetransmitDelay(const mesh::Packet *packet) {
266+
uint32_t t = (_radio->getEstAirtimeFor(packet->getPathByteLen() + packet->payload_len + 2) * 0.2f);
267+
return getRNG()->nextInt(0, 5*t + 1);
268+
}
269+
260270
uint8_t MyMesh::getExtraAckTransmitCount() const {
261271
return _prefs.multi_acks;
262272
}
@@ -308,6 +318,10 @@ bool MyMesh::shouldOverwriteWhenFull() const {
308318
return (_prefs.autoadd_config & AUTO_ADD_OVERWRITE_OLDEST) != 0;
309319
}
310320

321+
uint8_t MyMesh::getAutoAddMaxHops() const {
322+
return _prefs.autoadd_max_hops;
323+
}
324+
311325
void MyMesh::onContactOverwrite(const uint8_t* pub_key) {
312326
_store->deleteBlobByKey(pub_key, PUB_KEY_SIZE); // delete from storage
313327
if (_serial->isConnected()) {
@@ -340,7 +354,7 @@ void MyMesh::onDiscoveredContact(ContactInfo &contact, bool is_new, uint8_t path
340354
}
341355

342356
// add inbound-path to mem cache
343-
if (path && path_len <= sizeof(AdvertPath::path)) { // check path is valid
357+
if (path && mesh::Packet::isValidPathLen(path_len)) { // check path is valid
344358
AdvertPath* p = advert_paths;
345359
uint32_t oldest = 0xFFFFFFFF;
346360
for (int i = 0; i < ADVERT_PATH_TABLE_SIZE; i++) { // check if already in table, otherwise evict oldest
@@ -357,8 +371,7 @@ void MyMesh::onDiscoveredContact(ContactInfo &contact, bool is_new, uint8_t path
357371
memcpy(p->pubkey_prefix, contact.id.pub_key, sizeof(p->pubkey_prefix));
358372
strcpy(p->name, contact.name);
359373
p->recv_timestamp = getRTCClock()->getCurrentTime();
360-
p->path_len = path_len;
361-
memcpy(p->path, path, p->path_len);
374+
p->path_len = mesh::Packet::copyPath(p->path, path, path_len);
362375
}
363376

364377
if (!is_new) dirty_contacts_expiry = futureMillis(LAZY_CONTACTS_WRITE_DELAY); // only schedule lazy write for contacts that are in contacts[]
@@ -464,23 +477,23 @@ bool MyMesh::allowPacketForward(const mesh::Packet* packet) {
464477
void MyMesh::sendFloodScoped(const ContactInfo& recipient, mesh::Packet* pkt, uint32_t delay_millis) {
465478
// TODO: dynamic send_scope, depending on recipient and current 'home' Region
466479
if (send_scope.isNull()) {
467-
sendFlood(pkt, delay_millis);
480+
sendFlood(pkt, delay_millis, _prefs.path_hash_mode + 1);
468481
} else {
469482
uint16_t codes[2];
470483
codes[0] = send_scope.calcTransportCode(pkt);
471484
codes[1] = 0; // REVISIT: set to 'home' Region, for sender/return region?
472-
sendFlood(pkt, codes, delay_millis);
485+
sendFlood(pkt, codes, delay_millis, _prefs.path_hash_mode + 1);
473486
}
474487
}
475488
void MyMesh::sendFloodScoped(const mesh::GroupChannel& channel, mesh::Packet* pkt, uint32_t delay_millis) {
476489
// TODO: have per-channel send_scope
477490
if (send_scope.isNull()) {
478-
sendFlood(pkt, delay_millis);
491+
sendFlood(pkt, delay_millis, _prefs.path_hash_mode + 1);
479492
} else {
480493
uint16_t codes[2];
481494
codes[0] = send_scope.calcTransportCode(pkt);
482495
codes[1] = 0; // REVISIT: set to 'home' Region, for sender/return region?
483-
sendFlood(pkt, codes, delay_millis);
496+
sendFlood(pkt, codes, delay_millis, _prefs.path_hash_mode + 1);
484497
}
485498
}
486499

@@ -677,7 +690,7 @@ bool MyMesh::onContactPathRecv(ContactInfo& contact, uint8_t* in_path, uint8_t i
677690
if (tag == pending_discovery) { // check for matching response tag)
678691
pending_discovery = 0;
679692

680-
if (in_path_len > MAX_PATH_SIZE || out_path_len > MAX_PATH_SIZE) {
693+
if (!mesh::Packet::isValidPathLen(in_path_len) || !mesh::Packet::isValidPathLen(out_path_len)) {
681694
MESH_DEBUG_PRINTLN("onContactPathRecv, invalid path sizes: %d, %d", in_path_len, out_path_len);
682695
} else {
683696
int i = 0;
@@ -686,11 +699,9 @@ bool MyMesh::onContactPathRecv(ContactInfo& contact, uint8_t* in_path, uint8_t i
686699
memcpy(&out_frame[i], contact.id.pub_key, 6);
687700
i += 6; // pub_key_prefix
688701
out_frame[i++] = out_path_len;
689-
memcpy(&out_frame[i], out_path, out_path_len);
690-
i += out_path_len;
702+
i += mesh::Packet::writePath(&out_frame[i], out_path, out_path_len);
691703
out_frame[i++] = in_path_len;
692-
memcpy(&out_frame[i], in_path, in_path_len);
693-
i += in_path_len;
704+
i += mesh::Packet::writePath(&out_frame[i], in_path, in_path_len);
694705
// NOTE: telemetry data in 'extra' is discarded at present
695706

696707
_serial->writeFrame(out_frame, i);
@@ -776,9 +787,10 @@ uint32_t MyMesh::calcFloodTimeoutMillisFor(uint32_t pkt_airtime_millis) const {
776787
return SEND_TIMEOUT_BASE_MILLIS + (FLOOD_SEND_TIMEOUT_FACTOR * pkt_airtime_millis);
777788
}
778789
uint32_t MyMesh::calcDirectTimeoutMillisFor(uint32_t pkt_airtime_millis, uint8_t path_len) const {
790+
uint8_t path_hash_count = path_len & 63;
779791
return SEND_TIMEOUT_BASE_MILLIS +
780792
((pkt_airtime_millis * DIRECT_SEND_PERHOP_FACTOR + DIRECT_SEND_PERHOP_EXTRA_MILLIS) *
781-
(path_len + 1));
793+
(path_hash_count + 1));
782794
}
783795

784796
void MyMesh::onSendTimeout() {}
@@ -929,6 +941,7 @@ void MyMesh::handleCmdFrame(size_t len) {
929941
StrHelper::strzcpy((char *)&out_frame[i], FIRMWARE_VERSION, 20);
930942
i += 20;
931943
out_frame[i++] = _prefs.client_repeat; // v9+
944+
out_frame[i++] = _prefs.path_hash_mode; // v10+
932945
_serial->writeFrame(out_frame, i);
933946
} else if (cmd_frame[0] == CMD_APP_START &&
934947
len >= 8) { // sent when app establishes connection, respond with node ID
@@ -1106,7 +1119,8 @@ void MyMesh::handleCmdFrame(size_t len) {
11061119
}
11071120
if (pkt) {
11081121
if (len >= 2 && cmd_frame[1] == 1) { // optional param (1 = flood, 0 = zero hop)
1109-
sendFlood(pkt);
1122+
unsigned long delay_millis = 0;
1123+
sendFlood(pkt, delay_millis, _prefs.path_hash_mode + 1);
11101124
} else {
11111125
sendZeroHop(pkt);
11121126
}
@@ -1118,7 +1132,7 @@ void MyMesh::handleCmdFrame(size_t len) {
11181132
uint8_t *pub_key = &cmd_frame[1];
11191133
ContactInfo *recipient = lookupContactByPubKey(pub_key, PUB_KEY_SIZE);
11201134
if (recipient) {
1121-
recipient->out_path_len = -1;
1135+
recipient->out_path_len = OUT_PATH_UNKNOWN;
11221136
// recipient->lastmod = ?? shouldn't be needed, app already has this version of contact
11231137
dirty_contacts_expiry = futureMillis(LAZY_CONTACTS_WRITE_DELAY);
11241138
writeOKFrame();
@@ -1303,6 +1317,14 @@ void MyMesh::handleCmdFrame(size_t len) {
13031317
}
13041318
savePrefs();
13051319
writeOKFrame();
1320+
} else if (cmd_frame[0] == CMD_SET_PATH_HASH_MODE && cmd_frame[1] == 0 && len >= 3) {
1321+
if (cmd_frame[2] >= 3) {
1322+
writeErrFrame(ERR_CODE_ILLEGAL_ARG);
1323+
} else {
1324+
_prefs.path_hash_mode = cmd_frame[2];
1325+
savePrefs();
1326+
writeOKFrame();
1327+
}
13061328
} else if (cmd_frame[0] == CMD_REBOOT && memcmp(&cmd_frame[1], "reboot", 6) == 0) {
13071329
if (dirty_contacts_expiry) { // is there are pending dirty contacts write needed?
13081330
saveContacts();
@@ -1440,7 +1462,7 @@ void MyMesh::handleCmdFrame(size_t len) {
14401462
memset(&req_data[2], 0, 3); // reserved
14411463
getRNG()->random(&req_data[5], 4); // random blob to help make packet-hash unique
14421464
auto save = recipient->out_path_len; // temporarily force sendRequest() to flood
1443-
recipient->out_path_len = -1;
1465+
recipient->out_path_len = OUT_PATH_UNKNOWN;
14441466
int result = sendRequest(*recipient, req_data, sizeof(req_data), tag, est_timeout);
14451467
recipient->out_path_len = save;
14461468
if (result == MSG_SEND_FAILED) {
@@ -1677,11 +1699,12 @@ void MyMesh::handleCmdFrame(size_t len) {
16771699
}
16781700
}
16791701
if (found) {
1680-
out_frame[0] = RESP_CODE_ADVERT_PATH;
1681-
memcpy(&out_frame[1], &found->recv_timestamp, 4);
1682-
out_frame[5] = found->path_len;
1683-
memcpy(&out_frame[6], found->path, found->path_len);
1684-
_serial->writeFrame(out_frame, 6 + found->path_len);
1702+
int i = 0;
1703+
out_frame[i++] = RESP_CODE_ADVERT_PATH;
1704+
memcpy(&out_frame[i], &found->recv_timestamp, 4); i += 4;
1705+
out_frame[i++] = found->path_len;
1706+
i += mesh::Packet::writePath(&out_frame[i], found->path, found->path_len);
1707+
_serial->writeFrame(out_frame, i);
16851708
} else {
16861709
writeErrFrame(ERR_CODE_NOT_FOUND);
16871710
}
@@ -1766,12 +1789,16 @@ void MyMesh::handleCmdFrame(size_t len) {
17661789
}
17671790
} else if (cmd_frame[0] == CMD_SET_AUTOADD_CONFIG) {
17681791
_prefs.autoadd_config = cmd_frame[1];
1792+
if (len >= 3) {
1793+
_prefs.autoadd_max_hops = min(cmd_frame[2], (uint8_t)64);
1794+
}
17691795
savePrefs();
1770-
writeOKFrame();
1796+
writeOKFrame();
17711797
} else if (cmd_frame[0] == CMD_GET_AUTOADD_CONFIG) {
17721798
int i = 0;
17731799
out_frame[i++] = RESP_CODE_AUTOADD_CONFIG;
17741800
out_frame[i++] = _prefs.autoadd_config;
1801+
out_frame[i++] = _prefs.autoadd_max_hops;
17751802
_serial->writeFrame(out_frame, i);
17761803
} else if (cmd_frame[0] == CMD_GET_ALLOWED_REPEAT_FREQ) {
17771804
int i = 0;

examples/companion_radio/MyMesh.h

Lines changed: 6 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -5,14 +5,14 @@
55
#include "AbstractUITask.h"
66

77
/*------------ Frame Protocol --------------*/
8-
#define FIRMWARE_VER_CODE 9
8+
#define FIRMWARE_VER_CODE 10
99

1010
#ifndef FIRMWARE_BUILD_DATE
11-
#define FIRMWARE_BUILD_DATE "15 Feb 2026"
11+
#define FIRMWARE_BUILD_DATE "6 Mar 2026"
1212
#endif
1313

1414
#ifndef FIRMWARE_VERSION
15-
#define FIRMWARE_VERSION "v1.13.0"
15+
#define FIRMWARE_VERSION "v1.14.0"
1616
#endif
1717

1818
#if defined(NRF52_PLATFORM) || defined(STM32_PLATFORM)
@@ -106,6 +106,8 @@ class MyMesh : public BaseChatMesh, public DataStoreHost {
106106
float getAirtimeBudgetFactor() const override;
107107
int getInterferenceThreshold() const override;
108108
int calcRxDelay(float score, uint32_t air_time) const override;
109+
uint32_t getRetransmitDelay(const mesh::Packet *packet) override;
110+
uint32_t getDirectRetransmitDelay(const mesh::Packet *packet) override;
109111
uint8_t getExtraAckTransmitCount() const override;
110112
bool filterRecvFloodPacket(mesh::Packet* packet) override;
111113
bool allowPacketForward(const mesh::Packet* packet) override;
@@ -117,6 +119,7 @@ class MyMesh : public BaseChatMesh, public DataStoreHost {
117119
bool isAutoAddEnabled() const override;
118120
bool shouldAutoAddContactType(uint8_t type) const override;
119121
bool shouldOverwriteWhenFull() const override;
122+
uint8_t getAutoAddMaxHops() const override;
120123
void onContactsFull() override;
121124
void onContactOverwrite(const uint8_t* pub_key) override;
122125
bool onContactPathRecv(ContactInfo& from, uint8_t* in_path, uint8_t in_path_len, uint8_t* out_path, uint8_t out_path_len, uint8_t extra_type, uint8_t* extra, uint8_t extra_len) override;

examples/companion_radio/NodePrefs.h

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -29,4 +29,6 @@ struct NodePrefs { // persisted to file
2929
uint32_t gps_interval; // GPS read interval in seconds
3030
uint8_t autoadd_config; // bitmask for auto-add contacts config
3131
uint8_t client_repeat;
32+
uint8_t path_hash_mode; // which path mode to use when sending
33+
uint8_t autoadd_max_hops; // 0 = no limit, 1 = direct (0 hops), N = up to N-1 hops (max 64)
3234
};

0 commit comments

Comments
 (0)