Merge pull request #268 from jpirnay/refactor-wifi
perf: Speed up wifi reconnect
This commit is contained in:
@@ -407,6 +407,29 @@ bool JsonSettingsIO::saveWifi(const WifiCredentialStore& store, const char* path
|
||||
JsonObject obj = arr.add<JsonObject>();
|
||||
obj["ssid"] = cred.ssid;
|
||||
obj["password_obf"] = obfuscation::obfuscateToBase64(cred.password);
|
||||
bool hasHint = cred.channel != 0;
|
||||
for (int i = 0; i < 6 && !hasHint; i++) {
|
||||
if (cred.bssid[i] != 0) hasHint = true;
|
||||
}
|
||||
if (hasHint) {
|
||||
char bssidHex[13];
|
||||
snprintf(bssidHex, sizeof(bssidHex), "%02x%02x%02x%02x%02x%02x", cred.bssid[0], cred.bssid[1], cred.bssid[2],
|
||||
cred.bssid[3], cred.bssid[4], cred.bssid[5]);
|
||||
obj["bssid"] = bssidHex;
|
||||
obj["channel"] = cred.channel;
|
||||
}
|
||||
if (cred.ip[0] != 0) {
|
||||
char buf[16];
|
||||
snprintf(buf, sizeof(buf), "%d.%d.%d.%d", cred.ip[0], cred.ip[1], cred.ip[2], cred.ip[3]);
|
||||
obj["ip"] = buf;
|
||||
snprintf(buf, sizeof(buf), "%d.%d.%d.%d", cred.gateway[0], cred.gateway[1], cred.gateway[2], cred.gateway[3]);
|
||||
obj["gw"] = buf;
|
||||
snprintf(buf, sizeof(buf), "%d.%d.%d.%d", cred.mask[0], cred.mask[1], cred.mask[2], cred.mask[3]);
|
||||
obj["mask"] = buf;
|
||||
snprintf(buf, sizeof(buf), "%d.%d.%d.%d", cred.dns[0], cred.dns[1], cred.dns[2], cred.dns[3]);
|
||||
obj["dns"] = buf;
|
||||
obj["ts"] = cred.cacheTimestamp;
|
||||
}
|
||||
}
|
||||
|
||||
String json;
|
||||
@@ -464,6 +487,52 @@ bool JsonSettingsIO::loadWifi(WifiCredentialStore& store, const char* json, bool
|
||||
cred.password = obj["password"] | std::string("");
|
||||
if (!cred.password.empty() && needsResave) *needsResave = true;
|
||||
}
|
||||
const std::string bssidHex = obj["bssid"] | std::string("");
|
||||
const int channel = obj["channel"] | 0;
|
||||
if (bssidHex.size() == 12 && channel > 0 && channel <= 14) {
|
||||
bool parseOk = true;
|
||||
uint8_t parsed[6] = {0};
|
||||
for (int i = 0; i < 6 && parseOk; i++) {
|
||||
unsigned int byte = 0;
|
||||
if (sscanf(bssidHex.c_str() + i * 2, "%2x", &byte) != 1) {
|
||||
parseOk = false;
|
||||
} else {
|
||||
parsed[i] = static_cast<uint8_t>(byte);
|
||||
}
|
||||
}
|
||||
if (parseOk) {
|
||||
std::memcpy(cred.bssid, parsed, 6);
|
||||
cred.channel = static_cast<uint8_t>(channel);
|
||||
}
|
||||
}
|
||||
const auto parseQuad = [](const std::string& s, uint8_t out[4]) -> bool {
|
||||
unsigned int a = 0, b = 0, c = 0, d = 0;
|
||||
int consumed = 0;
|
||||
// %n stores characters consumed; require it to equal full string length so we
|
||||
// reject inputs with trailing garbage like "192.168.1.10xyz".
|
||||
if (sscanf(s.c_str(), "%u.%u.%u.%u%n", &a, &b, &c, &d, &consumed) != 4) return false;
|
||||
if (consumed < 0 || static_cast<size_t>(consumed) != s.size()) return false;
|
||||
if (a > 255 || b > 255 || c > 255 || d > 255) return false;
|
||||
out[0] = a;
|
||||
out[1] = b;
|
||||
out[2] = c;
|
||||
out[3] = d;
|
||||
return true;
|
||||
};
|
||||
const std::string ipStr = obj["ip"] | std::string("");
|
||||
const std::string gwStr = obj["gw"] | std::string("");
|
||||
const std::string maskStr = obj["mask"] | std::string("");
|
||||
const std::string dnsStr = obj["dns"] | std::string("");
|
||||
if (!ipStr.empty()) {
|
||||
uint8_t ip[4], gw[4], mask[4], dns[4];
|
||||
if (parseQuad(ipStr, ip) && parseQuad(gwStr, gw) && parseQuad(maskStr, mask) && parseQuad(dnsStr, dns)) {
|
||||
std::memcpy(cred.ip, ip, 4);
|
||||
std::memcpy(cred.gateway, gw, 4);
|
||||
std::memcpy(cred.mask, mask, 4);
|
||||
std::memcpy(cred.dns, dns, 4);
|
||||
cred.cacheTimestamp = obj["ts"] | 0u;
|
||||
}
|
||||
}
|
||||
store.credentials.push_back(cred);
|
||||
}
|
||||
|
||||
|
||||
@@ -5,6 +5,8 @@
|
||||
#include <Logging.h>
|
||||
#include <ObfuscationUtils.h>
|
||||
|
||||
#include <cstring>
|
||||
|
||||
// Initialize the static instance
|
||||
WifiCredentialStore WifiCredentialStore::instance;
|
||||
|
||||
@@ -82,6 +84,62 @@ const WifiCredential* WifiCredentialStore::findCredential(const std::string& ssi
|
||||
|
||||
bool WifiCredentialStore::hasSavedCredential(const std::string& ssid) const { return findCredential(ssid) != nullptr; }
|
||||
|
||||
bool WifiCredentialStore::updateConnectionCache(const std::string& ssid, const uint8_t bssid[6], uint8_t channel,
|
||||
const uint8_t ip[4], const uint8_t gateway[4], const uint8_t mask[4],
|
||||
const uint8_t dns[4], uint32_t cacheTimestamp) {
|
||||
const auto cred =
|
||||
find_if(credentials.begin(), credentials.end(), [&ssid](const WifiCredential& c) { return c.ssid == ssid; });
|
||||
if (cred == credentials.end()) {
|
||||
return false;
|
||||
}
|
||||
const bool sameHint = (channel == cred->channel) && std::memcmp(bssid, cred->bssid, 6) == 0;
|
||||
const bool sameIp = std::memcmp(ip, cred->ip, 4) == 0 && std::memcmp(gateway, cred->gateway, 4) == 0 &&
|
||||
std::memcmp(mask, cred->mask, 4) == 0 && std::memcmp(dns, cred->dns, 4) == 0;
|
||||
if (sameHint && sameIp) {
|
||||
// Refresh the TTL only if the cache is older than half the TTL window — otherwise
|
||||
// a long-running device that reconnects daily would let its 7-day TTL expire even
|
||||
// though the topology hasn't changed. Half-TTL avoids an SD write on every connect.
|
||||
constexpr uint32_t REFRESH_THRESHOLD_SECONDS = (7 * 24 * 60 * 60) / 2;
|
||||
const int64_t elapsed = static_cast<int64_t>(cacheTimestamp) - static_cast<int64_t>(cred->cacheTimestamp);
|
||||
if (cacheTimestamp != 0 && cred->cacheTimestamp != 0 && elapsed > REFRESH_THRESHOLD_SECONDS) {
|
||||
cred->cacheTimestamp = cacheTimestamp;
|
||||
return saveToFile();
|
||||
}
|
||||
return true;
|
||||
}
|
||||
std::memcpy(cred->bssid, bssid, 6);
|
||||
cred->channel = channel;
|
||||
std::memcpy(cred->ip, ip, 4);
|
||||
std::memcpy(cred->gateway, gateway, 4);
|
||||
std::memcpy(cred->mask, mask, 4);
|
||||
std::memcpy(cred->dns, dns, 4);
|
||||
cred->cacheTimestamp = cacheTimestamp;
|
||||
return saveToFile();
|
||||
}
|
||||
|
||||
bool WifiCredentialStore::clearConnectionCache(const std::string& ssid) {
|
||||
const auto cred =
|
||||
find_if(credentials.begin(), credentials.end(), [&ssid](const WifiCredential& c) { return c.ssid == ssid; });
|
||||
if (cred == credentials.end()) {
|
||||
return false;
|
||||
}
|
||||
bool empty = (cred->channel == 0) && (cred->ip[0] == 0);
|
||||
for (int i = 0; i < 6 && empty; i++) {
|
||||
if (cred->bssid[i] != 0) empty = false;
|
||||
}
|
||||
if (empty) {
|
||||
return true;
|
||||
}
|
||||
std::memset(cred->bssid, 0, 6);
|
||||
cred->channel = 0;
|
||||
std::memset(cred->ip, 0, 4);
|
||||
std::memset(cred->gateway, 0, 4);
|
||||
std::memset(cred->mask, 0, 4);
|
||||
std::memset(cred->dns, 0, 4);
|
||||
cred->cacheTimestamp = 0;
|
||||
return saveToFile();
|
||||
}
|
||||
|
||||
void WifiCredentialStore::setLastConnectedSsid(const std::string& ssid) {
|
||||
if (lastConnectedSsid != ssid) {
|
||||
lastConnectedSsid = ssid;
|
||||
|
||||
@@ -1,10 +1,24 @@
|
||||
#pragma once
|
||||
#include <cstdint>
|
||||
#include <string>
|
||||
#include <vector>
|
||||
|
||||
struct WifiCredential {
|
||||
std::string ssid;
|
||||
std::string password; // Plaintext in memory; obfuscated with hardware key on disk
|
||||
// Connection hint cached on successful connect. All-zero BSSID or channel==0 means
|
||||
// "no hint, do a full scan". Used to skip channel scanning on reconnect.
|
||||
uint8_t bssid[6] = {0, 0, 0, 0, 0, 0};
|
||||
uint8_t channel = 0;
|
||||
|
||||
// Cached IP configuration to skip DHCP on reconnect. Valid only when ip[0] != 0 AND
|
||||
// we connect to the same BSSID (cached above). cacheTimestamp is epoch seconds at
|
||||
// capture; 0 means "unknown time, no TTL enforced".
|
||||
uint8_t ip[4] = {0, 0, 0, 0};
|
||||
uint8_t gateway[4] = {0, 0, 0, 0};
|
||||
uint8_t mask[4] = {0, 0, 0, 0};
|
||||
uint8_t dns[4] = {0, 0, 0, 0};
|
||||
uint32_t cacheTimestamp = 0;
|
||||
};
|
||||
|
||||
class WifiCredentialStore;
|
||||
@@ -51,6 +65,16 @@ class WifiCredentialStore {
|
||||
bool removeCredential(const std::string& ssid);
|
||||
const WifiCredential* findCredential(const std::string& ssid) const;
|
||||
|
||||
// Update cached BSSID/channel hint AND IP configuration in one write. ip/gw/mask/dns
|
||||
// may be all-zero to mean "no IP cache". cacheTimestamp is epoch seconds (0 if unsynced).
|
||||
// Persists only if anything changed.
|
||||
bool updateConnectionCache(const std::string& ssid, const uint8_t bssid[6], uint8_t channel, const uint8_t ip[4],
|
||||
const uint8_t gateway[4], const uint8_t mask[4], const uint8_t dns[4],
|
||||
uint32_t cacheTimestamp);
|
||||
// Clear all cached hint + IP for this credential (after a hint-based connect failed
|
||||
// and we want the next attempt to start fresh with full scan + DHCP).
|
||||
bool clearConnectionCache(const std::string& ssid);
|
||||
|
||||
// Get all stored credentials (for UI display)
|
||||
const std::vector<WifiCredential>& getCredentials() const { return credentials; }
|
||||
|
||||
|
||||
@@ -2,12 +2,15 @@
|
||||
|
||||
#include <GfxRenderer.h>
|
||||
#include <HTTPClient.h>
|
||||
#include <HalClock.h>
|
||||
#include <I18n.h>
|
||||
#include <Logging.h>
|
||||
#include <NetworkClient.h>
|
||||
#include <WiFi.h>
|
||||
#include <esp_mac.h>
|
||||
|
||||
#include <cstring>
|
||||
#include <ctime>
|
||||
#include <map>
|
||||
|
||||
#include "MappedInputManager.h"
|
||||
@@ -46,6 +49,20 @@ String formatMacCompact(const uint8_t mac[6]) {
|
||||
void WifiSelectionActivity::onEnter() {
|
||||
Activity::onEnter();
|
||||
|
||||
// Timing instrumentation: split total connect time into association vs DHCP.
|
||||
// STA_CONNECTED = association (auth + 4-way handshake done).
|
||||
// STA_GOT_IP = DHCP done.
|
||||
evtIdConnected = WiFi.onEvent(
|
||||
[this](WiFiEvent_t /*event*/, WiFiEventInfo_t /*info*/) {
|
||||
LOG_DBG("WIFI", "EVT associated at %lu ms", millis() - connectionStartTime);
|
||||
},
|
||||
ARDUINO_EVENT_WIFI_STA_CONNECTED);
|
||||
evtIdGotIp = WiFi.onEvent(
|
||||
[this](WiFiEvent_t /*event*/, WiFiEventInfo_t /*info*/) {
|
||||
LOG_DBG("WIFI", "EVT got_ip at %lu ms", millis() - connectionStartTime);
|
||||
},
|
||||
ARDUINO_EVENT_WIFI_STA_GOT_IP);
|
||||
|
||||
// Load saved WiFi credentials - SD card operations need lock as we use SPI
|
||||
// for both
|
||||
{
|
||||
@@ -109,6 +126,15 @@ void WifiSelectionActivity::onEnter() {
|
||||
void WifiSelectionActivity::onExit() {
|
||||
Activity::onExit();
|
||||
|
||||
if (evtIdConnected != 0) {
|
||||
WiFi.removeEvent(evtIdConnected);
|
||||
evtIdConnected = 0;
|
||||
}
|
||||
if (evtIdGotIp != 0) {
|
||||
WiFi.removeEvent(evtIdGotIp);
|
||||
evtIdGotIp = 0;
|
||||
}
|
||||
|
||||
LOG_DBG("WIFI", "Free heap at onExit start: %d bytes", ESP.getFreeHeap());
|
||||
|
||||
// Stop any ongoing WiFi scan
|
||||
@@ -195,23 +221,11 @@ void WifiSelectionActivity::tryNextAutoCycleCandidate() {
|
||||
connectionStartTime = millis();
|
||||
connectedIP.clear();
|
||||
connectionError.clear();
|
||||
hintFallbackDone = false;
|
||||
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());
|
||||
}
|
||||
prepareForConnect();
|
||||
issueWifiBegin(/*useHint=*/true);
|
||||
}
|
||||
|
||||
void WifiSelectionActivity::processWifiScanResults() {
|
||||
@@ -334,23 +348,96 @@ void WifiSelectionActivity::attemptConnection() {
|
||||
connectionStartTime = millis();
|
||||
connectedIP.clear();
|
||||
connectionError.clear();
|
||||
hintFallbackDone = false;
|
||||
requestUpdate();
|
||||
|
||||
prepareForConnect();
|
||||
issueWifiBegin(/*useHint=*/true);
|
||||
}
|
||||
|
||||
void WifiSelectionActivity::prepareForConnect() {
|
||||
WiFi.persistent(false); // Credentials are managed by WifiCredentialStore; suppress SDK NVS auto-connect
|
||||
WiFi.mode(WIFI_STA);
|
||||
WiFi.disconnect(true, true); // Abort any in-progress SDK auto-connect and clear NVS-saved SSID
|
||||
delay(100);
|
||||
|
||||
// Only switch mode if we're not already STA — the mode setter touches the netif and
|
||||
// can take 50+ ms even when "no change" semantically.
|
||||
if (WiFi.getMode() != WIFI_STA) {
|
||||
WiFi.mode(WIFI_STA);
|
||||
}
|
||||
|
||||
// Only do the heavy disconnect(true,true) — which erases NVS and tears down the WPA
|
||||
// state machine — when there's actually something to tear down. From a fresh/idle
|
||||
// state it's a pure cost (~50–80 ms on this SoC).
|
||||
const wl_status_t status = WiFi.status();
|
||||
const bool needsReset = (status == WL_CONNECTED) || (status == WL_CONNECT_FAILED) || (status == WL_CONNECTION_LOST) ||
|
||||
(status == WL_NO_SSID_AVAIL);
|
||||
if (needsReset) {
|
||||
WiFi.disconnect(true, true);
|
||||
}
|
||||
|
||||
// Use stable base MAC so hostname suffix is deterministic across WiFi states.
|
||||
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());
|
||||
void WifiSelectionActivity::issueWifiBegin(bool useHint) {
|
||||
std::memset(currentAttemptBssid, 0, 6);
|
||||
currentAttemptChannel = 0;
|
||||
bool appliedStaticIp = false;
|
||||
|
||||
if (useHint) {
|
||||
const auto* cred = WIFI_STORE.findCredential(selectedSSID);
|
||||
if (cred && cred->channel != 0) {
|
||||
std::memcpy(currentAttemptBssid, cred->bssid, 6);
|
||||
currentAttemptChannel = cred->channel;
|
||||
|
||||
// Apply cached static IP only when the IP cache is keyed to this same BSSID and
|
||||
// (if we have a synced clock) hasn't aged out. cacheTimestamp==0 means it was
|
||||
// written before clock-sync; we trust it indefinitely in that case.
|
||||
if (cred->ip[0] != 0) {
|
||||
constexpr int64_t TTL_SECONDS = 7 * 24 * 60 * 60;
|
||||
const time_t now = HalClock::now();
|
||||
// Use signed arithmetic so we don't underflow when ts is in the future (which
|
||||
// happens when NTP corrects the clock backwards between cache write and read).
|
||||
// Future timestamps are treated as fresh (negative elapsed clamped to 0).
|
||||
const int64_t elapsed =
|
||||
(now == 0) ? 0 : (static_cast<int64_t>(now) - static_cast<int64_t>(cred->cacheTimestamp));
|
||||
const bool ttlOk = (cred->cacheTimestamp == 0) || (now == 0) || (elapsed < TTL_SECONDS);
|
||||
if (ttlOk) {
|
||||
IPAddress ip(cred->ip[0], cred->ip[1], cred->ip[2], cred->ip[3]);
|
||||
IPAddress gw(cred->gateway[0], cred->gateway[1], cred->gateway[2], cred->gateway[3]);
|
||||
IPAddress mask(cred->mask[0], cred->mask[1], cred->mask[2], cred->mask[3]);
|
||||
IPAddress dns(cred->dns[0], cred->dns[1], cred->dns[2], cred->dns[3]);
|
||||
WiFi.config(ip, gw, mask, dns);
|
||||
appliedStaticIp = true;
|
||||
} else {
|
||||
LOG_DBG("WIFI", "IP cache aged out (ts=%u, now=%ld), using DHCP", cred->cacheTimestamp, (long)now);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (!appliedStaticIp) {
|
||||
// Reset to DHCP in case a previous attempt left a static config behind.
|
||||
WiFi.config(IPAddress(), IPAddress(), IPAddress(), IPAddress());
|
||||
}
|
||||
|
||||
const char* pwd = (selectedRequiresPassword && !enteredPassword.empty()) ? enteredPassword.c_str() : nullptr;
|
||||
const unsigned long preBeginMs = millis() - connectionStartTime;
|
||||
if (currentAttemptChannel != 0) {
|
||||
LOG_DBG("WIFI", "WiFi.begin -> %s ch=%d bssid=%02x:%02x:%02x:%02x:%02x:%02x staticIp=%s (pre-begin %lu ms)",
|
||||
selectedSSID.c_str(), currentAttemptChannel, currentAttemptBssid[0], currentAttemptBssid[1],
|
||||
currentAttemptBssid[2], currentAttemptBssid[3], currentAttemptBssid[4], currentAttemptBssid[5],
|
||||
appliedStaticIp ? "yes" : "no", preBeginMs);
|
||||
WiFi.begin(selectedSSID.c_str(), pwd, currentAttemptChannel, currentAttemptBssid, true);
|
||||
} else {
|
||||
WiFi.begin(selectedSSID.c_str());
|
||||
LOG_DBG("WIFI", "WiFi.begin -> %s (no hint, pre-begin %lu ms)", selectedSSID.c_str(), preBeginMs);
|
||||
if (pwd) {
|
||||
WiFi.begin(selectedSSID.c_str(), pwd);
|
||||
} else {
|
||||
WiFi.begin(selectedSSID.c_str());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -399,11 +486,31 @@ void WifiSelectionActivity::checkConnectionStatus() {
|
||||
connectedIP = ipStr;
|
||||
autoConnecting = false;
|
||||
|
||||
// Save this as the last connected network - SD card operations need lock as
|
||||
// we use SPI for both
|
||||
LOG_DBG("WIFI", "Connected to %s in %lu ms (rssi=%d ch=%d ip=%s, hint=%s)", selectedSSID.c_str(),
|
||||
millis() - connectionStartTime, WiFi.RSSI(), WiFi.channel(), ipStr,
|
||||
currentAttemptChannel != 0 ? "yes" : "no");
|
||||
|
||||
// Save this as the last connected network and cache the full connection profile
|
||||
// (BSSID/channel + IP/gw/mask/dns) so the next reconnect can skip both channel
|
||||
// scanning and DHCP. SD card operations need lock as we use SPI for both.
|
||||
{
|
||||
RenderLock lock(*this);
|
||||
WIFI_STORE.setLastConnectedSsid(selectedSSID);
|
||||
const uint8_t* actualBssid = WiFi.BSSID();
|
||||
const int actualChannel = WiFi.channel();
|
||||
if (actualBssid && actualChannel > 0 && actualChannel <= 255) {
|
||||
const IPAddress gw = WiFi.gatewayIP();
|
||||
const IPAddress mask = WiFi.subnetMask();
|
||||
const IPAddress dns = WiFi.dnsIP();
|
||||
const uint8_t ipBytes[4] = {ip[0], ip[1], ip[2], ip[3]};
|
||||
const uint8_t gwBytes[4] = {gw[0], gw[1], gw[2], gw[3]};
|
||||
const uint8_t maskBytes[4] = {mask[0], mask[1], mask[2], mask[3]};
|
||||
const uint8_t dnsBytes[4] = {dns[0], dns[1], dns[2], dns[3]};
|
||||
const time_t nowEpoch = HalClock::now();
|
||||
WIFI_STORE.updateConnectionCache(selectedSSID, actualBssid, static_cast<uint8_t>(actualChannel), ipBytes,
|
||||
gwBytes, maskBytes, dnsBytes,
|
||||
nowEpoch > 0 ? static_cast<uint32_t>(nowEpoch) : 0u);
|
||||
}
|
||||
}
|
||||
|
||||
// Check for captive portal before declaring success
|
||||
@@ -429,6 +536,32 @@ void WifiSelectionActivity::checkConnectionStatus() {
|
||||
return;
|
||||
}
|
||||
|
||||
// If this attempt used a hint and the hint hasn't paid off (channel may have changed
|
||||
// because the AP is part of a mesh / roamed), retry once with a full scan before
|
||||
// surfacing failure or moving to the next candidate. Triggered on either a hard
|
||||
// failure status or a short timeout — whichever comes first.
|
||||
const bool usingHint = currentAttemptChannel != 0;
|
||||
const bool hintHardFail =
|
||||
usingHint && !hintFallbackDone && (status == WL_CONNECT_FAILED || status == WL_NO_SSID_AVAIL);
|
||||
const bool hintTimedOut =
|
||||
usingHint && !hintFallbackDone && (millis() - connectionStartTime > HINT_ATTEMPT_TIMEOUT_MS);
|
||||
if (hintHardFail || hintTimedOut) {
|
||||
LOG_DBG("WIFI", "Hint attempt did not connect (%s after %lu ms), retrying with full scan",
|
||||
hintHardFail ? "hard fail" : "timeout", millis() - connectionStartTime);
|
||||
hintFallbackDone = true;
|
||||
// Wipe the stale cache before retrying. If the fallback succeeds, the success
|
||||
// path writes a fresh cache (one extra SD write). If it fails or is interrupted,
|
||||
// we won't carry a known-bad hint into the next session.
|
||||
{
|
||||
RenderLock lock(*this);
|
||||
WIFI_STORE.clearConnectionCache(selectedSSID);
|
||||
}
|
||||
WiFi.disconnect(true, false);
|
||||
connectionStartTime = millis();
|
||||
issueWifiBegin(/*useHint=*/false);
|
||||
return;
|
||||
}
|
||||
|
||||
const unsigned long timeout =
|
||||
state == WifiSelectionState::AUTO_CYCLING ? AUTO_CYCLE_TIMEOUT_MS : CONNECTION_TIMEOUT_MS;
|
||||
|
||||
|
||||
@@ -86,8 +86,23 @@ class WifiSelectionActivity final : public Activity {
|
||||
// Connection timeouts
|
||||
static constexpr unsigned long CONNECTION_TIMEOUT_MS = 15000;
|
||||
static constexpr unsigned long AUTO_CYCLE_TIMEOUT_MS = 5000;
|
||||
// Faster timeout for the first hint-based attempt before falling back to full scan.
|
||||
// Hint-based connect on the correct channel usually completes in <2 s; if it doesn't,
|
||||
// the AP has likely moved (mesh roam, channel change) so it's cheaper to bail and rescan.
|
||||
static constexpr unsigned long HINT_ATTEMPT_TIMEOUT_MS = 3000;
|
||||
unsigned long connectionStartTime = 0;
|
||||
|
||||
// BSSID/channel hint used on the current attempt (channel==0 means no hint).
|
||||
uint8_t currentAttemptBssid[6] = {0};
|
||||
uint8_t currentAttemptChannel = 0;
|
||||
// Whether we've already done the silent fallback retry without the hint for this
|
||||
// user-initiated connection. Prevents loops if the AP genuinely isn't reachable.
|
||||
bool hintFallbackDone = false;
|
||||
|
||||
// WiFi event handler IDs so we can deregister on exit.
|
||||
uint16_t evtIdConnected = 0;
|
||||
uint16_t evtIdGotIp = 0;
|
||||
|
||||
void renderNetworkList() const;
|
||||
void renderPasswordEntry() const;
|
||||
void renderConnecting() const;
|
||||
@@ -104,6 +119,14 @@ class WifiSelectionActivity final : public Activity {
|
||||
void selectNetwork(int index);
|
||||
void attemptConnection();
|
||||
void checkConnectionStatus();
|
||||
// Issues WiFi.begin() either with the cached BSSID/channel hint (fast path) or without
|
||||
// (full scan fallback). `useHint=false` clears currentAttemptChannel so the success path
|
||||
// doesn't double-store the same hint.
|
||||
void issueWifiBegin(bool useHint);
|
||||
// Prepares the WiFi stack for a connect attempt: ensures STA mode and a clean state,
|
||||
// sets a deterministic hostname. Skips the expensive disconnect(true,true) when WiFi
|
||||
// is already idle so the warm reconnect path doesn't pay an NVS-erase cost.
|
||||
void prepareForConnect();
|
||||
bool checkCaptivePortal();
|
||||
std::string getSignalStrengthIndicator(int32_t rssi) const;
|
||||
|
||||
|
||||
Reference in New Issue
Block a user