From 49ba08cec98bb2ac8e95ab67dbc1fe6f67a05542 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Mon, 9 Mar 2026 23:20:05 -1000 Subject: [PATCH] [socket] Add lwip raw UDP socket implementation MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Add native UDP support to the lwip raw TCP socket layer used by ESP8266 and RP2040, eliminating the need for Arduino WiFiUDP fallback. Two new classes: - LWIPRawUDPImpl: send-only UDP (8 bytes overhead) - LWIPRawUDPRecvImpl: send+recv with fixed-size ring buffer (no heap allocation in recv callback) Factory functions: socket_udp(), socket_udp_recv(), socket_ip_udp(), socket_ip_udp_recv() with UDPSocket/UDPRecvSocket type aliases. Additive only — no consumer migration in this PR. --- esphome/components/socket/headers.h | 10 + .../components/socket/lwip_raw_tcp_impl.cpp | 448 ++++++++++++++++-- esphome/components/socket/lwip_raw_tcp_impl.h | 86 ++++ esphome/components/socket/socket.cpp | 26 + esphome/components/socket/socket.h | 19 +- 5 files changed, 549 insertions(+), 40 deletions(-) diff --git a/esphome/components/socket/headers.h b/esphome/components/socket/headers.h index 16e4d23d3b..9ee3873c33 100644 --- a/esphome/components/socket/headers.h +++ b/esphome/components/socket/headers.h @@ -20,6 +20,16 @@ #define IPPROTO_IP 0 #define IPPROTO_TCP 6 +#define IPPROTO_UDP 17 + +#define IP_ADD_MEMBERSHIP 3 +#define IP_DROP_MEMBERSHIP 4 + +// NOLINTNEXTLINE(readability-identifier-naming) +struct ip_mreq { + struct in_addr imr_multiaddr; + struct in_addr imr_interface; +}; #if LWIP_IPV6 #define AF_INET6 10 diff --git a/esphome/components/socket/lwip_raw_tcp_impl.cpp b/esphome/components/socket/lwip_raw_tcp_impl.cpp index d7fa6a2694..26d62cd298 100644 --- a/esphome/components/socket/lwip_raw_tcp_impl.cpp +++ b/esphome/components/socket/lwip_raw_tcp_impl.cpp @@ -9,6 +9,10 @@ #include "esphome/core/helpers.h" #include "esphome/core/log.h" +#include "lwip/igmp.h" +#include "lwip/pbuf.h" +#include "lwip/udp.h" + #ifdef USE_ESP8266 #include // For esp_schedule() #elif defined(USE_RP2040) @@ -138,6 +142,48 @@ static const char *const TAG = "socket.lwip"; #define LWIP_LOG(msg, ...) #endif +// ---- Shared helpers ---- + +/// Convert lwip ip_addr_t + host-order port to sockaddr, based on the socket's address family. +/// Shared by both TCP (LWIPRawCommon) and UDP (LWIPRawUDPImpl) implementations. +static int lwip_ip_to_sockaddr(sa_family_t family, const ip_addr_t *ip, uint16_t port_host, struct sockaddr *name, + socklen_t *addrlen) { + if (family == AF_INET) { + if (*addrlen < sizeof(struct sockaddr_in)) { + errno = EINVAL; + return -1; + } + auto *addr = reinterpret_cast(name); + addr->sin_family = AF_INET; + *addrlen = addr->sin_len = sizeof(struct sockaddr_in); + addr->sin_port = htons(port_host); + inet_addr_from_ip4addr(&addr->sin_addr, ip_2_ip4(ip)); + return 0; + } +#if LWIP_IPV6 + if (family == AF_INET6) { + if (*addrlen < sizeof(struct sockaddr_in6)) { + errno = EINVAL; + return -1; + } + auto *addr = reinterpret_cast(name); + addr->sin6_family = AF_INET6; + *addrlen = addr->sin6_len = sizeof(struct sockaddr_in6); + addr->sin6_port = htons(port_host); + // AF_INET6 sockets may receive IPv4 packets; convert to IPv4-mapped IPv6. + if (IP_IS_V4(ip)) { + ip_addr_t mapped; + ip4_2_ipv4_mapped_ipv6(ip_2_ip6(&mapped), ip_2_ip4(ip)); + inet6_addr_from_ip6addr(&addr->sin6_addr, ip_2_ip6(&mapped)); + } else { + inet6_addr_from_ip6addr(&addr->sin6_addr, ip_2_ip6(ip)); + } + return 0; + } +#endif + return -1; +} + // ---- LWIPRawCommon methods ---- LWIPRawCommon::~LWIPRawCommon() { @@ -372,43 +418,8 @@ int LWIPRawCommon::setsockopt(int level, int optname, const void *optval, sockle } int LWIPRawCommon::ip2sockaddr_(ip_addr_t *ip, uint16_t port, struct sockaddr *name, socklen_t *addrlen) { - if (this->family_ == AF_INET) { - if (*addrlen < sizeof(struct sockaddr_in)) { - errno = EINVAL; - return -1; - } - - struct sockaddr_in *addr = reinterpret_cast(name); - addr->sin_family = AF_INET; - *addrlen = addr->sin_len = sizeof(struct sockaddr_in); - addr->sin_port = port; - inet_addr_from_ip4addr(&addr->sin_addr, ip_2_ip4(ip)); - return 0; - } -#if LWIP_IPV6 - else if (this->family_ == AF_INET6) { - if (*addrlen < sizeof(struct sockaddr_in6)) { - errno = EINVAL; - return -1; - } - - struct sockaddr_in6 *addr = reinterpret_cast(name); - addr->sin6_family = AF_INET6; - *addrlen = addr->sin6_len = sizeof(struct sockaddr_in6); - addr->sin6_port = port; - - // AF_INET6 sockets are bound to IPv4 as well, so we may encounter IPv4 addresses that must be converted to IPv6. - if (IP_IS_V4(ip)) { - ip_addr_t mapped; - ip4_2_ipv4_mapped_ipv6(ip_2_ip6(&mapped), ip_2_ip4(ip)); - inet6_addr_from_ip6addr(&addr->sin6_addr, ip_2_ip6(&mapped)); - } else { - inet6_addr_from_ip6addr(&addr->sin6_addr, ip_2_ip6(ip)); - } - return 0; - } -#endif - return -1; + // TCP pcb stores port in network byte order; convert to host order for the shared helper + return lwip_ip_to_sockaddr(this->family_, ip, ntohs(port), name, addrlen); } // ---- LWIPRawImpl methods ---- @@ -774,11 +785,350 @@ err_t LWIPRawListenImpl::accept_fn_(struct tcp_pcb *newpcb, err_t err) { return ERR_OK; } +// ---- LWIPRawUDPImpl (send-only) methods ---- + +LWIPRawUDPImpl::LWIPRawUDPImpl(sa_family_t family) : family_(family) { +#if LWIP_IPV6 + this->pcb_ = udp_new_ip_type(family == AF_INET6 ? IPADDR_TYPE_ANY : IPADDR_TYPE_V4); +#else + this->pcb_ = udp_new(); +#endif +} + +LWIPRawUDPImpl::~LWIPRawUDPImpl() { + if (this->pcb_ != nullptr) { + udp_remove(this->pcb_); + this->pcb_ = nullptr; + } +} + +int LWIPRawUDPImpl::bind_internal_(const struct sockaddr *name, socklen_t addrlen) { + if (this->pcb_ == nullptr) { + errno = EBADF; + return -1; + } + if (name == nullptr) { + errno = EINVAL; + return -1; + } + ip_addr_t ip; + uint16_t port; + if (!sockaddr_to_lwip(name, addrlen, &ip, &port)) { + errno = EINVAL; + return -1; + } +#if LWIP_IPV6 + // For bind, use IPADDR_TYPE_ANY on IPv6 sockets to accept both IPv4 and IPv6 + // packets (dual-stack). sockaddr_to_lwip uses IPADDR_TYPE_V6 which is correct + // for sendto destinations but too restrictive for bind. + if (this->family_ == AF_INET6) { + ip.type = IPADDR_TYPE_ANY; + } +#endif + err_t err = udp_bind(this->pcb_, &ip, port); + if (err == ERR_USE) { + errno = EADDRINUSE; + return -1; + } + if (err == ERR_VAL) { + errno = EINVAL; + return -1; + } + if (err != ERR_OK) { + errno = EIO; + return -1; + } + return 0; +} + +int LWIPRawUDPImpl::bind(const struct sockaddr *name, socklen_t addrlen) { return this->bind_internal_(name, addrlen); } + +int LWIPRawUDPImpl::close() { + if (this->pcb_ == nullptr) { + errno = EBADF; + return -1; + } + udp_remove(this->pcb_); + this->pcb_ = nullptr; + return 0; +} + +bool LWIPRawUDPImpl::sockaddr_to_lwip(const struct sockaddr *addr, socklen_t addrlen, ip_addr_t *ip, uint16_t *port) { + if (addrlen < sizeof(sa_family_t)) + return false; +#if LWIP_IPV6 + if (addr->sa_family == AF_INET) { + if (addrlen < sizeof(sockaddr_in)) + return false; + auto *addr4 = reinterpret_cast(addr); + *port = ntohs(addr4->sin_port); + ip->type = IPADDR_TYPE_V4; + ip->u_addr.ip4.addr = addr4->sin_addr.s_addr; + return true; + } + if (addr->sa_family == AF_INET6) { + if (addrlen < sizeof(sockaddr_in6)) + return false; + auto *addr6 = reinterpret_cast(addr); + *port = ntohs(addr6->sin6_port); + ip->type = IPADDR_TYPE_V6; + memcpy(&ip->u_addr.ip6.addr, &addr6->sin6_addr.un.u8_addr, 16); + return true; + } +#else + if (addr->sa_family == AF_INET) { + if (addrlen < sizeof(sockaddr_in)) + return false; + auto *addr4 = reinterpret_cast(addr); + *port = ntohs(addr4->sin_port); + ip->addr = addr4->sin_addr.s_addr; + return true; + } +#endif + return false; +} + +int LWIPRawUDPImpl::ip2sockaddr_(const ip_addr_t *ip, uint16_t port, struct sockaddr *name, socklen_t *addrlen) { + // UDP recv callback provides port in host byte order + return lwip_ip_to_sockaddr(this->family_, ip, port, name, addrlen); +} + +ssize_t LWIPRawUDPImpl::sendto(const void *buf, size_t len, int flags, const struct sockaddr *dest_addr, + socklen_t addrlen) { + if (this->pcb_ == nullptr) { + errno = EBADF; + return -1; + } + if (buf == nullptr || dest_addr == nullptr) { + errno = EINVAL; + return -1; + } + + // pbuf_alloc takes u16_t length; reject oversized packets + if (len > UINT16_MAX) { + errno = EMSGSIZE; + return -1; + } + + ip_addr_t dst_ip; + uint16_t dst_port; + if (!sockaddr_to_lwip(dest_addr, addrlen, &dst_ip, &dst_port)) { + errno = EINVAL; + return -1; + } + + // Allocate pbuf and copy data + struct pbuf *pb = pbuf_alloc(PBUF_TRANSPORT, (uint16_t) len, PBUF_RAM); + if (pb == nullptr) { + errno = ENOMEM; + return -1; + } + memcpy(pb->payload, buf, len); + + err_t err = udp_sendto(this->pcb_, pb, &dst_ip, dst_port); + pbuf_free(pb); + + if (err != ERR_OK) { + errno = err == ERR_MEM ? ENOMEM : EIO; + return -1; + } + return (ssize_t) len; +} + +int LWIPRawUDPImpl::setsockopt(int level, int optname, const void *optval, socklen_t optlen) { + if (this->pcb_ == nullptr) { + errno = EBADF; + return -1; + } + if (level == SOL_SOCKET && optname == SO_REUSEADDR) { + // lwip raw UDP doesn't enforce port exclusivity the same way, + // but we accept this silently for compatibility + return 0; + } + if (level == SOL_SOCKET && optname == SO_BROADCAST) { + if (optval == nullptr || optlen < sizeof(int)) { + errno = EINVAL; + return -1; + } + int val = *reinterpret_cast(optval); + if (val) { + ip_set_option(this->pcb_, SOF_BROADCAST); + } else { + ip_reset_option(this->pcb_, SOF_BROADCAST); + } + return 0; + } + if (level == IPPROTO_IP && optname == IP_ADD_MEMBERSHIP) { + if (optval == nullptr || optlen < sizeof(struct ip_mreq)) { + errno = EINVAL; + return -1; + } + auto *mreq = reinterpret_cast(optval); + ip4_addr_t multiaddr; + multiaddr.addr = mreq->imr_multiaddr.s_addr; + ip4_addr_t ifaddr; + ifaddr.addr = mreq->imr_interface.s_addr; + err_t err = igmp_joingroup(&ifaddr, &multiaddr); + if (err != ERR_OK) { + errno = EIO; + return -1; + } + return 0; + } + if (level == IPPROTO_IP && optname == IP_DROP_MEMBERSHIP) { + if (optval == nullptr || optlen < sizeof(struct ip_mreq)) { + errno = EINVAL; + return -1; + } + auto *mreq = reinterpret_cast(optval); + ip4_addr_t multiaddr; + multiaddr.addr = mreq->imr_multiaddr.s_addr; + ip4_addr_t ifaddr; + ifaddr.addr = mreq->imr_interface.s_addr; + err_t err = igmp_leavegroup(&ifaddr, &multiaddr); + if (err != ERR_OK) { + errno = EIO; + return -1; + } + return 0; + } + errno = ENOPROTOOPT; + return -1; +} + +int LWIPRawUDPImpl::getsockopt(int level, int optname, void *optval, socklen_t *optlen) { + if (this->pcb_ == nullptr) { + errno = EBADF; + return -1; + } + if (level == SOL_SOCKET && optname == SO_REUSEADDR) { + if (optval == nullptr || optlen == nullptr || *optlen < sizeof(int)) { + errno = EINVAL; + return -1; + } + *reinterpret_cast(optval) = 1; + *optlen = sizeof(int); + return 0; + } + errno = ENOPROTOOPT; + return -1; +} + +int LWIPRawUDPImpl::setblocking(bool blocking) { + if (blocking) { + // blocking operation not supported on raw lwip + errno = EINVAL; + return -1; + } + return 0; +} + +// ---- LWIPRawUDPRecvImpl methods ---- + +LWIPRawUDPRecvImpl::~LWIPRawUDPRecvImpl() { + // Flush rx queue and unregister callback before base destructor removes pcb + if (this->pcb_ != nullptr) + this->close(); +} + +int LWIPRawUDPRecvImpl::close() { + // Unregister recv callback before removing pcb + if (this->pcb_ != nullptr) { + udp_recv(this->pcb_, nullptr, nullptr); + } + // Flush any queued rx packets + while (this->rx_read_idx_ != this->rx_write_idx_) { + auto &pkt = this->rx_queue_[this->rx_read_idx_]; + if (pkt.pb != nullptr) { + pbuf_free(pkt.pb); + pkt.pb = nullptr; + } + this->rx_read_idx_ = (this->rx_read_idx_ + 1) & UDP_RX_MASK; + } + // close() returns EBADF if already closed, which is fine from destructor + return LWIPRawUDPImpl::close(); +} + +int LWIPRawUDPRecvImpl::bind(const struct sockaddr *name, socklen_t addrlen) { + int ret = this->bind_internal_(name, addrlen); + if (ret != 0) + return ret; + // Register recv callback now that we're bound and ready to receive + udp_recv(this->pcb_, LWIPRawUDPRecvImpl::s_recv_fn, this); + return 0; +} + +ssize_t LWIPRawUDPRecvImpl::read(void *buf, size_t len) { return this->recvfrom(buf, len, nullptr, nullptr); } + +ssize_t LWIPRawUDPRecvImpl::recvfrom(void *buf, size_t len, struct sockaddr *src_addr, socklen_t *addrlen) { + if (this->pcb_ == nullptr) { + errno = EBADF; + return -1; + } + if (buf == nullptr && len > 0) { + errno = EINVAL; + return -1; + } + if (this->rx_read_idx_ == this->rx_write_idx_) { + errno = EWOULDBLOCK; + return -1; + } + + auto &pkt = this->rx_queue_[this->rx_read_idx_]; + size_t pkt_len = pkt.pb->tot_len; + size_t copy_len = std::min(len, pkt_len); + + // Copy data from pbuf chain + pbuf_copy_partial(pkt.pb, buf, copy_len, 0); + + // Fill in source address if requested + if (src_addr != nullptr && addrlen != nullptr) { + this->ip2sockaddr_(&pkt.src_addr, pkt.src_port, src_addr, addrlen); + } + + // Free the pbuf and advance the read pointer — must be last, + // as this publishes the slot to the producer (recv callback). + pbuf_free(pkt.pb); + pkt.pb = nullptr; + this->rx_read_idx_ = (this->rx_read_idx_ + 1) & UDP_RX_MASK; + + return (ssize_t) copy_len; +} + +void LWIPRawUDPRecvImpl::s_recv_fn(void *arg, struct udp_pcb *pcb, struct pbuf *p, const ip_addr_t *addr, u16_t port) { + auto *self = reinterpret_cast(arg); + self->recv_fn_(p, addr, port); +} + +void LWIPRawUDPRecvImpl::recv_fn_(struct pbuf *p, const ip_addr_t *addr, u16_t port) { + if (p == nullptr) + return; + + // Check if queue is full (next write position would collide with read position) + uint8_t next_write = (this->rx_write_idx_ + 1) & UDP_RX_MASK; + if (next_write == this->rx_read_idx_) { + // Drop packet — queue full + pbuf_free(p); + return; + } + + // Enqueue the packet — write data first, then publish by advancing write index. + auto &slot = this->rx_queue_[this->rx_write_idx_]; + slot.pb = p; + slot.src_addr = *addr; + slot.src_port = port; + this->rx_write_idx_ = next_write; + +#if defined(USE_ESP8266) || defined(USE_RP2040) + socket_wake(); +#endif +} + // ---- Factory functions ---- std::unique_ptr socket(int domain, int type, int protocol) { if (type != SOCK_STREAM) { - ESP_LOGE(TAG, "UDP sockets not supported on this platform, use WiFiUDP"); + ESP_LOGE(TAG, "Use socket_udp() for UDP sockets on this platform"); errno = EPROTOTYPE; return nullptr; } @@ -796,9 +1146,29 @@ std::unique_ptr socket_loop_monitored(int domain, int type, int protocol return socket(domain, type, protocol); } +std::unique_ptr socket_udp(int domain, int protocol) { + (void) protocol; // Raw lwip UDP ignores protocol; kept for API compatibility + auto sock = make_unique((sa_family_t) domain); + if (!sock->is_valid()) { + errno = ENOMEM; + return nullptr; + } + return sock; +} + +std::unique_ptr socket_udp_recv(int domain, int protocol) { + (void) protocol; // Raw lwip UDP ignores protocol; kept for API compatibility + auto sock = make_unique((sa_family_t) domain); + if (!sock->is_valid()) { + errno = ENOMEM; + return nullptr; + } + return sock; +} + std::unique_ptr socket_listen(int domain, int type, int protocol) { if (type != SOCK_STREAM) { - ESP_LOGE(TAG, "UDP sockets not supported on this platform, use WiFiUDP"); + ESP_LOGE(TAG, "Use socket_udp() for UDP sockets on this platform"); errno = EPROTOTYPE; return nullptr; } diff --git a/esphome/components/socket/lwip_raw_tcp_impl.h b/esphome/components/socket/lwip_raw_tcp_impl.h index 5b2c11cfe2..79ba82d438 100644 --- a/esphome/components/socket/lwip_raw_tcp_impl.h +++ b/esphome/components/socket/lwip_raw_tcp_impl.h @@ -15,6 +15,7 @@ #include "lwip/netif.h" #include "lwip/opt.h" #include "lwip/tcp.h" +#include "lwip/udp.h" namespace esphome::socket { @@ -201,6 +202,91 @@ class LWIPRawListenImpl : public LWIPRawCommon { uint8_t accepted_socket_count_ = 0; // Number of sockets currently in queue }; +/// Send-only UDP socket implementation for LWIP raw API. +/// Non-virtual, concrete type. Uses lwip/udp.h raw API. +/// No receive capability — use LWIPRawUDPRecvImpl for sockets that need to receive. +class LWIPRawUDPImpl { + public: + LWIPRawUDPImpl(sa_family_t family); + ~LWIPRawUDPImpl(); + LWIPRawUDPImpl(const LWIPRawUDPImpl &) = delete; + LWIPRawUDPImpl &operator=(const LWIPRawUDPImpl &) = delete; + + int bind(const struct sockaddr *name, socklen_t addrlen); + int close(); + + /// Send a UDP packet to the specified destination. + ssize_t sendto(const void *buf, size_t len, int flags, const struct sockaddr *dest_addr, socklen_t addrlen); + + int setsockopt(int level, int optname, const void *optval, socklen_t optlen); + int getsockopt(int level, int optname, void *optval, socklen_t *optlen); + + int setblocking(bool blocking); + + bool is_valid() const { return this->pcb_ != nullptr; } + bool ready() const { return false; } + int get_fd() const { return -1; } + + protected: + /// Convert sockaddr to lwip ip_addr_t and port. + static bool sockaddr_to_lwip(const struct sockaddr *addr, socklen_t addrlen, ip_addr_t *ip, uint16_t *port); + /// Convert lwip ip_addr_t and port to sockaddr. + int ip2sockaddr_(const ip_addr_t *ip, uint16_t port, struct sockaddr *name, socklen_t *addrlen); + + /// Shared bind logic — parses sockaddr and calls udp_bind. + int bind_internal_(const struct sockaddr *name, socklen_t addrlen); + + struct udp_pcb *pcb_{nullptr}; + sa_family_t family_{0}; +}; + +/// UDP socket with receive support for LWIP raw API. +/// Extends LWIPRawUDPImpl with a fixed-size ring buffer for incoming packets. +/// The recv callback is registered on bind(). +class LWIPRawUDPRecvImpl : public LWIPRawUDPImpl { + public: + using LWIPRawUDPImpl::LWIPRawUDPImpl; + ~LWIPRawUDPRecvImpl(); + + /// Close the socket, flushing any queued rx packets first. + int close(); + + /// Bind and register the recv callback for incoming packets. + int bind(const struct sockaddr *name, socklen_t addrlen); + + /// Read the next queued packet, discarding source address info. + /// If buf is smaller than the packet, data is silently truncated (returns bytes copied). + ssize_t read(void *buf, size_t len); + /// Read the next queued packet and return the source address. + /// If buf is smaller than the packet, data is silently truncated (returns bytes copied). + ssize_t recvfrom(void *buf, size_t len, struct sockaddr *src_addr, socklen_t *addrlen); + + /// Returns true if there are packets available to read. + bool ready() const { return this->rx_read_idx_ != this->rx_write_idx_; } + + protected: + static void s_recv_fn(void *arg, struct udp_pcb *pcb, struct pbuf *p, const ip_addr_t *addr, u16_t port); + void recv_fn_(struct pbuf *p, const ip_addr_t *addr, u16_t port); + + /// Lock-free SPSC ring buffer for received UDP packets. + /// Producer (recv callback, possibly IRQ context on RP2040) writes rx_write_idx_. + /// Consumer (main loop) writes rx_read_idx_. + /// No shared read-modify-write — safe without locking. + /// One slot is reserved to distinguish full from empty, giving 3 usable slots. + /// No heap allocation in the recv callback — packets are dropped if the queue is full. + static constexpr uint8_t UDP_RX_QUEUE_SIZE = 4; // Must be power of 2 + static constexpr uint8_t UDP_RX_MASK = UDP_RX_QUEUE_SIZE - 1; + static_assert((UDP_RX_QUEUE_SIZE & UDP_RX_MASK) == 0, "UDP_RX_QUEUE_SIZE must be power of 2"); + struct UDPRxPacket { + struct pbuf *pb{nullptr}; + ip_addr_t src_addr{}; + uint16_t src_port{0}; + }; + std::array rx_queue_{}; + volatile uint8_t rx_read_idx_{0}; ///< Written by consumer (main loop), read by producer + volatile uint8_t rx_write_idx_{0}; ///< Written by producer (recv callback), read by consumer +}; + } // namespace esphome::socket #endif // USE_SOCKET_IMPL_LWIP_TCP diff --git a/esphome/components/socket/socket.cpp b/esphome/components/socket/socket.cpp index bfb6ae8e13..d039c5436e 100644 --- a/esphome/components/socket/socket.cpp +++ b/esphome/components/socket/socket.cpp @@ -101,6 +101,32 @@ std::unique_ptr socket_ip_loop_monitored(int type, int protocol) { } #endif +#if !defined(USE_SOCKET_IMPL_LWIP_TCP) +// BSD and LWIP_SOCKETS: UDPSocket == UDPRecvSocket == Socket, so these just delegate. +std::unique_ptr socket_udp(int domain, int protocol) { + return esphome::socket::socket(domain, SOCK_DGRAM, protocol); +} +std::unique_ptr socket_udp_recv(int domain, int protocol) { + return esphome::socket::socket(domain, SOCK_DGRAM, protocol); +} +#endif + +std::unique_ptr socket_ip_udp(int protocol) { +#if USE_NETWORK_IPV6 + return socket_udp(AF_INET6, protocol); +#else + return socket_udp(AF_INET, protocol); +#endif +} + +std::unique_ptr socket_ip_udp_recv(int protocol) { +#if USE_NETWORK_IPV6 + return socket_udp_recv(AF_INET6, protocol); +#else + return socket_udp_recv(AF_INET, protocol); +#endif +} + socklen_t set_sockaddr(struct sockaddr *addr, socklen_t addrlen, const char *ip_address, uint16_t port) { #if USE_NETWORK_IPV6 if (strchr(ip_address, ':') != nullptr) { diff --git a/esphome/components/socket/socket.h b/esphome/components/socket/socket.h index a21bd64730..c790719816 100644 --- a/esphome/components/socket/socket.h +++ b/esphome/components/socket/socket.h @@ -27,17 +27,24 @@ namespace esphome::socket { // Type aliases — only one implementation is active per build. // Socket is the concrete type for connected sockets. // ListenSocket is the concrete type for listening/server sockets. -// On BSD and LWIP_SOCKETS, both aliases resolve to the same type. +// UDPSocket is the concrete type for UDP sockets. +// On BSD and LWIP_SOCKETS, all aliases resolve to the same type. // On LWIP_TCP, they are different types (no virtual dispatch between them). #ifdef USE_SOCKET_IMPL_BSD_SOCKETS using Socket = BSDSocketImpl; using ListenSocket = BSDSocketImpl; +using UDPSocket = BSDSocketImpl; +using UDPRecvSocket = BSDSocketImpl; #elif defined(USE_SOCKET_IMPL_LWIP_SOCKETS) using Socket = LwIPSocketImpl; using ListenSocket = LwIPSocketImpl; +using UDPSocket = LwIPSocketImpl; +using UDPRecvSocket = LwIPSocketImpl; #elif defined(USE_SOCKET_IMPL_LWIP_TCP) using Socket = LWIPRawImpl; using ListenSocket = LWIPRawListenImpl; +using UDPSocket = LWIPRawUDPImpl; +using UDPRecvSocket = LWIPRawUDPRecvImpl; #endif #ifdef USE_LWIP_FAST_SELECT @@ -68,6 +75,16 @@ std::unique_ptr socket(int domain, int type, int protocol); /// Create a socket in the newest available IP domain (IPv6 or IPv4) of the given type and protocol. std::unique_ptr socket_ip(int type, int protocol); +/// Create a send-only UDP socket of the given domain and protocol. +std::unique_ptr socket_udp(int domain, int protocol); +/// Create a send-only UDP socket in the newest available IP domain. +std::unique_ptr socket_ip_udp(int protocol); + +/// Create a UDP socket with receive support of the given domain and protocol. +std::unique_ptr socket_udp_recv(int domain, int protocol); +/// Create a UDP socket with receive support in the newest available IP domain. +std::unique_ptr socket_ip_udp_recv(int protocol); + /// Create a socket and monitor it for data in the main loop. /// Like socket() but also registers the socket with the Application's select() loop. /// WARNING: These functions are NOT thread-safe. They must only be called from the main loop