diff --git a/esphome/components/mdns/__init__.py b/esphome/components/mdns/__init__.py index f039bb69f03..676bb890b3f 100644 --- a/esphome/components/mdns/__init__.py +++ b/esphome/components/mdns/__init__.py @@ -192,6 +192,8 @@ async def to_code(config: ConfigType) -> None: if CORE.using_arduino: if CORE.is_esp8266: cg.add_library("ESP8266mDNS", None) + # mdns_esp8266.cpp owns a guarded MDNSResponder instead of the library global + cg.add_build_flag("-DNO_GLOBAL_MDNS") elif CORE.is_rp2: cg.add_library("LEAmDNS", None) diff --git a/esphome/components/mdns/mdns_esp8266.cpp b/esphome/components/mdns/mdns_esp8266.cpp index 1f0b3c9519d..684c55c4bc8 100644 --- a/esphome/components/mdns/mdns_esp8266.cpp +++ b/esphome/components/mdns/mdns_esp8266.cpp @@ -13,8 +13,56 @@ namespace esphome::mdns { +// Main-loop calls into LEAmDNS (update(), close()) can yield inside UdpContext::sendTimeout(); +// a packet arriving then re-enters LEAmDNS from lwIP on the same UdpContext and both sides free +// the same tx pbufs (#18760). Received packets are only counted during such a call and drained +// from the main loop afterwards. +class GuardedMDNSResponder : public ::esp8266::MDNSImplementation::MDNSResponder { + public: + void update_guarded() { + this->run_guarded_([this]() { this->update(); }); + } + void close_guarded() { + this->run_guarded_([this]() { this->close(); }); + } + + private: + template void run_guarded_(F &&fn) { + UdpContext *ctx = this->m_pUDPContext; + if (ctx == nullptr) { + fn(); + return; + } + // Set every time: a restart replaces the context together with its stock handler + ctx->onRx([this]() { this->on_rx_(); }); + this->pending_rx_ = 0; + this->in_loop_call_ = true; + fn(); + // Still counting here, so packets arriving during a yield in the drain queue behind it + while (this->pending_rx_ > 0) { + this->pending_rx_--; + this->_process(false); + } + this->in_loop_call_ = false; + } + + // lwIP receive callback (SYS context) + void on_rx_() { + if (this->in_loop_call_) { + this->pending_rx_++; + } else { + this->_callProcess(); + } + } + + volatile bool in_loop_call_{false}; + volatile uint8_t pending_rx_{0}; +}; + +static GuardedMDNSResponder mdns_responder; // NOLINT(cppcoreguidelines-avoid-non-const-global-variables) + static void register_esp8266(MDNSComponent *, StaticVector &services) { - MDNS.begin(App.get_name().c_str()); + mdns_responder.begin(App.get_name().c_str()); for (const auto &service : services) { // Strip the leading underscore from the proto and service_type. While it is @@ -30,10 +78,10 @@ static void register_esp8266(MDNSComponent *, StaticVectorset_interval(MDNS_POLL_ID, MDNS_UPDATE_INTERVAL_MS, []() { -#ifdef USE_MDNS_WIFI_LISTENER - // MDNS.update() can suspend the loop in UdpContext::sendTimeout() while a send is - // failing (radio off-channel during a roam scan, or mid reconnect); an incoming - // packet then re-enters LEAmDNS from lwIP and corrupts shared UdpContext state. - // Skip the tick while the radio cannot transmit (#18760), but keep polling while - // the AP is serving clients (AP-only or fallback AP with the STA down). - auto *wifi = wifi::global_wifi_component; - if (wifi->is_roaming() || (!wifi->is_connected() && !wifi->is_ap_active())) - return; -#endif - MDNS.update(); - }); + this->set_interval(MDNS_POLL_ID, MDNS_UPDATE_INTERVAL_MS, []() { mdns_responder.update_guarded(); }); this->set_timeout(MDNS_POLL_STOP_ID, MDNS_POLL_WINDOW_MS, [this]() { this->cancel_interval(MDNS_POLL_ID); }); } #endif @@ -81,7 +117,7 @@ void MDNSComponent::on_ip_state(const network::IPAddresses &ips, const network:: #endif void MDNSComponent::on_shutdown() { - MDNS.close(); + mdns_responder.close_guarded(); delay(10); } diff --git a/esphome/components/wifi/wifi_component.h b/esphome/components/wifi/wifi_component.h index 94fdd9bc142..2b1bc62e131 100644 --- a/esphome/components/wifi/wifi_component.h +++ b/esphome/components/wifi/wifi_component.h @@ -476,10 +476,6 @@ class WiFiComponent final : public Component { /// True while a post-connect roaming scan holds the radio off-channel. bool is_roaming_scan_active() const { return this->roaming_state_ == RoamingState::SCANNING; } - /// True while a post-connect roam is in progress (scanning off-channel, reassociating, - /// or recovering from a failed roam). - bool is_roaming() const { return this->roaming_state_ != RoamingState::IDLE; } - #ifdef USE_ESP32 /// esp_netif handle of the station interface, used by network for default-route /// arbitration. nullptr until wifi_lazy_init_() has run.