diff --git a/examples/companion_radio/CompanionWiFi.h b/examples/companion_radio/CompanionWiFi.h index 8cfd06ab..9aaeeb1e 100644 --- a/examples/companion_radio/CompanionWiFi.h +++ b/examples/companion_radio/CompanionWiFi.h @@ -45,6 +45,7 @@ enum class CompanionWiFiPowerSaveResult : uint8_t { // from the main loop so a button callback never tears down an active server. bool toggleCompanionWiFi(); bool isCompanionWiFiEnabled(); +bool hasCompanionWiFiCredentials(); // Display-facing station state. Arduino's cached WL status can briefly lag the // ESP-IDF association record, so accept either source while the AP link is // live. Unlike localIP(), the driver record is cleared on a real disconnect. @@ -55,6 +56,7 @@ enum class CompanionWiFiDisplayState : uint8_t { Setup, Off, Ready, + NotConfigured, Connecting, }; @@ -64,6 +66,11 @@ enum class CompanionWiFiDisplayState : uint8_t { void noteCompanionWiFiDisplayState(CompanionWiFiDisplayState state); void formatCompanionWiFiDisplayStatus(char* reply, size_t reply_size); +// A display callback only queues this request. The main loop owns the actual +// WebConfig/WiFi transition so touch handling never tears down network services +// from the UI task. +void requestCompanionWiFiSetup(); + // Reload saved credentials after a text-terminal update. The reconnect is // deferred so the command reply can leave USB/TCP before WiFi is restarted. void scheduleCompanionWiFiCredentialReload(); diff --git a/examples/companion_radio/main.cpp b/examples/companion_radio/main.cpp index 515d04ff..be9f6ecb 100644 --- a/examples/companion_radio/main.cpp +++ b/examples/companion_radio/main.cpp @@ -818,6 +818,7 @@ void halt() { static bool companion_wifi_active = false; static bool companion_wifi_disable_in_progress = false; static bool companion_wifi_services_stopped = false; + static volatile bool companion_wifi_setup_requested = false; static bool companion_wifi_credential_reload_pending = false; static unsigned long companion_wifi_credential_reload_at = 0; static bool companion_wifi_power_save_loaded = false; @@ -1149,6 +1150,10 @@ void halt() { return companion_wifi_requested; } + bool hasCompanionWiFiCredentials() { + return companion_wifi_has_credentials; + } + bool isCompanionWiFiConnected() { // The display describes the station link, not the lifecycle of the // Companion TCP services. A valid station association remains the source @@ -1167,7 +1172,8 @@ void halt() { void formatCompanionWiFiDisplayStatus(char* reply, size_t reply_size) { if (reply == nullptr || reply_size == 0) return; static const char* const names[] = { - "not-rendered", "setup", "off", "ready", "connecting", + "not-rendered", "setup", "off", "ready", "not-configured", + "connecting", }; uint8_t state = static_cast(companion_wifi_display_state); if (state >= sizeof(names) / sizeof(names[0])) state = 0; @@ -1186,6 +1192,10 @@ void halt() { WiFi.localIP().toString().c_str()); } + void requestCompanionWiFiSetup() { + companion_wifi_setup_requested = true; + } + bool toggleCompanionWiFi() { #if defined(COMPANION_EXCLUSIVE_WIFI_BLE) const CompanionTransportMode current = getCompanionTransportMode(); @@ -2156,6 +2166,15 @@ void loop() { #if defined(ESP32) && defined(WIFI_SSID) #ifdef WITH_WEBCONFIG the_mesh.serviceWebConfig(); + if (companion_wifi_setup_requested) { + companion_wifi_setup_requested = false; + if (companion_wifi_requested) { + char web_reply[160]; + if (!the_mesh.startWebConfig(true, web_reply)) { + WIFI_DEBUG_PRINTLN("Display WiFi setup request: %s", web_reply); + } + } + } #endif serviceCompanionWiFiCredentialReload(); serviceCompanionWiFiState(); diff --git a/examples/companion_radio/ui-new/UITask.cpp b/examples/companion_radio/ui-new/UITask.cpp index 26def2cb..a6944b8a 100644 --- a/examples/companion_radio/ui-new/UITask.cpp +++ b/examples/companion_radio/ui-new/UITask.cpp @@ -125,13 +125,16 @@ static void drawCompanionWiFiSetupPage(DisplayDriver& display) { setup_ssid, sizeof(setup_ssid), setup_ip, sizeof(setup_ip)); const bool wifi_enabled = isCompanionWiFiEnabled(); const bool wifi_connected = isCompanionWiFiConnected(); + const bool wifi_configured = hasCompanionWiFiCredentials(); const CompanionWiFiDisplayState state = setup_active ? CompanionWiFiDisplayState::Setup : !wifi_enabled ? CompanionWiFiDisplayState::Off : wifi_connected ? CompanionWiFiDisplayState::Ready - : CompanionWiFiDisplayState::Connecting; + : !wifi_configured + ? CompanionWiFiDisplayState::NotConfigured + : CompanionWiFiDisplayState::Connecting; static CompanionWiFiDisplayState previous_state = CompanionWiFiDisplayState::NotRendered; @@ -189,6 +192,12 @@ static void drawCompanionWiFiSetupPage(DisplayDriver& display) { display.drawTextEllipsized(4, 60, display.width() - 8, ssid.c_str()); const String ip = WiFi.localIP().toString(); display.drawTextCentered(display.width() / 2, 95, ip.c_str()); + } else if (!wifi_configured) { + display.setTextSize(3); + display.drawTextCentered(display.width() / 2, 45, "WIFI SETUP"); + display.setTextSize(2); + display.setColor(UIColor::secondary_txt); + display.drawTextCentered(display.width() / 2, 90, "TAP TO START"); } else { display.setTextSize(3); display.drawTextCentered(display.width() / 2, 45, "WIFI"); @@ -558,12 +567,16 @@ public: "tap center: inbox"); #ifdef WIFI_SSID - if (isCompanionWiFiEnabled()) { + if (!isCompanionWiFiEnabled()) { + strcpy(tmp, "WiFi: OFF"); + } else if (isCompanionWiFiConnected()) { IPAddress ip = WiFi.localIP(); snprintf(tmp, sizeof(tmp), "IP: %d.%d.%d.%d", ip[0], ip[1], ip[2], ip[3]); + } else if (hasCompanionWiFiCredentials()) { + strcpy(tmp, "WiFi: CONNECTING"); } else { - strcpy(tmp, "WiFi: OFF"); + strcpy(tmp, "WiFi: SETUP"); } display.setTextSize(1); mesh::ui::drawTextCenteredEllipsized( @@ -607,14 +620,20 @@ public: mesh::ui::drawTextCenteredEllipsized( display, layout.pairing, layout.pairing_label_y, "TAP"); #ifdef WIFI_SSID - if (isCompanionWiFiEnabled()) { + if (!isCompanionWiFiEnabled()) { + strcpy(tmp, "OFF"); + display.setTextSize(3); + } else if (isCompanionWiFiConnected()) { IPAddress ip = WiFi.localIP(); snprintf(tmp, sizeof(tmp), "IP: %d.%d.%d.%d", ip[0], ip[1], ip[2], ip[3]); display.setTextSize(1); - } else { - strcpy(tmp, "OFF"); + } else if (hasCompanionWiFiCredentials()) { + strcpy(tmp, "WAIT"); display.setTextSize(3); + } else { + strcpy(tmp, "SETUP"); + display.setTextSize(2); } mesh::ui::drawTextCenteredEllipsized( display, layout.pairing, layout.pairing_value_y, tmp); @@ -677,12 +696,16 @@ public: #endif #ifdef WIFI_SSID - if (isCompanionWiFiEnabled()) { + if (!isCompanionWiFiEnabled()) { + strcpy(tmp, "WiFi: OFF"); + } else if (isCompanionWiFiConnected()) { IPAddress ip = WiFi.localIP(); snprintf(tmp, sizeof(tmp), "IP: %d.%d.%d.%d", ip[0], ip[1], ip[2], ip[3]); + } else if (hasCompanionWiFiCredentials()) { + strcpy(tmp, "WiFi: CONNECTING"); } else { - strcpy(tmp, "WiFi: OFF"); + strcpy(tmp, "WiFi: SETUP"); } display.setTextSize(1); display.drawTextCentered(display.width() / 2, 54, tmp); @@ -1026,6 +1049,16 @@ public: _task->showMessages(); return true; } +#endif +#if UI_WIFI_SETUP_HOME_PAGE == 1 + if (c == KEY_ENTER && _page == HomePage::WIFI_SETUP) { + if (isCompanionWiFiEnabled() && !isCompanionWiFiConnected() + && !WebConfigServer::getSetupInfo(nullptr, 0, nullptr, 0)) { + requestCompanionWiFiSetup(); + _task->showAlert("Starting WiFi setup", 1000); + } + return true; + } #endif if (c == KEY_ENTER && _page == HomePage::ADVERT) { _task->notify(UIEventType::ack); diff --git a/test/test_indicator_render_profile.py b/test/test_indicator_render_profile.py index d2f89e05..f700079a 100644 --- a/test/test_indicator_render_profile.py +++ b/test/test_indicator_render_profile.py @@ -177,6 +177,9 @@ int main() { self.assertIn("if (_page == HomePage::WIFI_SETUP) return 1000;", ui) self.assertIn("wifi_setup_active && !_wifi_setup_was_active", ui) self.assertIn("isCompanionWiFiConnected()", page) + self.assertIn("hasCompanionWiFiCredentials()", page) + self.assertIn("CompanionWiFiDisplayState::NotConfigured", page) + self.assertIn('"TAP TO START"', page) self.assertIn("wifi_connected != _wifi_was_connected", ui) self.assertIn("CompanionWiFiDisplayState::Ready", page) self.assertIn("display.clear()", page) @@ -191,6 +194,9 @@ int main() { mesh = (ROOT / "examples/companion_radio/MyMesh.cpp").read_text() self.assertIn('strcmp(command, "get display.wifi")', mesh) self.assertIn("_task->gotoHomeScreen();", ui) + self.assertIn("requestCompanionWiFiSetup();", ui) + self.assertIn('strcpy(tmp, "WiFi: SETUP");', ui) + self.assertIn('strcpy(tmp, "SETUP");', ui) main = MAIN.read_text(encoding="utf-8") loop = main[main.index("\nvoid loop()") :] @@ -199,6 +205,8 @@ int main() { self.assertIn("&& UI_WIFI_SETUP_HOME_PAGE != 1", display_dispatch) self.assertIn("renderCompanionSetupDisplay();", display_dispatch) self.assertIn("#else\n ui_task.loop();", display_dispatch) + self.assertIn('"not-configured"', main) + self.assertIn("companion_wifi_setup_requested", loop) def test_wifi_qr_payload_is_standard_and_escaped(self): setup = WIFI_QR_PAYLOAD.read_text(encoding="utf-8")