DeterministicESPAsyncWebServer v6.27.1
Zero-allocation, bounded-execution async HTTP server for ESP32
Loading...
Searching...
No Matches
udp.cpp
Go to the documentation of this file.
1// Copyright (C) 2026 Douglas Quigg (dstroy0) <dquigg123@gmail.com>
2// SPDX-License-Identifier: AGPL-3.0-or-later
3
4/**
5 * @file udp.cpp
6 * @brief Layer 4 UDP datagram service - the only place lwIP UDP is touched.
7 */
8
10
11#include <string.h> // memcpy (both the lwIP and host builds)
12
13#if defined(ARDUINO)
14
15#include "lwip/pbuf.h"
16#include "lwip/priv/tcpip_priv.h" // tcpip_api_call - marshal raw udp_* onto tcpip_thread
17#include "lwip/udp.h"
18
19// A small fixed pool of bound UDP ports (e.g. SNMP :161 + captive DNS :53). No
20// heap: the pool and the shared receive scratch live in BSS.
22{
23 struct udp_pcb *pcb;
25 void *ctx;
26 bool used;
27};
28
29// All UDP transport state, owned by one instance (internal linkage): the listener table, the
30// shared single-datagram rx buffer, the shared outbound PCB, and the tcpip-thread reentrancy
31// flag. One named owner, unreachable from any other translation unit.
32//
33// in_tcpip_thread is true while a udp_recv trampoline (or a marshaled op) is running, i.e. while
34// we are already inside tcpip_thread. A handler replying from the trampoline then sends directly
35// instead of re-marshaling (which would deadlock on the tcpip mailbox) - the UDP mirror of
36// tcp.cpp's TransportCtx::in_tcpip_thread.
37struct UdpCtx
38{
40 uint8_t rx[DETWS_UDP_RX_BUF_SIZE]; // shared: lwIP delivers one datagram at a time
41 struct udp_pcb *out = nullptr; // one shared outbound PCB for det_udp_sendto()
42 volatile bool in_tcpip_thread = false;
43};
44static UdpCtx s_udp;
45
46// Concrete peer: lwIP source address/port plus the receiving PCB to reply on.
48{
49 const ip_addr_t *addr;
50 u16_t port;
51 struct udp_pcb *pcb;
52};
53
54// Raw send (alloc + copy + sendto + free). Only ever called in tcpip_thread.
55static bool udp_pbuf_send(struct udp_pcb *pcb, const ip_addr_t *addr, u16_t port, const uint8_t *data, size_t len)
56{
57 struct pbuf *p = pbuf_alloc(PBUF_TRANSPORT, (u16_t)len, PBUF_RAM);
58 if (!p)
59 return false;
60 memcpy(p->payload, data, len);
61 err_t e = udp_sendto(pcb, p, addr, port);
62 pbuf_free(p);
63 return e == ERR_OK;
64}
65
66// lwIP udp_recv trampoline: copy the (possibly chained) pbuf into a contiguous
67// scratch buffer and hand it to the registered handler. Runs in tcpip_thread, so a
68// reply the handler sends is already in-thread (flagged for the send helpers).
69static void udp_trampoline(void *arg, struct udp_pcb *pcb, struct pbuf *p, const ip_addr_t *addr, u16_t port)
70{
71 UdpListener *l = (UdpListener *)arg;
72 if (!p)
73 return;
74 u16_t n = (p->tot_len < sizeof(s_udp.rx)) ? p->tot_len : (u16_t)sizeof(s_udp.rx);
75 pbuf_copy_partial(p, s_udp.rx, n, 0);
76 pbuf_free(p);
77 if (l && l->handler)
78 {
79 DetUdpPeer peer = {addr, port, pcb};
80 bool prev = s_udp.in_tcpip_thread;
81 s_udp.in_tcpip_thread = true;
82 l->handler(s_udp.rx, n, &peer, l->ctx);
83 s_udp.in_tcpip_thread = prev;
84 }
85}
86
87// Raw lwIP UDP must run in tcpip_thread: with lwIP core-locking (arduino-esp32 3.x /
88// IDF 5.x) a udp_new/bind/recv/sendto from any other task asserts ("Required to lock
89// TCPIP core functionality"), and without it, it races the stack. det_udp_* therefore
90// marshal these ops via tcpip_api_call(), the same as the TCP transport.
91enum class DetUdpOp : uint8_t
92{
93 UDP_OP_LISTEN, // udp_new + bind + arm recv on s_udp.listeners[slot]
94 UDP_OP_SEND, // send to addr:port on an existing pcb
95 UDP_OP_SEND_OUT // send to addr:port on the shared lazy outbound pcb
96};
97
99{
100 struct tcpip_api_call_data base;
102 int slot; // LISTEN: index into s_udp.listeners
103 struct udp_pcb *pcb; // SEND: target pcb
104 ip_addr_t addr; // SEND / SEND_OUT: destination (by value - caller's may be transient)
105 u16_t port; // LISTEN: bind port; SEND / SEND_OUT: destination port
106 const uint8_t *data; // SEND / SEND_OUT
107 size_t len; // SEND / SEND_OUT
108 bool result;
109};
110
111// Runs in tcpip_thread via tcpip_api_call.
112static err_t udp_do(struct tcpip_api_call_data *c)
113{
114 DetUdpCall *k = (DetUdpCall *)c;
115 bool prev = s_udp.in_tcpip_thread;
116 s_udp.in_tcpip_thread = true;
117 k->result = false;
118 switch (k->op)
119 {
121 struct udp_pcb *pcb = udp_new();
122 if (pcb)
123 {
124 if (udp_bind(pcb, IP_ANY_TYPE, k->port) == ERR_OK)
125 {
126 s_udp.listeners[k->slot].pcb = pcb;
127 udp_recv(pcb, udp_trampoline, &s_udp.listeners[k->slot]);
128 k->result = true;
129 }
130 else
131 {
132 udp_remove(pcb);
133 }
134 }
135 break;
136 }
138 k->result = udp_pbuf_send(k->pcb, &k->addr, k->port, k->data, k->len);
139 break;
141 if (!s_udp.out)
142 s_udp.out = udp_new();
143 if (s_udp.out)
144 k->result = udp_pbuf_send(s_udp.out, &k->addr, k->port, k->data, k->len);
145 break;
146 }
147 s_udp.in_tcpip_thread = prev;
148 return ERR_OK;
149}
150
151bool det_udp_listen(uint16_t port, DetUdpHandler handler, void *ctx)
152{
153 for (int i = 0; i < DETWS_MAX_UDP_LISTENERS; i++)
154 {
155 if (s_udp.listeners[i].used)
156 continue;
157 // The trampoline reads handler/ctx once recv is armed, so set them first.
158 s_udp.listeners[i].handler = handler;
159 s_udp.listeners[i].ctx = ctx;
160 s_udp.listeners[i].pcb = nullptr;
161 DetUdpCall k;
162 memset(&k, 0, sizeof(k));
164 k.slot = i;
165 k.port = port;
166 tcpip_api_call(udp_do, &k.base); // always called off tcpip_thread (service begin())
167 if (!k.result)
168 {
169 s_udp.listeners[i].handler = nullptr;
170 return false;
171 }
172 s_udp.listeners[i].used = true;
173 return true;
174 }
175 return false; // pool exhausted
176}
177
178bool det_udp_send(struct DetUdpPeer *peer, const uint8_t *data, size_t len)
179{
180 if (!peer || !peer->pcb || !peer->addr || !data || len == 0)
181 return false;
182 if (s_udp.in_tcpip_thread) // replying from a handler (already in tcpip_thread)
183 return udp_pbuf_send(peer->pcb, peer->addr, peer->port, data, len);
184 DetUdpCall k;
185 memset(&k, 0, sizeof(k));
187 k.pcb = peer->pcb;
188 k.addr = *peer->addr;
189 k.port = peer->port;
190 k.data = data;
191 k.len = len;
192 tcpip_api_call(udp_do, &k.base);
193 return k.result;
194}
195
196bool det_udp_sendto(const char *dst_ip, uint16_t dst_port, const uint8_t *data, size_t len)
197{
198 if (!dst_ip || !data || len == 0)
199 return false;
200 ip_addr_t dst;
201 if (!ipaddr_aton(dst_ip, &dst))
202 return false;
203 if (s_udp.in_tcpip_thread)
204 {
205 if (!s_udp.out)
206 {
207 s_udp.out = udp_new();
208 if (!s_udp.out)
209 return false;
210 }
211 return udp_pbuf_send(s_udp.out, &dst, dst_port, data, len);
212 }
213 DetUdpCall k;
214 memset(&k, 0, sizeof(k));
216 k.addr = dst;
217 k.port = dst_port;
218 k.data = data;
219 k.len = len;
220 tcpip_api_call(udp_do, &k.base);
221 return k.result;
222}
223
224bool det_udp_peer_addr(const struct DetUdpPeer *peer, char *ip_out, size_t ip_cap, uint16_t *port_out)
225{
226 if (!peer || !peer->addr || !ip_out || ip_cap < 8)
227 return false;
228 ipaddr_ntoa_r(peer->addr, ip_out, (int)ip_cap);
229 if (port_out)
230 *port_out = peer->port;
231 return true;
232}
233
234bool det_udp_listener_sendto(uint16_t listen_port, const char *dst_ip, uint16_t dst_port, const uint8_t *data,
235 size_t len)
236{
237 if (!dst_ip || !data || len == 0)
238 return false;
239 ip_addr_t dst;
240 if (!ipaddr_aton(dst_ip, &dst))
241 return false;
242 struct udp_pcb *pcb = nullptr;
243 for (int i = 0; i < DETWS_MAX_UDP_LISTENERS; i++)
244 {
245 if (s_udp.listeners[i].used && s_udp.listeners[i].pcb && s_udp.listeners[i].pcb->local_port == listen_port)
246 {
247 pcb = s_udp.listeners[i].pcb;
248 break;
249 }
250 }
251 if (!pcb)
252 return false;
253 if (s_udp.in_tcpip_thread)
254 return udp_pbuf_send(pcb, &dst, dst_port, data, len);
255 DetUdpCall k;
256 memset(&k, 0, sizeof(k));
258 k.pcb = pcb;
259 k.addr = dst;
260 k.port = dst_port;
261 k.data = data;
262 k.len = len;
263 tcpip_api_call(udp_do, &k.base);
264 return k.result;
265}
266
267#else // host build: no lwIP. A test-injectable UDP mock keeps UDP-using services host-testable.
268
269// Concrete host peer: the source address/port of an injected datagram, which a service's handler
270// reads back via det_udp_peer_addr() to reply (or, for CoAP Observe, to key a registration).
271struct DetUdpPeer
272{
273 char ip[16];
274 uint16_t port;
275};
276
277// Host UDP mock state, owned by one instance (internal linkage): the bound listeners, a capture of
278// the last datagram sent (shared by det_udp_send/sendto/listener_sendto), and the listener_sendto
279// result knob. A test drives the receive path with det_udp_inject() and reads replies via
280// det_udp_captured(). One named owner, unreachable from any other translation unit.
281namespace
282{
283struct HostUdpListener
284{
285 uint16_t port;
286 DetUdpHandler handler;
287 void *ctx;
288 bool used;
289};
290struct HostUdpCtx
291{
292 HostUdpListener lst[DETWS_MAX_UDP_LISTENERS];
293 bool cap_on;
294 uint8_t cap_buf[2048];
295 size_t cap_len;
296 bool listener_sendto_ok;
297};
298HostUdpCtx s_udp = {{}, false, {}, 0, true};
299
300// Capture one outbound datagram if capture is enabled; return whether it was captured.
301bool host_capture(const uint8_t *data, size_t len)
302{
303 if (s_udp.cap_on && data && len && len <= sizeof(s_udp.cap_buf))
304 {
305 memcpy(s_udp.cap_buf, data, len);
306 s_udp.cap_len = len;
307 return true;
308 }
309 return false;
310}
311} // namespace
312
313bool det_udp_listen(uint16_t port, DetUdpHandler handler, void *ctx)
314{
315 // Rebind an existing port, else take a free slot, else evict slot 0 (host tests only).
316 HostUdpListener *slot = nullptr;
317 for (int i = 0; i < DETWS_MAX_UDP_LISTENERS; i++)
318 if (s_udp.lst[i].used && s_udp.lst[i].port == port)
319 slot = &s_udp.lst[i];
320 for (int i = 0; i < DETWS_MAX_UDP_LISTENERS && !slot; i++)
321 if (!s_udp.lst[i].used)
322 slot = &s_udp.lst[i];
323 if (!slot)
324 slot = &s_udp.lst[0];
325 slot->port = port;
326 slot->handler = handler;
327 slot->ctx = ctx;
328 slot->used = true;
329 return true;
330}
331
332void det_udp_inject(uint16_t listen_port, const char *src_ip, uint16_t src_port, const uint8_t *data, size_t len)
333{
334 for (int i = 0; i < DETWS_MAX_UDP_LISTENERS; i++)
335 if (s_udp.lst[i].used && s_udp.lst[i].port == listen_port && s_udp.lst[i].handler)
336 {
337 DetUdpPeer peer;
338 strncpy(peer.ip, src_ip ? src_ip : "", sizeof(peer.ip) - 1);
339 peer.ip[sizeof(peer.ip) - 1] = '\0';
340 peer.port = src_port;
341 s_udp.lst[i].handler(data, len, &peer, s_udp.lst[i].ctx);
342 return;
343 }
344}
345
346void det_udp_set_listener_sendto_result(bool ok)
347{
348 s_udp.listener_sendto_ok = ok;
349}
350
351void det_udp_reset_listeners()
352{
353 for (int i = 0; i < DETWS_MAX_UDP_LISTENERS; i++)
354 s_udp.lst[i] = {};
355 s_udp.listener_sendto_ok = true;
356}
357
358bool det_udp_send(struct DetUdpPeer *peer, const uint8_t *data, size_t len)
359{
360 (void)peer;
361 return host_capture(data, len);
362}
363
364void det_udp_capture_enable()
365{
366 s_udp.cap_on = true;
367 s_udp.cap_len = 0;
368}
369void det_udp_capture_reset()
370{
371 s_udp.cap_len = 0;
372}
373const uint8_t *det_udp_captured()
374{
375 return s_udp.cap_len ? s_udp.cap_buf : nullptr;
376}
377size_t det_udp_captured_len()
378{
379 return s_udp.cap_len;
380}
381
382bool det_udp_sendto(const char *dst_ip, uint16_t dst_port, const uint8_t *data, size_t len)
383{
384 (void)dst_ip;
385 (void)dst_port;
386 return host_capture(data, len); // captured "send" succeeds so the caller's success path runs
387}
388
389bool det_udp_peer_addr(const struct DetUdpPeer *peer, char *ip_out, size_t ip_cap, uint16_t *port_out)
390{
391 if (!peer)
392 return false;
393 if (ip_out && ip_cap)
394 {
395 strncpy(ip_out, peer->ip, ip_cap - 1);
396 ip_out[ip_cap - 1] = '\0';
397 }
398 if (port_out)
399 *port_out = peer->port;
400 return true;
401}
402
403bool det_udp_listener_sendto(uint16_t listen_port, const char *dst_ip, uint16_t dst_port, const uint8_t *data,
404 size_t len)
405{
406 (void)listen_port;
407 (void)dst_ip;
408 (void)dst_port;
409 if (!s_udp.listener_sendto_ok)
410 return false; // test knob: model an unreachable peer (drops the observer)
411 host_capture(data, len);
412 return true;
413}
414
415#endif // ARDUINO
#define DETWS_UDP_RX_BUF_SIZE
Shared receive-scratch size for the transport-layer UDP service.
#define DETWS_MAX_UDP_LISTENERS
Maximum simultaneously bound UDP ports (transport-layer UDP service).
DetUdpOp op
Definition udp.cpp:101
struct udp_pcb * pcb
Definition udp.cpp:103
size_t len
Definition udp.cpp:107
int slot
Definition udp.cpp:102
struct tcpip_api_call_data base
Definition udp.cpp:100
bool result
Definition udp.cpp:108
u16_t port
Definition udp.cpp:105
ip_addr_t addr
Definition udp.cpp:104
const uint8_t * data
Definition udp.cpp:106
struct udp_pcb * pcb
Definition udp.cpp:51
const ip_addr_t * addr
Definition udp.cpp:49
u16_t port
Definition udp.cpp:50
Definition udp.cpp:38
struct udp_pcb * out
Definition udp.cpp:41
uint8_t rx[DETWS_UDP_RX_BUF_SIZE]
Definition udp.cpp:40
UdpListener listeners[DETWS_MAX_UDP_LISTENERS]
Definition udp.cpp:39
volatile bool in_tcpip_thread
Definition udp.cpp:42
struct udp_pcb * pcb
Definition udp.cpp:23
DetUdpHandler handler
Definition udp.cpp:24
bool used
Definition udp.cpp:26
void * ctx
Definition udp.cpp:25
bool det_udp_peer_addr(const struct DetUdpPeer *peer, char *ip_out, size_t ip_cap, uint16_t *port_out)
Copy a received peer's source IPv4 address (dotted-quad) and port out.
Definition udp.cpp:224
DetUdpOp
Definition udp.cpp:92
@ UDP_OP_SEND_OUT
bool det_udp_sendto(const char *dst_ip, uint16_t dst_port, const uint8_t *data, size_t len)
Send a UDP datagram to an arbitrary destination (outbound client).
Definition udp.cpp:196
bool det_udp_send(struct DetUdpPeer *peer, const uint8_t *data, size_t len)
Send a datagram back to the peer captured during the handler call.
Definition udp.cpp:178
bool det_udp_listen(uint16_t port, DetUdpHandler handler, void *ctx)
Bind a UDP port and route incoming datagrams to handler.
Definition udp.cpp:151
bool det_udp_listener_sendto(uint16_t listen_port, const char *dst_ip, uint16_t dst_port, const uint8_t *data, size_t len)
Send a datagram from the listener bound on listen_port to an address.
Definition udp.cpp:234
Layer 4 (Transport) - connectionless UDP datagram service.
void(* DetUdpHandler)(const uint8_t *data, size_t len, struct DetUdpPeer *peer, void *ctx)
Datagram handler invoked for each received UDP packet.
Definition udp.h:49