Speed up wifi reconnect

This commit is contained in:
jpirnay
2026-05-24 16:27:19 +02:00
parent 7459b5572c
commit a9c7894120
5 changed files with 311 additions and 23 deletions
+149 -23
View File
@@ -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 (~5080 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,25 @@ 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;
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;