1
0
mirror of https://github.com/esphome/esphome.git synced 2026-02-08 00:31:58 +00:00

[wifi] Defer ESP8266 WiFi listener callbacks from system context to main loop (#13789)

This commit is contained in:
J. Nick Koston
2026-02-06 17:23:19 +01:00
committed by GitHub
parent c3622ef7fb
commit ec477801ca
6 changed files with 132 additions and 101 deletions

View File

@@ -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_();

View File

@@ -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<bool> sta, optional<bool> 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<WiFiAP> sta_;
std::vector<WiFiSTAPriority> sta_priorities_;
wifi_scan_vector_t<WiFiScanResult> scan_result_;
#ifdef USE_WIFI_AP
WiFiAP ap_;
#endif
float output_power_{NAN};
#ifdef USE_WIFI_IP_STATE_LISTENERS
StaticVector<WiFiIPStateListener *, ESPHOME_WIFI_IP_STATE_LISTENERS> 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:

View File

@@ -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

View File

@@ -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) {

View File

@@ -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
}

View File

@@ -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
}
}