15#if PC_ENABLE_COAP_OBSERVE || PC_COAP_DEDUP_ENTRIES > 0
23 static constexpr uint8_t COAP_OPT_OBSERVE = 6;
24 static constexpr uint8_t COAP_OPT_URI_PATH = 11;
25 static constexpr uint8_t COAP_OPT_CONTENT_FORMAT = 12;
26 static constexpr uint8_t COAP_OPT_URI_QUERY = 15;
27 static constexpr uint8_t COAP_OPT_BLOCK2 = 23;
28 static constexpr uint8_t COAP_OPT_BLOCK1 = 27;
31static const uint8_t COAP_PAYLOAD_MARKER = 0xFF;
32static const size_t COAP_MAX_TOKEN = 8;
45#if PC_COAP_DEDUP_ENTRIES > 0
63#if PC_ENABLE_COAP_OBSERVE
90#if PC_COAP_DEDUP_ENTRIES > 0
94#if PC_ENABLE_COAP_OBSERVE
98 int last_observe = -1;
99 uint8_t last_method = 0;
100 uint8_t last_token[8];
101 uint8_t last_tkl = 0;
104#if PC_ENABLE_COAP_BLOCK
112static CoapCtx s_coap;
114void pc_coap_server_reset()
116 s_coap.res_count = 0;
117 memset(s_coap.res, 0,
sizeof(s_coap.res));
118#if PC_ENABLE_COAP_BLOCK
122#if PC_COAP_DEDUP_ENTRIES > 0
125 s_coap.dedup[i].valid =
false;
130bool pc_coap_server_add_resource(
const char *path, uint8_t methods, CoapHandler handler)
136 s_coap.res[s_coap.res_count].path = path;
137 s_coap.res[s_coap.res_count].methods = methods;
138 s_coap.res[s_coap.res_count].handler = handler;
148static uint32_t opt_uint(
const uint8_t *v,
size_t n)
151 for (
size_t i = 0; i < n; i++)
161static bool seg_append(
char *buf,
size_t cap,
size_t *len,
char sep,
const uint8_t *seg,
size_t seglen)
163 size_t need = (sep ? 1u : 0u) + seglen + 1u;
164 if (*len + need > cap)
172 memcpy(buf + *len, seg, seglen);
178static const CoapResource *find_resource(
const CoapCtx &c,
const char *path)
180 for (
size_t i = 0; i < c.res_count; i++)
182 if (strcmp(c.res[i].path, path) == 0)
192static size_t emit_header(uint8_t *out,
size_t cap, CoapType type, uint8_t code, uint16_t mid,
const uint8_t *token,
195 if (cap < (
size_t)(4 + tkl))
199 out[0] = (uint8_t)((1 << 6) | ((uint8_t)type << 4) | tkl);
201 out[2] = (uint8_t)(mid >> 8);
202 out[3] = (uint8_t)(mid & 0xFF);
205 memcpy(out + 4, token, tkl);
207 return (
size_t)(4 + tkl);
212static uint8_t enc_uint_minimal(uint32_t v, uint8_t out[3])
217 out[k++] = (uint8_t)(v >> 16);
221 out[k++] = (uint8_t)(v >> 8);
225 out[k++] = (uint8_t)v;
235static size_t append_opt(uint8_t *resp,
size_t cap,
size_t n, uint32_t *last_opt, uint32_t opt_num,
const uint8_t *val,
238 uint32_t delta = opt_num - *last_opt;
239 uint8_t dn = (uint8_t)(delta < 13 ? delta : 13);
240 bool ext = delta >= 13;
241 if (n + 1u + (ext ? 1u : 0u) + vlen > cap)
245 resp[n++] = (uint8_t)((dn << 4) | vlen);
248 resp[n++] = (uint8_t)(delta - 13);
250 for (uint8_t i = 0; i < vlen; i++)
264static size_t emit_options_payload(uint8_t *resp,
size_t cap,
size_t n, uint8_t code, int32_t observe_seq,
265 CoapContentFormat content_format, int32_t block2_val, int32_t block1_val,
266 const uint8_t *payload,
size_t payload_len)
268 uint32_t last_opt = 0;
271 if (observe_seq >= 0 && (code >> 5) == 2)
273 n = append_opt(resp, cap, n, &last_opt, CoapOpt::COAP_OPT_OBSERVE, v,
274 enc_uint_minimal((uint32_t)observe_seq & 0xFFFFFF, v));
277 if (content_format != CoapContentFormat::COAP_CF_NONE)
279 n = append_opt(resp, cap, n, &last_opt, CoapOpt::COAP_OPT_CONTENT_FORMAT, v,
280 enc_uint_minimal((uint16_t)content_format, v));
283#if PC_ENABLE_COAP_BLOCK
286 n = append_opt(resp, cap, n, &last_opt, CoapOpt::COAP_OPT_BLOCK2, v, enc_uint_minimal((uint32_t)block2_val, v));
290 n = append_opt(resp, cap, n, &last_opt, CoapOpt::COAP_OPT_BLOCK1, v, enc_uint_minimal((uint32_t)block1_val, v));
299 if (n + 1 + payload_len > cap)
303 resp[n++] = COAP_PAYLOAD_MARKER;
304 memcpy(resp + n, payload, payload_len);
314size_t pc_coap_server_process_ex(
const uint8_t *req,
size_t req_len, uint8_t *resp,
size_t pc_resp_cap,
322 uint8_t ver = (req[0] >> 6) & 0x03;
323 CoapType type = (CoapType)((req[0] >> 4) & 0x03);
324 uint8_t tkl = req[0] & 0x0F;
325 uint8_t code = req[1];
326 uint16_t mid = (uint16_t)((req[2] << 8) | req[3]);
330 if (type != CoapType::COAP_TYPE_CON && type != CoapType::COAP_TYPE_NON)
334 CoapType rsp_type = (type == CoapType::COAP_TYPE_CON) ? CoapType::COAP_TYPE_ACK : CoapType::COAP_TYPE_NON;
338 if (ver != 1 || tkl > COAP_MAX_TOKEN)
340 return (type == CoapType::COAP_TYPE_CON)
341 ? emit_header(resp, pc_resp_cap, CoapType::COAP_TYPE_RST, 0, mid,
nullptr, 0)
345 const uint8_t *p = req + 4;
346 const uint8_t *end = req + req_len;
349 return (type == CoapType::COAP_TYPE_CON)
350 ? emit_header(resp, pc_resp_cap, CoapType::COAP_TYPE_RST, 0, mid,
nullptr, 0)
353 const uint8_t *token = p;
359 return (type == CoapType::COAP_TYPE_CON)
360 ? emit_header(resp, pc_resp_cap, CoapType::COAP_TYPE_RST, 0, mid,
nullptr, 0)
364#if PC_ENABLE_COAP_OBSERVE
365 s_coap.last_observe = -1;
366 s_coap.last_method = code;
367 s_coap.last_tkl = tkl;
370 memcpy(s_coap.last_token, token, tkl);
376 size_t query_len = 0;
377 s_coap.path[0] =
'\0';
378 s_coap.query[0] =
'\0';
379 CoapContentFormat req_cf = CoapContentFormat::COAP_CF_NONE;
380 const uint8_t *payload =
nullptr;
381 size_t payload_len = 0;
382 uint32_t opt_num = 0;
384 bool bad_option =
false;
385#if PC_ENABLE_COAP_BLOCK
386 int32_t req_block1 = -1;
387 int32_t req_block2 = -1;
393 if (b == COAP_PAYLOAD_MARKER)
396 payload_len = (size_t)(end - p);
399 uint32_t delta = b >> 4;
400 uint32_t olen = b & 0x0F;
401 if (delta == 15 || olen == 15)
414 delta = (uint32_t)(*p++) + 13;
416 else if (delta == 14)
423 delta = (uint32_t)((p[0] << 8) | p[1]) + 269;
433 olen = (uint32_t)(*p++) + 13;
442 olen = (uint32_t)((p[0] << 8) | p[1]) + 269;
451 const uint8_t *val = p;
456 case CoapOpt::COAP_OPT_URI_PATH:
457 if (!seg_append(s_coap.path,
sizeof(s_coap.path), &path_len,
'/', val, olen))
462 case CoapOpt::COAP_OPT_URI_QUERY:
463 if (!seg_append(s_coap.query,
sizeof(s_coap.query), &query_len, query_len ?
'&' :
'\0', val, olen))
468 case CoapOpt::COAP_OPT_CONTENT_FORMAT:
469 req_cf = (CoapContentFormat)opt_uint(val, olen);
471#if PC_ENABLE_COAP_OBSERVE
472 case CoapOpt::COAP_OPT_OBSERVE:
473 s_coap.last_observe = (int)opt_uint(val, olen);
476#if PC_ENABLE_COAP_BLOCK
477 case CoapOpt::COAP_OPT_BLOCK1:
484 req_block1 = (int32_t)opt_uint(val, olen);
487 case CoapOpt::COAP_OPT_BLOCK2:
494 req_block2 = (int32_t)opt_uint(val, olen);
517 return emit_header(resp, pc_resp_cap, rsp_type, (uint8_t)CoapResponseCode::COAP_RSP_BAD_REQUEST, mid, token,
522 return emit_header(resp, pc_resp_cap, rsp_type, (uint8_t)CoapResponseCode::COAP_RSP_BAD_OPTION, mid, token,
528 s_coap.path[0] =
'/';
529 s_coap.path[1] =
'\0';
537 if ((code >> 5) != 0 || code < (uint8_t)CoapMethod::COAP_GET || code > (uint8_t)CoapMethod::COAP_DELETE)
539 return emit_header(resp, pc_resp_cap, rsp_type, (uint8_t)CoapResponseCode::COAP_RSP_METHOD_NOT_ALLOWED, mid,
547 cresp.code = (uint8_t)CoapResponseCode::COAP_RSP_CONTENT;
548 cresp.content_format = CoapContentFormat::COAP_CF_NONE;
549 cresp.payload = s_coap.pl;
550 cresp.payload_cap =
sizeof(s_coap.pl);
551 cresp.payload_len = 0;
552 int32_t block1_echo = -1;
556 if (strcmp(s_coap.path,
"/.well-known/core") == 0)
558 if (code != (uint8_t)CoapMethod::COAP_GET)
560 return emit_header(resp, pc_resp_cap, rsp_type, (uint8_t)CoapResponseCode::COAP_RSP_METHOD_NOT_ALLOWED, mid,
564 for (
size_t i = 0; i < s_coap.res_count; i++)
566 const char *rpath = s_coap.res[i].path;
567 size_t plen = strnlen(rpath,
sizeof(s_coap.pl));
568 size_t need = (pl ? 1u : 0u) + 2u + plen;
569 if (pl + need >
sizeof(s_coap.pl))
575 s_coap.pl[pl++] =
',';
577 s_coap.pl[pl++] =
'<';
578 memcpy(s_coap.pl + pl, rpath, plen);
580 s_coap.pl[pl++] =
'>';
582 cresp.content_format = CoapContentFormat::COAP_CF_LINK;
583 cresp.payload_len = pl;
587 const CoapResource *r = find_resource(s_coap, s_coap.path);
590 return emit_header(resp, pc_resp_cap, rsp_type, (uint8_t)CoapResponseCode::COAP_RSP_NOT_FOUND, mid, token,
593 if (!(r->methods & (1u << code)))
595 return emit_header(resp, pc_resp_cap, rsp_type, (uint8_t)CoapResponseCode::COAP_RSP_METHOD_NOT_ALLOWED, mid,
599 const uint8_t *eff_payload = payload;
600 size_t eff_payload_len = payload_len;
602#if PC_ENABLE_COAP_BLOCK
604 if (req_block1 >= 0 && (code == (uint8_t)CoapMethod::COAP_POST || code == (uint8_t)CoapMethod::COAP_PUT))
606 uint32_t b = (uint32_t)req_block1;
607 uint32_t num = b >> 4;
608 uint8_t more = (uint8_t)((b >> 3) & 1);
609 uint8_t szx = (uint8_t)(b & 7);
612 return emit_header(resp, pc_resp_cap, rsp_type, (uint8_t)CoapResponseCode::COAP_RSP_BAD_OPTION, mid,
615 uint32_t bsize = 1u << (szx + 4);
623 if (szx != s_coap.b1_szx || (
size_t)num * bsize != s_coap.b1_len)
626 return emit_header(resp, pc_resp_cap, rsp_type, (uint8_t)CoapResponseCode::COAP_RSP_REQUEST_INCOMPLETE,
629 if (s_coap.b1_len + payload_len >
sizeof(s_coap.b1))
632 return emit_header(resp, pc_resp_cap, rsp_type, (uint8_t)CoapResponseCode::COAP_RSP_REQUEST_TOO_LARGE,
637 memcpy(s_coap.b1 + s_coap.b1_len, payload, payload_len);
639 s_coap.b1_len += payload_len;
645 size_t cn = emit_header(resp, pc_resp_cap, rsp_type, (uint8_t)CoapResponseCode::COAP_RSP_CONTINUE, mid,
651 return emit_options_payload(resp, pc_resp_cap, cn, (uint8_t)CoapResponseCode::COAP_RSP_CONTINUE, -1,
652 CoapContentFormat::COAP_CF_NONE, -1,
653 (int32_t)((num << 4) | (1u << 3) | szx),
nullptr, 0);
656 eff_payload = s_coap.b1;
657 eff_payload_len = s_coap.b1_len;
658 block1_echo = (int32_t)((num << 4) | szx);
663 creq.method = (CoapMethod)code;
664 creq.path = s_coap.path;
665 creq.query = s_coap.query;
666 creq.payload = eff_payload;
667 creq.payload_len = eff_payload_len;
668 creq.content_format = req_cf;
670 r->handler(&creq, &cresp);
671 if (cresp.payload_len >
sizeof(s_coap.pl))
673 cresp.payload_len =
sizeof(s_coap.pl);
677 int32_t block2_echo = -1;
679#if PC_ENABLE_COAP_BLOCK
680 if (block1_echo >= 0)
687 if ((cresp.code >> 5) == 2)
691 bool block_wise =
false;
694 uint32_t b = (uint32_t)req_block2;
696 szx = (uint8_t)(b & 7);
699 return emit_header(resp, pc_resp_cap, rsp_type, (uint8_t)CoapResponseCode::COAP_RSP_BAD_OPTION, mid,
708 else if (cresp.payload_len > (
size_t)(1u << (szx + 4)))
714 uint32_t bsize = 1u << (szx + 4);
715 size_t off = (size_t)num * bsize;
717 if (off > cresp.payload_len || (off == cresp.payload_len && num > 0))
719 return emit_header(resp, pc_resp_cap, rsp_type, (uint8_t)CoapResponseCode::COAP_RSP_BAD_REQUEST, mid,
722 size_t this_len = cresp.payload_len - off;
724 if (this_len > bsize)
729 block2_echo = (int32_t)((num << 4) | ((uint32_t)more << 3) | szx);
730 cresp.payload += off;
731 cresp.payload_len = this_len;
737 size_t n = emit_header(resp, pc_resp_cap, rsp_type, cresp.code, mid, token, tkl);
742 return emit_options_payload(resp, pc_resp_cap, n, cresp.code, observe_seq, cresp.content_format, block2_echo,
743 block1_echo, cresp.payload, cresp.payload_len);
746size_t pc_coap_server_process(
const uint8_t *req,
size_t req_len, uint8_t *resp,
size_t pc_resp_cap)
748 return pc_coap_server_process_ex(req, req_len, resp, pc_resp_cap, -1);
751#if PC_COAP_DEDUP_ENTRIES > 0
756bool pc_coap_dedup_lookup(
const char *src_ip, uint16_t src_port, uint16_t mid,
const uint8_t **out,
size_t *out_len)
765 const CoapDedupEntry &e = s_coap.dedup[i];
767 strncmp(e.ip, src_ip,
sizeof(e.ip)) == 0)
783void pc_coap_dedup_store(
const char *src_ip, uint16_t src_port, uint16_t mid,
const uint8_t *resp,
size_t len)
795 const CoapDedupEntry &e = s_coap.dedup[i];
796 if (e.valid && e.mid == mid && e.port == src_port && strncmp(e.ip, src_ip,
sizeof(e.ip)) == 0)
806 uint32_t age = now - e.stamp_ms;
813 CoapDedupEntry &e = s_coap.dedup[victim];
814 size_t iplen = strnlen(src_ip,
sizeof(e.ip) - 1);
815 memcpy(e.ip, src_ip, iplen);
820 e.len = (uint16_t)len;
821 memcpy(e.resp, resp, len);
830#if PC_COAP_DEDUP_ENTRIES > 0
833static bool coap_dedup_replay(
const uint8_t *data,
size_t len,
const struct pc_udp_peer *peer,
const char *ip,
834 uint16_t port,
bool have_peer)
836 if (!have_peer || len < 4 || ((data[0] >> 4) & 0x03) != (uint8_t)CoapType::COAP_TYPE_CON)
840 uint16_t mid = (uint16_t)((data[2] << 8) | data[3]);
841 const uint8_t *cached =
nullptr;
843 if (!pc_coap_dedup_lookup(ip, port, mid, &cached, &clen))
852static void coap_dedup_remember(
const uint8_t *data,
size_t len,
const char *ip, uint16_t port,
bool have_peer,
853 const uint8_t *resp,
size_t resp_len)
855 if (!have_peer || len < 4 || ((data[0] >> 4) & 0x03) != (uint8_t)CoapType::COAP_TYPE_CON)
859 uint16_t mid = (uint16_t)((data[2] << 8) | data[3]);
860 pc_coap_dedup_store(ip, port, mid, resp, resp_len);
864#if PC_ENABLE_COAP_OBSERVE
869static int find_resource_index(
const CoapCtx &c,
const char *path)
871 for (
size_t i = 0; i < c.res_count; i++)
873 if (strcmp(c.res[i].path, path) == 0)
881static bool same_token(
const CoapObserver *o,
const uint8_t *token, uint8_t tkl)
883 return o->tkl == tkl && (tkl == 0 || memcmp(o->token, token, tkl) == 0);
887static int obs_register(
const char *ip, uint16_t port,
const uint8_t *token, uint8_t tkl,
int res_idx)
891 if (s_coap.obs[i].active && s_coap.obs[i].res_idx == res_idx && s_coap.obs[i].port == port &&
892 same_token(&s_coap.obs[i], token, tkl) && strcmp(s_coap.obs[i].ip, ip) == 0)
899 if (!s_coap.obs[i].active)
901 s_coap.obs[i].active =
true;
902 strncpy(s_coap.obs[i].ip, ip,
sizeof(s_coap.obs[i].ip) - 1);
903 s_coap.obs[i].ip[
sizeof(s_coap.obs[i].ip) - 1] =
'\0';
904 s_coap.obs[i].port = port;
905 s_coap.obs[i].tkl = tkl;
908 memcpy(s_coap.obs[i].token, token, tkl);
910 s_coap.obs[i].res_idx = res_idx;
911 s_coap.obs[i].seq = 1;
920static void obs_remove(
const char *ip, uint16_t port,
const uint8_t *token, uint8_t tkl)
924 if (s_coap.obs[i].active && s_coap.obs[i].port == port && strcmp(s_coap.obs[i].ip, ip) == 0 &&
925 (token ==
nullptr || same_token(&s_coap.obs[i], token, tkl)))
927 s_coap.obs[i].active =
false;
932void pc_coap_notify(
const char *path)
934 int ridx = find_resource_index(s_coap, path);
941 if (!s_coap.obs[i].active || s_coap.obs[i].res_idx != ridx)
947 creq.method = CoapMethod::COAP_GET;
948 creq.path = s_coap.res[ridx].path;
950 creq.payload =
nullptr;
951 creq.payload_len = 0;
952 creq.content_format = CoapContentFormat::COAP_CF_NONE;
954 cresp.code = (uint8_t)CoapResponseCode::COAP_RSP_CONTENT;
955 cresp.content_format = CoapContentFormat::COAP_CF_NONE;
956 cresp.payload = s_coap.pl;
957 cresp.payload_cap =
sizeof(s_coap.pl);
958 cresp.payload_len = 0;
959 s_coap.res[ridx].handler(&creq, &cresp);
960 if (cresp.payload_len >
sizeof(s_coap.pl))
962 cresp.payload_len =
sizeof(s_coap.pl);
967 s_coap.obs[i].seq = (s_coap.obs[i].seq + 1) & 0xFFFFFF;
968 size_t n = emit_header(s_coap.tx,
sizeof(s_coap.tx), CoapType::COAP_TYPE_NON, cresp.code, mid,
969 s_coap.obs[i].token, s_coap.obs[i].tkl);
975 n = emit_options_payload(s_coap.tx,
sizeof(s_coap.tx), n, cresp.code, (int32_t)s_coap.obs[i].seq,
976 cresp.content_format, -1, -1, cresp.payload, cresp.payload_len);
980 s_coap.obs[i].active =
false;
986static void coap_udp_handler(
const uint8_t *data,
size_t len,
const struct pc_udp_peer *peer,
void *ctx)
994 if (len >= 1 && ((data[0] >> 4) & 0x03) == (uint8_t)CoapType::COAP_TYPE_RST)
998 obs_remove(ip, pport,
nullptr, 0);
1003#if PC_COAP_DEDUP_ENTRIES > 0
1005 if (coap_dedup_replay(data, len, peer, ip, pport, have_peer))
1011 size_t rn = pc_coap_server_process_ex(data, len, s_coap.tx,
sizeof(s_coap.tx), -1);
1019 if (s_coap.last_method == (uint8_t)CoapMethod::COAP_GET && s_coap.last_observe == 0 && have_peer)
1021 int ridx = find_resource_index(s_coap, s_coap.path);
1024 int slot = obs_register(ip, pport, s_coap.last_token, s_coap.last_tkl, ridx);
1029 pc_coap_server_process_ex(data, len, s_coap.tx,
sizeof(s_coap.tx), (int32_t)s_coap.obs[slot].seq);
1040 else if (s_coap.last_observe == 1 && have_peer)
1042 obs_remove(ip, pport, s_coap.last_token, s_coap.last_tkl);
1045#if PC_COAP_DEDUP_ENTRIES > 0
1046 coap_dedup_remember(data, len, ip, pport, have_peer, s_coap.tx, rn);
1051void pc_coap_server_begin(uint16_t port)
1056 s_coap.obs[i].active =
false;
1063static void coap_udp_handler(
const uint8_t *data,
size_t len,
const struct pc_udp_peer *peer,
void *ctx)
1066#if PC_COAP_DEDUP_ENTRIES > 0
1070 if (coap_dedup_replay(data, len, peer, ip, pport, have_peer))
1075 size_t rn = pc_coap_server_process(data, len, s_coap.tx,
sizeof(s_coap.tx));
1078#if PC_COAP_DEDUP_ENTRIES > 0
1079 coap_dedup_remember(data, len, ip, pport, have_peer, s_coap.tx, rn);
1085void pc_coap_server_begin(uint16_t port)
Pluggable monotonic clock for all library timing.
uint32_t pc_millis(void)
The library's monotonic time at 1000 Hz (milliseconds).
Zero-heap CoAP server (RFC 7252): message codec + a fixed resource table.
#define PC_COAP_DEDUP_RESP_MAX
Largest cached response the dedup cache retains per entry; a bigger response is not cached (a retrans...
#define PC_COAP_MAX_RESOURCES
Maximum registered CoAP resources (the server's fixed routing table).
#define PC_COAP_BLOCK_SZX_MAX
Largest block-size exponent (SZX) the server will use: block size = 2^(SZX+4) bytes,...
#define PC_COAP_MSG_BUF_SIZE
Static response-datagram buffer for the CoAP UDP server.
#define PC_COAP_DEDUP_ENTRIES
CoAP message de-duplication cache size (RFC 7252 sec 4.5). A Confirmable request the server has alrea...
#define PC_COAP_BLOCK1_MAX
Reassembly buffer for a block-wise (Block1) request upload, in bytes.
#define PC_COAP_OBSERVE_PORT
Default UDP port the CoAP observe transport notifies from (IANA well-known 5683).
#define PC_COAP_MAX_PATH
Maximum reconstructed Uri-Path length, including separators and the leading '/'.
#define PC_COAP_MAX_PAYLOAD
Maximum CoAP request/response payload in bytes.
#define PC_COAP_MAX_OBSERVERS
Maximum simultaneous CoAP observers (one slot per observed resource per client).
#define PC_COAP_MAX_QUERY
Maximum reconstructed Uri-Query length (segments joined by '&').
#define PC_COAP_DEDUP_LIFETIME_MS
How long (ms) a dedup entry stays fresh - RFC 7252 EXCHANGE_LIFETIME (~247 s) by default,...
bool pc_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.
bool pc_udp_listen(uint16_t port, pc_udp_handler handler, void *ctx)
Bind a UDP port and route incoming datagrams to handler.
bool pc_udp_send(const struct pc_udp_peer *peer, const uint8_t *data, size_t len)
Send a datagram back to the peer captured during the handler call.
bool pc_udp_peer_addr(const struct pc_udp_peer *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.
Layer 4 (Transport) - connectionless UDP datagram service.