Improve Companion terminal routing and controls
This commit is contained in:
1 parent
71619934fc
commit
4d51d49660
14 files changed
+709
-72
No files matched your search
@@ -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.
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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()
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -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
@@ -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;
|
||||
|
||||
Reference in new issue
Block a user