mirror of
https://github.com/esphome/esphome.git
synced 2026-10-14 10:12:52 +00:00
[udp] Use the socket UDP API on ESP8266 and RP2040 (#20558)
This commit is contained in:
@@ -218,8 +218,9 @@ template<typename F> static bool foreach_eligible_ipv6_if(F &&callback) {
|
||||
return found_any;
|
||||
#endif
|
||||
}
|
||||
#endif // USE_SOCKET_IMPL_BSD_SOCKETS || USE_SOCKET_IMPL_LWIP_SOCKETS
|
||||
|
||||
bool join_multicast_group(Socket *sock, const char *ip_address, uint32_t *if_index_out) {
|
||||
bool join_multicast_group(UDPSocket *sock, const char *ip_address, uint32_t *if_index_out) {
|
||||
if (strchr(ip_address, ':') == nullptr) {
|
||||
struct in_addr ipv4mc {};
|
||||
#ifdef USE_ZEPHYR
|
||||
@@ -249,7 +250,8 @@ bool join_multicast_group(Socket *sock, const char *ip_address, uint32_t *if_ind
|
||||
}
|
||||
return true;
|
||||
}
|
||||
#if USE_NETWORK_IPV6
|
||||
// The raw lwIP implementation has no IPv6 membership support
|
||||
#if USE_NETWORK_IPV6 && (defined(USE_SOCKET_IMPL_BSD_SOCKETS) || defined(USE_SOCKET_IMPL_LWIP_SOCKETS))
|
||||
struct ipv6_mreq imreq6 {};
|
||||
#ifdef USE_SOCKET_IMPL_BSD_SOCKETS
|
||||
#ifdef USE_ZEPHYR
|
||||
@@ -300,9 +302,10 @@ bool join_multicast_group(Socket *sock, const char *ip_address, uint32_t *if_ind
|
||||
#else
|
||||
errno = EINVAL;
|
||||
return false;
|
||||
#endif // USE_NETWORK_IPV6
|
||||
#endif // USE_NETWORK_IPV6 && fd based sockets
|
||||
}
|
||||
|
||||
#if defined(USE_SOCKET_IMPL_BSD_SOCKETS) || defined(USE_SOCKET_IMPL_LWIP_SOCKETS)
|
||||
#if USE_NETWORK_IPV6
|
||||
bool set_ipv6_multicast_if(Socket *sock, uint32_t if_index_in) {
|
||||
#if defined(USE_ZEPHYR) || defined(USE_SOCKET_IMPL_LWIP_SOCKETS)
|
||||
|
||||
@@ -191,16 +191,16 @@ inline socklen_t set_sockaddr(struct sockaddr *addr, socklen_t addrlen, const st
|
||||
/// Set a sockaddr to the any address and specified port for the IP version used by socket_ip().
|
||||
socklen_t set_sockaddr_any(struct sockaddr *addr, socklen_t addrlen, uint16_t port);
|
||||
|
||||
#if defined(USE_SOCKET_IMPL_BSD_SOCKETS) || defined(USE_SOCKET_IMPL_LWIP_SOCKETS)
|
||||
/// Join a multicast group on the given socket.
|
||||
/// @param sock The socket to join on
|
||||
/// @param ip_address Null-terminated multicast address string (IPv4 or IPv6)
|
||||
/// @param ip_address Null-terminated multicast address string (IPv4, or IPv6 except on LWIP_TCP)
|
||||
/// @param if_index_out If non-null, receives the interface index used (IPv4 always writes 0). For
|
||||
/// link-local IPv6 groups, the caller must also copy this into sin6_scope_id
|
||||
/// before bind() -- POSIX requires it, LwIP ignores it.
|
||||
/// @return true on success, false on failure (errno set)
|
||||
bool join_multicast_group(Socket *sock, const char *ip_address, uint32_t *if_index_out = nullptr);
|
||||
bool join_multicast_group(UDPSocket *sock, const char *ip_address, uint32_t *if_index_out = nullptr);
|
||||
|
||||
#if defined(USE_SOCKET_IMPL_BSD_SOCKETS) || defined(USE_SOCKET_IMPL_LWIP_SOCKETS)
|
||||
#if USE_NETWORK_IPV6
|
||||
/// Configure the outgoing IPv6 multicast interface on the given socket.
|
||||
/// @param sock The socket to configure
|
||||
|
||||
@@ -13,7 +13,7 @@ template<typename... Ts> class UDPWriteAction final : public Action<Ts...>, publ
|
||||
TEMPLATABLE_BYTES(data)
|
||||
|
||||
void play(const Ts &...x) override {
|
||||
// One write per packet: WiFiUDP appends all or nothing, so a failed allocation never sends a partial packet
|
||||
// One sendto per packet, so a failed allocation never sends a partial packet
|
||||
this->data_.template visit<STACK_PAYLOAD_SIZE>(
|
||||
[this](const uint8_t *data, size_t len) { this->parent_->send_packet(data, len); }, x...);
|
||||
}
|
||||
|
||||
@@ -8,9 +8,10 @@
|
||||
namespace esphome::udp {
|
||||
|
||||
ESPHOME_LOG_TAG(TAG, "udp");
|
||||
static constexpr uint32_t JOIN_POLL_ID = 0;
|
||||
static constexpr uint32_t JOIN_POLL_INTERVAL_MS = 1000;
|
||||
|
||||
void UDPComponent::setup() {
|
||||
#if defined(USE_SOCKET_IMPL_BSD_SOCKETS) || defined(USE_SOCKET_IMPL_LWIP_SOCKETS)
|
||||
for (const char *const *it = this->addresses_; it != nullptr && *it != nullptr; it++) {
|
||||
const char *address = *it;
|
||||
struct sockaddr saddr {};
|
||||
@@ -24,7 +25,7 @@ void UDPComponent::setup() {
|
||||
}
|
||||
// set up broadcast socket
|
||||
if (this->should_broadcast_) {
|
||||
this->broadcast_socket_ = socket::socket(AF_INET, SOCK_DGRAM, IPPROTO_IP);
|
||||
this->broadcast_socket_ = socket::socket_udp_send(AF_INET, IPPROTO_IP);
|
||||
if (this->broadcast_socket_ == nullptr) {
|
||||
this->status_set_error(LOG_STR("Could not create socket"));
|
||||
this->mark_failed();
|
||||
@@ -44,7 +45,7 @@ void UDPComponent::setup() {
|
||||
// create listening socket if we either want to subscribe to providers, or need to listen
|
||||
// for ping key broadcasts.
|
||||
if (this->should_listen_) {
|
||||
this->listen_socket_ = socket::socket(AF_INET, SOCK_DGRAM, IPPROTO_IP);
|
||||
this->listen_socket_ = socket::socket_udp(AF_INET, IPPROTO_IP);
|
||||
if (this->listen_socket_ == nullptr) {
|
||||
this->status_set_error(LOG_STR("Could not create socket"));
|
||||
this->mark_failed();
|
||||
@@ -69,22 +70,13 @@ void UDPComponent::setup() {
|
||||
server.sin_addr.s_addr = ESPHOME_INADDR_ANY;
|
||||
server.sin_port = htons(this->listen_port_);
|
||||
|
||||
if (this->listen_address_.has_value()) {
|
||||
// Only 16 bytes needed for IPv4, but use standard size for consistency
|
||||
char addr_buf[network::IP_ADDRESS_BUFFER_SIZE];
|
||||
this->listen_address_.value().str_to(addr_buf);
|
||||
struct ip_mreq imreq = {};
|
||||
imreq.imr_interface.s_addr = ESPHOME_INADDR_ANY;
|
||||
inet_aton(addr_buf, &imreq.imr_multiaddr);
|
||||
server.sin_addr.s_addr = imreq.imr_multiaddr.s_addr;
|
||||
ESP_LOGD(TAG, "Join multicast %s", addr_buf);
|
||||
err = this->listen_socket_->setsockopt(IPPROTO_IP, IP_ADD_MEMBERSHIP, &imreq, sizeof(imreq));
|
||||
if (err < 0) {
|
||||
ESP_LOGE(TAG, "Failed to set IP_ADD_MEMBERSHIP. Error %d", errno);
|
||||
this->status_set_error(LOG_STR("Failed to set IP_ADD_MEMBERSHIP"));
|
||||
this->mark_failed();
|
||||
return;
|
||||
}
|
||||
if (this->listen_address_ != nullptr) {
|
||||
inet_aton(this->listen_address_, &server.sin_addr);
|
||||
// Joined once the network is up; RP2040 has no interface to join before then
|
||||
this->set_interval(JOIN_POLL_ID, JOIN_POLL_INTERVAL_MS, [this]() {
|
||||
if (network::is_connected() && this->join_multicast_())
|
||||
this->cancel_interval(JOIN_POLL_ID);
|
||||
});
|
||||
}
|
||||
|
||||
err = this->listen_socket_->bind((struct sockaddr *) &server, sizeof(server));
|
||||
@@ -95,36 +87,35 @@ void UDPComponent::setup() {
|
||||
return;
|
||||
}
|
||||
}
|
||||
#endif
|
||||
#ifdef USE_SOCKET_IMPL_LWIP_TCP
|
||||
// 8266 and RP2040 `Duino
|
||||
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);
|
||||
this->status_set_warning(LOG_STR("invalid address"));
|
||||
continue;
|
||||
}
|
||||
|
||||
bool UDPComponent::join_multicast_() {
|
||||
if (socket::join_multicast_group(this->listen_socket_.get(), this->listen_address_)) {
|
||||
ESP_LOGD(TAG, "Joined multicast %s", this->listen_address_);
|
||||
if (this->join_warning_) {
|
||||
this->join_warning_ = false;
|
||||
this->status_clear_warning();
|
||||
}
|
||||
this->ipaddrs_.push_back(ipaddr);
|
||||
return true;
|
||||
}
|
||||
if (this->should_listen_)
|
||||
this->udp_client_.begin(this->listen_port_);
|
||||
#endif
|
||||
// Log the first failure only; the poll keeps retrying every second
|
||||
if (!this->join_failed_) {
|
||||
this->join_failed_ = true;
|
||||
ESP_LOGW(TAG, "Failed to set IP_ADD_MEMBERSHIP. Error %d", errno);
|
||||
// One warning bit: only a warning raised here is cleared on success, one from setup() stays
|
||||
if (!this->status_has_warning()) {
|
||||
this->status_set_warning(LOG_STR("Failed to set IP_ADD_MEMBERSHIP"));
|
||||
this->join_warning_ = true;
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
void UDPComponent::loop() {
|
||||
if (this->should_listen_) {
|
||||
std::array<uint8_t, MAX_PACKET_SIZE> buf;
|
||||
for (;;) {
|
||||
#if defined(USE_SOCKET_IMPL_BSD_SOCKETS) || defined(USE_SOCKET_IMPL_LWIP_SOCKETS)
|
||||
auto len = this->listen_socket_->read(buf.data(), buf.size());
|
||||
#endif
|
||||
#ifdef USE_SOCKET_IMPL_LWIP_TCP
|
||||
auto len = this->udp_client_.parsePacket();
|
||||
if (len > 0)
|
||||
len = this->udp_client_.read(buf.data(), buf.size());
|
||||
#endif
|
||||
if (len <= 0)
|
||||
break;
|
||||
size_t packet_len = static_cast<size_t>(len);
|
||||
@@ -144,9 +135,8 @@ void UDPComponent::dump_config() {
|
||||
const char *address = *it;
|
||||
ESP_LOGCONFIG(TAG, " Address: %s", address);
|
||||
}
|
||||
if (this->listen_address_.has_value()) {
|
||||
char addr_buf[network::IP_ADDRESS_BUFFER_SIZE];
|
||||
ESP_LOGCONFIG(TAG, " Listen address: %s", this->listen_address_.value().str_to(addr_buf));
|
||||
if (this->listen_address_ != nullptr) {
|
||||
ESP_LOGCONFIG(TAG, " Listen address: %s", this->listen_address_);
|
||||
}
|
||||
ESP_LOGCONFIG(TAG,
|
||||
" Broadcasting: %s\n"
|
||||
@@ -155,26 +145,12 @@ void UDPComponent::dump_config() {
|
||||
}
|
||||
|
||||
void UDPComponent::send_packet(const uint8_t *data, size_t size) {
|
||||
#if defined(USE_SOCKET_IMPL_BSD_SOCKETS) || defined(USE_SOCKET_IMPL_LWIP_SOCKETS)
|
||||
for (const auto &saddr : this->sockaddrs_) {
|
||||
auto result = this->broadcast_socket_->sendto(data, size, 0, &saddr, sizeof(saddr));
|
||||
if (result < 0) {
|
||||
ESP_LOGW(TAG, "sendto() error %d", errno);
|
||||
}
|
||||
}
|
||||
#endif
|
||||
#ifdef USE_SOCKET_IMPL_LWIP_TCP
|
||||
auto iface = IPAddress(0, 0, 0, 0);
|
||||
for (const auto &saddr : this->ipaddrs_) {
|
||||
if (this->udp_client_.beginPacketMulticast(saddr, this->broadcast_port_, iface, 128) != 0) {
|
||||
this->udp_client_.write(data, size);
|
||||
auto result = this->udp_client_.endPacket();
|
||||
if (result == 0) {
|
||||
ESP_LOGW(TAG, "udp.write() error");
|
||||
}
|
||||
}
|
||||
}
|
||||
#endif
|
||||
}
|
||||
} // namespace esphome::udp
|
||||
|
||||
|
||||
@@ -3,13 +3,7 @@
|
||||
#include "esphome/core/defines.h"
|
||||
#ifdef USE_NETWORK
|
||||
#include "esphome/core/helpers.h"
|
||||
#include "esphome/components/network/ip_address.h"
|
||||
#if defined(USE_SOCKET_IMPL_BSD_SOCKETS) || defined(USE_SOCKET_IMPL_LWIP_SOCKETS)
|
||||
#include "esphome/components/socket/socket.h"
|
||||
#endif
|
||||
#ifdef USE_SOCKET_IMPL_LWIP_TCP
|
||||
#include <WiFiUdp.h>
|
||||
#endif
|
||||
#include <array>
|
||||
#include <initializer_list>
|
||||
#include <span>
|
||||
@@ -22,7 +16,8 @@ class UDPComponent final : public Component {
|
||||
public:
|
||||
/// 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); }
|
||||
/// Multicast group literal; must outlive the component.
|
||||
void set_listen_address(const char *listen_addr) { this->listen_address_ = listen_addr; }
|
||||
void set_listen_port(uint16_t port) { this->listen_port_ = port; }
|
||||
void set_broadcast_port(uint16_t port) { this->broadcast_port_ = port; }
|
||||
void set_should_broadcast() { this->should_broadcast_ = true; }
|
||||
@@ -36,24 +31,22 @@ class UDPComponent final : public Component {
|
||||
float get_setup_priority() const override { return setup_priority::AFTER_WIFI; };
|
||||
|
||||
protected:
|
||||
bool join_multicast_();
|
||||
|
||||
uint16_t listen_port_{};
|
||||
uint16_t broadcast_port_{};
|
||||
bool should_broadcast_{};
|
||||
bool should_listen_{};
|
||||
bool join_failed_{};
|
||||
bool join_warning_{};
|
||||
CallbackManager<void(std::span<const uint8_t>)> packet_listeners_{};
|
||||
|
||||
#if defined(USE_SOCKET_IMPL_BSD_SOCKETS) || defined(USE_SOCKET_IMPL_LWIP_SOCKETS)
|
||||
std::unique_ptr<socket::Socket> broadcast_socket_ = nullptr;
|
||||
std::unique_ptr<socket::Socket> listen_socket_ = nullptr;
|
||||
std::unique_ptr<socket::UDPSendSocket> broadcast_socket_ = nullptr;
|
||||
std::unique_ptr<socket::UDPSocket> listen_socket_ = nullptr;
|
||||
std::vector<struct sockaddr> sockaddrs_{};
|
||||
#endif
|
||||
#ifdef USE_SOCKET_IMPL_LWIP_TCP
|
||||
std::vector<IPAddress> ipaddrs_{};
|
||||
WiFiUDP udp_client_{};
|
||||
#endif
|
||||
const char *const *addresses_{nullptr};
|
||||
|
||||
optional<network::IPAddress> listen_address_{};
|
||||
const char *listen_address_{nullptr};
|
||||
};
|
||||
|
||||
} // namespace esphome::udp
|
||||
|
||||
@@ -0,0 +1,22 @@
|
||||
esphome:
|
||||
name: udp-multicast-test
|
||||
|
||||
host:
|
||||
|
||||
api:
|
||||
|
||||
logger:
|
||||
level: DEBUG
|
||||
|
||||
udp:
|
||||
- id: test_udp
|
||||
listen_address: 239.1.2.3
|
||||
addresses:
|
||||
- "239.1.2.3"
|
||||
port:
|
||||
listen_port: UDP_LISTEN_PORT_PLACEHOLDER
|
||||
broadcast_port: UDP_LISTEN_PORT_PLACEHOLDER
|
||||
on_receive:
|
||||
- logger.log:
|
||||
format: "Received UDP: %d bytes"
|
||||
args: [data.size()]
|
||||
@@ -0,0 +1,75 @@
|
||||
"""Integration test for the UDP component's deferred multicast join."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import asyncio
|
||||
import socket
|
||||
|
||||
import pytest
|
||||
|
||||
from .types import APIClientConnectedFactory, RunCompiledFunction
|
||||
|
||||
MULTICAST_GROUP = "239.1.2.3"
|
||||
|
||||
|
||||
def _get_free_udp_port() -> int:
|
||||
"""Get a free UDP port by binding to port 0 and releasing."""
|
||||
sock = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)
|
||||
sock.bind(("127.0.0.1", 0))
|
||||
port = sock.getsockname()[1]
|
||||
sock.close()
|
||||
return port
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_udp_multicast_join(
|
||||
yaml_config: str,
|
||||
run_compiled: RunCompiledFunction,
|
||||
api_client_connected: APIClientConnectedFactory,
|
||||
) -> None:
|
||||
"""The group is joined from the poll after setup, once, and then receives."""
|
||||
log_lines: list[str] = []
|
||||
joined_event = asyncio.Event()
|
||||
receive_event = asyncio.Event()
|
||||
|
||||
def on_log_line(line: str) -> None:
|
||||
log_lines.append(line)
|
||||
if f"Joined multicast {MULTICAST_GROUP}" in line:
|
||||
joined_event.set()
|
||||
if "Received UDP:" in line:
|
||||
receive_event.set()
|
||||
|
||||
listen_port = _get_free_udp_port()
|
||||
config = yaml_config.replace("UDP_LISTEN_PORT_PLACEHOLDER", str(listen_port))
|
||||
|
||||
async with (
|
||||
run_compiled(config, line_callback=on_log_line),
|
||||
api_client_connected() as client,
|
||||
):
|
||||
device_info = await client.device_info()
|
||||
assert device_info is not None
|
||||
assert device_info.name == "udp-multicast-test"
|
||||
|
||||
try:
|
||||
await asyncio.wait_for(joined_event.wait(), timeout=5.0)
|
||||
except TimeoutError:
|
||||
pytest.fail(f"Multicast group was not joined. Log: {log_lines[-20:]}")
|
||||
|
||||
# The poll cancels itself after the join, so a second poll period must not join again
|
||||
await asyncio.sleep(2.5)
|
||||
join_lines = [line for line in log_lines if "Joined multicast" in line]
|
||||
assert len(join_lines) == 1, f"Expected one join, got: {join_lines}"
|
||||
assert not any("IP_ADD_MEMBERSHIP" in line for line in log_lines), (
|
||||
f"Join reported a failure. Log: {log_lines[-20:]}"
|
||||
)
|
||||
|
||||
send_sock = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)
|
||||
try:
|
||||
send_sock.sendto(b"MULTICAST_PAYLOAD", (MULTICAST_GROUP, listen_port))
|
||||
finally:
|
||||
send_sock.close()
|
||||
|
||||
try:
|
||||
await asyncio.wait_for(receive_event.wait(), timeout=5.0)
|
||||
except TimeoutError:
|
||||
pytest.fail(f"Multicast datagram was not received. Log: {log_lines[-20:]}")
|
||||
Reference in New Issue
Block a user