Fix Full Companion terminal diagnostics

This commit is contained in:
mikecarper committed 2026-08-29 16:57:44 -07:00
1 parent e666bbc6a8
commit 4c93df9d57
7 files changed
+176 -9

No files matched your search

+2
View File
@@ -47,6 +47,8 @@ over the normal binary USB, BLE, or TCP connection:
| Command | Availability |
|---|---|
| [`board`](cli_commands.md#show-the-hardware-name) | Every Companion text terminal and command `0x42` |
| [`memory`](#memory) | ESP32 Full Companion text terminal and command `0x42` |
| [`get/set radio.rxgain`](cli_commands.md#view-or-change-this-nodes-rx-boosted-gain-mode-sx12xx-and-lr1110-v1141) | Companion with a supported radio |
| [`get/set radio.fem.rxgain`](cli_commands.md#view-or-change-the-lora-fem-receive-path-gain-state-on-supported-boards) | Companion on a board with controllable LoRa FEM LNA |
| [`get/set wifi.powersave`](cli_commands.md#browser-configuration-portal-esp32-repeater-and-room-server) | ESP32 WiFi Companion; active transport constraints still apply |
+5 -3
View File
@@ -372,9 +372,11 @@ The terminal supports Companion chat commands, including `channels`,
`channel <name-or-slot> <message>`, remote administration with
`login <admin-password>` and `cmd <remote-command>`, and routed
`trace [recipient-name-or-prefix]`, plus local `ota`, `tempradio`, and
`normalradio` controls. ESP32 builds also provide local
WiFi credential, status, WebConfig, CLI-tab, and power-save controls. Every
Full Companion provides persistent `get/set usb.logging` and starts with
`normalradio` controls. `board` reports the exact hardware target. ESP32 Full
Companion also provides `memory`, which reports heap, internal allocation,
PSRAM, and offline-queue usage; it is omitted from non-ESP32 help. ESP32 builds
also provide local WiFi credential, status, WebConfig, and power-save controls.
Every Full Companion provides persistent `get/set usb.logging` and starts with
logging off on a fresh installation.
### ESP32 single USB serial port
+35 -6
View File
@@ -9,7 +9,12 @@
#include "helpers/radiolib/RxBoostedGainDefaults.h"
#include "helpers/radiolib/CadTiming.h"
#if defined(ESP32_PLATFORM) && defined(BOARD_HAS_PSRAM)
#if COMPANION_FEATURE_MEMORY_DIAGNOSTICS
#include <helpers/CompanionTerminalDiagnostics.h>
#endif
#if defined(ESP32_PLATFORM) \
&& (defined(BOARD_HAS_PSRAM) || COMPANION_FEATURE_MEMORY_DIAGNOSTICS)
#include <esp_heap_caps.h>
#endif
@@ -2043,6 +2048,31 @@ bool MyMesh::handleLocalControlCommand(const char* command, char* reply,
if (!command || !reply || reply_size == 0) return false;
while (*command == ' ') command++;
if (strcmp(command, "board") == 0) {
const char* hardware_name = board.getManufacturerName();
snprintf(reply, reply_size, "%s",
hardware_name != NULL ? hardware_name : "Unknown hardware");
return true;
}
#if COMPANION_FEATURE_MEMORY_DIAGNOSTICS
if (strcmp(command, "memory") == 0) {
const mesh::CompanionMemoryDiagnostics diagnostics = {
(uint32_t)ESP.getFreeHeap(),
(uint32_t)ESP.getMinFreeHeap(),
(uint32_t)ESP.getMaxAllocHeap(),
(uint32_t)heap_caps_get_free_size(MALLOC_CAP_INTERNAL),
(uint32_t)heap_caps_get_largest_free_block(MALLOC_CAP_INTERNAL),
(uint32_t)ESP.getFreePsram(),
(uint32_t)ESP.getPsramSize(),
offline_queue_len,
getOfflineQueueCapacity()
};
mesh::formatCompanionMemoryDiagnostics(reply, reply_size, diagnostics);
return true;
}
#endif
if (handleCadCommand(command, reply, reply_size)) return true;
if (strcmp(command, "get radio.rxgain") == 0) {
@@ -5979,6 +6009,10 @@ void MyMesh::handleTerminalCommand(char* command) {
FIRMWARE_VERSION, (unsigned)FIRMWARE_VER_CODE, FIRMWARE_BUILD_DATE);
} else if (strcmp(command, "help") == 0) {
terminalOutput().print("Commands:\r\n");
terminalOutput().print(" board\r\n");
#if COMPANION_FEATURE_MEMORY_DIAGNOSTICS
terminalOutput().print(" memory\r\n");
#endif
terminalOutput().print(" get display.rotation\r\n");
terminalOutput().print(" set display.rotation <0|90|180|270>\r\n");
terminalOutput().print(" set {name|lat|lon|freq|tx|af} {value}\r\n");
@@ -6220,11 +6254,6 @@ bool MyMesh::handleCommand(const char* command, uint32_t sender_timestamp,
return true;
}
if (strcmp(command, "board") == 0) {
snprintf(reply, reply_capacity, "%s", board.getManufacturerName());
return true;
}
if (strcmp(command, "ver") == 0) {
snprintf(reply, reply_capacity, "%s (Build: %s)", FIRMWARE_VERSION,
FIRMWARE_BUILD_DATE);
@@ -0,0 +1,39 @@
#pragma once
#include <stddef.h>
#include <stdint.h>
#include <stdio.h>
namespace mesh {
struct CompanionMemoryDiagnostics {
uint32_t heap_free;
uint32_t heap_min;
uint32_t heap_max_alloc;
uint32_t internal_free;
uint32_t internal_max_alloc;
uint32_t psram_free;
uint32_t psram_total;
int queue_used;
int queue_capacity;
};
inline bool formatCompanionMemoryDiagnostics(
char* output, size_t output_size,
const CompanionMemoryDiagnostics& diagnostics) {
if (output == nullptr || output_size == 0) return false;
const int written = snprintf(
output, output_size,
"Heap free=%lu min=%lu max=%lu int=%lu/%lu PSRAM=%lu/%lu queue=%d/%d",
(unsigned long)diagnostics.heap_free,
(unsigned long)diagnostics.heap_min,
(unsigned long)diagnostics.heap_max_alloc,
(unsigned long)diagnostics.internal_free,
(unsigned long)diagnostics.internal_max_alloc,
(unsigned long)diagnostics.psram_free,
(unsigned long)diagnostics.psram_total,
diagnostics.queue_used, diagnostics.queue_capacity);
return written >= 0 && static_cast<size_t>(written) < output_size;
}
} // namespace mesh
+2
View File
@@ -14,6 +14,7 @@ pio test -e native # all suites except KISS modem
pio test -e native_kiss_modem # KISS modem suite
pio test -e native -f test_webconfig_keys # a single suite
python3 test/test_indicator_display_profile.py # Indicator RAM/scale contract
python3 test/test_companion_terminal_profile.py # Companion CLI capability gates
```
A green `[PASSED]` per suite means GoogleTest returned 0 (all assertions
@@ -57,6 +58,7 @@ does not reflect the GoogleTest count -- run the built binary directly
| `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`, `src/helpers/CompanionHardwareCommandCompat.h` | response/required/best-effort classification; reserved capacity; stable priority; safe eviction; message-waiting coalescing; command `0x42` framed-CLI disambiguation and deprecated hardware-alias mapping |
| `test_companion_status_response` | `src/helpers/CompanionStatusResponse.h` | request-tag correlation and minimum status-response length, including rejection of the three-entry ACL payload that previously masqueraded as status |
| `test_companion_terminal_diagnostics` | `src/helpers/CompanionTerminalDiagnostics.h` | bounded Full Companion ESP32 heap/PSRAM/offline-queue diagnostic formatting, including truthful no-PSRAM output |
| `test_serial_mode_switch` | `src/helpers/ArduinoSerialInterface.cpp`, `src/helpers/MultiSerialInterface.h` | terminal/seeder control-sequence recognition, single-TTY logging transitions, and USB/TCP ownership; queued/atomic USB output under backpressure and short writes; partial-frame busy state; requester-affine replies, locked contact streams, and Bluetooth-only pairing 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 |
| `test_ble_mota_control` | `src/helpers/BleMotaStream.h`, `CompanionMotaControl.h` | encrypted mOTA channel ring buffering, overflow fail-closed behavior, request gating, and strict rejection of injected or USB-ownership control commands |
@@ -0,0 +1,48 @@
#include <gtest/gtest.h>
#include <cstring>
#include <helpers/CompanionTerminalDiagnostics.h>
TEST(CompanionTerminalDiagnostics, ReportsEveryEsp32MemoryField) {
const mesh::CompanionMemoryDiagnostics diagnostics = {
66244, 2516, 63428, 61004, 42812, 6442475, 8386055, 3, 512
};
char output[160] = {};
ASSERT_TRUE(mesh::formatCompanionMemoryDiagnostics(
output, sizeof(output), diagnostics));
EXPECT_STREQ(
"Heap free=66244 min=2516 max=63428 int=61004/42812 "
"PSRAM=6442475/8386055 queue=3/512",
output);
}
TEST(CompanionTerminalDiagnostics, TruthfullyReportsNoPsram) {
const mesh::CompanionMemoryDiagnostics diagnostics = {
104857, 98234, 90112, 104857, 90112, 0, 0, 0, 16
};
char output[160] = {};
ASSERT_TRUE(mesh::formatCompanionMemoryDiagnostics(
output, sizeof(output), diagnostics));
EXPECT_NE(nullptr, strstr(output, "PSRAM=0/0"));
EXPECT_NE(nullptr, strstr(output, "queue=0/16"));
}
TEST(CompanionTerminalDiagnostics, DetectsAReplyBufferThatIsTooSmall) {
const mesh::CompanionMemoryDiagnostics diagnostics = {
UINT32_MAX, UINT32_MAX, UINT32_MAX, UINT32_MAX, UINT32_MAX,
UINT32_MAX, UINT32_MAX, 512, 512
};
char output[24] = {};
EXPECT_FALSE(mesh::formatCompanionMemoryDiagnostics(
output, sizeof(output), diagnostics));
EXPECT_EQ('\0', output[sizeof(output) - 1]);
}
int main(int argc, char** argv) {
::testing::InitGoogleTest(&argc, argv);
return RUN_ALL_TESTS();
}
+45
View File
@@ -0,0 +1,45 @@
#!/usr/bin/env python3
from pathlib import Path
import re
root = Path(__file__).resolve().parents[1]
source = (root / "examples/companion_radio/MyMesh.cpp").read_text()
features = (root / "examples/companion_radio/CompanionFeatures.h").read_text()
local_start = source.index("bool MyMesh::handleLocalControlCommand(")
local_end = source.index("\n#if COMPANION_FEATURE_TEMP_RADIO\nvoid MyMesh::serviceTempRadio()", local_start)
local = source[local_start:local_end]
assert local.count('strcmp(command, "board") == 0') == 1
assert 'board.getManufacturerName()' in local
assert re.search(
r"#if COMPANION_FEATURE_MEMORY_DIAGNOSTICS\s+"
r'if \(strcmp\(command, "memory"\) == 0\).*?'
r"formatCompanionMemoryDiagnostics.*?#endif",
local,
re.DOTALL,
)
assert "offline_queue_len" in local
assert "getOfflineQueueCapacity()" in local
terminal_start = source.index("void MyMesh::handleTerminalCommand(")
terminal_end = source.index("\nvoid MyMesh::enterCLIRescue()", terminal_start)
terminal = source[terminal_start:terminal_end]
assert 'terminalOutput().print(" board\\r\\n")' in terminal
assert re.search(
r"#if COMPANION_FEATURE_MEMORY_DIAGNOSTICS\s+"
r'terminalOutput\(\)\.print\(" memory\\r\\n"\);\s+#endif',
terminal,
)
# This compile-time contract prevents either the implementation or help entry
# from leaking onto a non-ESP32 build merely because a recipe set the flag.
assert re.search(
r"#if COMPANION_FEATURE_MEMORY_DIAGNOSTICS && !defined\(ESP32_PLATFORM\)\s+"
r'#error "COMPANION_FEATURE_MEMORY_DIAGNOSTICS requires ESP32"',
features,
)
print("test_companion_terminal_profile: PASS")