Merge pull request #136 from jpirnay/feat-try-more-networks

feat: Wi-Fi fallback: auto-retry on saved networks when primary is unavailable
This commit is contained in:
jpirnay
2026-04-26 15:58:00 +02:00
committed by GitHub
2 changed files with 126 additions and 4 deletions
@@ -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<Candidate> 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:
+10 -1
View File
@@ -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<std::string> 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();