ESPHome 2026.11.0-dev
Loading...
Searching...
No Matches
lwip_raw_udp_impl.cpp
Go to the documentation of this file.
1#include "socket.h"
3
4#ifdef USE_SOCKET_IMPL_LWIP_TCP
5
6#include <cerrno>
7#include <cstring>
8
10#include "esphome/core/log.h"
11#include "esphome/core/wake.h"
13
14#include "lwip/igmp.h"
15#include "lwip/pbuf.h"
16#include "lwip/udp.h"
17
18namespace esphome::socket {
19
20// LWIP thread safety — see lwip_raw_common_impl.h for full explanation.
21// esphome::LwIPLock is the platform-provided RAII guard.
22// On RP2040, it acquires cyw43_arch_lwip_begin/end. On ESP8266, it's a no-op.
23#define LWIP_LOCK() esphome::LwIPLock lwip_lock_guard // NOLINT
24
25// ---- LWIPRawUDPSendImpl (send-only) methods ----
28 // Guard avoids acquiring the lwip lock when already closed
29 if (this->pcb_ != nullptr)
30 this->close();
31}
33int LWIPRawUDPSendImpl::bind(const struct sockaddr *name, socklen_t addrlen) {
34 LWIP_LOCK();
35 if (this->pcb_ == nullptr) {
36 errno = EBADF;
37 return -1;
38 }
39 if (name == nullptr) {
40 errno = EINVAL;
41 return -1;
42 }
43 ip_addr_t ip;
44 uint16_t port;
45 if (!sockaddr_to_lwip_bind(this->family_, name, addrlen, &ip, &port)) {
46 errno = EINVAL;
47 return -1;
48 }
49 err_t err = udp_bind(this->pcb_, &ip, port);
50 if (err != ERR_OK) {
51 errno = lwip_err_to_errno(err);
52 return -1;
53 }
54 return 0;
55}
56
58 LWIP_LOCK();
59 return this->close_internal_locked_();
60}
61
63 // Caller must hold LWIP_LOCK
64 if (this->pcb_ == nullptr) {
65 errno = EBADF;
66 return -1;
67 }
68 udp_remove(this->pcb_);
69 this->pcb_ = nullptr;
70 return 0;
71}
72
73int LWIPRawUDPSendImpl::ip2sockaddr_(const ip_addr_t *ip, uint16_t port, struct sockaddr *name, socklen_t *addrlen) {
74 // UDP recv callback provides port in host byte order
75 return lwip_ip_to_sockaddr(this->family_, ip, port, name, addrlen);
76}
77
78ssize_t LWIPRawUDPSendImpl::sendto(const void *buf, size_t len, int flags, const struct sockaddr *dest_addr,
79 socklen_t addrlen) {
80 (void) flags; // Flags (MSG_DONTWAIT, etc.) are ignored; raw lwip is always non-blocking
81 if (buf == nullptr || dest_addr == nullptr) {
82 errno = EINVAL;
83 return -1;
84 }
85
86 // pbuf_alloc takes u16_t length; reject oversized packets
87 if (len > UINT16_MAX) {
88 errno = EMSGSIZE;
89 return -1;
90 }
91
92 ip_addr_t dst_ip;
93 uint16_t dst_port;
94 if (!sockaddr_to_lwip(dest_addr, addrlen, &dst_ip, &dst_port)) {
95 errno = EINVAL;
96 return -1;
97 }
98
99 LWIP_LOCK();
100 if (this->pcb_ == nullptr) {
101 errno = EBADF;
102 return -1;
103 }
104
105 // Allocate pbuf and copy data
106 struct pbuf *pb = pbuf_alloc(PBUF_TRANSPORT, (uint16_t) len, PBUF_RAM);
107 if (pb == nullptr) {
108 errno = ENOMEM;
109 return -1;
110 }
111 memcpy(pb->payload, buf, len);
112
113 err_t err = udp_sendto(this->pcb_, pb, &dst_ip, dst_port);
114 pbuf_free(pb);
115
116 if (err != ERR_OK) {
117 errno = lwip_err_to_errno(err);
118 return -1;
119 }
120 return (ssize_t) len;
121}
122
123int LWIPRawUDPSendImpl::setsockopt(int level, int optname, const void *optval, socklen_t optlen) {
124 LWIP_LOCK();
125 if (this->pcb_ == nullptr) {
126 errno = EBADF;
127 return -1;
128 }
129 if (level == SOL_SOCKET && optname == SO_REUSEADDR) {
130 if (optval == nullptr || optlen < sizeof(int)) {
131 errno = EINVAL;
132 return -1;
133 }
134 // Effective only where lwip is built with SO_REUSE=1 (ESP8266 yes, RP2040 currently no)
135 if (*reinterpret_cast<const int *>(optval)) {
136 ip_set_option(this->pcb_, SOF_REUSEADDR);
137 } else {
138 ip_reset_option(this->pcb_, SOF_REUSEADDR);
139 }
140 return 0;
141 }
142 if (level == SOL_SOCKET && optname == SO_BROADCAST) {
143 if (optval == nullptr || optlen < sizeof(int)) {
144 errno = EINVAL;
145 return -1;
146 }
147 int val = *reinterpret_cast<const int *>(optval);
148 if (val) {
149 ip_set_option(this->pcb_, SOF_BROADCAST);
150 } else {
151 ip_reset_option(this->pcb_, SOF_BROADCAST);
152 }
153 return 0;
154 }
155 if (level == IPPROTO_IP && (optname == IP_ADD_MEMBERSHIP || optname == IP_DROP_MEMBERSHIP)) {
156 if (optval == nullptr || optlen < sizeof(struct ip_mreq)) {
157 errno = EINVAL;
158 return -1;
159 }
160 auto *mreq = reinterpret_cast<const struct ip_mreq *>(optval);
161 ip4_addr_t multiaddr{mreq->imr_multiaddr.s_addr};
162 ip4_addr_t ifaddr{mreq->imr_interface.s_addr};
163 err_t err =
164 optname == IP_ADD_MEMBERSHIP ? igmp_joingroup(&ifaddr, &multiaddr) : igmp_leavegroup(&ifaddr, &multiaddr);
165 if (err != ERR_OK) {
166 errno = EIO;
167 return -1;
168 }
169 return 0;
170 }
171 errno = ENOPROTOOPT;
172 return -1;
173}
174
175int LWIPRawUDPSendImpl::getsockopt(int level, int optname, void *optval, socklen_t *optlen) {
176 LWIP_LOCK();
177 if (this->pcb_ == nullptr) {
178 errno = EBADF;
179 return -1;
180 }
181 if (level == SOL_SOCKET && optname == SO_REUSEADDR) {
182 if (optval == nullptr || optlen == nullptr || *optlen < sizeof(int)) {
183 errno = EINVAL;
184 return -1;
185 }
186 *reinterpret_cast<int *>(optval) = ip_get_option(this->pcb_, SOF_REUSEADDR) ? 1 : 0;
187 *optlen = sizeof(int);
188 return 0;
189 }
190 errno = ENOPROTOOPT;
191 return -1;
192}
193
195 if (blocking) {
196 // blocking operation not supported on raw lwip
197 errno = EINVAL;
198 return -1;
199 }
200 return 0;
201}
202
203// ---- LWIPRawUDPImpl methods ----
204
205LWIPRawUDPImpl::LWIPRawUDPImpl(sa_family_t family, struct udp_pcb *pcb) : LWIPRawUDPSendImpl(family, pcb) {
206 // Registered here (not in bind) so unbound client sockets can receive replies
207 udp_recv(this->pcb_, LWIPRawUDPImpl::s_recv_fn, this);
208}
209
211 // Flush rx queue and unregister callback before base destructor removes pcb
212 if (this->pcb_ != nullptr)
213 this->close();
214}
215
217 LWIP_LOCK();
218 // Unregister recv callback before removing pcb
219 if (this->pcb_ != nullptr) {
220 udp_recv(this->pcb_, nullptr, nullptr);
221 }
222 // Flush queued rx packets; slots within rx_count_ always hold a live pbuf
223 for (; this->rx_count_ > 0; this->rx_count_--) {
224 pbuf_free(this->rx_queue_[this->rx_read_idx_].pb);
225 this->rx_read_idx_ = (this->rx_read_idx_ + 1) & UDP_RX_MASK;
226 }
227 // close_internal_locked_() returns EBADF if already closed, which is fine from destructor
228 return this->close_internal_locked_();
229}
230
231ssize_t LWIPRawUDPImpl::read(void *buf, size_t len) { return this->recvfrom(buf, len, nullptr, nullptr); }
232
233ssize_t LWIPRawUDPImpl::recvfrom(void *buf, size_t len, struct sockaddr *src_addr, socklen_t *addrlen) {
234 if (buf == nullptr && len > 0) {
235 errno = EINVAL;
236 return -1;
237 }
238 LWIP_LOCK();
239 if (this->pcb_ == nullptr) {
240 errno = EBADF;
241 return -1;
242 }
243 if (this->rx_count_ == 0) {
244 errno = EWOULDBLOCK;
245 return -1;
246 }
247
248 auto &pkt = this->rx_queue_[this->rx_read_idx_];
249 // On address conversion failure, still consume the packet — the failure is
250 // deterministic (family_ and *addrlen), so keeping it would wedge the queue
251 ssize_t ret = -1;
252 if (src_addr == nullptr || addrlen == nullptr ||
253 this->ip2sockaddr_(&pkt.src_addr, pkt.src_port, src_addr, addrlen) == 0) {
254 ret = (ssize_t) std::min(len, (size_t) pkt.pb->tot_len);
255 pbuf_copy_partial(pkt.pb, buf, ret, 0);
256 }
257 pbuf_free(pkt.pb);
258 this->rx_read_idx_ = (this->rx_read_idx_ + 1) & UDP_RX_MASK;
259 this->rx_count_--;
260 return ret;
261}
262
263void LWIPRawUDPImpl::s_recv_fn(void *arg, struct udp_pcb *pcb, struct pbuf *p, const ip_addr_t *addr, u16_t port) {
264 auto *self = reinterpret_cast<LWIPRawUDPImpl *>(arg);
265 self->recv_fn_(p, addr, port);
266}
267
268// LWIP CALLBACK — runs from IRQ context on RP2040 (low-priority user IRQ).
269// No heap allocation allowed — malloc is not IRQ-safe (see #14687).
270// No LWIP_LOCK() needed — lwip core already holds the async_context lock.
271void LWIPRawUDPImpl::recv_fn_(struct pbuf *p, const ip_addr_t *addr, u16_t port) {
272 if (p == nullptr)
273 return;
274
275 // Check if queue is full
276 if (this->rx_count_ >= UDP_RX_QUEUE_SIZE) {
277 // Drop packet — queue full. Can't log from IRQ context, so count it
278 // (saturating) for consumers to surface via get_rx_dropped().
279 if (this->rx_dropped_ != UINT16_MAX)
280 this->rx_dropped_++;
281 pbuf_free(p);
282 return;
283 }
284
285 // Enqueue the packet
286 uint8_t write_idx = (this->rx_read_idx_ + this->rx_count_) & UDP_RX_MASK;
287 auto &slot = this->rx_queue_[write_idx];
288 slot.pb = p;
289 slot.src_addr = *addr;
290 slot.src_port = port;
291 this->rx_count_++;
292
294}
295
296// ---- UDP Factory functions ----
297
298static struct udp_pcb *new_udp_pcb(int domain) {
299#if LWIP_IPV6
300 return udp_new_ip_type(domain == AF_INET6 ? IPADDR_TYPE_ANY : IPADDR_TYPE_V4);
301#else
302 return udp_new();
303#endif
304}
305
306std::unique_ptr<UDPSendSocket> socket_udp_send(int domain, int protocol) {
307 (void) protocol; // Raw lwip UDP ignores protocol; kept for API compatibility
308 LWIP_LOCK();
309 auto *pcb = new_udp_pcb(domain);
310 if (pcb == nullptr) {
311 errno = ENOMEM;
312 return nullptr;
313 }
314 return make_unique<LWIPRawUDPSendImpl>((sa_family_t) domain, pcb);
315}
316
317std::unique_ptr<UDPSocket> socket_udp(int domain, int protocol) {
318 (void) protocol; // Raw lwip UDP ignores protocol; kept for API compatibility
319 LWIP_LOCK();
320 auto *pcb = new_udp_pcb(domain);
321 if (pcb == nullptr) {
322 errno = ENOMEM;
323 return nullptr;
324 }
325 // Ctor registers the recv callback under the lock held here
326 return make_unique<LWIPRawUDPImpl>((sa_family_t) domain, pcb);
327}
328
329#undef LWIP_LOCK
330
331} // namespace esphome::socket
332
333#endif // USE_SOCKET_IMPL_LWIP_TCP
UDP socket with receive support for LWIP raw API.
static constexpr uint8_t UDP_RX_MASK
LWIPRawUDPImpl(sa_family_t family, struct udp_pcb *pcb)
Caller (the factory) must hold the lwip lock; registers the recv callback.
ssize_t recvfrom(void *buf, size_t len, struct sockaddr *src_addr, socklen_t *addrlen)
Read the next queued packet and return the source address.
int close()
Close the socket, flushing any queued rx packets first.
ssize_t read(void *buf, size_t len)
Read the next queued packet, discarding source address info.
void recv_fn_(struct pbuf *p, const ip_addr_t *addr, u16_t port)
static void s_recv_fn(void *arg, struct udp_pcb *pcb, struct pbuf *p, const ip_addr_t *addr, u16_t port)
static constexpr uint8_t UDP_RX_QUEUE_SIZE
Ring buffer for received UDP packets.
std::array< UDPRxPacket, UDP_RX_QUEUE_SIZE > rx_queue_
Send-only UDP socket implementation for LWIP raw API.
int getsockopt(int level, int optname, void *optval, socklen_t *optlen)
int bind(const struct sockaddr *name, socklen_t addrlen)
int setsockopt(int level, int optname, const void *optval, socklen_t optlen)
int ip2sockaddr_(const ip_addr_t *ip, uint16_t port, struct sockaddr *name, socklen_t *addrlen)
Convert lwip ip_addr_t and port to sockaddr.
int close_internal_locked_()
Shared close logic — removes the udp pcb. Caller must hold LWIP_LOCK.
ssize_t sendto(const void *buf, size_t len, int flags, const struct sockaddr *dest_addr, socklen_t addrlen)
Send a UDP packet to the specified destination.
uint16_t flags
uint32_t socklen_t
Definition headers.h:109
uint8_t sa_family_t
Definition headers.h:69
__int64 ssize_t
Definition httplib.h:178
@ IPADDR_TYPE_V4
Definition ip_address.h:32
struct in_addr ip4_addr_t
Definition ip_address.h:23
int ret
mopeka_std_values val[3]
std::unique_ptr< UDPSendSocket > socket_udp_send(int domain, int protocol)
Create a send-only UDP socket (socket_udp_send), a UDP socket with receive support (socket_udp),...
bool sockaddr_to_lwip(const struct sockaddr *addr, socklen_t addrlen, ip_addr_t *ip, uint16_t *port)
Convert sockaddr to lwip ip_addr_t and host-order port.
int lwip_err_to_errno(err_t err)
errno for a failed lwip call
bool sockaddr_to_lwip_bind(sa_family_t family, const struct sockaddr *addr, socklen_t addrlen, ip_addr_t *ip, uint16_t *port)
sockaddr_to_lwip variant for bind: promotes AF_INET6 sockets to IPADDR_TYPE_ANY so they accept both I...
std::unique_ptr< UDPSocket > socket_udp(int domain, int protocol)
int lwip_ip_to_sockaddr(sa_family_t family, const ip_addr_t *ip, uint16_t port_host, struct sockaddr *name, socklen_t *addrlen)
Convert lwip ip_addr_t + host-order port to sockaddr, based on the socket's address family.
const void size_t len
Definition hal.h:68
void IRAM_ATTR wake_loop_any_context()
IRAM_ATTR entry point for ISR callers — defined in wake_esp8266.cpp.
Platform-specific main loop wake primitives.