fix: join the AP our scan picked without rescanning every channel

Every WiFi connect took about 17.6 s.  Our scan already picks the AP and
passes its BSSID and channel to WiFi.begin(), but the connect still ran
with WIFI_ALL_CHANNEL_SCAN.  So the driver swept every channel again
before it sent the first auth frame.  A capture of the join showed the AP
silent for 17.6 s, the radio on ch7 while the AP is on ch6, and then
auth, association and the handshake all done in 80 ms.

When the BSSID is known (the normal connect, the strongest-AP path and
roaming), use WIFI_FAST_SCAN: the driver starts on that channel and joins
the first match, and with bssid_set only that AP matches.  The hidden-SSID
path still uses the all-channel scan, since there it is what finds the
strongest AP (rstrouse/ESPSomfy-RTS#294).  Soft reboots now connect in
0.3-1.5 s.

Also log how long each join took and the AP's security mode, on the
connect line and on the roaming-scan lines, so a slow connect shows up.

One boot in the old logs connected in 2.47 s, and nothing in its log
explains it.  After a power cut the first attempts can still fail on the
AP side; a soft reboot sends a deauth first and doesn't have that problem.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
cjkas 2026-10-19 16:00:12 +02:00
parent e963a5adb3
commit 6d1e9064c2
4 changed files with 57 additions and 4 deletions

View file

@ -2,6 +2,7 @@
#include <ETH.h>
#endif
#include <WiFi.h>
#include <esp_wifi.h>
#include <ESPmDNS.h>
#include <esp_task_wdt.h>
#include "esp_log.h"
@ -174,6 +175,7 @@ bool ESPNetwork::changeAP(const uint8_t *bssid, const int32_t channel) {
this->connType = conn_types_t::unset;
this->_connecting = true;
this->connectStart = millis();
WiFi.setScanMethod(WIFI_FAST_SCAN);
WiFi.begin(settings.WIFI.ssid, settings.WIFI.passphrase, channel, bssid);
this->connectStart = millis();
return false;
@ -460,6 +462,11 @@ bool ESPNetwork::connectWiFi(const uint8_t *bssid, const int32_t channel) {
this->connType = conn_types_t::unset;
ESP_LOGI(TAG, "WiFi begin...");
this->_connecting = true;
// Our own scan already picked the AP. An all-channel scan here makes the
// driver sweep every channel again first, about 17s, before it even
// authenticates. A fast scan starts on the AP's channel and joins the
// first match.
WiFi.setScanMethod(WIFI_FAST_SCAN);
WiFi.begin(settings.WIFI.ssid, settings.WIFI.passphrase, channel, bssid);
this->connectStart = millis();
}
@ -488,11 +495,14 @@ bool ESPNetwork::connectWiFi(const uint8_t *bssid, const int32_t channel) {
int32_t _channel = 0;
if(!settings.WIFI.hidden && this->getStrongestAP(settings.WIFI.ssid, _bssid, &_channel)) {
ESP_LOGD(TAG, "Found strongest AP %02X:%02X:%02X:%02X:%02X:%02X CH:%d", _bssid[0], _bssid[1], _bssid[2], _bssid[3], _bssid[4], _bssid[5], _channel);
WiFi.setScanMethod(WIFI_FAST_SCAN); // The AP is known, see connectWiFi(bssid).
WiFi.begin(settings.WIFI.ssid, settings.WIFI.passphrase, _channel, _bssid);
}
else
else {
// If the user has the hidden flag set just connect to whatever the AP gives us.
WiFi.setScanMethod(WIFI_ALL_CHANNEL_SCAN);
WiFi.begin(settings.WIFI.ssid, settings.WIFI.passphrase);
}
}
this->connectStart = millis();
return true;
@ -550,8 +560,9 @@ bool ESPNetwork::getStrongestAP(const char *ssid, uint8_t *bssid, int32_t *chann
if(connected) {
const uint8_t *b = WiFi.BSSID(i);
const uint8_t *cur = WiFi.BSSID();
ESP_LOGI(TAG, "AP %02X:%02X:%02X:%02X:%02X:%02X ch %ld %ld dBm%s", b[0], b[1], b[2], b[3], b[4], b[5],
(long)WiFi.channel(i), (long)WiFi.RSSI(i), (cur && memcmp(b, cur, 6) == 0) ? " (connected)" : "");
ESP_LOGI(TAG, "AP %02X:%02X:%02X:%02X:%02X:%02X ch %ld %ld dBm %s%s", b[0], b[1], b[2], b[3], b[4], b[5],
(long)WiFi.channel(i), (long)WiFi.RSSI(i), wifiAuthModeName(WiFi.encryptionType(i)),
(cur && memcmp(b, cur, 6) == 0) ? " (connected)" : "");
}
if(worthRoaming(connected, currentRssi, WiFi.RSSI(i), ROAM_MIN_GAIN_DB) && WiFi.RSSI(i) > strength) {
strength = WiFi.RSSI(i);
@ -601,7 +612,15 @@ void ESPNetwork::networkEvent(WiFiEvent_t event) {
if(settings.hostname[0] != '\0') WiFi.setHostname(settings.hostname);
break;
case ARDUINO_EVENT_WIFI_STA_STOP: ESP_LOGI(TAG, "(evt) WiFi clients stopped"); break;
case ARDUINO_EVENT_WIFI_STA_CONNECTED: ESP_LOGI(TAG, "(evt) Connected to WiFi STA access point"); break;
case ARDUINO_EVENT_WIFI_STA_CONNECTED: {
// How long the join took and the security the AP offers, so a slow
// connect shows in the log.
wifi_ap_record_t ap;
const bool haveAp = esp_wifi_sta_get_ap_info(&ap) == ESP_OK;
ESP_LOGI(TAG, "(evt) Connected to WiFi STA access point after %lums (%s)",
(unsigned long)(millis() - net.connectStart), haveAp ? wifiAuthModeName(ap.authmode) : "?");
break;
}
case ARDUINO_EVENT_WIFI_STA_DISCONNECTED:
ESP_LOGI(TAG, "(evt) Disconnected from WiFi STA access point. Connecting: %d", net.connecting());
net.connType = conn_types_t::unset;

View file

@ -147,6 +147,20 @@ void RssiBroadcast::sent(bool broadcast, int rssi, int channel, uint32_t now) {
bool worthRoaming(bool connected, int currentRssi, int apRssi, int minGain) {
return !connected || apRssi >= currentRssi + minGain;
}
const char *wifiAuthModeName(uint8_t authMode) {
switch(authMode) {
case 0: return "OPEN";
case 1: return "WEP";
case 2: return "WPA";
case 3: return "WPA2";
case 4: return "WPA/WPA2";
case 5: return "WPA2-ENT";
case 6: return "WPA3";
case 7: return "WPA2/WPA3";
case 8: return "WAPI";
default: return "UNKNOWN";
}
}
heap_emit_t heapEmitTarget(uint8_t num, uint32_t sinceLastMs, uint32_t intervalMs, bool roomHasClients) {
if(num != 255) return heap_emit_t::client;
if(sinceLastMs >= intervalMs) return heap_emit_t::broadcast;

View file

@ -74,6 +74,9 @@ struct RssiBroadcast {
// connected it has to be at least minGain dBm stronger than the current AP so
// a small swing doesn't make the device hop between APs.
bool worthRoaming(bool connected, int currentRssi, int apRssi, int minGain);
// A short name for a wifi_auth_mode_t value, for the logs. The values up to
// WAPI are the same in ESP-IDF 4.4 and 5.x.
const char *wifiAuthModeName(uint8_t authMode);
// Who gets a memStatus event.
enum class heap_emit_t : uint8_t { none, client, broadcast, room };
// num is the socket client asked for, 255 for everyone. A broadcast goes out