From f6c0a2f0218d0976dfbaac9933de817cc7359d59 Mon Sep 17 00:00:00 2001 From: Jakub <106778416+MarekZegare4@users.noreply.github.com> Date: Thu, 15 Oct 2026 14:04:58 +0200 Subject: [PATCH] refactor(companion): the app's links through upstream's MultiSerialInterface - Bluetooth + USB (+ WiFi/Ethernet/UART per build) in one interface_manager, as upstream; frames go to every enabled link, USB is read while BT is connected. DUAL_SERIAL now means ENABLE_USB_INTERFACE. - WiFi interface with upstream's wifi_* prefs and set/get wifi.* CLI. - isClientConnected moved into ArduinoSerialInterface (DTR on nRF52 and ESP32 native CDC). - UI uses isBluetoothEnabled/enableBluetooth/disableBluetooth; turning BT off leaves USB and the other links on. - DualSerialInterface removed. - Sim build gains DynamicConfigSerializer.cpp (broken since bd64c203). Co-Authored-By: Claude Opus 5.5 --- examples/companion_radio/AbstractUITask.h | 15 +- examples/companion_radio/MyMesh.cpp | 5 +- examples/companion_radio/MyMeshCli.h | 43 +++ examples/companion_radio/NodePrefs.h | 31 +- examples/companion_radio/main.cpp | 307 +++++++++++------- examples/companion_radio/ui-lvgl/QuickPanel.h | 4 +- .../companion_radio/ui-lvgl/QuickScreen.h | 6 +- examples/companion_radio/ui-lvgl/UITask.cpp | 2 +- examples/companion_radio/ui-lvgl/UITask.h | 4 +- examples/companion_radio/ui-new/HomeScreen.h | 6 +- examples/companion_radio/ui-new/UITask.h | 4 +- examples/companion_radio/ui-orig/UITask.h | 2 +- examples/companion_radio/ui-tiny/UITask.cpp | 10 +- examples/companion_radio/ui-tiny/UITask.h | 4 +- src/helpers/ArduinoSerialInterface.cpp | 11 + src/helpers/ArduinoSerialInterface.h | 3 + src/helpers/BaseSerialInterface.h | 1 + src/helpers/MultiSerialInterface.h | 32 ++ src/helpers/esp32/DualSerialInterface.h | 91 ------ src/helpers/nrf52/DualSerialInterface.h | 72 ---- variants/sim/build_wasm.sh | 1 + 21 files changed, 345 insertions(+), 309 deletions(-) delete mode 100644 src/helpers/esp32/DualSerialInterface.h delete mode 100644 src/helpers/nrf52/DualSerialInterface.h diff --git a/examples/companion_radio/AbstractUITask.h b/examples/companion_radio/AbstractUITask.h index 486b84e9..3e687a1f 100644 --- a/examples/companion_radio/AbstractUITask.h +++ b/examples/companion_radio/AbstractUITask.h @@ -4,7 +4,7 @@ #include #include #include -#include +#include #include // MAX_TEXT_LEN #include // TXT_TYPE_* #include @@ -32,10 +32,10 @@ enum class UIEventType { class UITaskBase { protected: mesh::MainBoard* _board; - BaseSerialInterface* _serial; + MultiSerialInterface* _serial; // the interface manager (main.cpp) bool _connected; - UITaskBase(mesh::MainBoard* board, BaseSerialInterface* serial) : _board(board), _serial(serial) { + UITaskBase(mesh::MainBoard* board, MultiSerialInterface* serial) : _board(board), _serial(serial) { _connected = false; } @@ -94,9 +94,10 @@ public: // open USB-CDC port). For app-connected behaviour like Auto buzzer mute. bool isClientConnected() const { return _serial->isClientConnected(); } uint16_t getBattMilliVolts() const { return _board->getBattMilliVolts(); } - bool isSerialEnabled() const { return _serial->isEnabled(); } - void enableSerial() { _serial->enable(); the_mesh.rememberBle(true); } - void disableSerial() { _serial->disable(); the_mesh.rememberBle(false); } + // Bluetooth alone (USB and the rest stay on), remembered over a restart. + bool isBluetoothEnabled() const { return _serial->isBluetoothEnabled(); } + void enableBluetooth() { _serial->enableBluetooth(); the_mesh.rememberBle(true); } + void disableBluetooth() { _serial->disableBluetooth(); the_mesh.rememberBle(false); } virtual void notify(UIEventType t = UIEventType::none) = 0; // Single choke point for every controlled power-down (low-battery auto-off, // long-press power-off, and every board.reboot() caller too): flush @@ -122,7 +123,7 @@ class AbstractUITask : public MyMesh::Listener, public UITaskBase { protected: int _queue_len = 0; // last onQueueSizeChanged() -- newMsg()'s msgcount - AbstractUITask(mesh::MainBoard* board, BaseSerialInterface* serial) : UITaskBase(board, serial) { } + AbstractUITask(mesh::MainBoard* board, MultiSerialInterface* serial) : UITaskBase(board, serial) { } public: // An end-to-end ACK (CRC) arrived for one of our sent messages — drives the diff --git a/examples/companion_radio/MyMesh.cpp b/examples/companion_radio/MyMesh.cpp index 58b73ba3..0101805e 100644 --- a/examples/companion_radio/MyMesh.cpp +++ b/examples/companion_radio/MyMesh.cpp @@ -1372,6 +1372,9 @@ MyMesh::MyMesh(mesh::Radio &radio, mesh::RNG &rng, mesh::RTCClock &rtc, SimpleMe _prefs.clearFields(); memset(_prefs.ch_scope_idx, NodePrefs::CH_SCOPE_DEFAULT, sizeof(_prefs.ch_scope_idx)); _prefs.airtime_factor = 1.0; +#ifdef ENABLE_WIFI_INTERFACE + _prefs.wifi_enabled = 1; // on, so a WiFi-only build works; it doesn't start without an SSID +#endif _prefs.tx_delay_factor = 0.5f; _prefs.direct_tx_delay_factor = 0.2f; strcpy(_prefs.node_name, "NONAME"); @@ -1570,7 +1573,7 @@ bool MyMesh::isValidClientRepeatFreq(uint32_t f) const { void MyMesh::startInterface(BaseSerialInterface &serial) { _serial = &serial; - if (!_prefs.ble_off) serial.enable(); + serial.enable(); // main.cpp then turns Bluetooth off if the user left it off (ble_off) } static bool isAllZero(const uint8_t* buf, size_t n) { diff --git a/examples/companion_radio/MyMeshCli.h b/examples/companion_radio/MyMeshCli.h index 85aa15ea..b9a2dae7 100644 --- a/examples/companion_radio/MyMeshCli.h +++ b/examples/companion_radio/MyMeshCli.h @@ -76,6 +76,49 @@ bool MyMesh::handleCliCommand(const char* command, char* reply, int n) { else { _prefs.tz_offset_hours = (int8_t)tz; savePrefs(); snprintf(reply, n, "OK"); } return true; } +#ifdef ENABLE_WIFI_INTERFACE + // The app over WiFi (upstream's commands; applied at the next start). + if (memcmp(command, "set wifi.ssid ", 14) == 0) { + StrHelper::strncpy(_prefs.wifi_ssid, &command[14], sizeof(_prefs.wifi_ssid)); + savePrefs(); + snprintf(reply, n, "> wifi.ssid is now %s (set wifi.pwd too, then reboot)", _prefs.wifi_ssid); + return true; + } + if (memcmp(command, "set wifi.pwd ", 13) == 0) { + StrHelper::strncpy(_prefs.wifi_pwd, &command[13], sizeof(_prefs.wifi_pwd)); + savePrefs(); + snprintf(reply, n, "> wifi.pwd updated (reboot to apply)"); + return true; + } + if (strcmp(command, "get wifi.pwd") == 0) { snprintf(reply, n, "> %s", _prefs.wifi_pwd); return true; } + if (strcmp(command, "set wifi.clear") == 0) { + _prefs.wifi_ssid[0] = 0; + _prefs.wifi_pwd[0] = 0; + savePrefs(); + snprintf(reply, n, "> wifi config cleared (reboot to apply)"); + return true; + } + if (strcmp(command, "get wifi.ssid") == 0) { + snprintf(reply, n, "> %s", _prefs.getWifiSSID()[0] ? _prefs.getWifiSSID() : "(not set)"); + return true; + } + if (memcmp(command, "set wifi.enabled ", 17) == 0) { + _prefs.wifi_enabled = atoi(&command[17]) ? 1 : 0; + savePrefs(); + snprintf(reply, n, "> wifi.enabled is now %d (reboot to apply)", _prefs.wifi_enabled); + return true; + } + if (strcmp(command, "get wifi.enabled") == 0) { snprintf(reply, n, "> %d", _prefs.wifi_enabled); return true; } + if (strcmp(command, "get wifi.status") == 0) { + snprintf(reply, n, WiFi.status() == WL_CONNECTED ? "> connected" : "> disconnected"); + return true; + } + if (strcmp(command, "get wifi.ip") == 0) { + if (WiFi.status() == WL_CONNECTED) snprintf(reply, n, "> %s", WiFi.localIP().toString().c_str()); + else snprintf(reply, n, "> (not connected)"); + return true; + } +#endif // The position adverts carry while the GPS is off (Settings > Privacy > Position). if (strcmp(command, "get lat") == 0) { snprintf(reply, n, "> %.6f", sensors.node_lat); return true; } if (strcmp(command, "get lon") == 0) { snprintf(reply, n, "> %.6f", sensors.node_lon); return true; } diff --git a/examples/companion_radio/NodePrefs.h b/examples/companion_radio/NodePrefs.h index ef4bfd19..f0625b1a 100644 --- a/examples/companion_radio/NodePrefs.h +++ b/examples/companion_radio/NodePrefs.h @@ -176,8 +176,14 @@ struct NodePrefsFields { // ── Connectivity (BLE) ──────────────────────────────────────────────── uint32_t ble_pin; // 1 = the user switched Bluetooth off: it stays off after a reboot or - // hibernate (MyMesh::startInterface). USB keeps working either way. + // hibernate (main.cpp). USB keeps working either way. uint8_t ble_off; +#ifdef ENABLE_WIFI_INTERFACE + // The app over WiFi (TCP), upstream's: an empty SSID uses the build's WIFI_SSID. + char wifi_ssid[33]; + char wifi_pwd[64]; + uint8_t wifi_enabled; +#endif // ── Power & battery ─────────────────────────────────────────────────── uint16_t low_batt_mv; // auto-shutdown threshold: 0=disabled, 3000-3500 mV @@ -893,6 +899,13 @@ class NodePrefs : public NodePrefsFields, public ConfigSerializer { } } void lockSolo(Section& s) { s.hex("hash", lock_screen_password); s.hex("salt", lock_screen_password_salt); } +#ifdef ENABLE_WIFI_INTERFACE + void wifiKeys(Section& s) { + s.def("ssid", wifi_ssid, sizeof(wifi_ssid)); + s.def("pwd", wifi_pwd, sizeof(wifi_pwd)); + s.def("enabled", wifi_enabled); + } +#endif void soloKeys(Section& s) { s.sub("radio", radio_solo); s.sub("repeater", repeater_solo); s.sub("power", power_solo); s.sub("display", display_solo); s.sub("clock", clock_solo); s.sub("sound", sound_solo); @@ -902,6 +915,9 @@ class NodePrefs : public NodePrefsFields, public ConfigSerializer { } Section radio, gps, repeat, comp; +#ifdef ENABLE_WIFI_INTERFACE + Section wifi; +#endif Section solo, radio_solo, preset[USER_RADIO_PRESET_MAX], repeater_solo, power_solo, display_solo, clock_solo, sound_solo, scope_solo, bot_solo, gps_solo, share_solo, locator_solo, lists_solo, gpio_solo, msgs_solo, lock_solo; @@ -918,6 +934,9 @@ protected: def("repeat", repeat); def("comp", comp); def("custom", custom); +#ifdef ENABLE_WIFI_INTERFACE + def("wifi", wifi); +#endif def("solo", solo); } @@ -929,6 +948,9 @@ public: NodePrefs() : radio(this, &NodePrefs::radioKeys), gps(this, &NodePrefs::gpsKeys), repeat(this, &NodePrefs::repeatKeys), comp(this, &NodePrefs::compKeys), +#ifdef ENABLE_WIFI_INTERFACE + wifi(this, &NodePrefs::wifiKeys), +#endif solo(this, &NodePrefs::soloKeys), radio_solo(this, &NodePrefs::radioSolo), preset{ {this, &NodePrefs::presetKeys}, {this, &NodePrefs::presetKeys}, {this, &NodePrefs::presetKeys}, {this, &NodePrefs::presetKeys} }, @@ -951,6 +973,13 @@ public: // The board's settings (upstream's getCustom()), for Board::attachDynamicPrefs(). KeyValueStore* getCustom() { return &custom; } +#ifdef ENABLE_WIFI_INTERFACE + #ifndef WIFI_SSID + #define WIFI_SSID "" + #endif + // use ssid from prefs, or fallback to ssid from build flags + const char* getWifiSSID() const { return wifi_ssid[0] ? wifi_ssid : WIFI_SSID; } +#endif bool isRepeatEn() const { return client_repeat != 0; } void setRepeatEn(bool en) { client_repeat = en ? 1 : 0; } diff --git a/examples/companion_radio/main.cpp b/examples/companion_radio/main.cpp index 8d354a21..2e642b12 100644 --- a/examples/companion_radio/main.cpp +++ b/examples/companion_radio/main.cpp @@ -65,72 +65,94 @@ static uint32_t _atoi(const char* sp) { DataStore store(sim_fs, rtc_clock); #endif -#ifdef ESP32 - #ifdef WIFI_SSID - #include - SerialWifiInterface serial_interface; - #ifndef TCP_PORT - #define TCP_PORT 5000 - #endif - #elif defined(DUAL_SERIAL) - #include - DualSerialInterface serial_interface; - #elif defined(BLE_PIN_CODE) +// The companion app's links (upstream's interface manager): Bluetooth, USB, +// WiFi, Ethernet, a UART, whichever the build has, all at once. DUAL_SERIAL +// (Solo's builds) is Bluetooth and USB. +#if defined(DUAL_SERIAL) && !defined(ENABLE_USB_INTERFACE) + #define ENABLE_USB_INTERFACE +#endif +#if !defined(BLE_PIN_CODE) && !defined(ENABLE_WIFI_INTERFACE) && !defined(ETHERNET_ENABLED) \ + && !defined(SERIAL_RX) && !defined(ENABLE_USB_INTERFACE) && !defined(SIM_PLATFORM) + #define ENABLE_USB_INTERFACE // no other link: the app on USB, as before +#endif +#include +MultiSerialInterface interface_manager; + +// include bluetooth interface +#if defined(BLE_PIN_CODE) + #ifdef ESP32 #include - SerialBLEInterface serial_interface; - #elif defined(SERIAL_RX) - #include - ArduinoSerialInterface serial_interface; - HardwareSerial companion_serial(1); - #else - #include - ArduinoSerialInterface serial_interface; - #endif -#elif defined(RP2040_PLATFORM) - //#ifdef WIFI_SSID - // #include - // SerialWifiInterface serial_interface; - // #ifndef TCP_PORT - // #define TCP_PORT 5000 - // #endif - // #elif defined(BLE_PIN_CODE) - // #include - // SerialBLEInterface serial_interface; - #if defined(SERIAL_RX) - #include - ArduinoSerialInterface serial_interface; - HardwareSerial companion_serial(1); - #else - #include - ArduinoSerialInterface serial_interface; - #endif -#elif defined(NRF52_PLATFORM) - #ifdef DUAL_SERIAL - #include - DualSerialInterface serial_interface; - #elif defined(BLE_PIN_CODE) + SerialBLEInterface bluetooth_interface; + #elif defined(NRF52_PLATFORM) #include - SerialBLEInterface serial_interface; + SerialBLEInterface bluetooth_interface; + #elif defined(RP2040_PLATFORM) + #include + SerialBLEInterface bluetooth_interface; #else - #include - ArduinoSerialInterface serial_interface; + #error "SerialBLEInterface is not defined for this platform" #endif -#elif defined(STM32_PLATFORM) +#endif + +// include wifi interface +#ifdef ENABLE_WIFI_INTERFACE + #ifndef WIFI_SSID + #define WIFI_SSID "" + #endif + #ifndef WIFI_PWD + #define WIFI_PWD "" + #endif + #ifndef TCP_PORT + #define TCP_PORT 5000 + #endif + #ifndef WIFI_RETRY_INTERVAL + #if defined(RP2040_PLATFORM) + #define WIFI_RETRY_INTERVAL 30000 // each attempt blocks loop(), so retry less often + #else + #define WIFI_RETRY_INTERVAL 10000 // millis between reconnect attempts + #endif + #endif + #ifndef WIFI_RETRY_TIMEOUT + #define WIFI_RETRY_TIMEOUT 5000 // RP2040: cap on how long one join may block loop() + #endif + #if defined(ESP32) || defined(RP2040_PLATFORM) + #include + SerialWifiInterface wifi_interface; + #else + #error "SerialWifiInterface is not defined for this platform" + #endif +#endif + +// include usb interface +#if defined(ENABLE_USB_INTERFACE) #include - ArduinoSerialInterface serial_interface; -#elif defined(SIM_PLATFORM) - // No real BLE/USB companion-app transport in Phase 1 -- always reports - // "not connected". See variants/sim/SimSerialInterface.h. + ArduinoSerialInterface usb_serial_interface; +#endif + +// include ethernet interface +#if defined(ETHERNET_ENABLED) + #include + ETHERNET_CLASS ethernet_interface; +#endif + +// include hardware serial interface +#if defined(SERIAL_RX) + #include + ArduinoSerialInterface hardware_serial_interface; + HardwareSerial companion_serial(1); +#endif + +#if defined(SIM_PLATFORM) + // No real BLE/USB companion-app transport -- always reports "not + // connected". See variants/sim/SimSerialInterface.h. #include - SimSerialInterface serial_interface; -#else - #error "need to define a serial interface" + SimSerialInterface sim_serial_interface; #endif /* GLOBAL OBJECTS */ #ifdef DISPLAY_CLASS #include "UITask.h" - UITask ui_task(&board, &serial_interface); + UITask ui_task(&board, &interface_manager); #endif StdRNG fast_rng; @@ -181,9 +203,13 @@ static void resolveBLEPin(bool has_display) { } /* WIFI RECONNECT TRACKERS */ -#if defined(ESP32) && defined(WIFI_SSID) +#ifdef ENABLE_WIFI_INTERFACE bool wifi_needs_reconnect = false; unsigned long last_wifi_reconnect_attempt = 0; + char wifi_ssid[33] = WIFI_SSID; // replaced by stored prefs at boot, if set + char wifi_pwd[64] = WIFI_PWD; + bool wifi_was_connected = false; + bool wifi_enabled = false; // set at boot from prefs; false also when the effective SSID is blank #endif void setup() { @@ -239,14 +265,6 @@ void setup() { #endif ); -#ifdef DUAL_SERIAL - serial_interface.begin(BLE_NAME_PREFIX, the_mesh.getNodePrefs()->node_name, the_mesh.getBLEPin(), Serial); -#elif defined(BLE_PIN_CODE) - serial_interface.begin(BLE_NAME_PREFIX, the_mesh.getNodePrefs()->node_name, the_mesh.getBLEPin()); -#else - serial_interface.begin(Serial); -#endif - the_mesh.startInterface(serial_interface); #elif defined(RP2040_PLATFORM) LittleFS.begin(); store.begin(); @@ -259,21 +277,6 @@ void setup() { #endif ); - //#ifdef WIFI_SSID - // WiFi.begin(WIFI_SSID, WIFI_PWD); - // serial_interface.begin(TCP_PORT); - // #elif defined(BLE_PIN_CODE) - // char dev_name[32+16]; - // sprintf(dev_name, "%s%s", BLE_NAME_PREFIX, the_mesh.getNodeName()); - // serial_interface.begin(dev_name, the_mesh.getBLEPin()); - #if defined(SERIAL_RX) - companion_serial.setPins(SERIAL_RX, SERIAL_TX); - companion_serial.begin(115200); - serial_interface.begin(companion_serial); - #else - serial_interface.begin(Serial); - #endif - the_mesh.startInterface(serial_interface); #elif defined(ESP32) ESP32_FS.begin(true); store.begin(); @@ -286,34 +289,6 @@ void setup() { #endif ); -#ifdef WIFI_SSID - board.setInhibitSleep(true); // prevent sleep when WiFi is active - WiFi.setAutoReconnect(true); - - WiFi.onEvent([](WiFiEvent_t event, WiFiEventInfo_t info){ - if (event == ARDUINO_EVENT_WIFI_STA_DISCONNECTED) { - WIFI_DEBUG_PRINTLN("WiFi disconnected. Flagging for reconnect..."); - wifi_needs_reconnect = true; - } else if (event == ARDUINO_EVENT_WIFI_STA_GOT_IP) { - WIFI_DEBUG_PRINTLN("WiFi connected successfully!"); - wifi_needs_reconnect = false; - } - }); - - WiFi.begin(WIFI_SSID, WIFI_PWD); - serial_interface.begin(TCP_PORT); -#elif defined(DUAL_SERIAL) - serial_interface.begin(BLE_NAME_PREFIX, the_mesh.getNodePrefs()->node_name, the_mesh.getBLEPin(), Serial); -#elif defined(BLE_PIN_CODE) - serial_interface.begin(BLE_NAME_PREFIX, the_mesh.getNodePrefs()->node_name, the_mesh.getBLEPin()); -#elif defined(SERIAL_RX) - companion_serial.setPins(SERIAL_RX, SERIAL_TX); - companion_serial.begin(115200); - serial_interface.begin(companion_serial); -#else - serial_interface.begin(Serial); -#endif - the_mesh.startInterface(serial_interface); #elif defined(SIM_PLATFORM) // sim_fs already exists/mkdir'd itself in its constructor above. store.begin(); @@ -325,12 +300,90 @@ void setup() { false #endif ); - serial_interface.begin(); - the_mesh.startInterface(serial_interface); #else #error "need to define filesystem" #endif +// add bluetooth interface +#if defined(BLE_PIN_CODE) + bluetooth_interface.begin(BLE_NAME_PREFIX, the_mesh.getNodePrefs()->node_name, the_mesh.getBLEPin()); + interface_manager.addInterface(InterfaceType::Bluetooth, &bluetooth_interface); +#endif + +// add wifi interface +#ifdef ENABLE_WIFI_INTERFACE + // use wifi ssid and password from prefs if ssid is not empty, otherwise use the build flag defaults + if (the_mesh.getNodePrefs()->wifi_ssid[0]) { + strcpy(wifi_ssid, the_mesh.getNodePrefs()->wifi_ssid); + strcpy(wifi_pwd, the_mesh.getNodePrefs()->wifi_pwd); + } + // only start wifi if enabled and ssid is not empty + wifi_enabled = the_mesh.getNodePrefs()->wifi_enabled; + if (wifi_enabled && wifi_ssid[0]) { +#if defined(ESP32) + board.setInhibitSleep(true); // prevent sleep when WiFi is active + WiFi.setAutoReconnect(true); + + WiFi.onEvent([](WiFiEvent_t event, WiFiEventInfo_t info){ + if (event == ARDUINO_EVENT_WIFI_STA_DISCONNECTED) { + WIFI_DEBUG_PRINTLN("WiFi disconnected (reason=%s). Flagging for reconnect...", + WiFi.disconnectReasonName((wifi_err_reason_t)info.wifi_sta_disconnected.reason)); + wifi_needs_reconnect = true; + } else if (event == ARDUINO_EVENT_WIFI_STA_GOT_IP) { + WIFI_DEBUG_PRINTLN("WiFi connected successfully!"); + wifi_needs_reconnect = false; + } + }); +#endif + + WIFI_DEBUG_PRINTLN("connecting to %s", wifi_ssid); + +#if defined(RP2040_PLATFORM) + // the join itself blocks inside the core (CYW43::begin busy-waits for the + // association), so every attempt stalls the mesh loop. beginNoBlock() only skips the + // extra DHCP wait. Give the first connect a full window, then bound the retries below. + WiFi.beginNoBlock(wifi_ssid, wifi_pwd); + last_wifi_reconnect_attempt = millis(); // let DHCP finish before the poll can retry +#else + WiFi.begin(wifi_ssid, wifi_pwd); +#endif + wifi_interface.begin(TCP_PORT); + interface_manager.addInterface(InterfaceType::WiFi, &wifi_interface); + } else { + WIFI_DEBUG_PRINTLN("wifi disabled"); + } +#endif + +// add usb interface +#if defined(ENABLE_USB_INTERFACE) + usb_serial_interface.begin(Serial); + interface_manager.addInterface(InterfaceType::USB, &usb_serial_interface); +#endif + +// add ethernet interface +#if defined(ETHERNET_ENABLED) + ethernet_interface.begin(); + interface_manager.addInterface(InterfaceType::Ethernet, ðernet_interface); +#endif + +// add hardware serial interface +#if defined(SERIAL_RX) + companion_serial.setPins(SERIAL_RX, SERIAL_TX); + companion_serial.begin(115200); + hardware_serial_interface.begin(companion_serial); + interface_manager.addInterface(InterfaceType::HardwareSerial, &hardware_serial_interface); +#endif + +#if defined(SIM_PLATFORM) + sim_serial_interface.begin(); + interface_manager.addInterface(InterfaceType::USB, &sim_serial_interface); +#endif + + the_mesh.startInterface(interface_manager); + // Bluetooth as the user left it (Settings, the quick panel): off stays off + // after a restart; the other links stay on. + if (the_mesh.getNodePrefs()->ble_off) interface_manager.disableBluetooth(); + store.restoreRTCTime(); sensors.begin(); @@ -367,6 +420,7 @@ void loop() { NRF_WDT->RR[0] = 0x6E524635UL; // pet watchdog #endif the_mesh.loop(); + interface_manager.loop(); sensors.loop(); #ifdef DISPLAY_CLASS ui_task.loop(); @@ -390,13 +444,34 @@ void loop() { #endif } -#if defined(ESP32) && defined(WIFI_SSID) - // Safely attempt to reconnect every 10 seconds if flagged - if (wifi_needs_reconnect && (millis() - last_wifi_reconnect_attempt > 10000)) { - WIFI_DEBUG_PRINTLN("Attempting manual WiFi reconnect..."); - WiFi.disconnect(); - WiFi.reconnect(); - last_wifi_reconnect_attempt = millis(); +#ifdef ENABLE_WIFI_INTERFACE + if (wifi_enabled) { + // RP2040 has no WiFi event callbacks, so poll the link state instead + #if defined(RP2040_PLATFORM) + wifi_needs_reconnect = (WiFi.status() != WL_CONNECTED); + if (wifi_was_connected == wifi_needs_reconnect) { // link state changed + wifi_was_connected = !wifi_needs_reconnect; + if (wifi_was_connected) { + WIFI_DEBUG_PRINTLN("connected, listening on %s:%d", WiFi.localIP().toString().c_str(), TCP_PORT); + } else { + WIFI_DEBUG_PRINTLN("link lost"); + } + } + #endif + + // Safely attempt to reconnect if flagged. On RP2040 each attempt blocks the mesh loop + // for up to WIFI_RETRY_TIMEOUT, so retry less often and cap how long a join may stall. + if (wifi_needs_reconnect && (millis() - last_wifi_reconnect_attempt > WIFI_RETRY_INTERVAL)) { + WIFI_DEBUG_PRINTLN("Attempting manual WiFi reconnect to %s (status %d)...", wifi_ssid, WiFi.status()); + #if defined(RP2040_PLATFORM) + WiFi.setTimeout(WIFI_RETRY_TIMEOUT); + WiFi.beginNoBlock(wifi_ssid, wifi_pwd); // no reconnect() on this platform + #else + WiFi.disconnect(); + WiFi.reconnect(); + #endif + last_wifi_reconnect_attempt = millis(); + } } #endif } diff --git a/examples/companion_radio/ui-lvgl/QuickPanel.h b/examples/companion_radio/ui-lvgl/QuickPanel.h index b8f9425d..c35d3aa1 100644 --- a/examples/companion_radio/ui-lvgl/QuickPanel.h +++ b/examples/companion_radio/ui-lvgl/QuickPanel.h @@ -314,7 +314,7 @@ bool UITask::quickPanelOpen() const { return qpanel::s_open; } void UITask::quickPanelRefresh() { using namespace qpanel; if (!s_panel) return; - tileSet(T_BT, LV_SYMBOL_BLUETOOTH, hasConnection() ? "Connected" : "Bluetooth", isSerialEnabled()); + tileSet(T_BT, LV_SYMBOL_BLUETOOTH, hasConnection() ? "Connected" : "Bluetooth", isBluetoothEnabled()); tileSet(T_WIFI, LV_SYMBOL_WIFI, lvport::netRadio() == lvport::NET_UP ? "Online" : "WiFi", lvport::wifiAllowed()); int32_t lat, lon; const bool gps = _core->gpsEnabled(); @@ -357,7 +357,7 @@ void UITask::quickPanelTile(int t, bool hold) { } // The tile shows what changed: no toast saying it again. switch (t) { - case T_BT: setBluetooth(!isSerialEnabled()); break; + case T_BT: setBluetooth(!isBluetoothEnabled()); break; case T_WIFI: wifiSetAllowed(!lvport::wifiAllowed()); break; case T_GPS: if (_core->gpsAvailable()) setGps(!_core->gpsEnabled()); break; case T_SOUND: diff --git a/examples/companion_radio/ui-lvgl/QuickScreen.h b/examples/companion_radio/ui-lvgl/QuickScreen.h index 480787a5..0640aae8 100644 --- a/examples/companion_radio/ui-lvgl/QuickScreen.h +++ b/examples/companion_radio/ui-lvgl/QuickScreen.h @@ -47,7 +47,7 @@ static void onQuickEditKb(lv_event_t* e) { s_ui->quickEditDone(lv_event_get_cod static void onQuickList(lv_event_t* e) { (void)e; s_ui->showQuickMsgs(); } static void onAdvertGo(lv_event_t* e) { s_ui->advertSend((uintptr_t)lv_event_get_user_data(e) != 0); } static void bluetoothHint(char* sub, size_t n) { - if (!s_ui->isSerialEnabled()) snprintf(sub, n, "Off"); + if (!s_ui->isBluetoothEnabled()) snprintf(sub, n, "Off"); else if (s_ui->hasConnection()) snprintf(sub, n, "App connected"); else if (the_mesh.getBLEPin()) snprintf(sub, n, "PIN %06lu", (unsigned long)the_mesh.getBLEPin()); else snprintf(sub, n, "Waiting for the app"); @@ -202,12 +202,12 @@ static void bluetoothRow(lv_obj_t* body) { char sub[48]; qview::bluetoothHint(sub, sizeof(sub)); lv_obj_t* sw = switchRow(body, LV_SYMBOL_BLUETOOTH " Bluetooth", sub, nullptr); - if (s_ui->isSerialEnabled()) lv_obj_add_state(sw, LV_STATE_CHECKED); + if (s_ui->isBluetoothEnabled()) lv_obj_add_state(sw, LV_STATE_CHECKED); lv_obj_add_event_cb(sw, qview::onBtSwitch, LV_EVENT_VALUE_CHANGED, NULL); } void UITask::setBluetooth(bool on) { - if (on) enableSerial(); else disableSerial(); + if (on) enableBluetooth(); else disableBluetooth(); if (!on) showToast("Bluetooth off - USB still works", 1500); refreshStatusBar(); } diff --git a/examples/companion_radio/ui-lvgl/UITask.cpp b/examples/companion_radio/ui-lvgl/UITask.cpp index 173c92ae..781de892 100644 --- a/examples/companion_radio/ui-lvgl/UITask.cpp +++ b/examples/companion_radio/ui-lvgl/UITask.cpp @@ -750,7 +750,7 @@ void UITask::refreshStatusBar() { struct Icon { const char* sym; uint32_t col; }; Icon icons[11]; int n = 0; - if (isSerialEnabled()) icons[n++] = { LV_SYMBOL_BLUETOOTH, hasConnection() ? theme::ACCENT : theme::TEXT_MUTED }; + if (isBluetoothEnabled()) icons[n++] = { LV_SYMBOL_BLUETOOTH, hasConnection() ? theme::ACCENT : theme::TEXT_MUTED }; if (lvport::wifiAllowed()) // WiFi switched on: accent while connected (map tiles, update) icons[n++] = { LV_SYMBOL_WIFI, lvport::netRadio() == lvport::NET_UP ? theme::ACCENT : theme::TEXT_MUTED }; int32_t lat, lon; diff --git a/examples/companion_radio/ui-lvgl/UITask.h b/examples/companion_radio/ui-lvgl/UITask.h index 1de516b5..eb702638 100644 --- a/examples/companion_radio/ui-lvgl/UITask.h +++ b/examples/companion_radio/ui-lvgl/UITask.h @@ -7,7 +7,7 @@ #include #include #include -#include +#include #include // A big zero-initialised static in PSRAM (ESP32): the internal RAM is what // WiFi starts and joins on. Not for what DMA or an interrupt touches. @@ -53,7 +53,7 @@ struct TextEntry { class UITask : public UITaskBase, public UiCoreHost { public: - UITask(mesh::MainBoard* board, BaseSerialInterface* serial) : UITaskBase(board, serial) {} + UITask(mesh::MainBoard* board, MultiSerialInterface* serial) : UITaskBase(board, serial) {} void begin(DisplayDriver* display, SensorManager* sensors, NodePrefs* node_prefs); void loop() override; diff --git a/examples/companion_radio/ui-new/HomeScreen.h b/examples/companion_radio/ui-new/HomeScreen.h index 55ff561b..6a17342f 100644 --- a/examples/companion_radio/ui-new/HomeScreen.h +++ b/examples/companion_radio/ui-new/HomeScreen.h @@ -328,7 +328,7 @@ class HomeScreen : public UIScreen { add("All messages", Q_MORE, QA_MORE, 0); n += recentChats(rows + n, quickFit() - 1); } else if (page == BLUETOOTH) { - const bool on = _task->isSerialEnabled(); + const bool on = _task->isBluetoothEnabled(); add("Bluetooth", Q_SWITCH, QA_BT, on ? 1 : 0); add("Phone", Q_VALUE, QA_NONE, 0); char* v = rows[n - 1].value; @@ -440,7 +440,7 @@ class HomeScreen : public UIScreen { case QA_BRIGHTNESS: case QA_AUTO_ADVERT: _quick_edit = true; break; // Left/Right change it, Enter / Back keep it case QA_BT: - if (_task->isSerialEnabled()) _task->disableSerial(); else _task->enableSerial(); + if (_task->isBluetoothEnabled()) _task->disableBluetooth(); else _task->enableBluetooth(); break; case QA_ADVERT: _task->notify(UIEventType::ack); @@ -609,7 +609,7 @@ class HomeScreen : public UIScreen { struct Sicon { bool active; const MiniIcon* icon; }; const Sicon icons[] = { { board.isExternalPowered(), &ICON_CHARGE }, // USB power: next to the battery - { _task->isSerialEnabled() && _task->isBLEConnected(), &ICON_BLUETOOTH }, + { _task->isBluetoothEnabled() && _task->isBLEConnected(), &ICON_BLUETOOTH }, { gps_on, gps_fix ? &ICON_GPS : &ICON_GPS_SEARCH }, { _node_prefs && _node_prefs->alarm_on, &ICON_ALARM }, { mute_on, &ICON_MUTE }, diff --git a/examples/companion_radio/ui-new/UITask.h b/examples/companion_radio/ui-new/UITask.h index 610d64a0..36ce3c75 100644 --- a/examples/companion_radio/ui-new/UITask.h +++ b/examples/companion_radio/ui-new/UITask.h @@ -4,7 +4,7 @@ #include #include #include -#include +#include #include #include #include @@ -284,7 +284,7 @@ public: void setNodeLockPassword(const char* plain); bool passwordLockEnabled() const; - UITask(mesh::MainBoard* board, BaseSerialInterface* serial) : UITaskBase(board, serial), _display(NULL), _sensors(NULL), _node_prefs(NULL) { + UITask(mesh::MainBoard* board, MultiSerialInterface* serial) : UITaskBase(board, serial), _display(NULL), _sensors(NULL), _node_prefs(NULL) { next_batt_chck = _next_refresh = 0; ui_started_at = 0; _batt_mv = 0; diff --git a/examples/companion_radio/ui-orig/UITask.h b/examples/companion_radio/ui-orig/UITask.h index 751ce0e9..56b1d056 100644 --- a/examples/companion_radio/ui-orig/UITask.h +++ b/examples/companion_radio/ui-orig/UITask.h @@ -61,7 +61,7 @@ class UITask : public AbstractUITask { public: - UITask(mesh::MainBoard* board, BaseSerialInterface* serial) : AbstractUITask(board, serial), _display(NULL), _sensors(NULL) { + UITask(mesh::MainBoard* board, MultiSerialInterface* serial) : AbstractUITask(board, serial), _display(NULL), _sensors(NULL) { _next_refresh = 0; ui_started_at = 0; } diff --git a/examples/companion_radio/ui-tiny/UITask.cpp b/examples/companion_radio/ui-tiny/UITask.cpp index a89b0150..2bce3e11 100644 --- a/examples/companion_radio/ui-tiny/UITask.cpp +++ b/examples/companion_radio/ui-tiny/UITask.cpp @@ -241,7 +241,7 @@ public: } else if (_page == HomePage::BLUETOOTH) { display.setColor(DisplayDriver::GREEN); display.drawXbm((display.width() - 32) / 2, 8, - _task->isSerialEnabled() ? bluetooth_on : bluetooth_off, + _task->isBluetoothEnabled() ? bluetooth_on : bluetooth_off, 32, 32); display.setTextSize(1); // display.drawTextCentered(display.width() / 2, 40 - 11, "toggle: " PRESS_LABEL); @@ -387,10 +387,10 @@ public: return true; } if (c == KEY_ENTER && _page == HomePage::BLUETOOTH) { - if (_task->isSerialEnabled()) { // toggle Bluetooth on/off - _task->disableSerial(); + if (_task->isBluetoothEnabled()) { // toggle Bluetooth on/off + _task->disableBluetooth(); } else { - _task->enableSerial(); + _task->enableBluetooth(); } return true; } @@ -674,7 +674,7 @@ void UITask::loop() { _cached_batt_mv, isBuzzerQuiet(), getGPSState(), - isSerialEnabled()); + isBluetoothEnabled()); bool status_dirty = _statusBar.needsRedraw(); bool content_dirty = (millis() >= _next_refresh && curr); diff --git a/examples/companion_radio/ui-tiny/UITask.h b/examples/companion_radio/ui-tiny/UITask.h index 97a19ffe..d4bb6162 100644 --- a/examples/companion_radio/ui-tiny/UITask.h +++ b/examples/companion_radio/ui-tiny/UITask.h @@ -4,7 +4,7 @@ #include #include #include -#include +#include #include #include @@ -71,7 +71,7 @@ class UITask : public AbstractUITask { public: - UITask(mesh::MainBoard* board, BaseSerialInterface* serial) : AbstractUITask(board, serial), _display(NULL), _sensors(NULL) { + UITask(mesh::MainBoard* board, MultiSerialInterface* serial) : AbstractUITask(board, serial), _display(NULL), _sensors(NULL) { next_batt_chck = _next_refresh = 0; _cached_batt_mv = 0; ui_started_at = 0; diff --git a/src/helpers/ArduinoSerialInterface.cpp b/src/helpers/ArduinoSerialInterface.cpp index 3c09b189..ebbdc3ba 100644 --- a/src/helpers/ArduinoSerialInterface.cpp +++ b/src/helpers/ArduinoSerialInterface.cpp @@ -22,6 +22,17 @@ bool ArduinoSerialInterface::isConnected() const { return true; // no way of knowing, so assume yes } +bool ArduinoSerialInterface::isClientConnected() const { +#if defined(NRF52_PLATFORM) || (defined(ARDUINO_USB_MODE) && ARDUINO_USB_MODE == 0 && defined(ARDUINO_USB_CDC_ON_BOOT) && ARDUINO_USB_CDC_ON_BOOT == 1) + // (bool)Serial: the host has the CDC port open (DTR) -- a serial monitor + // counts too, charging alone doesn't. + return _serial == (Stream*)&Serial && (bool)Serial; +#else + // A UART bridge: (bool)Serial is just "begun", always true. + return false; +#endif +} + bool ArduinoSerialInterface::isWriteBusy() const { return false; } diff --git a/src/helpers/ArduinoSerialInterface.h b/src/helpers/ArduinoSerialInterface.h index c4086353..615bf0d4 100644 --- a/src/helpers/ArduinoSerialInterface.h +++ b/src/helpers/ArduinoSerialInterface.h @@ -27,6 +27,9 @@ public: bool isEnabled() const override { return _isEnabled; } bool isConnected() const override; + // A host holding the port open: native USB tells (DTR), a UART bridge + // can't, so it never counts as an app being there. + bool isClientConnected() const override; bool isWriteBusy() const override; size_t writeFrame(const uint8_t src[], size_t len) override; diff --git a/src/helpers/BaseSerialInterface.h b/src/helpers/BaseSerialInterface.h index d1e7a685..88e83c08 100644 --- a/src/helpers/BaseSerialInterface.h +++ b/src/helpers/BaseSerialInterface.h @@ -14,6 +14,7 @@ public: virtual bool isEnabled() const = 0; virtual bool isConnected() const = 0; + virtual void loop() {}; // Returns true when a BLE companion app is connected. Default delegates to // isConnected(); override in dual-interface wrappers that always report // isConnected()=true but still need to distinguish BLE from USB state. diff --git a/src/helpers/MultiSerialInterface.h b/src/helpers/MultiSerialInterface.h index f7742b24..4cc9fe70 100644 --- a/src/helpers/MultiSerialInterface.h +++ b/src/helpers/MultiSerialInterface.h @@ -131,6 +131,38 @@ public: return false; } + // A companion app on Bluetooth (the pairing PIN, the BT indicator). + bool isBLEConnected() const override { + for(auto iface : _interfaces){ + if(iface.instance && iface.type == InterfaceType::Bluetooth && iface.instance->isEnabled() && iface.instance->isConnected()){ + return true; + } + } + return false; + } + + // A companion app on any of them (each says how it knows). + bool isClientConnected() const override { + if(!_enabled){ + return false; + } + for(auto iface : _interfaces){ + if(iface.instance && iface.instance->isEnabled() && iface.instance->isClientConnected()){ + return true; + } + } + return false; + } + + bool hasPendingFrames() const override { + for(auto iface : _interfaces){ + if(iface.instance && iface.instance->isEnabled() && iface.instance->hasPendingFrames()){ + return true; + } + } + return false; + } + // loop all interfaces void loop() override { for(auto iface : _interfaces){ diff --git a/src/helpers/esp32/DualSerialInterface.h b/src/helpers/esp32/DualSerialInterface.h deleted file mode 100644 index 76f44be4..00000000 --- a/src/helpers/esp32/DualSerialInterface.h +++ /dev/null @@ -1,91 +0,0 @@ -#pragma once - -#include "../BaseSerialInterface.h" -#include "../ArduinoSerialInterface.h" -#include "SerialBLEInterface.h" - -// ESP32 counterpart of helpers/nrf52/DualSerialInterface.h: wraps BLE + USB -// serial so one build serves both companion transports. BLE takes priority when -// connected, USB is always ready as a fallback. -// enable()/disable() control BLE only — USB is always on. -// BLE state machine is only pumped when BLE is enabled; USB is not read while -// BLE is connected. -class DualSerialInterface : public BaseSerialInterface { - SerialBLEInterface _ble; - ArduinoSerialInterface _usb; - uint8_t _ble_buf[MAX_FRAME_SIZE]; - uint8_t _usb_buf[MAX_FRAME_SIZE]; - bool _ble_enabled; - bool _ble_was_connected; - -public: - DualSerialInterface() : _ble_enabled(false), _ble_was_connected(false) {} - - void begin(const char* ble_prefix, char* node_name, uint32_t pin_code, Stream& usb_stream) { - _ble.begin(ble_prefix, node_name, pin_code); - _usb.begin(usb_stream); - _usb.enable(); // USB is always on - } - - void enable() override { _ble.enable(); _ble_enabled = true; } - void disable() override { _ble.disable(); _ble_enabled = false; } - bool isEnabled() const override { return _ble_enabled; } - - // Always true — USB is always available as fallback, so the mesh can send. - bool isConnected() const override { return true; } - // True only when a BLE companion app is paired and connected. - bool isBLEConnected() const override { return _ble_enabled && _ble.isConnected(); } -#if defined(ARDUINO_USB_MODE) && ARDUINO_USB_MODE == 0 && defined(ARDUINO_USB_CDC_ON_BOOT) && ARDUINO_USB_CDC_ON_BOOT == 1 - // Native USB CDC (e.g. Heltec V4): Serial's bool operator reflects whether - // the host actually has the port open, same DTR-style signal the nRF52 - // version reads. Counts as a connected client same as BLE, matching that - // version's behaviour. - bool isClientConnected() const override { return isBLEConnected() || (bool)Serial; } -#else - // BLE only, unlike the nRF52 version, which also counts a USB host holding the - // CDC port open. These boards reach USB through a UART bridge rather than - // native CDC, so there is no DTR to read: (bool)Serial is a plain "is the - // peripheral initialised", i.e. always true once begin() has run, and - // ArduinoSerialInterface::isConnected() likewise hardcodes true. Reporting - // that as a connected client would permanently convince the UI an app is - // watching the device and stop it waking the display for new messages. - bool isClientConnected() const override { return isBLEConnected(); } -#endif - - // USB can't tell without reading it. - bool hasPendingFrames() const override { - return (_ble_enabled && _ble.isConnected()) ? _ble.hasPendingFrames() : true; - } - - bool isWriteBusy() const override { - return (_ble_enabled && _ble.isConnected()) ? _ble.isWriteBusy() : _usb.isWriteBusy(); - } - - size_t writeFrame(const uint8_t src[], size_t len) override { - return (_ble_enabled && _ble.isConnected()) ? _ble.writeFrame(src, len) : _usb.writeFrame(src, len); - } - - size_t checkRecvFrame(uint8_t dest[]) override { - if (_ble_enabled) { - size_t ble_len = _ble.checkRecvFrame(_ble_buf); - bool ble_now = _ble.isConnected(); - - if (ble_now) { - _ble_was_connected = true; - if (ble_len > 0) { memcpy(dest, _ble_buf, ble_len); return ble_len; } - return 0; // BLE active — don't read USB to keep its state machine clean - } - - if (_ble_was_connected) { - // BLE just disconnected — reset USB state machine so stale partial frames don't block it - _ble_was_connected = false; - _usb.enable(); - return 0; - } - } - - size_t usb_len = _usb.checkRecvFrame(_usb_buf); - if (usb_len > 0) { memcpy(dest, _usb_buf, usb_len); return usb_len; } - return 0; - } -}; diff --git a/src/helpers/nrf52/DualSerialInterface.h b/src/helpers/nrf52/DualSerialInterface.h deleted file mode 100644 index 67ddfe34..00000000 --- a/src/helpers/nrf52/DualSerialInterface.h +++ /dev/null @@ -1,72 +0,0 @@ -#pragma once - -#include "../BaseSerialInterface.h" -#include "../ArduinoSerialInterface.h" -#include "SerialBLEInterface.h" - -// Wraps BLE + USB serial interfaces: BLE takes priority when connected, -// USB is always ready as a fallback. -// enable()/disable() control BLE only — USB is always on. -// BLE state machine is only pumped when BLE is enabled; USB is not read while BLE is connected. -class DualSerialInterface : public BaseSerialInterface { - SerialBLEInterface _ble; - ArduinoSerialInterface _usb; - uint8_t _ble_buf[MAX_FRAME_SIZE]; - uint8_t _usb_buf[MAX_FRAME_SIZE]; - bool _ble_enabled; - bool _ble_was_connected; - -public: - DualSerialInterface() : _ble_enabled(false), _ble_was_connected(false) {} - - void begin(const char* ble_prefix, char* node_name, uint32_t pin_code, Stream& usb_stream) { - _ble.begin(ble_prefix, node_name, pin_code); - _usb.begin(usb_stream); - _usb.enable(); // USB is always on - } - - void enable() override { _ble.enable(); _ble_enabled = true; } - void disable() override { _ble.disable(); _ble_enabled = false; } - bool isEnabled() const override { return _ble_enabled; } - - // Always true — USB is always available as fallback, so the mesh can send. - bool isConnected() const override { return true; } - // True only when a BLE companion app is paired and connected. - bool isBLEConnected() const override { return _ble_enabled && _ble.isConnected(); } - // A companion app is actually connected if BLE is bonded OR a host holds the - // USB-CDC port open. (bool)Serial == tud_cdc_n_connected() (DTR asserted) — a - // serial monitor counts too, but charging-only (no host) does not. - bool isClientConnected() const override { return isBLEConnected() || (bool)Serial; } - - bool isWriteBusy() const override { - return (_ble_enabled && _ble.isConnected()) ? _ble.isWriteBusy() : _usb.isWriteBusy(); - } - - size_t writeFrame(const uint8_t src[], size_t len) override { - return (_ble_enabled && _ble.isConnected()) ? _ble.writeFrame(src, len) : _usb.writeFrame(src, len); - } - - size_t checkRecvFrame(uint8_t dest[]) override { - if (_ble_enabled) { - size_t ble_len = _ble.checkRecvFrame(_ble_buf); - bool ble_now = _ble.isConnected(); - - if (ble_now) { - _ble_was_connected = true; - if (ble_len > 0) { memcpy(dest, _ble_buf, ble_len); return ble_len; } - return 0; // BLE active — don't read USB to keep its state machine clean - } - - if (_ble_was_connected) { - // BLE just disconnected — reset USB state machine so stale partial frames don't block it - _ble_was_connected = false; - _usb.enable(); - return 0; - } - } - - size_t usb_len = _usb.checkRecvFrame(_usb_buf); - if (usb_len > 0) { memcpy(dest, _usb_buf, usb_len); return usb_len; } - return 0; - } -}; diff --git a/variants/sim/build_wasm.sh b/variants/sim/build_wasm.sh index 7b36cdf2..93af3157 100755 --- a/variants/sim/build_wasm.sh +++ b/variants/sim/build_wasm.sh @@ -85,6 +85,7 @@ SRCS=( src/helpers/ClientACL.cpp src/helpers/CommonCLI.cpp src/helpers/ConfigSerializer.cpp + src/helpers/DynamicConfigSerializer.cpp src/helpers/DeviceDiag.cpp src/helpers/IdentityStore.cpp src/helpers/RegionMap.cpp