feat: auto-connect saved Wi-Fi networks (#2189)

Co-authored-by: Alexander Hoffer <git@alexanderhoffer.com>
This commit is contained in:
Alexander Hoff ❍
2026-07-11 22:47:26 -04:00
committed by GitHub
co-authored by Alexander Hoffer
parent e0253a8664
commit 5202bbf911
4 changed files with 164 additions and 21 deletions
+8 -3
View File
@@ -32,9 +32,14 @@ networks or in hotspot mode when you control who is connected.
## Join Network Mode ## Join Network Mode
1. Select **Join Network**. 1. Select **Join Network**.
2. Pick a 2.4 GHz Wi-Fi network from the scan results. 2. If you have saved Wi-Fi credentials, CrossPoint first tries the last
3. Enter the password if prompted. connected network, then other visible saved networks in signal-strength
4. Save credentials if you want the reader to reconnect automatically next time. 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: After connection, the reader shows:
+3
View File
@@ -29,7 +29,10 @@ STR_WIFI_NETWORKS: "Wi-Fi Networks"
STR_NO_NETWORKS: "No networks found" STR_NO_NETWORKS: "No networks found"
STR_NETWORKS_FOUND: "%zu networks found" STR_NETWORKS_FOUND: "%zu networks found"
STR_SCANNING: "Scanning..." STR_SCANNING: "Scanning..."
STR_FINDING_SAVED_WIFI: "Finding saved Wi-Fi..."
STR_CONNECTING: "Connecting..." STR_CONNECTING: "Connecting..."
STR_CONNECTING_SAVED_WIFI: "Connecting to saved Wi-Fi..."
STR_SHOW_NETWORKS: "Show"
STR_CONNECTED: "Connected!" STR_CONNECTED: "Connected!"
STR_CONNECTION_FAILED: "Connection Failed" STR_CONNECTION_FAILED: "Connection Failed"
STR_FORGET_NETWORK: "Forget Network?" STR_FORGET_NETWORK: "Forget Network?"
+138 -16
View File
@@ -6,6 +6,8 @@
#include <Logging.h> #include <Logging.h>
#include <WiFi.h> #include <WiFi.h>
#include <algorithm>
#include "CrossPointSettings.h" #include "CrossPointSettings.h"
#include "MappedInputManager.h" #include "MappedInputManager.h"
#include "WifiCredentialStore.h" #include "WifiCredentialStore.h"
@@ -35,6 +37,9 @@ void WifiSelectionActivity::onEnter() {
savePromptSelection = 0; savePromptSelection = 0;
forgetPromptSelection = 0; forgetPromptSelection = 0;
autoConnecting = false; autoConnecting = false;
manualNetworkListRequested = false;
autoAttemptedSsids.clear();
autoAttemptedSsids.reserve(WIFI_STORE.getCredentials().size());
// Cache MAC address for display // Cache MAC address for display
uint8_t mac[6]; uint8_t mac[6];
@@ -47,23 +52,20 @@ void WifiSelectionActivity::onEnter() {
// Trigger first update to show scanning message // Trigger first update to show scanning message
requestUpdate(); requestUpdate();
// Attempt to auto-connect to the last network // Attempt to auto-connect to known networks. Try the last successful
if (allowAutoConnect) { // 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(); const std::string lastSsid = WIFI_STORE.getLastConnectedSsid();
if (!lastSsid.empty()) { if (!lastSsid.empty()) {
const auto* cred = WIFI_STORE.findCredential(lastSsid); const auto* cred = WIFI_STORE.findCredential(lastSsid);
if (cred) { if (cred && tryAutoConnectCredential(*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();
return; return;
} }
} }
startWifiScan(true);
return;
} }
// Fallback to scanning // Fallback to scanning
@@ -87,8 +89,9 @@ void WifiSelectionActivity::onExit() {
LOG_DBG("WIFI", "Free heap at onExit end: %d bytes", ESP.getFreeHeap()); LOG_DBG("WIFI", "Free heap at onExit end: %d bytes", ESP.getFreeHeap());
} }
void WifiSelectionActivity::startWifiScan() { void WifiSelectionActivity::startWifiScan(const bool autoScan) {
autoConnecting = false; autoConnecting = autoScan;
manualNetworkListRequested = false;
state = WifiSelectionState::SCANNING; state = WifiSelectionState::SCANNING;
networks.clear(); networks.clear();
requestUpdate(); requestUpdate();
@@ -111,6 +114,8 @@ void WifiSelectionActivity::processWifiScanResults() {
} }
if (scanResult == WIFI_SCAN_FAILED) { if (scanResult == WIFI_SCAN_FAILED) {
autoConnecting = false;
manualNetworkListRequested = false;
state = WifiSelectionState::NETWORK_LIST; state = WifiSelectionState::NETWORK_LIST;
requestUpdate(); requestUpdate();
return; return;
@@ -154,6 +159,13 @@ void WifiSelectionActivity::processWifiScanResults() {
}); });
WiFi.scanDelete(); WiFi.scanDelete();
if (autoConnecting && !manualNetworkListRequested && tryNextSavedNetworkFromScan()) {
return;
}
autoConnecting = false;
manualNetworkListRequested = false;
state = WifiSelectionState::NETWORK_LIST; state = WifiSelectionState::NETWORK_LIST;
selectedNetworkIndex = 0; selectedNetworkIndex = 0;
requestUpdate(); 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() { void WifiSelectionActivity::attemptConnection() {
state = autoConnecting ? WifiSelectionState::AUTO_CONNECTING : WifiSelectionState::CONNECTING; state = autoConnecting ? WifiSelectionState::AUTO_CONNECTING : WifiSelectionState::CONNECTING;
connectionStartTime = millis(); connectionStartTime = millis();
@@ -282,15 +364,24 @@ void WifiSelectionActivity::checkConnectionStatus() {
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);
} }
if (autoConnecting) {
handleAutoConnectFailure();
return;
}
state = WifiSelectionState::CONNECTION_FAILED; state = WifiSelectionState::CONNECTION_FAILED;
requestUpdate(); requestUpdate();
return; return;
} }
// Check for timeout // 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(); WiFi.disconnect();
connectionError = tr(STR_ERROR_CONNECTION_TIMEOUT); connectionError = tr(STR_ERROR_CONNECTION_TIMEOUT);
if (autoConnecting) {
handleAutoConnectFailure();
return;
}
state = WifiSelectionState::CONNECTION_FAILED; state = WifiSelectionState::CONNECTION_FAILED;
requestUpdate(); requestUpdate();
return; return;
@@ -300,12 +391,33 @@ void WifiSelectionActivity::checkConnectionStatus() {
void WifiSelectionActivity::loop() { void WifiSelectionActivity::loop() {
// Check scan progress // Check scan progress
if (state == WifiSelectionState::SCANNING) { 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(); processWifiScanResults();
return; return;
} }
// Check connection progress // Check connection progress
if (state == WifiSelectionState::CONNECTING || state == WifiSelectionState::AUTO_CONNECTING) { 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(); checkConnectionStatus();
return; return;
} }
@@ -559,9 +671,15 @@ void WifiSelectionActivity::renderConnecting(const Rect* screen, const ThemeMetr
const auto top = screen->y + (screen->height - height) / 2; const auto top = screen->y + (screen->height - height) / 2;
if (state == WifiSelectionState::SCANNING) { 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 { } 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); EpdFontFamily::BOLD);
std::string ssidInfo = std::string(tr(STR_TO_PREFIX)) + selectedSSID; 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, "..."); ssidInfo.replace(22, ssidInfo.length() - 22, "...");
} }
UITheme::drawCenteredText(renderer, *screen, UI_10_FONT_ID, top, ssidInfo.c_str()); 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);
}
} }
} }
+15 -2
View File
@@ -11,6 +11,7 @@
struct Rect; struct Rect;
struct ThemeMetrics; struct ThemeMetrics;
struct WifiCredential;
// Structure to hold WiFi network information // Structure to hold WiFi network information
struct WifiNetworkInfo { struct WifiNetworkInfo {
@@ -71,15 +72,22 @@ class WifiSelectionActivity final : public Activity {
// Whether to attempt auto-connect on entry // Whether to attempt auto-connect on entry
const bool allowAutoConnect; 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; 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<std::string> autoAttemptedSsids;
// 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 timeout
static constexpr unsigned long CONNECTION_TIMEOUT_MS = 15000; static constexpr unsigned long CONNECTION_TIMEOUT_MS = 15000;
static constexpr unsigned long AUTO_CONNECTION_TIMEOUT_MS = 7000;
unsigned long connectionStartTime = 0; unsigned long connectionStartTime = 0;
void renderNetworkList(const Rect* screen, const ThemeMetrics* metrics) const; 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 renderConnectionFailed(const Rect* screen, const ThemeMetrics* metrics) const;
void renderForgetPrompt(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 processWifiScanResults();
void selectNetwork(int index); void selectNetwork(int index);
void attemptConnection(); void attemptConnection();
void checkConnectionStatus(); 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; std::string getSignalStrengthIndicator(int32_t rssi) const;
void onComplete(bool connected); void onComplete(bool connected);