mirror of
https://github.com/esphome/esphome.git
synced 2026-09-11 15:27:33 +00:00
[socket] Add lwip raw UDP socket implementation
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.
This commit is contained in:
@@ -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
|
||||
|
||||
@@ -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 <coredecls.h> // 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<struct sockaddr_in *>(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<struct sockaddr_in6 *>(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<struct sockaddr_in *>(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<struct sockaddr_in6 *>(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<const sockaddr_in *>(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<const sockaddr_in6 *>(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<const sockaddr_in *>(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<const int *>(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<const struct ip_mreq *>(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<const struct ip_mreq *>(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<int *>(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<LWIPRawUDPRecvImpl *>(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> 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> socket_loop_monitored(int domain, int type, int protocol
|
||||
return socket(domain, type, protocol);
|
||||
}
|
||||
|
||||
std::unique_ptr<UDPSocket> socket_udp(int domain, int protocol) {
|
||||
(void) protocol; // Raw lwip UDP ignores protocol; kept for API compatibility
|
||||
auto sock = make_unique<LWIPRawUDPImpl>((sa_family_t) domain);
|
||||
if (!sock->is_valid()) {
|
||||
errno = ENOMEM;
|
||||
return nullptr;
|
||||
}
|
||||
return sock;
|
||||
}
|
||||
|
||||
std::unique_ptr<UDPRecvSocket> socket_udp_recv(int domain, int protocol) {
|
||||
(void) protocol; // Raw lwip UDP ignores protocol; kept for API compatibility
|
||||
auto sock = make_unique<LWIPRawUDPRecvImpl>((sa_family_t) domain);
|
||||
if (!sock->is_valid()) {
|
||||
errno = ENOMEM;
|
||||
return nullptr;
|
||||
}
|
||||
return sock;
|
||||
}
|
||||
|
||||
std::unique_ptr<ListenSocket> 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;
|
||||
}
|
||||
|
||||
@@ -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<UDPRxPacket, UDP_RX_QUEUE_SIZE> 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
|
||||
|
||||
@@ -101,6 +101,32 @@ std::unique_ptr<ListenSocket> 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<UDPSocket> socket_udp(int domain, int protocol) {
|
||||
return esphome::socket::socket(domain, SOCK_DGRAM, protocol);
|
||||
}
|
||||
std::unique_ptr<UDPRecvSocket> socket_udp_recv(int domain, int protocol) {
|
||||
return esphome::socket::socket(domain, SOCK_DGRAM, protocol);
|
||||
}
|
||||
#endif
|
||||
|
||||
std::unique_ptr<UDPSocket> 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<UDPRecvSocket> 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) {
|
||||
|
||||
@@ -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> 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> socket_ip(int type, int protocol);
|
||||
|
||||
/// Create a send-only UDP socket of the given domain and protocol.
|
||||
std::unique_ptr<UDPSocket> socket_udp(int domain, int protocol);
|
||||
/// Create a send-only UDP socket in the newest available IP domain.
|
||||
std::unique_ptr<UDPSocket> socket_ip_udp(int protocol);
|
||||
|
||||
/// Create a UDP socket with receive support of the given domain and protocol.
|
||||
std::unique_ptr<UDPRecvSocket> socket_udp_recv(int domain, int protocol);
|
||||
/// Create a UDP socket with receive support in the newest available IP domain.
|
||||
std::unique_ptr<UDPRecvSocket> 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
|
||||
|
||||
Reference in New Issue
Block a user