[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 <noreply@anthropic.com>
This commit is contained in:
clydebarrow
2026-10-19 14:40:33 +11:00
co-authored by Claude Opus 5.5
parent 982eda4bab
commit 65da10e4d8
10 changed files with 148 additions and 23 deletions
+1
View File
@@ -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
@@ -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)
@@ -0,0 +1,37 @@
#include "udp_packet_interface.h"
#ifdef USE_NETWORK
#include <array>
#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<const uint8_t> 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<uint8_t, MAX_PACKET_SIZE> buf;
size_t len = data.copy_to(buf.data(), buf.size());
return this->parent_->send_packet(buf.data(), len);
}
} // namespace esphome::udp
#endif
@@ -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
@@ -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();
+30 -15
View File
@@ -205,18 +205,20 @@ void UDPComponent::set_poll_warning_(PollStep step, const LogString *what) {
}
void UDPComponent::loop() {
if (this->should_listen_) {
std::array<uint8_t, MAX_PACKET_SIZE> 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<size_t>(len);
ESP_LOGV(TAG, "Received packet of length %zu", packet_len);
this->packet_listeners_.call(std::span<const uint8_t>(buf.data(), packet_len));
}
if (!this->should_listen_) {
this->disable_loop();
return;
}
std::array<uint8_t, MAX_PACKET_SIZE> 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<size_t>(len);
ESP_LOGV(TAG, "Received packet of length %zu", packet_len);
this->packet_listeners_.call(std::span<const uint8_t>(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
+4 -3
View File
@@ -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<uint8_t> &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<uint8_t> &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<void(std::span<const uint8_t>)> packet_listeners_{};
LazyCallbackManager<void(std::span<const uint8_t>)> packet_listeners_{};
std::unique_ptr<socket::UDPSendSocket> broadcast_socket_ = nullptr;
std::unique_ptr<socket::UDPSocket> listen_socket_ = nullptr;
#ifdef USE_UDP_IPV6_SEND
@@ -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
@@ -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
@@ -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(