From 784f6c7a09c3421b9e02a24dc278a79368611461 Mon Sep 17 00:00:00 2001 From: mikecarper Date: Mon, 18 May 2026 16:06:33 -0700 Subject: [PATCH 1/4] Add repeater outpath CLI commands --- docs/cli_commands.md | 18 ++++ examples/simple_repeater/MyMesh.cpp | 149 +++++++++++++++++++++++++++- examples/simple_repeater/MyMesh.h | 2 +- examples/simple_repeater/main.cpp | 2 +- 4 files changed, 167 insertions(+), 4 deletions(-) diff --git a/docs/cli_commands.md b/docs/cli_commands.md index 99dced3658..1600ceb0ae 100644 --- a/docs/cli_commands.md +++ b/docs/cli_commands.md @@ -745,6 +745,24 @@ This document provides an overview of CLI commands that can be sent to MeshCore --- +#### View or set the direct path override for the current remote client +**Usage:** +- `get outpath` +- `set outpath ` +- `set outpath ` +- `set outpath clear` + +**Parameters:** +- `hash_size`: Path hash size (`1`, `2`, or `3` bytes per hop) +- `path_hex`: Concatenated hop hashes (must align to `hash_size`) +- `hopN_hex`: Comma-separated hop hashes, each `2`, `4`, or `6` hex characters and all the same width + +**Notes:** +- These commands require remote client context (they target the caller's ACL entry). +- `outpath` overrides the primary direct route used for replies to the caller. + +--- + #### Create a new region **Usage:** - `region put [parent_name]` diff --git a/examples/simple_repeater/MyMesh.cpp b/examples/simple_repeater/MyMesh.cpp index 666f79fc5c..1ef7837972 100644 --- a/examples/simple_repeater/MyMesh.cpp +++ b/examples/simple_repeater/MyMesh.cpp @@ -719,7 +719,7 @@ void MyMesh::onPeerDataRecv(mesh::Packet *packet, uint8_t type, int sender_idx, if (is_retry) { *reply = 0; } else { - handleCommand(sender_timestamp, command, reply); + handleCommand(sender_timestamp, client, command, reply); } int text_len = strlen(reply); if (text_len > 0) { @@ -1165,7 +1165,124 @@ void MyMesh::clearStats() { ((SimpleMeshTables *)getTables())->resetStats(); } -void MyMesh::handleCommand(uint32_t sender_timestamp, char *command, char *reply) { +static char* trimSpaces(char* s) { + while (*s == ' ') s++; + char* end = s + strlen(s); + while (end > s && end[-1] == ' ') end--; + *end = 0; + return s; +} + +static bool parsePathCommand(char* raw, uint8_t* out_path, uint8_t& out_path_len, const char*& err) { + if (raw == NULL || out_path == NULL) { + err = "Err - bad params"; + return false; + } + + char* spec = trimSpaces(raw); + if (*spec == 0) { + err = "Err - missing path"; + return false; + } + if (strcmp(spec, "clear") == 0 || strcmp(spec, "-") == 0 || strcmp(spec, "none") == 0) { + out_path_len = OUT_PATH_UNKNOWN; + return true; + } + + char* space = strchr(spec, ' '); + if (space != NULL) { + char* sep = space; + *space++ = 0; + space = trimSpaces(space); + + char* end_ptr = NULL; + long hash_size = strtol(spec, &end_ptr, 10); + if (end_ptr != spec && *end_ptr == 0 && hash_size >= 1 && hash_size <= 3 && *space != 0) { + int hex_len = strlen(space); + int step = (int)hash_size * 2; + if ((hex_len % step) != 0) { + err = "Err - hex length must align to hash size"; + return false; + } + + int hop_count = hex_len / step; + if (hop_count <= 0 || hop_count > 63 || hop_count * hash_size > MAX_PATH_SIZE) { + err = "Err - invalid hop count"; + return false; + } + if (!mesh::Utils::fromHex(out_path, hop_count * hash_size, space)) { + err = "Err - bad hex"; + return false; + } + out_path_len = (((uint8_t)hash_size - 1) << 6) | ((uint8_t)hop_count & 63); + return true; + } + + *sep = ' '; + } + + uint8_t hash_size = 0; + uint8_t hop_count = 0; + char* token = spec; + while (token && *token) { + char* comma = strchr(token, ','); + if (comma) *comma = 0; + token = trimSpaces(token); + + int hex_len = strlen(token); + if (!(hex_len == 2 || hex_len == 4 || hex_len == 6)) { + err = "Err - each hop must be 1/2/3 bytes hex"; + return false; + } + + uint8_t hop_hash_size = (uint8_t)(hex_len / 2); + if (hash_size == 0) { + hash_size = hop_hash_size; + } else if (hash_size != hop_hash_size) { + err = "Err - mixed hash sizes in path"; + return false; + } + + if (hop_count >= 63 || (hop_count + 1) * hash_size > MAX_PATH_SIZE) { + err = "Err - path too long"; + return false; + } + if (!mesh::Utils::fromHex(&out_path[hop_count * hash_size], hash_size, token)) { + err = "Err - bad hex"; + return false; + } + + hop_count++; + token = comma ? comma + 1 : NULL; + } + + if (hash_size == 0 || hop_count == 0) { + err = "Err - missing path"; + return false; + } + out_path_len = ((hash_size - 1) << 6) | (hop_count & 63); + return true; +} + +static void formatPathReply(const uint8_t* path, uint8_t path_len, char* out, size_t out_len) { + if (path_len == OUT_PATH_UNKNOWN) { + snprintf(out, out_len, "> unknown"); + return; + } + if (!mesh::Packet::isValidPathLen(path_len)) { + snprintf(out, out_len, "> invalid"); + return; + } + + uint8_t hash_size = (path_len >> 6) + 1; + uint8_t hop_count = path_len & 63; + uint8_t byte_len = hop_count * hash_size; + char hex[(MAX_PATH_SIZE * 2) + 1]; + mesh::Utils::toHex(hex, path, byte_len); + snprintf(out, out_len, "> hs=%u hops=%u hex=%s", (uint32_t)hash_size, (uint32_t)hop_count, hex); +} + +void MyMesh::handleCommand(uint32_t sender_timestamp, ClientInfo* sender, char *command, char *reply) { if (region_load_active) { if (StrHelper::isBlank(command)) { // empty/blank line, signal to terminate 'load' operation region_map = temp_map; // copy over the temp instance as new current map @@ -1242,6 +1359,34 @@ void MyMesh::handleCommand(uint32_t sender_timestamp, char *command, char *reply Serial.printf("\n"); } reply[0] = 0; + } else if (strcmp(command, "get outpath") == 0 + || strcmp(command, "set outpath") == 0 + || strncmp(command, "set outpath ", 12) == 0) { + bool is_get = strncmp(command, "get ", 4) == 0; + if (sender == NULL) { + strcpy(reply, "Err - command needs remote client context"); + } else if (is_get) { + formatPathReply(sender->out_path, sender->out_path_len, reply, 160); + } else { + char* spec = command + 11; // length of "set outpath" + if (*spec == ' ') spec++; + + uint8_t path[MAX_PATH_SIZE]; + uint8_t path_len = OUT_PATH_UNKNOWN; + const char* err = NULL; + if (!parsePathCommand(spec, path, path_len, err)) { + strcpy(reply, err ? err : "Err - invalid path"); + } else { + if (path_len == OUT_PATH_UNKNOWN) { + sender->out_path_len = OUT_PATH_UNKNOWN; + memset(sender->out_path, 0, sizeof(sender->out_path)); + } else { + sender->out_path_len = mesh::Packet::copyPath(sender->out_path, path, path_len); + } + dirty_contacts_expiry = futureMillis(LAZY_CONTACTS_WRITE_DELAY); + formatPathReply(sender->out_path, sender->out_path_len, reply, 160); + } + } } else if (memcmp(command, "discover.neighbors", 18) == 0) { const char* sub = command + 18; while (*sub == ' ') sub++; diff --git a/examples/simple_repeater/MyMesh.h b/examples/simple_repeater/MyMesh.h index 8ed0317e69..07900882c5 100644 --- a/examples/simple_repeater/MyMesh.h +++ b/examples/simple_repeater/MyMesh.h @@ -223,7 +223,7 @@ class MyMesh : public mesh::Mesh, public CommonCLICallbacks { void saveIdentity(const mesh::LocalIdentity& new_id) override; void clearStats() override; - void handleCommand(uint32_t sender_timestamp, char* command, char* reply); + void handleCommand(uint32_t sender_timestamp, ClientInfo* sender, char* command, char* reply); void loop(); #if defined(WITH_BRIDGE) diff --git a/examples/simple_repeater/main.cpp b/examples/simple_repeater/main.cpp index e37078ce5f..4841800a50 100644 --- a/examples/simple_repeater/main.cpp +++ b/examples/simple_repeater/main.cpp @@ -124,7 +124,7 @@ void loop() { Serial.print('\n'); command[len - 1] = 0; // replace newline with C string null terminator char reply[160]; - the_mesh.handleCommand(0, command, reply); // NOTE: there is no sender_timestamp via serial! + the_mesh.handleCommand(0, NULL, command, reply); // NOTE: there is no sender_timestamp via serial! if (reply[0]) { Serial.print(" -> "); Serial.println(reply); } From 6784a8f3f65e7172df359afb5f1d0f6f351f8725 Mon Sep 17 00:00:00 2001 From: mikecarper Date: Mon, 18 May 2026 16:10:57 -0700 Subject: [PATCH 2/4] Infer repeater outpath hash size from hops --- docs/cli_commands.md | 6 ++--- examples/simple_repeater/MyMesh.cpp | 38 +++-------------------------- 2 files changed, 5 insertions(+), 39 deletions(-) diff --git a/docs/cli_commands.md b/docs/cli_commands.md index 1600ceb0ae..bab45c11f3 100644 --- a/docs/cli_commands.md +++ b/docs/cli_commands.md @@ -748,17 +748,15 @@ This document provides an overview of CLI commands that can be sent to MeshCore #### View or set the direct path override for the current remote client **Usage:** - `get outpath` -- `set outpath ` - `set outpath ` - `set outpath clear` **Parameters:** -- `hash_size`: Path hash size (`1`, `2`, or `3` bytes per hop) -- `path_hex`: Concatenated hop hashes (must align to `hash_size`) -- `hopN_hex`: Comma-separated hop hashes, each `2`, `4`, or `6` hex characters and all the same width +- `hopN_hex`: Hop hash, `2`, `4`, or `6` hex characters. All hops must use the same width. **Notes:** - These commands require remote client context (they target the caller's ACL entry). +- The path hash size is inferred from the hop hash width. - `outpath` overrides the primary direct route used for replies to the caller. --- diff --git a/examples/simple_repeater/MyMesh.cpp b/examples/simple_repeater/MyMesh.cpp index 1ef7837972..fb03764fe3 100644 --- a/examples/simple_repeater/MyMesh.cpp +++ b/examples/simple_repeater/MyMesh.cpp @@ -1189,38 +1189,6 @@ static bool parsePathCommand(char* raw, uint8_t* out_path, uint8_t& out_path_len return true; } - char* space = strchr(spec, ' '); - if (space != NULL) { - char* sep = space; - *space++ = 0; - space = trimSpaces(space); - - char* end_ptr = NULL; - long hash_size = strtol(spec, &end_ptr, 10); - if (end_ptr != spec && *end_ptr == 0 && hash_size >= 1 && hash_size <= 3 && *space != 0) { - int hex_len = strlen(space); - int step = (int)hash_size * 2; - if ((hex_len % step) != 0) { - err = "Err - hex length must align to hash size"; - return false; - } - - int hop_count = hex_len / step; - if (hop_count <= 0 || hop_count > 63 || hop_count * hash_size > MAX_PATH_SIZE) { - err = "Err - invalid hop count"; - return false; - } - if (!mesh::Utils::fromHex(out_path, hop_count * hash_size, space)) { - err = "Err - bad hex"; - return false; - } - out_path_len = (((uint8_t)hash_size - 1) << 6) | ((uint8_t)hop_count & 63); - return true; - } - - *sep = ' '; - } - uint8_t hash_size = 0; uint8_t hop_count = 0; char* token = spec; @@ -1231,7 +1199,7 @@ static bool parsePathCommand(char* raw, uint8_t* out_path, uint8_t& out_path_len int hex_len = strlen(token); if (!(hex_len == 2 || hex_len == 4 || hex_len == 6)) { - err = "Err - each hop must be 1/2/3 bytes hex"; + err = "Err - bad params"; return false; } @@ -1239,12 +1207,12 @@ static bool parsePathCommand(char* raw, uint8_t* out_path, uint8_t& out_path_len if (hash_size == 0) { hash_size = hop_hash_size; } else if (hash_size != hop_hash_size) { - err = "Err - mixed hash sizes in path"; + err = "Err - bad params"; return false; } if (hop_count >= 63 || (hop_count + 1) * hash_size > MAX_PATH_SIZE) { - err = "Err - path too long"; + err = "Err - bad params"; return false; } if (!mesh::Utils::fromHex(&out_path[hop_count * hash_size], hash_size, token)) { From 60429c9b4b7bf3c84e07a161386aec00dd06fca3 Mon Sep 17 00:00:00 2001 From: mikecarper Date: Mon, 18 May 2026 16:37:45 -0700 Subject: [PATCH 3/4] Add repeater outpath flood mode --- docs/cli_commands.md | 3 +++ examples/simple_repeater/MyMesh.cpp | 32 +++++++++++++++++++---------- src/helpers/ClientACL.h | 3 ++- 3 files changed, 26 insertions(+), 12 deletions(-) diff --git a/docs/cli_commands.md b/docs/cli_commands.md index bab45c11f3..38fbebf6f7 100644 --- a/docs/cli_commands.md +++ b/docs/cli_commands.md @@ -750,6 +750,7 @@ This document provides an overview of CLI commands that can be sent to MeshCore - `get outpath` - `set outpath ` - `set outpath clear` +- `set outpath flood` **Parameters:** - `hopN_hex`: Hop hash, `2`, `4`, or `6` hex characters. All hops must use the same width. @@ -758,6 +759,8 @@ This document provides an overview of CLI commands that can be sent to MeshCore - These commands require remote client context (they target the caller's ACL entry). - The path hash size is inferred from the hop hash width. - `outpath` overrides the primary direct route used for replies to the caller. +- `clear` forgets the current direct path and allows normal path discovery to repopulate it. +- `flood` forces replies to use flood packets and ignores later discovered direct paths. --- diff --git a/examples/simple_repeater/MyMesh.cpp b/examples/simple_repeater/MyMesh.cpp index fb03764fe3..4c25d9a445 100644 --- a/examples/simple_repeater/MyMesh.cpp +++ b/examples/simple_repeater/MyMesh.cpp @@ -128,7 +128,7 @@ uint8_t MyMesh::handleLoginReq(const mesh::Identity& sender, const uint8_t* secr } } - if (is_flood) { + if (is_flood && client->out_path_len != OUT_PATH_FORCE_FLOOD) { client->out_path_len = OUT_PATH_UNKNOWN; // need to rediscover out_path } @@ -672,7 +672,7 @@ void MyMesh::onPeerDataRecv(mesh::Packet *packet, uint8_t type, int sender_idx, mesh::Packet *reply = createDatagram(PAYLOAD_TYPE_RESPONSE, client->id, secret, reply_data, reply_len); if (reply) { - if (client->out_path_len != OUT_PATH_UNKNOWN) { // we have an out_path, so send DIRECT + if (mesh::Packet::isValidPathLen(client->out_path_len)) { // we have an out_path, so send DIRECT sendDirect(reply, client->out_path, client->out_path_len, SERVER_RESPONSE_DELAY); } else { sendFloodReply(reply, SERVER_RESPONSE_DELAY, packet->getPathHashSize()); @@ -705,10 +705,10 @@ void MyMesh::onPeerDataRecv(mesh::Packet *packet, uint8_t type, int sender_idx, mesh::Packet *ack = createAck(ack_hash); if (ack) { - if (client->out_path_len == OUT_PATH_UNKNOWN) { - sendFloodReply(ack, TXT_ACK_DELAY, packet->getPathHashSize()); - } else { + if (mesh::Packet::isValidPathLen(client->out_path_len)) { sendDirect(ack, client->out_path, client->out_path_len, TXT_ACK_DELAY); + } else { + sendFloodReply(ack, TXT_ACK_DELAY, packet->getPathHashSize()); } } } @@ -733,10 +733,10 @@ void MyMesh::onPeerDataRecv(mesh::Packet *packet, uint8_t type, int sender_idx, auto reply = createDatagram(PAYLOAD_TYPE_TXT_MSG, client->id, secret, temp, 5 + text_len); if (reply) { - if (client->out_path_len == OUT_PATH_UNKNOWN) { - sendFloodReply(reply, CLI_REPLY_DELAY_MILLIS, packet->getPathHashSize()); - } else { + if (mesh::Packet::isValidPathLen(client->out_path_len)) { sendDirect(reply, client->out_path, client->out_path_len, CLI_REPLY_DELAY_MILLIS); + } else { + sendFloodReply(reply, CLI_REPLY_DELAY_MILLIS, packet->getPathHashSize()); } } } @@ -756,7 +756,9 @@ bool MyMesh::onPeerPathRecv(mesh::Packet *packet, int sender_idx, const uint8_t auto client = acl.getClientByIdx(i); // store a copy of path, for sendDirect() - client->out_path_len = mesh::Packet::copyPath(client->out_path, path, path_len); + if (client->out_path_len != OUT_PATH_FORCE_FLOOD) { + client->out_path_len = mesh::Packet::copyPath(client->out_path, path, path_len); + } client->last_activity = getRTCClock()->getCurrentTime(); } else { MESH_DEBUG_PRINTLN("onPeerPathRecv: invalid peer idx: %d", i); @@ -1188,6 +1190,10 @@ static bool parsePathCommand(char* raw, uint8_t* out_path, uint8_t& out_path_len out_path_len = OUT_PATH_UNKNOWN; return true; } + if (strcmp(spec, "flood") == 0) { + out_path_len = OUT_PATH_FORCE_FLOOD; + return true; + } uint8_t hash_size = 0; uint8_t hop_count = 0; @@ -1233,6 +1239,10 @@ static bool parsePathCommand(char* raw, uint8_t* out_path, uint8_t& out_path_len } static void formatPathReply(const uint8_t* path, uint8_t path_len, char* out, size_t out_len) { + if (path_len == OUT_PATH_FORCE_FLOOD) { + snprintf(out, out_len, "> flood"); + return; + } if (path_len == OUT_PATH_UNKNOWN) { snprintf(out, out_len, "> unknown"); return; @@ -1345,9 +1355,9 @@ void MyMesh::handleCommand(uint32_t sender_timestamp, ClientInfo* sender, char * if (!parsePathCommand(spec, path, path_len, err)) { strcpy(reply, err ? err : "Err - invalid path"); } else { - if (path_len == OUT_PATH_UNKNOWN) { - sender->out_path_len = OUT_PATH_UNKNOWN; + if (path_len == OUT_PATH_UNKNOWN || path_len == OUT_PATH_FORCE_FLOOD) { memset(sender->out_path, 0, sizeof(sender->out_path)); + sender->out_path_len = path_len; } else { sender->out_path_len = mesh::Packet::copyPath(sender->out_path, path, path_len); } diff --git a/src/helpers/ClientACL.h b/src/helpers/ClientACL.h index b758f7068d..e0b8c54287 100644 --- a/src/helpers/ClientACL.h +++ b/src/helpers/ClientACL.h @@ -10,7 +10,8 @@ #define PERM_ACL_READ_WRITE 2 #define PERM_ACL_ADMIN 3 -#define OUT_PATH_UNKNOWN 0xFF +#define OUT_PATH_FORCE_FLOOD 0xFE +#define OUT_PATH_UNKNOWN 0xFF struct ClientInfo { mesh::Identity id; From d762c61347f6affe6a9d7a84e435de08d3b6ac82 Mon Sep 17 00:00:00 2001 From: mikecarper Date: Mon, 18 May 2026 16:38:45 -0700 Subject: [PATCH 4/4] Reset repeater flood outpath on login --- docs/cli_commands.md | 2 +- examples/simple_repeater/MyMesh.cpp | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/docs/cli_commands.md b/docs/cli_commands.md index 38fbebf6f7..2cb818c618 100644 --- a/docs/cli_commands.md +++ b/docs/cli_commands.md @@ -760,7 +760,7 @@ This document provides an overview of CLI commands that can be sent to MeshCore - The path hash size is inferred from the hop hash width. - `outpath` overrides the primary direct route used for replies to the caller. - `clear` forgets the current direct path and allows normal path discovery to repopulate it. -- `flood` forces replies to use flood packets and ignores later discovered direct paths. +- `flood` forces replies to use flood packets until the client logs in again. --- diff --git a/examples/simple_repeater/MyMesh.cpp b/examples/simple_repeater/MyMesh.cpp index 4c25d9a445..2cc6f117fd 100644 --- a/examples/simple_repeater/MyMesh.cpp +++ b/examples/simple_repeater/MyMesh.cpp @@ -128,7 +128,7 @@ uint8_t MyMesh::handleLoginReq(const mesh::Identity& sender, const uint8_t* secr } } - if (is_flood && client->out_path_len != OUT_PATH_FORCE_FLOOD) { + if (is_flood) { client->out_path_len = OUT_PATH_UNKNOWN; // need to rediscover out_path }