From ceada8632553398018656a273a8a8f7a3e598b2d Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Thu, 23 Apr 2026 19:41:23 -0500 Subject: [PATCH] [mdns] Drive event-driven polling from Ethernet IP state events on RP2040 MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Extends the WiFi-only listener pattern from the previous commit to also subscribe to EthernetIPStateListener when Ethernet is configured. RP2040 can run mDNS over a W5500 ethernet shield without WiFi, and mDNS and WiFi are mutually exclusive on RP2040 (the framework doesn't support both simultaneously on the CYW43/PIO paths), so this adds the ethernet-only path without touching the WiFi path. ESPHome's wifi and ethernet components already publish compatible IP state listener APIs (`WiFiIPStateListener::on_ip_state` and `EthernetIPStateListener::on_ip_state` with identical signatures). MDNSComponent multiply-inherits both when available; a single on_ip_state() override satisfies both vtable entries. - New `USE_MDNS_WIFI_LISTENER` / `USE_MDNS_ETHERNET_LISTENER` gates control per- interface subscription. `USE_MDNS_EVENT_DRIVEN_POLLING` fires if either is available. - Python side now calls `ethernet.request_ethernet_ip_state_listener()` when ethernet is in the config (RP2040 only — ESP8266 has no ethernet driver). - setup() seeds current state for each registered listener so an already-up interface still triggers MDNS.begin() + polling window under AFTER_CONNECTION priority. Tests: adds `test-enabled-ethernet.rp2040-ard.yaml` covering the ethernet-only path. Existing `test-enabled.rp2040-ard.yaml` (WiFi-only) and ESP8266 tests continue to pass. --- esphome/components/mdns/__init__.py | 26 +++++++---- esphome/components/mdns/mdns_component.h | 28 ++++++++--- esphome/components/mdns/mdns_esp8266.cpp | 6 ++- esphome/components/mdns/mdns_rp2040.cpp | 46 +++++++++++++------ .../mdns/common-enabled-ethernet.yaml | 23 ++++++++++ .../test-enabled-ethernet.rp2040-ard.yaml | 1 + 6 files changed, 99 insertions(+), 31 deletions(-) create mode 100644 tests/components/mdns/common-enabled-ethernet.yaml create mode 100644 tests/components/mdns/test-enabled-ethernet.rp2040-ard.yaml diff --git a/esphome/components/mdns/__init__.py b/esphome/components/mdns/__init__.py index dc86a314da..db915e5f89 100644 --- a/esphome/components/mdns/__init__.py +++ b/esphome/components/mdns/__init__.py @@ -169,16 +169,24 @@ async def to_code(config): elif CORE.is_rp2040: cg.add_library("LEAmDNS", None) - # ESP8266 and RP2040 use a WiFi 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. When WiFi is absent (e.g. an - # ethernet-only RP2040 build) the component falls back to the legacy polling - # loop at MDNS_UPDATE_INTERVAL_MS. - if (CORE.is_esp8266 or CORE.is_rp2040) and "wifi" in CORE.config: - from esphome.components import wifi + # 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. + if CORE.is_esp8266 or CORE.is_rp2040: + if "wifi" in CORE.config: + from esphome.components import wifi - wifi.request_wifi_ip_state_listener() + wifi.request_wifi_ip_state_listener() + if CORE.is_rp2040 and "ethernet" in CORE.config: + from esphome.components import ethernet + + ethernet.request_ethernet_ip_state_listener() if CORE.is_esp32: add_idf_component(name="espressif/mdns", ref="1.11.0") diff --git a/esphome/components/mdns/mdns_component.h b/esphome/components/mdns/mdns_component.h index 32a365bb62..ea74e22818 100644 --- a/esphome/components/mdns/mdns_component.h +++ b/esphome/components/mdns/mdns_component.h @@ -6,14 +6,24 @@ #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 a WiFi IP state -// listener slot is available — the mdns Python to_code() requests one when WiFi is in -// the config. If it's not (e.g. clang-tidy running without full codegen, or an -// ethernet-only RP2040 build), the component falls back to the legacy polling loop. -#if (defined(USE_ESP8266) || defined(USE_RP2040)) && defined(USE_WIFI) && defined(USE_WIFI_IP_STATE_LISTENERS) +// 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. +#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))) #include "esphome/components/network/ip_address.h" -#include "esphome/components/wifi/wifi_component.h" #define USE_MDNS_EVENT_DRIVEN_POLLING +#if defined(USE_WIFI) && defined(USE_WIFI_IP_STATE_LISTENERS) +#include "esphome/components/wifi/wifi_component.h" +#define USE_MDNS_WIFI_LISTENER +#endif +#if defined(USE_ETHERNET) && defined(USE_ETHERNET_IP_STATE_LISTENERS) +#include "esphome/components/ethernet/ethernet_component.h" +#define USE_MDNS_ETHERNET_LISTENER +#endif #endif namespace esphome::mdns { @@ -58,10 +68,14 @@ struct MDNSService { }; class MDNSComponent final : public Component -#ifdef USE_MDNS_EVENT_DRIVEN_POLLING +#ifdef USE_MDNS_WIFI_LISTENER , public wifi::WiFiIPStateListener #endif +#ifdef USE_MDNS_ETHERNET_LISTENER + , + public ethernet::EthernetIPStateListener +#endif { public: void setup() override; diff --git a/esphome/components/mdns/mdns_esp8266.cpp b/esphome/components/mdns/mdns_esp8266.cpp index 072be497db..c7737bcb14 100644 --- a/esphome/components/mdns/mdns_esp8266.cpp +++ b/esphome/components/mdns/mdns_esp8266.cpp @@ -7,10 +7,10 @@ #include "esphome/core/application.h" #include "esphome/core/hal.h" #include "esphome/core/log.h" -#ifdef USE_MDNS_EVENT_DRIVEN_POLLING +#include "mdns_component.h" +#ifdef USE_MDNS_WIFI_LISTENER #include "esphome/components/wifi/wifi_component.h" #endif -#include "mdns_component.h" namespace esphome::mdns { @@ -50,6 +50,8 @@ void MDNSComponent::setup() { // 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. wifi::global_wifi_component->add_ip_state_listener(this); this->start_polling_window_(); #else diff --git a/esphome/components/mdns/mdns_rp2040.cpp b/esphome/components/mdns/mdns_rp2040.cpp index cc2d81fe24..8a6a36096f 100644 --- a/esphome/components/mdns/mdns_rp2040.cpp +++ b/esphome/components/mdns/mdns_rp2040.cpp @@ -6,9 +6,12 @@ #include "esphome/core/application.h" #include "esphome/core/log.h" #include "mdns_component.h" -#ifdef USE_MDNS_EVENT_DRIVEN_POLLING +#ifdef USE_MDNS_WIFI_LISTENER #include "esphome/components/wifi/wifi_component.h" #endif +#ifdef USE_MDNS_ETHERNET_LISTENER +#include "esphome/components/ethernet/ethernet_component.h" +#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. @@ -57,19 +60,36 @@ void MDNSComponent::setup() { // // 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 WiFiIPStateListener callback so it's - // thread-safe. + // 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 +#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. - const auto ips = wifi::global_wifi_component->wifi_sta_ip_addresses(); - if (ips[0].is_set()) { - this->on_ip_state(ips, wifi::global_wifi_component->get_dns_address(0), - wifi::global_wifi_component->get_dns_address(1)); + { + const auto ips = wifi::global_wifi_component->wifi_sta_ip_addresses(); + if (ips[0].is_set()) { + this->on_ip_state(ips, wifi::global_wifi_component->get_dns_address(0), + wifi::global_wifi_component->get_dns_address(1)); + } } +#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()) { + this->on_ip_state(ips, network::IPAddress{}, network::IPAddress{}); + } + } +#endif #else - // Fallback (non-WiFi build): poll forever, checking connection state each tick. + // 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_) { @@ -91,11 +111,11 @@ void MDNSComponent::setup() { #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 (see - // wifi_component_pico_w.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. + // 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. if (!ips[0].is_set()) { return; } diff --git a/tests/components/mdns/common-enabled-ethernet.yaml b/tests/components/mdns/common-enabled-ethernet.yaml new file mode 100644 index 0000000000..bfa9321d43 --- /dev/null +++ b/tests/components/mdns/common-enabled-ethernet.yaml @@ -0,0 +1,23 @@ +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 diff --git a/tests/components/mdns/test-enabled-ethernet.rp2040-ard.yaml b/tests/components/mdns/test-enabled-ethernet.rp2040-ard.yaml new file mode 100644 index 0000000000..f84a0bc276 --- /dev/null +++ b/tests/components/mdns/test-enabled-ethernet.rp2040-ard.yaml @@ -0,0 +1 @@ +<<: !include common-enabled-ethernet.yaml