From 6011a109936e11f6fb56b3d2f586acb164882d82 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Thu, 17 Sep 2026 16:37:48 -0500 Subject: [PATCH] [wifi] Keep the ESP8266 SDK from hanging when its small management frame pool is empty With power_save_mode NONE the SDK sends a null function frame every 10 s from its power management timer. The 24 byte frame comes from a fixed pool of eight small buffers; when that pool is empty ieee80211_getmgtframe returns NULL and pm_send_nullfunc asserts into an endless loop until the soft watchdog resets the device. Wrap the allocator so an empty pool falls back to the heap backed path the SDK already uses for larger frames, and log when that happens. --- esphome/components/wifi/__init__.py | 4 +++ .../wifi/wifi_component_esp8266.cpp | 27 +++++++++++++++++++ 2 files changed, 31 insertions(+) diff --git a/esphome/components/wifi/__init__.py b/esphome/components/wifi/__init__.py index c22d49e6654..e6790dc9d54 100644 --- a/esphome/components/wifi/__init__.py +++ b/esphome/components/wifi/__init__.py @@ -739,6 +739,10 @@ async def to_code(config): if CORE.is_esp8266: cg.add_library("ESP8266WiFi", None) + # Route the SDK's management frame allocator through the wrapper in + # wifi_component_esp8266.cpp so an empty small frame pool does not hang + # the device in pm_send_nullfunc. + cg.add_build_flag("-Wl,--wrap=ieee80211_getmgtframe") if CONF_PHY_MODE in config: cg.add_define("USE_WIFI_PHY_MODE") cg.add(var.set_phy_mode(config[CONF_PHY_MODE])) diff --git a/esphome/components/wifi/wifi_component_esp8266.cpp b/esphome/components/wifi/wifi_component_esp8266.cpp index 60ec3f9a4d5..98742d16c2c 100644 --- a/esphome/components/wifi/wifi_component_esp8266.cpp +++ b/esphome/components/wifi/wifi_component_esp8266.cpp @@ -38,6 +38,28 @@ extern "C" { #include "esphome/core/progmem.h" #include "esphome/core/util.h" +// Management frames of 64 bytes or less come from a fixed pool of eight SDK buffers. +// Larger frames are allocated from the heap and freed on completion. +static constexpr int SMALL_MGMT_FRAME_MAX = 64; +// Times the small pool was empty and a heap buffer was handed out instead. +static uint8_t small_mgmt_pool_exhausted_count = 0; + +extern "C" { +void *__real_ieee80211_getmgtframe(void **frm, int headroom, int pktlen); + +// When the small pool is empty the SDK returns nullptr and pm_send_nullfunc spins in an +// assert until the soft watchdog resets the device (#19371). Retry with a size just above +// the pool limit so the SDK takes the heap backed path it already uses for larger frames. +void *__wrap_ieee80211_getmgtframe(void **frm, int headroom, int pktlen) { + void *buf = __real_ieee80211_getmgtframe(frm, headroom, pktlen); + if (buf != nullptr || headroom + pktlen > SMALL_MGMT_FRAME_MAX) + return buf; + if (small_mgmt_pool_exhausted_count != UINT8_MAX) + small_mgmt_pool_exhausted_count++; + return __real_ieee80211_getmgtframe(frm, headroom, SMALL_MGMT_FRAME_MAX + 1 - headroom); +} +} + namespace esphome::wifi { static const char *const TAG = "wifi_esp8266"; @@ -981,6 +1003,11 @@ void WiFiComponent::process_pending_callbacks_() { // to main loop context (full stack). Connect state listeners are handled // by notify_connect_state_listeners_() in the shared state machine code. + if (small_mgmt_pool_exhausted_count != 0) { + ESP_LOGW(TAG, "SDK small management frame pool empty %u times, used heap instead", small_mgmt_pool_exhausted_count); + small_mgmt_pool_exhausted_count = 0; + } + #ifdef USE_WIFI_CONNECT_STATE_LISTENERS if (this->pending_.disconnect) { this->pending_.disconnect = false;