From adcd7961c9d93ef76228d737f3ab097c1b94691e Mon Sep 17 00:00:00 2001 From: Jeremy Klein Date: Mon, 4 May 2026 19:19:00 -0700 Subject: [PATCH] feat: self-heal from transient WiFi loss, add dBm indicator during WebServerActivity (#1780) ## Summary * **What is the goal of this PR?** I was seeing hangs during File Transfer. Troubleshooting via serial showed it had to do with the SHUTTING_DOWN state. Strengthened the WiFi State Machine a bit and added self-healing. Additionally, I found it obnoxious to need to keep referring to serial for my wifi strength, so a dBm meter is added opposing the SSID when on the File Transfer page. * **What changes are included?** The dBm meter was at risk of causing rapid screen updates (if, say, we hovered right around a threshold), so I've implemented a basic hysteresis around this. RISING/FALLING would be more canonical variable names, but are reserved in the framework. ## Additional Context My X4 has a terrible antenna, apparently, and I was running into this failure condition pretty regularly. Wifi "bars" were chosen as a relatively pan-cultural glyph rather than relying on localization. Tested on hardware at -83 dBm under sustained EPUB upload (60 books, ~30 MB). Transient losses up to ~14s now ride through; pre-fix the same losses required a power-cycle after as little as a 2s blip. --- ### AI Usage While CrossPoint doesn't have restrictions on AI tools in contributing, please be transparent about their usage as it helps set the right context for reviewers. Did you use AI tools to help write this code? Nope. Commits are hand written. Claude was used to build a local test harness for validation only during iteration. --- .../network/CrossPointWebServerActivity.cpp | 91 +++++++++++++++++-- .../network/CrossPointWebServerActivity.h | 9 ++ src/network/CrossPointWebServer.cpp | 3 + 3 files changed, 93 insertions(+), 10 deletions(-) diff --git a/src/activities/network/CrossPointWebServerActivity.cpp b/src/activities/network/CrossPointWebServerActivity.cpp index b3cc8642..71f1bf41 100644 --- a/src/activities/network/CrossPointWebServerActivity.cpp +++ b/src/activities/network/CrossPointWebServerActivity.cpp @@ -30,6 +30,16 @@ constexpr int QR_CODE_HEIGHT = 198; // DNS server for captive portal (redirects all DNS queries to our IP) DNSServer* dnsServer = nullptr; constexpr uint16_t DNS_PORT = 53; + +// 0..4 bars from RSSI (dBm), with 3 dBm hysteresis on currentBars to suppress flicker. +int barsForRssi(int rssi, int currentBars) { + static constexpr int RISE_DBM[] = {-85, -75, -65, -55}; + static constexpr int FALL_DBM[] = {-88, -78, -68, -58}; + int bars = std::clamp(currentBars, 0, 4); + while (bars < 4 && rssi >= RISE_DBM[bars]) bars++; + while (bars > 0 && rssi < FALL_DBM[bars - 1]) bars--; + return bars; +} } // namespace void CrossPointWebServerActivity::onEnter() { @@ -247,6 +257,7 @@ void CrossPointWebServerActivity::startWebServer() { if (webServer->isRunning()) { state = WebServerActivityState::SERVER_RUNNING; LOG_DBG("WEBACT", "Web server started successfully"); + lastWifiBars = isApMode ? 0 : barsForRssi(WiFi.RSSI(), 0); // Force an immediate render since we're transitioning from a subactivity // that had its own rendering task. We need to make sure our display is shown. @@ -282,18 +293,42 @@ void CrossPointWebServerActivity::loop() { if (millis() - lastWifiCheck > 2000) { // Check every 2 seconds lastWifiCheck = millis(); const wl_status_t wifiStatus = WiFi.status(); + // Driver auto-reconnect handles retries; abandon (via onGoHome) only + // after WIFI_ABANDON_MS, otherwise the activity freezes on a blip. + bool repaint = false; if (wifiStatus != WL_CONNECTED) { - LOG_DBG("WEBACT", "WiFi disconnected! Status: %d", wifiStatus); - // Show error and exit gracefully - state = WebServerActivityState::SHUTTING_DOWN; - requestUpdate(); - return; - } - // Log weak signal warnings - const int rssi = WiFi.RSSI(); - if (rssi < -75) { - LOG_DBG("WEBACT", "Warning: Weak WiFi signal: %d dBm", rssi); + if (consecutiveDisconnects == 0) { + firstDisconnectAt = millis(); + repaint = true; + } + consecutiveDisconnects++; + LOG_DBG("WEBACT", "WiFi not connected (status=%d, consecutive=%d, total=%lu ms)", wifiStatus, + consecutiveDisconnects, millis() - firstDisconnectAt); + if (millis() - firstDisconnectAt > WIFI_ABANDON_MS) { + LOG_DBG("WEBACT", "WiFi unavailable for >%lu s; returning to network selection", WIFI_ABANDON_MS / 1000UL); + state = WebServerActivityState::SHUTTING_DOWN; + onGoHome(); + return; + } + } else { + if (consecutiveDisconnects > 0) { + LOG_DBG("WEBACT", "WiFi recovered after %d failed checks (%lu ms)", consecutiveDisconnects, + millis() - firstDisconnectAt); + repaint = true; + } + consecutiveDisconnects = 0; + firstDisconnectAt = 0; + const int rssi = WiFi.RSSI(); + if (rssi < -75) { + LOG_DBG("WEBACT", "Warning: Weak WiFi signal: %d dBm", rssi); + } + const int bars = barsForRssi(rssi, lastWifiBars); + if (bars != lastWifiBars) { + lastWifiBars = bars; + repaint = true; + } } + if (repaint) requestUpdate(); } } @@ -376,6 +411,10 @@ void CrossPointWebServerActivity::renderServerRunning() const { GUI.drawSubHeader(renderer, Rect{0, metrics.topPadding + metrics.headerHeight, pageWidth, metrics.tabBarHeight}, connectedSSID.c_str()); + if (!isApMode) { + renderWifiIndicator(metrics.topPadding + metrics.headerHeight); + } + int startY = metrics.topPadding + metrics.headerHeight + metrics.tabBarHeight + metrics.verticalSpacing * 2; int height10 = renderer.getLineHeight(UI_10_FONT_ID); if (isApMode) { @@ -440,3 +479,35 @@ void CrossPointWebServerActivity::renderServerRunning() const { const auto labels = mappedInput.mapLabels(tr(STR_EXIT), "", "", ""); GUI.drawButtonHints(renderer, labels.btn1, labels.btn2, labels.btn3, labels.btn4); } + +void CrossPointWebServerActivity::renderWifiIndicator(int subHeaderTop) const { + constexpr int BAR_COUNT = 4; + constexpr int BAR_WIDTH = 4; + constexpr int BAR_GAP = 2; + constexpr int ICON_HEIGHT = 14; + const auto& metrics = UITheme::getInstance().getMetrics(); + const int iconWidth = BAR_COUNT * BAR_WIDTH + (BAR_COUNT - 1) * BAR_GAP; + const int iconRight = renderer.getScreenWidth() - metrics.contentSidePadding; + const int iconLeft = iconRight - iconWidth; + const int iconBottom = subHeaderTop + metrics.tabBarHeight - metrics.verticalSpacing; + + const bool wifiUp = (WiFi.status() == WL_CONNECTED) && (consecutiveDisconnects == 0); + if (wifiUp) { + for (int i = 0; i < BAR_COUNT; i++) { + const int barHeight = (i + 1) * ICON_HEIGHT / BAR_COUNT; + const int x = iconLeft + i * (BAR_WIDTH + BAR_GAP); + const int y = iconBottom - barHeight; + if (i < lastWifiBars) { + renderer.fillRect(x, y, BAR_WIDTH, barHeight, true); + } else { + renderer.drawRect(x, y, BAR_WIDTH, barHeight, true); + } + } + } else { + const int xSize = ICON_HEIGHT; + const int x0 = iconRight - xSize; + const int y0 = iconBottom - xSize; + renderer.drawLine(x0, y0, x0 + xSize, y0 + xSize, 2, true); + renderer.drawLine(x0, y0 + xSize, x0 + xSize, y0, 2, true); + } +} diff --git a/src/activities/network/CrossPointWebServerActivity.h b/src/activities/network/CrossPointWebServerActivity.h index 5069f199..bc685c6b 100644 --- a/src/activities/network/CrossPointWebServerActivity.h +++ b/src/activities/network/CrossPointWebServerActivity.h @@ -44,7 +44,16 @@ class CrossPointWebServerActivity final : public Activity { // Performance monitoring unsigned long lastHandleClientTime = 0; + // Sustained WiFi-loss tracking; abandon only after WIFI_ABANDON_MS. + int consecutiveDisconnects = 0; + unsigned long firstDisconnectAt = 0; + static constexpr unsigned long WIFI_ABANDON_MS = 5UL * 60UL * 1000UL; + + // Cached signal-strength bracket (0..4) for the WiFi indicator. + int lastWifiBars = 0; + void renderServerRunning() const; + void renderWifiIndicator(int subHeaderTop) const; void onNetworkModeSelected(NetworkMode mode); void onWifiSelectionComplete(bool connected); diff --git a/src/network/CrossPointWebServer.cpp b/src/network/CrossPointWebServer.cpp index 2346f221..713981ea 100644 --- a/src/network/CrossPointWebServer.cpp +++ b/src/network/CrossPointWebServer.cpp @@ -119,6 +119,9 @@ void CrossPointWebServer::begin() { // Disable WiFi sleep to improve responsiveness and prevent 'unreachable' errors. // This is critical for reliable web server operation on ESP32. WiFi.setSleep(false); + // Default varies by ESP32 core version. The activity's loss-recovery loop + // relies on driver retries during transient disconnects. + WiFi.setAutoReconnect(true); // Note: WebServer class doesn't have setNoDelay() in the standard ESP32 library. // We rely on disabling WiFi sleep for responsiveness.