diff --git a/esphome/components/esp32_ble/ble.cpp b/esphome/components/esp32_ble/ble.cpp index a202afb2d2..aa8e21aff2 100644 --- a/esphome/components/esp32_ble/ble.cpp +++ b/esphome/components/esp32_ble/ble.cpp @@ -81,8 +81,6 @@ void ESP32BLE::disable() { this->state_ = BLE_COMPONENT_STATE_DISABLE; } -bool ESP32BLE::is_active() { return this->state_ == BLE_COMPONENT_STATE_ACTIVE; } - #ifdef USE_ESP32_BLE_ADVERTISING void ESP32BLE::advertising_start() { this->advertising_init_(); diff --git a/esphome/components/esp32_ble/ble.h b/esphome/components/esp32_ble/ble.h index f3fb8aa1c3..134d68f455 100644 --- a/esphome/components/esp32_ble/ble.h +++ b/esphome/components/esp32_ble/ble.h @@ -135,7 +135,7 @@ class ESP32BLE : public Component { void enable(); void disable(); - bool is_active(); + ESPHOME_ALWAYS_INLINE bool is_active() { return this->state_ == BLE_COMPONENT_STATE_ACTIVE; } void setup() override; void loop() override; void dump_config() override; diff --git a/esphome/components/esp32_ble_server/ble_server.cpp b/esphome/components/esp32_ble_server/ble_server.cpp index ecc53e197f..1dd97db486 100644 --- a/esphome/components/esp32_ble_server/ble_server.cpp +++ b/esphome/components/esp32_ble_server/ble_server.cpp @@ -7,7 +7,6 @@ #ifdef USE_ESP32 -#include #include #include #include @@ -39,16 +38,17 @@ void BLEServer::loop() { case RUNNING: { // Start all services that are pending to start if (!this->services_to_start_.empty()) { - for (auto &service : this->services_to_start_) { + size_t write_idx = 0; + for (auto service : this->services_to_start_) { if (service->is_created()) { service->start(); // Needs to be called once per characteristic in the service } + // Keep services still pending (drop those that are starting or running) + if (!service->is_starting() && !service->is_running()) { + this->services_to_start_[write_idx++] = service; + } } - // Remove services that have been started - this->services_to_start_.erase( - std::remove_if(this->services_to_start_.begin(), this->services_to_start_.end(), - [](BLEService *service) { return service->is_starting() || service->is_running(); }), - this->services_to_start_.end()); + this->services_to_start_.erase(this->services_to_start_.begin() + write_idx, this->services_to_start_.end()); } break; } @@ -91,8 +91,6 @@ void BLEServer::loop() { } } -bool BLEServer::is_running() { return this->parent_->is_active() && this->state_ == RUNNING; } - bool BLEServer::can_proceed() { return this->is_running() || !this->parent_->is_active(); } void BLEServer::restart_advertising_() { diff --git a/esphome/components/esp32_ble_server/ble_server.h b/esphome/components/esp32_ble_server/ble_server.h index ff7e0044e4..1b419d2ee4 100644 --- a/esphome/components/esp32_ble_server/ble_server.h +++ b/esphome/components/esp32_ble_server/ble_server.h @@ -32,7 +32,7 @@ class BLEServer : public Component, public GATTsEventHandler, public BLEStatusEv float get_setup_priority() const override; bool can_proceed() override; - bool is_running(); + ESPHOME_ALWAYS_INLINE bool is_running() { return this->parent_->is_active() && this->state_ == RUNNING; } void set_manufacturer_data(const std::vector &data) { this->manufacturer_data_ = data; diff --git a/esphome/components/socket/headers.h b/esphome/components/socket/headers.h index 0eece6480f..101613de25 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 3bcbd88085..69a8e0e9c8 100644 --- a/esphome/components/socket/lwip_raw_tcp_impl.cpp +++ b/esphome/components/socket/lwip_raw_tcp_impl.cpp @@ -10,6 +10,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) @@ -143,6 +147,51 @@ 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. +/// @param port_host Port in host byte order. TCP callers must convert from network order first +/// (tcp_pcb stores ports in network byte order); UDP callers can pass directly +/// (lwip udp_recv callback provides port in host byte order). +/// 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; +} + // Clear arg, recv, and err callbacks, then abort a connected PCB. // Only valid for full tcp_pcb (not tcp_pcb_listen). // Must be called before destroying the object that tcp_arg points to — @@ -176,6 +225,59 @@ static err_t pcb_detach_close(struct tcp_pcb *pcb) { return err; } +/// Convert sockaddr to lwip ip_addr_t and host-order port. +/// For IPv6, sets type to IPADDR_TYPE_V6 (callers that need dual-stack should +/// override to IPADDR_TYPE_ANY after calling). +/// Shared by both TCP (LWIPRawCommon) and UDP (LWIPRawUDPImpl) bind/sendto paths. +static bool sockaddr_to_lwip(const struct sockaddr *addr, socklen_t addrlen, ip_addr_t *ip, uint16_t *port) { + if (addrlen < sizeof(struct sockaddr)) + 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; +} + +/// Map lwip bind error to errno. Returns 0 on success, -1 on error with errno set. +static int lwip_bind_err(err_t err) { + if (err == ERR_OK) + return 0; + if (err == ERR_USE) { + errno = EADDRINUSE; + } else if (err == ERR_VAL) { + errno = EINVAL; + } else { + errno = EIO; + } + return -1; +} + // ---- LWIPRawCommon methods ---- LWIPRawCommon::~LWIPRawCommon() { @@ -198,59 +300,20 @@ int LWIPRawCommon::bind(const struct sockaddr *name, socklen_t addrlen) { return -1; } ip_addr_t ip; - in_port_t port; + uint16_t port; + if (!sockaddr_to_lwip(name, addrlen, &ip, &port)) { + errno = EINVAL; + return -1; + } #if LWIP_IPV6 - if (this->family_ == AF_INET) { - if (addrlen < sizeof(sockaddr_in)) { - errno = EINVAL; - return -1; - } - auto *addr4 = reinterpret_cast(name); - port = ntohs(addr4->sin_port); - ip.type = IPADDR_TYPE_V4; - ip.u_addr.ip4.addr = addr4->sin_addr.s_addr; - LWIP_LOG("tcp_bind(%p ip=%s port=%u)", this->pcb_, ip4addr_ntoa(&ip.u_addr.ip4), port); - } else if (this->family_ == AF_INET6) { - if (addrlen < sizeof(sockaddr_in6)) { - errno = EINVAL; - return -1; - } - auto *addr6 = reinterpret_cast(name); - port = ntohs(addr6->sin6_port); + // Use IPADDR_TYPE_ANY for dual-stack (accept both IPv4 and IPv6) + if (this->family_ == AF_INET6) { ip.type = IPADDR_TYPE_ANY; - memcpy(&ip.u_addr.ip6.addr, &addr6->sin6_addr.un.u8_addr, 16); - LWIP_LOG("tcp_bind(%p ip=%s port=%u)", this->pcb_, ip6addr_ntoa(&ip.u_addr.ip6), port); - } else { - errno = EINVAL; - return -1; } -#else - if (this->family_ != AF_INET) { - errno = EINVAL; - return -1; - } - auto *addr4 = reinterpret_cast(name); - port = ntohs(addr4->sin_port); - ip.addr = addr4->sin_addr.s_addr; - LWIP_LOG("tcp_bind(%p ip=%u port=%u)", this->pcb_, ip.addr, port); #endif err_t err = tcp_bind(this->pcb_, &ip, port); - if (err == ERR_USE) { - LWIP_LOG(" -> err ERR_USE"); - errno = EADDRINUSE; - return -1; - } - if (err == ERR_VAL) { - LWIP_LOG(" -> err ERR_VAL"); - errno = EINVAL; - return -1; - } - if (err != ERR_OK) { - LWIP_LOG(" -> err %d", err); - errno = EIO; - return -1; - } - return 0; + LWIP_LOG(" -> err %d", err); + return lwip_bind_err(err); } int LWIPRawCommon::close() { @@ -435,43 +498,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 ---- @@ -958,11 +986,331 @@ 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) { + LWIP_LOCK(); +#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() { + // Early return avoids acquiring the lwip lock when pcb_ is already null + // (e.g., after LWIPRawUDPRecvImpl::close() already cleaned up). + if (this->pcb_ == nullptr) + return; + LWIP_LOCK(); + udp_remove(this->pcb_); + this->pcb_ = nullptr; +} + +int LWIPRawUDPImpl::bind_internal_locked_(const struct sockaddr *name, socklen_t addrlen) { + // Caller must hold LWIP_LOCK + 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 + return lwip_bind_err(udp_bind(this->pcb_, &ip, port)); +} + +int LWIPRawUDPImpl::bind(const struct sockaddr *name, socklen_t addrlen) { + LWIP_LOCK(); + return this->bind_internal_locked_(name, addrlen); +} + +int LWIPRawUDPImpl::close() { + LWIP_LOCK(); + return this->close_internal_locked_(); +} + +int LWIPRawUDPImpl::close_internal_locked_() { + // Caller must hold LWIP_LOCK + if (this->pcb_ == nullptr) { + errno = EBADF; + return -1; + } + udp_remove(this->pcb_); + this->pcb_ = nullptr; + return 0; +} + +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) { + (void) flags; // Flags (MSG_DONTWAIT, etc.) are ignored; raw lwip is always non-blocking + LWIP_LOCK(); + 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) { + LWIP_LOCK(); + 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) { + LWIP_LOCK(); + 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() { + LWIP_LOCK(); + // Unregister recv callback before removing pcb + if (this->pcb_ != nullptr) { + udp_recv(this->pcb_, nullptr, nullptr); + } + // Flush any queued rx packets + while (this->rx_count_ > 0) { + 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; + this->rx_count_--; + } + // close_internal_locked_() returns EBADF if already closed, which is fine from destructor + return this->close_internal_locked_(); +} + +int LWIPRawUDPRecvImpl::bind(const struct sockaddr *name, socklen_t addrlen) { + LWIP_LOCK(); + int ret = this->bind_internal_locked_(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 (buf == nullptr && len > 0) { + errno = EINVAL; + return -1; + } + LWIP_LOCK(); + if (this->pcb_ == nullptr) { + errno = EBADF; + return -1; + } + if (this->rx_count_ == 0) { + 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 ip2sockaddr_ fails (e.g., addrlen too small), fail the entire recvfrom + // rather than silently returning data without a source address. + if (src_addr != nullptr && addrlen != nullptr && + this->ip2sockaddr_(&pkt.src_addr, pkt.src_port, src_addr, addrlen) != 0) { + // Don't consume the packet on address conversion failure + return -1; + } + + // Free the pbuf and advance the read pointer + pbuf_free(pkt.pb); + pkt.pb = nullptr; + this->rx_read_idx_ = (this->rx_read_idx_ + 1) & UDP_RX_MASK; + this->rx_count_--; + + 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); +} + +// LWIP CALLBACK — runs from IRQ context on RP2040 (low-priority user IRQ). +// No heap allocation allowed — malloc is not IRQ-safe (see #14687). +// No LWIP_LOCK() needed — lwip core already holds the async_context lock. +void LWIPRawUDPRecvImpl::recv_fn_(struct pbuf *p, const ip_addr_t *addr, u16_t port) { + if (p == nullptr) + return; + + // Check if queue is full + if (this->rx_count_ >= UDP_RX_QUEUE_SIZE) { + // Drop packet — queue full + pbuf_free(p); + return; + } + + // Enqueue the packet + uint8_t write_idx = (this->rx_read_idx_ + this->rx_count_) & UDP_RX_MASK; + auto &slot = this->rx_queue_[write_idx]; + slot.pb = p; + slot.src_addr = *addr; + slot.src_port = port; + this->rx_count_++; + +#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; } @@ -980,9 +1328,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 3c27d71062..5900950707 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 { @@ -212,6 +213,99 @@ class LWIPRawListenImpl : public LWIPRawCommon { uint8_t accepted_socket_count_ = 0; // Number of entries 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 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. Caller must hold LWIP_LOCK. + int bind_internal_locked_(const struct sockaddr *name, socklen_t addrlen); + + /// Shared close logic — unregisters and removes udp pcb. Caller must hold LWIP_LOCK. + int close_internal_locked_(); + + 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(). +/// +/// Note: close() and bind() intentionally hide the base class methods to add +/// recv callback registration/cleanup. This is safe because these classes are +/// never used polymorphically (no virtual dispatch) — callers always use the +/// concrete LWIPRawUDPRecvImpl type via the UDPRecvSocket alias. +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). + /// Note: unlike POSIX MSG_TRUNC, this does not return the original packet length on truncation. + 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). + /// Note: unlike POSIX MSG_TRUNC, this does not return the original packet length on truncation. + ssize_t recvfrom(void *buf, size_t len, struct sockaddr *src_addr, socklen_t *addrlen); + + /// Returns true if there are packets available to read. + /// Intentionally unlocked — same rationale as LWIPRawImpl::ready(). + bool ready() const { return this->rx_count_ > 0; } + + 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); + + /// Ring buffer for received UDP packets. + /// Both producer (recv callback) and consumer (main loop) are serialized by the + /// lwip lock — the callback runs under lwip core lock, and consumer methods hold + /// LWIP_LOCK(). All 4 slots are usable (no wasted slot for full/empty distinction). + /// No heap allocation in the recv callback — packets are dropped if the queue is full. + static constexpr uint8_t UDP_RX_QUEUE_SIZE = 4; + 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_{}; + uint8_t rx_read_idx_{0}; + uint8_t rx_count_{0}; +}; + } // 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