From 7afa6b11a8a4068ec1795b1f485dbee538cb880e Mon Sep 17 00:00:00 2001 From: mikecarper Date: Sat, 15 Aug 2026 18:12:09 -0700 Subject: [PATCH] Add T-Beam WiFi button toggle --- docs/WiFi.md | 6 + docs/companion_radio_full.md | 7 + examples/companion_radio/CompanionWiFi.h | 10 + examples/companion_radio/DataStore.cpp | 6 + examples/companion_radio/MyMesh.cpp | 11 + examples/companion_radio/MyMesh.h | 2 + examples/companion_radio/NodePrefs.h | 1 + examples/companion_radio/main.cpp | 205 ++++++++++++++---- examples/companion_radio/ui-new/UITask.cpp | 23 +- examples/companion_radio/ui-new/UITask.h | 1 + src/helpers/WiFiSetupPortal.cpp | 9 + src/helpers/WiFiSetupPortal.h | 2 + src/helpers/esp32/SerialWifiInterface.cpp | 9 + src/helpers/esp32/SerialWifiInterface.h | 1 + .../test_companion_node_prefs.cpp | 15 ++ 15 files changed, 263 insertions(+), 45 deletions(-) create mode 100644 examples/companion_radio/CompanionWiFi.h diff --git a/docs/WiFi.md b/docs/WiFi.md index 686e5765..369650a3 100644 --- a/docs/WiFi.md +++ b/docs/WiFi.md @@ -181,6 +181,12 @@ service on ports 5001 and 5002. See the [full Companion guide](./companion_radio_full.md) for its build, terminal mode, and complete update-source workflow. +The LilyGo T-Beam 1W Full Companion maps a triple click of the physical button +labeled `17` to a persistent WiFi on/off toggle. Turning WiFi off closes the +WebConfig, Companion TCP, mOTA, and MQTT network services while USB, BLE, the +display, GPS, and LoRa remain available. The same triple click turns WiFi back +on even after reboot. + The companion loads runtime credentials saved in NVS. A non-placeholder compile-time `WIFI_SSID`/`WIFI_PWD` can be used as a first-boot fallback, but saved credentials take priority. With no credentials, its WebConfig portal diff --git a/docs/companion_radio_full.md b/docs/companion_radio_full.md index 0cfcac72..fdf6b4b3 100644 --- a/docs/companion_radio_full.md +++ b/docs/companion_radio_full.md @@ -80,6 +80,13 @@ clock and keeps GPS awake. Full Companion transports remain available in both states; WiFi modem sleep stays enabled when BLE is present because coexistence requires it. The selected state is retained after reboot. +On the LilyGo T-Beam 1W Full Companion, triple-click the physical button +labeled `17` to turn the ESP32 WiFi radio and all WiFi services off or on. The +screen confirms `WiFi: OFF` or `WiFi: ON`, and the selected state is retained +after reboot. When WiFi is off, TCP ports 5000-5002, WebConfig, and MQTT are +stopped; USB, BLE, the display, GPS, and LoRa continue to operate. Triple-click +button 17 again to restore WiFi, including the saved station or setup-AP mode. + Artifacts are written to `out/` by default. On 4 MB ESP32 boards, the full target uses a single 3 MB application diff --git a/examples/companion_radio/CompanionWiFi.h b/examples/companion_radio/CompanionWiFi.h new file mode 100644 index 00000000..cd400787 --- /dev/null +++ b/examples/companion_radio/CompanionWiFi.h @@ -0,0 +1,10 @@ +#pragma once + +#if defined(ESP32) && defined(WIFI_SSID) + +// The requested state is persisted immediately. Network services transition +// from the main loop so a button callback never tears down an active server. +bool toggleCompanionWiFi(); +bool isCompanionWiFiEnabled(); + +#endif diff --git a/examples/companion_radio/DataStore.cpp b/examples/companion_radio/DataStore.cpp index ba7005b3..1099e485 100644 --- a/examples/companion_radio/DataStore.cpp +++ b/examples/companion_radio/DataStore.cpp @@ -336,6 +336,10 @@ void DataStore::loadPrefsInt(const char *filename, CompanionNodePrefs& _prefs, d if (file.available() >= (int)sizeof(_prefs.powersaving_enabled)) { file.read((uint8_t *)&_prefs.powersaving_enabled, sizeof(_prefs.powersaving_enabled)); // 137 + if (file.available() >= (int)sizeof(_prefs.wifi_enabled)) { + file.read((uint8_t *)&_prefs.wifi_enabled, + sizeof(_prefs.wifi_enabled)); // 138 + } } } @@ -401,6 +405,8 @@ bool DataStore::savePrefs(const CompanionNodePrefs& _prefs, double node_lat, dou sizeof(_prefs.rx_ps_preamble)) == sizeof(_prefs.rx_ps_preamble); // 136 success = success && file.write((uint8_t *)&_prefs.powersaving_enabled, sizeof(_prefs.powersaving_enabled)) == sizeof(_prefs.powersaving_enabled); // 137 + success = success && file.write((uint8_t *)&_prefs.wifi_enabled, + sizeof(_prefs.wifi_enabled)) == sizeof(_prefs.wifi_enabled); // 138 #if defined(NRF52_PLATFORM) success = file.commit(success); diff --git a/examples/companion_radio/MyMesh.cpp b/examples/companion_radio/MyMesh.cpp index a51a8add..ca95099e 100644 --- a/examples/companion_radio/MyMesh.cpp +++ b/examples/companion_radio/MyMesh.cpp @@ -1356,6 +1356,7 @@ MyMesh::MyMesh(mesh::Radio &radio, mesh::RNG &rng, mesh::RTCClock &rtc, SimpleMe _prefs.rx_ps_rx_us = RX_POWERSAVING_DEFAULT_RX_US; _prefs.rx_ps_sleep_us = RX_POWERSAVING_DEFAULT_SLEEP_US; _prefs.powersaving_enabled = 0; + _prefs.wifi_enabled = 1; recalcRxPowerSavingFromLevel(_prefs.rx_ps_level, _prefs.sf, _prefs.bw, _prefs.rx_ps_preamble, &_prefs.rx_ps_rx_us, &_prefs.rx_ps_sleep_us); @@ -1439,6 +1440,7 @@ void MyMesh::begin(bool has_display) { _prefs.radio_fem_txgain = constrain(_prefs.radio_fem_txgain, 0, 1); _prefs.rx_powersaving_enabled = constrain(_prefs.rx_powersaving_enabled, 0, 1); _prefs.powersaving_enabled = constrain(_prefs.powersaving_enabled, 0, 1); + _prefs.wifi_enabled = constrain(_prefs.wifi_enabled, 0, 1); _prefs.rx_ps_level = constrain(_prefs.rx_ps_level, 0, 10); if (_prefs.rx_ps_preamble != 16 && _prefs.rx_ps_preamble != 32) { _prefs.rx_ps_preamble = 0; @@ -1813,6 +1815,11 @@ void MyMesh::serviceMQTT(const char* wifi_ssid, const char* wifi_password) { } } } + +void MyMesh::stopMQTT() { + if (_mqtt_started && _mqtt_bridge) _mqtt_bridge->end(); + _mqtt_started = false; +} #endif #ifdef WITH_WEBCONFIG @@ -1938,6 +1945,10 @@ void MyMesh::serviceWebConfig() { if (_webconfig) _webconfig->tick(millis()); } +bool MyMesh::isWebConfigActiveOrStopping() const { + return _webconfig && (_webconfig->isRunning() || _webconfig->isStopping()); +} + bool MyMesh::isWebConfigSetupActive() const { return _webconfig && _webconfig->mode() == WebConfigServer::MODE_SETUP && !_webconfig->isStopping(); diff --git a/examples/companion_radio/MyMesh.h b/examples/companion_radio/MyMesh.h index eb3d1587..c471ce7f 100644 --- a/examples/companion_radio/MyMesh.h +++ b/examples/companion_radio/MyMesh.h @@ -121,6 +121,7 @@ public: #if defined(WITH_MQTT_BRIDGE) && defined(ESP32_PLATFORM) && defined(WIFI_SSID) void serviceMQTT(const char* wifi_ssid, const char* wifi_password); + void stopMQTT(); bool isMQTTConfigured() const { return _mqtt_configured; } #endif @@ -128,6 +129,7 @@ public: bool startWebConfig(bool force_ap, char* reply); void stopWebConfig(); void serviceWebConfig(); + bool isWebConfigActiveOrStopping() const; bool isWebConfigSetupActive() const; bool isWebConfigWiFiRecoveryActive() const; diff --git a/examples/companion_radio/NodePrefs.h b/examples/companion_radio/NodePrefs.h index 1a4a6216..e95e0df2 100644 --- a/examples/companion_radio/NodePrefs.h +++ b/examples/companion_radio/NodePrefs.h @@ -45,6 +45,7 @@ struct CompanionNodePrefs { // persisted to file uint8_t rx_ps_level; // 0=manual timings, 1..10=level-derived uint8_t rx_ps_preamble; // 0=auto from SF, otherwise 16 or 32 uint8_t powersaving_enabled; // device CPU/GPS idle power saving + uint8_t wifi_enabled; // Companion WiFi radio and services // Keep the upstream repeat API while retaining the existing binary prefs // layout used by this branch. diff --git a/examples/companion_radio/main.cpp b/examples/companion_radio/main.cpp index 43ce9587..a1298d7c 100644 --- a/examples/companion_radio/main.cpp +++ b/examples/companion_radio/main.cpp @@ -1,6 +1,7 @@ #include // needed for PlatformIO #include #include "MyMesh.h" +#include "CompanionWiFi.h" #ifdef ESP32_PLATFORM #include "esp_bt.h" @@ -431,6 +432,27 @@ void halt() { bool wifi_setup_recovery_mode = false; static char configured_wifi_ssid[32]; static char configured_wifi_password[64]; + static bool companion_wifi_has_credentials = false; + static bool companion_wifi_requested = true; + static bool companion_wifi_active = false; + static bool companion_wifi_disable_in_progress = false; + static bool companion_wifi_services_stopped = false; + + bool isCompanionWiFiEnabled() { + return companion_wifi_requested; + } + + bool toggleCompanionWiFi() { + companion_wifi_requested = !companion_wifi_requested; + the_mesh.getNodePrefs()->wifi_enabled = companion_wifi_requested ? 1 : 0; + the_mesh.savePrefs(); + if (!companion_wifi_requested && companion_wifi_active) { + companion_wifi_disable_in_progress = true; + } + WIFI_DEBUG_PRINTLN("GPIO 17 triple click requested WiFi %s", + companion_wifi_requested ? "on" : "off"); + return companion_wifi_requested; + } #if defined(WITH_WEBCONFIG) && defined(DISPLAY_CLASS) static DisplayDriver* companion_setup_display = nullptr; @@ -492,6 +514,7 @@ void halt() { configured_wifi_ssid[sizeof(configured_wifi_ssid) - 1] = '\0'; strncpy(configured_wifi_password, password ? password : "", sizeof(configured_wifi_password) - 1); configured_wifi_password[sizeof(configured_wifi_password) - 1] = '\0'; + companion_wifi_has_credentials = true; return true; } #endif @@ -502,6 +525,7 @@ void halt() { `normalradio` here so a host can run an end-to-end mOTA source without occupying the binary port. */ #if defined(ESP32) && defined(WIFI_SSID) && defined(ENABLE_OTA) #include // mesh::ota::handle_ota_command(line, reply, board) + #include #ifndef OTA_CONSOLE_TCP_PORT #define OTA_CONSOLE_TCP_PORT 5002 #endif @@ -510,6 +534,18 @@ void halt() { static char ota_console_line[128]; static uint8_t ota_console_len = 0; + static void ota_console_start() { + ota_console_server.begin(); + WIFI_DEBUG_PRINTLN("OTA console listening on :%d (nc %d, type `ota ...`)", + OTA_CONSOLE_TCP_PORT, OTA_CONSOLE_TCP_PORT); + } + + static void ota_console_stop() { + if (ota_console_client) ota_console_client.stop(); + ota_console_server.end(); + ota_console_len = 0; + } + static void ota_console_loop() { if (!ota_console_client || !ota_console_client.connected()) { WiFiClient c = ota_console_server.available(); @@ -543,6 +579,107 @@ void halt() { } #endif +#if defined(ESP32) && defined(WIFI_SSID) + static void resetCompanionWiFiRecoveryState() { + wifi_reconnect_tracker = WiFiReconnectPolicy::Tracker(); + wifi_setup_attempted = false; + last_wifi_setup_attempt = 0; + wifi_setup_recovery_mode = false; + } + + static void startCompanionWiFi() { + if (companion_wifi_active) return; + + board.setInhibitSleep(true); + WiFi.setAutoReconnect(true); + resetCompanionWiFiRecoveryState(); + + if (companion_wifi_has_credentials) { + WiFi.mode(WIFI_STA); + wifi_reconnect_tracker.noteDisconnected(millis()); + WiFi.begin(configured_wifi_ssid, configured_wifi_password); + } +#ifndef WITH_WEBCONFIG + else if (!wifiSetupPortal().begin(COMPANION_WIFI_SETUP_AP, + saveCompanionWiFi, nullptr)) { + WIFI_DEBUG_PRINTLN("WiFi setup: could not start setup portal"); + } +#endif +#ifdef WITH_WEBCONFIG + if (WebConfigServer::loadEnabled(true)) { + char web_reply[160]; + the_mesh.startWebConfig(!companion_wifi_has_credentials, web_reply); + WIFI_DEBUG_PRINTLN("%s", web_reply); + } +#endif + +#if defined(BLE_PIN_CODE) + WiFi.setSleep(true); +#else + WiFi.setSleep(false); +#endif + wifi_interface.begin(TCP_PORT); + wifi_interface.enable(); +#ifdef ENABLE_OTA + ota_console_start(); +#endif + companion_wifi_active = true; + companion_wifi_services_stopped = false; + WIFI_DEBUG_PRINTLN("WiFi enabled by GPIO 17 control"); + } + + static void stopCompanionWiFiServices() { + if (companion_wifi_services_stopped) return; + wifi_interface.end(); +#ifdef ENABLE_OTA + ota_console_stop(); + mesh::ota::WiFiOtaSeeder::stop(); +#endif +#ifdef WITH_MQTT_BRIDGE + the_mesh.stopMQTT(); +#endif + companion_wifi_services_stopped = true; + } + + static bool finishStoppingCompanionWiFi() { + stopCompanionWiFiServices(); +#ifdef WITH_WEBCONFIG + if (the_mesh.isWebConfigActiveOrStopping()) { + the_mesh.stopWebConfig(); + return false; + } +#else + if (wifiSetupPortal().isActive()) { + wifiSetupPortal().stop(); + return false; + } + if (wifiSetupPortal().isStopping()) return false; +#endif + + WiFi.setAutoReconnect(false); + WiFi.disconnect(true, false); + WiFi.mode(WIFI_OFF); + board.setInhibitSleep(false); + resetCompanionWiFiRecoveryState(); + companion_wifi_active = false; + WIFI_DEBUG_PRINTLN("WiFi radio disabled by GPIO 17 control"); + return true; + } + + static void serviceCompanionWiFiState() { + if (!companion_wifi_requested && companion_wifi_active) { + companion_wifi_disable_in_progress = true; + } + if (companion_wifi_disable_in_progress) { + if (!finishStoppingCompanionWiFi()) return; + companion_wifi_disable_in_progress = false; + } + if (companion_wifi_requested && !companion_wifi_active) { + startCompanionWiFi(); + } + } +#endif + void setup() { Serial.begin(115200); board.begin(); @@ -644,64 +781,39 @@ void setup() { // add wifi interface #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; automatic recovery is active"); + if (companion_wifi_requested) { + WIFI_DEBUG_PRINTLN("WiFi disconnected; automatic recovery is active"); + } } else if (event == ARDUINO_EVENT_WIFI_STA_GOT_IP) { WIFI_DEBUG_PRINTLN("connected! IP %s (companion app on :%d)", WiFi.localIP().toString().c_str(), TCP_PORT); } }); - bool have_wifi = WiFiSetupPortal::loadStoredCredentials( + companion_wifi_has_credentials = WiFiSetupPortal::loadStoredCredentials( configured_wifi_ssid, sizeof(configured_wifi_ssid), configured_wifi_password, sizeof(configured_wifi_password)); - if (!have_wifi && !WiFiSetupPortal::isPlaceholderSSID(WIFI_SSID)) { + if (!companion_wifi_has_credentials + && !WiFiSetupPortal::isPlaceholderSSID(WIFI_SSID)) { strncpy(configured_wifi_ssid, WIFI_SSID, sizeof(configured_wifi_ssid) - 1); configured_wifi_ssid[sizeof(configured_wifi_ssid) - 1] = '\0'; strncpy(configured_wifi_password, WIFI_PWD, sizeof(configured_wifi_password) - 1); configured_wifi_password[sizeof(configured_wifi_password) - 1] = '\0'; - have_wifi = true; + companion_wifi_has_credentials = true; } - if (have_wifi) { - WiFi.mode(WIFI_STA); - wifi_reconnect_tracker.noteDisconnected(millis()); - WiFi.begin(configured_wifi_ssid, configured_wifi_password); - } -#ifndef WITH_WEBCONFIG - else if (!wifiSetupPortal().begin(COMPANION_WIFI_SETUP_AP, saveCompanionWiFi, nullptr)) { - WIFI_DEBUG_PRINTLN("WiFi setup: could not start setup portal"); - } -#endif - #ifdef WITH_WEBCONFIG - // WiFi companions expose the shared WebUI by default. With no stored - // credentials it opens the captive setup AP; otherwise it waits for the - // station connection and serves the same page on the LAN. - if (WebConfigServer::loadEnabled(true)) { - char web_reply[160]; - the_mesh.startWebConfig(!have_wifi, web_reply); - WIFI_DEBUG_PRINTLN("%s", web_reply); - } - #endif - // ESP-IDF requires WiFi modem sleep when Bluetooth is active. Full ESP32 - // companions provide both transports, so use the lowest sleep policy there; - // WiFi-only companions keep modem sleep disabled to avoid pauses in SX1262 - // SPI/DIO service. -#if defined(BLE_PIN_CODE) - WiFi.setSleep(true); -#else - WiFi.setSleep(false); -#endif - wifi_interface.begin(TCP_PORT); interface_manager.addInterface(InterfaceType::WiFi, &wifi_interface); - #ifdef ENABLE_OTA - ota_console_server.begin(); // dedicated OTA text-console port (`nc 5002` -> `ota ...`) - WIFI_DEBUG_PRINTLN("OTA console listening on :%d (nc %d, type `ota ...`)", OTA_CONSOLE_TCP_PORT, OTA_CONSOLE_TCP_PORT); - #endif + companion_wifi_requested = the_mesh.getNodePrefs()->wifi_enabled != 0; + if (companion_wifi_requested) { + startCompanionWiFi(); + } else { + WiFi.setAutoReconnect(false); + WiFi.mode(WIFI_OFF); + board.setInhibitSleep(false); + WIFI_DEBUG_PRINTLN("WiFi remains off from the saved GPIO 17 setting"); + } #endif // add usb interface @@ -729,6 +841,9 @@ void setup() { #endif the_mesh.startInterface(interface_manager); +#if defined(ESP32) && defined(WIFI_SSID) + if (!companion_wifi_requested) wifi_interface.disable(); +#endif sensors.begin(); #if ENV_INCLUDE_GPS == 1 @@ -766,11 +881,12 @@ void loop() { sensors.loop(); #ifdef DISPLAY_CLASS #if defined(ESP32) && defined(WIFI_SSID) && defined(WITH_WEBCONFIG) - if (the_mesh.isWebConfigSetupActive() + if (isCompanionWiFiEnabled() && (the_mesh.isWebConfigSetupActive() #ifdef WITH_MQTT_BRIDGE || !the_mesh.isMQTTConfigured() #endif - ) { + )) { + ui_task.serviceWiFiToggleButton(); renderCompanionSetupDisplay(); } else { ui_task.loop(); @@ -807,6 +923,8 @@ void loop() { #ifdef WITH_WEBCONFIG the_mesh.serviceWebConfig(); #endif + serviceCompanionWiFiState(); + if (companion_wifi_requested && companion_wifi_active) { #ifdef ENABLE_OTA ota_console_loop(); // service the OTA text console (port 5002) #endif @@ -903,5 +1021,6 @@ void loop() { #ifdef WITH_MQTT_BRIDGE the_mesh.serviceMQTT(configured_wifi_ssid, configured_wifi_password); #endif + } #endif } diff --git a/examples/companion_radio/ui-new/UITask.cpp b/examples/companion_radio/ui-new/UITask.cpp index b5bf84a6..61df4244 100644 --- a/examples/companion_radio/ui-new/UITask.cpp +++ b/examples/companion_radio/ui-new/UITask.cpp @@ -1,6 +1,7 @@ #include "UITask.h" #include #include "../MyMesh.h" +#include "../CompanionWiFi.h" #include "target.h" #ifdef WIFI_SSID #include @@ -234,8 +235,12 @@ public: display.drawTextCentered(display.width() / 2, 22, tmp); #ifdef WIFI_SSID - IPAddress ip = WiFi.localIP(); - snprintf(tmp, sizeof(tmp), "IP: %d.%d.%d.%d", ip[0], ip[1], ip[2], ip[3]); + if (isCompanionWiFiEnabled()) { + IPAddress ip = WiFi.localIP(); + snprintf(tmp, sizeof(tmp), "IP: %d.%d.%d.%d", ip[0], ip[1], ip[2], ip[3]); + } else { + strcpy(tmp, "WiFi: OFF"); + } display.setTextSize(1); display.drawTextCentered(display.width() / 2, 54, tmp); #endif @@ -622,6 +627,14 @@ void UITask::begin(DisplayDriver* display, SensorManager* sensors, CompanionNode setCurrScreen(splash); } +void UITask::serviceWiFiToggleButton() { +#if defined(TBEAM_1W) && defined(WIFI_SSID) && defined(PIN_USER_BTN) + if (user_btn.check() == BUTTON_EVENT_TRIPLE_CLICK) { + handleTripleClick(KEY_SELECT); + } +#endif +} + void UITask::showAlert(const char* text, int duration_millis) { strcpy(_alert, text); _alert_expiry = millis() + duration_millis; @@ -1000,7 +1013,13 @@ char UITask::handleDoubleClick(char c) { char UITask::handleTripleClick(char c) { MESH_DEBUG_PRINTLN("UITask: triple click triggered"); checkDisplayOn(c); +#if defined(TBEAM_1W) && defined(WIFI_SSID) + const bool enabled = toggleCompanionWiFi(); + showAlert(enabled ? "WiFi: ON" : "WiFi: OFF", 1200); + _next_refresh = 0; +#else toggleBuzzer(); +#endif c = 0; return c; } diff --git a/examples/companion_radio/ui-new/UITask.h b/examples/companion_radio/ui-new/UITask.h index fc517a88..efcda4f6 100644 --- a/examples/companion_radio/ui-new/UITask.h +++ b/examples/companion_radio/ui-new/UITask.h @@ -80,6 +80,7 @@ public: curr = NULL; } void begin(DisplayDriver* display, SensorManager* sensors, CompanionNodePrefs* node_prefs); + void serviceWiFiToggleButton(); void gotoHomeScreen() { setCurrScreen(home); } void showAlert(const char* text, int duration_millis); diff --git a/src/helpers/WiFiSetupPortal.cpp b/src/helpers/WiFiSetupPortal.cpp index ff446368..9e3a47bf 100644 --- a/src/helpers/WiFiSetupPortal.cpp +++ b/src/helpers/WiFiSetupPortal.cpp @@ -321,6 +321,15 @@ bool WiFiSetupPortal::begin(const char* ap_name, SaveCallback save_callback, voi return true; } +void WiFiSetupPortal::stop() { + _active = false; +} + +bool WiFiSetupPortal::isStopping() const { + const PortalImpl* impl = static_cast(_impl); + return impl && impl->task != nullptr; +} + void WiFiSetupPortal::configureRecovery(const char* ssid, const char* password, uint32_t interval_ms, uint32_t initial_delay_ms) { diff --git a/src/helpers/WiFiSetupPortal.h b/src/helpers/WiFiSetupPortal.h index d5568d0d..dfed50a6 100644 --- a/src/helpers/WiFiSetupPortal.h +++ b/src/helpers/WiFiSetupPortal.h @@ -14,6 +14,7 @@ public: // Starts an open setup AP and captive HTTP form. The AP remains available // after a failed association and shuts down shortly after a successful one. bool begin(const char* ap_name, SaveCallback save_callback, void* context); + void stop(); // Retry interval is the steady-state cadence. initial_delay_ms defaults to // that interval, but callers that opened the portal partway through an // outage can preserve the original reconnect schedule with a shorter delay. @@ -21,6 +22,7 @@ public: uint32_t interval_ms, uint32_t initial_delay_ms = 0); bool isActive() const { return _active; } + bool isStopping() const; IPAddress apIP() const { return IPAddress(192, 168, 4, 1); } // Companion WiFi builds keep their runtime credentials in NVS. diff --git a/src/helpers/esp32/SerialWifiInterface.cpp b/src/helpers/esp32/SerialWifiInterface.cpp index 0f974d17..cc432519 100644 --- a/src/helpers/esp32/SerialWifiInterface.cpp +++ b/src/helpers/esp32/SerialWifiInterface.cpp @@ -7,6 +7,15 @@ void SerialWifiInterface::begin(int port) { server.begin(port); } +void SerialWifiInterface::end() { + disable(); + if (client) client.stop(); + server.end(); + deviceConnected = false; + resetReceivedFrameHeader(); + clearBuffers(); +} + // ---------- public methods void SerialWifiInterface::enable() { if (_isEnabled) return; diff --git a/src/helpers/esp32/SerialWifiInterface.h b/src/helpers/esp32/SerialWifiInterface.h index 8be7beb2..5959e2d9 100644 --- a/src/helpers/esp32/SerialWifiInterface.h +++ b/src/helpers/esp32/SerialWifiInterface.h @@ -45,6 +45,7 @@ public: } void begin(int port); + void end(); // BaseSerialInterface methods void enable() override; diff --git a/test/test_companion_node_prefs/test_companion_node_prefs.cpp b/test/test_companion_node_prefs/test_companion_node_prefs.cpp index ef2b0134..ffc2cf06 100644 --- a/test/test_companion_node_prefs/test_companion_node_prefs.cpp +++ b/test/test_companion_node_prefs/test_companion_node_prefs.cpp @@ -65,6 +65,21 @@ TEST(CompanionNodePrefs, DevicePowerSavingIsIndependentFromRxps) { EXPECT_EQ(1, prefs.powersaving_enabled); } +TEST(CompanionNodePrefs, WiFiStateIsIndependentFromPowerSaving) { + CompanionNodePrefs prefs = {}; + prefs.wifi_enabled = 1; + prefs.powersaving_enabled = 0; + + EXPECT_EQ(1, prefs.wifi_enabled); + EXPECT_EQ(0, prefs.powersaving_enabled); + + prefs.wifi_enabled = 0; + prefs.powersaving_enabled = 1; + + EXPECT_EQ(0, prefs.wifi_enabled); + EXPECT_EQ(1, prefs.powersaving_enabled); +} + #if 0 // Re-enable test once we can SET fem_ values in companion TEST(CompanionNodePrefs, RxGainSettingsRoundTripIndependently) {