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
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:
+3
View File
@@ -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?"
+138 -16
View File
@@ -6,6 +6,8 @@
#include <Logging.h>
#include <WiFi.h>
#include <algorithm>
#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);
}
}
}
+15 -2
View File
@@ -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<std::string> 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);