diff --git a/esphome/components/wifi/wifi_component.cpp b/esphome/components/wifi/wifi_component.cpp index e9b78c9225..0fe98162f3 100644 --- a/esphome/components/wifi/wifi_component.cpp +++ b/esphome/components/wifi/wifi_component.cpp @@ -1470,6 +1470,14 @@ void WiFiComponent::check_connecting_finished(uint32_t now) { this->notify_connect_state_listeners_(); #endif +#if defined(USE_ESP8266) && defined(USE_WIFI_IP_STATE_LISTENERS) && defined(USE_WIFI_MANUAL_IP) + // On ESP8266, GOT_IP event may not fire for static IP configurations, + // so notify IP state listeners here as a fallback. + if (const WiFiAP *config = this->get_selected_sta_(); config && config->get_manual_ip().has_value()) { + this->notify_ip_state_listeners_(); + } +#endif + return; } @@ -1481,7 +1489,11 @@ void WiFiComponent::check_connecting_finished(uint32_t now) { } if (this->error_from_callback_) { + // ESP8266: logging done in callback, listeners deferred via pending_.disconnect + // Other platforms: just log generic failure message +#ifndef USE_ESP8266 ESP_LOGW(TAG, "Connecting to network failed (callback)"); +#endif this->retry_connect(); return; } @@ -2202,8 +2214,31 @@ void WiFiComponent::notify_connect_state_listeners_() { listener->on_wifi_connect_state(StringRef(ssid, strlen(ssid)), bssid); } } + +void WiFiComponent::notify_disconnect_state_listeners_() { + constexpr uint8_t empty_bssid[6] = {}; + for (auto *listener : this->connect_state_listeners_) { + listener->on_wifi_connect_state(StringRef(), empty_bssid); + } +} #endif // USE_WIFI_CONNECT_STATE_LISTENERS +#ifdef USE_WIFI_IP_STATE_LISTENERS +void WiFiComponent::notify_ip_state_listeners_() { + for (auto *listener : this->ip_state_listeners_) { + listener->on_ip_state(this->wifi_sta_ip_addresses(), this->get_dns_address(0), this->get_dns_address(1)); + } +} +#endif // USE_WIFI_IP_STATE_LISTENERS + +#ifdef USE_WIFI_SCAN_RESULTS_LISTENERS +void WiFiComponent::notify_scan_results_listeners_() { + for (auto *listener : this->scan_results_listeners_) { + listener->on_wifi_scan_results(this->scan_result_); + } +} +#endif // USE_WIFI_SCAN_RESULTS_LISTENERS + void WiFiComponent::check_roaming_(uint32_t now) { // Guard: not for hidden networks (may not appear in scan) const WiFiAP *selected = this->get_selected_sta_(); diff --git a/esphome/components/wifi/wifi_component.h b/esphome/components/wifi/wifi_component.h index 98f339809a..58f19c184a 100644 --- a/esphome/components/wifi/wifi_component.h +++ b/esphome/components/wifi/wifi_component.h @@ -594,6 +594,9 @@ class WiFiComponent : public Component { void connect_soon_(); void wifi_loop_(); +#ifdef USE_ESP8266 + void process_pending_callbacks_(); +#endif bool wifi_mode_(optional sta, optional ap); bool wifi_sta_pre_setup_(); bool wifi_apply_output_power_(float output_power); @@ -635,6 +638,16 @@ class WiFiComponent : public Component { #ifdef USE_WIFI_CONNECT_STATE_LISTENERS /// Notify connect state listeners (called after state machine reaches STA_CONNECTED) void notify_connect_state_listeners_(); + /// Notify connect state listeners of disconnection + void notify_disconnect_state_listeners_(); +#endif +#ifdef USE_WIFI_IP_STATE_LISTENERS + /// Notify IP state listeners with current addresses + void notify_ip_state_listeners_(); +#endif +#ifdef USE_WIFI_SCAN_RESULTS_LISTENERS + /// Notify scan results listeners with current scan results + void notify_scan_results_listeners_(); #endif #ifdef USE_ESP8266 @@ -658,13 +671,13 @@ class WiFiComponent : public Component { void wifi_scan_done_callback_(); #endif + // Large/pointer-aligned members first FixedVector sta_; std::vector sta_priorities_; wifi_scan_vector_t scan_result_; #ifdef USE_WIFI_AP WiFiAP ap_; #endif - float output_power_{NAN}; #ifdef USE_WIFI_IP_STATE_LISTENERS StaticVector ip_state_listeners_; #endif @@ -681,6 +694,15 @@ class WiFiComponent : public Component { #ifdef USE_WIFI_FAST_CONNECT ESPPreferenceObject fast_connect_pref_; #endif +#ifdef USE_WIFI_CONNECT_TRIGGER + Trigger<> connect_trigger_; +#endif +#ifdef USE_WIFI_DISCONNECT_TRIGGER + Trigger<> disconnect_trigger_; +#endif +#if defined(USE_ESP32) && defined(USE_WIFI_RUNTIME_POWER_SAVE) + SemaphoreHandle_t high_performance_semaphore_{nullptr}; +#endif // Post-connect roaming constants static constexpr uint32_t ROAMING_CHECK_INTERVAL = 5 * 60 * 1000; // 5 minutes @@ -688,7 +710,8 @@ class WiFiComponent : public Component { static constexpr int8_t ROAMING_GOOD_RSSI = -49; // Skip scan if signal is excellent static constexpr uint8_t ROAMING_MAX_ATTEMPTS = 3; - // Group all 32-bit integers together + // 4-byte members + float output_power_{NAN}; uint32_t action_started_; uint32_t last_connected_{0}; uint32_t reboot_timeout_{}; @@ -697,7 +720,7 @@ class WiFiComponent : public Component { uint32_t ap_timeout_{}; #endif - // Group all 8-bit values together + // 1-byte enums and integers WiFiComponentState state_{WIFI_COMPONENT_STATE_OFF}; WiFiPowerSaveMode power_save_{WIFI_POWER_SAVE_NONE}; WifiMinAuthMode min_auth_mode_{WIFI_MIN_AUTH_MODE_WPA2}; @@ -708,17 +731,39 @@ class WiFiComponent : public Component { // int8_t limits to 127 APs (enforced in __init__.py via MAX_WIFI_NETWORKS) int8_t selected_sta_index_{-1}; uint8_t roaming_attempts_{0}; - #if USE_NETWORK_IPV6 uint8_t num_ipv6_addresses_{0}; #endif /* USE_NETWORK_IPV6 */ + bool error_from_callback_{false}; + RetryHiddenMode retry_hidden_mode_{RetryHiddenMode::BLIND_RETRY}; + RoamingState roaming_state_{RoamingState::IDLE}; +#if defined(USE_ESP32) && defined(USE_WIFI_RUNTIME_POWER_SAVE) + WiFiPowerSaveMode configured_power_save_{WIFI_POWER_SAVE_NONE}; +#endif - // Group all boolean values together + // Bools and bitfields + // Pending listener callbacks deferred from platform callbacks to main loop. + struct { +#ifdef USE_WIFI_CONNECT_STATE_LISTENERS + // Deferred until state machine reaches STA_CONNECTED so wifi.connected + // condition returns true in listener automations. + bool connect_state : 1; +#ifdef USE_ESP8266 + // ESP8266: also defer disconnect notification to main loop + bool disconnect : 1; +#endif +#endif +#if defined(USE_ESP8266) && defined(USE_WIFI_IP_STATE_LISTENERS) + bool got_ip : 1; +#endif +#if defined(USE_ESP8266) && defined(USE_WIFI_SCAN_RESULTS_LISTENERS) + bool scan_complete : 1; +#endif + } pending_{}; bool has_ap_{false}; #if defined(USE_WIFI_CONNECT_TRIGGER) || defined(USE_WIFI_DISCONNECT_TRIGGER) bool handled_connected_state_{false}; #endif - bool error_from_callback_{false}; bool scan_done_{false}; bool ap_setup_{false}; bool ap_started_{false}; @@ -733,32 +778,10 @@ class WiFiComponent : public Component { bool keep_scan_results_{false}; bool has_completed_scan_after_captive_portal_start_{ false}; // Tracks if we've completed a scan after captive portal started - RetryHiddenMode retry_hidden_mode_{RetryHiddenMode::BLIND_RETRY}; bool skip_cooldown_next_cycle_{false}; bool post_connect_roaming_{true}; // Enabled by default - RoamingState roaming_state_{RoamingState::IDLE}; #if defined(USE_ESP32) && defined(USE_WIFI_RUNTIME_POWER_SAVE) - WiFiPowerSaveMode configured_power_save_{WIFI_POWER_SAVE_NONE}; bool is_high_performance_mode_{false}; - - SemaphoreHandle_t high_performance_semaphore_{nullptr}; -#endif - -#ifdef USE_WIFI_CONNECT_STATE_LISTENERS - // Pending listener notifications deferred until state machine reaches appropriate state. - // Listeners are notified after state transitions complete so conditions like - // wifi.connected return correct values in automations. - // Uses bitfields to minimize memory; more flags may be added as needed. - struct { - bool connect_state : 1; // Notify connect state listeners after STA_CONNECTED - } pending_{}; -#endif - -#ifdef USE_WIFI_CONNECT_TRIGGER - Trigger<> connect_trigger_; -#endif -#ifdef USE_WIFI_DISCONNECT_TRIGGER - Trigger<> disconnect_trigger_; #endif private: diff --git a/esphome/components/wifi/wifi_component_esp8266.cpp b/esphome/components/wifi/wifi_component_esp8266.cpp index 0765fdc03b..6e2adcbf04 100644 --- a/esphome/components/wifi/wifi_component_esp8266.cpp +++ b/esphome/components/wifi/wifi_component_esp8266.cpp @@ -506,16 +506,6 @@ void WiFiComponent::wifi_event_callback(System_Event_t *event) { // Defer listener notification until state machine reaches STA_CONNECTED // This ensures wifi.connected condition returns true in listener automations global_wifi_component->pending_.connect_state = true; -#endif - // For static IP configurations, GOT_IP event may not fire, so notify IP listeners here -#if defined(USE_WIFI_IP_STATE_LISTENERS) && defined(USE_WIFI_MANUAL_IP) - if (const WiFiAP *config = global_wifi_component->get_selected_sta_(); - config && config->get_manual_ip().has_value()) { - for (auto *listener : global_wifi_component->ip_state_listeners_) { - listener->on_ip_state(global_wifi_component->wifi_sta_ip_addresses(), - global_wifi_component->get_dns_address(0), global_wifi_component->get_dns_address(1)); - } - } #endif break; } @@ -534,16 +524,9 @@ void WiFiComponent::wifi_event_callback(System_Event_t *event) { } s_sta_connected = false; s_sta_connecting = false; - // IMPORTANT: Set error flag BEFORE notifying listeners. - // This ensures is_connected() returns false during listener callbacks, - // which is critical for proper reconnection logic (e.g., roaming). global_wifi_component->error_from_callback_ = true; #ifdef USE_WIFI_CONNECT_STATE_LISTENERS - // Notify listeners AFTER setting error flag so they see correct state - static constexpr uint8_t EMPTY_BSSID[6] = {}; - for (auto *listener : global_wifi_component->connect_state_listeners_) { - listener->on_wifi_connect_state(StringRef(), EMPTY_BSSID); - } + global_wifi_component->pending_.disconnect = true; #endif break; } @@ -555,8 +538,6 @@ void WiFiComponent::wifi_event_callback(System_Event_t *event) { // https://lbsfilm.at/blog/wpa2-authenticationmode-downgrade-in-espressif-microprocessors if (it.old_mode != AUTH_OPEN && it.new_mode == AUTH_OPEN) { ESP_LOGW(TAG, "Potential Authmode downgrade detected, disconnecting"); - // we can't call retry_connect() from this context, so disconnect immediately - // and notify main thread with error_from_callback_ wifi_station_disconnect(); global_wifi_component->error_from_callback_ = true; } @@ -570,10 +551,8 @@ void WiFiComponent::wifi_event_callback(System_Event_t *event) { network::IPAddress(&it.gw).str_to(gw_buf), network::IPAddress(&it.mask).str_to(mask_buf)); s_sta_got_ip = true; #ifdef USE_WIFI_IP_STATE_LISTENERS - for (auto *listener : global_wifi_component->ip_state_listeners_) { - listener->on_ip_state(global_wifi_component->wifi_sta_ip_addresses(), global_wifi_component->get_dns_address(0), - global_wifi_component->get_dns_address(1)); - } + // Defer listener callbacks to main loop - system context has limited stack + global_wifi_component->pending_.got_ip = true; #endif break; } @@ -785,9 +764,7 @@ void WiFiComponent::wifi_scan_done_callback_(void *arg, STATUS status) { needs_full ? "" : " (filtered)"); this->scan_done_ = true; #ifdef USE_WIFI_SCAN_RESULTS_LISTENERS - for (auto *listener : global_wifi_component->scan_results_listeners_) { - listener->on_wifi_scan_results(global_wifi_component->scan_result_); - } + this->pending_.scan_complete = true; // Defer listener callbacks to main loop #endif } @@ -974,7 +951,34 @@ network::IPAddress WiFiComponent::wifi_gateway_ip_() { return network::IPAddress(&ip.gw); } network::IPAddress WiFiComponent::wifi_dns_ip_(int num) { return network::IPAddress(dns_getserver(num)); } -void WiFiComponent::wifi_loop_() {} +void WiFiComponent::wifi_loop_() { this->process_pending_callbacks_(); } + +void WiFiComponent::process_pending_callbacks_() { + // Process callbacks deferred from ESP8266 SDK system context (~2KB stack) + // to main loop context (full stack). Connect state listeners are handled + // by notify_connect_state_listeners_() in the shared state machine code. + +#ifdef USE_WIFI_CONNECT_STATE_LISTENERS + if (this->pending_.disconnect) { + this->pending_.disconnect = false; + this->notify_disconnect_state_listeners_(); + } +#endif + +#ifdef USE_WIFI_IP_STATE_LISTENERS + if (this->pending_.got_ip) { + this->pending_.got_ip = false; + this->notify_ip_state_listeners_(); + } +#endif + +#ifdef USE_WIFI_SCAN_RESULTS_LISTENERS + if (this->pending_.scan_complete) { + this->pending_.scan_complete = false; + this->notify_scan_results_listeners_(); + } +#endif +} } // namespace esphome::wifi #endif diff --git a/esphome/components/wifi/wifi_component_esp_idf.cpp b/esphome/components/wifi/wifi_component_esp_idf.cpp index 22bf4be483..d74d083954 100644 --- a/esphome/components/wifi/wifi_component_esp_idf.cpp +++ b/esphome/components/wifi/wifi_component_esp_idf.cpp @@ -753,9 +753,7 @@ void WiFiComponent::wifi_process_event_(IDFWiFiEvent *data) { // For static IP configurations, GOT_IP event may not fire, so notify IP listeners here #if defined(USE_WIFI_IP_STATE_LISTENERS) && defined(USE_WIFI_MANUAL_IP) if (const WiFiAP *config = this->get_selected_sta_(); config && config->get_manual_ip().has_value()) { - for (auto *listener : this->ip_state_listeners_) { - listener->on_ip_state(this->wifi_sta_ip_addresses(), this->get_dns_address(0), this->get_dns_address(1)); - } + this->notify_ip_state_listeners_(); } #endif @@ -779,10 +777,7 @@ void WiFiComponent::wifi_process_event_(IDFWiFiEvent *data) { s_sta_connecting = false; error_from_callback_ = true; #ifdef USE_WIFI_CONNECT_STATE_LISTENERS - static constexpr uint8_t EMPTY_BSSID[6] = {}; - for (auto *listener : this->connect_state_listeners_) { - listener->on_wifi_connect_state(StringRef(), EMPTY_BSSID); - } + this->notify_disconnect_state_listeners_(); #endif } else if (data->event_base == IP_EVENT && data->event_id == IP_EVENT_STA_GOT_IP) { @@ -793,9 +788,7 @@ void WiFiComponent::wifi_process_event_(IDFWiFiEvent *data) { ESP_LOGV(TAG, "static_ip=" IPSTR " gateway=" IPSTR, IP2STR(&it.ip_info.ip), IP2STR(&it.ip_info.gw)); this->got_ipv4_address_ = true; #ifdef USE_WIFI_IP_STATE_LISTENERS - for (auto *listener : this->ip_state_listeners_) { - listener->on_ip_state(this->wifi_sta_ip_addresses(), this->get_dns_address(0), this->get_dns_address(1)); - } + this->notify_ip_state_listeners_(); #endif #if USE_NETWORK_IPV6 @@ -804,9 +797,7 @@ void WiFiComponent::wifi_process_event_(IDFWiFiEvent *data) { ESP_LOGV(TAG, "IPv6 address=" IPV6STR, IPV62STR(it.ip6_info.ip)); this->num_ipv6_addresses_++; #ifdef USE_WIFI_IP_STATE_LISTENERS - for (auto *listener : this->ip_state_listeners_) { - listener->on_ip_state(this->wifi_sta_ip_addresses(), this->get_dns_address(0), this->get_dns_address(1)); - } + this->notify_ip_state_listeners_(); #endif #endif /* USE_NETWORK_IPV6 */ @@ -883,9 +874,7 @@ void WiFiComponent::wifi_process_event_(IDFWiFiEvent *data) { ESP_LOGV(TAG, "Scan complete: %u found, %zu stored%s", number, this->scan_result_.size(), needs_full ? "" : " (filtered)"); #ifdef USE_WIFI_SCAN_RESULTS_LISTENERS - for (auto *listener : this->scan_results_listeners_) { - listener->on_wifi_scan_results(this->scan_result_); - } + this->notify_scan_results_listeners_(); #endif } else if (data->event_base == WIFI_EVENT && data->event_id == WIFI_EVENT_AP_START) { diff --git a/esphome/components/wifi/wifi_component_libretiny.cpp b/esphome/components/wifi/wifi_component_libretiny.cpp index 285a520ef5..5fd9d7663b 100644 --- a/esphome/components/wifi/wifi_component_libretiny.cpp +++ b/esphome/components/wifi/wifi_component_libretiny.cpp @@ -468,9 +468,7 @@ void WiFiComponent::wifi_process_event_(LTWiFiEvent *event) { if (const WiFiAP *config = this->get_selected_sta_(); config && config->get_manual_ip().has_value()) { s_sta_state = LTWiFiSTAState::CONNECTED; #ifdef USE_WIFI_IP_STATE_LISTENERS - for (auto *listener : this->ip_state_listeners_) { - listener->on_ip_state(this->wifi_sta_ip_addresses(), this->get_dns_address(0), this->get_dns_address(1)); - } + this->notify_ip_state_listeners_(); #endif } #endif @@ -527,10 +525,7 @@ void WiFiComponent::wifi_process_event_(LTWiFiEvent *event) { } #ifdef USE_WIFI_CONNECT_STATE_LISTENERS - static constexpr uint8_t EMPTY_BSSID[6] = {}; - for (auto *listener : this->connect_state_listeners_) { - listener->on_wifi_connect_state(StringRef(), EMPTY_BSSID); - } + this->notify_disconnect_state_listeners_(); #endif break; } @@ -553,18 +548,14 @@ void WiFiComponent::wifi_process_event_(LTWiFiEvent *event) { network::IPAddress(WiFi.gatewayIP()).str_to(gw_buf)); s_sta_state = LTWiFiSTAState::CONNECTED; #ifdef USE_WIFI_IP_STATE_LISTENERS - for (auto *listener : this->ip_state_listeners_) { - listener->on_ip_state(this->wifi_sta_ip_addresses(), this->get_dns_address(0), this->get_dns_address(1)); - } + this->notify_ip_state_listeners_(); #endif break; } case ESPHOME_EVENT_ID_WIFI_STA_GOT_IP6: { ESP_LOGV(TAG, "Got IPv6"); #ifdef USE_WIFI_IP_STATE_LISTENERS - for (auto *listener : this->ip_state_listeners_) { - listener->on_ip_state(this->wifi_sta_ip_addresses(), this->get_dns_address(0), this->get_dns_address(1)); - } + this->notify_ip_state_listeners_(); #endif break; } @@ -708,9 +699,7 @@ void WiFiComponent::wifi_scan_done_callback_() { needs_full ? "" : " (filtered)"); WiFi.scanDelete(); #ifdef USE_WIFI_SCAN_RESULTS_LISTENERS - for (auto *listener : this->scan_results_listeners_) { - listener->on_wifi_scan_results(this->scan_result_); - } + this->notify_scan_results_listeners_(); #endif } diff --git a/esphome/components/wifi/wifi_component_pico_w.cpp b/esphome/components/wifi/wifi_component_pico_w.cpp index 1ce36c2d93..818ad1059c 100644 --- a/esphome/components/wifi/wifi_component_pico_w.cpp +++ b/esphome/components/wifi/wifi_component_pico_w.cpp @@ -264,9 +264,7 @@ void WiFiComponent::wifi_loop_() { ESP_LOGV(TAG, "Scan complete: %zu found, %zu stored%s", s_scan_result_count, this->scan_result_.size(), needs_full ? "" : " (filtered)"); #ifdef USE_WIFI_SCAN_RESULTS_LISTENERS - for (auto *listener : this->scan_results_listeners_) { - listener->on_wifi_scan_results(this->scan_result_); - } + this->notify_scan_results_listeners_(); #endif } @@ -290,9 +288,7 @@ void WiFiComponent::wifi_loop_() { #if defined(USE_WIFI_IP_STATE_LISTENERS) && defined(USE_WIFI_MANUAL_IP) if (const WiFiAP *config = this->get_selected_sta_(); config && config->get_manual_ip().has_value()) { s_sta_had_ip = true; - for (auto *listener : this->ip_state_listeners_) { - listener->on_ip_state(this->wifi_sta_ip_addresses(), this->get_dns_address(0), this->get_dns_address(1)); - } + this->notify_ip_state_listeners_(); } #endif } else if (!is_connected && s_sta_was_connected) { @@ -301,10 +297,7 @@ void WiFiComponent::wifi_loop_() { s_sta_had_ip = false; ESP_LOGV(TAG, "Disconnected"); #ifdef USE_WIFI_CONNECT_STATE_LISTENERS - static constexpr uint8_t EMPTY_BSSID[6] = {}; - for (auto *listener : this->connect_state_listeners_) { - listener->on_wifi_connect_state(StringRef(), EMPTY_BSSID); - } + this->notify_disconnect_state_listeners_(); #endif } @@ -322,9 +315,7 @@ void WiFiComponent::wifi_loop_() { s_sta_had_ip = true; ESP_LOGV(TAG, "Got IP address"); #ifdef USE_WIFI_IP_STATE_LISTENERS - for (auto *listener : this->ip_state_listeners_) { - listener->on_ip_state(this->wifi_sta_ip_addresses(), this->get_dns_address(0), this->get_dns_address(1)); - } + this->notify_ip_state_listeners_(); #endif } }