diff --git a/src/activities/network/WifiSelectionActivity.cpp b/src/activities/network/WifiSelectionActivity.cpp index 1c45441b..0046b862 100644 --- a/src/activities/network/WifiSelectionActivity.cpp +++ b/src/activities/network/WifiSelectionActivity.cpp @@ -70,6 +70,9 @@ void WifiSelectionActivity::onEnter() { savePromptSelection = 0; forgetPromptSelection = 0; autoConnecting = false; + autoCycleCandidates.clear(); + autoCycleCandidateIndex = 0; + autoCycleAfterScan = false; const std::string persistedMac = formatMacDashed(mac); if (WIFI_STORE.getLastKnownMacAddress() != persistedMac) { @@ -122,6 +125,7 @@ void WifiSelectionActivity::onExit() { void WifiSelectionActivity::startWifiScan() { autoConnecting = false; + // autoCycleAfterScan intentionally preserved when set by the auto-cycle flow state = WifiSelectionState::SCANNING; networks.clear(); requestUpdate(); @@ -135,6 +139,81 @@ void WifiSelectionActivity::startWifiScan() { WiFi.scanNetworks(true); // true = async scan } +void WifiSelectionActivity::buildAutoCycleCandidates() { + autoCycleCandidates.clear(); + autoCycleCandidateIndex = 0; + + const std::string& skipSsid = WIFI_STORE.getLastConnectedSsid(); // already tried + + struct Candidate { + std::string ssid; + int32_t rssi; + }; + std::vector candidates; + + for (const auto& net : networks) { + if (net.ssid == skipSsid) continue; + if (!WIFI_STORE.hasSavedCredential(net.ssid)) continue; + candidates.push_back({net.ssid, net.rssi}); + } + + std::sort(candidates.begin(), candidates.end(), + [](const Candidate& a, const Candidate& b) { return a.rssi > b.rssi; }); + + std::transform(candidates.begin(), candidates.end(), std::back_inserter(autoCycleCandidates), + [](const Candidate& c) { return c.ssid; }); + + LOG_DBG("WIFI", "Auto-cycle candidates: %zu", autoCycleCandidates.size()); +} + +void WifiSelectionActivity::tryNextAutoCycleCandidate() { + if (autoCycleCandidateIndex >= autoCycleCandidates.size()) { + // All candidates exhausted — fall through to manual selection + LOG_DBG("WIFI", "Auto-cycle exhausted, falling through to network list"); + state = WifiSelectionState::NETWORK_LIST; + selectedNetworkIndex = 0; + requestUpdate(); + return; + } + + const std::string& ssid = autoCycleCandidates[autoCycleCandidateIndex++]; + const auto* cred = WIFI_STORE.findCredential(ssid); + if (!cred) { + tryNextAutoCycleCandidate(); // Credential disappeared, skip + return; + } + + LOG_DBG("WIFI", "Auto-cycle trying %s (%zu/%zu)", ssid.c_str(), autoCycleCandidateIndex, autoCycleCandidates.size()); + + selectedSSID = cred->ssid; + enteredPassword = cred->password; + selectedRequiresPassword = !cred->password.empty(); + usedSavedPassword = true; + autoConnecting = false; + + state = WifiSelectionState::AUTO_CYCLING; + connectionStartTime = millis(); + connectedIP.clear(); + connectionError.clear(); + requestUpdate(); + + WiFi.persistent(false); + WiFi.mode(WIFI_STA); + WiFi.disconnect(true, true); + delay(100); + + uint8_t baseMac[6]; + readDeviceBaseMac(baseMac); + String hostname = "CrossPoint-Reader-" + formatMacCompact(baseMac); + WiFi.setHostname(hostname.c_str()); + + if (selectedRequiresPassword && !enteredPassword.empty()) { + WiFi.begin(selectedSSID.c_str(), enteredPassword.c_str()); + } else { + WiFi.begin(selectedSSID.c_str()); + } +} + void WifiSelectionActivity::processWifiScanResults() { const int16_t scanResult = WiFi.scanComplete(); @@ -144,6 +223,7 @@ void WifiSelectionActivity::processWifiScanResults() { } if (scanResult == WIFI_SCAN_FAILED) { + autoCycleAfterScan = false; state = WifiSelectionState::NETWORK_LIST; requestUpdate(); return; @@ -191,6 +271,14 @@ void WifiSelectionActivity::processWifiScanResults() { }); WiFi.scanDelete(); + + if (autoCycleAfterScan) { + autoCycleAfterScan = false; + buildAutoCycleCandidates(); + tryNextAutoCycleCandidate(); + return; + } + state = WifiSelectionState::NETWORK_LIST; selectedNetworkIndex = 0; requestUpdate(); @@ -296,7 +384,8 @@ bool WifiSelectionActivity::checkCaptivePortal() { } void WifiSelectionActivity::checkConnectionStatus() { - if (state != WifiSelectionState::CONNECTING && state != WifiSelectionState::AUTO_CONNECTING) { + if (state != WifiSelectionState::CONNECTING && state != WifiSelectionState::AUTO_CONNECTING && + state != WifiSelectionState::AUTO_CYCLING) { return; } @@ -340,7 +429,20 @@ void WifiSelectionActivity::checkConnectionStatus() { return; } + const unsigned long timeout = + state == WifiSelectionState::AUTO_CYCLING ? AUTO_CYCLE_TIMEOUT_MS : CONNECTION_TIMEOUT_MS; + if (status == WL_CONNECT_FAILED || status == WL_NO_SSID_AVAIL) { + if (state == WifiSelectionState::AUTO_CONNECTING) { + // Primary SSID failed — scan and try remaining saved credentials + autoCycleAfterScan = true; + startWifiScan(); + return; + } + if (state == WifiSelectionState::AUTO_CYCLING) { + tryNextAutoCycleCandidate(); + return; + } connectionError = tr(STR_ERROR_GENERAL_FAILURE); if (status == WL_NO_SSID_AVAIL) { connectionError = tr(STR_ERROR_NETWORK_NOT_FOUND); @@ -351,8 +453,17 @@ void WifiSelectionActivity::checkConnectionStatus() { } // Check for timeout - if (millis() - connectionStartTime > CONNECTION_TIMEOUT_MS) { + if (millis() - connectionStartTime > timeout) { WiFi.disconnect(); + if (state == WifiSelectionState::AUTO_CONNECTING) { + autoCycleAfterScan = true; + startWifiScan(); + return; + } + if (state == WifiSelectionState::AUTO_CYCLING) { + tryNextAutoCycleCandidate(); + return; + } connectionError = tr(STR_ERROR_CONNECTION_TIMEOUT); state = WifiSelectionState::CONNECTION_FAILED; requestUpdate(); @@ -368,7 +479,8 @@ void WifiSelectionActivity::loop() { } // Check connection progress - if (state == WifiSelectionState::CONNECTING || state == WifiSelectionState::AUTO_CONNECTING) { + if (state == WifiSelectionState::CONNECTING || state == WifiSelectionState::AUTO_CONNECTING || + state == WifiSelectionState::AUTO_CYCLING) { checkConnectionStatus(); return; } @@ -573,6 +685,7 @@ void WifiSelectionActivity::render(RenderLock&&) { switch (state) { case WifiSelectionState::AUTO_CONNECTING: + case WifiSelectionState::AUTO_CYCLING: renderConnecting(); break; case WifiSelectionState::SCANNING: diff --git a/src/activities/network/WifiSelectionActivity.h b/src/activities/network/WifiSelectionActivity.h index 42d37862..35fb4cf5 100644 --- a/src/activities/network/WifiSelectionActivity.h +++ b/src/activities/network/WifiSelectionActivity.h @@ -21,6 +21,7 @@ struct WifiNetworkInfo { // WiFi selection states enum class WifiSelectionState { AUTO_CONNECTING, // Trying to connect to the last known network + AUTO_CYCLING, // Cycling through remaining saved credentials after AUTO_CONNECTING failed SCANNING, // Scanning for networks NETWORK_LIST, // Displaying available networks PASSWORD_ENTRY, // Entering password for selected network @@ -73,12 +74,18 @@ class WifiSelectionActivity final : public Activity { // Whether we are attempting to auto-connect bool autoConnecting = false; + // Saved-credential candidates for auto-cycling (SSIDs visible in scan, sorted by RSSI desc) + std::vector autoCycleCandidates; + size_t autoCycleCandidateIndex = 0; + bool autoCycleAfterScan = false; // Scan was triggered to build cycle candidates + // Save/forget prompt selection (0 = Yes, 1 = No) int savePromptSelection = 0; int forgetPromptSelection = 0; - // Connection timeout + // Connection timeouts static constexpr unsigned long CONNECTION_TIMEOUT_MS = 15000; + static constexpr unsigned long AUTO_CYCLE_TIMEOUT_MS = 5000; unsigned long connectionStartTime = 0; void renderNetworkList() const; @@ -92,6 +99,8 @@ class WifiSelectionActivity final : public Activity { void startWifiScan(); void processWifiScanResults(); + void buildAutoCycleCandidates(); + void tryNextAutoCycleCandidate(); void selectNetwork(int index); void attemptConnection(); void checkConnectionStatus();