Improve Companion terminal routing and controls

This commit is contained in:
mikecarper committed 2026-08-11 12:19:41 -07:00
1 parent 71619934fc
commit 4d51d49660
14 files changed
+709 -72

No files matched your search

+29 -7
View File
@@ -134,21 +134,43 @@ The terminal supports Companion chat commands, including `channels`,
```text
channels
channel #rgdata Hello from Eugene 👋
show
show channels on
to Hilltop Repeater
path A1B2C3,D4E5F6
path 7773D0 7E7662
login my-admin-password
cmd ver
trace
```
The terminal `list [n]` command displays favorite contacts first and orders
each favorite/non-favorite group by its most recent advertisement. This does
not alter the binary Companion contact-list protocol.
Unsolicited terminal output starts in a quiet mode: advertisements and
ordinary channel messages are hidden, while `#emergency` messages remain
visible. Use `show adverts on|off`, `show channels on|off`, and
`show emergency on|off` to control each category independently; plain `show`
reports their state. These runtime filters affect terminal printing only and
reset to their defaults after reboot.
The `to` command selects the remote-administration target. `path` shows its
saved outgoing route; `path direct`, `path clear`, or a comma-separated list
such as `path A1B2C3,D4E5F6` changes the route used by subsequent `login`,
`send`, and `cmd` commands. Every hop must use the same 2-, 4-, or 6-digit
hexadecimal width. 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.
saved outgoing route; `path direct`, `path clear`, or a list separated by
spaces, commas, or both changes the route used by subsequent `login`, `send`,
and `cmd` commands. Every hop must use the same 2-, 4-, or 6-digit hexadecimal
width. 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>` and use a response window
of 300% of the route estimate. `DIRECT via path <hop,...>` displays the exact
saved prefixes copied into the packet. Remote ACL permissions determine which
commands the target accepts. The matching reply reports its round-trip time
from local queueing through result reception, including radio transit and
remote execution. Only one terminal `cmd` can be pending at a time.
Incoming unicast replies are labeled `ROUTED`. Their exact return prefixes are
not available at the destination because each forwarder consumes its prefix;
use `trace` to verify the return route.
With no argument, `trace` uses the current `to` recipient. A name-prefix
argument traces that contact directly without changing the current recipient.
+7 -7
View File
@@ -32,6 +32,13 @@ reply that its history is empty.
## Decode a reply
<div class="telemetry-tool" data-telemetry-decoder>
<div class="telemetry-examples" aria-label="Load an example reply">
<strong>Try an example:</strong>
<button type="button" data-telemetry-example="temperature">Temperature</button>
<button type="button" data-telemetry-example="voltage">Voltage</button>
<button type="button" data-telemetry-example="gps">GPS</button>
</div>
<label for="telemetry-reply-input">CLI reply or Base64 payload</label>
<textarea
id="telemetry-reply-input"
@@ -55,13 +62,6 @@ reply that its history is empty.
</label>
</div>
<div class="telemetry-examples" aria-label="Load an example reply">
<strong>Try an example:</strong>
<button type="button" data-telemetry-example="temperature">Temperature</button>
<button type="button" data-telemetry-example="voltage">Voltage</button>
<button type="button" data-telemetry-example="gps">GPS</button>
</div>
<div class="telemetry-error" data-role="error" role="alert" aria-live="polite" hidden></div>
<section class="telemetry-results" data-role="results" aria-live="polite" hidden>
+40 -7
View File
@@ -130,7 +130,26 @@ Imports the given card to your contacts.
```
list {n}
```
List all contacts by most recent. (optional {n}, is the last n by advertisement date)
Lists favorite contacts first, then all remaining contacts. Each group is
ordered by the most recent advertisement. Optional `{n}` limits the displayed
contacts after applying that order.
```
show
show adverts {on|off}
show channels {on|off}
show emergency {on|off}
```
Controls unsolicited receive output in the USB terminal. Plain `show` reports
the current settings, and `show {category}` reports one category. At boot,
advertisements and ordinary channel messages are hidden while `#emergency`
messages are shown. The three controls are independent, so `show channels on`
does not override `show emergency off`.
These filters affect terminal printing only. Messages still enter the offline
queue and are delivered through the binary Companion protocol. Changes remain
active when switching between terminal and binary mode and reset to their
defaults after reboot.
```
to
@@ -151,23 +170,24 @@ all path changes require a recipient to be selected first.
```
path direct
path clear
path {hop-hash[,hop-hash...]}
path {hop-hash...}
```
Sets the outgoing path used by subsequent `login`, `send`, and `cmd` commands.
`direct` selects a zero-hop route. `clear` forgets the saved route, causing the
next operation to use flood routing and allowing normal path discovery to
learn a replacement.
Explicit paths use comma-separated hop hashes. Each hop must contain exactly
2, 4, or 6 hexadecimal digits, and every hop in one path must use the same
width. Spaces around commas and hexadecimal letter case do not matter. The
setting is saved with the selected contact.
Explicit paths use spaces, commas, or a mixture of both between hop hashes.
Each hop must contain exactly 2, 4, or 6 hexadecimal digits, and every hop in
one path must use the same width. Whitespace and hexadecimal letter case do
not matter. The setting is saved with the selected contact.
For example:
```text
to Hilltop Repeater
path A1B2C3,D4E5F6
path 7773D0 7E7662
path
login my-admin-password
```
@@ -192,7 +212,14 @@ 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}`.
and any reply appears asynchronously as `CLI -> from {name}`. The response
window is 300% of the route estimate. A routed send is displayed as
`DIRECT via path {hop,...}` with the exact prefixes copied into the packet; in
MeshCore, `DIRECT` is the route class for an explicit path, not a synonym for
zero hops. A matched reply also shows its local round-trip time, measured from
queueing the command through receiving the result. This includes both radio
directions and remote execution; it is not execution-only CPU time. Only one
terminal `cmd` can be pending at a time.
For example:
@@ -208,6 +235,11 @@ 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.
Incoming direct-route messages are labeled `ROUTED`, not `DIRECT`. Forwarders
consume direct-route prefixes as the packet travels, so the destination cannot
recover the reply's actual hop history from the received packet. Use `trace`
when the return route itself must be verified.
```
send {text}
```
@@ -286,6 +318,7 @@ For example:
channels
channel #rgdata Hello from Eugene 👋
channel 2 Another message
show channels on
```
Messages are UTF-8. Emoji use multiple bytes toward the available message
+162 -30
View File
@@ -534,7 +534,7 @@ void MyMesh::onDiscoveredContact(ContactInfo &contact, bool is_new, uint8_t path
}
#ifdef ENABLE_USB_INTERFACE
if (_terminal_mode) {
if (_terminal_mode && _terminal_display.shouldShowAdvert()) {
Serial.printf("\r\nADVERT from -> %s\r\n", contact.name);
Serial.printf(" type: %s\r\n", terminalContactTypeName(contact.type));
Serial.print(" public key: ");
@@ -701,7 +701,10 @@ ContactInfo* MyMesh::processAck(const uint8_t *data) {
}
void MyMesh::queueMessage(const ContactInfo &from, uint8_t txt_type, mesh::Packet *pkt,
uint32_t sender_timestamp, const uint8_t *extra, int extra_len, const char *text) {
uint32_t sender_timestamp, const uint8_t *extra,
int extra_len, const char *text,
bool terminal_command_reply,
uint32_t terminal_command_elapsed_millis) {
int i = 0;
if (app_target_ver >= 3) {
out_frame[i++] = RESP_CODE_CONTACT_MSG_RECV_V3;
@@ -738,9 +741,15 @@ void MyMesh::queueMessage(const ContactInfo &from, uint8_t txt_type, mesh::Packe
#ifdef ENABLE_USB_INTERFACE
if (_terminal_mode) {
const char* kind = txt_type == TXT_TYPE_CLI_DATA ? "CLI" : "MSG";
Serial.printf("\r\n(%s) %s -> from %s\r\n %s\r\n> ",
pkt->isRouteDirect() ? "DIRECT" : "FLOOD", kind,
from.name, text);
Serial.printf("\r\n(%s) %s -> from %s",
mesh::cli::terminalInboundRouteLabel(
pkt->isRouteDirect()), kind,
from.name);
if (terminal_command_reply) {
Serial.printf(" (round trip %lu ms)",
(unsigned long)terminal_command_elapsed_millis);
}
Serial.printf("\r\n %s\r\n> ", text);
}
#endif
@@ -842,7 +851,15 @@ void MyMesh::onMessageRecv(const ContactInfo &from, mesh::Packet *pkt, uint32_t
void MyMesh::onCommandDataRecv(const ContactInfo &from, mesh::Packet *pkt, uint32_t sender_timestamp,
const char *text) {
markConnectionActive(from); // in case this is from a server, and we have a connection
queueMessage(from, TXT_TYPE_CLI_DATA, pkt, sender_timestamp, NULL, 0, text);
bool terminal_command_reply = false;
uint32_t terminal_command_elapsed_millis = 0;
#ifdef ENABLE_USB_INTERFACE
terminal_command_reply = _terminal_command.takeReply(
from.id.pub_key, _ms->getMillis(), terminal_command_elapsed_millis);
if (terminal_command_reply) _terminal_command_target[0] = 0;
#endif
queueMessage(from, TXT_TYPE_CLI_DATA, pkt, sender_timestamp, NULL, 0, text,
terminal_command_reply, terminal_command_elapsed_millis);
}
void MyMesh::onSignedMessageRecv(const ContactInfo &from, mesh::Packet *pkt, uint32_t sender_timestamp,
@@ -855,6 +872,9 @@ void MyMesh::onSignedMessageRecv(const ContactInfo &from, mesh::Packet *pkt, uin
void MyMesh::onChannelMessageRecv(const mesh::GroupChannel &channel, mesh::Packet *pkt, uint32_t timestamp,
const char *text) {
const bool is_emergency_channel =
memcmp(channel.secret, EMERGENCY_CHANNEL_SECRET,
sizeof(EMERGENCY_CHANNEL_SECRET)) == 0;
int i = 0;
if (app_target_ver >= 3) {
out_frame[i++] = RESP_CODE_CHANNEL_MSG_RECV_V3;
@@ -891,7 +911,8 @@ void MyMesh::onChannelMessageRecv(const mesh::GroupChannel &channel, mesh::Packe
}
#ifdef ENABLE_USB_INTERFACE
if (_terminal_mode) {
if (_terminal_mode
&& _terminal_display.shouldShowChannel(is_emergency_channel)) {
ChannelDetails details;
const char* channel_name = getChannel(channel_idx, details) ? details.name : "Unknown";
Serial.printf("\r\nCHANNEL MSG -> %s (%s)\r\n %s\r\n> ",
@@ -908,7 +929,7 @@ void MyMesh::onChannelMessageRecv(const mesh::GroupChannel &channel, mesh::Packe
if (_ui) _ui->newMsg(path_len, channel_name, text, offline_queue_len);
#endif
if (pkt->isRouteFlood() && memcmp(channel.secret, EMERGENCY_CHANNEL_SECRET, sizeof(EMERGENCY_CHANNEL_SECRET)) == 0) {
if (pkt->isRouteFlood() && is_emergency_channel) {
bool zero_path = pkt->getPathHashCount() == 0;
uint16_t key = emergencyClientRepeatKey(pkt);
if (hasEmergencyClientRepeat(key)) {
@@ -1283,6 +1304,7 @@ MyMesh::MyMesh(mesh::Radio &radio, mesh::RNG &rng, mesh::RTCClock &rtc, SimpleMe
memset(_terminal_login_key, 0, sizeof(_terminal_login_key));
_terminal_login_expires_at = 0;
_terminal_login_target[0] = 0;
clearTerminalCommand();
clearTerminalTrace();
#endif
saved_radio_apply_pending = false;
@@ -3434,6 +3456,7 @@ void MyMesh::enterTerminalMode() {
_terminal_recipient_set = false;
memset(_terminal_recipient_key, 0, sizeof(_terminal_recipient_key));
clearTerminalLogin();
clearTerminalCommand();
clearTerminalTrace();
Serial.print("\r\n===== MeshCore Chat Terminal =====\r\n\r\n");
@@ -3449,6 +3472,7 @@ void MyMesh::exitTerminalMode() {
_terminal_recipient_set = false;
memset(_terminal_recipient_key, 0, sizeof(_terminal_recipient_key));
clearTerminalLogin();
clearTerminalCommand();
clearTerminalTrace();
}
@@ -3466,7 +3490,7 @@ ContactInfo* MyMesh::getTerminalRecipient() {
void MyMesh::printTerminalPath(const ContactInfo& recipient) {
Serial.printf(" Path to %s: ", recipient.name);
if (recipient.out_path_len == OUT_PATH_UNKNOWN) {
Serial.print("unknown (next login uses FLOOD)\r\n");
Serial.print("unknown (next send uses FLOOD)\r\n");
return;
}
if (!mesh::Packet::isValidPathLen(recipient.out_path_len)) {
@@ -3477,7 +3501,7 @@ void MyMesh::printTerminalPath(const ContactInfo& recipient) {
const uint8_t hash_size = (recipient.out_path_len >> 6) + 1;
const uint8_t hop_count = recipient.out_path_len & 63;
if (hop_count == 0) {
Serial.print("direct (zero hop; login uses DIRECT)\r\n");
Serial.print("direct (zero hop)\r\n");
return;
}
@@ -3487,7 +3511,7 @@ void MyMesh::printTerminalPath(const ContactInfo& recipient) {
&recipient.out_path[(size_t)hop * hash_size],
hash_size);
}
Serial.printf(" (%u %s, %u-byte hashes; login uses DIRECT)\r\n",
Serial.printf(" (%u %s, %u-byte hashes; used by DIRECT sends)\r\n",
(unsigned)hop_count, hop_count == 1 ? "hop" : "hops",
(unsigned)hash_size);
}
@@ -3507,7 +3531,7 @@ void MyMesh::handleTerminalPath(ContactInfo& recipient,
case mesh::cli::TerminalPathParseResult::Valid:
break;
case mesh::cli::TerminalPathParseResult::Missing:
Serial.print(" ERROR: use path <direct|clear|hop[,hop...]>\r\n");
Serial.print(" ERROR: use path <direct|clear|hops separated by spaces or commas>\r\n");
return;
case mesh::cli::TerminalPathParseResult::InvalidPrefix:
Serial.print(" ERROR: each path hop must be 2, 4, or 6 hex digits\r\n");
@@ -3516,7 +3540,7 @@ void MyMesh::handleTerminalPath(ContactInfo& recipient,
Serial.print(" ERROR: all path hops must use the same width\r\n");
return;
case mesh::cli::TerminalPathParseResult::InvalidSeparator:
Serial.print(" ERROR: separate path hops with commas\r\n");
Serial.print(" ERROR: separate path hops with spaces or commas\r\n");
return;
case mesh::cli::TerminalPathParseResult::TooManyHops:
Serial.print(" ERROR: path must contain at most 63 hops\r\n");
@@ -3612,6 +3636,39 @@ void MyMesh::listTerminalChannels() {
if (!found) Serial.print(" (none configured)\r\n");
}
void MyMesh::handleTerminalDisplayCommand(const char* arguments) {
mesh::TerminalDisplayCommand display_command;
const mesh::TerminalDisplayParseResult result =
mesh::parseTerminalDisplayCommand(arguments, display_command);
if (result == mesh::TerminalDisplayParseResult::InvalidCategory
|| result == mesh::TerminalDisplayParseResult::InvalidValue) {
Serial.print(
" ERROR: use show [adverts|channels|emergency] [on|off]\r\n");
return;
}
if (result == mesh::TerminalDisplayParseResult::StatusAll) {
Serial.printf(" Terminal display: adverts %s, channels %s, emergency %s\r\n",
_terminal_display.isEnabled(
mesh::TerminalDisplayCategory::Adverts) ? "on" : "off",
_terminal_display.isEnabled(
mesh::TerminalDisplayCategory::Channels) ? "on" : "off",
_terminal_display.isEnabled(
mesh::TerminalDisplayCategory::Emergency) ? "on" : "off");
return;
}
if (result == mesh::TerminalDisplayParseResult::Updated) {
_terminal_display.setEnabled(display_command.category,
display_command.enabled);
}
Serial.printf(" Terminal display: %s %s\r\n",
mesh::terminalDisplayCategoryName(display_command.category),
_terminal_display.isEnabled(display_command.category)
? "on" : "off");
}
void MyMesh::sendTerminalChannelMessage(ChannelDetails& channel,
const char* text) {
const size_t prefix_len = strlen(_prefs.node_name) + 2; // "name: "
@@ -3632,6 +3689,31 @@ void MyMesh::sendTerminalChannelMessage(ChannelDetails& channel,
}
}
void MyMesh::printTerminalSendStatus(const char* operation,
const ContactInfo& recipient, int result,
uint32_t timeout_millis) {
Serial.printf(" %s sent to %s (", operation, recipient.name);
if (result == MSG_SEND_SENT_FLOOD) {
Serial.print("FLOOD");
} else {
const uint8_t hop_count = recipient.out_path_len & 63;
if (hop_count == 0) {
Serial.print("zero-hop DIRECT");
} else {
const uint8_t hash_size = (recipient.out_path_len >> 6) + 1;
Serial.print("DIRECT via path ");
for (uint8_t hop = 0; hop < hop_count; hop++) {
if (hop != 0) Serial.print(',');
mesh::Utils::printHex(
Serial, &recipient.out_path[(size_t)hop * hash_size], hash_size);
}
Serial.printf("; %u %s, %u-byte hashes", (unsigned)hop_count,
hop_count == 1 ? "hop" : "hops", (unsigned)hash_size);
}
}
Serial.printf(", timeout %lu ms)\r\n", (unsigned long)timeout_millis);
}
void MyMesh::clearTerminalLogin() {
if (_terminal_login_pending && pending_login != 0
&& memcmp(&pending_login, _terminal_login_key,
@@ -3690,10 +3772,23 @@ void MyMesh::sendTerminalLogin(ContactInfo& recipient,
_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);
printTerminalSendStatus("Login", recipient, result, timeout);
}
void MyMesh::clearTerminalCommand() {
_terminal_command.clear();
_terminal_command_target[0] = 0;
}
void MyMesh::serviceTerminalCommand() {
uint32_t elapsed_millis = 0;
if (!_terminal_command.expire(_ms->getMillis(), elapsed_millis)) return;
if (_terminal_mode) {
Serial.printf("\r\n ERROR: command to %s timed out after %lu ms.\r\n> ",
_terminal_command_target, (unsigned long)elapsed_millis);
}
_terminal_command_target[0] = 0;
}
void MyMesh::sendTerminalCommand(ContactInfo& recipient,
@@ -3705,7 +3800,25 @@ void MyMesh::sendTerminalCommand(ContactInfo& recipient,
return;
}
const size_t command_len = command == NULL ? 0 : strlen(command);
serviceTerminalCommand();
if (_terminal_command.isPending()) {
Serial.printf(" ERROR: command to %s is still pending\r\n",
_terminal_command_target);
return;
}
// Be defensive if a caller passes the complete terminal line instead of
// the already-parsed argument. Never transmit the local "cmd" wrapper.
const char* on_air_command = command;
const char* nested_command = NULL;
if (mesh::cli::parseTerminalArgumentCommand(
command, "cmd", nested_command)
== mesh::cli::TerminalArgumentCommandMatch::Valid) {
on_air_command = nested_command;
}
const size_t command_len =
on_air_command == NULL ? 0 : strlen(on_air_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);
@@ -3715,18 +3828,21 @@ void MyMesh::sendTerminalCommand(ContactInfo& recipient,
const uint32_t logical_request_id =
getRTCClock()->getCurrentTimeUnique();
const uint32_t timestamp = getRTCClock()->getCurrentTimeUnique();
const uint32_t command_started_at = _ms->getMillis();
uint32_t est_timeout = 0;
const int result = sendCommandData(recipient, timestamp, 0, command,
const int result = sendCommandData(recipient, timestamp, 0, on_air_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);
_terminal_command.begin(recipient.id.pub_key, command_started_at,
est_timeout);
StrHelper::strzcpy(_terminal_command_target, recipient.name,
sizeof(_terminal_command_target));
printTerminalSendStatus("Remote command", recipient, result, est_timeout);
}
void MyMesh::clearTerminalTrace() {
@@ -3884,6 +4000,7 @@ void MyMesh::sendTerminalRawTrace(const char* arguments) {
void MyMesh::handleTerminalCommand(char* command) {
while (*command == ' ' || *command == '\t') command++;
if (*command == 0) return;
mesh::cli::normalizeCommandVerb(command);
#if defined(COMPANION_RADIO_FULL)
char full_reply[160];
@@ -3907,8 +4024,20 @@ void MyMesh::handleTerminalCommand(char* command) {
const char* path_spec = NULL;
const mesh::cli::TerminalArgumentCommandMatch path_match =
mesh::cli::parseTerminalArgumentCommand(command, "path", path_spec);
const char* recipient_prefix = NULL;
const mesh::cli::TerminalArgumentCommandMatch recipient_match =
mesh::cli::parseTerminalArgumentCommand(command, "to",
recipient_prefix);
const char* display_arguments = NULL;
const mesh::cli::TerminalArgumentCommandMatch display_match =
mesh::cli::parseTerminalArgumentCommand(command, "show",
display_arguments);
if (strcmp(command, "channels") == 0) {
if (display_match != mesh::cli::TerminalArgumentCommandMatch::NoMatch) {
handleTerminalDisplayCommand(
display_match == mesh::cli::TerminalArgumentCommandMatch::Valid
? display_arguments : NULL);
} else if (strcmp(command, "channels") == 0) {
listTerminalChannels();
} else if (channel_match != mesh::cli::TerminalChannelCommandMatch::NoMatch) {
if (channel_match != mesh::cli::TerminalChannelCommandMatch::Valid) {
@@ -3993,11 +4122,11 @@ void MyMesh::handleTerminalCommand(char* command) {
} else {
Serial.print(" ERROR: clock cannot go backwards\r\n");
}
} else if (strncmp(command, "to ", 3) == 0) {
const char* prefix = command + 3;
} else if (recipient_match
== mesh::cli::TerminalArgumentCommandMatch::Valid) {
ContactInfo* recipient = NULL;
if (*prefix != 0 && strlen(prefix) < sizeof(ContactInfo::name)) {
recipient = searchContactsByPrefix(prefix);
if (strlen(recipient_prefix) < sizeof(ContactInfo::name)) {
recipient = searchContactsByPrefix(recipient_prefix);
}
if (recipient == NULL || recipient->type == ADV_TYPE_NONE) {
Serial.print(" ERROR: name prefix not found\r\n");
@@ -4006,7 +4135,8 @@ void MyMesh::handleTerminalCommand(char* command) {
_terminal_recipient_set = true;
Serial.printf(" Recipient %s selected\r\n", recipient->name);
}
} else if (strcmp(command, "to") == 0) {
} else if (recipient_match
== mesh::cli::TerminalArgumentCommandMatch::MissingArgument) {
ContactInfo* recipient = getTerminalRecipient();
if (recipient != NULL) {
Serial.printf(" Current recipient: %s\r\n", recipient->name);
@@ -4162,8 +4292,9 @@ void MyMesh::handleTerminalCommand(char* command) {
Serial.print(" clock\r\n");
Serial.print(" time <epoch-seconds>\r\n");
Serial.print(" list [n]\r\n");
Serial.print(" show [adverts|channels|emergency] [on|off]\r\n");
Serial.print(" to [recipient name or prefix]\r\n");
Serial.print(" path [direct|clear|hop[,hop...]]\r\n");
Serial.print(" path [direct|clear|hops separated by spaces or commas]\r\n");
Serial.print(" send <text>\r\n");
Serial.print(" login <admin-password>\r\n");
Serial.print(" cmd <remote-command>\r\n");
@@ -4405,6 +4536,7 @@ void MyMesh::loop() {
BaseChatMesh::loop();
#ifdef ENABLE_USB_INTERFACE
serviceTerminalLogin();
serviceTerminalCommand();
serviceTerminalTrace();
#endif
if (!command_radio_apply_pending && saved_radio_apply_pending && !hasOutbound()
+14 -1
View File
@@ -43,6 +43,8 @@
#include <helpers/LogicalMessageCache.h>
#include <helpers/SimpleMeshTables.h>
#include <helpers/StaticPoolPacketManager.h>
#include <helpers/TerminalCommandTracker.h>
#include <helpers/TerminalDisplayFilter.h>
#include <target.h>
/* ---------------------------------- CONFIGURATION ------------------------------------- */
@@ -202,7 +204,9 @@ protected:
#endif
ContactInfo* processAck(const uint8_t *data) override;
void queueMessage(const ContactInfo &from, uint8_t txt_type, mesh::Packet *pkt, uint32_t sender_timestamp,
const uint8_t *extra, int extra_len, const char *text);
const uint8_t *extra, int extra_len, const char *text,
bool terminal_command_reply=false,
uint32_t terminal_command_elapsed_millis=0);
void onMessageRecv(const ContactInfo &from, mesh::Packet *pkt, uint32_t sender_timestamp,
const char *text) override;
@@ -280,9 +284,15 @@ private:
void importTerminalCard(char* command);
void listTerminalChannels();
void sendTerminalChannelMessage(ChannelDetails& channel, const char* text);
void handleTerminalDisplayCommand(const char* arguments);
void printTerminalSendStatus(const char* operation,
const ContactInfo& recipient, int result,
uint32_t timeout_millis);
void clearTerminalLogin();
void serviceTerminalLogin();
void sendTerminalLogin(ContactInfo& recipient, const char* password);
void clearTerminalCommand();
void serviceTerminalCommand();
void sendTerminalCommand(ContactInfo& recipient, const char* command);
void clearTerminalTrace();
void serviceTerminalTrace();
@@ -339,6 +349,7 @@ private:
bool _cli_rescue;
#ifdef ENABLE_USB_INTERFACE
bool _terminal_mode;
mesh::TerminalDisplayFilter _terminal_display;
bool _terminal_recipient_set;
uint8_t _terminal_recipient_key[PUB_KEY_SIZE];
uint8_t _terminal_tmp_buf[MAX_TRANS_UNIT];
@@ -346,6 +357,8 @@ private:
uint8_t _terminal_login_key[4];
unsigned long _terminal_login_expires_at;
char _terminal_login_target[32];
mesh::TerminalCommandTracker<PUB_KEY_SIZE> _terminal_command;
char _terminal_command_target[32];
bool _terminal_trace_pending;
uint8_t _terminal_trace_hash_size;
uint32_t _terminal_trace_tag;
+31 -10
View File
@@ -1,5 +1,7 @@
#include <helpers/BaseChatMesh.h>
#include <helpers/ContactListOrder.h>
#include <helpers/RemoteCliRequest.h>
#include <helpers/RemoteCliTimeout.h>
#include <Utils.h>
#ifndef SERVER_RESPONSE_DELAY
@@ -89,7 +91,7 @@ ContactInfo* BaseChatMesh::allocateContactSlot(bool transient_only) {
} else if (shouldOverwriteWhenFull()) {
// Find oldest non-favourite contact by oldest lastmod timestamp
for (int i = MAX_ANON_CONTACTS; i < num_contacts; i++) {
bool is_favourite = (contacts[i].flags & 0x01) != 0;
bool is_favourite = mesh::isFavoriteContact(contacts[i].flags);
if (!is_favourite && contacts[i].lastmod < oldest_lastmod) {
oldest_lastmod = contacts[i].lastmod;
oldest_idx = i;
@@ -495,6 +497,7 @@ int BaseChatMesh::sendCommandData(const ContactInfo& recipient,
uint32_t timestamp, uint8_t attempt,
const char* text, uint32_t& est_timeout,
uint32_t logical_request_id) {
est_timeout = 0;
int text_len = strlen(text);
if (text_len > MAX_TEXT_LEN) return MSG_SEND_FAILED;
@@ -514,18 +517,35 @@ int BaseChatMesh::sendCommandData(const ContactInfo& recipient,
recipient.getSharedSecret(self_id), temp,
payload_len);
if (pkt == NULL) return MSG_SEND_FAILED;
if (recipient.out_path_len != OUT_PATH_UNKNOWN
&& !mesh::Packet::isValidPathLen(recipient.out_path_len)) {
releasePacket(pkt);
return MSG_SEND_FAILED;
}
uint32_t t = _radio->getEstAirtimeFor(pkt->getRawLength());
const uint32_t base_timeout = recipient.out_path_len == OUT_PATH_UNKNOWN
? calcFloodTimeoutMillisFor(t)
: calcDirectTimeoutMillisFor(t, recipient.out_path_len);
if (!mesh::calculateRemoteCliTimeoutMillis(base_timeout, est_timeout)) {
releasePacket(pkt);
return MSG_SEND_FAILED;
}
int rc;
bool sent;
if (recipient.out_path_len == OUT_PATH_UNKNOWN) {
sendFloodScoped(recipient, pkt);
txt_send_timeout = futureMillis(est_timeout = calcFloodTimeoutMillisFor(t));
sent = sendFloodScoped(recipient, pkt);
rc = MSG_SEND_SENT_FLOOD;
} else {
sendDirect(pkt, recipient.out_path, recipient.out_path_len);
txt_send_timeout = futureMillis(est_timeout = calcDirectTimeoutMillisFor(t, recipient.out_path_len));
sent = sendDirect(pkt, recipient.out_path, recipient.out_path_len);
rc = MSG_SEND_SENT_DIRECT;
}
if (!sent) {
est_timeout = 0;
return MSG_SEND_FAILED;
}
txt_send_timeout = futureMillis(est_timeout);
return rc;
}
@@ -851,12 +871,12 @@ void BaseChatMesh::resetPathTo(ContactInfo& recipient) {
static ContactInfo* table; // pass via global :-(
static int cmp_adv_timestamp(const void *a, const void *b) {
static int cmp_contact_list_order(const void *a, const void *b) {
int a_idx = *((int *)a);
int b_idx = *((int *)b);
if (table[b_idx].last_advert_timestamp > table[a_idx].last_advert_timestamp) return 1;
if (table[b_idx].last_advert_timestamp < table[a_idx].last_advert_timestamp) return -1;
return 0;
return mesh::compareContactListOrder(
table[a_idx].flags, table[a_idx].last_advert_timestamp,
table[b_idx].flags, table[b_idx].last_advert_timestamp);
}
void BaseChatMesh::scanRecentContacts(int last_n, ContactVisitor* visitor) {
@@ -864,7 +884,8 @@ void BaseChatMesh::scanRecentContacts(int last_n, ContactVisitor* visitor) {
sort_array[i] = i;
}
table = contacts; // pass via global *sigh* :-(
qsort(sort_array, num_contacts, sizeof(sort_array[0]), cmp_adv_timestamp);
qsort(sort_array, num_contacts, sizeof(sort_array[0]),
cmp_contact_list_order);
if (last_n == 0) {
last_n = num_contacts; // scan ALL
+17 -7
View File
@@ -58,6 +58,13 @@ enum class TerminalPathParseResult : uint8_t {
RouteTooLong,
};
// Direct-route prefixes are consumed hop by hop, so a destination cannot
// distinguish a zero-hop packet from one that arrived through repeaters.
// "ROUTED" describes the received unicast class without claiming zero hops.
inline const char* terminalInboundRouteLabel(bool is_direct_route) {
return is_direct_route ? "ROUTED" : "FLOOD";
}
struct RecentRepeaterGetQuery {
int page;
uint8_t search_prefix[3];
@@ -204,10 +211,11 @@ inline bool terminalPathKeywordMatches(const char* text,
return *text == 0;
}
// Parse: direct | clear | <hop>[,<hop> ...]
// Parse: direct | clear | <hop>[<separator><hop> ...]
// Each explicit hop is a one-, two-, or three-byte hexadecimal prefix. All
// hops must use the same width because that width is encoded once for the
// complete MeshCore direct path.
// complete MeshCore direct path. Separators may be commas, spaces, tabs, or
// mixtures of a comma and surrounding whitespace.
inline TerminalPathParseResult parseTerminalPath(
const char* input, uint8_t* output, size_t output_capacity,
uint8_t max_hops, TerminalPath& result) {
@@ -269,14 +277,16 @@ inline TerminalPathParseResult parseTerminalPath(
}
hop_count++;
input += token_len;
bool consumed_comma = false;
input = skipRecentRepeaterSpaces(input);
if (*input == 0) break;
if (*input != ',') {
return TerminalPathParseResult::InvalidSeparator;
if (*input == ',') {
consumed_comma = true;
input = skipRecentRepeaterSpaces(input + 1);
}
input = skipRecentRepeaterSpaces(input + 1);
if (*input == 0) {
return TerminalPathParseResult::InvalidPrefix;
if (consumed_comma) return TerminalPathParseResult::InvalidPrefix;
break;
}
}
+25
View File
@@ -0,0 +1,25 @@
#pragma once
#include <stdint.h>
namespace mesh {
static constexpr uint8_t CONTACT_FLAG_FAVORITE = 0x01;
inline bool isFavoriteContact(uint8_t flags) {
return (flags & CONTACT_FLAG_FAVORITE) != 0;
}
// Negative means A sorts before B. Favorites come first, with the newest
// advertisement first inside both the favorite and non-favorite groups.
inline int compareContactListOrder(uint8_t a_flags, uint32_t a_timestamp,
uint8_t b_flags, uint32_t b_timestamp) {
const bool a_favorite = isFavoriteContact(a_flags);
const bool b_favorite = isFavoriteContact(b_flags);
if (a_favorite != b_favorite) return a_favorite ? -1 : 1;
if (a_timestamp > b_timestamp) return -1;
if (a_timestamp < b_timestamp) return 1;
return 0;
}
} // namespace mesh
+27
View File
@@ -0,0 +1,27 @@
#pragma once
#include <stdint.h>
namespace mesh {
static constexpr uint32_t REMOTE_CLI_TIMEOUT_TOTAL_PERCENT = 300UL;
static constexpr uint32_t REMOTE_CLI_MAX_TIMEOUT_MILLIS = 0x7FFFFFFFUL;
// Remote CLI_DATA waits for an application-level reply rather than a packet
// ACK. Use three times the normal route estimate and keep the result within
// the signed delta range accepted by futureMillis().
inline bool calculateRemoteCliTimeoutMillis(uint32_t base_timeout_millis,
uint32_t& timeout_millis) {
timeout_millis = 0;
if (base_timeout_millis == 0) return false;
const uint64_t total =
static_cast<uint64_t>(base_timeout_millis)
* REMOTE_CLI_TIMEOUT_TOTAL_PERCENT / 100UL;
if (total > REMOTE_CLI_MAX_TIMEOUT_MILLIS) return false;
timeout_millis = static_cast<uint32_t>(total);
return true;
}
} // namespace mesh
+64
View File
@@ -0,0 +1,64 @@
#pragma once
#include <stddef.h>
#include <stdint.h>
#include <string.h>
namespace mesh {
// Tracks one terminal-originated remote command. CLI_DATA replies do not echo
// a request identifier, so the sender must serialize commands and match the
// next reply by the full contact key.
template <size_t KeySize>
class TerminalCommandTracker {
public:
TerminalCommandTracker() { clear(); }
bool begin(const uint8_t* key, uint32_t sent_at,
uint32_t timeout_millis) {
if (_pending || key == NULL || timeout_millis == 0) return false;
memcpy(_key, key, KeySize);
_sent_at = sent_at;
_timeout_millis = timeout_millis;
_pending = true;
return true;
}
bool isPending() const { return _pending; }
bool takeReply(const uint8_t* key, uint32_t received_at,
uint32_t& elapsed_millis) {
elapsed_millis = 0;
if (!_pending || key == NULL || memcmp(_key, key, KeySize) != 0) {
return false;
}
elapsed_millis = received_at - _sent_at;
clear();
return true;
}
bool expire(uint32_t now, uint32_t& elapsed_millis) {
elapsed_millis = 0;
if (!_pending) return false;
const uint32_t elapsed = now - _sent_at;
if (elapsed < _timeout_millis) return false;
elapsed_millis = elapsed;
clear();
return true;
}
void clear() {
_pending = false;
memset(_key, 0, sizeof(_key));
_sent_at = 0;
_timeout_millis = 0;
}
private:
bool _pending;
uint8_t _key[KeySize];
uint32_t _sent_at;
uint32_t _timeout_millis;
};
} // namespace mesh
+141
View File
@@ -0,0 +1,141 @@
#pragma once
#include <stddef.h>
#include <stdint.h>
#include <string.h>
namespace mesh {
enum class TerminalDisplayCategory : uint8_t {
None = 0,
Adverts,
Channels,
Emergency,
};
enum class TerminalDisplayParseResult : uint8_t {
StatusAll = 0,
StatusOne,
Updated,
InvalidCategory,
InvalidValue,
};
struct TerminalDisplayCommand {
TerminalDisplayCategory category;
bool enabled;
};
// Runtime-only terminal filters. Binary Companion delivery and offline queueing
// are deliberately unaffected by these display choices.
class TerminalDisplayFilter {
public:
TerminalDisplayFilter()
: _adverts(false), _channels(false), _emergency(true) {}
bool isEnabled(TerminalDisplayCategory category) const {
if (category == TerminalDisplayCategory::Adverts) return _adverts;
if (category == TerminalDisplayCategory::Channels) return _channels;
if (category == TerminalDisplayCategory::Emergency) return _emergency;
return false;
}
void setEnabled(TerminalDisplayCategory category, bool enabled) {
if (category == TerminalDisplayCategory::Adverts) _adverts = enabled;
else if (category == TerminalDisplayCategory::Channels) _channels = enabled;
else if (category == TerminalDisplayCategory::Emergency) _emergency = enabled;
}
bool shouldShowAdvert() const { return _adverts; }
bool shouldShowChannel(bool is_emergency) const {
return is_emergency ? _emergency : _channels;
}
private:
bool _adverts;
bool _channels;
bool _emergency;
};
inline char terminalDisplayAsciiLower(char value) {
return value >= 'A' && value <= 'Z' ? value + ('a' - 'A') : value;
}
inline bool terminalDisplayTokenEquals(const char* token, size_t token_len,
const char* expected) {
if (token == NULL || expected == NULL || strlen(expected) != token_len) {
return false;
}
for (size_t i = 0; i < token_len; i++) {
if (terminalDisplayAsciiLower(token[i])
!= terminalDisplayAsciiLower(expected[i])) {
return false;
}
}
return true;
}
inline const char* skipTerminalDisplaySpaces(const char* text) {
while (text != NULL && (*text == ' ' || *text == '\t')) text++;
return text;
}
inline const char* terminalDisplayCategoryName(
TerminalDisplayCategory category) {
if (category == TerminalDisplayCategory::Adverts) return "adverts";
if (category == TerminalDisplayCategory::Channels) return "channels";
if (category == TerminalDisplayCategory::Emergency) return "emergency";
return "unknown";
}
inline TerminalDisplayParseResult parseTerminalDisplayCommand(
const char* arguments, TerminalDisplayCommand& command) {
command.category = TerminalDisplayCategory::None;
command.enabled = false;
const char* cursor = skipTerminalDisplaySpaces(arguments);
if (cursor == NULL || *cursor == 0) {
return TerminalDisplayParseResult::StatusAll;
}
const char* category_token = cursor;
while (*cursor != 0 && *cursor != ' ' && *cursor != '\t') cursor++;
const size_t category_len = (size_t)(cursor - category_token);
if (terminalDisplayTokenEquals(category_token, category_len, "advert")
|| terminalDisplayTokenEquals(category_token, category_len, "adverts")
|| terminalDisplayTokenEquals(category_token, category_len,
"advertisements")) {
command.category = TerminalDisplayCategory::Adverts;
} else if (terminalDisplayTokenEquals(category_token, category_len,
"channel")
|| terminalDisplayTokenEquals(category_token, category_len,
"channels")) {
command.category = TerminalDisplayCategory::Channels;
} else if (terminalDisplayTokenEquals(category_token, category_len,
"emergency")) {
command.category = TerminalDisplayCategory::Emergency;
} else {
return TerminalDisplayParseResult::InvalidCategory;
}
cursor = skipTerminalDisplaySpaces(cursor);
if (*cursor == 0) return TerminalDisplayParseResult::StatusOne;
const char* value_token = cursor;
while (*cursor != 0 && *cursor != ' ' && *cursor != '\t') cursor++;
const size_t value_len = (size_t)(cursor - value_token);
cursor = skipTerminalDisplaySpaces(cursor);
if (*cursor != 0) return TerminalDisplayParseResult::InvalidValue;
if (terminalDisplayTokenEquals(value_token, value_len, "on")) {
command.enabled = true;
} else if (terminalDisplayTokenEquals(value_token, value_len, "off")) {
command.enabled = false;
} else {
return TerminalDisplayParseResult::InvalidValue;
}
return TerminalDisplayParseResult::Updated;
}
} // namespace mesh
+2 -1
View File
@@ -42,7 +42,8 @@ does not reflect the GoogleTest count -- run the built binary directly
| `test_telemetry_history` | `src/helpers/TelemetryHistory.h` | 30-minute rings; seven-day temperature/voltage and dynamically sized GPS retention; exact 1 C temperature/status encoding; separate Base64 series payloads; 14-bit GPS differentials; resize preservation, heap budgets, and 1-based paging bounds |
| `test_flood_filter_policy` | `src/helpers/FloodFilterPolicy.h` | unordered blacklist matching; ordered 1/2/3-byte pbyte rule prefixes; original incoming scope classes and canonical region-name identity; channel-authentication cache key comparison; priority ordering and terminal stop masks; bridge-bucket and regionless channel-target selector encoding; `require=region` and per-channel scope-gate truth tables; fast/slow timing; adding, replacing, and preserving packet scope |
| `test_logical_message_cache` | `src/helpers/LogicalMessageCache.h` | bounded logical-message mapping; stable retry timestamps; exact older retries after newer messages; stale and same-timestamp mismatch rejection |
| `test_remote_cli_reply_cache` | `src/helpers/RemoteCliReplyCache.h`, `src/helpers/RemoteCliRequest.h` | authenticated logical-request matching; bounded recent-reply history; backward-compatible retry identity; empty-response completion; on-air truncation and clearing |
| `test_cli_command_utils` | `src/helpers/CLICommandUtils.h`, `src/helpers/ContactListOrder.h`, `src/helpers/TerminalCommandTracker.h`, `src/helpers/TerminalDisplayFilter.h` | terminal verb/argument/path parsing; routed receive labels; quiet display defaults and independent emergency filtering; favorite-first contact ordering; single-command reply matching, round-trip timing, and rollover-safe expiration |
| `test_remote_cli_reply_cache` | `src/helpers/RemoteCliReplyCache.h`, `src/helpers/RemoteCliRequest.h`, `src/helpers/RemoteCliTimeout.h` | authenticated logical-request matching; bounded recent-reply history; backward-compatible retry identity; 300% response timeout; empty-response completion; on-air truncation and clearing |
| `test_companion_frame_queue` | `src/helpers/CompanionFrameQueue.h` | response/required/best-effort classification; reserved capacity; stable priority; safe eviction; message-waiting coalescing |
| `test_serial_mode_switch` | `src/helpers/ArduinoSerialInterface.cpp`, `src/helpers/MultiSerialInterface.h` | independent terminal/seeder control-sequence recognition across reads and binary-frame boundaries; passthrough ownership of USB input and suppression of binary output; Bluetooth-only connection and pairing-request routing |
| `test_ble_tx_stall_watchdog` | `src/helpers/BleTxStallWatchdog.h` | exact BLE fragment progress; blocked-reply timeout; rollover-safe elapsed time; disconnect recovery retry and completion |
@@ -1,8 +1,106 @@
#include <gtest/gtest.h>
#include <helpers/CLICommandUtils.h>
#include <helpers/ContactListOrder.h>
#include <helpers/TerminalCommandTracker.h>
#include <helpers/TerminalDisplayFilter.h>
#include <helpers/WiFiPowerSave.h>
TEST(ContactListOrder, SortsFavoritesBeforeNewerNonFavorites) {
EXPECT_LT(mesh::compareContactListOrder(0x01, 100, 0x00, 200), 0);
EXPECT_GT(mesh::compareContactListOrder(0x00, 200, 0x01, 100), 0);
}
TEST(ContactListOrder, SortsEachGroupByNewestAdvertisement) {
EXPECT_LT(mesh::compareContactListOrder(0x01, 200, 0x01, 100), 0);
EXPECT_GT(mesh::compareContactListOrder(0x00, 100, 0x00, 200), 0);
EXPECT_EQ(0, mesh::compareContactListOrder(0x01, 100, 0x01, 100));
}
TEST(CLICommandUtils, LabelsInboundDirectRouteAsRouted) {
EXPECT_STREQ("ROUTED", mesh::cli::terminalInboundRouteLabel(true));
EXPECT_STREQ("FLOOD", mesh::cli::terminalInboundRouteLabel(false));
}
TEST(TerminalDisplayFilter, UsesQuietDefaultsWithEmergencyVisible) {
mesh::TerminalDisplayFilter filter;
EXPECT_FALSE(filter.shouldShowAdvert());
EXPECT_FALSE(filter.shouldShowChannel(false));
EXPECT_TRUE(filter.shouldShowChannel(true));
}
TEST(TerminalDisplayFilter, KeepsChannelAndEmergencyControlsIndependent) {
mesh::TerminalDisplayFilter filter;
filter.setEnabled(mesh::TerminalDisplayCategory::Channels, true);
filter.setEnabled(mesh::TerminalDisplayCategory::Emergency, false);
filter.setEnabled(mesh::TerminalDisplayCategory::Adverts, true);
EXPECT_TRUE(filter.shouldShowAdvert());
EXPECT_TRUE(filter.shouldShowChannel(false));
EXPECT_FALSE(filter.shouldShowChannel(true));
}
TEST(TerminalDisplayFilter, ParsesStatusAndUpdatesCaseInsensitively) {
using mesh::TerminalDisplayCategory;
using mesh::TerminalDisplayParseResult;
mesh::TerminalDisplayCommand command;
EXPECT_EQ(TerminalDisplayParseResult::StatusAll,
mesh::parseTerminalDisplayCommand(NULL, command));
EXPECT_EQ(TerminalDisplayParseResult::StatusOne,
mesh::parseTerminalDisplayCommand(" channels ", command));
EXPECT_EQ(TerminalDisplayCategory::Channels, command.category);
EXPECT_EQ(TerminalDisplayParseResult::Updated,
mesh::parseTerminalDisplayCommand(" Emergency OFF ", command));
EXPECT_EQ(TerminalDisplayCategory::Emergency, command.category);
EXPECT_FALSE(command.enabled);
EXPECT_EQ(TerminalDisplayParseResult::Updated,
mesh::parseTerminalDisplayCommand("advertisements on", command));
EXPECT_EQ(TerminalDisplayCategory::Adverts, command.category);
EXPECT_TRUE(command.enabled);
}
TEST(TerminalDisplayFilter, RejectsUnknownCategoriesAndValues) {
mesh::TerminalDisplayCommand command;
EXPECT_EQ(mesh::TerminalDisplayParseResult::InvalidCategory,
mesh::parseTerminalDisplayCommand("messages on", command));
EXPECT_EQ(mesh::TerminalDisplayParseResult::InvalidValue,
mesh::parseTerminalDisplayCommand("channels maybe", command));
EXPECT_EQ(mesh::TerminalDisplayParseResult::InvalidValue,
mesh::parseTerminalDisplayCommand("emergency off extra", command));
}
TEST(TerminalCommandTracker, MatchesReplyAndReportsRoundTrip) {
mesh::TerminalCommandTracker<4> tracker;
const uint8_t target[] = {0x11, 0x22, 0x33, 0x44};
const uint8_t other[] = {0x11, 0x22, 0x33, 0x45};
uint32_t elapsed = 99;
ASSERT_TRUE(tracker.begin(target, 1000, 3000));
EXPECT_FALSE(tracker.begin(target, 1100, 3000));
EXPECT_FALSE(tracker.takeReply(other, 1500, elapsed));
EXPECT_EQ(0UL, elapsed);
EXPECT_TRUE(tracker.isPending());
EXPECT_TRUE(tracker.takeReply(target, 1750, elapsed));
EXPECT_EQ(750UL, elapsed);
EXPECT_FALSE(tracker.isPending());
}
TEST(TerminalCommandTracker, ExpiresSafelyAcrossMillisRollover) {
mesh::TerminalCommandTracker<4> tracker;
const uint8_t target[] = {0xAA, 0xBB, 0xCC, 0xDD};
uint32_t elapsed = 0;
ASSERT_TRUE(tracker.begin(target, 0xFFFFFFF0UL, 32));
EXPECT_FALSE(tracker.expire(0x0000000FUL, elapsed));
EXPECT_TRUE(tracker.expire(0x00000010UL, elapsed));
EXPECT_EQ(32UL, elapsed);
EXPECT_FALSE(tracker.isPending());
}
TEST(CLICommandUtils, NormalizesCapitalizedSetVerb) {
char command[] = "Set altpath 600000,0d2784,F8DADA";
@@ -123,6 +221,20 @@ TEST(CLICommandUtils, ParsesTerminalLoginAndRemoteCommandArguments) {
"logins password", "login", argument));
}
TEST(CLICommandUtils, ParsesCapitalizedRecipientWithRepeatedWhitespace) {
using mesh::cli::TerminalArgumentCommandMatch;
char command[] = "To SEA Mercerwood";
const char* recipient = nullptr;
mesh::cli::normalizeCommandVerb(command);
EXPECT_STREQ("to SEA Mercerwood", command);
EXPECT_EQ(TerminalArgumentCommandMatch::Valid,
mesh::cli::parseTerminalArgumentCommand(
command, "to", recipient));
EXPECT_STREQ("SEA Mercerwood", recipient);
}
TEST(CLICommandUtils, ParsesTerminalDirectAndExplicitPaths) {
using mesh::cli::TerminalPathMode;
using mesh::cli::TerminalPathParseResult;
@@ -157,6 +269,25 @@ TEST(CLICommandUtils, ParsesTerminalDirectAndExplicitPaths) {
EXPECT_EQ(0, memcmp(expected, route, sizeof(expected)));
}
TEST(CLICommandUtils, ParsesSpaceSeparatedTerminalPath) {
using mesh::cli::TerminalPathMode;
using mesh::cli::TerminalPathParseResult;
uint8_t route[6] = {};
mesh::cli::TerminalPath path;
ASSERT_EQ(TerminalPathParseResult::Valid,
mesh::cli::parseTerminalPath(
"7773D0 7E7662", route, sizeof(route), 63, path));
const uint8_t expected[] = {0x77, 0x73, 0xD0, 0x7E, 0x76, 0x62};
EXPECT_EQ(TerminalPathMode::Explicit, path.mode);
EXPECT_EQ(3, path.hash_size);
EXPECT_EQ(2, path.hop_count);
EXPECT_EQ(0x82, path.encoded_len);
EXPECT_EQ(sizeof(expected), path.byte_len);
EXPECT_EQ(0, memcmp(expected, route, sizeof(expected)));
}
TEST(CLICommandUtils, RejectsMalformedTerminalPaths) {
using mesh::cli::TerminalPathParseResult;
uint8_t route[4] = {};
@@ -177,9 +308,9 @@ TEST(CLICommandUtils, RejectsMalformedTerminalPaths) {
EXPECT_EQ(TerminalPathParseResult::MixedPrefixSize,
mesh::cli::parseTerminalPath(
"AA,BBBB", route, sizeof(route), 63, path));
EXPECT_EQ(TerminalPathParseResult::InvalidSeparator,
EXPECT_EQ(TerminalPathParseResult::InvalidPrefix,
mesh::cli::parseTerminalPath(
"AA BB", route, sizeof(route), 63, path));
"AA,,BB", route, sizeof(route), 63, path));
EXPECT_EQ(TerminalPathParseResult::TooManyHops,
mesh::cli::parseTerminalPath(
"AA,BB", route, sizeof(route), 1, path));
@@ -2,6 +2,23 @@
#include <helpers/RemoteCliReplyCache.h>
#include <helpers/RemoteCliRequest.h>
#include <helpers/RemoteCliTimeout.h>
TEST(RemoteCliTimeout, UsesThreeHundredPercentOfRouteEstimate) {
uint32_t timeout = 0;
ASSERT_TRUE(mesh::calculateRemoteCliTimeoutMillis(6218, timeout));
EXPECT_EQ(18654UL, timeout);
}
TEST(RemoteCliTimeout, RejectsInvalidOrOverflowingEstimate) {
uint32_t timeout = 123;
EXPECT_FALSE(mesh::calculateRemoteCliTimeoutMillis(0, timeout));
EXPECT_EQ(0UL, timeout);
EXPECT_FALSE(mesh::calculateRemoteCliTimeoutMillis(0x7FFFFFFFUL, timeout));
EXPECT_EQ(0UL, timeout);
}
TEST(RemoteCliReplyCache, ReplaysOnlyTheSameAuthenticatedRequest) {
mesh::RemoteCliReplyCache cache;