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
19 changes: 19 additions & 0 deletions docs/cli_commands.md
Original file line number Diff line number Diff line change
Expand Up @@ -745,6 +745,25 @@ 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 <hop1_hex,hop2_hex,...>`
- `set outpath clear`
- `set outpath flood`

**Parameters:**
- `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.
- `clear` forgets the current direct path and allows normal path discovery to repopulate it.
- `flood` forces replies to use flood packets until the client logs in again.

---

#### Create a new region
**Usage:**
- `region put <name> [parent_name]`
Expand Down
143 changes: 133 additions & 10 deletions examples/simple_repeater/MyMesh.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -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());
Expand Down Expand Up @@ -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());
}
}
}
Expand All @@ -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) {
Expand All @@ -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());
}
}
}
Expand All @@ -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);
Expand Down Expand Up @@ -1165,7 +1167,100 @@ 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;
}
if (strcmp(spec, "flood") == 0) {
out_path_len = OUT_PATH_FORCE_FLOOD;
return true;
}

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 - bad params";
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 - bad params";
return false;
}

if (hop_count >= 63 || (hop_count + 1) * hash_size > MAX_PATH_SIZE) {
err = "Err - bad params";
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_FORCE_FLOOD) {
snprintf(out, out_len, "> flood");
return;
}
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
Expand Down Expand Up @@ -1242,6 +1337,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 || 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);
}
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++;
Expand Down
2 changes: 1 addition & 1 deletion examples/simple_repeater/MyMesh.h
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down
2 changes: 1 addition & 1 deletion examples/simple_repeater/main.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -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);
}
Expand Down
3 changes: 2 additions & 1 deletion src/helpers/ClientACL.h
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down