Expand full Companion terminal tooling

This commit is contained in:
mikecarper
2026-08-10 14:53:39 -07:00
parent 88ba5b26cb
commit 4e91cc9487
10 changed files with 1235 additions and 41 deletions
+58 -6
View File
@@ -81,15 +81,59 @@ USB starts in Binary mode for MeshCore apps and `meshcli`:
meshcli -s /dev/ttyACM0 -b 115200 ver
```
Open the port at 115200 and send the terminal start token as an exact line:
Open the port with the terminal start token sent automatically:
```bash
picocom -b 115200 /dev/ttyACM0
+++MESHCORE-TERM-START
picocom -b 115200 \
--imap spchex \
--initstring '+++MESHCORE-TERM-START' \
/dev/ttyACM0
```
The terminal supports Companion chat commands plus local `ota`, `tempradio`,
and `normalradio` controls. Return to Binary mode with:
The input map prevents any Binary Companion control bytes received during the
mode transition from changing the local terminal's character set or display
state while leaving UTF-8 emoji intact. The banner confirms that terminal mode
is active; do not enter the start token again after it appears.
The terminal supports Companion chat commands, including `channels`,
`channel <name-or-slot> <message>`, remote administration with
`login <admin-password>` and `cmd <remote-command>`, and routed
`trace [recipient-name-or-prefix]`, plus local `ota`, `tempradio`, and
`normalradio` controls. For example:
```text
channels
channel #rgdata Hello from Eugene 👋
to Hilltop Repeater
login my-admin-password
cmd ver
trace
```
The `to` command selects the remote-administration target. Login passwords are
masked during entry and limited by the radio protocol to 15 UTF-8 bytes. Wait
for the asynchronous login result before using `cmd`; command replies appear
as `CLI -> from <name>`. Remote ACL permissions determine which commands the
target accepts.
With no argument, `trace` uses the current `to` recipient. A name-prefix
argument traces that contact directly without changing the current recipient.
The contact must already have a known direct path; results show the SNR at each
hop, or a timeout if the round trip does not return.
An explicit route can use 1-, 2-, or 4-byte hexadecimal prefixes. Spaces,
commas, and mixed separators are accepted:
```text
trace path 1 12 34 56 34 12
trace path 2 1234,ABCD,5678,ABCD,1234
trace path 4 12345678, ABCDEF01 89ABCDEF, ABCDEF01,12345678
```
The entered route must include both the outward and return prefixes. Exact
three-byte traces are not supported.
Return to Binary mode with:
```text
+++MESHCORE-TERM-STOP
@@ -146,12 +190,20 @@ motatool serve --dir ./motas --tcp 192.168.1.50:5001 -v
Use the USB terminal briefly to schedule TempRadio, then return to Binary mode
and close the terminal:
```bash
picocom -b 115200 \
--imap spchex \
--initstring '+++MESHCORE-TERM-START' \
/dev/ttyACM1
```
```text
+++MESHCORE-TERM-START
tempradio 909.950,250,7,5,120
+++MESHCORE-TERM-STOP
```
After sending the stop token, exit `picocom` with Ctrl-A, Ctrl-X.
Start the serial seeder on that same port:
```bash
+14 -5
View File
@@ -129,11 +129,20 @@ meshcli -r -s /dev/ttyACM1 -b 115200 "ota status"
The command must print an `OTA | ... target:XXXXXXXX` status.
For an nRF52 full Companion, open the source port in a terminal, send
`+++MESHCORE-TERM-START`, and run `ota status`. It must report `OTA seeder`,
`install:disabled`, and target `00000000`; send `+++MESHCORE-TERM-STOP` before
closing the terminal. The automation detects and performs this token-wrapped
preflight itself, so no extra command-line option is needed.
For an nRF52 full Companion, open the source port with terminal mode selected
automatically:
```bash
picocom -b 115200 \
--imap spchex \
--initstring '+++MESHCORE-TERM-START' \
/dev/ttyACM1
```
Run `ota status`. It must report `OTA seeder`, `install:disabled`, and target
`00000000`; send `+++MESHCORE-TERM-STOP` before closing the terminal. The
automation detects and performs this token-wrapped preflight itself, so no
extra command-line option is needed.
For an ESP32 full Companion, test its separate WiFi control console instead:
+122 -9
View File
@@ -5,13 +5,31 @@ Below are the commands you can enter into the Terminal Chat clients:
## Companion USB mode
A Companion USB build starts in the normal binary Companion protocol at
115200 baud. To use this terminal from the same firmware image, connect a
serial terminal and send this exact sequence:
115200 baud. Use this command to switch the same USB connection into terminal
mode as soon as `picocom` opens it:
```sh
picocom --baud 115200 \
--imap spchex \
--initstring '+++MESHCORE-TERM-START' \
/dev/ttyACM0
```
`--initstring` sends this exact terminal-start sequence automatically:
```
+++MESHCORE-TERM-START
```
Binary Companion frames can contain terminal control bytes. The `spchex` input
map renders those bytes as bracketed hexadecimal during the short transition
instead of allowing them to change the local terminal's character set or
display state. It leaves high-bit bytes unchanged so a UTF-8 terminal displays
emoji and non-ASCII text normally. Do not add `8bithex` unless you explicitly
want UTF-8 bytes displayed as sequences such as `[f0][9f][91][8b]`. Once the
terminal banner appears, the start sequence has already succeeded; do not
enter it again as a terminal command.
Send the following exact sequence to return to the binary protocol:
```
@@ -29,12 +47,6 @@ devices really change the UART timing and receive corrupt data. Binary mode is
the framed Companion API used by apps and `meshcli`; close the terminal before
opening that port from an app.
For example:
```sh
picocom --baud 115200 /dev/ttyACM0
```
## Commands
```
@@ -120,11 +132,90 @@ to {name-prefix}
```
Sets the recipient to the _first_ matching contact (in 'list') by the name prefix. (ie. you don't have to type whole name)
```
login {admin-password}
```
Sends a remote login request to the current recipient. Select a repeater,
room, or other remotely managed node with `to {name-prefix}` first. The
password is masked with `*` while it is entered and must be 1-15 UTF-8 bytes;
longer passwords are rejected instead of truncated.
Login results arrive asynchronously. A successful modern response displays
the remote ACL permissions byte and server protocol level. A wrong password,
an unreachable target, or a server that does not support remote login normally
produces a timeout because those nodes do not send a rejection packet.
```
cmd {remote-command}
```
Sends CLI data to the current recipient. Wait for the login result before
sending the first command. The remote node applies its own ACL permissions,
and any reply appears asynchronously as `CLI -> from {name}`.
For example:
```text
to Hilltop Repeater
login my-admin-password
LOGIN -> Hilltop Repeater accepted (ACL permissions 0x03, server v13)
cmd ver
cmd get radio
```
The exact commands and permissions depend on the target firmware. `cmd` does
not run a command on the local Companion; it sends the text over LoRa to the
selected node.
```
send {text}
```
Sends the text message (as DM) to current recipient.
```
trace
```
Traces the saved round-trip route to the current recipient and displays the
SNR at each hop. Select the recipient first with `to {name-prefix}`.
```
trace {name-prefix}
```
Traces a recipient directly without changing the current `to` selection. A
trace requires a known direct path; use normal messaging or path discovery
first if the terminal reports that no valid path is available. Only one
terminal trace can be pending at a time, and a missing response is reported as
a timeout.
For example:
```text
to Hilltop Repeater
trace
trace Downtown
```
The displayed route uses one- or two-byte node hashes and per-hop SNR values.
Saved three-byte paths are traced with two-byte prefixes because the trace
packet format has no three-byte hash-size mode.
To trace an explicit route instead of a saved contact path, provide the prefix
size followed by the complete ordered route:
```text
trace path 1 12 34 56 34 12
trace path 2 1234,ABCD,5678,ABCD,1234
trace path 4 12345678, ABCDEF01 89ABCDEF, ABCDEF01,12345678
```
Prefix separators may be spaces, commas, or any mixture of them. Each prefix
must contain exactly 2, 4, or 8 hexadecimal digits for a 1-, 2-, or 4-byte
trace respectively. Three-byte traces are not supported.
The prefixes are used exactly in the order entered. To receive the trace
result, enter the complete outward route followed by its return route, as in
the mirrored examples above. A route that does not return to this node will
eventually report a timeout.
```
reset path
```
@@ -133,4 +224,26 @@ Resets the path to current recipient, for new path discovery.
```
public {text}
```
Sends the text message to the built-in 'public' group channel
Sends the text message to the built-in `Public` group channel.
```
channels
```
Lists the configured channel slots and names without exposing their secrets.
```
channel {name-or-slot} {text}
```
Sends a message to any configured channel by its exact name or numeric slot.
Use the slot shown by `channels` when a channel name contains spaces.
For example:
```text
channels
channel #rgdata Hello from Eugene 👋
channel 2 Another message
```
Messages are UTF-8. Emoji use multiple bytes toward the available message
length, which also includes the sender-name prefix added over the air.
+456 -12
View File
@@ -4,6 +4,11 @@
#include <Mesh.h>
#include "helpers/radiolib/RXPowerSaving.h"
#ifdef ENABLE_USB_INTERFACE
#include <helpers/CLICommandUtils.h>
#include <helpers/TracePathHelpers.h>
#endif
#if defined(COMPANION_RADIO_FULL)
#include <helpers/ota/OtaCli.h>
#endif
@@ -888,7 +893,7 @@ void MyMesh::onChannelMessageRecv(const mesh::GroupChannel &channel, mesh::Packe
if (_terminal_mode) {
ChannelDetails details;
const char* channel_name = getChannel(channel_idx, details) ? details.name : "Unknown";
Serial.printf("\r\nPUBLIC CHANNEL MSG -> %s (%s)\r\n %s\r\n> ",
Serial.printf("\r\nCHANNEL MSG -> %s (%s)\r\n %s\r\n> ",
channel_name, pkt->isRouteDirect() ? "DIRECT" : "FLOOD", text);
}
#endif
@@ -1011,20 +1016,38 @@ uint8_t MyMesh::onContactRequest(const ContactInfo &contact, uint32_t sender_tim
}
void MyMesh::onContactResponse(const ContactInfo &contact, const uint8_t *data, uint8_t len) {
if (data == NULL || len < 4) return;
uint32_t tag;
memcpy(&tag, data, 4);
if (pending_login && memcmp(&pending_login, contact.id.pub_key, 4) == 0) { // check for login response
// yes, is response to pending sendLogin()
#ifdef ENABLE_USB_INTERFACE
const bool terminal_login_response = _terminal_login_pending
&& memcmp(_terminal_login_key, contact.id.pub_key,
sizeof(_terminal_login_key)) == 0;
#endif
pending_login = 0;
int i = 0;
if (memcmp(&data[4], "OK", 2) == 0) { // legacy Repeater login OK response
#ifdef ENABLE_USB_INTERFACE
bool login_success = false;
bool modern_login = false;
#endif
if (len >= 6 && memcmp(&data[4], "OK", 2) == 0) { // legacy Repeater login OK response
#ifdef ENABLE_USB_INTERFACE
login_success = true;
#endif
out_frame[i++] = PUSH_CODE_LOGIN_SUCCESS;
out_frame[i++] = 0; // legacy: is_admin = false
memcpy(&out_frame[i], contact.id.pub_key, 6);
i += 6; // pub_key_prefix
} else if (data[4] == RESP_SERVER_LOGIN_OK) { // new login response
} else if (len >= 13 && data[4] == RESP_SERVER_LOGIN_OK) { // new login response
#ifdef ENABLE_USB_INTERFACE
login_success = true;
modern_login = true;
#endif
uint16_t keep_alive_secs = ((uint16_t)data[5]) * 16;
if (keep_alive_secs > 0) {
startConnection(contact, keep_alive_secs);
@@ -1044,6 +1067,24 @@ void MyMesh::onContactResponse(const ContactInfo &contact, const uint8_t *data,
i += 6; // pub_key_prefix
}
_serial->writeFrame(out_frame, i);
#ifdef ENABLE_USB_INTERFACE
if (terminal_login_response) {
if (_terminal_mode) {
if (login_success && modern_login) {
Serial.printf("\r\nLOGIN -> %s accepted (ACL permissions 0x%02X, server v%u)\r\n> ",
_terminal_login_target, (unsigned)data[7],
(unsigned)data[12]);
} else if (login_success) {
Serial.printf("\r\nLOGIN -> %s accepted (legacy server)\r\n> ",
_terminal_login_target);
} else {
Serial.printf("\r\nLOGIN -> %s rejected\r\n> ",
_terminal_login_target);
}
}
clearTerminalLogin();
}
#endif
} else if (len > 4 && // check for status response
pending_status &&
memcmp(&pending_status, contact.id.pub_key, 4) == 0 // legacy matching scheme
@@ -1178,6 +1219,35 @@ void MyMesh::onTraceRecv(mesh::Packet *packet, uint32_t tag, uint32_t auth_code,
i += path_len >> path_sz;
out_frame[i++] = (int8_t)(packet->getSNR() * 4); // extra/final SNR (to this node)
#ifdef ENABLE_USB_INTERFACE
if (_terminal_mode && _terminal_trace_pending
&& tag == _terminal_trace_tag && auth_code == _terminal_trace_auth) {
const uint8_t hash_size = _terminal_trace_hash_size;
const uint8_t hop_count = hash_size == 0 ? 0 : path_len / hash_size;
const uint8_t response_hash_size = 1 << (flags & 0x03);
const unsigned long elapsed = _ms->getMillis() - _terminal_trace_sent_at;
Serial.printf("\r\nTRACE -> %s (%lu ms)\r\n",
_terminal_trace_target, elapsed);
if (hash_size == 0 || response_hash_size != hash_size
|| path_len % hash_size != 0
|| hop_count >= MAX_PATH_SIZE) {
Serial.print(" ERROR: malformed trace response\r\n> ");
} else {
Serial.print(" ");
for (uint8_t hop = 0; hop < hop_count; hop++) {
Serial.print(((float)(int8_t)path_snrs[hop]) / 4.0f, 2);
Serial.print(" dB -> [");
mesh::Utils::printHex(Serial, &path_hashes[hop * hash_size],
hash_size);
Serial.print("] -> ");
}
Serial.print(packet->getSNR(), 2);
Serial.print(" dB\r\n> ");
}
clearTerminalTrace();
}
#endif
if (_serial->isConnected()) {
_serial->writeFrame(out_frame, i);
} else {
@@ -1208,6 +1278,11 @@ MyMesh::MyMesh(mesh::Radio &radio, mesh::RNG &rng, mesh::RTCClock &rtc, SimpleMe
_terminal_mode = false;
_terminal_recipient_set = false;
memset(_terminal_recipient_key, 0, sizeof(_terminal_recipient_key));
_terminal_login_pending = false;
memset(_terminal_login_key, 0, sizeof(_terminal_login_key));
_terminal_login_expires_at = 0;
_terminal_login_target[0] = 0;
clearTerminalTrace();
#endif
saved_radio_apply_pending = false;
radio_apply_retry_at = 0;
@@ -3358,6 +3433,8 @@ void MyMesh::enterTerminalMode() {
_terminal_mode = true;
_terminal_recipient_set = false;
memset(_terminal_recipient_key, 0, sizeof(_terminal_recipient_key));
clearTerminalLogin();
clearTerminalTrace();
Serial.print("\r\n===== MeshCore Chat Terminal =====\r\n\r\n");
Serial.printf("WELCOME %s\r\n", _prefs.node_name);
@@ -3371,6 +3448,8 @@ void MyMesh::exitTerminalMode() {
_terminal_mode = false;
_terminal_recipient_set = false;
memset(_terminal_recipient_key, 0, sizeof(_terminal_recipient_key));
clearTerminalLogin();
clearTerminalTrace();
}
ContactInfo* MyMesh::getTerminalRecipient() {
@@ -3446,8 +3525,278 @@ void MyMesh::importTerminalCard(char* command) {
Serial.print(" OK - contact import queued\r\n");
}
void MyMesh::listTerminalChannels() {
bool found = false;
Serial.print("Channels:\r\n");
for (int i = 0; i < MAX_GROUP_CHANNELS; i++) {
ChannelDetails channel;
if (getChannel(i, channel) && channel.name[0] != 0) {
Serial.printf(" %d: %s\r\n", i, channel.name);
found = true;
}
}
if (!found) Serial.print(" (none configured)\r\n");
}
void MyMesh::sendTerminalChannelMessage(ChannelDetails& channel,
const char* text) {
const size_t prefix_len = strlen(_prefs.node_name) + 2; // "name: "
const size_t max_text_len = prefix_len < MAX_TEXT_LEN
? MAX_TEXT_LEN - prefix_len : 0;
const size_t text_len = text == NULL ? 0 : strlen(text);
if (text_len == 0) {
Serial.print(" ERROR: message is empty\r\n");
} else if (text_len > max_text_len) {
Serial.printf(" ERROR: message must be 1-%u UTF-8 bytes for this node name\r\n",
(unsigned)max_text_len);
} else if (sendGroupMessage(getRTCClock()->getCurrentTimeUnique(),
channel.channel, _prefs.node_name, text,
text_len)) {
Serial.printf(" Sent to %s.\r\n", channel.name);
} else {
Serial.print(" ERROR: unable to send\r\n");
}
}
void MyMesh::clearTerminalLogin() {
if (_terminal_login_pending && pending_login != 0
&& memcmp(&pending_login, _terminal_login_key,
sizeof(_terminal_login_key)) == 0) {
pending_login = 0;
}
_terminal_login_pending = false;
memset(_terminal_login_key, 0, sizeof(_terminal_login_key));
_terminal_login_expires_at = 0;
_terminal_login_target[0] = 0;
}
void MyMesh::serviceTerminalLogin() {
if (!_terminal_login_pending) return;
const unsigned long now = _ms->getMillis();
if (_terminal_login_expires_at != now
&& !millisHasNowPassed(_terminal_login_expires_at)) {
return;
}
if (_terminal_mode) {
Serial.printf("\r\n ERROR: login to %s timed out (wrong password or no response).\r\n> ",
_terminal_login_target);
}
clearTerminalLogin();
}
void MyMesh::sendTerminalLogin(ContactInfo& recipient,
const char* password) {
serviceTerminalLogin();
if (_terminal_login_pending) {
Serial.printf(" ERROR: login to %s is still pending\r\n",
_terminal_login_target);
return;
}
const size_t password_len = password == NULL ? 0 : strlen(password);
if (password_len == 0 || password_len > 15) {
Serial.print(" ERROR: password must be 1-15 UTF-8 bytes\r\n");
return;
}
uint32_t est_timeout = 0;
const int result = sendLogin(recipient, password, est_timeout);
if (result == MSG_SEND_FAILED) {
Serial.print(" ERROR: unable to send login\r\n");
return;
}
clearPendingReqs();
memcpy(&pending_login, recipient.id.pub_key, sizeof(pending_login));
_terminal_login_pending = true;
memcpy(_terminal_login_key, recipient.id.pub_key,
sizeof(_terminal_login_key));
const uint32_t timeout = est_timeout + est_timeout / 5;
_terminal_login_expires_at = futureMillis(timeout);
StrHelper::strzcpy(_terminal_login_target, recipient.name,
sizeof(_terminal_login_target));
Serial.printf(" Login sent to %s (%s, timeout %lu ms)\r\n",
recipient.name,
result == MSG_SEND_SENT_FLOOD ? "FLOOD" : "DIRECT",
(unsigned long)timeout);
}
void MyMesh::sendTerminalCommand(ContactInfo& recipient,
const char* command) {
serviceTerminalLogin();
if (_terminal_login_pending) {
Serial.printf(" ERROR: login to %s is still pending\r\n",
_terminal_login_target);
return;
}
const size_t command_len = command == NULL ? 0 : strlen(command);
if (command_len == 0 || command_len > MAX_TEXT_LEN) {
Serial.printf(" ERROR: remote command must be 1-%u UTF-8 bytes\r\n",
(unsigned)MAX_TEXT_LEN);
return;
}
const uint32_t logical_request_id =
getRTCClock()->getCurrentTimeUnique();
const uint32_t timestamp = getRTCClock()->getCurrentTimeUnique();
uint32_t est_timeout = 0;
const int result = sendCommandData(recipient, timestamp, 0, command,
est_timeout, logical_request_id);
if (result == MSG_SEND_FAILED) {
Serial.print(" ERROR: unable to send remote command\r\n");
return;
}
Serial.printf(" Remote command sent to %s (%s, timeout %lu ms)\r\n",
recipient.name,
result == MSG_SEND_SENT_FLOOD ? "FLOOD" : "DIRECT",
(unsigned long)est_timeout);
}
void MyMesh::clearTerminalTrace() {
_terminal_trace_pending = false;
_terminal_trace_hash_size = 0;
_terminal_trace_tag = 0;
_terminal_trace_auth = 0;
_terminal_trace_sent_at = 0;
_terminal_trace_expires_at = 0;
_terminal_trace_target[0] = 0;
}
void MyMesh::serviceTerminalTrace() {
if (!_terminal_trace_pending) return;
const unsigned long now = _ms->getMillis();
if (_terminal_trace_expires_at != now
&& !millisHasNowPassed(_terminal_trace_expires_at)) {
return;
}
if (_terminal_mode) {
Serial.printf("\r\n ERROR: trace to %s timed out.\r\n> ",
_terminal_trace_target);
}
clearTerminalTrace();
}
void MyMesh::sendTerminalTraceRoute(const uint8_t* route, uint8_t hash_size,
uint8_t hop_count, const char* target) {
serviceTerminalTrace();
if (_terminal_trace_pending) {
Serial.printf(" ERROR: trace to %s is still pending\r\n",
_terminal_trace_target);
return;
}
if (route == NULL || hop_count == 0 || hop_count >= MAX_PATH_SIZE) {
Serial.print(" ERROR: trace path must contain 1-63 prefixes\r\n");
return;
}
const size_t route_byte_len = static_cast<size_t>(hash_size) * hop_count;
if (route_byte_len > MAX_PACKET_PAYLOAD - 9) {
Serial.print(" ERROR: trace path is too long\r\n");
return;
}
const uint8_t flags = mesh::traceFlagsForHashSize(hash_size);
if (flags == 0xFF) {
Serial.print(" ERROR: trace hash size must be 1, 2, or 4 bytes\r\n");
return;
}
uint32_t tag = 0;
uint32_t auth = 0;
getRNG()->random((uint8_t*)&tag, sizeof(tag));
getRNG()->random((uint8_t*)&auth, sizeof(auth));
mesh::Packet* packet = createTrace(tag, auth, flags);
if (packet == NULL) {
Serial.print(" ERROR: unable to allocate trace packet\r\n");
return;
}
const uint32_t airtime = _radio->getEstAirtimeFor(9 + route_byte_len + 2);
const uint32_t timeout = calcDirectTimeoutMillisFor(airtime, hop_count);
if (!sendDirect(packet, route, static_cast<uint8_t>(route_byte_len))) {
Serial.print(" ERROR: unable to send trace\r\n");
return;
}
_terminal_trace_pending = true;
_terminal_trace_hash_size = hash_size;
_terminal_trace_tag = tag;
_terminal_trace_auth = auth;
_terminal_trace_sent_at = _ms->getMillis();
_terminal_trace_expires_at = futureMillis(timeout + timeout / 5);
StrHelper::strzcpy(_terminal_trace_target, target,
sizeof(_terminal_trace_target));
Serial.printf(" Trace sent to %s (%u route hops, timeout %lu ms)\r\n",
target, (unsigned)hop_count,
(unsigned long)(timeout + timeout / 5));
}
void MyMesh::sendTerminalTrace(ContactInfo& recipient) {
if (recipient.out_path_len == OUT_PATH_UNKNOWN
|| !mesh::Packet::isValidPathLen(recipient.out_path_len)) {
Serial.print(" ERROR: recipient has no valid direct path\r\n");
return;
}
const bool include_endpoint = recipient.type == ADV_TYPE_REPEATER
|| recipient.type == ADV_TYPE_ROOM;
mesh::RoundTripTracePath route;
if (!mesh::buildRoundTripTracePath(
recipient.out_path, recipient.out_path_len, recipient.id.pub_key,
include_endpoint, _terminal_tmp_buf, MAX_PACKET_PAYLOAD - 9,
route)) {
Serial.print(" ERROR: recipient has no traceable round-trip path\r\n");
return;
}
if (route.hop_count >= MAX_PATH_SIZE) {
Serial.print(" ERROR: round-trip trace path is too long\r\n");
return;
}
sendTerminalTraceRoute(_terminal_tmp_buf, route.hash_size, route.hop_count,
recipient.name);
}
void MyMesh::sendTerminalRawTrace(const char* arguments) {
mesh::RawTracePath route;
const mesh::RawTracePathParseResult parsed = mesh::parseRawTracePath(
arguments, _terminal_tmp_buf, MAX_PACKET_PAYLOAD - 9,
MAX_PATH_SIZE - 1, route);
switch (parsed) {
case mesh::RawTracePathParseResult::Valid:
break;
case mesh::RawTracePathParseResult::MissingHashSize:
case mesh::RawTracePathParseResult::MissingPrefixes:
Serial.print(" ERROR: use trace path <1|2|4> <prefixes...>\r\n");
return;
case mesh::RawTracePathParseResult::InvalidHashSize:
Serial.print(" ERROR: trace hash size must be 1, 2, or 4 bytes\r\n");
return;
case mesh::RawTracePathParseResult::InvalidPrefix:
Serial.printf(" ERROR: every prefix must be exactly %u hex digits\r\n",
(unsigned)route.hash_size * 2);
return;
case mesh::RawTracePathParseResult::TooManyHops:
Serial.print(" ERROR: trace path must contain at most 63 prefixes\r\n");
return;
case mesh::RawTracePathParseResult::RouteTooLong:
Serial.print(" ERROR: trace path is too long\r\n");
return;
}
char target[32];
snprintf(target, sizeof(target), "raw %u-byte path",
(unsigned)route.hash_size);
sendTerminalTraceRoute(_terminal_tmp_buf, route.hash_size, route.hop_count,
target);
}
void MyMesh::handleTerminalCommand(char* command) {
while (*command == ' ') command++;
while (*command == ' ' || *command == '\t') command++;
if (*command == 0) return;
#if defined(COMPANION_RADIO_FULL)
@@ -3458,7 +3807,50 @@ void MyMesh::handleTerminalCommand(char* command) {
}
#endif
if (strncmp(command, "send ", 5) == 0) {
mesh::cli::TerminalChannelMessage channel_message;
const mesh::cli::TerminalChannelCommandMatch channel_match =
mesh::cli::parseTerminalChannelMessage(command, channel_message);
const char* login_password = NULL;
const mesh::cli::TerminalArgumentCommandMatch login_match =
mesh::cli::parseTerminalArgumentCommand(command, "login",
login_password);
const char* remote_command = NULL;
const mesh::cli::TerminalArgumentCommandMatch command_match =
mesh::cli::parseTerminalArgumentCommand(command, "cmd",
remote_command);
if (strcmp(command, "channels") == 0) {
listTerminalChannels();
} else if (channel_match != mesh::cli::TerminalChannelCommandMatch::NoMatch) {
if (channel_match != mesh::cli::TerminalChannelCommandMatch::Valid) {
Serial.print(" ERROR: use channel <name-or-slot> <message>\r\n");
return;
}
ChannelDetails channel;
bool found = false;
size_t requested_index = 0;
if (mesh::cli::parseTerminalChannelIndex(
channel_message, MAX_GROUP_CHANNELS, requested_index)) {
found = getChannel((int)requested_index, channel)
&& channel.name[0] != 0;
} else {
for (int i = 0; i < MAX_GROUP_CHANNELS; i++) {
if (getChannel(i, channel) && channel.name[0] != 0
&& mesh::cli::terminalChannelNameMatches(channel_message,
channel.name)) {
found = true;
break;
}
}
}
if (!found) {
Serial.print(" ERROR: channel not found (use 'channels')\r\n");
} else {
sendTerminalChannelMessage(channel, channel_message.text);
}
} else if (strncmp(command, "send ", 5) == 0) {
ContactInfo* recipient = getTerminalRecipient();
const char* text = command + 5;
if (recipient == NULL) {
@@ -3491,15 +3883,10 @@ void MyMesh::handleTerminalCommand(char* command) {
} else if (strncmp(command, "public ", 7) == 0) {
ChannelDetails channel;
const char* text = command + 7;
if (*text == 0) {
Serial.print(" ERROR: message is empty\r\n");
} else if (!getChannel(0, channel)) {
if (!getChannel(0, channel) || channel.name[0] == 0) {
Serial.print(" ERROR: Public channel is unavailable\r\n");
} else if (sendGroupMessage(getRTCClock()->getCurrentTimeUnique(), channel.channel,
_prefs.node_name, text, strlen(text))) {
Serial.print(" Sent.\r\n");
} else {
Serial.print(" ERROR: unable to send\r\n");
sendTerminalChannelMessage(channel, text);
}
} else if (strcmp(command, "list") == 0 || strncmp(command, "list ", 5) == 0) {
int count = command[4] == ' ' ? atoi(command + 5) : 0;
@@ -3537,6 +3924,53 @@ void MyMesh::handleTerminalCommand(char* command) {
} else {
Serial.print(" No recipient selected\r\n");
}
} else if (login_match
!= mesh::cli::TerminalArgumentCommandMatch::NoMatch) {
ContactInfo* recipient = getTerminalRecipient();
if (login_match
!= mesh::cli::TerminalArgumentCommandMatch::Valid) {
Serial.print(" ERROR: use login <admin-password>\r\n");
} else if (recipient == NULL) {
Serial.print(" ERROR: no recipient selected (use 'to' first)\r\n");
} else {
sendTerminalLogin(*recipient, login_password);
}
} else if (command_match
!= mesh::cli::TerminalArgumentCommandMatch::NoMatch) {
ContactInfo* recipient = getTerminalRecipient();
if (command_match
!= mesh::cli::TerminalArgumentCommandMatch::Valid) {
Serial.print(" ERROR: use cmd <remote-command>\r\n");
} else if (recipient == NULL) {
Serial.print(" ERROR: no recipient selected (use 'to' first)\r\n");
} else {
sendTerminalCommand(*recipient, remote_command);
}
} else if (strcmp(command, "trace path") == 0
|| strncmp(command, "trace path ", 11) == 0
|| strncmp(command, "trace path\t", 11) == 0) {
sendTerminalRawTrace(command + 10);
} else if (strcmp(command, "trace") == 0
|| strncmp(command, "trace ", 6) == 0) {
ContactInfo* recipient = NULL;
if (command[5] == ' ') {
const char* prefix = command + 6;
while (*prefix == ' ') prefix++;
if (*prefix != 0 && strlen(prefix) < sizeof(ContactInfo::name)) {
recipient = searchContactsByPrefix(prefix);
}
if (recipient == NULL || recipient->type == ADV_TYPE_NONE) {
Serial.print(" ERROR: recipient prefix not found\r\n");
return;
}
} else {
recipient = getTerminalRecipient();
if (recipient == NULL) {
Serial.print(" ERROR: no recipient selected (use 'to' first)\r\n");
return;
}
}
sendTerminalTrace(*recipient);
} else if (strcmp(command, "advert") == 0) {
Serial.print(advert() ? " advert sent (zero hop)\r\n"
: " ERROR: unable to send advert\r\n");
@@ -3607,9 +4041,15 @@ void MyMesh::handleTerminalCommand(char* command) {
Serial.print(" list [n]\r\n");
Serial.print(" to [recipient name or prefix]\r\n");
Serial.print(" send <text>\r\n");
Serial.print(" login <admin-password>\r\n");
Serial.print(" cmd <remote-command>\r\n");
Serial.print(" trace [recipient name or prefix]\r\n");
Serial.print(" trace path <1|2|4> <prefixes...>\r\n");
Serial.print(" advert\r\n");
Serial.print(" reset path\r\n");
Serial.print(" public <text>\r\n");
Serial.print(" channels\r\n");
Serial.print(" channel <name-or-slot> <text>\r\n");
#if defined(COMPANION_RADIO_FULL)
Serial.print(" tempradio [freq,bw,sf,cr,minutes]\r\n");
Serial.print(" normalradio\r\n");
@@ -3839,6 +4279,10 @@ void MyMesh::loop() {
serviceTempRadio();
#endif
BaseChatMesh::loop();
#ifdef ENABLE_USB_INTERFACE
serviceTerminalLogin();
serviceTerminalTrace();
#endif
if (!command_radio_apply_pending && saved_radio_apply_pending && !hasOutbound()
#if defined(COMPANION_RADIO_FULL)
&& !_temp_radio_applied && _temp_radio_set_at == 0
+23
View File
@@ -275,6 +275,18 @@ private:
#ifdef ENABLE_USB_INTERFACE
ContactInfo* getTerminalRecipient();
void importTerminalCard(char* command);
void listTerminalChannels();
void sendTerminalChannelMessage(ChannelDetails& channel, const char* text);
void clearTerminalLogin();
void serviceTerminalLogin();
void sendTerminalLogin(ContactInfo& recipient, const char* password);
void sendTerminalCommand(ContactInfo& recipient, const char* command);
void clearTerminalTrace();
void serviceTerminalTrace();
void sendTerminalTraceRoute(const uint8_t* route, uint8_t hash_size,
uint8_t hop_count, const char* target);
void sendTerminalTrace(ContactInfo& recipient);
void sendTerminalRawTrace(const char* arguments);
#endif
bool isValidClientRepeatFreq(uint32_t f) const;
bool hasLocationTelemetryRecipient();
@@ -327,6 +339,17 @@ private:
bool _terminal_recipient_set;
uint8_t _terminal_recipient_key[PUB_KEY_SIZE];
uint8_t _terminal_tmp_buf[MAX_TRANS_UNIT];
bool _terminal_login_pending;
uint8_t _terminal_login_key[4];
unsigned long _terminal_login_expires_at;
char _terminal_login_target[32];
bool _terminal_trace_pending;
uint8_t _terminal_trace_hash_size;
uint32_t _terminal_trace_tag;
uint32_t _terminal_trace_auth;
unsigned long _terminal_trace_sent_at;
unsigned long _terminal_trace_expires_at;
char _terminal_trace_target[32];
#endif
bool saved_radio_apply_pending;
unsigned long radio_apply_retry_at;
+12 -9
View File
@@ -58,6 +58,7 @@ MultiSerialInterface interface_manager;
// include usb interface
#if defined(ENABLE_USB_INTERFACE)
#include <helpers/ArduinoSerialInterface.h>
#include <helpers/CLICommandUtils.h>
static const char USB_TERMINAL_START_TOKEN[] = "+++MESHCORE-TERM-START";
static const char USB_TERMINAL_STOP_TOKEN[] = "+++MESHCORE-TERM-STOP";
#if defined(NRF52_PLATFORM) && defined(COMPANION_RADIO_FULL) && defined(OTA_FOLDER_SERIAL)
@@ -133,6 +134,11 @@ static size_t usb_mota_line_len = 0;
static bool usb_mota_disconnect_armed = false;
#endif
static void clearUsbTerminalLine() {
memset(usb_terminal_line, 0, sizeof(usb_terminal_line));
usb_terminal_line_len = 0;
}
static bool isUsbTerminalDataConnected() {
#if defined(RP2040_PLATFORM)
return (bool)Serial;
@@ -143,8 +149,7 @@ static bool isUsbTerminalDataConnected() {
static void enterUsbTerminalMode() {
usb_serial_interface.setPassthroughMode(true);
usb_terminal_line_len = 0;
usb_terminal_line[0] = 0;
clearUsbTerminalLine();
usb_terminal_discard_line = false;
usb_terminal_disconnect_armed = isUsbTerminalDataConnected();
the_mesh.enterTerminalMode();
@@ -156,8 +161,7 @@ static void leaveUsbTerminalMode(bool acknowledge) {
}
the_mesh.exitTerminalMode();
usb_serial_interface.setPassthroughMode(false);
usb_terminal_line_len = 0;
usb_terminal_line[0] = 0;
clearUsbTerminalLine();
usb_terminal_discard_line = false;
usb_terminal_disconnect_armed = false;
}
@@ -291,15 +295,13 @@ static void serviceUsbTerminal() {
if (usb_terminal_line_len == 0) continue;
Serial.print("\r\n");
the_mesh.handleTerminalCommand(usb_terminal_line);
usb_terminal_line_len = 0;
usb_terminal_line[0] = 0;
clearUsbTerminalLine();
Serial.print("> ");
return; // service at most one command per mesh loop
}
if (usb_terminal_line_len >= sizeof(usb_terminal_line) - 1) {
usb_terminal_line_len = 0;
usb_terminal_line[0] = 0;
clearUsbTerminalLine();
usb_terminal_discard_line = true;
Serial.print("\r\n ERROR: command too long\r\n");
continue;
@@ -307,7 +309,8 @@ static void serviceUsbTerminal() {
usb_terminal_line[usb_terminal_line_len++] = c;
usb_terminal_line[usb_terminal_line_len] = 0;
Serial.print(c);
Serial.print(mesh::cli::shouldMaskTerminalInput(usb_terminal_line) ? '*'
: c);
if (strcmp(usb_terminal_line, USB_TERMINAL_STOP_TOKEN) == 0) {
leaveUsbTerminalMode(true);
+111
View File
@@ -29,17 +29,128 @@ enum class RecentRepeaterGetMatch : uint8_t {
Invalid,
};
enum class TerminalChannelCommandMatch : uint8_t {
NoMatch = 0,
Valid,
MissingSelector,
MissingMessage,
};
enum class TerminalArgumentCommandMatch : uint8_t {
NoMatch = 0,
Valid,
MissingArgument,
};
struct RecentRepeaterGetQuery {
int page;
uint8_t search_prefix[3];
uint8_t search_prefix_len;
};
struct TerminalChannelMessage {
const char* selector;
size_t selector_len;
const char* text;
};
inline const char* skipRecentRepeaterSpaces(const char* text) {
while (text != nullptr && (*text == ' ' || *text == '\t')) text++;
return text;
}
inline TerminalArgumentCommandMatch parseTerminalArgumentCommand(
const char* command, const char* verb, const char*& argument) {
argument = nullptr;
if (command == nullptr || verb == nullptr || *verb == 0) {
return TerminalArgumentCommandMatch::NoMatch;
}
const size_t verb_len = strlen(verb);
for (size_t i = 0; i < verb_len; i++) {
char actual = command[i];
char expected = verb[i];
if (actual >= 'A' && actual <= 'Z') actual += 'a' - 'A';
if (expected >= 'A' && expected <= 'Z') expected += 'a' - 'A';
if (actual != expected) return TerminalArgumentCommandMatch::NoMatch;
}
if (command[verb_len] != 0 && command[verb_len] != ' '
&& command[verb_len] != '\t') {
return TerminalArgumentCommandMatch::NoMatch;
}
const char* cursor = skipRecentRepeaterSpaces(command + verb_len);
if (*cursor == 0) return TerminalArgumentCommandMatch::MissingArgument;
argument = cursor;
return TerminalArgumentCommandMatch::Valid;
}
// Return true once terminal input has reached a login password. The caller
// can still retain the real bytes for command handling while echoing '*'.
inline bool shouldMaskTerminalInput(const char* line) {
line = skipRecentRepeaterSpaces(line);
const char* password = nullptr;
return parseTerminalArgumentCommand(line, "login", password)
== TerminalArgumentCommandMatch::Valid;
}
inline TerminalChannelCommandMatch parseTerminalChannelMessage(
const char* command, TerminalChannelMessage& message) {
message.selector = nullptr;
message.selector_len = 0;
message.text = nullptr;
if (command == nullptr || strncmp(command, "channel", 7) != 0) {
return TerminalChannelCommandMatch::NoMatch;
}
const char* cursor = command + 7;
if (*cursor != 0 && *cursor != ' ' && *cursor != '\t') {
return TerminalChannelCommandMatch::NoMatch;
}
cursor = skipRecentRepeaterSpaces(cursor);
if (*cursor == 0) return TerminalChannelCommandMatch::MissingSelector;
message.selector = cursor;
while (*cursor != 0 && *cursor != ' ' && *cursor != '\t') cursor++;
message.selector_len = static_cast<size_t>(cursor - message.selector);
cursor = skipRecentRepeaterSpaces(cursor);
if (*cursor == 0) return TerminalChannelCommandMatch::MissingMessage;
message.text = cursor;
return TerminalChannelCommandMatch::Valid;
}
inline bool parseTerminalChannelIndex(const TerminalChannelMessage& message,
size_t max_channels,
size_t& channel_index) {
if (message.selector == nullptr || message.selector_len == 0
|| max_channels == 0) {
return false;
}
size_t value = 0;
for (size_t i = 0; i < message.selector_len; i++) {
const char c = message.selector[i];
if (c < '0' || c > '9') return false;
const size_t digit = static_cast<size_t>(c - '0');
if (digit >= max_channels
|| value > (max_channels - 1 - digit) / 10) {
return false;
}
value = value * 10 + digit;
}
if (value >= max_channels) return false;
channel_index = value;
return true;
}
inline bool terminalChannelNameMatches(const TerminalChannelMessage& message,
const char* channel_name) {
return message.selector != nullptr && channel_name != nullptr
&& strlen(channel_name) == message.selector_len
&& memcmp(channel_name, message.selector, message.selector_len) == 0;
}
inline int recentRepeaterHexNibble(char c) {
if (c >= '0' && c <= '9') return c - '0';
if (c >= 'a' && c <= 'f') return c - 'a' + 10;
+174
View File
@@ -0,0 +1,174 @@
#pragma once
#include <stddef.h>
#include <stdint.h>
#include <string.h>
namespace mesh {
struct RoundTripTracePath {
uint8_t hash_size;
uint8_t hop_count;
size_t byte_len;
};
enum class RawTracePathParseResult : uint8_t {
Valid,
MissingHashSize,
InvalidHashSize,
MissingPrefixes,
InvalidPrefix,
TooManyHops,
RouteTooLong,
};
struct RawTracePath {
uint8_t hash_size;
uint8_t hop_count;
size_t byte_len;
};
inline bool isRawTracePathSeparator(char c) {
return c == ' ' || c == '\t' || c == ',';
}
inline int rawTraceHexNibble(char c) {
if (c >= '0' && c <= '9') return c - '0';
if (c >= 'a' && c <= 'f') return c - 'a' + 10;
if (c >= 'A' && c <= 'F') return c - 'A' + 10;
return -1;
}
// Parse: <hash-size> <prefix>[,<prefix> ...]
// Prefix separators may be commas, spaces, tabs, or any mixture of them.
// TRACE uses power-of-two hash sizes, so only 1, 2, and 4 are accepted.
inline RawTracePathParseResult parseRawTracePath(const char* input,
uint8_t* output,
size_t output_capacity,
uint8_t max_hops,
RawTracePath& result) {
result.hash_size = 0;
result.hop_count = 0;
result.byte_len = 0;
if (input == nullptr) return RawTracePathParseResult::MissingHashSize;
while (*input == ' ' || *input == '\t') input++;
if (*input == 0) return RawTracePathParseResult::MissingHashSize;
const char width_char = *input++;
if (width_char != '1' && width_char != '2' && width_char != '4') {
return RawTracePathParseResult::InvalidHashSize;
}
if (*input != 0 && !isRawTracePathSeparator(*input)) {
return RawTracePathParseResult::InvalidHashSize;
}
const uint8_t hash_size = static_cast<uint8_t>(width_char - '0');
result.hash_size = hash_size;
while (isRawTracePathSeparator(*input)) input++;
if (*input == 0) return RawTracePathParseResult::MissingPrefixes;
if (output == nullptr || output_capacity < hash_size) {
return RawTracePathParseResult::RouteTooLong;
}
const size_t expected_chars = static_cast<size_t>(hash_size) * 2;
size_t offset = 0;
uint8_t hop_count = 0;
while (*input != 0) {
const char* token = input;
size_t token_len = 0;
while (input[token_len] != 0
&& !isRawTracePathSeparator(input[token_len])) {
token_len++;
}
if (token_len != expected_chars) {
return RawTracePathParseResult::InvalidPrefix;
}
if (hop_count >= max_hops) {
return RawTracePathParseResult::TooManyHops;
}
if (offset + hash_size > output_capacity) {
return RawTracePathParseResult::RouteTooLong;
}
for (uint8_t i = 0; i < hash_size; i++) {
const int high = rawTraceHexNibble(token[i * 2]);
const int low = rawTraceHexNibble(token[i * 2 + 1]);
if (high < 0 || low < 0) {
return RawTracePathParseResult::InvalidPrefix;
}
output[offset++] = static_cast<uint8_t>((high << 4) | low);
}
hop_count++;
input += token_len;
while (isRawTracePathSeparator(*input)) input++;
}
result.hash_size = hash_size;
result.hop_count = hop_count;
result.byte_len = offset;
return RawTracePathParseResult::Valid;
}
// Build the route used by Companion clients for a trace that returns to its
// origin: saved path, optional repeater/room endpoint, then the saved path in
// reverse. Stored three-byte paths are traced with their first two bytes per
// hop because the TRACE flag format has no unambiguous three-byte mode.
inline bool buildRoundTripTracePath(const uint8_t* saved_path,
uint8_t encoded_path_len,
const uint8_t* endpoint_hash,
bool include_endpoint,
uint8_t* output,
size_t output_capacity,
RoundTripTracePath& result) {
result.hash_size = 0;
result.hop_count = 0;
result.byte_len = 0;
const size_t saved_hash_size = (encoded_path_len >> 6) + 1;
const size_t saved_hop_count = encoded_path_len & 63;
if (saved_hash_size == 0 || saved_hash_size > 3
|| (saved_hop_count > 0 && saved_path == nullptr)
|| (include_endpoint && endpoint_hash == nullptr)) {
return false;
}
const size_t trace_hash_size = saved_hash_size == 3 ? 2 : saved_hash_size;
const size_t route_hop_count = saved_hop_count * 2
+ (include_endpoint ? 1 : 0);
if (route_hop_count == 0 || route_hop_count > 255
|| trace_hash_size > output_capacity
|| route_hop_count > output_capacity / trace_hash_size
|| output == nullptr) {
return false;
}
size_t offset = 0;
for (size_t i = 0; i < saved_hop_count; i++) {
memcpy(&output[offset], &saved_path[i * saved_hash_size], trace_hash_size);
offset += trace_hash_size;
}
if (include_endpoint) {
memcpy(&output[offset], endpoint_hash, trace_hash_size);
offset += trace_hash_size;
}
for (size_t i = saved_hop_count; i > 0; i--) {
memcpy(&output[offset], &saved_path[(i - 1) * saved_hash_size],
trace_hash_size);
offset += trace_hash_size;
}
result.hash_size = static_cast<uint8_t>(trace_hash_size);
result.hop_count = static_cast<uint8_t>(route_hop_count);
result.byte_len = offset;
return true;
}
inline uint8_t traceFlagsForHashSize(uint8_t hash_size) {
return hash_size == 1 ? 0
: hash_size == 2 ? 1
: hash_size == 4 ? 2
: 0xFF;
}
} // namespace mesh
@@ -40,6 +40,98 @@ TEST(CLICommandUtils, HandlesLeadingWhitespaceAndSingleWordCommands) {
EXPECT_STREQ("powersaving", single_command);
}
TEST(CLICommandUtils, ParsesTerminalChannelMessagesWithUtf8Text) {
using mesh::cli::TerminalChannelCommandMatch;
mesh::cli::TerminalChannelMessage message;
EXPECT_EQ(TerminalChannelCommandMatch::Valid,
mesh::cli::parseTerminalChannelMessage(
"channel #rgdata Hello from Eugene 👋", message));
EXPECT_EQ(7u, message.selector_len);
EXPECT_EQ(0, memcmp("#rgdata", message.selector, message.selector_len));
EXPECT_STREQ("Hello from Eugene 👋", message.text);
EXPECT_TRUE(mesh::cli::terminalChannelNameMatches(message, "#rgdata"));
EXPECT_FALSE(mesh::cli::terminalChannelNameMatches(message, "#RGDATA"));
}
TEST(CLICommandUtils, ParsesTerminalChannelSlotsStrictly) {
using mesh::cli::TerminalChannelCommandMatch;
mesh::cli::TerminalChannelMessage message;
size_t channel_index = 99;
EXPECT_EQ(TerminalChannelCommandMatch::Valid,
mesh::cli::parseTerminalChannelMessage(
"channel 2 Slot message", message));
EXPECT_TRUE(mesh::cli::parseTerminalChannelIndex(message, 8,
channel_index));
EXPECT_EQ(2u, channel_index);
EXPECT_EQ(TerminalChannelCommandMatch::Valid,
mesh::cli::parseTerminalChannelMessage(
"channel 8 Out of range", message));
EXPECT_FALSE(mesh::cli::parseTerminalChannelIndex(message, 8,
channel_index));
EXPECT_EQ(TerminalChannelCommandMatch::Valid,
mesh::cli::parseTerminalChannelMessage(
"channel 2name Named channel", message));
EXPECT_FALSE(mesh::cli::parseTerminalChannelIndex(message, 8,
channel_index));
EXPECT_TRUE(mesh::cli::terminalChannelNameMatches(message, "2name"));
}
TEST(CLICommandUtils, RejectsIncompleteTerminalChannelMessages) {
using mesh::cli::TerminalChannelCommandMatch;
mesh::cli::TerminalChannelMessage message;
EXPECT_EQ(TerminalChannelCommandMatch::MissingSelector,
mesh::cli::parseTerminalChannelMessage("channel", message));
EXPECT_EQ(TerminalChannelCommandMatch::MissingMessage,
mesh::cli::parseTerminalChannelMessage("channel #rgdata", message));
EXPECT_EQ(TerminalChannelCommandMatch::NoMatch,
mesh::cli::parseTerminalChannelMessage("channels", message));
EXPECT_EQ(TerminalChannelCommandMatch::NoMatch,
mesh::cli::parseTerminalChannelMessage("channelized test", message));
}
TEST(CLICommandUtils, ParsesTerminalLoginAndRemoteCommandArguments) {
using mesh::cli::TerminalArgumentCommandMatch;
const char* argument = nullptr;
EXPECT_EQ(TerminalArgumentCommandMatch::Valid,
mesh::cli::parseTerminalArgumentCommand(
"login admin password", "login", argument));
EXPECT_STREQ("admin password", argument);
EXPECT_EQ(TerminalArgumentCommandMatch::Valid,
mesh::cli::parseTerminalArgumentCommand(
"LOGIN CaseSensitivePassword", "login", argument));
EXPECT_STREQ("CaseSensitivePassword", argument);
EXPECT_EQ(TerminalArgumentCommandMatch::Valid,
mesh::cli::parseTerminalArgumentCommand(
"cmd\tget stats", "cmd", argument));
EXPECT_STREQ("get stats", argument);
EXPECT_EQ(TerminalArgumentCommandMatch::MissingArgument,
mesh::cli::parseTerminalArgumentCommand(
"login ", "login", argument));
EXPECT_EQ(nullptr, argument);
EXPECT_EQ(TerminalArgumentCommandMatch::NoMatch,
mesh::cli::parseTerminalArgumentCommand(
"logins password", "login", argument));
}
TEST(CLICommandUtils, MasksOnlyTerminalLoginPasswordInput) {
EXPECT_FALSE(mesh::cli::shouldMaskTerminalInput("login"));
EXPECT_FALSE(mesh::cli::shouldMaskTerminalInput("login "));
EXPECT_TRUE(mesh::cli::shouldMaskTerminalInput("login s"));
EXPECT_TRUE(mesh::cli::shouldMaskTerminalInput("LOGIN s"));
EXPECT_TRUE(mesh::cli::shouldMaskTerminalInput(" login secret phrase"));
EXPECT_FALSE(mesh::cli::shouldMaskTerminalInput("cmd login secret"));
EXPECT_FALSE(mesh::cli::shouldMaskTerminalInput("login-status"));
}
TEST(CLICommandUtils, ParsesStrictDecimalValues) {
float value = 0.0f;
@@ -0,0 +1,173 @@
#include <gtest/gtest.h>
#include <helpers/TracePathHelpers.h>
TEST(TracePathHelpers, BuildsOneByteRoundTripThroughEndpoint) {
const uint8_t saved[] = {0x11, 0x22};
const uint8_t endpoint[] = {0x33};
uint8_t route[8] = {0};
mesh::RoundTripTracePath result;
ASSERT_TRUE(mesh::buildRoundTripTracePath(
saved, 2, endpoint, true, route, sizeof(route), result));
const uint8_t expected[] = {0x11, 0x22, 0x33, 0x22, 0x11};
EXPECT_EQ(1, result.hash_size);
EXPECT_EQ(5, result.hop_count);
EXPECT_EQ(sizeof(expected), result.byte_len);
EXPECT_EQ(0, memcmp(expected, route, sizeof(expected)));
EXPECT_EQ(0, mesh::traceFlagsForHashSize(result.hash_size));
}
TEST(TracePathHelpers, OmitsNonForwardingEndpoint) {
const uint8_t saved[] = {0x11, 0x22};
uint8_t route[8] = {0};
mesh::RoundTripTracePath result;
ASSERT_TRUE(mesh::buildRoundTripTracePath(
saved, 2, nullptr, false, route, sizeof(route), result));
const uint8_t expected[] = {0x11, 0x22, 0x22, 0x11};
EXPECT_EQ(4, result.hop_count);
EXPECT_EQ(sizeof(expected), result.byte_len);
EXPECT_EQ(0, memcmp(expected, route, sizeof(expected)));
}
TEST(TracePathHelpers, PreservesTwoByteHashes) {
const uint8_t saved[] = {0x11, 0x12, 0x21, 0x22};
const uint8_t endpoint[] = {0x31, 0x32};
uint8_t route[16] = {0};
mesh::RoundTripTracePath result;
ASSERT_TRUE(mesh::buildRoundTripTracePath(
saved, static_cast<uint8_t>((1 << 6) | 2), endpoint, true,
route, sizeof(route), result));
const uint8_t expected[] = {
0x11, 0x12, 0x21, 0x22, 0x31, 0x32, 0x21, 0x22, 0x11, 0x12};
EXPECT_EQ(2, result.hash_size);
EXPECT_EQ(5, result.hop_count);
EXPECT_EQ(sizeof(expected), result.byte_len);
EXPECT_EQ(0, memcmp(expected, route, sizeof(expected)));
EXPECT_EQ(1, mesh::traceFlagsForHashSize(result.hash_size));
}
TEST(TracePathHelpers, SafelyDownConvertsThreeByteHashes) {
const uint8_t saved[] = {
0x11, 0x12, 0x13, 0x21, 0x22, 0x23};
const uint8_t endpoint[] = {0x31, 0x32, 0x33};
uint8_t route[16] = {0};
mesh::RoundTripTracePath result;
ASSERT_TRUE(mesh::buildRoundTripTracePath(
saved, static_cast<uint8_t>((2 << 6) | 2), endpoint, true,
route, sizeof(route), result));
const uint8_t expected[] = {
0x11, 0x12, 0x21, 0x22, 0x31, 0x32, 0x21, 0x22, 0x11, 0x12};
EXPECT_EQ(2, result.hash_size);
EXPECT_EQ(5, result.hop_count);
EXPECT_EQ(sizeof(expected), result.byte_len);
EXPECT_EQ(0, memcmp(expected, route, sizeof(expected)));
}
TEST(TracePathHelpers, RejectsUnknownEmptyAndOversizedRoutes) {
const uint8_t saved[] = {0x11, 0x22};
const uint8_t endpoint[] = {0x33};
uint8_t route[4] = {0};
mesh::RoundTripTracePath result;
EXPECT_FALSE(mesh::buildRoundTripTracePath(
saved, 0xFF, endpoint, true, route, sizeof(route), result));
EXPECT_FALSE(mesh::buildRoundTripTracePath(
nullptr, 0, nullptr, false, route, sizeof(route), result));
EXPECT_FALSE(mesh::buildRoundTripTracePath(
saved, 2, endpoint, true, route, sizeof(route), result));
}
TEST(TracePathHelpers, ParsesOneBytePrefixesSeparatedBySpaces) {
uint8_t route[8] = {0};
mesh::RawTracePath result;
ASSERT_EQ(mesh::RawTracePathParseResult::Valid,
mesh::parseRawTracePath("1 11 22 3a 22 11", route,
sizeof(route), 63, result));
const uint8_t expected[] = {0x11, 0x22, 0x3A, 0x22, 0x11};
EXPECT_EQ(1, result.hash_size);
EXPECT_EQ(5, result.hop_count);
EXPECT_EQ(sizeof(expected), result.byte_len);
EXPECT_EQ(0, memcmp(expected, route, sizeof(expected)));
}
TEST(TracePathHelpers, ParsesTwoBytePrefixesWithMixedSeparators) {
uint8_t route[16] = {0};
mesh::RawTracePath result;
ASSERT_EQ(mesh::RawTracePathParseResult::Valid,
mesh::parseRawTracePath("2 1122, 3344 ,AAbb,3344 1122",
route, sizeof(route), 63, result));
const uint8_t expected[] = {
0x11, 0x22, 0x33, 0x44, 0xAA, 0xBB, 0x33, 0x44, 0x11, 0x22};
EXPECT_EQ(2, result.hash_size);
EXPECT_EQ(5, result.hop_count);
EXPECT_EQ(sizeof(expected), result.byte_len);
EXPECT_EQ(0, memcmp(expected, route, sizeof(expected)));
}
TEST(TracePathHelpers, ParsesFourBytePrefixesSeparatedByCommas) {
uint8_t route[16] = {0};
mesh::RawTracePath result;
ASSERT_EQ(mesh::RawTracePathParseResult::Valid,
mesh::parseRawTracePath("4,01234567,89abcdef,01234567,",
route, sizeof(route), 63, result));
const uint8_t expected[] = {
0x01, 0x23, 0x45, 0x67, 0x89, 0xAB, 0xCD, 0xEF,
0x01, 0x23, 0x45, 0x67};
EXPECT_EQ(4, result.hash_size);
EXPECT_EQ(3, result.hop_count);
EXPECT_EQ(sizeof(expected), result.byte_len);
EXPECT_EQ(0, memcmp(expected, route, sizeof(expected)));
EXPECT_EQ(2, mesh::traceFlagsForHashSize(result.hash_size));
}
TEST(TracePathHelpers, RejectsThreeByteAndMalformedRawPaths) {
uint8_t route[8] = {0};
mesh::RawTracePath result;
EXPECT_EQ(mesh::RawTracePathParseResult::InvalidHashSize,
mesh::parseRawTracePath("3 112233", route, sizeof(route), 63,
result));
EXPECT_EQ(mesh::RawTracePathParseResult::MissingHashSize,
mesh::parseRawTracePath(" ", route, sizeof(route), 63, result));
EXPECT_EQ(mesh::RawTracePathParseResult::MissingPrefixes,
mesh::parseRawTracePath("2 , ", route, sizeof(route), 63,
result));
EXPECT_EQ(mesh::RawTracePathParseResult::InvalidPrefix,
mesh::parseRawTracePath("2 123 4567", route, sizeof(route), 63,
result));
EXPECT_EQ(mesh::RawTracePathParseResult::InvalidPrefix,
mesh::parseRawTracePath("1 GG", route, sizeof(route), 63,
result));
}
TEST(TracePathHelpers, EnforcesRawPathHopAndByteLimits) {
uint8_t route[8] = {0};
mesh::RawTracePath result;
EXPECT_EQ(mesh::RawTracePathParseResult::TooManyHops,
mesh::parseRawTracePath("1 11 22 33", route, sizeof(route), 2,
result));
EXPECT_EQ(mesh::RawTracePathParseResult::RouteTooLong,
mesh::parseRawTracePath("4 11223344 55667788 99AABBCC", route,
sizeof(route), 63, result));
EXPECT_EQ(0xFF, mesh::traceFlagsForHashSize(3));
}
int main(int argc, char** argv) {
::testing::InitGoogleTest(&argc, argv);
return RUN_ALL_TESTS();
}