diff --git a/docs/webserver.md b/docs/webserver.md index 9af95674..e42f3237 100644 --- a/docs/webserver.md +++ b/docs/webserver.md @@ -32,9 +32,14 @@ networks or in hotspot mode when you control who is connected. ## Join Network Mode 1. Select **Join Network**. -2. Pick a 2.4 GHz Wi-Fi network from the scan results. -3. Enter the password if prompted. -4. Save credentials if you want the reader to reconnect automatically next time. +2. If you have saved Wi-Fi credentials, CrossPoint first tries the last + connected network, then other visible saved networks in signal-strength + order. Press **Back** to cancel or **Confirm** to stop auto-connect and show + the network list. +3. If the network list is shown, pick a 2.4 GHz Wi-Fi network from the scan + results. +4. Enter the password if prompted. +5. Save credentials if you want the reader to reconnect automatically next time. After connection, the reader shows: diff --git a/lib/I18n/translations/english.yaml b/lib/I18n/translations/english.yaml index e636cbe3..43d61ff2 100644 --- a/lib/I18n/translations/english.yaml +++ b/lib/I18n/translations/english.yaml @@ -29,7 +29,10 @@ STR_WIFI_NETWORKS: "Wi-Fi Networks" STR_NO_NETWORKS: "No networks found" STR_NETWORKS_FOUND: "%zu networks found" STR_SCANNING: "Scanning..." +STR_FINDING_SAVED_WIFI: "Finding saved Wi-Fi..." STR_CONNECTING: "Connecting..." +STR_CONNECTING_SAVED_WIFI: "Connecting to saved Wi-Fi..." +STR_SHOW_NETWORKS: "Show" STR_CONNECTED: "Connected!" STR_CONNECTION_FAILED: "Connection Failed" STR_FORGET_NETWORK: "Forget Network?" diff --git a/src/activities/network/WifiSelectionActivity.cpp b/src/activities/network/WifiSelectionActivity.cpp index 9d473699..50b3c88d 100644 --- a/src/activities/network/WifiSelectionActivity.cpp +++ b/src/activities/network/WifiSelectionActivity.cpp @@ -6,6 +6,8 @@ #include #include +#include + #include "CrossPointSettings.h" #include "MappedInputManager.h" #include "WifiCredentialStore.h" @@ -35,6 +37,9 @@ void WifiSelectionActivity::onEnter() { savePromptSelection = 0; forgetPromptSelection = 0; autoConnecting = false; + manualNetworkListRequested = false; + autoAttemptedSsids.clear(); + autoAttemptedSsids.reserve(WIFI_STORE.getCredentials().size()); // Cache MAC address for display uint8_t mac[6]; @@ -47,23 +52,20 @@ void WifiSelectionActivity::onEnter() { // Trigger first update to show scanning message requestUpdate(); - // Attempt to auto-connect to the last network - if (allowAutoConnect) { + // Attempt to auto-connect to known networks. Try the last successful + // network first for speed, then scan and try any visible saved networks by + // signal strength. The user can interrupt this and show the scan result. + if (allowAutoConnect && !WIFI_STORE.getCredentials().empty()) { const std::string lastSsid = WIFI_STORE.getLastConnectedSsid(); if (!lastSsid.empty()) { const auto* cred = WIFI_STORE.findCredential(lastSsid); - if (cred) { - LOG_DBG("WIFI", "Attempting to auto-connect to %s", lastSsid.c_str()); - selectedSSID = cred->ssid; - enteredPassword = cred->password; - selectedRequiresPassword = !cred->password.empty(); - usedSavedPassword = true; - autoConnecting = true; - attemptConnection(); - requestUpdate(); + if (cred && tryAutoConnectCredential(*cred)) { return; } } + + startWifiScan(true); + return; } // Fallback to scanning @@ -87,8 +89,9 @@ void WifiSelectionActivity::onExit() { LOG_DBG("WIFI", "Free heap at onExit end: %d bytes", ESP.getFreeHeap()); } -void WifiSelectionActivity::startWifiScan() { - autoConnecting = false; +void WifiSelectionActivity::startWifiScan(const bool autoScan) { + autoConnecting = autoScan; + manualNetworkListRequested = false; state = WifiSelectionState::SCANNING; networks.clear(); requestUpdate(); @@ -111,6 +114,8 @@ void WifiSelectionActivity::processWifiScanResults() { } if (scanResult == WIFI_SCAN_FAILED) { + autoConnecting = false; + manualNetworkListRequested = false; state = WifiSelectionState::NETWORK_LIST; requestUpdate(); return; @@ -154,6 +159,13 @@ void WifiSelectionActivity::processWifiScanResults() { }); WiFi.scanDelete(); + + if (autoConnecting && !manualNetworkListRequested && tryNextSavedNetworkFromScan()) { + return; + } + + autoConnecting = false; + manualNetworkListRequested = false; state = WifiSelectionState::NETWORK_LIST; selectedNetworkIndex = 0; requestUpdate(); @@ -204,6 +216,76 @@ void WifiSelectionActivity::selectNetwork(const int index) { } } +bool WifiSelectionActivity::hasAttemptedAutoSsid(const std::string& ssid) const { + return std::find(autoAttemptedSsids.begin(), autoAttemptedSsids.end(), ssid) != autoAttemptedSsids.end(); +} + +bool WifiSelectionActivity::tryAutoConnectCredential(const WifiCredential& cred) { + if (hasAttemptedAutoSsid(cred.ssid)) { + return false; + } + + LOG_DBG("WIFI", "Attempting saved network: %s", cred.ssid.c_str()); + autoAttemptedSsids.push_back(cred.ssid); + selectedSSID = cred.ssid; + enteredPassword = cred.password; + selectedRequiresPassword = !cred.password.empty(); + usedSavedPassword = true; + autoConnecting = true; + manualNetworkListRequested = false; + attemptConnection(); + requestUpdate(); + return true; +} + +bool WifiSelectionActivity::tryNextSavedNetworkFromScan() { + for (const auto& network : networks) { + if (!network.hasSavedPassword || hasAttemptedAutoSsid(network.ssid)) { + continue; + } + + const auto* cred = WIFI_STORE.findCredential(network.ssid); + if (cred && tryAutoConnectCredential(*cred)) { + return true; + } + } + return false; +} + +void WifiSelectionActivity::handleAutoConnectFailure() { + LOG_DBG("WIFI", "Saved network failed: %s", selectedSSID.c_str()); + WiFi.disconnect(); + + if (!networks.empty()) { + if (tryNextSavedNetworkFromScan()) { + return; + } + autoConnecting = false; + state = WifiSelectionState::NETWORK_LIST; + selectedNetworkIndex = 0; + requestUpdate(); + return; + } + + startWifiScan(true); +} + +void WifiSelectionActivity::showNetworkListFromAutoConnect() { + LOG_DBG("WIFI", "User requested manual network list"); + WiFi.disconnect(); + autoConnecting = false; + manualNetworkListRequested = true; + + if (networks.empty()) { + startWifiScan(false); + return; + } + + state = WifiSelectionState::NETWORK_LIST; + selectedNetworkIndex = 0; + requestUpdate(); +} + void WifiSelectionActivity::attemptConnection() { state = autoConnecting ? WifiSelectionState::AUTO_CONNECTING : WifiSelectionState::CONNECTING; connectionStartTime = millis(); @@ -282,15 +364,24 @@ void WifiSelectionActivity::checkConnectionStatus() { if (status == WL_NO_SSID_AVAIL) { connectionError = tr(STR_ERROR_NETWORK_NOT_FOUND); } + if (autoConnecting) { + handleAutoConnectFailure(); + return; + } state = WifiSelectionState::CONNECTION_FAILED; requestUpdate(); return; } // Check for timeout - if (millis() - connectionStartTime > CONNECTION_TIMEOUT_MS) { + const unsigned long timeoutMs = autoConnecting ? AUTO_CONNECTION_TIMEOUT_MS : CONNECTION_TIMEOUT_MS; + if (millis() - connectionStartTime > timeoutMs) { WiFi.disconnect(); connectionError = tr(STR_ERROR_CONNECTION_TIMEOUT); + if (autoConnecting) { + handleAutoConnectFailure(); + return; + } state = WifiSelectionState::CONNECTION_FAILED; requestUpdate(); return; @@ -300,12 +391,33 @@ void WifiSelectionActivity::checkConnectionStatus() { void WifiSelectionActivity::loop() { // Check scan progress if (state == WifiSelectionState::SCANNING) { + if (mappedInput.wasPressed(MappedInputManager::Button::Back)) { + WiFi.scanDelete(); + onComplete(false); + return; + } + if (autoConnecting && mappedInput.wasPressed(MappedInputManager::Button::Confirm)) { + autoConnecting = false; + manualNetworkListRequested = true; + requestUpdate(); + } processWifiScanResults(); return; } // Check connection progress if (state == WifiSelectionState::CONNECTING || state == WifiSelectionState::AUTO_CONNECTING) { + if (state == WifiSelectionState::AUTO_CONNECTING) { + if (mappedInput.wasPressed(MappedInputManager::Button::Back)) { + WiFi.disconnect(); + onComplete(false); + return; + } + if (mappedInput.wasPressed(MappedInputManager::Button::Confirm)) { + showNetworkListFromAutoConnect(); + return; + } + } checkConnectionStatus(); return; } @@ -559,9 +671,15 @@ void WifiSelectionActivity::renderConnecting(const Rect* screen, const ThemeMetr const auto top = screen->y + (screen->height - height) / 2; if (state == WifiSelectionState::SCANNING) { - UITheme::drawCenteredText(renderer, *screen, UI_10_FONT_ID, top, tr(STR_SCANNING)); + UITheme::drawCenteredText(renderer, *screen, UI_10_FONT_ID, top, + autoConnecting ? tr(STR_FINDING_SAVED_WIFI) : tr(STR_SCANNING)); + if (autoConnecting) { + const auto labels = mappedInput.mapLabels(tr(STR_CANCEL), tr(STR_SHOW_NETWORKS), "", ""); + GUI.drawButtonHints(renderer, labels.btn1, labels.btn2, labels.btn3, labels.btn4); + } } else { - UITheme::drawCenteredText(renderer, *screen, UI_12_FONT_ID, top - 40, tr(STR_CONNECTING), true, + UITheme::drawCenteredText(renderer, *screen, UI_12_FONT_ID, top - 40, + autoConnecting ? tr(STR_CONNECTING_SAVED_WIFI) : tr(STR_CONNECTING), true, EpdFontFamily::BOLD); std::string ssidInfo = std::string(tr(STR_TO_PREFIX)) + selectedSSID; @@ -569,6 +687,10 @@ void WifiSelectionActivity::renderConnecting(const Rect* screen, const ThemeMetr ssidInfo.replace(22, ssidInfo.length() - 22, "..."); } UITheme::drawCenteredText(renderer, *screen, UI_10_FONT_ID, top, ssidInfo.c_str()); + if (autoConnecting) { + const auto labels = mappedInput.mapLabels(tr(STR_CANCEL), tr(STR_SHOW_NETWORKS), "", ""); + GUI.drawButtonHints(renderer, labels.btn1, labels.btn2, labels.btn3, labels.btn4); + } } } diff --git a/src/activities/network/WifiSelectionActivity.h b/src/activities/network/WifiSelectionActivity.h index 4b534d17..43eecf08 100644 --- a/src/activities/network/WifiSelectionActivity.h +++ b/src/activities/network/WifiSelectionActivity.h @@ -11,6 +11,7 @@ struct Rect; struct ThemeMetrics; +struct WifiCredential; // Structure to hold WiFi network information struct WifiNetworkInfo { @@ -71,15 +72,22 @@ class WifiSelectionActivity final : public Activity { // Whether to attempt auto-connect on entry const bool allowAutoConnect; - // Whether we are attempting to auto-connect + // Whether we are attempting to auto-connect or auto-scan saved networks. bool autoConnecting = false; + // True when the user stopped auto-connect and asked to see the scan result. + bool manualNetworkListRequested = false; + + // Saved SSIDs already attempted during the current auto-connect session. + std::vector autoAttemptedSsids; + // Save/forget prompt selection (0 = Yes, 1 = No) int savePromptSelection = 0; int forgetPromptSelection = 0; // Connection timeout static constexpr unsigned long CONNECTION_TIMEOUT_MS = 15000; + static constexpr unsigned long AUTO_CONNECTION_TIMEOUT_MS = 7000; unsigned long connectionStartTime = 0; void renderNetworkList(const Rect* screen, const ThemeMetrics* metrics) const; @@ -90,11 +98,16 @@ class WifiSelectionActivity final : public Activity { void renderConnectionFailed(const Rect* screen, const ThemeMetrics* metrics) const; void renderForgetPrompt(const Rect* screen, const ThemeMetrics* metrics) const; - void startWifiScan(); + void startWifiScan(bool autoScan = false); void processWifiScanResults(); void selectNetwork(int index); void attemptConnection(); void checkConnectionStatus(); + bool tryAutoConnectCredential(const WifiCredential& cred); + bool tryNextSavedNetworkFromScan(); + void handleAutoConnectFailure(); + void showNetworkListFromAutoConnect(); + bool hasAttemptedAutoSsid(const std::string& ssid) const; std::string getSignalStrengthIndicator(int32_t rssi) const; void onComplete(bool connected);