From 65da10e4d8e1f6d2569b962621dffba424f36985 Mon Sep 17 00:00:00 2001 From: clydebarrow <2366188+clydebarrow@users.noreply.github.com> Date: Sun, 18 Oct 2026 15:53:39 +1100 Subject: [PATCH] [udp] Add UDP packet interface link Each packet is one UDP datagram, using the settings of the udp component. The udp component now reports whether a packet was sent, and an ENOMEM refusal from lwIP is logged at verbose level as the packet can be sent again. Co-Authored-By: Claude Opus 5.5 --- CODEOWNERS | 1 + .../udp/packet_interface/__init__.py | 32 +++++++++++++ .../packet_interface/udp_packet_interface.cpp | 37 +++++++++++++++ .../packet_interface/udp_packet_interface.h | 24 ++++++++++ .../udp/packet_transport/udp_transport.cpp | 3 -- esphome/components/udp/udp_component.cpp | 45 ++++++++++++------- esphome/components/udp/udp_component.h | 7 +-- tests/components/packet_interface/common.yaml | 14 ++++++ tests/components/packet_transport/common.yaml | 6 ++- tests/unit_tests/test_packet_interface.py | 2 + 10 files changed, 148 insertions(+), 23 deletions(-) create mode 100644 esphome/components/udp/packet_interface/__init__.py create mode 100644 esphome/components/udp/packet_interface/udp_packet_interface.cpp create mode 100644 esphome/components/udp/packet_interface/udp_packet_interface.h diff --git a/CODEOWNERS b/CODEOWNERS index 716eeff48d5..f0d6fa950f9 100644 --- a/CODEOWNERS +++ b/CODEOWNERS @@ -680,6 +680,7 @@ esphome/components/uart/packet_transport/* @clydebarrow esphome/components/uart_mux/* @kbx81 esphome/components/uart_tcp/* @Bascht74 esphome/components/udp/* @clydebarrow +esphome/components/udp/packet_interface/* @clydebarrow esphome/components/ufire_ec/* @pvizeli esphome/components/ufire_ise/* @pvizeli esphome/components/ufm01/* @ljungqvist diff --git a/esphome/components/udp/packet_interface/__init__.py b/esphome/components/udp/packet_interface/__init__.py new file mode 100644 index 00000000000..1e7de08f415 --- /dev/null +++ b/esphome/components/udp/packet_interface/__init__.py @@ -0,0 +1,32 @@ +import esphome.codegen as cg +from esphome.components import packet_interface +from esphome.components.packet_interface import ( + new_packet_interface, + packet_interface_schema, +) +from esphome.types import ConfigType + +from .. import CONF_UDP_ID, UDP_SCHEMA, udp_ns + +CODEOWNERS = ["@clydebarrow"] + +DEPENDENCIES = ["udp"] + +# Size of the receive buffer in udp_component.h +MAX_PACKET_SIZE = 508 + +UdpPacketInterface = udp_ns.class_( + "UdpPacketInterface", + packet_interface.PacketInterface, +) + +CONFIG_SCHEMA = packet_interface_schema( + UdpPacketInterface, MAX_PACKET_SIZE, MAX_PACKET_SIZE +).extend(UDP_SCHEMA) + + +async def to_code(config: ConfigType) -> None: + udp_var = await cg.get_variable(config[CONF_UDP_ID]) + cg.add(udp_var.set_should_listen()) + cg.add(udp_var.set_should_broadcast()) + await new_packet_interface(config, udp_var) diff --git a/esphome/components/udp/packet_interface/udp_packet_interface.cpp b/esphome/components/udp/packet_interface/udp_packet_interface.cpp new file mode 100644 index 00000000000..1462203daa7 --- /dev/null +++ b/esphome/components/udp/packet_interface/udp_packet_interface.cpp @@ -0,0 +1,37 @@ +#include "udp_packet_interface.h" +#ifdef USE_NETWORK +#include +#include "esphome/components/network/util.h" +#include "esphome/core/log.h" + +namespace esphome::udp { + +using packet_interface::PacketBuffer; +using packet_interface::PacketMetaData; + +ESPHOME_LOG_TAG(TAG, "udp.packet_interface"); + +void UdpPacketInterface::setup() { + this->parent_->add_listener([this](std::span buf) { + this->on_receive_from_interface_(PacketBuffer(buf.data(), buf.size()), {}); + }); +} + +bool UdpPacketInterface::send_to_interface(const PacketBuffer &data, const PacketMetaData &meta_data) { + if (!network::is_connected()) + return false; + if (data.is_contiguous()) { + auto span = data.as_span(); + return this->parent_->send_packet(span.data(), span.size()); + } + if (data.size() > MAX_PACKET_SIZE) { + ESP_LOGW(TAG, "Packet of %zu bytes is larger than %zu", data.size(), MAX_PACKET_SIZE); + return false; + } + std::array buf; + size_t len = data.copy_to(buf.data(), buf.size()); + return this->parent_->send_packet(buf.data(), len); +} + +} // namespace esphome::udp +#endif diff --git a/esphome/components/udp/packet_interface/udp_packet_interface.h b/esphome/components/udp/packet_interface/udp_packet_interface.h new file mode 100644 index 00000000000..42cfff395a6 --- /dev/null +++ b/esphome/components/udp/packet_interface/udp_packet_interface.h @@ -0,0 +1,24 @@ +#pragma once + +#include "esphome/core/defines.h" + +// udp_component.h declares nothing without USE_NETWORK +#ifdef USE_NETWORK +#include "esphome/components/packet_interface/packet_interface.h" +#include "../udp_component.h" + +namespace esphome::udp { + +class UdpPacketInterface : public packet_interface::PacketInterface { + public: + explicit UdpPacketInterface(UDPComponent *udp_component) : parent_(udp_component) {} + + void setup() override; + bool send_to_interface(const packet_interface::PacketBuffer &data, + const packet_interface::PacketMetaData &meta_data) override; + + protected: + UDPComponent *parent_; +}; +} // namespace esphome::udp +#endif diff --git a/esphome/components/udp/packet_transport/udp_transport.cpp b/esphome/components/udp/packet_transport/udp_transport.cpp index 2e49b676ce8..550449e8779 100644 --- a/esphome/components/udp/packet_transport/udp_transport.cpp +++ b/esphome/components/udp/packet_transport/udp_transport.cpp @@ -1,12 +1,9 @@ -#include "esphome/core/log.h" #include "esphome/core/application.h" #include "esphome/components/network/util.h" #include "udp_transport.h" namespace esphome::udp { -ESPHOME_LOG_TAG(TAG, "udp_transport"); - bool UDPTransport::should_send() { return network::is_connected(); } void UDPTransport::setup() { PacketTransport::setup(); diff --git a/esphome/components/udp/udp_component.cpp b/esphome/components/udp/udp_component.cpp index e143f93a4be..535fdd62f29 100644 --- a/esphome/components/udp/udp_component.cpp +++ b/esphome/components/udp/udp_component.cpp @@ -205,18 +205,20 @@ void UDPComponent::set_poll_warning_(PollStep step, const LogString *what) { } void UDPComponent::loop() { - if (this->should_listen_) { - std::array buf; - for (;;) { - if (!this->listen_socket_->ready()) - break; - auto len = this->listen_socket_->read(buf.data(), buf.size()); - if (len <= 0) - break; - size_t packet_len = static_cast(len); - ESP_LOGV(TAG, "Received packet of length %zu", packet_len); - this->packet_listeners_.call(std::span(buf.data(), packet_len)); - } + if (!this->should_listen_) { + this->disable_loop(); + return; + } + std::array buf; + for (;;) { + if (!this->listen_socket_->ready()) + break; + auto len = this->listen_socket_->read(buf.data(), buf.size()); + if (len <= 0) + break; + size_t packet_len = static_cast(len); + ESP_LOGV(TAG, "Received packet of length %zu", packet_len); + this->packet_listeners_.call(std::span(buf.data(), packet_len)); } } @@ -239,9 +241,10 @@ void UDPComponent::dump_config() { YESNO(this->should_broadcast_), YESNO(this->should_listen_)); } -void UDPComponent::send_packet(const uint8_t *data, size_t size) { +bool UDPComponent::send_packet(const uint8_t *data, size_t size) { if (this->is_failed()) - return; // a send socket may be missing + return false; // a send socket may be missing + bool sent = true; for (const auto &entry : this->sockaddrs_) { #ifdef USE_UDP_IPV6_SEND const bool v6 = entry.sa.sa_family == AF_INET6; @@ -256,11 +259,23 @@ void UDPComponent::send_packet(const uint8_t *data, size_t size) { socket::UDPSendSocket *sock = this->broadcast_socket_.get(); socklen_t sockaddr_size = sizeof(sockaddr_in); #endif + if (sock == nullptr) { + ESP_LOGW(TAG, "Broadcast socket not initialized"); + return false; + } auto result = sock->sendto(data, size, 0, &entry.sa, sockaddr_size); if (result < 0) { - ESP_LOGW(TAG, "sendto() error %d", errno); + // lwIP refuses a packet with ENOMEM while the destination's ARP lookup is pending and its queue is full; + // the packet can be sent again later + if (errno == ENOMEM) { + ESP_LOGV(TAG, "sendto() out of memory"); + } else { + ESP_LOGW(TAG, "sendto() error %d", errno); + } + sent = false; } } + return sent; } } // namespace esphome::udp diff --git a/esphome/components/udp/udp_component.h b/esphome/components/udp/udp_component.h index b4dd4b2ac43..f6c0181a0d7 100644 --- a/esphome/components/udp/udp_component.h +++ b/esphome/components/udp/udp_component.h @@ -35,8 +35,9 @@ class UDPComponent final : public Component { void setup() override; void loop() override; void dump_config() override; - void send_packet(const uint8_t *data, size_t size); - void send_packet(const std::vector &buf) { this->send_packet(buf.data(), buf.size()); } + /// Returns false if the packet could not be sent to every destination. + bool send_packet(const uint8_t *data, size_t size); + bool send_packet(const std::vector &buf) { return this->send_packet(buf.data(), buf.size()); } float get_setup_priority() const override { return setup_priority::AFTER_WIFI; }; protected: @@ -53,7 +54,7 @@ class UDPComponent final : public Component { bool network_poll_(); void set_poll_warning_(PollStep step, const LogString *what); - CallbackManager)> packet_listeners_{}; + LazyCallbackManager)> packet_listeners_{}; std::unique_ptr broadcast_socket_ = nullptr; std::unique_ptr listen_socket_ = nullptr; #ifdef USE_UDP_IPV6_SEND diff --git a/tests/components/packet_interface/common.yaml b/tests/components/packet_interface/common.yaml index 25185683325..d0049f8a9ef 100644 --- a/tests/components/packet_interface/common.yaml +++ b/tests/components/packet_interface/common.yaml @@ -1,7 +1,17 @@ +wifi: + ssid: MySSID + password: password1 + +udp: + - id: packet_interface_udp + packet_interface: - platform: uart id: packet_interface_uart max_packet_size: 256 + - platform: udp + id: packet_interface_network + udp_id: packet_interface_udp sensor: - platform: template @@ -13,3 +23,7 @@ packet_transport: packet_interface: packet_interface_uart sensors: - packet_interface_sensor + - platform: packet_interface + packet_interface: packet_interface_network + sensors: + - packet_interface_sensor diff --git a/tests/components/packet_transport/common.yaml b/tests/components/packet_transport/common.yaml index 5c6c8dd6368..67ad6adea75 100644 --- a/tests/components/packet_transport/common.yaml +++ b/tests/components/packet_transport/common.yaml @@ -3,12 +3,14 @@ wifi: password: password1 udp: - listen_address: 239.0.60.53 - addresses: ["239.0.60.53"] + - id: transport_udp_component + listen_address: 239.0.60.53 + addresses: ["239.0.60.53"] packet_transport: - platform: udp id: transport_udp + udp_id: transport_udp_component update_interval: 5s encryption: "our key goes here" rolling_code_enable: true diff --git a/tests/unit_tests/test_packet_interface.py b/tests/unit_tests/test_packet_interface.py index b6f4645f17c..3e594592dec 100644 --- a/tests/unit_tests/test_packet_interface.py +++ b/tests/unit_tests/test_packet_interface.py @@ -6,12 +6,14 @@ from esphome import config_validation as cv from esphome.components import packet_interface from esphome.components.packet_interface import CONF_MAX_PACKET_SIZE from esphome.components.uart import packet_interface as uart_packet_interface +from esphome.components.udp import packet_interface as udp_packet_interface @pytest.mark.parametrize( ("schema", "default", "limit"), [ (uart_packet_interface.CONFIG_SCHEMA, 508, 4096), + (udp_packet_interface.CONFIG_SCHEMA, 508, 508), ], ) def test_platform_max_packet_size(