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:
@@ -70,6 +70,9 @@ void WifiSelectionActivity::onEnter() {
|
|||||||
savePromptSelection = 0;
|
savePromptSelection = 0;
|
||||||
forgetPromptSelection = 0;
|
forgetPromptSelection = 0;
|
||||||
autoConnecting = false;
|
autoConnecting = false;
|
||||||
|
autoCycleCandidates.clear();
|
||||||
|
autoCycleCandidateIndex = 0;
|
||||||
|
autoCycleAfterScan = false;
|
||||||
|
|
||||||
const std::string persistedMac = formatMacDashed(mac);
|
const std::string persistedMac = formatMacDashed(mac);
|
||||||
if (WIFI_STORE.getLastKnownMacAddress() != persistedMac) {
|
if (WIFI_STORE.getLastKnownMacAddress() != persistedMac) {
|
||||||
@@ -122,6 +125,7 @@ void WifiSelectionActivity::onExit() {
|
|||||||
|
|
||||||
void WifiSelectionActivity::startWifiScan() {
|
void WifiSelectionActivity::startWifiScan() {
|
||||||
autoConnecting = false;
|
autoConnecting = false;
|
||||||
|
// autoCycleAfterScan intentionally preserved when set by the auto-cycle flow
|
||||||
state = WifiSelectionState::SCANNING;
|
state = WifiSelectionState::SCANNING;
|
||||||
networks.clear();
|
networks.clear();
|
||||||
requestUpdate();
|
requestUpdate();
|
||||||
@@ -135,6 +139,81 @@ void WifiSelectionActivity::startWifiScan() {
|
|||||||
WiFi.scanNetworks(true); // true = async scan
|
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() {
|
void WifiSelectionActivity::processWifiScanResults() {
|
||||||
const int16_t scanResult = WiFi.scanComplete();
|
const int16_t scanResult = WiFi.scanComplete();
|
||||||
|
|
||||||
@@ -144,6 +223,7 @@ void WifiSelectionActivity::processWifiScanResults() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
if (scanResult == WIFI_SCAN_FAILED) {
|
if (scanResult == WIFI_SCAN_FAILED) {
|
||||||
|
autoCycleAfterScan = false;
|
||||||
state = WifiSelectionState::NETWORK_LIST;
|
state = WifiSelectionState::NETWORK_LIST;
|
||||||
requestUpdate();
|
requestUpdate();
|
||||||
return;
|
return;
|
||||||
@@ -191,6 +271,14 @@ void WifiSelectionActivity::processWifiScanResults() {
|
|||||||
});
|
});
|
||||||
|
|
||||||
WiFi.scanDelete();
|
WiFi.scanDelete();
|
||||||
|
|
||||||
|
if (autoCycleAfterScan) {
|
||||||
|
autoCycleAfterScan = false;
|
||||||
|
buildAutoCycleCandidates();
|
||||||
|
tryNextAutoCycleCandidate();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
state = WifiSelectionState::NETWORK_LIST;
|
state = WifiSelectionState::NETWORK_LIST;
|
||||||
selectedNetworkIndex = 0;
|
selectedNetworkIndex = 0;
|
||||||
requestUpdate();
|
requestUpdate();
|
||||||
@@ -296,7 +384,8 @@ bool WifiSelectionActivity::checkCaptivePortal() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
void WifiSelectionActivity::checkConnectionStatus() {
|
void WifiSelectionActivity::checkConnectionStatus() {
|
||||||
if (state != WifiSelectionState::CONNECTING && state != WifiSelectionState::AUTO_CONNECTING) {
|
if (state != WifiSelectionState::CONNECTING && state != WifiSelectionState::AUTO_CONNECTING &&
|
||||||
|
state != WifiSelectionState::AUTO_CYCLING) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -340,7 +429,20 @@ void WifiSelectionActivity::checkConnectionStatus() {
|
|||||||
return;
|
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 (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);
|
connectionError = tr(STR_ERROR_GENERAL_FAILURE);
|
||||||
if (status == WL_NO_SSID_AVAIL) {
|
if (status == WL_NO_SSID_AVAIL) {
|
||||||
connectionError = tr(STR_ERROR_NETWORK_NOT_FOUND);
|
connectionError = tr(STR_ERROR_NETWORK_NOT_FOUND);
|
||||||
@@ -351,8 +453,17 @@ void WifiSelectionActivity::checkConnectionStatus() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Check for timeout
|
// Check for timeout
|
||||||
if (millis() - connectionStartTime > CONNECTION_TIMEOUT_MS) {
|
if (millis() - connectionStartTime > timeout) {
|
||||||
WiFi.disconnect();
|
WiFi.disconnect();
|
||||||
|
if (state == WifiSelectionState::AUTO_CONNECTING) {
|
||||||
|
autoCycleAfterScan = true;
|
||||||
|
startWifiScan();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (state == WifiSelectionState::AUTO_CYCLING) {
|
||||||
|
tryNextAutoCycleCandidate();
|
||||||
|
return;
|
||||||
|
}
|
||||||
connectionError = tr(STR_ERROR_CONNECTION_TIMEOUT);
|
connectionError = tr(STR_ERROR_CONNECTION_TIMEOUT);
|
||||||
state = WifiSelectionState::CONNECTION_FAILED;
|
state = WifiSelectionState::CONNECTION_FAILED;
|
||||||
requestUpdate();
|
requestUpdate();
|
||||||
@@ -368,7 +479,8 @@ void WifiSelectionActivity::loop() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Check connection progress
|
// Check connection progress
|
||||||
if (state == WifiSelectionState::CONNECTING || state == WifiSelectionState::AUTO_CONNECTING) {
|
if (state == WifiSelectionState::CONNECTING || state == WifiSelectionState::AUTO_CONNECTING ||
|
||||||
|
state == WifiSelectionState::AUTO_CYCLING) {
|
||||||
checkConnectionStatus();
|
checkConnectionStatus();
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
@@ -573,6 +685,7 @@ void WifiSelectionActivity::render(RenderLock&&) {
|
|||||||
|
|
||||||
switch (state) {
|
switch (state) {
|
||||||
case WifiSelectionState::AUTO_CONNECTING:
|
case WifiSelectionState::AUTO_CONNECTING:
|
||||||
|
case WifiSelectionState::AUTO_CYCLING:
|
||||||
renderConnecting();
|
renderConnecting();
|
||||||
break;
|
break;
|
||||||
case WifiSelectionState::SCANNING:
|
case WifiSelectionState::SCANNING:
|
||||||
|
|||||||
@@ -21,6 +21,7 @@ struct WifiNetworkInfo {
|
|||||||
// WiFi selection states
|
// WiFi selection states
|
||||||
enum class WifiSelectionState {
|
enum class WifiSelectionState {
|
||||||
AUTO_CONNECTING, // Trying to connect to the last known network
|
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
|
SCANNING, // Scanning for networks
|
||||||
NETWORK_LIST, // Displaying available networks
|
NETWORK_LIST, // Displaying available networks
|
||||||
PASSWORD_ENTRY, // Entering password for selected network
|
PASSWORD_ENTRY, // Entering password for selected network
|
||||||
@@ -73,12 +74,18 @@ class WifiSelectionActivity final : public Activity {
|
|||||||
// Whether we are attempting to auto-connect
|
// Whether we are attempting to auto-connect
|
||||||
bool autoConnecting = false;
|
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)
|
// Save/forget prompt selection (0 = Yes, 1 = No)
|
||||||
int savePromptSelection = 0;
|
int savePromptSelection = 0;
|
||||||
int forgetPromptSelection = 0;
|
int forgetPromptSelection = 0;
|
||||||
|
|
||||||
// Connection timeout
|
// Connection timeouts
|
||||||
static constexpr unsigned long CONNECTION_TIMEOUT_MS = 15000;
|
static constexpr unsigned long CONNECTION_TIMEOUT_MS = 15000;
|
||||||
|
static constexpr unsigned long AUTO_CYCLE_TIMEOUT_MS = 5000;
|
||||||
unsigned long connectionStartTime = 0;
|
unsigned long connectionStartTime = 0;
|
||||||
|
|
||||||
void renderNetworkList() const;
|
void renderNetworkList() const;
|
||||||
@@ -92,6 +99,8 @@ class WifiSelectionActivity final : public Activity {
|
|||||||
|
|
||||||
void startWifiScan();
|
void startWifiScan();
|
||||||
void processWifiScanResults();
|
void processWifiScanResults();
|
||||||
|
void buildAutoCycleCandidates();
|
||||||
|
void tryNextAutoCycleCandidate();
|
||||||
void selectNetwork(int index);
|
void selectNetwork(int index);
|
||||||
void attemptConnection();
|
void attemptConnection();
|
||||||
void checkConnectionStatus();
|
void checkConnectionStatus();
|
||||||
|
|||||||
Reference in New Issue
Block a user