From 60e511282ac5e0464f98aaf0562e1084e95de952 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Sun, 4 Oct 2026 19:45:18 -0500 Subject: [PATCH] [udp] Keep addresses in a shared flash table (#20085) --- esphome/components/udp/__init__.py | 10 ++++++- esphome/components/udp/udp_component.cpp | 9 ++++-- esphome/components/udp/udp_component.h | 7 ++--- tests/component_tests/udp/__init__.py | 0 .../udp/config/udp_addresses.yaml | 14 +++++++++ .../udp/config/udp_no_addresses.yaml | 12 ++++++++ .../component_tests/udp/test_udp_addresses.py | 29 +++++++++++++++++++ 7 files changed, 73 insertions(+), 8 deletions(-) create mode 100644 tests/component_tests/udp/__init__.py create mode 100644 tests/component_tests/udp/config/udp_addresses.yaml create mode 100644 tests/component_tests/udp/config/udp_no_addresses.yaml create mode 100644 tests/component_tests/udp/test_udp_addresses.py diff --git a/esphome/components/udp/__init__.py b/esphome/components/udp/__init__.py index d96a731e9c..316d4fc311 100644 --- a/esphome/components/udp/__init__.py +++ b/esphome/components/udp/__init__.py @@ -124,7 +124,15 @@ async def to_code(config: ConfigType) -> None: cg.add(var.set_broadcast_port(conf_port[CONF_BROADCAST_PORT])) if (listen_address := str(config[CONF_LISTEN_ADDRESS])) != "255.255.255.255": cg.add(var.set_listen_address(listen_address)) - cg.add(var.set_addresses([str(addr) for addr in config[CONF_ADDRESSES]])) + # Shared flash table ended by nullptr, so the component stores only a pointer. + if addresses := [str(addr) for addr in config[CONF_ADDRESSES]]: + cg.add( + var.set_addresses( + cg.shared_progmem_array( + "udp_addresses", cg.const_char_ptr, [*addresses, cg.nullptr] + ) + ) + ) for conf in config.get(CONF_ON_RECEIVE, []): trigger_id = cg.new_Pvariable(conf[CONF_TRIGGER_ID]) trigger = await automation.build_automation(trigger_id, trigger_argtype, conf) diff --git a/esphome/components/udp/udp_component.cpp b/esphome/components/udp/udp_component.cpp index f662cba4cf..904ed97dc0 100644 --- a/esphome/components/udp/udp_component.cpp +++ b/esphome/components/udp/udp_component.cpp @@ -11,7 +11,8 @@ static const char *const TAG = "udp"; void UDPComponent::setup() { #if defined(USE_SOCKET_IMPL_BSD_SOCKETS) || defined(USE_SOCKET_IMPL_LWIP_SOCKETS) - for (const auto &address : this->addresses_) { + for (const char *const *it = this->addresses_; it != nullptr && *it != nullptr; it++) { + const char *address = *it; struct sockaddr saddr {}; if (socket::set_sockaddr(&saddr, sizeof(saddr), address, this->broadcast_port_) == 0) { ESP_LOGW(TAG, "Invalid address %s", address); @@ -97,7 +98,8 @@ void UDPComponent::setup() { #endif #ifdef USE_SOCKET_IMPL_LWIP_TCP // 8266 and RP2040 `Duino - for (const auto &address : this->addresses_) { + for (const char *const *it = this->addresses_; it != nullptr && *it != nullptr; it++) { + const char *address = *it; auto ipaddr = IPAddress(); if (!ipaddr.fromString(address)) { ESP_LOGW(TAG, "Invalid address %s", address); @@ -138,7 +140,8 @@ void UDPComponent::dump_config() { " Listen Port: %u\n" " Broadcast Port: %u", this->listen_port_, this->broadcast_port_); - for (const char *address : this->addresses_) { + for (const char *const *it = this->addresses_; it != nullptr && *it != nullptr; it++) { + const char *address = *it; ESP_LOGCONFIG(TAG, " Address: %s", address); } if (this->listen_address_.has_value()) { diff --git a/esphome/components/udp/udp_component.h b/esphome/components/udp/udp_component.h index 274e0119ee..17e27fb770 100644 --- a/esphome/components/udp/udp_component.h +++ b/esphome/components/udp/udp_component.h @@ -20,9 +20,8 @@ namespace esphome::udp { static const size_t MAX_PACKET_SIZE = 508; class UDPComponent final : public Component { public: - void set_addresses(std::initializer_list addresses) { this->addresses_ = addresses; } - /// Prevent accidental use of std::string which would dangle - void set_addresses(std::initializer_list addresses) = delete; + /// nullptr-terminated table of addresses; must outlive the component. + void set_addresses(const char *const *addresses) { this->addresses_ = addresses; } void set_listen_address(const char *listen_addr) { this->listen_address_ = network::IPAddress(listen_addr); } void set_listen_port(uint16_t port) { this->listen_port_ = port; } void set_broadcast_port(uint16_t port) { this->broadcast_port_ = port; } @@ -52,7 +51,7 @@ class UDPComponent final : public Component { std::vector ipaddrs_{}; WiFiUDP udp_client_{}; #endif - FixedVector addresses_{}; + const char *const *addresses_{nullptr}; optional listen_address_{}; }; diff --git a/tests/component_tests/udp/__init__.py b/tests/component_tests/udp/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/tests/component_tests/udp/config/udp_addresses.yaml b/tests/component_tests/udp/config/udp_addresses.yaml new file mode 100644 index 0000000000..405c863e10 --- /dev/null +++ b/tests/component_tests/udp/config/udp_addresses.yaml @@ -0,0 +1,14 @@ +esphome: + name: test + +esp8266: + board: d1_mini + +wifi: + ssid: test + password: password1 + +udp: + addresses: + - 10.0.0.1 + - 10.0.0.2 diff --git a/tests/component_tests/udp/config/udp_no_addresses.yaml b/tests/component_tests/udp/config/udp_no_addresses.yaml new file mode 100644 index 0000000000..d9d3a53a65 --- /dev/null +++ b/tests/component_tests/udp/config/udp_no_addresses.yaml @@ -0,0 +1,12 @@ +esphome: + name: test + +esp8266: + board: d1_mini + +wifi: + ssid: test + password: password1 + +udp: + addresses: [] diff --git a/tests/component_tests/udp/test_udp_addresses.py b/tests/component_tests/udp/test_udp_addresses.py new file mode 100644 index 0000000000..434ef02dcf --- /dev/null +++ b/tests/component_tests/udp/test_udp_addresses.py @@ -0,0 +1,29 @@ +"""Tests for the udp component codegen.""" + +from collections.abc import Callable +from pathlib import Path + + +def test_addresses_are_a_progmem_table( + generate_main: Callable[[str | Path], str], + component_config_path: Callable[[str], Path], +) -> None: + """Addresses live in a nullptr-terminated flash table instead of a heap vector.""" + main_cpp = generate_main(component_config_path("udp_addresses.yaml")) + + assert ( + "static constexpr const char * udp_addresses[] PROGMEM = " + '{"10.0.0.1", "10.0.0.2", nullptr};' in main_cpp + ) + assert "->set_addresses(udp_addresses);" in main_cpp + + +def test_empty_addresses_emit_no_table( + generate_main: Callable[[str | Path], str], + component_config_path: Callable[[str], Path], +) -> None: + """An empty address list leaves the pointer null instead of emitting a table.""" + main_cpp = generate_main(component_config_path("udp_no_addresses.yaml")) + + assert "udp_addresses" not in main_cpp + assert "set_addresses(" not in main_cpp