ProtoCore v0.0.2
Deterministic, zero-heap network stack for embedded targets
Loading...
Searching...
No Matches
coap.h
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 coap.h
6 * @brief Zero-heap CoAP server (RFC 7252): message codec + a fixed resource table.
7 *
8 * The server is split into a pure, host-testable core and an ESP32-only UDP
9 * transport (mirroring the SNMP agent's split):
10 *
11 * - pc_coap_server_process() takes a complete request datagram and produces a
12 * complete response datagram in a caller buffer - no sockets, no heap. It is
13 * unit-tested on the host (env:native_coap).
14 * - pc_coap_server_begin() binds the transport-layer UDP service on :5683
15 * (Arduino only) and feeds received datagrams through pc_coap_server_process().
16 *
17 * The message layer uses the piggybacked-response model: a CON request is answered
18 * with a piggybacked ACK, a NON request with a NON response. Message de-duplication
19 * (RFC 7252 sec 4.5) is implemented - a retransmitted CON is re-answered from a small
20 * cache keyed on (source endpoint, Message-ID) WITHOUT re-running its handler, so a
21 * client's retransmission cannot execute a non-idempotent request twice (see
22 * PC_COAP_DEDUP_*). Separate (deferred) responses are a deliberate non-goal (this is
23 * a synchronous, in-line server: a request is answered before the handler returns),
24 * and there is no CON retransmission because the server never sends a Confirmable
25 * message - notifications go out Non-confirmable. The /.well-known/core resource-
26 * discovery listing (RFC 6690) is served. The codec understands the Uri-Path,
27 * Uri-Query and Content-Format options; other options are skipped. Block-wise transfer
28 * (RFC 7959, the Block1/Block2 options) is available under PC_ENABLE_COAP_BLOCK;
29 * resource observation (RFC 7641) under PC_ENABLE_COAP_OBSERVE.
30 *
31 * The resource table is a fixed BSS array of PC_COAP_MAX_RESOURCES entries.
32 * Register handlers with pc_coap_server_add_resource(); the path string is
33 * referenced by pointer and must outlive the server (point it at flash/static
34 * data, like the rest of the library's strings).
35 */
36
37#ifndef PROTOCORE_COAP_H
38#define PROTOCORE_COAP_H
39
40#include "protocore_config.h"
41#include <stddef.h>
42#include <stdint.h>
43
44#if PC_ENABLE_COAP
45
46// CoAP message types (RFC 7252 §3, the 2-bit T field).
47enum class CoapType : uint8_t
48{
49 COAP_TYPE_CON = 0, ///< Confirmable (answered with a piggybacked ACK).
50 COAP_TYPE_NON = 1, ///< Non-confirmable (answered with a NON response).
51 COAP_TYPE_ACK = 2, ///< Acknowledgement.
52 COAP_TYPE_RST = 3, ///< Reset (rejects a message; sent for a malformed/empty CON).
53};
54
55// CoAP request method codes (class 0; the on-wire Code byte's detail field).
56enum class CoapMethod : uint8_t
57{
58 COAP_GET = 1,
59 COAP_POST = 2,
60 COAP_PUT = 3,
61 COAP_DELETE = 4,
62};
63
64// Allowed-methods bitmask for pc_coap_server_add_resource() (bit per method). A mask is OR'd together,
65// so these are integer constants in a namespacing struct, not an enum class (which would force a cast
66// at every |). The bit position is the CoapMethod ordinal.
67struct CoapMethodMask
68{
69 static constexpr uint8_t COAP_ALLOW_GET = 1u << (unsigned)CoapMethod::COAP_GET; ///< 0x02
70 static constexpr uint8_t COAP_ALLOW_POST = 1u << (unsigned)CoapMethod::COAP_POST; ///< 0x04
71 static constexpr uint8_t COAP_ALLOW_PUT = 1u << (unsigned)CoapMethod::COAP_PUT; ///< 0x08
72 static constexpr uint8_t COAP_ALLOW_DELETE = 1u << (unsigned)CoapMethod::COAP_DELETE; ///< 0x10
73};
74
75/** @brief Build a CoAP response Code byte from its class and detail (e.g. COAP_CODE(2,5) = 2.05). */
76#define COAP_CODE(c, dd) ((uint8_t)(((c) << 5) | ((dd) & 0x1F)))
77
78// Common CoAP response codes (RFC 7252 §5.9; 2.31 / 4.08 / 4.13 from RFC 7959).
79enum class CoapResponseCode : uint8_t
80{
81 COAP_RSP_CREATED = COAP_CODE(2, 1), ///< 2.01
82 COAP_RSP_DELETED = COAP_CODE(2, 2), ///< 2.02
83 COAP_RSP_VALID = COAP_CODE(2, 3), ///< 2.03
84 COAP_RSP_CHANGED = COAP_CODE(2, 4), ///< 2.04
85 COAP_RSP_CONTENT = COAP_CODE(2, 5), ///< 2.05
86 COAP_RSP_CONTINUE = COAP_CODE(2, 31), ///< 2.31 (block-wise: more Block1 blocks expected)
87 COAP_RSP_BAD_REQUEST = COAP_CODE(4, 0), ///< 4.00
88 COAP_RSP_BAD_OPTION = COAP_CODE(4, 2), ///< 4.02
89 COAP_RSP_NOT_FOUND = COAP_CODE(4, 4), ///< 4.04
90 COAP_RSP_METHOD_NOT_ALLOWED = COAP_CODE(4, 5), ///< 4.05
91 COAP_RSP_NOT_ACCEPTABLE = COAP_CODE(4, 6), ///< 4.06
92 COAP_RSP_REQUEST_INCOMPLETE = COAP_CODE(4, 8), ///< 4.08 (block-wise: out-of-order / lost Block1)
93 COAP_RSP_REQUEST_TOO_LARGE = COAP_CODE(4, 13), ///< 4.13 (block-wise: reassembly buffer exceeded)
94 COAP_RSP_INTERNAL_ERROR = COAP_CODE(5, 0), ///< 5.00
95 COAP_RSP_NOT_IMPLEMENTED = COAP_CODE(5, 1), ///< 5.01
96};
97
98// CoAP Content-Format identifiers (RFC 7252 §12.3). CoapContentFormat::COAP_CF_NONE means "absent".
99enum class CoapContentFormat : uint16_t
100{
101 COAP_CF_TEXT = 0, ///< text/plain;charset=utf-8
102 COAP_CF_LINK = 40, ///< application/link-format
103 COAP_CF_XML = 41, ///< application/xml
104 COAP_CF_OCTET = 42, ///< application/octet-stream
105 COAP_CF_JSON = 50, ///< application/json
106 COAP_CF_CBOR = 60, ///< application/cbor
107 COAP_CF_NONE = 0xFFFF, ///< no Content-Format option present / emitted
108};
109
110/**
111 * @brief A decoded CoAP request handed to a resource handler.
112 *
113 * All pointers reference transport- or library-owned scratch valid only for the
114 * duration of the handler call; copy out anything you need to keep.
115 */
116struct CoapRequest
117{
118 CoapMethod method; ///< COAP_GET / COAP_POST / COAP_PUT / COAP_DELETE.
119 const char *path; ///< reconstructed Uri-Path, e.g. "/temp" (always begins with '/').
120 const char *query; ///< reconstructed Uri-Query (segments joined by '&'), or "" if none.
121 const uint8_t *payload; ///< request payload bytes (may be nullptr if payload_len == 0).
122 size_t payload_len; ///< request payload length in bytes.
123 CoapContentFormat content_format; ///< request Content-Format, or COAP_CF_NONE if absent.
124};
125
126/**
127 * @brief A response a resource handler fills in.
128 *
129 * @p code defaults to 2.05 Content; set it to another CoapResponseCode as
130 * appropriate. Write the response body into @p payload (capacity @p payload_cap)
131 * and set @p payload_len. Set @p content_format to describe the body, or leave it
132 * CoapContentFormat::COAP_CF_NONE for an empty/typeless response.
133 */
134struct CoapResponse
135{
136 uint8_t code; ///< response Code byte (see CoapResponseCode); defaults to CoapResponseCode::COAP_RSP_CONTENT.
137 CoapContentFormat content_format; ///< COAP_CF_* describing the body, or COAP_CF_NONE.
138 uint8_t *payload; ///< caller-provided buffer to write the response body into.
139 size_t payload_cap; ///< capacity of @p payload in bytes.
140 size_t payload_len; ///< bytes written by the handler (0 = empty body).
141};
142
143/** @brief Resource handler: read @p req, fill @p resp. */
144typedef void (*CoapHandler)(const CoapRequest *req, CoapResponse *resp);
145
146// ---------------------------------------------------------------------------
147// Server configuration / resource registration
148// ---------------------------------------------------------------------------
149
150/** @brief Reset the server and clear the resource table. Call before registering resources. */
151void pc_coap_server_reset();
152
153/**
154 * @brief Register a resource at @p path served by @p handler.
155 *
156 * @param path resource path beginning with '/' (referenced by pointer, not copied).
157 * @param methods allowed-methods bitmask (e.g. CoapMethodMask::COAP_ALLOW_GET | CoapMethodMask::COAP_ALLOW_PUT); a
158 * request using a method not in the mask is answered 4.05 Method Not Allowed.
159 * @param handler invoked for an allowed method on a matching path.
160 * @return false if the table is full.
161 */
162bool pc_coap_server_add_resource(const char *path, uint8_t methods, CoapHandler handler);
163
164// ---------------------------------------------------------------------------
165// Core processing (host-testable; no sockets, no heap)
166// ---------------------------------------------------------------------------
167
168/**
169 * @brief Process one CoAP request datagram and build the response datagram.
170 *
171 * Parses the message, reconstructs the Uri-Path/Uri-Query, dispatches against the
172 * resource table, and encodes a piggybacked response (ACK for CON, NON for NON).
173 * A malformed or unsupported-version CON is answered with an RST; a malformed NON
174 * (or any ACK/RST received) yields no response.
175 *
176 * @param req request datagram bytes.
177 * @param req_len number of bytes in @p req.
178 * @param resp destination buffer for the response datagram.
179 * @param pc_resp_cap capacity of @p resp.
180 * @return number of response bytes written, or 0 to send nothing.
181 */
182size_t pc_coap_server_process(const uint8_t *req, size_t req_len, uint8_t *resp, size_t pc_resp_cap);
183
184/**
185 * @brief Like pc_coap_server_process(), but include an Observe option (RFC 7641) in
186 * a successful (2.xx) response carrying the notification sequence
187 * @p observe_seq (a value < 0 omits it). Used by the Observe transport.
188 */
189size_t pc_coap_server_process_ex(const uint8_t *req, size_t req_len, uint8_t *resp, size_t pc_resp_cap,
190 int32_t observe_seq);
191
192#if PC_COAP_DEDUP_ENTRIES > 0
193/**
194 * @brief Message de-duplication lookup (RFC 7252 sec 4.5). If a Confirmable request from @p src_ip :
195 * @p src_port with Message-ID @p mid was answered within PC_COAP_DEDUP_LIFETIME_MS, report its
196 * cached response so the transport can resend it without re-running the handler.
197 * @return true and (on non-null out params) the cached response bytes + length; false on a miss.
198 */
199bool pc_coap_dedup_lookup(const char *src_ip, uint16_t src_port, uint16_t mid, const uint8_t **out, size_t *out_len);
200
201/**
202 * @brief Record the response sent for a Confirmable (@p src_ip : @p src_port, @p mid) exchange so a later
203 * retransmission is deduplicated. A response longer than PC_COAP_DEDUP_RESP_MAX is not cached.
204 */
205void pc_coap_dedup_store(const char *src_ip, uint16_t src_port, uint16_t mid, const uint8_t *resp, size_t len);
206#endif
207
208// ---------------------------------------------------------------------------
209// UDP transport (binds via the transport-layer UDP service; no-op on host)
210// ---------------------------------------------------------------------------
211
212/**
213 * @brief Bind the server to UDP @p port (default 5683) via the transport-layer UDP service.
214 *
215 * Callback-driven (no per-loop servicing). Call after WiFi is up. On non-Arduino
216 * builds pc_udp_listen() is a stub, so the core remains host-testable.
217 */
218void pc_coap_server_begin(uint16_t port = 5683);
219
220#if PC_ENABLE_COAP_OBSERVE
221/**
222 * @brief Push a notification to every observer of @p path (RFC 7641).
223 *
224 * Re-renders the resource (invokes its GET handler) and sends the current
225 * representation as a CoAP notification - from the bound server port, carrying
226 * each observer's token and an increasing Observe sequence. Call this whenever the
227 * resource's state changes. A send failure drops that observer. No-op on a host
228 * build. A client registers by sending a GET with the Observe option (0); it
229 * deregisters with Observe (1), a Reset, or by going away.
230 */
231void pc_coap_notify(const char *path);
232#endif
233
234#endif // PC_ENABLE_COAP
235
236#endif // PROTOCORE_COAP_H
User-facing configuration for ProtoCore.