[wifi] Fix WiFi recovery after failed connection attempts (#12483)

This commit is contained in:
J. Nick Koston
2025-12-14 16:26:34 -06:00
committed by GitHub
parent fa5b14fad4
commit ffce80f96c
3 changed files with 32 additions and 5 deletions
+23 -3
View File
@@ -205,6 +205,21 @@ static constexpr uint32_t WIFI_COOLDOWN_DURATION_MS = 500;
/// While connecting, WiFi can't beacon the AP properly, so needs longer cooldown /// While connecting, WiFi can't beacon the AP properly, so needs longer cooldown
static constexpr uint32_t WIFI_COOLDOWN_WITH_AP_ACTIVE_MS = 30000; static constexpr uint32_t WIFI_COOLDOWN_WITH_AP_ACTIVE_MS = 30000;
/// Timeout for WiFi scan operations
/// This is a fallback in case we don't receive a scan done callback from the WiFi driver.
/// Normal scans complete via callback; this only triggers if something goes wrong.
static constexpr uint32_t WIFI_SCAN_TIMEOUT_MS = 31000;
/// Timeout for WiFi connection attempts
/// This is a fallback in case we don't receive connection success/failure callbacks.
/// Some platforms (especially LibreTiny/Beken) can take 30-60 seconds to connect,
/// particularly with fast_connect enabled where no prior scan provides channel info.
/// Do not lower this value - connection failures are detected via callbacks, not timeout.
/// If this timeout fires prematurely while a connection is still in progress, it causes
/// cascading failures: the subsequent scan will also fail because the WiFi driver is
/// still busy with the previous connection attempt.
static constexpr uint32_t WIFI_CONNECT_TIMEOUT_MS = 46000;
static constexpr uint8_t get_max_retries_for_phase(WiFiRetryPhase phase) { static constexpr uint8_t get_max_retries_for_phase(WiFiRetryPhase phase) {
switch (phase) { switch (phase) {
case WiFiRetryPhase::INITIAL_CONNECT: case WiFiRetryPhase::INITIAL_CONNECT:
@@ -1035,7 +1050,7 @@ __attribute__((noinline)) static void log_scan_result(const WiFiScanResult &res)
void WiFiComponent::check_scanning_finished() { void WiFiComponent::check_scanning_finished() {
if (!this->scan_done_) { if (!this->scan_done_) {
if (millis() - this->action_started_ > 30000) { if (millis() - this->action_started_ > WIFI_SCAN_TIMEOUT_MS) {
ESP_LOGE(TAG, "Scan timeout"); ESP_LOGE(TAG, "Scan timeout");
this->retry_connect(); this->retry_connect();
} }
@@ -1184,8 +1199,9 @@ void WiFiComponent::check_connecting_finished() {
} }
uint32_t now = millis(); uint32_t now = millis();
if (now - this->action_started_ > 30000) { if (now - this->action_started_ > WIFI_CONNECT_TIMEOUT_MS) {
ESP_LOGW(TAG, "Connection timeout"); ESP_LOGW(TAG, "Connection timeout, aborting connection attempt");
this->wifi_disconnect_();
this->retry_connect(); this->retry_connect();
return; return;
} }
@@ -1405,6 +1421,10 @@ bool WiFiComponent::transition_to_phase_(WiFiRetryPhase new_phase) {
// without disrupting the captive portal/improv connection // without disrupting the captive portal/improv connection
if (!this->is_captive_portal_active_() && !this->is_esp32_improv_active_()) { if (!this->is_captive_portal_active_() && !this->is_esp32_improv_active_()) {
this->restart_adapter(); this->restart_adapter();
} else {
// Even when skipping full restart, disconnect to clear driver state
// Without this, platforms like LibreTiny may think we're still connecting
this->wifi_disconnect_();
} }
// Clear scan flag - we're starting a new retry cycle // Clear scan flag - we're starting a new retry cycle
this->did_scan_this_cycle_ = false; this->did_scan_this_cycle_ = false;
@@ -720,6 +720,7 @@ void WiFiComponent::wifi_process_event_(IDFWiFiEvent *data) {
} else if (data->event_base == WIFI_EVENT && data->event_id == WIFI_EVENT_STA_STOP) { } else if (data->event_base == WIFI_EVENT && data->event_id == WIFI_EVENT_STA_STOP) {
ESP_LOGV(TAG, "STA stop"); ESP_LOGV(TAG, "STA stop");
s_sta_started = false; s_sta_started = false;
s_sta_connecting = false;
} else if (data->event_base == WIFI_EVENT && data->event_id == WIFI_EVENT_STA_AUTHMODE_CHANGE) { } else if (data->event_base == WIFI_EVENT && data->event_id == WIFI_EVENT_STA_AUTHMODE_CHANGE) {
const auto &it = data->data.sta_authmode_change; const auto &it = data->data.sta_authmode_change;
@@ -291,6 +291,7 @@ void WiFiComponent::wifi_event_callback_(esphome_wifi_event_id_t event, esphome_
} }
case ESPHOME_EVENT_ID_WIFI_STA_STOP: { case ESPHOME_EVENT_ID_WIFI_STA_STOP: {
ESP_LOGV(TAG, "STA stop"); ESP_LOGV(TAG, "STA stop");
s_sta_connecting = false;
break; break;
} }
case ESPHOME_EVENT_ID_WIFI_STA_CONNECTED: { case ESPHOME_EVENT_ID_WIFI_STA_CONNECTED: {
@@ -322,7 +323,7 @@ void WiFiComponent::wifi_event_callback_(esphome_wifi_event_id_t event, esphome_
// wifi_sta_connect_status_() to return IDLE. The main loop then sees // wifi_sta_connect_status_() to return IDLE. The main loop then sees
// "Unknown connection status 0" (wifi_component.cpp check_connecting_finished) // "Unknown connection status 0" (wifi_component.cpp check_connecting_finished)
// and calls retry_connect(), aborting a connection that may succeed moments later. // and calls retry_connect(), aborting a connection that may succeed moments later.
// Real connection failures will have ssid/bssid populated, or we'll hit the 30s timeout. // Real connection failures will have ssid/bssid populated, or we'll hit the connection timeout.
if (it.ssid_len == 0 && s_sta_connecting) { if (it.ssid_len == 0 && s_sta_connecting) {
ESP_LOGV(TAG, "Ignoring disconnect event with empty ssid while connecting (reason=%s)", ESP_LOGV(TAG, "Ignoring disconnect event with empty ssid while connecting (reason=%s)",
get_disconnect_reason_str(it.reason)); get_disconnect_reason_str(it.reason));
@@ -527,7 +528,12 @@ bool WiFiComponent::wifi_start_ap_(const WiFiAP &ap) {
network::IPAddress WiFiComponent::wifi_soft_ap_ip() { return {WiFi.softAPIP()}; } network::IPAddress WiFiComponent::wifi_soft_ap_ip() { return {WiFi.softAPIP()}; }
#endif // USE_WIFI_AP #endif // USE_WIFI_AP
bool WiFiComponent::wifi_disconnect_() { return WiFi.disconnect(); } bool WiFiComponent::wifi_disconnect_() {
// Clear connecting flag first so disconnect events aren't ignored
// and wifi_sta_connect_status_() returns IDLE instead of CONNECTING
s_sta_connecting = false;
return WiFi.disconnect();
}
bssid_t WiFiComponent::wifi_bssid() { bssid_t WiFiComponent::wifi_bssid() {
bssid_t bssid{}; bssid_t bssid{};