From bf2d4e4b9394a1f6da7d2106a12f190ea1734fa4 Mon Sep 17 00:00:00 2001 From: mikecarper Date: Mon, 31 Aug 2026 18:45:44 -0700 Subject: [PATCH] Make Indicator WiFi display authoritative --- examples/companion_radio/CompanionWiFi.h | 15 ++++++++++ examples/companion_radio/MyMesh.cpp | 5 ++++ examples/companion_radio/main.cpp | 33 +++++++++++++++++++++- examples/companion_radio/ui-new/UITask.cpp | 24 ++++++++++++++-- test/test_indicator_render_profile.py | 12 ++++++++ 5 files changed, 86 insertions(+), 3 deletions(-) diff --git a/examples/companion_radio/CompanionWiFi.h b/examples/companion_radio/CompanionWiFi.h index 697aea5be..8cfd06abe 100644 --- a/examples/companion_radio/CompanionWiFi.h +++ b/examples/companion_radio/CompanionWiFi.h @@ -1,5 +1,6 @@ #pragma once +#include #include #ifndef UI_WIFI_SETUP_HOME_PAGE @@ -49,6 +50,20 @@ bool isCompanionWiFiEnabled(); // live. Unlike localIP(), the driver record is cleared on a real disconnect. bool isCompanionWiFiConnected(); +enum class CompanionWiFiDisplayState : uint8_t { + NotRendered, + Setup, + Off, + Ready, + Connecting, +}; + +// Record the branch that was actually drawn. This is exposed through the +// read-only `get display.wifi` terminal diagnostic so a headless test host can +// distinguish network state from a stale or incorrect LCD frame. +void noteCompanionWiFiDisplayState(CompanionWiFiDisplayState state); +void formatCompanionWiFiDisplayStatus(char* reply, size_t reply_size); + // 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/MyMesh.cpp b/examples/companion_radio/MyMesh.cpp index e0641f36a..febbc43f0 100644 --- a/examples/companion_radio/MyMesh.cpp +++ b/examples/companion_radio/MyMesh.cpp @@ -2294,6 +2294,11 @@ bool MyMesh::handleLocalControlCommand(const char* command, char* reply, #endif #if defined(ESP32) && defined(WIFI_SSID) + if (strcmp(command, "get display.wifi") == 0) { + formatCompanionWiFiDisplayStatus(reply, reply_size); + return true; + } + #if defined(COMPANION_EXCLUSIVE_WIFI_BLE) if (strcmp(command, "get companion.transport") == 0) { snprintf(reply, reply_size, "%s", diff --git a/examples/companion_radio/main.cpp b/examples/companion_radio/main.cpp index b94d835ad..73075cd7d 100644 --- a/examples/companion_radio/main.cpp +++ b/examples/companion_radio/main.cpp @@ -822,6 +822,9 @@ void halt() { static unsigned long companion_wifi_credential_reload_at = 0; static bool companion_wifi_power_save_loaded = false; static uint8_t companion_wifi_power_save = mesh::wifi::kDefaultPowerSave; + static CompanionWiFiDisplayState companion_wifi_display_state = + CompanionWiFiDisplayState::NotRendered; + static uint32_t companion_wifi_display_frames = 0; #if defined(COMPANION_EXCLUSIVE_WIFI_BLE) static CompanionTransportMode companion_transport_boot_mode = @@ -1145,12 +1148,40 @@ void halt() { } bool isCompanionWiFiConnected() { - if (!companion_wifi_requested || !companion_wifi_active) return false; + // The display describes the station link, not the lifecycle of the + // Companion TCP services. A valid station association remains the source + // of truth even if service bookkeeping is delayed or being recovered. + if (!companion_wifi_requested) return false; if (WiFi.status() == WL_CONNECTED) return true; wifi_ap_record_t access_point = {}; return esp_wifi_sta_get_ap_info(&access_point) == ESP_OK; } + void noteCompanionWiFiDisplayState(CompanionWiFiDisplayState state) { + companion_wifi_display_state = state; + companion_wifi_display_frames++; + } + + 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", + }; + uint8_t state = static_cast(companion_wifi_display_state); + if (state >= sizeof(names) / sizeof(names[0])) state = 0; + wifi_ap_record_t access_point = {}; + const bool idf_associated = + esp_wifi_sta_get_ap_info(&access_point) == ESP_OK; + snprintf(reply, reply_size, + "> state=%s, frames=%lu, requested=%u, services=%u, " + "wl=%d, idf=%u, IP=%s", + names[state], (unsigned long)companion_wifi_display_frames, + companion_wifi_requested ? 1U : 0U, + companion_wifi_active ? 1U : 0U, (int)WiFi.status(), + idf_associated ? 1U : 0U, + WiFi.localIP().toString().c_str()); + } + bool toggleCompanionWiFi() { #if defined(COMPANION_EXCLUSIVE_WIFI_BLE) const CompanionTransportMode current = getCompanionTransportMode(); diff --git a/examples/companion_radio/ui-new/UITask.cpp b/examples/companion_radio/ui-new/UITask.cpp index 719f4bc55..26def2cbb 100644 --- a/examples/companion_radio/ui-new/UITask.cpp +++ b/examples/companion_radio/ui-new/UITask.cpp @@ -123,6 +123,26 @@ static void drawCompanionWiFiSetupPage(DisplayDriver& display) { char setup_ip[16] = {0}; const bool setup_active = WebConfigServer::getSetupInfo( setup_ssid, sizeof(setup_ssid), setup_ip, sizeof(setup_ip)); + const bool wifi_enabled = isCompanionWiFiEnabled(); + const bool wifi_connected = isCompanionWiFiConnected(); + const CompanionWiFiDisplayState state = setup_active + ? CompanionWiFiDisplayState::Setup + : !wifi_enabled + ? CompanionWiFiDisplayState::Off + : wifi_connected + ? CompanionWiFiDisplayState::Ready + : CompanionWiFiDisplayState::Connecting; + static CompanionWiFiDisplayState previous_state = + CompanionWiFiDisplayState::NotRendered; + + // Frame hashing normally suppresses identical LCD transfers. Explicitly + // invalidate that cache on a network-state transition so a retained + // CONNECTING frame can never survive a READY render. + if (state != previous_state) { + display.clear(); + previous_state = state; + } + noteCompanionWiFiDisplayState(state); // Keep the setup content below the shared title bar and page dots. Compact // text makes its physical size identical on the 480 canvas and the @@ -155,14 +175,14 @@ static void drawCompanionWiFiSetupPage(DisplayDriver& display) { display.drawTextCentered(display.width() / 2, 70, "QR UNAVAILABLE"); display.setColor(UIColor::secondary_txt); display.drawTextCentered(display.width() / 2, 100, setup_ip); - } else if (!isCompanionWiFiEnabled()) { + } else if (!wifi_enabled) { display.setTextSize(3); display.drawTextCentered(display.width() / 2, 45, "WIFI OFF"); display.setTextSize(2); display.setColor(UIColor::secondary_txt); display.drawTextCentered(display.width() / 2, 90, "SELECT WIFI"); display.drawTextCentered(display.width() / 2, 112, "THEN REBOOT"); - } else if (isCompanionWiFiConnected()) { + } else if (wifi_connected) { display.setTextSize(2); display.drawTextCentered(display.width() / 2, 25, "WIFI READY"); const String ssid = WiFi.SSID(); diff --git a/test/test_indicator_render_profile.py b/test/test_indicator_render_profile.py index 8dc71f2b1..d2f89e051 100644 --- a/test/test_indicator_render_profile.py +++ b/test/test_indicator_render_profile.py @@ -178,6 +178,18 @@ int main() { self.assertIn("wifi_setup_active && !_wifi_setup_was_active", ui) self.assertIn("isCompanionWiFiConnected()", page) self.assertIn("wifi_connected != _wifi_was_connected", ui) + self.assertIn("CompanionWiFiDisplayState::Ready", page) + self.assertIn("display.clear()", page) + self.assertIn("noteCompanionWiFiDisplayState(state)", page) + + main = (ROOT / "examples/companion_radio/main.cpp").read_text() + helper = main[main.index("bool isCompanionWiFiConnected()"): + main.index("bool toggleCompanionWiFi()")] + self.assertNotIn("!companion_wifi_active", helper) + self.assertIn("esp_wifi_sta_get_ap_info", helper) + + mesh = (ROOT / "examples/companion_radio/MyMesh.cpp").read_text() + self.assertIn('strcmp(command, "get display.wifi")', mesh) self.assertIn("_task->gotoHomeScreen();", ui) main = MAIN.read_text(encoding="utf-8")