[mdns] Drop fallback paths, collapse everything under USE_MDNS_EVENT_DRIVEN_POLLING

mDNS on ESP8266/RP2040 always runs over a network interface (WiFi on ESP8266;
WiFi or W5500/etc. ethernet on RP2040), and every such interface already
publishes an IP state listener in tree. USE_MDNS_EVENT_DRIVEN_POLLING is
therefore always defined in production, so the fallback set_interval() paths
in both platform files and the was_connected_ bookkeeping are dead code.

Also drop the ethernet-specific test — the existing wifi and ethernet+mdns
combos are already covered by the mdns test fixtures paired with their network
component tests.

Trim redundant comments throughout: the header now documents the ~9s
probe/announce window and why update() can stop afterward in a few lines
instead of a full essay; platform files keep only the non-obvious bits (why
RP2040 needs to drive begin()/notifyAPChange() itself, why re-arming on any
listener notification is correct).
This commit is contained in:
J. Nick Koston
2026-04-23 19:57:48 -05:00
parent ceada86325
commit 09fe59c5cc
7 changed files with 29 additions and 144 deletions
+3 -9
View File
@@ -169,15 +169,9 @@ async def to_code(config):
elif CORE.is_rp2040:
cg.add_library("LEAmDNS", None)
# ESP8266 and RP2040 use a network IP state listener to arm a bounded
# MDNS.update() polling window only while the library is in its probe+announce
# phase. This eliminates the steady-state 50ms interval that ran forever
# (1200+ dispatches per minute) and its scheduler overhead.
#
# We subscribe to any listener interface that's configured (WiFi and/or
# Ethernet); the same on_ip_state() override satisfies both because the
# listener signatures match. If neither is available the component falls
# back to the legacy polling loop at MDNS_UPDATE_INTERVAL_MS.
# Subscribe to the network IP state listener(s) so MDNS.update() is only
# scheduled during the probe+announce phase. Same on_ip_state() override
# serves both WiFi and Ethernet (signatures match).
if CORE.is_esp8266 or CORE.is_rp2040:
if "wifi" in CORE.config:
from esphome.components import wifi
+2 -3
View File
@@ -192,9 +192,8 @@ void MDNSComponent::compile_records_(StaticVector<MDNSService, MDNS_SERVICE_COUN
#ifdef USE_MDNS_EVENT_DRIVEN_POLLING
void MDNSComponent::start_polling_window_() {
// Re-arming replaces the previous window. The scheduler's set_interval/set_timeout
// with a uint32_t ID already does atomic cancel-and-add for items sharing that ID
// (see Scheduler::set_timer_common_), so no explicit cancel is needed.
// uint32_t-ID set_interval/set_timeout already does atomic cancel-and-add, so
// re-arming replaces the previous window implicitly.
this->set_interval(MDNS_POLL_ID, MDNS_UPDATE_INTERVAL_MS, mdns_pump_update);
this->set_timeout(MDNS_POLL_STOP_ID, MDNS_POLL_WINDOW_MS, [this]() { this->cancel_interval(MDNS_POLL_ID); });
}
+12 -35
View File
@@ -5,12 +5,8 @@
#include "esphome/core/automation.h"
#include "esphome/core/component.h"
#include "esphome/core/helpers.h"
// Event-driven polling replaces the legacy set_interval() loop on platforms that need
// scheduler-backed MDNS.update() (ESP8266, RP2040). It's enabled when at least one
// compatible network IP state listener slot is available — the mdns Python to_code()
// requests a WiFi slot when WiFi is in the config and an Ethernet slot when Ethernet
// is in the config. If neither is available (e.g. clang-tidy running without full
// codegen), the component falls back to the legacy polling loop.
// On ESP8266 and RP2040 the scheduler-backed MDNS.update() polling window is armed by
// IP state listener events on whichever network interface is configured.
#if (defined(USE_ESP8266) || defined(USE_RP2040)) && \
((defined(USE_WIFI) && defined(USE_WIFI_IP_STATE_LISTENERS)) || \
(defined(USE_ETHERNET) && defined(USE_ETHERNET_IP_STATE_LISTENERS)))
@@ -29,9 +25,8 @@
namespace esphome::mdns {
#ifdef USE_MDNS_EVENT_DRIVEN_POLLING
/// Call MDNS.update() on the target platform. Defined in the per-platform cpp file so
/// the shared component code can drive the polling window without pulling in
/// platform-specific mDNS headers.
/// Platform-specific MDNS.update() trampoline. Defined in mdns_<platform>.cpp so the
/// shared code can schedule it without including the platform's mDNS header.
void mdns_pump_update();
#endif
@@ -81,28 +76,13 @@ class MDNSComponent final : public Component
void setup() override;
void dump_config() override;
// On ESP8266 and RP2040, MDNS.update() calls _process(true) which only manages
// timer-driven state machines (probe/announce timeouts and service query cache TTLs).
// Incoming mDNS packets are handled independently via the lwIP onRx UDP callback and
// are NOT affected by how often update() is called.
//
// The work has a bounded lifetime: after MDNS.begin() (or _restart() triggered by a
// network interface change) the library sends 3 probes 250ms apart followed by 8
// announcements 1000ms apart, after which all internal timeouts are set to
// resetToNeverExpires(). ESPHome does not issue mDNS service queries, so the service
// query cache is always empty. Every subsequent update() call is pure overhead.
//
// When USE_MDNS_EVENT_DRIVEN_POLLING is defined we arm a bounded polling window from
// WiFiIPStateListener events so update() only runs during the probe+announce phase;
// outside that window no update() calls occur. Otherwise (fallback), we poll at
// MDNS_UPDATE_INTERVAL_MS forever.
static constexpr uint32_t MDNS_UPDATE_INTERVAL_MS = 50;
#ifdef USE_MDNS_EVENT_DRIVEN_POLLING
// Boot probe+announce phase is ~9.0s (3*250ms probes + 8*1000ms announces). Window
// includes margin for the initial `rand() % MDNS_PROBE_DELAY` jitter and for the
// debounced internal restart triggered by netif status changes on ESP8266.
static constexpr uint32_t MDNS_POLL_WINDOW_MS = 12000;
// Scheduler IDs (uint32_t variants avoid name hashing/strcmp on cancel paths)
// LEAmDNS has meaningful work only during the probe+announce phase (3×250ms probes +
// 8×1000ms announces, ~9s). Afterwards every internal timer is resetToNeverExpires()
// and update() becomes pure overhead. We arm a bounded polling window from IP state
// listener events so update() runs only during that phase.
static constexpr uint32_t MDNS_UPDATE_INTERVAL_MS = 50;
static constexpr uint32_t MDNS_POLL_WINDOW_MS = 12000; // ~9s phase + jitter/restart margin
static constexpr uint32_t MDNS_POLL_ID = 0;
static constexpr uint32_t MDNS_POLL_STOP_ID = 1;
#endif
@@ -133,8 +113,8 @@ class MDNSComponent final : public Component
protected:
#ifdef USE_MDNS_EVENT_DRIVEN_POLLING
/// Arm a bounded polling window so MDNS.update() runs at MDNS_UPDATE_INTERVAL_MS
/// for MDNS_POLL_WINDOW_MS. A subsequent call replaces the previous window.
/// Arm a fresh MDNS_POLL_WINDOW_MS polling window. Idempotent — re-arming replaces
/// the previous window via the scheduler's atomic cancel-and-add on matching IDs.
void start_polling_window_();
#endif
/// Helper to set up services and MAC buffers, then call platform-specific registration
@@ -181,9 +161,6 @@ class MDNSComponent final : public Component
#endif
#ifdef USE_RP2040
bool initialized_{false};
#if !defined(USE_MDNS_EVENT_DRIVEN_POLLING)
bool was_connected_{false};
#endif
#endif
void compile_records_(StaticVector<MDNSService, MDNS_SERVICE_COUNT> &services, char *mac_address_buf);
};
+4 -23
View File
@@ -8,9 +8,7 @@
#include "esphome/core/hal.h"
#include "esphome/core/log.h"
#include "mdns_component.h"
#ifdef USE_MDNS_WIFI_LISTENER
#include "esphome/components/wifi/wifi_component.h"
#endif
namespace esphome::mdns {
@@ -39,41 +37,24 @@ static void register_esp8266(MDNSComponent *, StaticVector<MDNSService, MDNS_SER
}
}
#ifdef USE_MDNS_EVENT_DRIVEN_POLLING
void mdns_pump_update() { MDNS.update(); }
#endif
void MDNSComponent::setup() {
this->setup_buffers_and_register_(register_esp8266);
#ifdef USE_MDNS_EVENT_DRIVEN_POLLING
// Arduino LEAmDNS registers its own LwipIntf::statusChangeCB that calls _restart()
// on every netif status change (link up, IP up, etc.), so we don't trigger begin()
// or restart here — we just cover the probe+announce window with a bounded polling
// schedule. The listener catches subsequent reconnects and re-arms the window.
// ESP8266 has no ethernet driver in the Arduino build so only the WiFi listener
// branch is reachable here.
// LEAmDNS's own LwipIntf::statusChangeCB drives _restart() on netif changes; we only
// need to arm the polling window around the initial probe/announce and each reconnect.
wifi::global_wifi_component->add_ip_state_listener(this);
this->start_polling_window_();
#else
// Fallback for builds without a WiFi IP state listener (e.g. clang-tidy without
// codegen defines). Matches the pre-PR behaviour: poll forever at 50ms.
this->set_interval(MDNS_UPDATE_INTERVAL_MS, []() { MDNS.update(); });
#endif
}
#ifdef USE_MDNS_EVENT_DRIVEN_POLLING
void MDNSComponent::on_ip_state(const network::IPAddresses &ips, const network::IPAddress &,
const network::IPAddress &) {
// ESPHome's WiFiIPStateListener only notifies on IP acquisition (GOT_IP events on
// ESP8266 — see wifi_component_esp8266.cpp), not on IP loss, so every notification
// represents a fresh IP that the LEAmDNS library's lwIP callback will trigger a
// _restart() for. Always re-arm the polling window — start_polling_window_() is
// idempotent (scheduler does atomic cancel-and-add on matching IDs).
// IP listener only fires on acquisition (not loss), so any notification is a fresh
// IP worth re-arming for. start_polling_window_() is idempotent.
if (ips[0].is_set()) {
this->start_polling_window_();
}
}
#endif
void MDNSComponent::on_shutdown() {
MDNS.close();
+8 -50
View File
@@ -14,7 +14,6 @@
#endif
// Arduino-Pico's PolledTimeout.h (pulled in by ESP8266mDNS.h) redefines IRAM_ATTR to empty.
// Save and restore our definition around the include to avoid a redefinition warning.
#pragma push_macro("IRAM_ATTR")
#undef IRAM_ATTR
#include <ESP8266mDNS.h>
@@ -26,10 +25,7 @@ static void register_rp2040(MDNSComponent *, StaticVector<MDNSService, MDNS_SERV
MDNS.begin(App.get_name().c_str());
for (const auto &service : services) {
// Strip the leading underscore from the proto and service_type. While it is
// part of the wire protocol to have an underscore, and for example ESP-IDF
// expects the underscore to be there, the ESP8266 implementation always adds
// the underscore itself.
// ESP8266mDNS always adds the leading underscore itself, so strip it here.
auto *proto = MDNS_STR_ARG(service.proto);
while (*proto == '_') {
proto++;
@@ -46,28 +42,17 @@ static void register_rp2040(MDNSComponent *, StaticVector<MDNSService, MDNS_SERV
}
}
#ifdef USE_MDNS_EVENT_DRIVEN_POLLING
void mdns_pump_update() { MDNS.update(); }
#endif
void MDNSComponent::setup() {
// RP2040's LEAmDNS library registers a LwipIntf::stateUpCB() callback to restart
// mDNS when the network interface reconnects. However, stateUpCB() is stubbed out
// in arduino-pico's LwipIntfCB.cpp because the original ESP8266 implementation used
// schedule_function() which doesn't exist in arduino-pico, and the callback can't
// safely run directly since netif status callbacks fire from IRQ context
// (PICO_CYW43_ARCH_THREADSAFE_BACKGROUND) while _restart() allocates UDP sockets.
//
// Workaround: defer MDNS.begin() and service registration until the network has an IP,
// then call notifyAPChange() on subsequent reconnects to restart mDNS probing and
// announcing — all from main loop context via the IP state listener callback(s) so
// it's thread-safe. We subscribe to any listener interface that's active (WiFi and/or
// Ethernet); the same on_ip_state() override serves both because the signatures match.
#ifdef USE_MDNS_EVENT_DRIVEN_POLLING
// arduino-pico stubs out LwipIntf::stateUpCB (the netif status callback LEAmDNS uses
// on ESP8266 for auto-restart), so we must drive begin()/notifyAPChange() from our
// own IP state listener. Both WiFi and Ethernet have the same listener signature —
// one on_ip_state() override serves both.
#ifdef USE_MDNS_WIFI_LISTENER
wifi::global_wifi_component->add_ip_state_listener(this);
// AFTER_CONNECTION priority means the network may already be up when setup() runs;
// the listener only fires on subsequent state changes, so seed the current state.
// AFTER_CONNECTION priority means the network may already be up; the listener only
// fires on subsequent changes, so seed the current state.
{
const auto ips = wifi::global_wifi_component->wifi_sta_ip_addresses();
if (ips[0].is_set()) {
@@ -78,8 +63,6 @@ void MDNSComponent::setup() {
#endif
#ifdef USE_MDNS_ETHERNET_LISTENER
ethernet::global_eth_component->add_ip_state_listener(this);
// Seed current Ethernet state for the same reason — if the interface is already up
// when mdns setup() runs, we need to kick off begin() + polling here.
if (ethernet::global_eth_component->is_connected()) {
const auto ips = ethernet::global_eth_component->get_ip_addresses();
if (ips[0].is_set()) {
@@ -87,35 +70,11 @@ void MDNSComponent::setup() {
}
}
#endif
#else
// Fallback (no IP state listener available): poll forever, checking connection state
// each tick.
this->set_interval(MDNS_UPDATE_INTERVAL_MS, [this]() {
bool connected = network::is_connected();
if (connected && !this->was_connected_) {
if (!this->initialized_) {
this->setup_buffers_and_register_(register_rp2040);
this->initialized_ = true;
} else {
MDNS.notifyAPChange();
}
}
this->was_connected_ = connected;
if (this->initialized_) {
MDNS.update();
}
});
#endif
}
#ifdef USE_MDNS_EVENT_DRIVEN_POLLING
void MDNSComponent::on_ip_state(const network::IPAddresses &ips, const network::IPAddress &,
const network::IPAddress &) {
// Both WiFi and Ethernet IP state listeners only notify on IP acquisition (see
// wifi_component_pico_w.cpp and ethernet_component.cpp), not on IP loss, so every
// notification represents a fresh IP that needs a probe/announce cycle. The library's
// internal LwipIntf::stateUpCB is stubbed out on arduino-pico (see setup()), so we
// drive begin/restart ourselves from this callback.
// Listener only fires on IP acquisition (not loss); every event is a fresh IP.
if (!ips[0].is_set()) {
return;
}
@@ -127,7 +86,6 @@ void MDNSComponent::on_ip_state(const network::IPAddresses &ips, const network::
}
this->start_polling_window_();
}
#endif
void MDNSComponent::on_shutdown() {
MDNS.close();
@@ -1,23 +0,0 @@
ethernet:
type: W5500
clk_pin: 18
mosi_pin: 19
miso_pin: 16
cs_pin: 17
interrupt_pin: 21
reset_pin: 20
manual_ip:
static_ip: 192.168.178.56
gateway: 192.168.178.1
subnet: 255.255.255.0
domain: .local
mac_address: "02:AA:BB:CC:DD:01"
mdns:
disabled: false
services:
- service: _test_service
protocol: _tcp
port: 8888
txt:
static_string: Anything
@@ -1 +0,0 @@
<<: !include common-enabled-ethernet.yaml