mirror of
https://github.com/mikecarper/MeshCore.git
synced 2026-09-01 19:18:17 +00:00
Expand full Companion terminal tooling
This commit is contained in:
@@ -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
|
||||
|
||||
@@ -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
@@ -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.
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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();
|
||||
}
|
||||
Reference in New Issue
Block a user