From 5e6a74be50a9922266a3bd251f6d2621ebb4ddd5 Mon Sep 17 00:00:00 2001 From: mikecarper Date: Tue, 8 Sep 2026 19:56:12 -0700 Subject: [PATCH] Enable the shared terminal on WiFi-only Companions --- examples/companion_radio/CompanionFeatures.h | 9 +++++ examples/companion_radio/MyMesh.cpp | 38 ++++++++++---------- examples/companion_radio/MyMesh.h | 16 ++++----- examples/companion_radio/main.cpp | 8 +++++ scripts/check_firmware_ram.py | 2 +- test/test_firmware_ram.py | 5 ++- test/test_web_terminal_stream.py | 16 +++++++++ 7 files changed, 65 insertions(+), 29 deletions(-) diff --git a/examples/companion_radio/CompanionFeatures.h b/examples/companion_radio/CompanionFeatures.h index ea666827..224d3656 100644 --- a/examples/companion_radio/CompanionFeatures.h +++ b/examples/companion_radio/CompanionFeatures.h @@ -2,6 +2,15 @@ #include #include +// 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; diff --git a/examples/companion_radio/MyMesh.cpp b/examples/companion_radio/MyMesh.cpp index adb0d6c8..816539b3 100644 --- a/examples/companion_radio/MyMesh.cpp +++ b/examples/companion_radio/MyMesh.cpp @@ -42,7 +42,7 @@ #endif #include -#ifdef ENABLE_USB_INTERFACE +#if COMPANION_FEATURE_TEXT_TERMINAL #include #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(); diff --git a/examples/companion_radio/MyMesh.h b/examples/companion_radio/MyMesh.h index 07fb541c..bcda2437 100644 --- a/examples/companion_radio/MyMesh.h +++ b/examples/companion_radio/MyMesh.h @@ -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, diff --git a/examples/companion_radio/main.cpp b/examples/companion_radio/main.cpp index fce2bb9b..f20a4c5a 100644 --- a/examples/companion_radio/main.cpp +++ b/examples/companion_radio/main.cpp @@ -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() { diff --git a/scripts/check_firmware_ram.py b/scripts/check_firmware_ram.py index 861cef59..94df44c3 100644 --- a/scripts/check_firmware_ram.py +++ b/scripts/check_firmware_ram.py @@ -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: diff --git a/test/test_firmware_ram.py b/test/test_firmware_ram.py index 09cab9e7..653637d8 100644 --- a/test/test_firmware_ram.py +++ b/test/test_firmware_ram.py @@ -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"], diff --git a/test/test_web_terminal_stream.py b/test/test_web_terminal_stream.py index 97dafe0b..78030e04 100644 --- a/test/test_web_terminal_stream.py +++ b/test/test_web_terminal_stream.py @@ -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