Enable the shared terminal on WiFi-only Companions

This commit is contained in:
mikecarper committed 2026-09-08 19:56:12 -07:00
1 parent c946adadfe
commit 373c2daebe
7 files changed
+65 -29

No files matched your search

@@ -2,6 +2,15 @@
#include <helpers/CompanionJohnConfig.h>
#include <helpers/ota/OtaMemoryPolicy.h>
// The role terminal is also used by WebConfig on WiFi-only Companions. Its
// parser and reply tracking do not require a physical USB transport.
#if defined(ENABLE_USB_INTERFACE) \
|| (defined(ESP32_PLATFORM) && defined(WIFI_SSID) && !defined(WEBCONFIG_DISABLED))
#define COMPANION_FEATURE_TEXT_TERMINAL 1
#else
#define COMPANION_FEATURE_TEXT_TERMINAL 0
#endif
// Features are selected independently so adding a transport or changing an
// image-size profile cannot accidentally remove unrelated Companion behavior.
// Keep the legacy umbrella as a compatibility input for existing target files;
+19 -19
View File
@@ -42,7 +42,7 @@
#endif
#include <helpers/CLICommandUtils.h>
#ifdef ENABLE_USB_INTERFACE
#if COMPANION_FEATURE_TEXT_TERMINAL
#include <helpers/TracePathHelpers.h>
#endif
@@ -224,7 +224,7 @@ static bool save_filter(const ContactInfo& c);
// First 16 bytes of SHA-256("#testing"), encoded as base64.
#define TESTING_GROUP_PSK "zeXoLPUVZH3LVHp5pPBl0Q=="
#ifdef ENABLE_USB_INTERFACE
#if COMPANION_FEATURE_TEXT_TERMINAL
static const char* terminalContactTypeName(uint8_t type) {
if (type == ADV_TYPE_CHAT) return "Chat";
if (type == ADV_TYPE_REPEATER) return "Repeater";
@@ -708,7 +708,7 @@ void MyMesh::onDiscoveredContact(ContactInfo &contact, bool is_new, uint8_t path
#endif
}
#ifdef ENABLE_USB_INTERFACE
#if COMPANION_FEATURE_TEXT_TERMINAL
if (hasTerminalOutput() && _terminal_display.shouldShowAdvert()) {
Stream& output = terminalOutput();
output.printf("\r\nADVERT from -> %s\r\n", contact.name);
@@ -759,7 +759,7 @@ int MyMesh::getRecentlyHeard(AdvertPath dest[], int max_num) {
return max_num;
}
#ifdef ENABLE_USB_INTERFACE
#if COMPANION_FEATURE_TEXT_TERMINAL
void MyMesh::onContactVisit(const ContactInfo& contact) {
if (contact.type == ADV_TYPE_NONE) return;
@@ -779,7 +779,7 @@ void MyMesh::onContactPathUpdated(const ContactInfo &contact) {
scheduleContactWrite(contact);
#ifdef ENABLE_USB_INTERFACE
#if COMPANION_FEATURE_TEXT_TERMINAL
if (hasTerminalOutput()) {
terminalOutput().printf("\r\nPATH updated -> %s\r\n> ", contact.name);
}
@@ -815,7 +815,7 @@ void MyMesh::expireExpectedAcks() {
if (entry.expires_at == now || millisHasNowPassed(entry.expires_at)) {
if (!hasActiveRetries(entry.retry_key)) {
#ifdef ENABLE_USB_INTERFACE
#if COMPANION_FEATURE_TEXT_TERMINAL
if (entry.terminal_origin && hasTerminalOutput()) {
terminalOutput().print("\r\n ERROR: timed out, no ACK.\r\n> ");
}
@@ -872,7 +872,7 @@ ContactInfo* MyMesh::processAck(const uint8_t *data) {
out_frame, 9);
}
#ifdef ENABLE_USB_INTERFACE
#if COMPANION_FEATURE_TEXT_TERMINAL
if (expected_ack_table[i].terminal_origin && hasTerminalOutput()) {
terminalOutput().printf("\r\n Got ACK! (round trip: %lu ms)\r\n> ",
(unsigned long)trip_time);
@@ -927,7 +927,7 @@ void MyMesh::queueMessage(const ContactInfo &from, uint8_t txt_type, mesh::Packe
_serial->writeFrame(frame, 1);
}
#ifdef ENABLE_USB_INTERFACE
#if COMPANION_FEATURE_TEXT_TERMINAL
if (hasTerminalOutput()) {
Stream& output = terminalOutput();
const char* kind = txt_type == TXT_TYPE_CLI_DATA ? "CLI" : "MSG";
@@ -1058,7 +1058,7 @@ void MyMesh::onCommandDataRecv(const ContactInfo &from, mesh::Packet *pkt, uint3
markConnectionActive(from); // in case this is from a server, and we have a connection
bool terminal_command_reply = false;
uint32_t terminal_command_elapsed_millis = 0;
#ifdef ENABLE_USB_INTERFACE
#if COMPANION_FEATURE_TEXT_TERMINAL
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;
@@ -1127,7 +1127,7 @@ void MyMesh::onChannelMessageRecv(const mesh::GroupChannel &channel, mesh::Packe
#endif
}
#ifdef ENABLE_USB_INTERFACE
#if COMPANION_FEATURE_TEXT_TERMINAL
if (hasTerminalOutput()
&& _terminal_display.shouldShowChannel(is_emergency_channel)) {
ChannelDetails details;
@@ -1269,18 +1269,18 @@ void MyMesh::onContactResponse(const ContactInfo &contact, const uint8_t *data,
if (pending_login && memcmp(&pending_login, contact.id.pub_key, 4) == 0) { // check for login response
// yes, is response to pending sendLogin()
#ifdef ENABLE_USB_INTERFACE
#if COMPANION_FEATURE_TEXT_TERMINAL
const bool terminal_login_response = _terminal_login_pending
&& memcmp(_terminal_login_key, contact.id.pub_key,
sizeof(_terminal_login_key)) == 0;
#endif
int i = 0;
#ifdef ENABLE_USB_INTERFACE
#if COMPANION_FEATURE_TEXT_TERMINAL
bool login_success = false;
bool modern_login = false;
#endif
if (len >= 6 && memcmp(&data[4], "OK", 2) == 0) { // legacy Repeater login OK response
#ifdef ENABLE_USB_INTERFACE
#if COMPANION_FEATURE_TEXT_TERMINAL
login_success = true;
#endif
out_frame[i++] = PUSH_CODE_LOGIN_SUCCESS;
@@ -1288,7 +1288,7 @@ void MyMesh::onContactResponse(const ContactInfo &contact, const uint8_t *data,
memcpy(&out_frame[i], contact.id.pub_key, 6);
i += 6; // pub_key_prefix
} else if (len >= 13 && data[4] == RESP_SERVER_LOGIN_OK) { // new login response
#ifdef ENABLE_USB_INTERFACE
#if COMPANION_FEATURE_TEXT_TERMINAL
login_success = true;
modern_login = true;
#endif
@@ -1314,7 +1314,7 @@ void MyMesh::onContactResponse(const ContactInfo &contact, const uint8_t *data,
// no Binary route. Binary logins retain their exact requester even if a
// different transport has issued a command in the meantime.
writePendingSerialFrame(out_frame, i);
#ifdef ENABLE_USB_INTERFACE
#if COMPANION_FEATURE_TEXT_TERMINAL
if (terminal_login_response) {
if (hasTerminalOutput()) {
Stream& output = terminalOutput();
@@ -1474,7 +1474,7 @@ void MyMesh::onTraceRecv(mesh::Packet *packet, uint32_t tag, uint32_t auth_code,
i += path_len >> path_sz;
out_frame[i++] = (int8_t)(packet->getSNR() * 4); // extra/final SNR (to this node)
#ifdef ENABLE_USB_INTERFACE
#if COMPANION_FEATURE_TEXT_TERMINAL
if (hasTerminalOutput() && _terminal_trace_pending
&& tag == _terminal_trace_tag && auth_code == _terminal_trace_auth) {
Stream& output = terminalOutput();
@@ -1539,7 +1539,7 @@ MyMesh::MyMesh(mesh::Radio &radio, mesh::RNG &rng, mesh::RTCClock &rtc, SimpleMe
_iter_total_count = 0;
_iter_table_revision = 0;
_cli_rescue = false;
#ifdef ENABLE_USB_INTERFACE
#if COMPANION_FEATURE_TEXT_TERMINAL
_terminal_mode = false;
_terminal_output = NULL;
_terminal_recipient_set = false;
@@ -6776,7 +6776,7 @@ void MyMesh::appendRxPowerSavingAdjustmentNote(char* reply, size_t reply_size,
(unsigned)_prefs.rx_ps_level);
}
#ifdef ENABLE_USB_INTERFACE
#if COMPANION_FEATURE_TEXT_TERMINAL
Stream& MyMesh::terminalOutput() {
return _terminal_output != NULL ? *_terminal_output : Serial;
}
@@ -8468,7 +8468,7 @@ void MyMesh::loop() {
#ifdef COMPANION_MESH_CLOCK_SYNC
_clock_sync.loop();
#endif
#ifdef ENABLE_USB_INTERFACE
#if COMPANION_FEATURE_TEXT_TERMINAL
serviceTerminalLogin();
serviceTerminalCommand();
serviceTerminalTrace();
+8 -8
View File
@@ -122,7 +122,7 @@ struct AdvertPath {
};
class MyMesh : public BaseChatMesh, public DataStoreHost, public UIShutdownGuard
#ifdef ENABLE_USB_INTERFACE
#if COMPANION_FEATURE_TEXT_TERMINAL
, public ContactVisitor
#endif
#ifdef WITH_WEBCONFIG
@@ -187,7 +187,7 @@ public:
void execCommand(char* cmd, char* reply) override;
bool supportsCliTerminal() const override { return true; }
void execAdminCommand(char* cmd, char* reply) override;
#ifdef ENABLE_USB_INTERFACE
#if COMPANION_FEATURE_TEXT_TERMINAL
bool supportsStreamTerminal() const override { return true; }
bool beginStreamTerminal(Stream& output) override;
bool ownsStreamTerminal(const Stream& output) const override {
@@ -207,7 +207,7 @@ public:
bool advert();
void enterCLIRescue();
#ifdef ENABLE_USB_INTERFACE
#if COMPANION_FEATURE_TEXT_TERMINAL
void enterTerminalMode();
void exitTerminalMode();
// Clear state owned by the current text-terminal host without changing the
@@ -281,7 +281,7 @@ protected:
bool onContactPathRecv(ContactInfo& from, uint8_t* in_path, uint8_t in_path_len, uint8_t* out_path, uint8_t out_path_len, uint8_t extra_type, uint8_t* extra, uint8_t extra_len) override;
void onDiscoveredContact(ContactInfo &contact, bool is_new, uint8_t path_len, const uint8_t* path) override;
void onContactPathUpdated(const ContactInfo &contact) override;
#ifdef ENABLE_USB_INTERFACE
#if COMPANION_FEATURE_TEXT_TERMINAL
void onContactVisit(const ContactInfo& contact) override;
#endif
ContactInfo* processAck(const uint8_t *data) override;
@@ -403,7 +403,7 @@ private:
void formatWiFiPowerSaving(char* reply, size_t reply_size) const;
void syncWiFiPowerSaving();
#endif
#ifdef ENABLE_USB_INTERFACE
#if COMPANION_FEATURE_TEXT_TERMINAL
Stream& terminalOutput();
bool hasTerminalOutput() const { return _terminal_output != NULL; }
void printTerminalBanner(bool show_binary_stop);
@@ -515,7 +515,7 @@ private:
bool _iter_start_pending;
bool _iter_contact_pending;
bool _cli_rescue;
#ifdef ENABLE_USB_INTERFACE
#if COMPANION_FEATURE_TEXT_TERMINAL
bool _terminal_mode;
Stream* _terminal_output;
mesh::TerminalDisplayFilter _terminal_display;
@@ -620,7 +620,7 @@ private:
uint8_t text_fingerprint[MAX_HASH_SIZE];
uint8_t retry_key[MAX_HASH_SIZE];
BaseSerialInterface* reply_route;
#ifdef ENABLE_USB_INTERFACE
#if COMPANION_FEATURE_TEXT_TERMINAL
bool terminal_origin;
#endif
};
@@ -635,7 +635,7 @@ private:
void expireExpectedAcks();
AckTableEntry* findPendingTextMessage(
const uint8_t text_fingerprint[MAX_HASH_SIZE], uint32_t message_timestamp);
#ifdef ENABLE_USB_INTERFACE
#if COMPANION_FEATURE_TEXT_TERMINAL
void rememberTerminalAck(ContactInfo& recipient, const char* text,
uint32_t message_timestamp, uint32_t expected_ack,
uint32_t est_timeout,
+8
View File
@@ -1086,6 +1086,14 @@ void MyMesh::endStreamTerminal(Stream& output) {
enterUsbTerminalMode();
}
}
#elif defined(WITH_WEBCONFIG)
bool MyMesh::beginStreamTerminal(Stream& output) {
return enterNetworkTerminalMode(output);
}
void MyMesh::endStreamTerminal(Stream& output) {
exitNetworkTerminalMode(output);
}
#endif
void halt() {
+1 -1
View File
@@ -75,7 +75,7 @@ def requirements(platform, defines, target):
parts["mqtt_connections_buffers"] = 24576
if "ENABLE_OTA" in defines:
parts["ota_source_scratch"] = 8192
if (companion and "ENABLE_USB_INTERFACE" in defines and "WIFI_SSID" in defines
if (companion and "WIFI_SSID" in defines
and "WEBCONFIG_DISABLED" not in defines):
parts["browser_terminal_session"] = 2048
if "BOARD_HAS_PSRAM" not in defines:
+4 -1
View File
@@ -73,6 +73,9 @@ class FirmwareRamTest(unittest.TestCase):
psram = ram.requirements("ESP32_PLATFORM", {**defines, "BOARD_HAS_PSRAM": 1}, "v4_companion")
self.assertEqual(plain["required_heap_bytes"] - base["required_heap_bytes"], 6144)
self.assertEqual(psram["required_heap_bytes"] - base["required_heap_bytes"], 2048)
wifi_only = dict(defines)
del wifi_only["ENABLE_USB_INTERFACE"]
self.assertEqual(ram.requirements("ESP32_PLATFORM", wifi_only, "v4_companion"), plain)
repeater = ram.requirements("ESP32_PLATFORM", defines, "v4_repeater")
self.assertNotIn("browser_terminal_session", repeater["components"])
@@ -237,7 +240,7 @@ int main() {
base = ram.requirements("ESP32_PLATFORM", {}, "companion")["required_heap_bytes"]
flags = {"BLE_PIN_CODE": 1, "WIFI_SSID": "test", "WITH_MQTT_BRIDGE": 1}
both = ram.requirements("ESP32_PLATFORM", flags, "companion")["required_heap_bytes"]
self.assertEqual(both - base, 32768 + 49152 + 24576)
self.assertEqual(both - base, 32768 + 49152 + 24576 + 6144)
flags["COMPANION_EXCLUSIVE_WIFI_BLE"] = 1
self.assertEqual(ram.requirements("ESP32_PLATFORM", flags, "companion")["required_heap_bytes"], both - 32768)
self.assertGreater(ram.requirements("NRF52_PLATFORM", {"DISPLAY_CLASS": "ST7735Display"}, "companion")["required_heap_bytes"],
+16
View File
@@ -9,6 +9,22 @@ ROOT = Path(__file__).resolve().parents[1]
class WebTerminalStreamTest(unittest.TestCase):
def test_terminal_feature_compiles_with_wifi_without_usb(self):
for defines, enabled in [
(["ESP32_PLATFORM", 'WIFI_SSID="ssid"'], True),
(["ESP32_PLATFORM", 'WIFI_SSID="ssid"', "WEBCONFIG_DISABLED"], False),
(["NRF52_PLATFORM", "ENABLE_USB_INTERFACE"], True),
(["ESP32_PLATFORM"], False),
]:
with self.subTest(defines=defines):
result = subprocess.run([
"c++", "-E", "-dM", "-x", "c++", "-", "-I" + str(ROOT / "src"),
"-include", str(ROOT / "examples/companion_radio/CompanionFeatures.h"),
*["-D" + value for value in defines],
], input="", text=True, capture_output=True, check=True)
self.assertIn("#define COMPANION_FEATURE_TEXT_TERMINAL " + str(int(enabled)),
result.stdout)
def test_growth_drain_failure_and_release(self):
headers = {
"Arduino.h": """#pragma once