ProtoCore v0.0.1
Deterministic, zero-heap network stack for embedded targets
Loading...
Searching...
No Matches
protocore_config.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 protocore_config.h
6 * @brief User-facing configuration for ProtoCore.
7 *
8 * **Compile-time sizing constants**
9 * These govern static array dimensions and must be set before the first
10 * library header is included. Define any of them in your sketch or in a
11 * build flag before including this file to override the defaults:
12 * @code
13 * // platformio.ini
14 * build_flags = -DMAX_CONNS=8 -DBODY_BUF_SIZE=512
15 * @endcode
16 *
17 * **Runtime parameters - flash or RAM, your choice**
18 * `WebServerConfig` holds values that can be changed without a rebuild.
19 * On ESP32, `PROGMEM` is a no-op (const data lands in DROM automatically).
20 * On AVR it places data in flash and requires `pgm_read_*` accessors - this
21 * library targets ESP32 only, so both forms read identically via pointer:
22 * @code
23 * // Flash (PROGMEM, no RAM cost at runtime):
24 * const WebServerConfig my_cfg PROGMEM = { .conn_timeout_ms = 10000 };
25 *
26 * // RAM (can be changed at runtime):
27 * WebServerConfig my_cfg = { .conn_timeout_ms = 10000 };
28 *
29 * server.begin(80, &my_cfg);
30 * @endcode
31 * Pass `nullptr` (or omit the argument) to use the built-in default
32 * (`CONN_TIMEOUT_MS`, 5000 ms idle timeout).
33 */
34
35#ifndef PROTOCORE_CONFIG_H
36#define PROTOCORE_CONFIG_H
37
38#include <stdint.h>
39
40// Per-variant default sizing (chip / PSRAM / flash profiles). Included before the sizing
41// defaults below so a board profile can raise them for a larger target; your -D / build_opt.h
42// overrides still win (every profile default is #ifndef-guarded).
44
45// ---------------------------------------------------------------------------
46// Compile-time capacity constants (affect static array sizes)
47// ---------------------------------------------------------------------------
48
49/**
50 * @brief Maximum simultaneous TCP connections (fixed static pool; ~3.95 KB of internal RAM per slot).
51 *
52 * Default 8: a keep-alive/concurrency server needs headroom above its peak concurrent client count,
53 * because a connection closed by the keep-alive fairness cap (PC_KEEPALIVE_MAX_REQUESTS) briefly
54 * holds its slot in CONN_CLOSING while it drains, and a reconnecting client needs a free slot mean-
55 * while - if concurrency equals the pool size there is none, and the overflow connection is refused
56 * (correct backpressure, but it caps clean throughput at concurrency == MAX_CONNS - 1). Set lower
57 * (e.g. -DMAX_CONNS=4, ~16 KB less RAM) on a RAM-constrained target, or higher (16/32) for a
58 * connection-heavy HTTP server; the event queue tracks it automatically (EVT_QUEUE_DEPTH below).
59 */
60#ifndef MAX_CONNS
61#define MAX_CONNS 8
62#endif
63
64/**
65 * @brief Disable Nagle's algorithm (set TCP_NODELAY) on every accepted connection.
66 *
67 * A request/response server is latency-first: the response is buffered whole (`tcp_write`) and pushed with a
68 * single `tcp_output`, so Nagle only ever delays the final sub-MSS segment of a multi-segment response (or a
69 * streamed chunk) - it waits for the peer's ACK of the prior segment, costing a ~40-200 ms delayed-ACK stall
70 * for no bandwidth benefit here. Disabling it lets that tail go out immediately. Set to 0 only if the device
71 * mainly streams bulk data and you prefer Nagle's segment coalescing over per-response latency.
72 */
73#ifndef PC_TCP_NODELAY
74#define PC_TCP_NODELAY 1
75#endif
76
77/**
78 * @brief Enable DiffServ QoS marking (RFC 2474) on outbound traffic. Default off.
79 *
80 * When set, the transport can stamp the 6-bit DSCP into the DS field (the high 6 bits of the IPv4 TOS /
81 * IPv6 Traffic-Class byte) of every outbound TCP connection and UDP datagram, so a QoS-aware network - and
82 * the Wi-Fi driver's 802.11e WMM access-category mapping - can prioritize safety / real-time packets (e.g.
83 * the Expedited-Forwarding class, DSCP 46) over best-effort. `network_drivers/transport/diffserv.h` exposes a
84 * server-wide default (`pc_set_default_dscp`), a UDP default (`pc_udp_set_dscp`), a per-listener override
85 * (`pc_listen_set_dscp`), and a per-connection setter (`pc_conn_set_dscp`) so an individual flow can carry
86 * any DSCP - useful both for real QoS and for arbitrarily tagging traffic in network testing. The DSCP is
87 * applied on tcpip_thread (accept / connect / udp create), so no extra marshalling is added to the hot path.
88 * Default off (zero cost: the marking code and the DSCP state are compiled out).
89 */
90#ifndef PC_ENABLE_DIFFSERV
91#define PC_ENABLE_DIFFSERV 0
92#endif
93
94/**
95 * @brief Use the SWAR base64 decoder (classify 4 characters per 32-bit word). Default on.
96 *
97 * base64 decode is the one base64 path that touches a secret (the Basic-auth credential, RFC 7617, and the
98 * JWT / JWS segments, RFC 7515), so it must be **constant-time** - the character -> value mapping evaluated
99 * with branchless arithmetic masks, no data-dependent branch or table. Two constant-time implementations are
100 * available: a scalar one that classifies a character at a time, and this SWAR one that packs 4 characters
101 * into a word and classifies all four lanes in parallel with guard-bit range masks (every base64 character
102 * is < 0x80, so borrows never cross lanes). Both are byte-identical (`test_base64` runs against each) and
103 * both are constant-time; measured on the ESP32-S3, SWAR is ~1.9x faster than the scalar path and 5.36x
104 * faster than mbedTLS (882 vs 1639 vs 4728 cyc on a credential), at 0.00-cycle input-dependent variance.
105 * SWAR is the default; set to 0 for the smaller, simpler scalar decoder if code size matters more than the
106 * ~1.9x once-per-request decode win. Portable (any 32-bit target); encode is unaffected (always software).
107 */
108#ifndef PC_BASE64_SWAR
109#define PC_BASE64_SWAR 1
110#endif
111
112/** @brief Ring-buffer capacity in bytes per connection slot (feature floors enforced last, in
113 * board_drivers/board_profiles/derived_sizing.h - a value below what an enabled feature needs is raised there). */
114#ifndef RX_BUF_SIZE
115#define RX_BUF_SIZE 1024
116#endif
117
118/**
119 * @brief Compile-time default for connection idle timeout in milliseconds.
120 *
121 * The actual runtime value is stored in `WebServerConfig::conn_timeout_ms`
122 * and loaded into `DeterministicAsyncTCP::conn_timeout_ms` by init().
123 */
124#ifndef CONN_TIMEOUT_MS
125#define CONN_TIMEOUT_MS 5000
126#endif
127
128/**
129 * @brief Request-header read deadline in milliseconds (slow-loris defense). Default 10 s; 0 disables.
130 *
131 * The idle timeout (CONN_TIMEOUT_MS) refreshes on every accepted byte, so a slow-loris that trickles one
132 * header byte just under the idle window holds a connection slot forever and, with a few connections, denies
133 * the whole fixed pool to legitimate clients (a connection-slot DoS - verified on HW). This is an ABSOLUTE
134 * deadline from the first byte of a request to the end of its HEADERS that a trickle cannot reset: a connection
135 * whose request headers are not complete within PC_REQUEST_TIMEOUT_MS is answered 408 and closed, freeing the
136 * slot (the nginx client_header_timeout semantic). It is scoped to the header phase, so it never reaps a
137 * legitimate slow body: a large streaming upload (PARSE_BODY) is governed by the streaming handler + idle
138 * timer, not this deadline. It also does not touch WebSocket / SSE slots (long-lived by design). Lower it to
139 * tighten the window on a trusted LAN; 0 turns the defense off.
140 */
141#ifndef PC_REQUEST_TIMEOUT_MS
142#define PC_REQUEST_TIMEOUT_MS 10000
143#endif
144
145/**
146 * @brief Upper bound (ms) a slot may dwell in ConnState::CONN_CLOSING after a graceful close
147 * before the idle sweep force-aborts it.
148 *
149 * On a graceful (local) close the slot stays in ConnState::CONN_CLOSING - keeping its PCB and
150 * callbacks - until the peer ACKs the response (then it frees itself in the sent
151 * callback). If the peer never ACKs (dead/black-holed), this bound lets the
152 * timeout sweep reclaim the slot so the fixed pool cannot leak.
153 */
154#ifndef PC_CLOSING_TIMEOUT_MS
155#define PC_CLOSING_TIMEOUT_MS 2000
156#endif
157
158// ---------------------------------------------------------------------------
159// Worker model (server task concurrency)
160// ---------------------------------------------------------------------------
161//
162// The server pipeline (drain events -> dispatch -> send) runs in one or more
163// dedicated FreeRTOS "worker" tasks instead of the user's loop(). Each worker
164// owns a disjoint partition of conn_pool slots (slot i -> worker i %
165// PC_WORKER_COUNT) and its own scratch arena, so no two workers ever touch
166// the same slot: shared-nothing, no hot-path locks, latency stays bounded
167// (determinism preserved) while cores run disjoint connections in parallel.
168//
169// PC_WORKER_COUNT == 1 (default) is byte-for-byte the single-pipeline model:
170// one worker owns every slot, one arena, the existing single event queue. N > 1
171// is opt-in. The arena BSS cost is PC_SCRATCH_ARENA_SIZE * PC_WORKER_COUNT.
172
173/** @brief Number of server worker tasks (slots partitioned i % N). Default 1. */
174#ifndef PC_WORKER_COUNT
175#define PC_WORKER_COUNT 1
176#endif
177
178// Total scratch arenas. Each independent task that touches the shared scratch arena needs its own
179// (the single-accessor-per-arena invariant is what makes it lock-free). The server's workers own
180// slots 0..PC_WORKER_COUNT-1; the reverse-SSH client runs in its own task and owns one more, so it
181// can decrypt packets without borrowing a worker's arena (a foreign-task use trips the tripwire).
182#if PC_ENABLE_SSH_CLIENT
183#define PC_SCRATCH_SLOTS (PC_WORKER_COUNT + 1)
184#define PC_SSH_CLIENT_SCRATCH_SLOT (PC_WORKER_COUNT) ///< the arena the client task owns.
185#else
186#define PC_SCRATCH_SLOTS (PC_WORKER_COUNT)
187#endif
188
189/**
190 * @brief Stack (bytes) for each server worker task (ESP32).
191 *
192 * Floor note: two heavy computations run on the worker.
193 * - RSA-2048 verification (OIDC / SSH host key / JWKS via the mbedTLS bignum
194 * modexp) uses ~7 KB (measured on a DevKitV1).
195 * - SSH modern crypto (curve25519-sha256 KEX + ssh-ed25519, software field
196 * arithmetic in radix-2^16) peaks at ~10.5 KB (measured on an ESP32-S3): the
197 * deep pc_gf call chain plus the on-accelerator field inversion nests deeper
198 * than the RSA path.
199 * So the default adapts: 12 KB when SSH is enabled (curve/ed25519 can be
200 * negotiated), 8 KB otherwise. Do NOT lower it below the matching floor
201 * (::PC_WORKER_STACK_CURVE_MIN for SSH, ::PC_WORKER_STACK_RSA_MIN for
202 * OIDC) or the first handshake overflows the task stack - a build-time guard
203 * (bottom of this file) enforces the floor so a lowered stack is caught at
204 * compile time.
205 */
206// True when any feature that runs the SSH-class handshake is built. Derived, not configurable, and
207// defined unconditionally: a predicate that exists only inside one #ifndef is a trap for the next
208// person who tests it further down, where it would silently read 0.
209#if (defined(PC_ENABLE_SSH) && PC_ENABLE_SSH) || (defined(PC_ENABLE_SSH_CLIENT) && PC_ENABLE_SSH_CLIENT) || \
210 (defined(PC_ENABLE_HTTP3) && PC_ENABLE_HTTP3)
211#define PC_SSH_ANY 1
212#else
213#define PC_SSH_ANY 0
214#endif
215
216#ifndef PC_WORKER_TASK_STACK
217// SSH (curve25519 + ssh-ed25519, server OR the reverse-SSH client) and HTTP/3 (the QUIC TLS-1.3
218// handshake reuses the same pc_ed25519 signer for CertificateVerify) all peak at ~10.5 KB on the
219// worker task; the PQ/T hybrid (PC_ENABLE_PQC_KEX) runs ML-KEM-768 on top, ~7 KB more. The default
220// tracks the matching floor so a hybrid build is provisioned, not starved; the guard at the bottom of
221// this file is the backstop when the stack is set by hand. (A flag set only in the config block below,
222// not via -D, is still undefined here and reads as 0 - the guard then catches any shortfall.)
223// sntrup761x25519-sha512 (on by default with the PQC hybrid, but a standalone -D can enable it without
224// ML-KEM) is the heavy case: the reverse-SSH CLIENT runs KeyGen+Decaps whose FO re-encrypt peaks ~32 KB,
225// the SERVER runs Encaps only (~22 KB). ML-KEM alone stays at 16 KB, so a build that explicitly drops
226// sntrup761 keeps the lighter floor. This default block only sees -D flags (the config defaults below
227// have not run yet), so sntrup761's default-tracks-PQC is assumed here and the guard at the bottom is
228// the backstop when it is toggled in the config block instead of via -D.
229#if (PC_ENABLE_PQC_KEX || (defined(PC_ENABLE_SSH_SNTRUP761) && PC_ENABLE_SSH_SNTRUP761)) && PC_SSH_ANY
230#if defined(PC_ENABLE_SSH_SNTRUP761) && !PC_ENABLE_SSH_SNTRUP761
231#define PC_WORKER_TASK_STACK 16384
232#elif defined(PC_ENABLE_SSH_CLIENT) && PC_ENABLE_SSH_CLIENT
233#define PC_WORKER_TASK_STACK 40960
234#else
235#define PC_WORKER_TASK_STACK 32768
236#endif
237#elif PC_SSH_ANY
238#define PC_WORKER_TASK_STACK 12288
239#else
240#define PC_WORKER_TASK_STACK 8192
241#endif
242#endif
243
244/**
245 * @brief Minimum worker-task stack (bytes) required once an RSA-2048 verifier is
246 * compiled in (OIDC / SSH).
247 *
248 * The mbedTLS bignum modexp alone consumes ~7 KB; 8 KB leaves room for the rest
249 * of the request call chain. Overridable only for an advanced build that marshals
250 * every RSA verify onto a dedicated larger-stack task (then the worker itself never
251 * runs one) - otherwise leave it at the default.
252 */
253#ifndef PC_WORKER_STACK_RSA_MIN
254#define PC_WORKER_STACK_RSA_MIN 8192
255#endif
256
257/**
258 * @brief Minimum worker-task stack (bytes) required once SSH is compiled in.
259 *
260 * SSH can negotiate curve25519-sha256 + ssh-ed25519, whose software field
261 * arithmetic peaks at ~10.5 KB of worker stack; 12 KB leaves ~1.8 KB of margin
262 * for the rest of the handshake call chain (comparable to the RSA floor's
263 * margin). Raise both this and ::PC_WORKER_TASK_STACK together if you extend
264 * the handshake, or force RSA/DH only (ssh_kex_set_prefer_rsa) on a very tight
265 * build - but the server still advertises the modern suite, so a modern-only
266 * client would still exercise it.
267 */
268#ifndef PC_WORKER_STACK_CURVE_MIN
269#define PC_WORKER_STACK_CURVE_MIN 12288
270#endif
271
272/** @brief FreeRTOS priority for each server worker task (ESP32). */
273#ifndef PC_WORKER_TASK_PRIORITY
274#define PC_WORKER_TASK_PRIORITY 5
275#endif
276
277/**
278 * @brief Core that worker 0 pins to (ESP32). Worker k pins to (PC_WORKER_CORE
279 * + k) % portNUM_PROCESSORS. Default 1 (APP_CPU), keeping Core 0 lean for the
280 * WiFi/lwIP stack and offloading the user's loop().
281 */
282#ifndef PC_WORKER_CORE
283#define PC_WORKER_CORE 1
284#endif
285
286/**
287 * @brief Depth of each worker's deferred-callback queue.
288 *
289 * App code on loop() or another task submits work to a slot's owning worker via
290 * pc_defer() / pc_defer_slot(); the worker runs it in its own single-thread
291 * context, so an async push (ws_send / pc_sse_send from a timer) is race-free. Each
292 * worker has one queue of this depth (entries are a {fn, arg} pair, ~8 bytes).
293 */
294#ifndef PC_DEFER_QUEUE_DEPTH
295#define PC_DEFER_QUEUE_DEPTH 8
296#endif
297
298/**
299 * @brief Idle-sweep timeout, in FreeRTOS ticks, that a worker blocks between
300 * service iterations when no events are pending.
301 *
302 * The worker no longer free-runs a poll: it blocks on a task notification and a
303 * producer (a new connection event or a deferred submission) wakes it the moment
304 * work arrives, so event latency is independent of this value. The block still
305 * times out after this many ticks so the idle timeout sweep (check_timeouts) keeps
306 * reaping stale connections when nothing is in flight.
307 *
308 * Default 1 (1 tick at the Arduino 1 kHz FreeRTOS config) preserves the original
309 * idle cadence byte-for-byte. Because events now wake the worker immediately,
310 * raising it lowers idle wakeups (CPU/power on a battery device) WITHOUT the
311 * latency penalty the old poll-based knob carried - e.g. 100 -> a ~10 Hz idle
312 * sweep, still far below any connection timeout. The internal time base stays
313 * 1000 Hz regardless (see services/system/clock.h).
314 */
315#ifndef PC_WORKER_POLL_TICKS
316#define PC_WORKER_POLL_TICKS 1
317#endif
318
319#if PC_WORKER_COUNT < 1
320#error "ProtoCore: PC_WORKER_COUNT must be >= 1"
321#endif
322#if PC_WORKER_COUNT > MAX_CONNS
323#error "ProtoCore: PC_WORKER_COUNT must be <= MAX_CONNS"
324#endif
325
326// ---------------------------------------------------------------------------
327// Preempting work queue (PC_ENABLE_PREEMPT_QUEUE) - v5 real-time ingest
328// ---------------------------------------------------------------------------
329//
330// Fixed-capacity queues, each feeding one core-pinned processing task: a producer
331// posts a fixed-size item (from a task or an ISR) and the scheduler preempts straight
332// to the task. There are named lanes - one USER lane exposed to the app, and internal
333// DMA / forwarding / device-access lanes that run at a higher priority so internal
334// ingest always preempts user work. Queue storage is static (zero heap), so depth +
335// item size are compile-time; a task's stack is created only when its lane starts.
336// The no-lane pc_pq_* API drives the USER lane. See preempt_queue.h.
337
338/** @brief Enable the preempting work queue primitive (default off). */
339#ifndef PC_ENABLE_PREEMPT_QUEUE
340#define PC_ENABLE_PREEMPT_QUEUE 0
341#endif
342
343/** @brief Capacity of the preempting queue in items (static-allocated). */
344#ifndef PC_PQ_DEPTH
345#define PC_PQ_DEPTH 16
346#endif
347
348/** @brief Bytes per preempting-queue item (the posted item must fit). */
349#ifndef PC_PQ_ITEM_SIZE
350#define PC_PQ_ITEM_SIZE 32
351#endif
352
353/** @brief Stack (bytes) for each preempting-queue processing task (ESP32). */
354#ifndef PC_PQ_STACK
355#define PC_PQ_STACK 4096
356#endif
357
358/**
359 * @brief Base FreeRTOS priority for the internal preempting lanes (DMA / forwarding /
360 * device access). They run at this and just above, so internal ingest preempts
361 * the user lane; keep it above the user lane's priority and below the lwIP tcpip
362 * (18) / WiFi tasks so networking is never starved. See preempt_queue.h.
363 */
364#ifndef PC_PQ_INTERNAL_PRIORITY
365#define PC_PQ_INTERNAL_PRIORITY 8
366#endif
367
368#if PC_ENABLE_PREEMPT_QUEUE && (PC_PQ_DEPTH < 1 || PC_PQ_ITEM_SIZE < 1)
369#error "ProtoCore: PC_PQ_DEPTH and PC_PQ_ITEM_SIZE must be >= 1"
370#endif
371
372// ---------------------------------------------------------------------------
373// DMA peripheral ingest / egress (PC_ENABLE_DMA) - v5 hardware ingest
374// ---------------------------------------------------------------------------
375//
376// Move peripheral bytes (UART / I2C / SPI) between the wire and a static buffer
377// with the CPU free during the transfer; a DMA-complete event carries the bytes
378// to a user callback, which typically posts a descriptor into the preempting work
379// queue (PC_ENABLE_PREEMPT_QUEUE) so the heavy processing runs off the ISR. RX
380// is double-buffered (ping-pong): the completed buffer is handed up while the DMA
381// engine fills the other. Storage is static (zero heap) - channel count and buffer
382// size are compile-time. See services/system/dma/dma.h.
383//
384// PC_DMA_SIMULATE routes the transfers through an in-memory ingress/egress
385// simulator (feed bytes in, capture bytes out, optional TX->RX loopback) so the
386// whole pipeline is exercised with no physical loopback wire - on the host test
387// bench and, with the flag set, on the device itself. It is the shipped, tested
388// engine; a real silicon backend plugs into pc_dma_hw_* when PC_DMA_SIMULATE=0.
389
390/** @brief Enable the DMA peripheral ingest / egress primitive (default off). */
391#ifndef PC_ENABLE_DMA
392#define PC_ENABLE_DMA 0
393#endif
394
395/** @brief Number of DMA channels (static-allocated; each is one peripheral link). */
396#ifndef PC_DMA_CHANNELS
397#define PC_DMA_CHANNELS 2
398#endif
399
400/** @brief Bytes per DMA transfer buffer (RX is double-buffered at this size). */
401#ifndef PC_DMA_BUF_SIZE
402#define PC_DMA_BUF_SIZE 256
403#endif
404
405/**
406 * @brief Route DMA transfers through the ingress/egress simulator (default on).
407 * Set to 0 to drive real silicon via the pc_dma_hw_* backend hooks.
408 */
409#ifndef PC_DMA_SIMULATE
410#define PC_DMA_SIMULATE 1
411#endif
412
413#if PC_ENABLE_DMA && (PC_DMA_CHANNELS < 1 || PC_DMA_BUF_SIZE < 1)
414#error "ProtoCore: PC_DMA_CHANNELS and PC_DMA_BUF_SIZE must be >= 1"
415#endif
416
417// ---------------------------------------------------------------------------
418// Trace capture: pre/post-trigger window assembler (PC_ENABLE_TRACE_CAPTURE)
419// ---------------------------------------------------------------------------
420//
421// Sits downstream of PC_ENABLE_DMA (or any other sample source) on a high-rate
422// acquisition front end: pc_tc_feed() is called with every batch of arriving samples
423// and a continuously-running pre-trigger ring always holds the most recent samples;
424// pc_tc_trigger() freezes that ring as the pre-trigger half of a window and the next
425// arriving samples fill the post-trigger half, so the emitted window straddles the
426// trigger instant like a benchtop oscilloscope's pretrigger/posttrigger split. One
427// capture in flight at a time, fail-closed. Storage is static (zero heap) - the sum of
428// the configured pretrigger + posttrigger sample counts must fit PC_TC_MAX_WINDOW_SAMPLES.
429// See services/system/trace_capture/trace_capture.h.
430
431/** @brief Enable the pre/post-trigger window assembler (default off). */
432#ifndef PC_ENABLE_TRACE_CAPTURE
433#define PC_ENABLE_TRACE_CAPTURE 0
434#endif
435
436/** @brief Max samples a window may hold (pretrigger_samples + posttrigger_samples), static-allocated. */
437#ifndef PC_TC_MAX_WINDOW_SAMPLES
438#define PC_TC_MAX_WINDOW_SAMPLES 4096
439#endif
440
441#if PC_ENABLE_TRACE_CAPTURE && PC_TC_MAX_WINDOW_SAMPLES < 1
442#error "ProtoCore: PC_TC_MAX_WINDOW_SAMPLES must be >= 1"
443#endif
444
445// ---------------------------------------------------------------------------
446// AD9238 SPI configuration-port codec (PC_ENABLE_AD9238)
447// ---------------------------------------------------------------------------
448//
449// A pure codec for the AD9238 dual ADC's low-speed SPI CONFIGURATION port (power-down,
450// output format, output test patterns, offset trim) - NOT its parallel sample-data bus,
451// which is out of an MCU's reach at this part's sample rates. See services/peripherals/ad9238/ad9238.h
452// for the hardware-verification caveat: the per-register bit fields are transcribed from
453// the datasheet, not yet confirmed against physical silicon.
454
455/** @brief Enable the AD9238 SPI configuration-port codec (default off). */
456#ifndef PC_ENABLE_AD9238
457#define PC_ENABLE_AD9238 0
458#endif
459
460// ---------------------------------------------------------------------------
461// Interface forwarding plane (PC_ENABLE_FORWARD) - v5 hardware ingest
462// ---------------------------------------------------------------------------
463//
464// A forwarding plane over the ingest pipeline: register interfaces (Wi-Fi STA / AP,
465// Ethernet, a peripheral bus, a radio), each with an egress send callback, then add
466// per-pair allow / deny rules with an optional rate cap. A frame arriving on one
467// interface (pc_forward_ingress(), typically from a DMA-complete event posted onto the
468// FORWARD lane) is forwarded to every allowed destination, so the device bridges / routes
469// between its interfaces instead of only terminating traffic. Default-deny and fail-closed
470// (a full destination or an exceeded rate cap drops, never blocks). Static tables (zero
471// heap). See services/net/forward/forward.h.
472
473/** @brief Enable the interface forwarding plane (default off). */
474#ifndef PC_ENABLE_FORWARD
475#define PC_ENABLE_FORWARD 0
476#endif
477
478/** @brief Max interfaces the forwarding plane tracks (static-allocated). */
479#ifndef PC_FWD_MAX_IFACES
480#define PC_FWD_MAX_IFACES 4
481#endif
482
483/** @brief Max forwarding rules (src -> dst allow/deny + rate cap; static-allocated). */
484#ifndef PC_FWD_MAX_RULES
485#define PC_FWD_MAX_RULES 8
486#endif
487
488/** @brief Max ingress access-control entries (byte-pattern permit/deny; static). */
489#ifndef PC_FWD_MAX_ACL
490#define PC_FWD_MAX_ACL 8
491#endif
492
493/** @brief Bytes an ACL entry can match (its pattern / mask length). */
494#ifndef PC_FWD_ACL_PATLEN
495#define PC_FWD_ACL_PATLEN 4
496#endif
497
498/** @brief Max policy routes (byte-pattern -> egress interface; static). Policy routes take
499 * precedence over the src->dst rules, so tagged traffic leaves a chosen interface. */
500#ifndef PC_FWD_MAX_ROUTES
501#define PC_FWD_MAX_ROUTES 8
502#endif
503
504/** @brief Build-time toggle for the forwarding-path inspection hook (default off, for cost +
505 * privacy). When 1, pc_forward_set_inspector() installs a runtime callback that observes
506 * / filters each ingress frame before it is forwarded; when 0 the hook is compiled out
507 * entirely (no call site). Runtime toggle: register or clear (null) the inspector. */
508#ifndef PC_FWD_INSPECT
509#define PC_FWD_INSPECT 0
510#endif
511
512#if PC_ENABLE_FORWARD && (PC_FWD_MAX_IFACES < 1 || PC_FWD_MAX_RULES < 1 || PC_FWD_ACL_PATLEN < 1)
513#error "ProtoCore: PC_FWD_MAX_IFACES / PC_FWD_MAX_RULES / PC_FWD_ACL_PATLEN must be >= 1"
514#endif
515
516// ---------------------------------------------------------------------------
517// Radio / wireless gateway (PC_ENABLE_GATEWAY) - v5 southbound-to-northbound bridge
518// ---------------------------------------------------------------------------
519//
520// The generic gateway pattern: a southbound radio (LoRa / nRF24 / Zigbee / ... reached
521// over SPI / I2C / UART) is a "port"; a frame it receives (data-ready ISR -> DMA -> the
522// FORWARD lane -> a per-radio codec) is handed to pc_gateway_uplink(), which envelopes it with
523// its source node address / port / RSSI and publishes it northbound through the uplink
524// callback (wire it to MQTT / HTTP / WebSocket / UDP). A northbound command goes the other
525// way through pc_gateway_downlink() to the port's transmit callback. The radio TX + the
526// northbound publish are callbacks (the seam a real radio driver / protocol binding plugs
527// into), so the bridge is host- and device-testable with no radio. Static tables (zero
528// heap). See services/net/gateway/gateway.h.
529
530/** @brief Enable the radio / wireless gateway bridge (default off). */
531#ifndef PC_ENABLE_GATEWAY
532#define PC_ENABLE_GATEWAY 0
533#endif
534
535/** @brief Max southbound gateway ports (radios / buses; static-allocated). */
536#ifndef PC_GW_MAX_PORTS
537#define PC_GW_MAX_PORTS 4
538#endif
539
540/** @brief Default northbound topic prefix (overridable at runtime via pc_gateway_set_topic_prefix). */
541#ifndef PC_GW_DEFAULT_PREFIX
542#define PC_GW_DEFAULT_PREFIX "gw"
543#endif
544
545#if PC_ENABLE_GATEWAY && (PC_GW_MAX_PORTS < 1)
546#error "ProtoCore: PC_GW_MAX_PORTS must be >= 1"
547#endif
548
549// ---------------------------------------------------------------------------
550// LoRa radio (PC_ENABLE_LORA) - Semtech SX127x / RFM95-96 codec + driver
551// ---------------------------------------------------------------------------
552//
553// A per-radio codec + driver that plugs into the gateway (PC_ENABLE_GATEWAY): the
554// RadioHead-compatible 4-byte frame header (to / from / id / flags) codec, and an SX127x
555// register driver over a caller-supplied register-access bus (so the SPI + chip-select
556// wiring is the integration's, and the register protocol is host-testable with a mock
557// bus). Bridge received frames northbound with pc_gateway_uplink(); the actual RF link needs
558// the module to verify. See services/radio/lora/lora.h.
559
560/** @brief Enable the LoRa (SX127x) radio codec + driver (default off). */
561#ifndef PC_ENABLE_LORA
562#define PC_ENABLE_LORA 0
563#endif
564
565/** @brief Max LoRa payload bytes (SX127x FIFO is 256; RadioHead uses 251 + 4 header). */
566#ifndef PC_LORA_MAX_PAYLOAD
567#define PC_LORA_MAX_PAYLOAD 251
568#endif
569
570#if PC_ENABLE_LORA && (PC_LORA_MAX_PAYLOAD < 1 || PC_LORA_MAX_PAYLOAD > 251)
571#error "ProtoCore: PC_LORA_MAX_PAYLOAD must be 1..251"
572#endif
573
574// ---------------------------------------------------------------------------
575// nRF24 radio (PC_ENABLE_NRF24) - Nordic nRF24L01+ 2.4 GHz driver
576// ---------------------------------------------------------------------------
577//
578// A radio driver that plugs into the gateway (PC_ENABLE_GATEWAY). The nRF24L01+ speaks
579// an SPI command protocol (not plain register r/w) and needs a separate CE pin, so the
580// driver runs over a caller-supplied SPI transfer + CE bus (nrf_bus). Its hardware pipe
581// addressing means the "source address" of a received frame is the pipe number - no
582// in-payload header, so there is no separate codec. Bridge received payloads northbound
583// with pc_gateway_uplink(port, pipe, ...); the RF link needs the module to verify.
584// See services/radio/nrf24/nrf24.h.
585
586/** @brief Enable the nRF24L01+ radio driver (default off). */
587#ifndef PC_ENABLE_NRF24
588#define PC_ENABLE_NRF24 0
589#endif
590
591/** @brief nRF24 fixed payload width in bytes (1..32; the chip's static payload size). */
592#ifndef PC_NRF24_PAYLOAD
593#define PC_NRF24_PAYLOAD 32
594#endif
595
596#if PC_ENABLE_NRF24 && (PC_NRF24_PAYLOAD < 1 || PC_NRF24_PAYLOAD > 32)
597#error "ProtoCore: PC_NRF24_PAYLOAD must be 1..32"
598#endif
599
600// ---------------------------------------------------------------------------
601// EnOcean ESP3 (PC_ENABLE_ENOCEAN) - energy-harvesting 868 MHz serial codec
602// ---------------------------------------------------------------------------
603//
604// A UART telegram codec for EnOcean's ESP3 (EnOcean Serial Protocol 3), the framing used
605// by USB/serial EnOcean gateways (TCM 310 / USB 300): sync 0x55, a 4-byte header (data
606// length, optional length, packet type) protected by CRC8, then data + optional data
607// protected by a second CRC8. pc_esp3_parse() frames one telegram out of a byte stream and
608// verifies both CRCs; pc_esp3_build() assembles one. Pure (no UART code - you feed it the
609// serial bytes), so it is fully host-testable. See services/radio/enocean/enocean.h.
610
611/** @brief Enable the EnOcean ESP3 serial codec (default off). */
612#ifndef PC_ENABLE_ENOCEAN
613#define PC_ENABLE_ENOCEAN 0
614#endif
615
616/** @brief Reject an ESP3 telegram whose declared data length exceeds this (framing sanity). */
617#ifndef PC_ENOCEAN_MAX_DATA
618#define PC_ENOCEAN_MAX_DATA 512
619#endif
620
621#if PC_ENABLE_ENOCEAN && (PC_ENOCEAN_MAX_DATA < 1)
622#error "ProtoCore: PC_ENOCEAN_MAX_DATA must be >= 1"
623#endif
624
625// ---------------------------------------------------------------------------
626// PN532 NFC (PC_ENABLE_PN532) - NXP PN532 NFC/RFID controller frame codec
627// ---------------------------------------------------------------------------
628//
629// The NXP PN532 (I2C / SPI / HSU) command-frame protocol - a tag read/write bridged to an
630// HTTP / MQTT event. The chip is driven by "normal information frames" (00 00 FF | LEN |
631// LCS | TFI | PData | DCS | 00) with a length checksum and a data checksum, plus a 6-byte
632// ACK frame. pc_pn532_build_frame() / pc_pn532_parse_frame() assemble and verify those frames
633// (the per-command PData is the application's), and pc_pn532_is_ack() detects the ACK. Pure -
634// you carry the frame bytes over your I2C / SPI / UART - so it is fully host-testable.
635// See services/peripherals/pn532/pn532.h.
636
637/** @brief Enable the PN532 NFC frame codec (default off). */
638#ifndef PC_ENABLE_PN532
639#define PC_ENABLE_PN532 0
640#endif
641
642/** @brief Reject a PN532 normal frame whose declared length exceeds this (framing sanity). */
643#ifndef PC_PN532_MAX_DATA
644#define PC_PN532_MAX_DATA 254
645#endif
646
647#if PC_ENABLE_PN532 && (PC_PN532_MAX_DATA < 1 || PC_PN532_MAX_DATA > 254)
648#error "ProtoCore: PC_PN532_MAX_DATA must be 1..254"
649#endif
650
651// ---------------------------------------------------------------------------
652// Sigfox (PC_ENABLE_SIGFOX) - Wisol / Murata Sigfox modem AT-command codec
653// ---------------------------------------------------------------------------
654//
655// Tiny low-power uplinks over the Sigfox 0G network. A Wisol / Murata Sigfox modem is
656// driven by AT commands over UART: pc_sigfox_build_uplink() formats an `AT$SF=<hex>` frame
657// for a <= 12-byte payload, and pc_sigfox_parse_response() classifies the modem's reply
658// (OK / ERROR / still pending). Pure text-command codec - you carry it over your UART - so
659// it is fully host-testable. See services/radio/sigfox/sigfox.h.
660
661/** @brief Enable the Sigfox AT-command codec (default off). */
662#ifndef PC_ENABLE_SIGFOX
663#define PC_ENABLE_SIGFOX 0
664#endif
665
666/** @brief Maximum Sigfox uplink payload (the network caps a message at 12 bytes). */
667#ifndef PC_SIGFOX_MAX_PAYLOAD
668#define PC_SIGFOX_MAX_PAYLOAD 12
669#endif
670
671#if PC_ENABLE_SIGFOX && (PC_SIGFOX_MAX_PAYLOAD < 1 || PC_SIGFOX_MAX_PAYLOAD > 12)
672#error "ProtoCore: PC_SIGFOX_MAX_PAYLOAD must be 1..12"
673#endif
674
675// ---------------------------------------------------------------------------
676// Z-Wave (PC_ENABLE_ZWAVE) - Silicon Labs Z-Wave Serial API frame codec
677// ---------------------------------------------------------------------------
678//
679// The host-side Serial API of a Silicon Labs 500 / 700-series Z-Wave controller over UART:
680// a Z-Wave mesh bridged to the web. Data frames are SOF (0x01) | LEN | Type | Command |
681// Data | Checksum, where the checksum is 0xFF XOR-folded over LEN..last-data; single-byte
682// ACK (0x06) / NAK (0x15) / CAN (0x18) frames flow-control them. pc_zwave_build_frame() /
683// pc_zwave_parse_frame() assemble and verify a data frame; the per-command payload is the
684// application's. Pure - you carry the bytes over your UART - so it is fully host-testable.
685// See services/radio/zwave/zwave.h.
686
687/** @brief Enable the Z-Wave Serial API frame codec (default off). */
688#ifndef PC_ENABLE_ZWAVE
689#define PC_ENABLE_ZWAVE 0
690#endif
691
692/** @brief Reject a Z-Wave frame whose declared length exceeds this data cap (sanity). */
693#ifndef PC_ZWAVE_MAX_DATA
694#define PC_ZWAVE_MAX_DATA 64
695#endif
696
697#if PC_ENABLE_ZWAVE && (PC_ZWAVE_MAX_DATA < 1)
698#error "ProtoCore: PC_ZWAVE_MAX_DATA must be >= 1"
699#endif
700
701// ---------------------------------------------------------------------------
702// Zigbee (PC_ENABLE_ZIGBEE) - Silicon Labs EZSP / ASH serial framing codec
703// ---------------------------------------------------------------------------
704//
705// The ASH (Asynchronous Serial Host) data-link layer that carries EZSP frames to a Silicon
706// Labs EmberZNet NCP over UART - a Zigbee network bridged to the web. ASH delimits frames
707// with a Flag byte (0x7E), byte-stuffs the reserved control bytes, and protects each frame
708// with a CRC-16/CCITT. pc_ash_frame_encode() wraps a control byte + payload into a stuffed,
709// CRC'd frame; pc_ash_frame_decode() unstuffs + verifies one. The EZSP command payload the
710// frame carries (version, stack status, an incoming APS message, ...) is the application's.
711// pc_ash_frame_decode() removes the stuffing and verifies the CRC. Pure - you carry the bytes
712// over your UART - so it is fully host-testable. See services/radio/zigbee/zigbee.h.
713
714/** @brief Enable the Zigbee EZSP / ASH framing codec (default off). */
715#ifndef PC_ENABLE_ZIGBEE
716#define PC_ENABLE_ZIGBEE 0
717#endif
718
719/** @brief Max ASH payload bytes (an EZSP frame; the ASH data field caps near 128). */
720#ifndef PC_ZIGBEE_MAX_DATA
721#define PC_ZIGBEE_MAX_DATA 128
722#endif
723
724#if PC_ENABLE_ZIGBEE && (PC_ZIGBEE_MAX_DATA < 1)
725#error "ProtoCore: PC_ZIGBEE_MAX_DATA must be >= 1"
726#endif
727
728// ---------------------------------------------------------------------------
729// Thread (PC_ENABLE_THREAD) - OpenThread spinel over HDLC-lite framing codec
730// ---------------------------------------------------------------------------
731//
732// The HDLC-lite framing that carries spinel frames to an OpenThread radio co-processor
733// (RCP: an nRF52840 / EFR32) over UART - an 802.15.4 / Thread mesh bridged to IP / the web.
734// Each spinel frame is wrapped by HDLC-lite: an FCS (CRC-16/X-25) is appended, the reserved
735// bytes are byte-stuffed, and a Flag byte (0x7E) terminates it. pc_spinel_frame_encode() /
736// pc_spinel_frame_decode() do the framing + FCS; the spinel command inside (a property
737// get/set/insert, a stream frame) is the application's. Pure - you carry the bytes over your
738// UART - so it is fully host-testable. See services/radio/thread/thread.h.
739
740/** @brief Enable the Thread spinel / HDLC-lite framing codec (default off). */
741#ifndef PC_ENABLE_THREAD
742#define PC_ENABLE_THREAD 0
743#endif
744
745/** @brief Max spinel payload bytes carried in one HDLC-lite frame. */
746#ifndef PC_THREAD_MAX_DATA
747#define PC_THREAD_MAX_DATA 256
748#endif
749
750#if PC_ENABLE_THREAD && (PC_THREAD_MAX_DATA < 1)
751#error "ProtoCore: PC_THREAD_MAX_DATA must be >= 1"
752#endif
753
754// ---------------------------------------------------------------------------
755// Wired Ethernet PHY (PC_ENABLE_ETHERNET) - run the server over an RMII PHY
756// ---------------------------------------------------------------------------
757//
758// Bring up a wired Ethernet link (an RMII PHY: LAN8720 / TLK110 / RTL8201 / DP83848) so the
759// server runs over Ethernet instead of (or alongside) Wi-Fi. init_eth_physical() is a thin
760// wrapper over the Arduino ETH library; the PHY pins / type / clock come from the standard
761// ETH_PHY_* build flags for your board (see example Ethernet). The egress reporting
762// (pc_net_egress -> pc_iface::PC_IFACE_ETH) and the per-route interface classifier already handle a
763// wired route, so once the link has an IP the server accepts on it with no other change.
764// Default off (zero cost / the ETH library is not linked). ESP32-only.
765
766/** @brief Enable wired Ethernet bring-up (init_eth_physical / eth_ready). Default off. */
767#ifndef PC_ENABLE_ETHERNET
768#define PC_ENABLE_ETHERNET 0
769#endif
770
771// W5500 SPI Ethernet (arduino-esp32 3.x only). Set PC_ETH_W5500=1 to select the SPI PHY over the RMII
772// default; the pins below are the ESP32-S3-DevKitC wiring (HSPI / SPI3). The 2.x ETH library has no W5500,
773// so init_eth_physical() falls back to the RMII ETH.begin() when the core is older.
774#ifndef PC_ETH_W5500
775#define PC_ETH_W5500 0
776#endif
777#ifndef PC_ETH_W5500_CS
778#define PC_ETH_W5500_CS 7 ///< chip select
779#endif
780#ifndef PC_ETH_W5500_RST
781#define PC_ETH_W5500_RST 6 ///< reset
782#endif
783#ifndef PC_ETH_W5500_INT
784#define PC_ETH_W5500_INT 5 ///< interrupt
785#endif
786#ifndef PC_ETH_W5500_SCK
787#define PC_ETH_W5500_SCK 12 ///< HSPI clock (S3-DevKitC default)
788#endif
789#ifndef PC_ETH_W5500_MISO
790#define PC_ETH_W5500_MISO 13 ///< HSPI MISO (S3-DevKitC default)
791#endif
792#ifndef PC_ETH_W5500_MOSI
793#define PC_ETH_W5500_MOSI 11 ///< HSPI MOSI (S3-DevKitC default)
794#endif
795// W5500 SPI clock in MHz. The W5500 datasheet allows up to 33.3 MHz; 20 is the arduino-esp32 default and
796// a safe value for breadboard jumper wiring. Higher clocks raise throughput (the link is SPI-bound, not
797// PHY-bound) but need clean, short wiring - marginal signal integrity at high MHz corrupts frames.
798#ifndef PC_ETH_W5500_SPI_MHZ
799#define PC_ETH_W5500_SPI_MHZ 20 ///< W5500 SPI clock (MHz); raise for throughput on clean wiring
800#endif
801
802/**
803 * @brief Enable IPv6 on the network interface (dual-stack). Default off.
804 *
805 * When set, init_ipv6_physical() turns on IPv6 for the Wi-Fi netif (SLAAC link-local plus any
806 * router-advertised global address). The TCP and UDP listeners already bind IPADDR_TYPE_ANY, so
807 * the server accepts IPv6 connections the moment the interface has a v6 address; the pc_ip core
808 * (network_drivers/network/ip.h) parses / formats / classifies both families. Requires an
809 * lwIP built with LWIP_IPV6=1 (the stock Arduino-ESP32 core ships it).
810 */
811#ifndef PC_ENABLE_IPV6
812#define PC_ENABLE_IPV6 0
813#endif
814
815/**
816 * @brief Wi-Fi promiscuous (monitor) capture (PC_ENABLE_PROMISC). Default off.
817 *
818 * Passive 802.11 sniffing: pc_promisc_begin() puts the radio in promiscuous mode on a channel and
819 * delivers every frame to a sink (services/radio/promisc). Wire that sink into the forwarding plane
820 * (PC_ENABLE_FORWARD) to bridge captured Wi-Fi frames to another interface - e.g. stream them
821 * to a wired collector over Ethernet. Ships a pure 802.11 header parser and libpcap framing
822 * (DLT_IEEE802_11) so a forwarded frame is a valid PCAP a wired Wireshark / tcpdump can read.
823 */
824#ifndef PC_ENABLE_PROMISC
825#define PC_ENABLE_PROMISC 0
826#endif
827
828/**
829 * @brief Wired field-bus listen-only capture (PC_ENABLE_BUS_CAPTURE). Default off.
830 *
831 * The wired counterpart to promiscuous Wi-Fi capture: bus_capture_begin() installs the CAN (TWAI)
832 * controller in listen-only mode - it decodes every frame on the bus but never ACKs or transmits,
833 * so it stays invisible - and delivers each CanFrame to a sink (services/system/bus_capture). Wire the
834 * sink into the forwarding plane (PC_ENABLE_FORWARD) to bridge captured CAN frames to another
835 * interface. can_to_socketcan() formats a frame as a Linux SocketCAN frame so, with the libpcap
836 * DLT_CAN_SOCKETCAN link type, the stream is a capture Wireshark reads.
837 */
838#ifndef PC_ENABLE_BUS_CAPTURE
839#define PC_ENABLE_BUS_CAPTURE 0
840#endif
841
842// Feature / service / codec tuning knobs are consolidated at the END of this file,
843// under "Feature tuning knobs (grouped and gated by feature)" - placed there so every
844// PC_ENABLE_* flag is already resolved and each group can gate on its own feature.
845
846/** @brief Maximum HTTP headers stored per request. */
847#ifndef MAX_HEADERS
848#define MAX_HEADERS 8
849#endif
850
851/** @brief Maximum URL path length (including leading `/`). */
852#ifndef MAX_PATH_LEN
853#define MAX_PATH_LEN 64
854#endif
855
856/**
857 * @brief Maximum header field-name length (e.g. `"Content-Type"`).
858 *
859 * Must accommodate the longest header name the app needs to read by key.
860 * Standard names reach 30+ chars (`Sec-WebSocket-Extensions` = 24,
861 * `Access-Control-Request-Headers` = 30), so the default leaves margin; an
862 * over-long key is truncated (not rejected) by the parser.
863 */
864#ifndef MAX_KEY_LEN
865#define MAX_KEY_LEN 32
866#endif
867
868/** @brief Maximum header field-value length. */
869#ifndef MAX_VAL_LEN
870#define MAX_VAL_LEN 48
871#endif
872
873/** @brief Maximum raw query-string length (everything after `?`). */
874#ifndef MAX_QUERY_LEN
875#define MAX_QUERY_LEN 128
876#endif
877
878/** @brief Maximum number of parsed query-string parameters. */
879#ifndef MAX_QUERY_PARAMS
880#define MAX_QUERY_PARAMS 8
881#endif
882
883/** @brief Maximum number of `:name` path parameters captured per route match. */
884#ifndef MAX_PATH_PARAMS
885#define MAX_PATH_PARAMS 4
886#endif
887
888/**
889 * @brief Capacity for the full `Authorization` header value (Digest auth).
890 *
891 * A Digest `Authorization` header (username, realm, nonce, uri, response,
892 * qop, nc, cnonce) is far longer than MAX_VAL_LEN, so when PC_ENABLE_AUTH
893 * is set the parser captures it whole into a dedicated per-request buffer.
894 */
895#ifndef DIGEST_AUTH_HDR_MAX
896#define DIGEST_AUTH_HDR_MAX 384
897#endif
898
899/**
900 * @brief Lifetime of a Digest `nonce`, in milliseconds (default 5 minutes).
901 *
902 * The server mints a stateless, keyed, timestamped nonce (RFC 7616 3.3) rather
903 * than a fixed one: each challenge carries the issue time plus a MAC over the
904 * server secret, so no per-nonce table is needed. A client `Authorization` whose
905 * nonce is older than this window is treated as @c stale - the credentials are
906 * re-checked and, if correct, the server reissues a fresh challenge with
907 * `stale=true` so the client retries transparently (no re-prompt). This bounds
908 * how long a captured Digest response can be replayed without any server-side
909 * state, which the shared-nothing worker model could not hold safely.
910 */
911#ifndef PC_DIGEST_NONCE_LIFETIME_MS
912#define PC_DIGEST_NONCE_LIFETIME_MS (5u * 60u * 1000u)
913#endif
914
915/** @brief Maximum query-parameter key length. */
916#ifndef QUERY_KEY_LEN
917#define QUERY_KEY_LEN 24
918#endif
919
920/** @brief Maximum query-parameter value length. */
921#ifndef QUERY_VAL_LEN
922#define QUERY_VAL_LEN 48
923#endif
924
925/**
926 * @brief Maximum request body bytes stored in `HttpReq::body`.
927 *
928 * Bodies larger than this trigger a 413 Payload Too Large response -
929 * the parser detects the overflow via `Content-Length` before any body
930 * bytes arrive, so no data is read or stored for oversized requests.
931 */
932#ifndef BODY_BUF_SIZE
933#define BODY_BUF_SIZE 256
934#endif
935
936/** @brief Maximum simultaneously registered routes. */
937#ifndef MAX_ROUTES
938#define MAX_ROUTES 16
939#endif
940
941/**
942 * @brief Maximum globally-registered middleware functions.
943 *
944 * The middleware chain is a fixed array of function pointers run in
945 * registration order before a request reaches its route handler (see
946 * PC::use()). Costs MAX_MIDDLEWARE pointers of BSS; an empty chain
947 * adds no per-request work.
948 */
949#ifndef MAX_MIDDLEWARE
950#define MAX_MIDDLEWARE 4
951#endif
952
953/**
954 * @brief Per-chunk staging buffer for send_chunked()'s ChunkSource (max bytes a
955 * source produces per call, hence the largest single chunk on the wire).
956 *
957 * Allocated on the worker stack only while a chunk is being framed - no persistent
958 * RAM cost. The pump asks the source for at most this many bytes (or fewer when the
959 * send window is smaller), so it bounds the chunk size, not the total body.
960 *
961 * Sized to one TCP segment (~MSS): the pump frames + sends each chunk in a single
962 * tcpip_thread round-trip (~23 us on-device), so a bigger chunk = fewer round-trips per
963 * byte. 1440 keeps the framed chunk within one segment; raise it (up to the send window)
964 * to cut the round-trip count further on a fast transport (e.g. Ethernet), at more stack.
965 */
966#ifndef CHUNK_BUF_SIZE
967#define CHUNK_BUF_SIZE 1440
968#endif
969
970/**
971 * @brief Maximum object/array nesting depth for the JsonWriter (see json.h).
972 *
973 * Bounds the writer's per-level comma-tracking stack (one bool per level);
974 * begin_object()/begin_array() beyond this fail the writer instead of
975 * overflowing. No heap; ~JSON_MAX_DEPTH bytes of stack inside the writer object.
976 */
977#ifndef JSON_MAX_DEPTH
978#define JSON_MAX_DEPTH 8
979#endif
980
981/**
982 * @brief Step budget for the regex route matcher (see on_regex()).
983 *
984 * The matcher is a bounded backtracker: it counts match steps and fails closed
985 * (no match) once this budget is exhausted, so a pathological pattern can never
986 * backtrack unboundedly. Keeps regex routing deterministic. Routing patterns hit
987 * only a handful of steps; the default leaves wide margin.
988 */
989#ifndef RE_MAX_STEPS
990#define RE_MAX_STEPS 2000
991#endif
992
993// ---------------------------------------------------------------------------
994// WebSocket sizing constants
995// ---------------------------------------------------------------------------
996
997/**
998 * @brief Maximum simultaneous WebSocket connections.
999 *
1000 * Each connection occupies one TCP slot from MAX_CONNS and one entry in
1001 * ws_pool[]. MAX_WS_CONNS + MAX_SSE_CONNS must not exceed MAX_CONNS.
1002 */
1003#ifndef MAX_WS_CONNS
1004#define MAX_WS_CONNS 2
1005#endif
1006
1007/**
1008 * @brief Maximum WebSocket frame payload in bytes.
1009 *
1010 * Frames larger than this are rejected with Close code 1009 (Message Too Big).
1011 * Fragmented messages are not supported; each message must fit in one frame.
1012 */
1013#ifndef WS_FRAME_SIZE
1014#define WS_FRAME_SIZE 512
1015#endif
1016
1017// ---------------------------------------------------------------------------
1018// Server-Sent Events sizing constants
1019// ---------------------------------------------------------------------------
1020
1021/**
1022 * @brief Maximum simultaneous SSE connections.
1023 *
1024 * Each connection occupies one TCP slot from MAX_CONNS and one entry in
1025 * pc_sse_pool[]. MAX_WS_CONNS + MAX_SSE_CONNS must not exceed MAX_CONNS.
1026 */
1027#ifndef MAX_SSE_CONNS
1028#define MAX_SSE_CONNS 2
1029#endif
1030
1031/**
1032 * @brief Output buffer size in bytes for a single SSE event.
1033 *
1034 * An event larger than this is silently truncated. The buffer holds the
1035 * formatted `data: ...\n\n` line before it is handed to tcp_write().
1036 */
1037#ifndef SSE_BUF_SIZE
1038#define SSE_BUF_SIZE 256
1039#endif
1040
1041// ---------------------------------------------------------------------------
1042// Static file serving sizing constants
1043// ---------------------------------------------------------------------------
1044
1045/**
1046 * @brief Bytes read from the filesystem and passed to tcp_write() per loop().
1047 *
1048 * Each read+send is one tcpip_thread round-trip (~23 us on-device), so a larger chunk =
1049 * fewer round-trips per byte (better throughput on a fast transport), at more peak stack.
1050 * Must be <= RX_BUF_SIZE to avoid stalling the TCP send window; 1024 tracks the default
1051 * RX_BUF_SIZE. Lower it (e.g. -DFILE_CHUNK_SIZE=512) on a stack-constrained target.
1052 */
1053#ifndef FILE_CHUNK_SIZE
1054#define FILE_CHUNK_SIZE 1024
1055#endif
1056
1057// ---------------------------------------------------------------------------
1058// Basic Auth sizing constants
1059// ---------------------------------------------------------------------------
1060
1061/**
1062 * @brief Maximum username or password length for HTTP Basic Authentication.
1063 *
1064 * Both username and password must fit in this many bytes including the
1065 * null terminator. Longer credentials are silently rejected with 401.
1066 */
1067#ifndef MAX_AUTH_LEN
1068#define MAX_AUTH_LEN 32
1069#endif
1070
1071// ---------------------------------------------------------------------------
1072// Multipart form-data sizing constants
1073// ---------------------------------------------------------------------------
1074
1075/**
1076 * @brief Maximum simultaneously parsed multipart parts per request.
1077 *
1078 * Parts beyond this limit are silently ignored. A typical upload form
1079 * has 1-4 fields; increase this for forms with more.
1080 */
1081#ifndef MAX_MULTIPART_PARTS
1082#define MAX_MULTIPART_PARTS 4
1083#endif
1084
1085/**
1086 * @brief Maximum MIME boundary length (RFC 2046 allows up to 70 characters).
1087 */
1088#ifndef MAX_BOUNDARY_LEN
1089#define MAX_BOUNDARY_LEN 72
1090#endif
1091
1092// ---------------------------------------------------------------------------
1093// Event queue depth
1094// ---------------------------------------------------------------------------
1095
1096/**
1097 * @brief Depth of the FreeRTOS event queue shared between lwIP callbacks and
1098 * the main-loop task.
1099 *
1100 * Each slot holds one TcpEvt (8 bytes). The queue is the only heap
1101 * allocation the library makes at begin() time:
1102 *
1103 * heap = sizeof(StaticQueue_t) + EVT_QUEUE_DEPTH * sizeof(TcpEvt)
1104 *
1105 * Must be large enough to absorb a burst of MAX_CONNS * 4 events without
1106 * blocking the lwIP thread, so it tracks MAX_CONNS automatically (a raised
1107 * MAX_CONNS never trips the EVT_QUEUE_DEPTH >= MAX_CONNS * 4 guard below).
1108 */
1109#ifndef EVT_QUEUE_DEPTH
1110#define EVT_QUEUE_DEPTH (MAX_CONNS * 4)
1111#endif
1112
1113// ---------------------------------------------------------------------------
1114// Internal response buffer sizing constants
1115// ---------------------------------------------------------------------------
1116
1117/**
1118 * @brief Stack buffer for HTTP response header lines in send() / send_empty() /
1119 * send_unauth() / serve_file().
1120 *
1121 * Must be large enough to hold the status line, Content-Type, Content-Length,
1122 * Connection, and any CORS headers. The CORS block alone can reach
1123 * CORS_HDR_BUF_SIZE bytes, so this value should be at least
1124 * CORS_HDR_BUF_SIZE + 96.
1125 */
1126#ifndef RESP_HDR_BUF_SIZE
1127#define RESP_HDR_BUF_SIZE 768
1128#endif
1129
1130/**
1131 * @brief Per-connection buffer for app-supplied custom response headers and
1132 * cookies.
1133 *
1134 * Filled by add_response_header() / set_cookie() and injected into send() /
1135 * send_empty() / redirect() the same way the CORS block is. RESP_HDR_BUF_SIZE
1136 * must be large enough to hold the status line plus the CORS block plus this
1137 * block (see the assert below).
1138 */
1139#ifndef EXTRA_HDR_BUF_SIZE
1140#define EXTRA_HDR_BUF_SIZE 256
1141#endif
1142
1143/**
1144 * @brief Stack buffer for the HTTP 101 Switching Protocols response sent during
1145 * the WebSocket handshake.
1146 *
1147 * Must hold: status line + Upgrade + Connection + Sec-WebSocket-Accept (28
1148 * base64 chars) + CRLF pairs. Minimum is ~120 bytes; default leaves margin.
1149 */
1150#ifndef WS_HDR_BUF_SIZE
1151#define WS_HDR_BUF_SIZE 256
1152#endif
1153
1154/**
1155 * @brief Size of the pre-built CORS header block stored in PC.
1156 *
1157 * Built once by set_cors() and injected into every response. Must hold
1158 * Access-Control-Allow-Origin, Access-Control-Allow-Methods, and
1159 * Access-Control-Allow-Headers lines for the configured origin.
1160 */
1161#ifndef CORS_HDR_BUF_SIZE
1162#define CORS_HDR_BUF_SIZE 192
1163#endif
1164
1165/**
1166 * @brief Size of the optional Cache-Control header line stored in PC.
1167 *
1168 * Built once by set_cache_control() and injected into static-file responses
1169 * (serve_file / serve_static) beside the ETag. Holds "Cache-Control: <value>\r\n".
1170 */
1171#ifndef CACHE_CONTROL_BUF_SIZE
1172#define CACHE_CONTROL_BUF_SIZE 64
1173#endif
1174
1175// ---------------------------------------------------------------------------
1176// Feature flags
1177// ---------------------------------------------------------------------------
1178// Set any of these to 0 in your sketch BEFORE including this library to strip
1179// the feature from the build entirely (no code, no RAM, no flash cost).
1180//
1181// #define PC_ENABLE_WEBSOCKET 0
1182// #include <protocore.h>
1183//
1184// ---------------------------------------------------------------------------
1185// BUILD-FLAG DEPENDENCY TREE
1186// ---------------------------------------------------------------------------
1187// Most features are independent. A few build on another feature and cannot
1188// compile without it; those HARD dependencies are enforced near the bottom of
1189// this file with a clear #error, so an illegal combination fails fast at
1190// compile time instead of producing a cryptic linker error. Enable a child
1191// only together with its parent(s).
1192//
1193// Hard dependencies (child requires parent):
1194//
1195// FILE_SERVING
1196// |-- WEBDAV
1197// `-- RANGE
1198// TLS
1199// |-- MTLS
1200// |-- TLS_RESUMPTION
1201// |-- HTTP_CLIENT_TLS (also requires HTTP_CLIENT)
1202// |-- MQTT_TLS (also requires MQTT)
1203// `-- WS_CLIENT_TLS (also requires WS_CLIENT)
1204// WEBSOCKET
1205// |-- WS_DEFLATE
1206// `-- WEB_TERMINAL
1207// SSE
1208// `-- DASHBOARD
1209// STATS
1210// `-- METRICS
1211// AUTH
1212// `-- AUTH_LOCKOUT
1213// SNMP
1214// |-- SnmpVersion::SNMP_V3
1215// `-- SNMP_TRAP
1216// COAP
1217// |-- COAP_OBSERVE
1218// `-- COAP_BLOCK
1219// OPCUA
1220// `-- OPCUA_CLIENT
1221// CONFIG_STORE
1222// `-- CONFIG_IO
1223//
1224// Optional integrations (these build fine on their own; the named feature is
1225// simply inert or reduced until you also enable the other flag):
1226//
1227// WEBHOOK + HTTP_CLIENT : without it, pc_webhook_post() returns -1
1228// OAUTH2 + HTTP_CLIENT : the token-endpoint POST helpers compile only with it
1229// DASHBOARD + WEBSOCKET : adds live control widgets; the SSE value stream works alone
1230//
1231// Auto-derived (do NOT set these yourself; the library computes them):
1232//
1233// STREAM_BODY = OTA || UPLOAD
1234// CLIENT_TLS = HTTP_CLIENT_TLS || MQTT_TLS || WS_CLIENT_TLS
1235// CAPTURE_AUTH_HEADER = AUTH || JWT || OIDC
1236//
1237// The same tree appears in README.md and examples/Foundation/Configuration.
1238// ---------------------------------------------------------------------------
1239
1240/** @brief WebSocket support (RFC 6455 framing + SHA-1/base64 handshake). */
1241#ifndef PC_ENABLE_WEBSOCKET
1242#define PC_ENABLE_WEBSOCKET 1
1243#endif
1244
1245/**
1246 * @brief WebSocket permessage-deflate (RFC 7692) - bidirectional compression.
1247 *
1248 * When set (and PC_ENABLE_WEBSOCKET is on), the server negotiates the
1249 * `permessage-deflate` extension and both decompresses inbound compressed (RSV1)
1250 * messages via a bounded INFLATE (network_drivers/presentation/inflate.*) and
1251 * compresses outbound data frames via a bounded DEFLATE
1252 * (network_drivers/presentation/deflate.*); both borrow their table scratch from
1253 * the shared per-dispatch arena. The extension is negotiated with
1254 * `{client,server}_no_context_takeover` so every message (de)compresses
1255 * independently - no window is carried between messages. An outbound frame that
1256 * would not shrink is sent uncompressed (the per-message RSV1 flag permits this).
1257 * Default off.
1258 */
1259#ifndef PC_ENABLE_WS_DEFLATE
1260#define PC_ENABLE_WS_DEFLATE 0
1261#endif
1262
1263/**
1264 * @brief WebSocket outbound fragmentation size (RFC 6455 sec 5.4), in payload bytes. 0 = off.
1265 *
1266 * When >0, an outbound data message (text/binary) longer than this many payload bytes is split into
1267 * that-sized WebSocket frames - the first carrying the opcode (and the RFC 7692 RSV1 bit if the message
1268 * is compressed), the rest CONTINUATION, the last with FIN - instead of one large frame. Sizing it near
1269 * the TCP MSS (e.g. 1400) keeps each frame within whole segments (MTU-aligned) and lets a peer with a
1270 * bounded per-frame reassembly buffer receive an arbitrarily long message. The runtime override is
1271 * ws_set_frag_size(). Compression applies to the whole message first, then the compressed bytes are
1272 * split. Default 0 (one frame per message, unchanged).
1273 */
1274#ifndef PC_WS_FRAG_SIZE
1275#define PC_WS_FRAG_SIZE 0
1276#endif
1277
1278/** @brief Server-Sent Events push support. */
1279#ifndef PC_ENABLE_SSE
1280#define PC_ENABLE_SSE 1
1281#endif
1282
1283/** @brief multipart/form-data body parser. */
1284#ifndef PC_ENABLE_MULTIPART
1285#define PC_ENABLE_MULTIPART 1
1286#endif
1287
1288/**
1289 * @brief Zero-heap CBOR (RFC 8949) encoder for compact binary payloads.
1290 *
1291 * Default off. When set, network_drivers/presentation/codec/cbor/cbor.h provides a writer
1292 * that serializes ints, strings, byte strings, arrays, maps, booleans, null, and
1293 * float32 into a caller-provided buffer - a compact binary alternative to the JSON
1294 * writer for telemetry. Pure, no heap, host-tested against the RFC 8949 vectors.
1295 */
1296#ifndef PC_ENABLE_CBOR
1297#define PC_ENABLE_CBOR 0
1298#endif
1299
1300/**
1301 * @brief Zero-heap MessagePack encoder and decoder for compact binary payloads.
1302 *
1303 * Default off. When set, network_drivers/presentation/codec/msgpack/msgpack.h provides a
1304 * writer that serializes ints, strings, byte strings, arrays, maps, booleans, nil,
1305 * and float32 into a caller-provided buffer, plus a cursor decoder (pc_msgpack_peek /
1306 * pc_msgpack_read_*, no-copy strings) over a caller buffer - the MessagePack-format
1307 * sibling of the CBOR / JSON readers and writers. Pure, no heap, host-tested
1308 * against the spec encodings and round-trip.
1309 */
1310#ifndef PC_ENABLE_MSGPACK
1311#define PC_ENABLE_MSGPACK 0
1312#endif
1313
1314/** @brief Static file serving via Arduino FS (LittleFS, SPIFFS, SD). */
1315#ifndef PC_ENABLE_FILE_SERVING
1316#define PC_ENABLE_FILE_SERVING 1
1317#endif
1318
1319/**
1320 * @brief WebDAV server (RFC 4918, class 1 + advisory locks) over the file system.
1321 *
1322 * Default off. When set (requires PC_ENABLE_FILE_SERVING), dav() mounts an FS
1323 * subtree that answers the WebDAV methods - OPTIONS, PROPFIND (Depth 0/1),
1324 * PROPPATCH, GET, HEAD, PUT, DELETE, MKCOL, COPY, MOVE, and advisory LOCK/UNLOCK -
1325 * so a client (rclone, cadaver, curl, or a mounted network drive) can browse and
1326 * edit files. PROPFIND returns a 207 Multi-Status document built into a fixed
1327 * buffer (PC_WEBDAV_BUF_SIZE); a Depth-1 listing is capped at
1328 * PC_WEBDAV_MAX_ENTRIES children. PROPPATCH returns a 207 with each requested
1329 * property refused 403 Forbidden (the live properties are read-only, no dead-
1330 * property store) - this keeps Windows Explorer / macOS Finder, which PROPPATCH a
1331 * timestamp right after a PUT, from erroring on a 405. PUT streams the request
1332 * body straight to the file (via the shared streaming-body sink), so an upload is
1333 * not bounded by BODY_BUF_SIZE. Locks are advisory (a synthetic token is issued
1334 * but not enforced). See docs/SECURITY.md before exposing it.
1335 */
1336#ifndef PC_ENABLE_WEBDAV
1337#define PC_ENABLE_WEBDAV 0
1338#endif
1339
1340/** @brief Buffer (BSS) for a WebDAV 207 Multi-Status response, in bytes (see PC_ENABLE_WEBDAV). */
1341#ifndef PC_WEBDAV_BUF_SIZE
1342#define PC_WEBDAV_BUF_SIZE 2048
1343#endif
1344
1345/** @brief Maximum children listed in a WebDAV Depth-1 PROPFIND (bounds the response). */
1346#ifndef PC_WEBDAV_MAX_ENTRIES
1347#define PC_WEBDAV_MAX_ENTRIES 32
1348#endif
1349
1350/** @brief Maximum properties echoed in a WebDAV PROPPATCH 207 response (bounds the response). */
1351#ifndef PC_WEBDAV_MAX_PROPS
1352#define PC_WEBDAV_MAX_PROPS 16
1353#endif
1354
1355/**
1356 * @brief HTTP method-token buffer size (bytes, including the NUL).
1357 *
1358 * Sized for the longest method the server must recognize: 8 normally (OPTIONS),
1359 * grown to fit the WebDAV methods (PROPPATCH is 9 chars) when WebDAV is enabled.
1360 */
1361#ifndef PC_METHOD_BUF_SIZE
1362#if PC_ENABLE_WEBDAV
1363#define PC_METHOD_BUF_SIZE 12
1364#else
1365#define PC_METHOD_BUF_SIZE 8
1366#endif
1367#endif
1368
1369/** @brief HTTP Basic Authentication per-route. */
1370#ifndef PC_ENABLE_AUTH
1371#define PC_ENABLE_AUTH 1
1372#endif
1373
1374/** @brief Telnet server support (RFC 854 / IAC option negotiation). */
1375#ifndef PC_ENABLE_TELNET
1376#define PC_ENABLE_TELNET 0
1377#endif
1378
1379/** @brief SSH server support (RFC 4253/4252/4254). */
1380#ifndef PC_ENABLE_SSH
1381#define PC_ENABLE_SSH 0
1382#endif
1383
1384/**
1385 * @brief Outbound SSH client + reverse tunnel (RFC 4254 §7.1 tcpip-forward, the `ssh -R` seam).
1386 *
1387 * The device dials OUT to a relay as the SSH *client* and asks it to forward a port back, so a device
1388 * behind NAT stays reachable. Reuses the transport crypto (curve25519 / ecdh-p256 / dh-group14 KEX;
1389 * ssh-ed25519 / rsa / ecdsa host-key verify; chacha / aes-gcm / aes-ctr) and the role-aware packet
1390 * layer. Needs the outbound TCP client transport. Default off.
1391 */
1392#ifndef PC_ENABLE_SSH_CLIENT
1393#define PC_ENABLE_SSH_CLIENT 0
1394#endif
1395
1396/**
1397 * @brief Post-quantum hybrid key exchange: ML-KEM-768 + X25519 (FIPS 203 / RFC 9370 combiner).
1398 *
1399 * Adds the mlkem768x25519-sha256 SSH KEX method (draft-ietf-sshm-mlkem-hybrid-kex) and the
1400 * X25519MLKEM768 TLS 1.3 group (IANA 0x11ec) for HTTP/3, so a PQC-capable peer (OpenSSH 9.x+ and
1401 * current browsers, which now DEFAULT to hybrid) negotiates a quantum-resistant handshake instead of
1402 * down-negotiating to classical X25519. The device is always the responder, so only ML-KEM Encaps
1403 * ships (no KeyGen/Decaps, so none of the constant-time FO-comparison surface). The NTT core is
1404 * software with Montgomery reduction over q=3329 (the MPI accelerator is for RSA/DH-sized operands,
1405 * not 12-bit coefficients). Requires PC_ENABLE_SSH and/or PC_ENABLE_HTTP3.
1406 */
1407#ifndef PC_ENABLE_PQC_KEX
1408#define PC_ENABLE_PQC_KEX 0
1409#endif
1410
1411/**
1412 * @brief Streamlined NTRU Prime sntrup761x25519-sha512@openssh.com SSH KEX (default: tracks
1413 * ::PC_ENABLE_PQC_KEX).
1414 *
1415 * A second PQ/T hybrid alongside ML-KEM: sntrup761 (a lattice KEM with a conservative security
1416 * margin, OpenSSH's long-standing default) crossed with X25519, SHA-512 exchange hash. On by
1417 * default wherever the PQC hybrid is enabled so a PQC-capable peer gets both methods offered.
1418 * It is heavier than ML-KEM on the worker stack - the server runs Encaps (~22 KB peak) and the
1419 * reverse-SSH client runs KeyGen+Decaps (the FO re-encrypt peaks ~32 KB) - so a footprint-bound
1420 * PQC build (e.g. a classic-ESP32 that only wants ML-KEM) can set this to 0 to drop sntrup761 and
1421 * keep the lighter ::PC_WORKER_STACK_PQC_MIN floor. Requires ::PC_ENABLE_PQC_KEX.
1422 */
1423#ifndef PC_ENABLE_SSH_SNTRUP761
1424#define PC_ENABLE_SSH_SNTRUP761 PC_ENABLE_PQC_KEX
1425#endif
1426
1427/**
1428 * @brief Modbus TCP slave/server (Modbus Application Protocol v1.1b3) on TCP/502.
1429 *
1430 * Default off. When set, listen(502, ConnProto::PROTO_MODBUS) serves a fixed data model
1431 * (coils, discrete inputs, holding + input registers, all in BSS) over Modbus
1432 * TCP: Read/Write Coils (FC 1/5/15), Read Discrete Inputs (FC 2), Read/Write
1433 * Holding Registers (FC 3/6/16), and Read Input Registers (FC 4). The codec
1434 * (MBAP framing + PDU dispatch) is pure and host-tested; the TCP transport is
1435 * ESP32-only. The application reads/writes the model with the accessor functions
1436 * and is notified of client writes via pc_modbus_on_write(). Modbus has no
1437 * authentication or encryption - run it only on a trusted control network.
1438 */
1439#ifndef PC_ENABLE_MODBUS
1440#define PC_ENABLE_MODBUS 0
1441#endif
1442
1443/**
1444 * @brief Modbus RTU framing (serial / RS-485) over the same data model + PDU dispatch.
1445 *
1446 * Default off; implies PC_ENABLE_MODBUS. Adds the RTU ADU codec
1447 * `pc_modbus_rtu_process_adu()` - a `[slave addr][PDU][CRC16]` frame (CRC16-Modbus,
1448 * little-endian) around the existing host-tested PDU dispatch: a CRC mismatch or a
1449 * non-matching unit address is dropped silently (no reply, per the spec), and a
1450 * broadcast (address 0) is executed without a reply. The codec is pure and
1451 * host-tested; feed it from a UART/RS-485 driver (the serial transport is the
1452 * application's, framed by the 3.5-char inter-frame idle).
1453 */
1454#ifndef PC_ENABLE_MODBUS_RTU
1455#define PC_ENABLE_MODBUS_RTU 0
1456#endif
1457// RTU is a framing over the same PDU codec, so it needs Modbus compiled in. The requirement is
1458// derived rather than written back over PC_ENABLE_MODBUS: a user's flag is an input and stays one,
1459// so -DPC_ENABLE_MODBUS=0 keeps meaning what it says. Code guards on PC_NEED_MODBUS.
1460#define PC_NEED_MODBUS (PC_ENABLE_MODBUS || PC_ENABLE_MODBUS_RTU)
1461
1462/**
1463 * @brief CloudEvents v1.0 (CNCF) event envelope (structured JSON + binary headers).
1464 *
1465 * Default off. Adds `services/cloudevents`: `pc_cloudevents_build_json()` emits a
1466 * structured `application/cloudevents+json` envelope (required `id`/`source`/`type`
1467 * + optional `subject`/`datacontenttype`/`data`) via the JSON writer, and
1468 * `pc_cloudevents_from_headers()` reads an inbound binary-mode event's `ce-*` headers.
1469 * Makes the device's events interoperable with serverless / event-mesh consumers.
1470 */
1471#ifndef PC_ENABLE_CLOUDEVENTS
1472#define PC_ENABLE_CLOUDEVENTS 0
1473#endif
1474
1475/**
1476 * @brief Redis RESP2/RESP3 wire codec (`services/iot/redis_resp`).
1477 *
1478 * Default off. A zero-heap command encoder (a command becomes a RESP array of bulk strings) and a
1479 * streaming reply decoder that reads one value at a time, so an arbitrarily nested reply is walked
1480 * with only the caller's loop state and never a tree allocation. Covers RESP2 and the RESP3
1481 * additions (null / boolean / double / big number / bulk error / verbatim / map / set / push).
1482 * Pure codec - you hand it byte buffers; the connection is the application's. Host-tested against
1483 * the spec vectors and a real redis-server.
1484 */
1485#ifndef PC_ENABLE_REDIS
1486#define PC_ENABLE_REDIS 0
1487#endif
1488
1489/**
1490 * @brief STOMP 1.2 frame codec (`services/stomp`).
1491 *
1492 * Default off. A zero-heap frame builder (`pc_stomp_build_frame`, command + escaped
1493 * headers + NUL-terminated body) + a non-mutating parser (`pc_stomp_parse_frame`,
1494 * command / header slices / body, honoring `content-length`) so the device can talk
1495 * to a STOMP broker (ActiveMQ / RabbitMQ / Artemis) over the shipped outbound client
1496 * transport, or STOMP-over-WebSocket via the WS client. Pure codec, host-tested.
1497 */
1498#ifndef PC_ENABLE_STOMP
1499#define PC_ENABLE_STOMP 0
1500#endif
1501
1502/** @brief Max header lines parsed per STOMP frame (extras beyond this are ignored). */
1503#ifndef PC_STOMP_MAX_HEADERS
1504#define PC_STOMP_MAX_HEADERS 16
1505#endif
1506
1507/**
1508 * @brief MQTT-SN v1.2 wire codec (`services/iot/mqtt/mqtt_sn`).
1509 *
1510 * Default off. A zero-heap message builder + parser for MQTT for Sensor Networks - the
1511 * UDP / non-TCP MQTT variant for constrained, lossy links (numeric topic IDs instead of
1512 * strings, gateway discovery, sleeping-client keep-alive). Builds CONNECT / REGISTER /
1513 * PUBLISH / SUBSCRIBE / PINGREQ / DISCONNECT / SEARCHGW and parses CONNACK / REGACK /
1514 * PUBACK / SUBACK / PUBLISH / REGISTER, including the 1- and 3-octet Length forms. Pure
1515 * codec, host-tested; the datagram send (pc_udp_sendto) and topic registry are the app's.
1516 */
1517#ifndef PC_ENABLE_MQTT_SN
1518#define PC_ENABLE_MQTT_SN 0
1519#endif
1520
1521/**
1522 * @brief Flow-record export codec (`services/flow_export`).
1523 *
1524 * Default off. A zero-heap exporter-side codec for on-device flow accounting: NetFlow v5
1525 * (fixed 24-octet header + 48-octet records), NetFlow v9 (RFC 3954), and IPFIX (RFC 7011),
1526 * the latter two via a small cursor that emits a Template then matching Data records and
1527 * patches the message length (IPFIX) or record count (v9) on finish. Pure codec,
1528 * host-tested; the flow cache (5-tuple + counters) and the UDP send (pc_udp_sendto) are
1529 * the application's. Pairs with the telemetry / observability services.
1530 */
1531#ifndef PC_ENABLE_FLOW_EXPORT
1532#define PC_ENABLE_FLOW_EXPORT 0
1533#endif
1534
1535/**
1536 * @brief Protocol Buffers wire codec (`services/protobuf`).
1537 *
1538 * Default off. A zero-heap streaming Protobuf encoder + cursor reader over caller buffers
1539 * (the same shape as the CBOR / MessagePack codecs): varint / ZigZag / fixed32 / fixed64 /
1540 * length-delimited fields, with embedded messages built into a sub-buffer and added via
1541 * `pc_pb_bytes`. Pure codec, host-tested against the spec vectors. This is the standalone
1542 * Protobuf deliverable; gRPC (framed Protobuf over HTTP/2) is gated on the HTTP/2 item.
1543 */
1544#ifndef PC_ENABLE_PROTOBUF
1545#define PC_ENABLE_PROTOBUF 0
1546#endif
1547
1548/**
1549 * @brief WAMP messaging codec (`services/wamp`).
1550 *
1551 * Default off. A zero-heap codec for the Web Application Messaging Protocol (unified RPC +
1552 * PubSub over WebSocket): builders for HELLO / SUBSCRIBE / PUBLISH / CALL / REGISTER /
1553 * YIELD / GOODBYE (JSON arrays emitted via the shared JsonWriter) and a positional parser
1554 * that pulls the message type, ids, and URIs out of an inbound array. Rides the shipped
1555 * WebSocket layer; the session / subscription / registration tables are the application's.
1556 * Pure codec, host-tested. Builds on the always-on JSON writer.
1557 */
1558#ifndef PC_ENABLE_WAMP
1559#define PC_ENABLE_WAMP 0
1560#endif
1561
1562/**
1563 * @brief SunSpec Modbus device-information-model codec (`services/sunspec`).
1564 *
1565 * Default off. A zero-heap codec for the SunSpec Alliance register maps layered on the
1566 * holding-register model: a model-chain walker (verify the `SunS` marker, then iterate each
1567 * model's id / length / body) + typed point readers (u16 / i16 / u32 / i32 / string) and a
1568 * map writer (marker, model headers + points, end model). Makes a solar inverter / meter /
1569 * battery interoperable. Pure codec, host-tested; pairs with the Modbus service.
1570 */
1571#ifndef PC_ENABLE_SUNSPEC
1572#define PC_ENABLE_SUNSPEC 0
1573#endif
1574
1575/**
1576 * @brief IEEE C37.118.2 synchrophasor frame codec (`services/c37118`).
1577 *
1578 * Default off. A zero-heap builder + CRC-validating parser for the PMU / PDC wide-area
1579 * measurement wire protocol: `pc_c37118_build_frame` / `pc_c37118_build_command` emit a
1580 * `SYNC FRAMESIZE IDCODE SOC FRACSEC DATA CHK` frame (CHK = CRC-CCITT) and
1581 * `pc_c37118_parse_frame` validates the CRC and reports the frame type / ids / timestamp /
1582 * payload slice. Frames any payload and fully handles the fixed Command frame. Pure codec,
1583 * host-tested.
1584 */
1585#ifndef PC_ENABLE_C37118
1586#define PC_ENABLE_C37118 0
1587#endif
1588
1589/**
1590 * @brief DNP3 (IEEE 1815) data-link frame codec (`services/dnp3`).
1591 *
1592 * Default off. A zero-heap builder + CRC-validating parser for the SCADA / utility
1593 * outstation data-link layer: `pc_dnp3_build_frame` emits the `0x0564 LEN CTRL DEST SRC CRC`
1594 * header block + the CRC'd 16-octet user-data blocks, and `pc_dnp3_parse_frame` validates the
1595 * header and every block CRC (CRC-16/DNP) and de-blocks the user data. Pure codec,
1596 * host-tested; the transport-function reassembly and the application layer are layered on
1597 * the de-blocked user data.
1598 */
1599#ifndef PC_ENABLE_DNP3
1600#define PC_ENABLE_DNP3 0
1601#endif
1602
1603/**
1604 * @brief CANopen (CiA 301) message codec (`services/canopen`).
1605 *
1606 * Default off. A zero-heap builder + parser for the CANopen messaging set over classic CAN
1607 * frames (`shared_primitives/can.h`): NMT node control, SYNC, TIME, heartbeat / boot-up,
1608 * EMCY, PDO process data, and expedited SDO read / write / abort. The 11-bit COB-ID is a
1609 * 4-bit function code plus a 7-bit node id; builders compute it, parsers classify it back.
1610 * The object dictionary is the application's; SDO is expedited only (segmented / block not
1611 * yet covered). Pure codec, host-tested. Drive it from the ESP32 TWAI peripheral or an
1612 * MCP2515 over SPI to bridge a CANopen bus onto Wi-Fi.
1613 */
1614#ifndef PC_ENABLE_CANOPEN
1615#define PC_ENABLE_CANOPEN 0
1616#endif
1617
1618/**
1619 * @brief CiA 402 / IEC 61800-7-201 drive + motion profile (`services/cia402`).
1620 *
1621 * Default off. Requires CANOPEN. The standardized servo / stepper drive profile over CANopen:
1622 * `pc_cia402_state` decodes the power state machine from the Statusword (the CiA 402 mask/value
1623 * table), `pc_cia402_controlword` / `pc_cia402_enable_sequence` produce the Controlword commands that
1624 * walk an axis to Operation Enabled, and the `pc_cia402_sdo_set_*` / `pc_cia402_pack_command` helpers
1625 * write Controlword / Modes of Operation / target position-velocity-torque via the shipped
1626 * CANopen SDO / PDO codec. State masks + command values + object indices verified against
1627 * IEC 61800-7-201. Pure profile, host-tested. Turns the CAN stack into a motion master; close
1628 * the loop with a `services/control` PID.
1629 */
1630#ifndef PC_ENABLE_CIA402
1631#define PC_ENABLE_CIA402 0
1632#endif
1633
1634/**
1635 * @brief Closed-loop control law (`services/control`).
1636 *
1637 * Default off. A zero-heap, FPU-accelerated PID controller (single-precision float, FMA-folded,
1638 * IRAM-placeable with PC_CONTROL_IRAM=1) with derivative-on-measurement, an optional
1639 * derivative low-pass, output clamping, and anti-windup by back-calculation plus a hard integral
1640 * clamp, and a feed-forward term - plus inline control-law primitives (clamp / deadband / slew /
1641 * low-pass). `pid_update` runs one loop; `pid_update_n` runs a batch of axes off one tick. Pair
1642 * it with a plant it can command (a `services/cia402` drive, a dshot ESC, a heater) and tune the
1643 * gains offline with `tools/pid_tune.py`. Pure math, host-tested.
1644 */
1645#ifndef PC_ENABLE_CONTROL
1646#define PC_ENABLE_CONTROL 0
1647#endif
1648
1649/**
1650 * @brief SAE J1939 message codec (`services/j1939`).
1651 *
1652 * Default off. A zero-heap codec for the heavy-duty-vehicle / agriculture / marine / genset
1653 * CAN higher-layer protocol over 29-bit extended frames (`shared_primitives/can.h`):
1654 * `pc_j1939_encode_id` / `pc_j1939_decode_id` pack and unpack the priority / PGN / SA / DA
1655 * identifier (PDU1 peer + PDU2 broadcast), `pc_j1939_build_message` emits single frames,
1656 * `pc_j1939_build_request` / `pc_j1939_build_address_claim` (+ `pc_j1939_build_name`) handle the
1657 * Request PGN and Address Claimed messages, and the Transport Protocol (BAM announce +
1658 * TP.DT packets) reassembles multi-packet messages up to `PC_J1939_TP_MAX` octets. Pure
1659 * codec, host-tested. Drive it from the ESP32 TWAI peripheral or an MCP2515 over SPI.
1660 */
1661#ifndef PC_ENABLE_J1939
1662#define PC_ENABLE_J1939 0
1663#endif
1664
1665/**
1666 * @brief DeviceNet link-adaptation codec (`services/devicenet`).
1667 *
1668 * Default off. The CAN-specific layer of "CIP over CAN": the 11-bit DeviceNet identifier as a
1669 * Message Group (1..4) + Message ID + MAC ID (`pc_devicenet_encode_id` / `pc_devicenet_decode_id`),
1670 * the explicit-message header octet, single-frame explicit messages, and the fragmentation
1671 * protocol with a reassembler (`pc_devicenet_frag_feed`) for bodies longer than one CAN frame.
1672 * The CIP application layer (services / EPATH / data) is the same one EtherNet/IP uses, so
1673 * build the body with the existing `pc_cip_*` functions (`PC_ENABLE_CIP`). Pure codec,
1674 * host-tested. Drive it from the ESP32 TWAI peripheral or an MCP2515 over SPI.
1675 */
1676#ifndef PC_ENABLE_DEVICENET
1677#define PC_ENABLE_DEVICENET 0
1678#endif
1679
1680/**
1681 * @brief NMEA 2000 codec (`services/nmea2000`).
1682 *
1683 * Default off; implies PC_ENABLE_J1939 (NMEA 2000 is J1939 at the transport layer). A
1684 * zero-heap codec for the marine instrumentation network over CAN: it reuses the J1939 29-bit
1685 * identifier codec and adds the NMEA-specific Fast Packet transport - `pc_n2k_fastpacket_build_frame`
1686 * splits a 9..223-octet message across frames (a control octet of sequence + frame counter,
1687 * the first frame carrying the total length) and `pc_n2k_fastpacket_feed` reassembles it;
1688 * `pc_n2k_build_single` wraps a single-frame message. Pure codec, host-tested. Drive it from the
1689 * ESP32 TWAI peripheral or an MCP2515 over SPI to bridge an NMEA 2000 backbone onto Wi-Fi.
1690 */
1691#ifndef PC_ENABLE_NMEA2000
1692#define PC_ENABLE_NMEA2000 0
1693#endif
1694// NMEA 2000 reuses the J1939 identifier codec, so it needs J1939 compiled in.
1695#define PC_NEED_J1939 (PC_ENABLE_J1939 || PC_ENABLE_NMEA2000)
1696
1697/**
1698 * @brief Wired M-Bus (Meter-Bus, EN 13757) frame codec (`services/mbus`).
1699 *
1700 * Default off. A zero-heap builder + parser for the M-Bus link-layer frames used by utility
1701 * meters (water / gas / heat / electricity): the single-character ACK, the short frame
1702 * (`10 C A CS 16`), and the long / control frame (`68 L L 68 C A CI ... CS 16`, 8-bit sum
1703 * checksum), plus `pc_mbus_record_next` which walks the EN 13757-3 variable-data records
1704 * (DIF / VIF, skipping DIFE / VIFE extension chains and decoding the data length). Pure codec,
1705 * host-tested. Talk to the powered two-wire bus over a UART through an M-Bus level converter
1706 * (e.g. a TSS721-based master) and bridge meter readings onto Wi-Fi.
1707 */
1708#ifndef PC_ENABLE_MBUS
1709#define PC_ENABLE_MBUS 0
1710#endif
1711
1712/**
1713 * @brief IEC 60870-5-101 / -104 telecontrol (SCADA) codec (`services/iec60870`).
1714 *
1715 * Default off. The utility-SCADA protocol in both transports: the -104 APCI over TCP
1716 * (`68 LEN` + 4 control octets in I / S / U formats via `pc_iec104_build_i/_s/_u` + `pc_iec104_parse`),
1717 * the shared ASDU header + 3-octet Information Object Address (`pc_iec_asdu_build_header` /
1718 * `pc_iec_asdu_parse_header`, `pc_iec_put_ioa` / `pc_iec_get_ioa`), and the -101 FT1.2 serial link
1719 * frames (fixed + variable, 8-bit sum checksum, via `pc_iec101_build_fixed` / `_variable` +
1720 * `pc_iec101_parse`). Named type-id / cause-of-transmission constants are provided; the
1721 * per-type information elements are the application's. Pure codec, host-tested. Run -104 over
1722 * the shipped TCP stack or -101 over a UART/RS-485 transceiver to bridge an RTU onto Wi-Fi.
1723 */
1724#ifndef PC_ENABLE_IEC60870
1725#define PC_ENABLE_IEC60870 0
1726#endif
1727
1728/**
1729 * @brief SDI-12 sensor-bus codec (`services/sdi12`).
1730 *
1731 * Default off. A zero-heap command / response codec for the 1200-baud single-wire ASCII bus
1732 * used by environmental / agricultural sensors: builders for the standard commands
1733 * (`pc_sdi12_build_measure` / `_concurrent` / `_data` / `_identify` / `_change_address` /
1734 * `_query_address`), a parser for the measurement response (`pc_sdi12_parse_measure`: seconds
1735 * until ready + value count), a data-value splitter (`pc_sdi12_parse_values`), and the SDI-12
1736 * CRC (`pc_sdi12_crc16` / `pc_sdi12_crc_encode` / `pc_sdi12_check_crc`) for the CRC-protected `aMC!` /
1737 * `aCC!` variants. Pure codec, host-tested. Drive the single 1200-baud line over a UART (with
1738 * a small level / direction circuit) and bridge sensor readings onto Wi-Fi.
1739 */
1740#ifndef PC_ENABLE_SDI12
1741#define PC_ENABLE_SDI12 0
1742#endif
1743
1744/**
1745 * @brief DMX512 + RDM (ANSI E1.20) lighting codec (`services/dmx`).
1746 *
1747 * Default off. A zero-heap codec for stage / architectural lighting over RS-485: `pc_dmx_build` /
1748 * `pc_dmx_get_channel` assemble and read the positional DMX512 slot packet (a start code + up to
1749 * 512 channels), and the RDM (Remote Device Management) functions build / parse the addressed
1750 * management packet that shares the wire - `pc_rdm_build` / `pc_rdm_parse` with 48-bit source /
1751 * destination UIDs (`pc_rdm_uid`), a command class + parameter id, and the 16-bit additive
1752 * checksum (`pc_rdm_checksum`). Pure codec, host-tested. Drive a `MAX485`-class transceiver on a
1753 * UART (250 kbit/s, 8N2; the break is the application's) and bridge a lighting rig onto Wi-Fi.
1754 */
1755#ifndef PC_ENABLE_DMX
1756#define PC_ENABLE_DMX 0
1757#endif
1758
1759/**
1760 * @brief NMEA 0183 sentence codec (`services/nmea0183`).
1761 *
1762 * Default off. A zero-heap codec for the marine / GPS ASCII protocol (`$GPGGA,...*47`):
1763 * `pc_nmea0183_build` emits a sentence (adding the `$`, XOR checksum, and CR/LF), `pc_nmea0183_parse`
1764 * validates the `*HH` checksum and splits the comma-separated fields (deriving talker id +
1765 * sentence type from the address field), and `pc_nmea0183_field_float` / `_int` decode field
1766 * values. Sentence framing + checksum verified against the NMEA 0183 standard (the canonical
1767 * GGA example); pure and host-tested. GPS / marine receivers are cheap UART breakouts, so this
1768 * is a plain HardwareSerial link (4800 / 9600 baud); bridge position / wind / depth onto Wi-Fi.
1769 */
1770#ifndef PC_ENABLE_NMEA0183
1771#define PC_ENABLE_NMEA0183 0
1772#endif
1773
1774/**
1775 * @brief Wi-Fi roaming decision layer (`services/roaming`).
1776 *
1777 * The pure policy that decides whether and where to roam to a better access point, given the current
1778 * link RSSI, a candidate neighbor list (from an 802.11k neighbor report or a scan), and an optional
1779 * 802.11v BSS-Transition-Management hint from the network. `pc_roam_decide` fuses those into a
1780 * roam/stay decision with a target BSSID + channel and a reason (a disassociation-imminent BTM, a
1781 * network-suggested BTM, or a weak link with a clearly stronger candidate past a hysteresis margin).
1782 * Pure, stateless, host-tested; the actual scan / neighbor-report request and the 802.11r fast
1783 * transition that executes the decision live in the Wi-Fi supplicant.
1784 */
1785#ifndef PC_ENABLE_ROAMING
1786#define PC_ENABLE_ROAMING 0
1787#endif
1788
1789/**
1790 * @brief u-blox UBX binary GNSS protocol codec (`services/ubx`).
1791 *
1792 * The binary companion to NMEA 0183 that u-blox receivers speak on the same UART. `pc_ubx_build`
1793 * frames a message (sync chars B5 62, class/id, little-endian length, payload, 8-bit Fletcher
1794 * checksum), `pc_ubx_build_poll` emits a zero-length poll request, `pc_ubx_parse` validates one
1795 * frame, and `pc_ubx_stream_feed` demultiplexes UBX frames out of a mixed NMEA+UBX byte stream
1796 * (handing every non-UBX byte back for an NMEA line assembler). Pure and host-tested; pairs with
1797 * services/timing_position/nmea0183 for a full GNSS link (send config/poll as UBX, read fixes as either).
1798 */
1799#ifndef PC_ENABLE_UBX
1800#define PC_ENABLE_UBX 0
1801#endif
1802
1803/**
1804 * @brief PTP / IEEE 1588-2008 (PTPv2) message codec + slave clock math (`services/ptp`).
1805 *
1806 * The Precision Time Protocol disciplines LAN clocks to sub-microsecond accuracy. `pc_ptp_build_*`
1807 * / `pc_ptp_parse_*` frame and decode the PTPv2 wire format (34-octet common header, 10-octet
1808 * 48-bit-seconds+32-bit-ns timestamp, and the Sync / Delay_Req / Follow_Up / Delay_Resp / Announce
1809 * messages, all big-endian), and `pc_ptp_compute` derives an ordinary-clock slave's offset-from-
1810 * master and mean-path-delay from the four transfer timestamps. Pure and host-tested; the UDP
1811 * transport (event port 319, general port 320) and local timestamping are the application's. Example
1812 * Ptp is an ordinary-clock slave.
1813 */
1814#ifndef PC_ENABLE_PTP
1815#define PC_ENABLE_PTP 0
1816#endif
1817
1818/**
1819 * @brief IO-Link (SDCI, IEC 61131-9) data-link message codec (`services/iolink`).
1820 *
1821 * Default off. The point-to-point smart-sensor link's data-link message layer: the M-sequence
1822 * Control octet (`pc_iol_mc` + decoders), the checksum / type octet of a master message
1823 * (`pc_iol_ckt`), the checksum / status octet of a device reply (`pc_iol_cks`), and the SDCI message
1824 * checksum (`pc_iol_checksum6` / `pc_iol_finalize` / `pc_iol_verify`) implemented straight from IO-Link
1825 * spec v1.1.4 Annex A.1.6 (the 0x52 seed + the 8-to-6-bit compression of equation A.1). Lay
1826 * the M-sequence / ISDU octets out per your device profile, then finalize / verify with this
1827 * codec. Pure codec, host-tested. The wire is a UART through an IO-Link transceiver
1828 * (e.g. MAX14819 / L6360); bridge sensor data onto Wi-Fi.
1829 */
1830#ifndef PC_ENABLE_IOLINK
1831#define PC_ENABLE_IOLINK 0
1832#endif
1833
1834/**
1835 * @brief gRPC-Web message framing (`services/grpcweb`).
1836 *
1837 * Default off. A zero-heap length-prefixed frame builder + parser for gRPC-Web, the
1838 * HTTP/1.1-reachable subset of gRPC (gRPC proper needs HTTP/2). `pc_grpcweb_frame_message`
1839 * wraps a Protobuf message in the 5-octet `[flags][len BE32]` prefix, `pc_grpcweb_frame_trailer`
1840 * emits the 0x80 trailers frame (`grpc-status` / `grpc-message`), and `pc_grpcweb_parse` reads
1841 * one frame back. Wraps the Protobuf codec (`PC_ENABLE_PROTOBUF`) over the shipped
1842 * HTTP/1.1 server/client. Pure codec, host-tested.
1843 */
1844#ifndef PC_ENABLE_GRPC_WEB
1845#define PC_ENABLE_GRPC_WEB 0
1846#endif
1847
1848/**
1849 * @brief OMA LwM2M TLV codec (`services/lwm2m`).
1850 *
1851 * Default off. A zero-heap writer + cursor reader for the LwM2M `application/vnd.oma.lwm2m+tlv`
1852 * resource encoding (Type / Identifier / Length / Value, 8-/16-bit ids, 0-/8-/16-/24-bit
1853 * lengths), carried over the shipped CoAP service for device management. Value helpers for
1854 * shortest-form integers, booleans, strings, and floats. Pure codec, host-tested.
1855 */
1856#ifndef PC_ENABLE_LWM2M
1857#define PC_ENABLE_LWM2M 0
1858#endif
1859
1860/**
1861 * @brief Omron FINS frame codec (`services/fins`).
1862 *
1863 * Default off. A zero-heap command/response builder + parser for the Factory Interface
1864 * Network Service (FINS/UDP): `pc_fins_build_command` / `pc_fins_build_memory_area_read` emit the
1865 * 10-octet routing header + command code + parameters, and `pc_fins_parse_command` /
1866 * `pc_fins_parse_response` read them back (the response end code MRES/SRES included). Talks to
1867 * an Omron PLC over the shipped UDP transport (pc_udp_sendto). Pure codec, host-tested.
1868 */
1869#ifndef PC_ENABLE_FINS
1870#define PC_ENABLE_FINS 0
1871#endif
1872
1873/**
1874 * @brief Omron Host Link (C-mode) frame codec (`services/hostlink`).
1875 *
1876 * Default off. A zero-heap ASCII command/response codec for the Omron serial host-link
1877 * protocol (the RS-232/485 sibling of FINS): `pc_hostlink_build` emits `@UU` + header code +
1878 * text + FCS + `*`CR, and `pc_hostlink_parse` FCS-validates and splits a frame
1879 * (`pc_hostlink_end_code` reads a response's end code). FCS is the 8-bit XOR from `@` through
1880 * the text. Pure codec, host-tested; the serial transport is the application's.
1881 */
1882#ifndef PC_ENABLE_HOSTLINK
1883#define PC_ENABLE_HOSTLINK 0
1884#endif
1885
1886/**
1887 * @brief SCPI / IEEE 488.2 instrument-control codec (`services/scpi`).
1888 *
1889 * Default off. A zero-heap codec for the text command language nearly every modern bench
1890 * instrument speaks (DMMs, scopes, power supplies, function generators, SMUs, loads) over a raw
1891 * TCP socket on port 5025: a command builder (`pc_scpi_build` joins a `:`-hierarchy header with
1892 * comma-separated parameters + terminator), the IEEE 488.2 common commands (`*IDN?` / `*RST` /
1893 * `*CLS` / `*ESR?` / `*STB?` / ...), response parsers (numeric NR1/NR2/NR3, boolean, quoted
1894 * string, and definite/indefinite arbitrary block `#<n><len><data>` for waveform captures), the
1895 * status-byte / event-status-register / error-queue model, and a SCPI short/long-form header
1896 * matcher for dispatching incoming commands. Makes the device a bench-instrument controller or a
1897 * wireless bridge that fans instrument telemetry into HTTP/MQTT. Pure codec, host-tested; the TCP
1898 * transport is the application's.
1899 */
1900#ifndef PC_ENABLE_SCPI
1901#define PC_ENABLE_SCPI 0
1902#endif
1903/** @brief SCPI error/event queue depth (entries). The SCPI status model requires a queue; when it
1904 * overflows the tail entry is replaced with -350 "Queue overflow" per the standard. */
1905#ifndef PC_SCPI_ERR_QUEUE
1906#define PC_SCPI_ERR_QUEUE 8
1907#endif
1908
1909/**
1910 * @brief HiSLIP (High-Speed LAN Instrument Protocol) message codec (`services/hislip`).
1911 *
1912 * Default off. A zero-heap codec for the IVI Foundation's modern LXI instrument transport (IVI-6.1)
1913 * on TCP port 4880 - the successor to VXI-11 that carries SCPI at higher throughput over two TCP
1914 * channels (synchronous + asynchronous). `pc_hislip_build_header` / `pc_hislip_parse_header`
1915 * frame the fixed 16-byte header ("HS" prologue + message type + control code + 32-bit parameter +
1916 * 64-bit payload length), with helpers for the Initialize / AsyncInitialize handshake and the
1917 * Data / DataEND message carrying a SCPI payload + its message id. Pairs with `PC_ENABLE_SCPI`
1918 * (the payload). Pure codec, host-tested; the two TCP connections are the application's.
1919 */
1920#ifndef PC_ENABLE_HISLIP
1921#define PC_ENABLE_HISLIP 0
1922#endif
1923
1924/**
1925 * @brief VXI-11 (TCP/IP Instrument Protocol) codec (`services/vxi11`).
1926 *
1927 * Default off. A zero-heap codec for the legacy LXI instrument transport that predates HiSLIP:
1928 * VXI-11 over ONC RPC (Sun RPC) with XDR encoding. Provides the reusable XDR / ONC-RPC primitives
1929 * (record-marking header, CALL / REPLY message framing, AUTH_NONE, length-prefixed opaque/string,
1930 * 4-byte alignment) plus the DEVICE_CORE procedures - create_link / device_write / device_read /
1931 * device_readstb / destroy_link (program 0x0607AF v1) - and the portmapper GETPORT call that maps
1932 * the program to its TCP port. Still the fallback transport for a large installed base of LAN
1933 * instruments. Pairs with `PC_ENABLE_SCPI` (the payload). Pure codec, host-tested; the TCP
1934 * connection is the application's.
1935 */
1936#ifndef PC_ENABLE_VXI11
1937#define PC_ENABLE_VXI11 0
1938#endif
1939
1940/**
1941 * @brief GPIB-over-LAN (Prologix-style) controller command codec (`services/gpib`).
1942 *
1943 * Default off. A zero-heap codec for the Prologix-compatible `++` command set that drives a
1944 * legacy IEEE-488 (GPIB) bench of instruments through a Prologix GPIB-Ethernet / GPIB-USB adapter
1945 * (raw socket on TCP 1234): builders for the control commands (`++addr` / `++mode` / `++read` /
1946 * `++eoi` / `++eos` / `++spoll` / `++clr` / `++trg` / `++ver` / ...), the data-line escaping (a
1947 * leading ESC before a CR / LF / ESC / `+` byte in instrument data), and response parsers (the
1948 * address / serial-poll status byte / version string). Makes the device a bridge into pre-LAN test
1949 * gear that will never speak SCPI-over-TCP directly. Pure codec, host-tested; the socket / serial
1950 * link to the adapter is the application's.
1951 */
1952#ifndef PC_ENABLE_GPIB
1953#define PC_ENABLE_GPIB 0
1954#endif
1955
1956/**
1957 * @brief Haas Machine Data Collection (MDC) Q-command codec (`services/haas_mdc`).
1958 *
1959 * Default off. A zero-heap codec for the documented Haas Automation MDC protocol - the `?Q` command
1960 * set a Haas CNC mill / lathe control answers over RS-232 or its Ethernet port: builders for the
1961 * numbered queries (`?Q100` machine serial number, `?Q101` control software version, `?Q102` model,
1962 * `?Q104` mode, `?Q300` power-on time, `?Q500` active program + run status + parts counter, and
1963 * `?Q600 <var>` macro / system-variable read) and a parser for the `>`-wrapped, comma-delimited
1964 * responses (and the unprompted `DPRNT(...)` lines a running G-code program emits). Makes the device
1965 * a fixed-BSS CNC data collector that fans machine status into HTTP / MQTT. Pure codec, host-tested;
1966 * the serial / TCP link to the control is the application's.
1967 */
1968#ifndef PC_ENABLE_HAAS_MDC
1969#define PC_ENABLE_HAAS_MDC 0
1970#endif
1971
1972/**
1973 * @brief PackML / OMAC packaging-machine state model (`services/packml`).
1974 *
1975 * The ISA-TR88.00.02 PackML state machine (17 states + the Reset/Start/Stop/Hold/.../Abort/Clear commands)
1976 * plus the Command/Status/Admin PackTags (current state + unit mode, machine speed, production counters) as
1977 * a fixed-BSS state engine + owned service. Pure, host-tested; the OPC UA / tag transport is the
1978 * application's.
1979 */
1980#ifndef PC_ENABLE_PACKML
1981#define PC_ENABLE_PACKML 0
1982#endif
1983
1984/**
1985 * @brief Heidenhain LSV/2 telegram codec (`services/lsv2`).
1986 *
1987 * Default off. A zero-heap codec for the LSV/2 protocol Heidenhain TNC controls speak for DNC + data
1988 * access over serial or Ethernet (LSV/2-over-TCP, port 19000): the telegram framer (a 4-byte
1989 * big-endian payload-length prefix, a 4-character command / response mnemonic, then the payload) plus
1990 * typed builders for login / logout (`A_LG` / `A_LO`), the null-terminated-filename file commands
1991 * (`R_FL` load, `C_FL` send, `C_FD` delete, `C_DC` change dir, `C_DM` / `C_DD` make / delete dir), and
1992 * the run-info request (`R_RI` with a 2-byte run-info selector: execution state, selected program,
1993 * override, program state), and readers for the response mnemonics (`T_OK` / `T_FD` and the `T_ER` /
1994 * `T_BD` two-byte error-class + error-code). A CNC-native southbound source for the common European
1995 * control alongside `services/focas` / `services/haas_mdc`. Pure codec, host-tested; the serial / TCP
1996 * link to the control is the application's.
1997 */
1998#ifndef PC_ENABLE_LSV2
1999#define PC_ENABLE_LSV2 0
2000#endif
2001
2002/**
2003 * @brief IKEv2 (RFC 7296) message + payload codec (`services/ikev2`).
2004 *
2005 * Default off. A zero-heap builder / parser for the Internet Key Exchange v2 wire format that
2006 * negotiates IPsec security associations over UDP 500 / 4500 (NAT-T) - tier 1 (the pure framing) of an
2007 * IKEv2/IPsec stack: the 28-octet IKE header and the generic payload chain (SA -> proposals ->
2008 * transforms with the key-length attribute, KE, Ni/Nr nonce, IDi/IDr, CERT/CERTREQ, AUTH, N notify, D
2009 * delete, TSi/TSr traffic selectors, and the SK encrypted-payload envelope). Build / parse into caller
2010 * buffers only - no sockets and no crypto; the Diffie-Hellman math, the SKEYSEED / SK_* key derivation,
2011 * the SK AEAD, and the IKE_SA_INIT -> IKE_AUTH state machine are later tiers that reuse the crypto the
2012 * library already ships. Field layouts verified against RFC 7296 + IANA and cross-checked byte-for-byte
2013 * against scapy's IKEv2 codec. Pure codec, host-tested; the UDP transport is the application's.
2014 */
2015#ifndef PC_ENABLE_IKEV2
2016#define PC_ENABLE_IKEV2 0
2017#endif
2018
2019/**
2020 * @brief SenML (RFC 8428) measurement-pack builder (`services/senml`).
2021 *
2022 * Default off; implies PC_ENABLE_CBOR (the SenML-CBOR form uses the CBOR writer). A
2023 * zero-heap SenML-JSON + SenML-CBOR encoder over the shipped JSON / CBOR codecs: the caller
2024 * fills a `SenmlRecord` array (base name/time, name, unit, one value, time) and
2025 * `pc_senml_json_build` / `pc_senml_cbor_build` emit the whole pack. Numbers are emitted as
2026 * integers when integral (so timestamps keep precision), else floats. The standard
2027 * measurement format for CoAP / LwM2M / HTTP telemetry. Pure codec, host-tested.
2028 */
2029#ifndef PC_ENABLE_SENML
2030#define PC_ENABLE_SENML 0
2031#endif
2032// SenML's binary form is CBOR, so it needs the CBOR codec compiled in.
2033#define PC_NEED_CBOR (PC_ENABLE_CBOR || PC_ENABLE_SENML)
2034
2035/**
2036 * @brief Allen-Bradley DF1 full-duplex frame codec (`services/df1`).
2037 *
2038 * Default off. A zero-heap framing + DLE byte-stuffing + BCC/CRC codec for the Rockwell
2039 * serial PLC data-link layer (pub. 1770-6.5.16): `pc_df1_build_frame` wraps application data in
2040 * `DLE STX ... DLE ETX` with a doubled-DLE escape and a BCC (2's complement of the data sum)
2041 * or CRC-16 (over the data + ETX, low byte first), and `pc_df1_parse_frame` validates the check
2042 * and un-stuffs the data. Pure codec, host-tested; the application header is the app's.
2043 */
2044#ifndef PC_ENABLE_DF1
2045#define PC_ENABLE_DF1 0
2046#endif
2047
2048/**
2049 * @brief Siemens SIMATIC serial point-to-point: 3964R link + RK512 telegrams (`services/simatic`).
2050 *
2051 * Default off. The pre-Ethernet Siemens PtP link (CP 341/441/524/525): the 3964R byte-oriented,
2052 * half-duplex link protocol (STX/DLE handshake, DLE-doubling, XOR BCC on the "R" variant, priority
2053 * arbitration on an STX collision, QVZ/ZVZ timeouts + retry) plus the RK512 computer-link telegrams
2054 * (SEND / FETCH addressing a DB / flag / I-O area, big-endian words). A pure framing/telegram codec +
2055 * one owned link-state-machine context; the RS-232 / RS-485 UART is the application's. Host-tested
2056 * against an independent python 3964R+RK512 reference peer.
2057 */
2058#ifndef PC_ENABLE_SIMATIC
2059#define PC_ENABLE_SIMATIC 0
2060#endif
2061
2062/** @brief 3964R block-body buffer size (built/received bytes: DLE-stuffed payload + DLE ETX + BCC). */
2063#ifndef PC_SIMATIC_BLOCK_MAX
2064#define PC_SIMATIC_BLOCK_MAX 256
2065#endif
2066
2067/** @brief 3964R QVZ (Quittungsverzugszeit): handshake acknowledge-delay timeout, ms. */
2068#ifndef PC_SIMATIC_QVZ_MS
2069#define PC_SIMATIC_QVZ_MS 2000
2070#endif
2071
2072/** @brief 3964R ZVZ (Zeichenverzugszeit): inter-character timeout while receiving a block, ms. */
2073#ifndef PC_SIMATIC_ZVZ_MS
2074#define PC_SIMATIC_ZVZ_MS 200
2075#endif
2076
2077/**
2078 * @brief TPKT (RFC 1006) + COTP (X.224 class 0) frame codec (`services/cotp`).
2079 *
2080 * Default off. A zero-heap "ISO transport on TCP" framing codec - the reusable foundation
2081 * under S7comm and IEC 61850 MMS. `pc_tpkt_build` / `pc_tpkt_parse` handle the 4-octet TPKT
2082 * envelope; `pc_cotp_build_dt` wraps user data in a Data TPDU, `pc_cotp_build_cr` builds a
2083 * Connection Request (with the TPDU-size parameter + caller TSAP params), and `pc_cotp_parse`
2084 * reports the TPDU type and the DT data / CR-CC refs. Pure codec, host-tested.
2085 */
2086#ifndef PC_ENABLE_COTP
2087#define PC_ENABLE_COTP 0
2088#endif
2089
2090/**
2091 * @brief Siemens S7comm PDU codec (`services/s7comm`).
2092 *
2093 * Default off. A zero-heap builder + parser for the S7-300/400 communication PDUs carried
2094 * inside a COTP Data TPDU (PC_ENABLE_COTP) over ISO-on-TCP (port 102): `pc_s7_build_setup`
2095 * (Setup Communication), `pc_s7_build_read_request` (Read Var, S7-ANY items over DB/I/Q/M),
2096 * `pc_s7_parse_header`, and `pc_s7_read_next_item` (the response data items, honoring the
2097 * length-in-bits transport sizes + even-item padding). Constants verified against the
2098 * Wireshark S7comm dissector. Pure codec, host-tested; wrap the PDU with COTP + TPKT.
2099 */
2100#ifndef PC_ENABLE_S7COMM
2101#define PC_ENABLE_S7COMM 0
2102#endif
2103
2104/**
2105 * @brief Mitsubishi MELSEC MC protocol (binary 3E) codec (`services/melsec`).
2106 *
2107 * Default off. A zero-heap batch-read request builder + response parser for MELSEC PLCs over
2108 * TCP/UDP: `pc_melsec_build_read` emits the binary 3E batch-read (word) frame (little-endian
2109 * fields, subheader 0x5000, command 0x0401, the device code + 24-bit head device + point
2110 * count), and `pc_melsec_parse_response` validates the 0xD000 response and reports the end code
2111 * + the read data. Frame layout + device codes verified against a third-party MC impl. Pure
2112 * codec, host-tested. Completes the major-vendor PLC read set (FINS / Host Link / DF1 / S7).
2113 */
2114#ifndef PC_ENABLE_MELSEC
2115#define PC_ENABLE_MELSEC 0
2116#endif
2117
2118/**
2119 * @brief Beckhoff ADS / AMS protocol codec (`services/ads`).
2120 *
2121 * Default off. A zero-heap builder + parser for the TwinCAT PC-based-control protocol over TCP
2122 * 48898: `pc_ads_build_*` emit complete AMS/TCP + AMS-header frames (little-endian, target-before-
2123 * source addressing, cmd id + state flags + cbData + invoke id) for ReadDeviceInfo / Read /
2124 * Write / ReadWrite / ReadState / WriteControl / Add+DeleteNotification, and `pc_ads_parse_*` decode
2125 * the responses (including the DeviceNotification stamp/sample stream). ReadWrite drives symbol-
2126 * by-name access (name -> handle via index group 0xF003, value via 0xF005). AMS header layout +
2127 * command ids verified against the Beckhoff InfoSys spec. Pure codec, host-tested; the caller
2128 * owns the TCP socket and the AMS route on the target router.
2129 */
2130#ifndef PC_ENABLE_ADS
2131#define PC_ENABLE_ADS 0
2132#endif
2133
2134/**
2135 * @brief FANUC FOCAS Ethernet protocol codec (`services/focas`).
2136 *
2137 * Default off. A zero-heap builder + parser for the FANUC CNC data protocol over TCP 8193:
2138 * `pc_focas_build_*` emit the complete on-wire frames (a 10-octet big-endian envelope + payload) for
2139 * the open/close handshake and the documented read functions (SysInfo, alarm status, CNC
2140 * parameters, macro variables, position/axis data, actual feed / spindle), and `pc_focas_parse_*`
2141 * decode the responses (echoed selector + status + data), including the ODBSYS SysInfo layout and
2142 * the FANUC 8-octet `data / base^exp` value encoding. Frame layout, selector encoding, and value
2143 * decoding reverse-engineered by and cross-checked against diohpix/pyfanuc. Pure codec, host-
2144 * tested; the caller owns the TCP socket and drives the open -> command -> close sequence.
2145 */
2146#ifndef PC_ENABLE_FOCAS
2147#define PC_ENABLE_FOCAS 0
2148#endif
2149
2150/**
2151 * @brief FANUC Stream Motion (option J519) UDP codec (`services/fanuc_j519`).
2152 *
2153 * Default off. The robot counterpart to `PC_ENABLE_FOCAS` (which speaks to FANUC CNCs): Stream
2154 * Motion is the real-time external motion interface on R-30iB / R-30iA robot controllers, where an
2155 * external controller streams joint or Cartesian setpoints over UDP 60015 at the controller's
2156 * interpolation rate (typically 125 / 250 Hz) and the robot answers each command with its measured
2157 * Cartesian pose, joint pose, and per-axis motor current. Zero-heap and symmetric: `pc_j519_build_*`
2158 * emit the Start / Motion / Stop / Request packets and `pc_j519_parse_*` decode the Robot Status /
2159 * Ack replies, plus the mirrored pair so the device can stand in as a robot simulator for bench work.
2160 * Every field is LITTLE-endian (unlike FOCAS) and floats are IEEE-754 binary32. The packet type word
2161 * is reused across directions (0 = Start or Robot Status, 3 = Request or Ack), so the direction is in
2162 * the function name and each parser requires its packet's exact length. Field offsets, sizes, type
2163 * codes, I/O-type and threshold enumerations, and the status bits were taken from the public Wireshark
2164 * dissector fanuc-stream-motion/packet-fanuc-stream-motion-j519 - no FANUC source or header is used.
2165 * Pure codec, host-tested; the caller owns the UDP socket and the real-time cadence.
2166 */
2167#ifndef PC_ENABLE_FANUC_J519
2168#define PC_ENABLE_FANUC_J519 0
2169#endif
2170
2171/**
2172 * @brief BACnet/IP BVLC + NPDU codec (`services/bacnet`).
2173 *
2174 * Default off. A zero-heap framing codec for the ASHRAE 135 building-automation network
2175 * layer over UDP (47808): `pc_bvlc_build` / `pc_bvlc_parse` handle the BVLC envelope (type 0x81,
2176 * function, length), and `pc_npdu_build` / `pc_npdu_parse` handle the NPDU (version + NPCI control
2177 * + optional DNET/DADR destination addressing + hop count) and slice the APDU. The APDU
2178 * (application-layer services / object model) layers on top. Pure codec, host-tested.
2179 */
2180#ifndef PC_ENABLE_BACNET
2181#define PC_ENABLE_BACNET 0
2182#endif
2183
2184/**
2185 * @brief EtherNet/IP encapsulation codec (`services/enip`).
2186 *
2187 * Default off. A zero-heap builder + parser for the ODVA EtherNet/IP encapsulation layer
2188 * (TCP/UDP 44818) that carries CIP: `pc_eip_build` / `pc_eip_parse` handle the 24-octet header
2189 * (little-endian command / length / session handle / status / sender context / options),
2190 * `pc_eip_build_register_session` opens a session, and `pc_eip_build_send_rr_data` /
2191 * `pc_eip_parse_send_rr_data` wrap + unwrap a CIP message as an unconnected message (Common
2192 * Packet Format: Null Address + Unconnected Data items). Commands + CPF item types verified
2193 * against the Wireshark ENIP dissector. Pure codec, host-tested; the CIP message is the app's.
2194 */
2195#ifndef PC_ENABLE_ENIP
2196#define PC_ENABLE_ENIP 0
2197#endif
2198
2199/**
2200 * @brief AMQP 0-9-1 frame codec (`services/amqp`).
2201 *
2202 * Default off. A zero-heap frame builder + parser for the RabbitMQ wire protocol so a device
2203 * can be an AMQP client: `pc_amqp_protocol_header` (the `"AMQP" 0 0 9 1` preamble),
2204 * `pc_amqp_build_frame` / `pc_amqp_parse_frame` (type + channel + size + payload + the 0xCE
2205 * frame-end), `pc_amqp_build_method` / `pc_amqp_parse_method` (a METHOD frame's class-id /
2206 * method-id / arguments), and `pc_amqp_build_heartbeat`. Pure codec, host-tested; the method
2207 * arguments and the connection are the application's. Rides the outbound client transport.
2208 */
2209#ifndef PC_ENABLE_AMQP
2210#define PC_ENABLE_AMQP 0
2211#endif
2212
2213/**
2214 * @brief CIP (Common Industrial Protocol) message codec (`services/cip`).
2215 *
2216 * Default off. A zero-heap CIP request builder + response parser for the message that rides
2217 * inside an EtherNet/IP Unconnected Data item (PC_ENABLE_ENIP): `pc_cip_build_epath` (the
2218 * class/instance/attribute logical-segment EPATH), `pc_cip_build_request` /
2219 * `pc_cip_build_get_attr_single`, and `pc_cip_parse_response` (service / general status / data).
2220 * Service codes + the logical-segment encoding verified against the Wireshark CIP dissector.
2221 * Pure codec, host-tested; wrap the request with `pc_eip_build_send_rr_data` for a working read.
2222 */
2223#ifndef PC_ENABLE_CIP
2224#define PC_ENABLE_CIP 0
2225#endif
2226
2227/**
2228 * @brief NATS client protocol codec (`services/nats`).
2229 *
2230 * Default off. A zero-heap builder + parser for the text-based NATS pub/sub protocol so a
2231 * device can be a NATS client: `pc_nats_build_connect` / `pc_nats_build_pub` / `pc_nats_build_sub` /
2232 * `pc_nats_build_unsub` / `pc_nats_build_ping` / `pc_nats_build_pong`, and `pc_nats_parse` which decodes
2233 * an inbound MSG / INFO / PING / PONG / +OK / -ERR (MSG yields subject / sid / reply-to /
2234 * payload). Line-oriented (CRLF), space-delimited; only PUB and MSG carry a payload. Pure
2235 * codec, host-tested; rides the outbound client transport.
2236 */
2237#ifndef PC_ENABLE_NATS
2238#define PC_ENABLE_NATS 0
2239#endif
2240
2241/**
2242 * @brief HAProxy PROXY protocol codec (`services/proxy_protocol`).
2243 *
2244 * Default off. A zero-heap parser + builder for the PROXY protocol header a load balancer /
2245 * reverse proxy prepends, so the server recovers the real client IPv4 behind one.
2246 * `proxy_parse` detects + decodes a v1 (text `PROXY TCP4 ...`) or v2 (binary signature +
2247 * ver_cmd / fam / address block) header and reports the bytes to skip; `proxy_v1_build` /
2248 * `proxy_v2_build` emit a TCP/IPv4 header. Pure codec, host-tested; the application feeds it
2249 * the first bytes of an accepted connection.
2250 */
2251#ifndef PC_ENABLE_PROXY_PROTOCOL
2252#define PC_ENABLE_PROXY_PROTOCOL 0
2253#endif
2254
2255/**
2256 * @brief Sparkplug B payload + topic codec (`services/sparkplug`).
2257 *
2258 * Default off; implies PC_ENABLE_PROTOBUF (the payload is a Protobuf message). A zero-heap
2259 * builder for the Eclipse Sparkplug B industrial-IoT MQTT payload (`pc_spb_build_payload` /
2260 * `pc_spb_build_metric`, over the protobuf codec) and its topic namespace (`pc_spb_build_topic`,
2261 * `spBv1.0/group/type/node[/device]`). Field numbers + datatype codes verified against the
2262 * Eclipse Tahu sparkplug_b.proto. Pure codec, host-tested; publish it with the MQTT client.
2263 */
2264#ifndef PC_ENABLE_SPARKPLUG
2265#define PC_ENABLE_SPARKPLUG 0
2266#endif
2267// Sparkplug B payloads are protobuf messages, so it needs the protobuf codec compiled in.
2268#define PC_NEED_PROTOBUF (PC_ENABLE_PROTOBUF || PC_ENABLE_SPARKPLUG)
2269
2270/** @brief Max serialized size of one Sparkplug B metric submessage (stack temp, bytes). */
2271#ifndef PC_SPB_METRIC_MAX
2272#define PC_SPB_METRIC_MAX 256
2273#endif
2274
2275/**
2276 * @brief Opt-in Modbus master codec + register scanner (PC_ENABLE_MODBUS_MASTER).
2277 *
2278 * Default off. services/fieldbus/modbus/pc_modbus_master builds Modbus TCP read-request ADUs
2279 * and parses the responses (register values or exception), so an app can poll /
2280 * auto-discover a slave's registers. Pure and host-tested as a full round-trip
2281 * against the slave codec (pc_modbus_process_adu); the actual send is the app's TCP.
2282 */
2283#ifndef PC_ENABLE_MODBUS_MASTER
2284#define PC_ENABLE_MODBUS_MASTER 0
2285#endif
2286
2287/** @brief Number of Modbus coils (FC 1/5/15), single-bit R/W (BSS, bit-packed). */
2288#ifndef PC_MODBUS_COILS
2289#define PC_MODBUS_COILS 64
2290#endif
2291
2292/** @brief Number of Modbus discrete inputs (FC 2), single-bit read-only (BSS, bit-packed). */
2293#ifndef PC_MODBUS_DISCRETE_INPUTS
2294#define PC_MODBUS_DISCRETE_INPUTS 64
2295#endif
2296
2297/** @brief Number of Modbus holding registers (FC 3/6/16), 16-bit R/W (BSS). */
2298#ifndef PC_MODBUS_HOLDING_REGS
2299#define PC_MODBUS_HOLDING_REGS 64
2300#endif
2301
2302/** @brief Number of Modbus input registers (FC 4), 16-bit read-only (BSS). */
2303#ifndef PC_MODBUS_INPUT_REGS
2304#define PC_MODBUS_INPUT_REGS 64
2305#endif
2306
2307/**
2308 * @brief TLS (HTTPS/WSS) via mbedTLS with a static memory pool (ESP32-only).
2309 *
2310 * When set, the server can accept TLS connections using mbedTLS configured with
2311 * MBEDTLS_MEMORY_BUFFER_ALLOC_C over a fixed BSS arena (PC_TLS_ARENA_SIZE) -
2312 * no system heap, so the determinism guarantee is preserved. The TLS engine is
2313 * compiled only on Arduino/ESP32 (mbedTLS is not part of the native build).
2314 * Default off.
2315 */
2316#ifndef PC_ENABLE_TLS
2317#define PC_ENABLE_TLS 0
2318#endif
2319
2320/** @brief Maximum simultaneous TLS connections (each holds mbedTLS record buffers). */
2321#ifndef MAX_TLS_CONNS
2322#define MAX_TLS_CONNS 1
2323#endif
2324
2325/**
2326 * @brief TLS session resumption via RFC 5077 session tickets (requires PC_ENABLE_TLS).
2327 *
2328 * Default off. When set, the TLS 1.2 server issues encrypted session tickets and
2329 * accepts them on reconnect, so a returning client completes an abbreviated
2330 * handshake (no certificate or full key exchange) - much faster and far less CPU
2331 * than the ~RSA/ECDHE full handshake. Resumption is stateless: the session state
2332 * lives in the client's ticket, sealed with a server-held key, so there is no
2333 * growing per-session cache (the determinism / zero-heap-growth guarantee holds;
2334 * only a small fixed ticket key and a little arena headroom are added). The ticket
2335 * key rotates automatically on the PC_TLS_TICKET_LIFETIME_S schedule. Needs the
2336 * mbedTLS build to provide MBEDTLS_SSL_TICKET_C (stock arduino-esp32 does).
2337 */
2338#ifndef PC_ENABLE_TLS_RESUMPTION
2339#define PC_ENABLE_TLS_RESUMPTION 0
2340#endif
2341
2342/** @brief Session-ticket lifetime / key-rotation period in seconds (see PC_ENABLE_TLS_RESUMPTION). */
2343#ifndef PC_TLS_TICKET_LIFETIME_S
2344#define PC_TLS_TICKET_LIFETIME_S 86400
2345#endif
2346
2347/**
2348 * @brief Mutual TLS - require and verify a client certificate (mTLS).
2349 *
2350 * Default off. When set (requires PC_ENABLE_TLS), the server can be given a
2351 * trust-anchor CA via PC::tls_require_client_cert(): the TLS handshake
2352 * then demands a client certificate chaining to that CA
2353 * (MBEDTLS_SSL_VERIFY_REQUIRED) and aborts the connection if the client presents
2354 * none or an untrusted one. The verified peer's subject DN is available to
2355 * handlers via PC::tls_client_subject(). Strong transport-level client
2356 * authentication with no passwords.
2357 */
2358#ifndef PC_ENABLE_MTLS
2359#define PC_ENABLE_MTLS 0
2360#endif
2361
2362/** @brief Maximum length of a verified mTLS peer subject DN string (incl. NUL). */
2363#ifndef PC_MTLS_SUBJECT_MAX
2364#define PC_MTLS_SUBJECT_MAX 128
2365#endif
2366
2367/**
2368 * @brief SNMP agent (v1/v2c, + v3 USM when PC_ENABLE_SNMP_V3) over lwIP UDP.
2369 *
2370 * Zero-heap ASN.1 BER codec + a fixed MIB table on UDP/161. Default off. The BER
2371 * codec itself is gated by this flag and is otherwise unit-tested standalone
2372 * (env:native_snmp).
2373 */
2374#ifndef PC_ENABLE_SNMP
2375#define PC_ENABLE_SNMP 0
2376#endif
2377
2378/** @brief Add SNMPv3 USM (auth via HMAC-SHA, privacy via AES-128-CFB). Default off. */
2379#ifndef PC_ENABLE_SNMP_V3
2380#define PC_ENABLE_SNMP_V3 0
2381#endif
2382
2383/**
2384 * @brief Outbound SNMP notifications - traps and informs (requires PC_ENABLE_SNMP).
2385 *
2386 * Default off. When set, src/services/net/snmp/pc_snmp_notify.h sends SNMPv2c (and, with
2387 * PC_ENABLE_SNMP_V3, SNMPv3 USM) Trap / InformRequest PDUs to a manager over
2388 * UDP - so the agent can push alerts instead of only answering polls. Reuses the
2389 * BER codec and the transport-layer UDP service; the PDU builder is host-testable.
2390 */
2391#ifndef PC_ENABLE_SNMP_TRAP
2392#define PC_ENABLE_SNMP_TRAP 0
2393#endif
2394
2395/** @brief Maximum extra variable-bindings (beyond sysUpTime/snmpTrapOID) in one notification. */
2396#ifndef PC_SNMP_TRAP_MAX_VARBINDS
2397#define PC_SNMP_TRAP_MAX_VARBINDS 8
2398#endif
2399
2400/** @brief Static datagram buffer for an outbound SNMP notification, bytes. */
2401#ifndef PC_SNMP_TRAP_BUF_SIZE
2402#define PC_SNMP_TRAP_BUF_SIZE 1024
2403#endif
2404
2405/** @brief Maximum sub-identifiers (arcs) in an SNMP object identifier. */
2406#ifndef SNMP_MAX_OID_LEN
2407#define SNMP_MAX_OID_LEN 32
2408#endif
2409
2410/**
2411 * @brief Maximum registered MIB objects (the agent's fixed OID table).
2412 *
2413 * Each entry holds its OID, a value descriptor, and optional get/set callbacks
2414 * (see src/services/net/snmp/pc_snmp_agent.h). The table lives in BSS; entries are
2415 * scanned linearly (small table) and need not be registered in OID order.
2416 */
2417#ifndef SNMP_MAX_MIB_ENTRIES
2418#define SNMP_MAX_MIB_ENTRIES 16
2419#endif
2420
2421/**
2422 * @brief Maximum variable bindings the agent will emit in one response.
2423 *
2424 * Bounds GetBulk expansion (max-repetitions is clamped so the total response
2425 * varbind count never exceeds this) and the per-request decode scratch.
2426 */
2427#ifndef SNMP_MAX_VARBINDS
2428#define SNMP_MAX_VARBINDS 16
2429#endif
2430
2431/**
2432 * @brief Static request/response datagram buffers for the SNMP UDP agent.
2433 *
2434 * Two buffers of this size live in BSS (one in, one out) - no heap. 484 is the
2435 * RFC 1157 minimum maximum message size; the default holds a one-frame UDP
2436 * payload so GetBulk walks fit without IP fragmentation.
2437 */
2438#ifndef SNMP_MSG_BUF_SIZE
2439#define SNMP_MSG_BUF_SIZE 1472
2440#endif
2441
2442/** @brief Maximum SNMP community-string length (including null terminator). */
2443#ifndef SNMP_COMMUNITY_MAX
2444#define SNMP_COMMUNITY_MAX 32
2445#endif
2446
2447/** @brief Default read-only community (overridable at runtime via pc_snmp_agent_init). Deployments
2448 * SHOULD change this from the RFC-1157 well-known "public" for anything but a closed network. */
2449#ifndef PC_SNMP_DEFAULT_RO_COMMUNITY
2450#define PC_SNMP_DEFAULT_RO_COMMUNITY "public"
2451#endif
2452
2453/** @brief Maximum SNMPv3 USM user-name length (including null terminator). */
2454#ifndef SNMP_V3_USER_MAX
2455#define SNMP_V3_USER_MAX 32
2456#endif
2457
2458/** @brief Maximum SNMPv3 authoritative engine-ID length in bytes (RFC 3411 allows 5..32). */
2459#ifndef SNMP_V3_ENGINEID_MAX
2460#define SNMP_V3_ENGINEID_MAX 32
2461#endif
2462
2463// ---------------------------------------------------------------------------
2464// CoAP server sizing constants (PC_ENABLE_COAP must be 1)
2465// ---------------------------------------------------------------------------
2466
2467/**
2468 * @brief CoAP server (RFC 7252) over UDP/5683.
2469 *
2470 * A zero-heap Constrained Application Protocol endpoint: a fixed resource table
2471 * dispatched against the request's Uri-Path, with a pure host-testable message
2472 * codec (parse/build) and an ESP32 UDP binding via the transport-layer UDP
2473 * service. Default off; the codec is otherwise unit-tested standalone
2474 * (env:native_coap).
2475 */
2476#ifndef PC_ENABLE_COAP
2477#define PC_ENABLE_COAP 0
2478#endif
2479
2480/**
2481 * @brief CoAP resource observation - RFC 7641 (requires PC_ENABLE_COAP).
2482 *
2483 * Default off. When set, a client GET with the Observe option registers as an
2484 * observer of a resource; the application calls pc_coap_notify(path) to push the
2485 * resource's current representation to every observer (a CoAP notification from
2486 * the server port with an increasing Observe sequence). Observers are dropped on
2487 * a deregister GET, a client RST, or send failure.
2488 */
2489#ifndef PC_ENABLE_COAP_OBSERVE
2490#define PC_ENABLE_COAP_OBSERVE 0
2491#endif
2492
2493/** @brief Maximum simultaneous CoAP observers (one slot per observed resource per client). */
2494#ifndef PC_COAP_MAX_OBSERVERS
2495#define PC_COAP_MAX_OBSERVERS 4
2496#endif
2497
2498/**
2499 * @brief CoAP message de-duplication cache size (RFC 7252 sec 4.5). A Confirmable request the server has
2500 * already answered is recognized by its (source endpoint, Message-ID) and re-answered with the
2501 * cached response WITHOUT re-running the handler - so a client's CON retransmission cannot execute
2502 * a non-idempotent request (POST/PUT/DELETE) twice. Set to 0 to compile the dedup cache out.
2503 */
2504#ifndef PC_COAP_DEDUP_ENTRIES
2505#define PC_COAP_DEDUP_ENTRIES 4
2506#endif
2507
2508/** @brief Largest cached response the dedup cache retains per entry; a bigger response is not cached (a
2509 * retransmission re-processes it, fine for the idempotent GET whose block-wise reply exceeds this). */
2510#ifndef PC_COAP_DEDUP_RESP_MAX
2511#define PC_COAP_DEDUP_RESP_MAX 256
2512#endif
2513
2514/** @brief How long (ms) a dedup entry stays fresh - RFC 7252 EXCHANGE_LIFETIME (~247 s) by default, past
2515 * which a repeat Message-ID is treated as a new exchange. */
2516#ifndef PC_COAP_DEDUP_LIFETIME_MS
2517#define PC_COAP_DEDUP_LIFETIME_MS 247000u
2518#endif
2519
2520/**
2521 * @brief CoAP block-wise transfer - RFC 7959 (requires PC_ENABLE_COAP).
2522 *
2523 * Default off. When set, the server understands the Block2 (descriptive,
2524 * responses) and Block1 (control, request uploads) options:
2525 * - Block2: a representation larger than one block, or any GET that carries a
2526 * Block2 option, is served one block at a time. A constrained client requests
2527 * a small block size (SZX) and pages through with ascending block numbers; the
2528 * server re-renders the (idempotent) resource and slices out the asked-for
2529 * block, setting the More bit until the last.
2530 * - Block1: a POST/PUT payload larger than one block is reassembled into a
2531 * single BSS buffer. Each non-final block is acknowledged 2.31 Continue; the
2532 * final block dispatches the handler with the whole reassembled payload.
2533 *
2534 * One block-wise transfer is reassembled at a time (deterministic, single
2535 * buffer); an out-of-order or oversized block yields 4.08 / 4.13. Size1/Size2
2536 * options and the /.well-known/core listing are out of scope.
2537 */
2538#ifndef PC_ENABLE_COAP_BLOCK
2539#define PC_ENABLE_COAP_BLOCK 0
2540#endif
2541
2542/** @brief Largest block-size exponent (SZX) the server will use: block size = 2^(SZX+4) bytes, SZX 0..6 (16..1024). */
2543#ifndef PC_COAP_BLOCK_SZX_MAX
2544#define PC_COAP_BLOCK_SZX_MAX 6
2545#endif
2546
2547/**
2548 * @brief Reassembly buffer for a block-wise (Block1) request upload, in bytes.
2549 *
2550 * One buffer of this size lives in BSS only when PC_ENABLE_COAP_BLOCK is set.
2551 * It bounds the largest payload a chunked POST/PUT can deliver to a handler.
2552 */
2553#ifndef PC_COAP_BLOCK1_MAX
2554#define PC_COAP_BLOCK1_MAX 1024
2555#endif
2556
2557/**
2558 * @brief Maximum registered CoAP resources (the server's fixed routing table).
2559 *
2560 * Each entry holds a path pointer, an allowed-methods bitmask, and a handler.
2561 * The table lives in BSS and is scanned linearly (small table).
2562 */
2563#ifndef PC_COAP_MAX_RESOURCES
2564#define PC_COAP_MAX_RESOURCES 8
2565#endif
2566
2567/** @brief Maximum reconstructed Uri-Path length, including separators and the leading '/'. */
2568#ifndef PC_COAP_MAX_PATH
2569#define PC_COAP_MAX_PATH 64
2570#endif
2571
2572/** @brief Maximum reconstructed Uri-Query length (segments joined by '&'). */
2573#ifndef PC_COAP_MAX_QUERY
2574#define PC_COAP_MAX_QUERY 64
2575#endif
2576
2577/**
2578 * @brief Maximum CoAP request/response payload in bytes.
2579 *
2580 * Sizes the static scratch a handler writes its response body into and bounds
2581 * the request payload handed to it. One buffer of this size lives in BSS.
2582 */
2583#ifndef PC_COAP_MAX_PAYLOAD
2584#define PC_COAP_MAX_PAYLOAD 256
2585#endif
2586
2587/**
2588 * @brief Static response-datagram buffer for the CoAP UDP server.
2589 *
2590 * One buffer of this size lives in BSS (the request is transport-owned). Must
2591 * hold a 4-byte header + token (<=8) + the Content-Format option + a 0xFF marker
2592 * + PC_COAP_MAX_PAYLOAD bytes. When block-wise transfer is enabled it must
2593 * also hold one full block (2^(PC_COAP_BLOCK_SZX_MAX+4) bytes) + option
2594 * overhead, so the default grows accordingly.
2595 */
2596#ifndef PC_COAP_MSG_BUF_SIZE
2597#if PC_ENABLE_COAP_BLOCK
2598#define PC_COAP_MSG_BUF_SIZE 1152
2599#else
2600#define PC_COAP_MSG_BUF_SIZE 512
2601#endif
2602#endif
2603
2604/** @brief Default UDP port the CoAP observe transport notifies from (IANA well-known 5683). */
2605#ifndef PC_COAP_OBSERVE_PORT
2606#define PC_COAP_OBSERVE_PORT 5683
2607#endif
2608
2609/**
2610 * @brief Bytes of the static BSS arena mbedTLS allocates from (PC_ENABLE_TLS).
2611 *
2612 * All mbedTLS allocations (per-connection record buffers, handshake temporaries,
2613 * cert/key parsing) are served from this fixed arena via a custom allocator
2614 * installed with mbedtls_platform_set_calloc_free() - never the system heap. Must
2615 * cover the worst-case handshake peak for MAX_TLS_CONNS; if undersized the
2616 * handshake fails cleanly (no corruption). Measured peak for ONE ECDSA P-256
2617 * connection on Arduino-esp32 (16 KB IN + 16 KB OUT records) is ~41.5 KB, so the
2618 * default leaves a small margin. An RSA cert/larger chain needs more; query the
2619 * live peak via pc_tls_arena_peak(). NOTE: a second concurrent TLS connection
2620 * roughly doubles the record-buffer cost (~32 KB more), which overflows the
2621 * static DRAM budget - keep MAX_TLS_CONNS at 1 unless you shrink the IDF record
2622 * sizes (CONFIG_MBEDTLS_SSL_IN/OUT_CONTENT_LEN, needs an ESP-IDF build).
2623 */
2624#ifndef PC_TLS_ARENA_SIZE
2625// The arena is SHARED across all TLS connections, so it must cover the peak for MAX_TLS_CONNS: ~48 KB
2626// for the first handshake (ECDSA P-256, 16 KB IN + 16 KB OUT records + temporaries) plus ~32 KB of
2627// record buffers per additional concurrent connection. Auto-derive so a profile that raises
2628// MAX_TLS_CONNS (a PSRAM board, via PC_TLS_ARENA_IN_PSRAM) grows the arena to match instead of
2629// silently starving the second handshake. MAX_TLS_CONNS == 1 keeps the historical 49152.
2630#define PC_TLS_ARENA_SIZE (49152 + (MAX_TLS_CONNS - 1) * 32768)
2631#endif
2632
2633/**
2634 * @brief Place the TLS arena in external PSRAM instead of internal DRAM (ESP32).
2635 *
2636 * The internal static-DRAM ceiling (`dram0_0_seg`) is only ~122 KB, so a single
2637 * ~48 KB arena already uses a large slice and a second concurrent connection
2638 * (MAX_TLS_CONNS > 1) overflows it. On a board with PSRAM, set this to 1 to move
2639 * the arena to external RAM via `EXT_RAM_BSS_ATTR` / `EXT_RAM_ATTR`, freeing the
2640 * whole `PC_TLS_ARENA_SIZE` back to internal DRAM so many connections fit.
2641 * Requires `CONFIG_SPIRAM_ALLOW_BSS_SEG_EXTERNAL_MEMORY` (and PSRAM enabled) in the
2642 * ESP-IDF/PlatformIO config; without it the attribute is a no-op and the arena
2643 * stays in DRAM (safe fallback). No effect on the native host build.
2644 */
2645#ifndef PC_TLS_ARENA_IN_PSRAM
2646#define PC_TLS_ARENA_IN_PSRAM 0
2647#endif
2648
2649/**
2650 * @brief Cap TLS records via the Maximum Fragment Length extension (RFC 6066).
2651 *
2652 * 0 (default) leaves the 16 KB TLS record ceiling. Set to 512, 1024, 2048, or 4096
2653 * to negotiate a smaller maximum record. On a mbedTLS build with variable-length
2654 * record buffers this shrinks the per-connection arena footprint (so more concurrent
2655 * connections fit); on a fixed-buffer build it still bounds the on-wire record size
2656 * (bandwidth / latency on a constrained link) and honors a client's MFL request.
2657 * Applied to both the server and the outbound client config. Needs an mbedTLS build
2658 * with `MBEDTLS_SSL_MAX_FRAGMENT_LENGTH` (else it is a no-op).
2659 */
2660#ifndef PC_TLS_MAX_FRAG_LEN
2661#define PC_TLS_MAX_FRAG_LEN 0
2662#endif
2663
2664/**
2665 * @brief Lead the ECDHE curve/group preference with secp256r1 (P-256) instead of x25519.
2666 *
2667 * PER-VARIANT by default, because the ECC silicon differs wildly between dies. On a chip with a
2668 * hardware NIST-ECC accelerator (`PC_HW_ECC` = 1: ESP32-P4/C5/C6/C61/H2/H4/... where mbedTLS routes
2669 * P-256 through the HW via `ecc_alt`), P-256 is dramatically faster than x25519, which stays software
2670 * (measured on an ESP32-P4: P-256 ECDHE ~10 ms vs x25519 ~132 ms, and the full TLS handshake ~29 ms
2671 * vs ~160 ms - a 5.5x win). On a chip WITHOUT ECC HW (`PC_HW_ECC` = 0: ESP32-S3/S2/classic), both
2672 * curves are software and near-identical in the full handshake, so x25519 (the security-preferred
2673 * modern default) leads and this stays 0 (the S3 order is unchanged).
2674 *
2675 * So the default tracks `PC_HW_ECC` - the profile's assertion that the die has NIST-ECC HW - and is
2676 * overridable: force `-DPC_TLS_ECDHE_PREFER_P256=0` to mandate x25519-first even on an ECC-HW chip
2677 * (a deployment policy choice), or `=1` to prefer P-256 on a chip whose profile has not flagged HW ECC.
2678 * This only reorders PREFERENCE; every curve stays enabled, so a peer that offers just one still
2679 * connects. Applied to both the server and outbound-client configs (tls.cpp `tls_apply_curve_pref`).
2680 */
2681#ifndef PC_TLS_ECDHE_PREFER_P256
2682#define PC_TLS_ECDHE_PREFER_P256 PC_HW_ECC
2683#endif
2684
2685/**
2686 * @brief Acknowledge that a MAX_TLS_CONNS > 1 build has been sized to fit.
2687 *
2688 * The whole TLS arena is static `.bss` and the internal `dram0_0_seg` ceiling is only
2689 * ~122 KB, so a second concurrent connection's arena overflows it on a stock build.
2690 * A validation guard (bottom of this file) therefore rejects MAX_TLS_CONNS > 1 unless
2691 * you have taken one of the paths in docs/KNOWN_LIMITATIONS.md - move the arena to
2692 * PSRAM (`PC_TLS_ARENA_IN_PSRAM`, which satisfies the guard on its own), shrink the
2693 * mbedTLS records in a custom ESP-IDF build, or reclaim internal DRAM - and then set
2694 * this to 1 to confirm the build was sized deliberately.
2695 */
2696#ifndef PC_TLS_ACK_MULTI_CONN_DRAM
2697#define PC_TLS_ACK_MULTI_CONN_DRAM 0
2698#endif
2699
2700// ---------------------------------------------------------------------------
2701// Optional network services (ESP32-only thin wrappers; each default-off so it
2702// costs no code/RAM/flash unless explicitly enabled).
2703// ---------------------------------------------------------------------------
2704
2705/** @brief mDNS / DNS-SD advertisement (`name.local` + `_http._tcp`) via ESPmDNS. */
2706#ifndef PC_ENABLE_MDNS
2707#define PC_ENABLE_MDNS 0
2708#endif
2709
2710/** @brief SNTP wall-clock time sync via the ESP-IDF SNTP client. */
2711#ifndef PC_ENABLE_NTP
2712#define PC_ENABLE_NTP 0
2713#endif
2714
2715/**
2716 * @brief NTP/SNTP time server (RFC 5905 / RFC 4330 server mode) on UDP/123 (services/pc_ntp_server).
2717 *
2718 * Turns the device into a local time source: it answers client NTP requests from its own
2719 * clock (`pc_time_now()` + the `pc_millis()` sub-second fraction), so an offline or
2720 * air-gapped LAN can keep its devices in sync without reaching the public NTP pool. The
2721 * 48-byte response codec is pure and host-tested; the wire binding is the transport UDP
2722 * service. Get the device's own time first (e.g. PC_ENABLE_NTP upstream, an RTC, or GPS
2723 * via a time source) - when it has none, the server stays silent rather than serve bad time.
2724 */
2725#ifndef PC_ENABLE_NTP_SERVER
2726#define PC_ENABLE_NTP_SERVER 0
2727#endif
2728
2729/** @brief Stratum the NTP server advertises (distance from a reference clock; 1-15). */
2730#ifndef PC_NTP_SERVER_STRATUM
2731#define PC_NTP_SERVER_STRATUM 3
2732#endif
2733
2734/**
2735 * @brief Authoritative DNS server (services/net/dns_server) on UDP/53.
2736 *
2737 * Default off. Resolves a small fixed table of `name -> IPv4` A records you register with
2738 * pc_dns_server_add(), so devices on an offline / air-gapped LAN can use names instead of raw
2739 * IPs (a companion to the NTP server for offline infrastructure). Answers A/IN queries from
2740 * the table, returns NXDOMAIN for unknown names, and ignores other query types. The response
2741 * builder is pure and host-tested; the wire binding is the transport UDP service. This is a
2742 * general resolver, distinct from the provisioning captive-portal DNS (which answers every
2743 * query with the softAP IP) - do not enable both (they both bind :53).
2744 */
2745#ifndef PC_ENABLE_DNS_SERVER
2746#define PC_ENABLE_DNS_SERVER 0
2747#endif
2748
2749/** @brief Max A records in the DNS server's fixed table. */
2750#ifndef PC_DNS_SERVER_MAX_RECORDS
2751#define PC_DNS_SERVER_MAX_RECORDS 8
2752#endif
2753
2754/** @brief TTL (seconds) the DNS server puts on its answers. */
2755#ifndef PC_DNS_SERVER_TTL
2756#define PC_DNS_SERVER_TTL 60
2757#endif
2758
2759/** @brief Max length of a queried/stored DNS name (bytes, incl NUL). */
2760#ifndef PC_DNS_NAME_MAX
2761#define PC_DNS_NAME_MAX 128
2762#endif
2763
2764/**
2765 * @brief Auto-inject a `Date` response header (RFC 7231 7.1.1.2) when a wall-clock
2766 * time is available.
2767 *
2768 * Default off: a clock-less device must not emit a wrong `Date`, and most embedded
2769 * responses do not need one, so it stays off the hot path. When set, every dynamic
2770 * response carries `Date: <IMF-fixdate>` - but only once a real time exists; before a
2771 * source has valid time it is silently omitted (still correct for a clock-less boot).
2772 *
2773 * The time is taken from the multi-source registry (any enabled NTP / GPS / RTC / ...
2774 * by priority) when PC_ENABLE_TIME_SOURCE is set - register your sources with
2775 * pc_time_source_add() (pc_rtc_time_source, pc_ntp_time_source, ...). Otherwise it comes
2776 * straight from NTP (pc_ntp_http_date). Needs at least one such time source to emit.
2777 */
2778#ifndef PC_HTTP_EMIT_DATE
2779#define PC_HTTP_EMIT_DATE 0
2780#endif
2781
2782/**
2783 * @brief Multi-source time fallback (NTP / RTC / GPS / ... by priority).
2784 *
2785 * When set, src/services/timing_position/time_source/time_source.h provides a small registry of
2786 * user-defined time sources, each a callback returning Unix epoch seconds (0 when
2787 * that source has no valid time). pc_time_now() queries them in priority order
2788 * (lowest value first) and returns the first valid result, so the device falls
2789 * back automatically when its preferred clock is unavailable. Pure and zero-heap
2790 * (a fixed source table); host-testable. Default off.
2791 */
2792#ifndef PC_ENABLE_TIME_SOURCE
2793#define PC_ENABLE_TIME_SOURCE 0
2794#endif
2795
2796/** @brief Maximum registered time sources (PC_ENABLE_TIME_SOURCE). */
2797#ifndef PC_TIME_SOURCE_MAX
2798#define PC_TIME_SOURCE_MAX 4
2799#endif
2800
2801/**
2802 * @brief Shared I2C bus pins for the sensor / peripheral drivers (RTC, SHT3x, MPR121, ADS1115,
2803 * INA219, PCA9685). All of them share one bus via pc_i2c_begin() (services/peripherals/i2c.h), so
2804 * this is the single place to move it. The default -1 uses the platform's default pins (GPIO 21
2805 * SDA / 22 SCL on the classic ESP32). Set both to free GPIOs when those pins are taken - most
2806 * importantly a **wired-Ethernet PHY**: the LAN8720 RMII uses GPIO 21 (TX_EN) and GPIO 22
2807 * (TXD1) on the classic ESP32 (WROOM/WROVER) and the ESP32-P4 (which have the RMII EMAC), so
2808 * with that Ethernet on, move the I2C bus off them (e.g. 32 / 33). The ESP32-S3/C3 have no RMII
2809 * MAC and use an SPI Ethernet (W5500) instead - relocate the bus off whatever SPI pins that
2810 * uses. UART peripherals (LD2410) take their RX/TX pins at pc_ld2410_begin(), so remap those too.
2811 */
2812#ifndef PC_I2C_SDA_PIN
2813#define PC_I2C_SDA_PIN -1
2814#endif
2815#ifndef PC_I2C_SCL_PIN
2816#define PC_I2C_SCL_PIN -1
2817#endif
2818
2819/**
2820 * @brief I2C real-time-clock driver (DS1307 / DS3231) - a battery-backed time source.
2821 *
2822 * Default off. services/peripherals/rtc reads and sets a DS1307/DS3231 RTC over I2C (Wire), so the device
2823 * keeps accurate wall-clock time across reboots and power loss with no network - the ideal
2824 * fallback below GPS and above upstream NTP in a time-source chain (feeds `pc_time_now()`
2825 * and the NTP server). The BCD<->epoch conversion (7 time registers, 12/24-hour, leap years,
2826 * range validation) is pure and host-tested; only the register read/write touches I2C.
2827 */
2828#ifndef PC_ENABLE_RTC
2829#define PC_ENABLE_RTC 0
2830#endif
2831
2832/** @brief I2C address of the RTC (DS1307/DS3231 are fixed at 0x68). */
2833#ifndef PC_RTC_I2C_ADDR
2834#define PC_RTC_I2C_ADDR 0x68
2835#endif
2836
2837/**
2838 * @brief HLK-LD2410 24 GHz mmWave presence / motion radar (UART).
2839 *
2840 * Default off. services/peripherals/ld2410 syncs to the LD2410's framed serial output (256000 baud) and
2841 * decodes the target report - presence state (none / moving / stationary / both), the moving
2842 * and stationary target distance (cm) and energy (0-100), and, in engineering mode, the
2843 * per-gate energies - plus encodes the config commands (enter / exit config, enable / disable
2844 * engineering mode, restart). The frame sync + decode is pure and host-tested; only the UART
2845 * read/write touches hardware. A cheap solder-and-test breakout: wave a hand, watch presence.
2846 */
2847#ifndef PC_ENABLE_LD2410
2848#define PC_ENABLE_LD2410 0
2849#endif
2850
2851/**
2852 * @brief RCWL-0516 microwave Doppler presence sensor + the shared one-GPIO presence facade
2853 * (`services/rcwl0516`).
2854 *
2855 * Default off. The RCWL-0516 has no data protocol - just one 3.3 V OUT pin that latches HIGH on a
2856 * moving reflector - so the whole problem is timing, not bytes. `PresenceCore` is a debounced,
2857 * hold-extended view of one active-high presence pin: a level must hold for
2858 * `PC_RCWL0516_DEBOUNCE_MS` before it is believed (the OUT pin is comparator-driven and chatters
2859 * around the threshold), and presence then persists for `PC_RCWL0516_HOLD_MS` past the last
2860 * believed-HIGH sample, so the module's ~2 s retrigger gaps read as one continuous occupied span
2861 * instead of a flapping boolean. The core is pure and takes an explicit `now` like
2862 * `PC_ENABLE_HOTSWAP`, so it is host-tested against a synthetic clock with no GPIO, and every
2863 * elapsed-time test is an unsigned difference - wrap-safe across a `millis()` rollover. It is
2864 * deliberately sensor-agnostic: the RCWL-0516 is only its first user (via `pc_rcwl0516_core_init`),
2865 * and an HMMD OUT pin, a PIR, or an HB100 reuse the same core with their own two constants.
2866 */
2867#ifndef PC_ENABLE_RCWL0516
2868#define PC_ENABLE_RCWL0516 0
2869#endif
2870
2871/**
2872 * @brief Waveshare HMMD 24 GHz mmWave micro-motion radar codec (`services/hmmd`).
2873 *
2874 * Default off. The HMMD (Waveshare's FMCW micro-motion module on the S3KM1110 / SXKMxxx0 radar SoC)
2875 * is a close relative of the LD2410 and shares its framing exactly: a report frame
2876 * `F4 F3 F2 F1 | len(2) | ... | F8 F7 F6 F5` and a command frame
2877 * `FD FC FB FA | len(2) | word(2) | [value] | 04 03 02 01`, everything little-endian. Where the
2878 * LD2410 splits moving vs stationary across 9 gates, the HMMD reports one detection flag, one
2879 * distance, and the per-gate energy of 16 gates - it is a micro-motion detector, so a still person
2880 * breathing is the case it exists to catch. `pc_hmmd_parse_report` decodes a report and
2881 * `HmmdStream` reassembles frames byte-by-byte with resync on noise (fixed buffer, no heap),
2882 * mirroring `Ld2410Stream`; the command encoders cover open/close command mode plus the firmware
2883 * version (0x0000), serial number (0x0011), parameter config (0x0008) and register (0x0002) reads,
2884 * and `pc_hmmd_parse_ack` decodes the replies. The module's bare GPIO OUT pin carries no protocol -
2885 * feed it to `PresenceCore` from `PC_ENABLE_RCWL0516` for debounced, hold-extended presence
2886 * (an application-level wiring choice; this service does not depend on that one). Framing, payload
2887 * layout, and command encoding come from the public 2Grey/s3km1110 reference; no vendor SDK is used.
2888 */
2889#ifndef PC_ENABLE_HMMD
2890#define PC_ENABLE_HMMD 0
2891#endif
2892
2893/** @brief HMMD UART baud rate (the module's factory default is 115200). */
2894#ifndef PC_HMMD_BAUD
2895#define PC_HMMD_BAUD 115200
2896#endif
2897
2898/**
2899 * @brief IEC 61784-3 black-channel Safety Communication Layer primitives (`services/safety_scl`).
2900 *
2901 * Default off. The functional-safety profiles (PROFIsafe / IEC 61784-3-3, CIP Safety / -3-2, FSoE /
2902 * -3-12, IO-Link Safety) all treat the underlying fieldbus as an untrusted "black channel" and layer
2903 * the same three end-to-end checks on top: a CRC signature over the safety payload, a monitoring /
2904 * consecutive counter, and a receive watchdog. This lands the counter state machine, the watchdog,
2905 * and the fail-safe state machine that combines them, so each profile's codec composes these rather
2906 * than reimplementing them. It deliberately does **not** compute the CRC: every profile defines its
2907 * own polynomial, width, seed and input ordering, those constants live in paid standards, and a
2908 * guessed CRC in a safety layer would look authoritative while silently failing to detect the
2909 * corruption it exists to catch - so the caller passes its profile's verdict in as `signature_ok`
2910 * and this module owns the consequence, which is profile-independent. Fail-safe latches: once any
2911 * check fails the connection stays fail-safe until an explicit `pc_scl_reset`, because a safety
2912 * layer that silently reheals lets an intermittent fault present as a working link. Pure, with an
2913 * explicit `now` like `PC_ENABLE_HOTSWAP`, host-tested against a synthetic clock, and wrap-safe
2914 * across a `millis()` rollover. No heap, no stdlib.
2915 */
2916#ifndef PC_ENABLE_SAFETY_SCL
2917#define PC_ENABLE_SAFETY_SCL 0
2918#endif
2919
2920/** @brief LD2410 UART baud rate (the module's fixed factory default is 256000). */
2921#ifndef PC_LD2410_BAUD
2922#define PC_LD2410_BAUD 256000
2923#endif
2924
2925/**
2926 * @brief DFRobot SEN0192 10.525 GHz microwave Doppler motion sensor (single digital OUT line).
2927 *
2928 * Default off. services/peripherals/sen0192 tracks the module's OUT line as a debounced presence signal: it asserts
2929 * presence on an active sample and holds it for PC_SEN0192_HOLD_MS after the last active sample, so
2930 * brief gaps between Doppler returns don't make presence flap. The presence state machine is pure and
2931 * host-tested; only the GPIO read touches hardware. Unlike a PIR it senses motion through thin non-metal
2932 * enclosures and is unaffected by ambient light / temperature.
2933 */
2934#ifndef PC_ENABLE_SEN0192
2935#define PC_ENABLE_SEN0192 0
2936#endif
2937
2938/** @brief GPIO the SEN0192 OUT line is wired to. */
2939#ifndef PC_SEN0192_PIN
2940#define PC_SEN0192_PIN 4
2941#endif
2942
2943/** @brief Presence is held this many ms after the last active (motion) sample before it clears. */
2944#ifndef PC_SEN0192_HOLD_MS
2945#define PC_SEN0192_HOLD_MS 2000
2946#endif
2947
2948/** @brief SEN0192 OUT polarity: 1 = the OUT line reads HIGH on motion, 0 = active-LOW. */
2949#ifndef PC_SEN0192_ACTIVE_HIGH
2950#define PC_SEN0192_ACTIVE_HIGH 1
2951#endif
2952
2953/**
2954 * @brief NXP MPR121 12-channel capacitive-touch controller (I2C).
2955 *
2956 * Default off. services/peripherals/mpr121 decodes the touch-status word (12 electrode bits + proximity +
2957 * over-current) and the 10-bit filtered / baseline per-electrode data, and builds the register
2958 * init sequence (soft reset, the NXP filter/AFE defaults, per-electrode touch/release
2959 * thresholds, and the electrode-configuration start). The decode + init-sequence builder are
2960 * pure and host-tested; only the register read/write touches I2C. A cheap solder-and-test
2961 * breakout for touch buttons / sliders: wire it up, touch a pad, watch the bit set.
2962 */
2963#ifndef PC_ENABLE_MPR121
2964#define PC_ENABLE_MPR121 0
2965#endif
2966
2967/** @brief I2C address of the MPR121 (0x5A default; 0x5B/0x5C/0x5D via the ADDR pin). */
2968#ifndef PC_MPR121_I2C_ADDR
2969#define PC_MPR121_I2C_ADDR 0x5A
2970#endif
2971
2972/** @brief MPR121 per-electrode touch threshold (delta counts from baseline; NXP AN3944 suggests ~4..12).
2973 * Higher = less sensitive. Keep the release threshold below it for hysteresis. */
2974#ifndef PC_MPR121_TOUCH_THRESHOLD
2975#define PC_MPR121_TOUCH_THRESHOLD 12
2976#endif
2977
2978/** @brief MPR121 per-electrode release threshold (delta counts; should be below the touch threshold). */
2979#ifndef PC_MPR121_RELEASE_THRESHOLD
2980#define PC_MPR121_RELEASE_THRESHOLD 6
2981#endif
2982
2983/**
2984 * @brief Sensirion SHT3x temperature / humidity sensor (I2C).
2985 *
2986 * Default off. services/peripherals/sht3x issues the single-shot measurement command, checks the CRC-8 on
2987 * each returned word (polynomial 0x31, init 0xFF - the Sensirion check value 0xBEEF -> 0x92),
2988 * and converts the raw 16-bit ticks to temperature and relative humidity in integer milli-units
2989 * (no float printf needed). The CRC + conversion are pure and host-tested; only the command
2990 * write / data read touches I2C. A cheap solder-and-test breakout (GY-SHT31 etc.) for
2991 * environmental telemetry: read it, bridge it onto the network.
2992 */
2993#ifndef PC_ENABLE_SHT3X
2994#define PC_ENABLE_SHT3X 0
2995#endif
2996
2997/** @brief I2C address of the SHT3x (0x44 with ADDR low; 0x45 with ADDR high). */
2998#ifndef PC_SHT3X_I2C_ADDR
2999#define PC_SHT3X_I2C_ADDR 0x44
3000#endif
3001
3002/**
3003 * @brief NXP PCA9685 16-channel 12-bit PWM / servo driver (I2C).
3004 *
3005 * Default off. services/peripherals/pca9685 computes the PRESCALE value for a PWM frequency from the 25 MHz
3006 * oscillator, the per-channel register address, the 12-bit ON/OFF pulse counts, and a servo
3007 * pulse-width (microseconds) -> count conversion; it also emits the 5-byte channel PWM write.
3008 * The prescale / count math + the register encoder are pure and host-tested; only the register
3009 * writes touch I2C. A cheap solder-and-test breakout for driving up to 16 servos or LEDs.
3010 */
3011#ifndef PC_ENABLE_PCA9685
3012#define PC_ENABLE_PCA9685 0
3013#endif
3014
3015/** @brief I2C address of the PCA9685 (0x40 default; the six address pins select 0x40..0x7F). */
3016#ifndef PC_PCA9685_I2C_ADDR
3017#define PC_PCA9685_I2C_ADDR 0x40
3018#endif
3019
3020/** @brief Default PWM output frequency in Hz (50 Hz suits hobby servos). */
3021#ifndef PC_PCA9685_FREQ
3022#define PC_PCA9685_FREQ 50
3023#endif
3024
3025/**
3026 * @brief TI ADS1115 16-bit ADC (I2C) - a precise external analog input.
3027 *
3028 * Default off. services/peripherals/ads1115 builds the 16-bit config register (OS start, single-ended
3029 * channel MUX, programmable gain, single-shot mode, data rate, comparator disabled) for a
3030 * single-shot reading, and converts the signed 16-bit result to microvolts for the selected
3031 * gain's full-scale range. The config encoder + conversion are pure and host-tested; only the
3032 * register write / conversion read touches I2C. A cheap solder-and-test breakout for reading
3033 * batteries, potentiometers, and analog sensors with far more resolution than the ESP32 ADC.
3034 */
3035#ifndef PC_ENABLE_ADS1115
3036#define PC_ENABLE_ADS1115 0
3037#endif
3038
3039/** @brief I2C address of the ADS1115 (0x48 with ADDR to GND; 0x49/0x4A/0x4B for VDD/SDA/SCL). */
3040#ifndef PC_ADS1115_I2C_ADDR
3041#define PC_ADS1115_I2C_ADDR 0x48
3042#endif
3043
3044/** @brief Default ADS1115 PGA gain code (ADS1115_GAIN_*): 0=+/-6.144V, 1=+/-4.096V, 2=+/-2.048V (default),
3045 * 3=+/-1.024V, 4=+/-0.512V, 5=+/-0.256V. Also the fallback when a read passes an invalid gain. */
3046#ifndef PC_ADS1115_GAIN
3047#define PC_ADS1115_GAIN 2 // ADS1115_GAIN_2 (+/- 2.048 V)
3048#endif
3049
3050/** @brief Default ADS1115 data-rate code (ADS1115_DR_*): 0=8, 1=16, 2=32, 3=64, 4=128 (default), 5=250,
3051 * 6=475, 7=860 SPS. The single-shot read waits the matching conversion time. */
3052#ifndef PC_ADS1115_DR
3053#define PC_ADS1115_DR 4 // ADS1115_DR_128 (128 SPS)
3054#endif
3055
3056/** @brief ADS1115 input mode: 0 = single-ended (AINx vs GND), 1 = differential. In differential mode the
3057 * channel selects the pair: 0=AIN0-AIN1, 1=AIN0-AIN3, 2=AIN1-AIN3, 3=AIN2-AIN3. */
3058#ifndef PC_ADS1115_DIFFERENTIAL
3059#define PC_ADS1115_DIFFERENTIAL 0
3060#endif
3061
3062/**
3063 * @brief TI INA219 high-side current / power monitor (I2C).
3064 *
3065 * Default off. services/peripherals/ina219 decodes the bus-voltage register (LSB 4 mV) and the shunt-voltage
3066 * register (LSB 10 uV), computes the calibration register from the shunt resistance and the
3067 * chosen current LSB, and scales the raw current / power registers to microamps / microwatts.
3068 * The decode + calibration + scaling math are pure and host-tested; only the register read /
3069 * write touches I2C. A cheap solder-and-test breakout for measuring how much current and power a
3070 * circuit actually draws.
3071 */
3072#ifndef PC_ENABLE_INA219
3073#define PC_ENABLE_INA219 0
3074#endif
3075
3076/** @brief I2C address of the INA219 (0x40 default; the A0/A1 pins select 0x40..0x4F). */
3077#ifndef PC_INA219_I2C_ADDR
3078#define PC_INA219_I2C_ADDR 0x40
3079#endif
3080
3081/** @brief Default INA219 current LSB in microamps per bit (calibration input). The fallback when
3082 * pc_ina219_begin() is passed 0. 100 uA/bit with a 100 mohm shunt -> a 2 A full-scale range. */
3083#ifndef PC_INA219_CURRENT_LSB_UA
3084#define PC_INA219_CURRENT_LSB_UA 100
3085#endif
3086
3087/** @brief Default INA219 shunt resistance in milliohms (calibration input). The fallback when
3088 * pc_ina219_begin() is passed 0. 100 mohm is the common breakout value. */
3089#ifndef PC_INA219_SHUNT_MOHM
3090#define PC_INA219_SHUNT_MOHM 100
3091#endif
3092
3093/**
3094 * @brief Typed NVS configuration store (WiFi creds, IP config, ... as blobs).
3095 *
3096 * When set, src/services/storage/config_store/config_store.h provides a typed key/value
3097 * API (string / u32 / blob) that routes core settings into the ESP32's native
3098 * NVS partition (via `Preferences`) instead of a JSON file on the filesystem -
3099 * which survives FS corruption and is the corruption-resistant home for
3100 * credentials. On host builds it is backed by a fixed in-memory table so the
3101 * typed contract is unit-testable. Default off.
3102 */
3103#ifndef PC_ENABLE_CONFIG_STORE
3104#define PC_ENABLE_CONFIG_STORE 0
3105#endif
3106
3107/** @brief Max key/value entries in the host (test) config backend. */
3108#ifndef PC_CONFIG_MAX_ENTRIES
3109#define PC_CONFIG_MAX_ENTRIES 16
3110#endif
3111
3112/** @brief Max key length incl. null (NVS caps keys at 15 chars). */
3113#ifndef PC_CONFIG_KEY_MAX
3114#define PC_CONFIG_KEY_MAX 16
3115#endif
3116
3117/** @brief Max value bytes per entry in the host (test) config backend. */
3118#ifndef PC_CONFIG_VAL_MAX
3119#define PC_CONFIG_VAL_MAX 64
3120#endif
3121
3122/**
3123 * @brief Stable device UUID derived from the chip MAC (RFC 4122 v5).
3124 *
3125 * When set, src/services/system/device_id/device_id.h derives a deterministic v5 UUID
3126 * from a MAC (via the library's SHA-1) - a storage-free, stable identity for
3127 * mDNS hostnames, MQTT client IDs, etc. The MAC->UUID core is host-testable;
3128 * pc_device_uuid() reads the ESP32 factory MAC. Default off.
3129 */
3130#ifndef PC_ENABLE_DEVICE_ID
3131#define PC_ENABLE_DEVICE_ID 0
3132#endif
3133
3134/**
3135 * @brief Telemetry math helpers (moving-window stats, rate-of-change, totalizer).
3136 *
3137 * Default off. When set, src/services/iot/telemetry/telemetry.h provides zero-heap
3138 * pure-computation helpers over caller-supplied storage: a moving-window stats
3139 * accumulator (mean / variance / stddev / min / max), a derivative / rate-of-
3140 * change tracker, and a trapezoidal run-time totalizer. No ESP32 dependency, so
3141 * the whole cluster is host-testable; it feeds dashboards, alert triggers, and
3142 * odometer-style counters.
3143 */
3144#ifndef PC_ENABLE_TELEMETRY
3145#define PC_ENABLE_TELEMETRY 0
3146#endif
3147
3148/**
3149 * @brief Real-time SVG dashboard (PC_ENABLE_DASHBOARD; requires PC_ENABLE_SSE).
3150 *
3151 * Default off. Serves a self-contained, hand-rolled SVG dashboard page whose
3152 * widgets are declared in a fixed compile-time pc_widget table (zero-heap,
3153 * deterministic). The page fetches the widget layout as JSON and subscribes to an
3154 * SSE stream of live values; pc_dashboard_set() + pc_dashboard_publish()
3155 * push the current readings. The widget-table -> JSON serializers are
3156 * host-testable; WebSocket controls are a follow-up.
3157 */
3158#ifndef PC_ENABLE_DASHBOARD
3159#define PC_ENABLE_DASHBOARD 0
3160#endif
3161
3162/**
3163 * @brief Embed the theme stylesheet library as runtime-selectable blobs (default off).
3164 *
3165 * Off by default: build-time theme injection (`<!--#theme NAME-->`) costs nothing extra, but
3166 * embedding the whole library for runtime switching links every theme's CSS into flash (~1 KB each).
3167 * When set, application/binary_asset_blobs.{h,cpp} exposes `pc_theme_css(name)` + the registry
3168 * `PC_THEME_BLOBS`, so a route (e.g. `/themes/<name>.css`) or a picker can switch themes live.
3169 * Regenerate with `web_assets/wizard/gen_theme_blobs.py` after adding a theme.
3170 */
3171#ifndef PC_ENABLE_THEMES
3172#define PC_ENABLE_THEMES 0
3173#endif
3174
3175/**
3176 * @brief Include the trademark-named themes in the embedded set (default on / open-source).
3177 *
3178 * A few themes are named after a company or product (Darcula, Windows XP, Discord, Spotify, ...). The
3179 * palette is just colors, but a commercial product should not ship the branded name, so a commercial
3180 * build sets this to 0 to drop those blobs from the registry (the list is `RESTRICTED` in
3181 * `web_assets/wizard/gen_themes.py`). The open-source (AGPL) build keeps them.
3182 */
3183#ifndef PC_THEMES_INCLUDE_TRADEMARKED
3184#define PC_THEMES_INCLUDE_TRADEMARKED 1
3185#endif
3186
3187/** @brief Maximum widgets in the dashboard table (BSS value array). */
3188#ifndef PC_DASHBOARD_MAX_WIDGETS
3189#define PC_DASHBOARD_MAX_WIDGETS 16
3190#endif
3191
3192/** @brief Stack buffer for the dashboard layout / values JSON (bytes). */
3193#ifndef PC_DASHBOARD_JSON_BUF
3194#define PC_DASHBOARD_JSON_BUF 1024
3195#endif
3196
3197/**
3198 * @brief Opt-in flash partition-map monitor endpoint (PC_ENABLE_PARTITION_MONITOR).
3199 *
3200 * Default off. When set, services/storage/partition_monitor reports the device's flash
3201 * partition table (label, kind, type / subtype, offset, size, and which app slot
3202 * is running) as JSON, for diagnostics and OTA dashboards. The partition walk uses
3203 * esp_partition / esp_ota_ops; the JSON serializer and the kind classifier are
3204 * pure and host-testable.
3205 */
3206#ifndef PC_ENABLE_PARTITION_MONITOR
3207#define PC_ENABLE_PARTITION_MONITOR 0
3208#endif
3209
3210/** @brief Maximum partitions the monitor reports (BSS table). */
3211#ifndef PC_PARTITION_MAX
3212#define PC_PARTITION_MAX 16
3213#endif
3214
3215/** @brief Stack buffer for the partition-map JSON (bytes). */
3216#ifndef PC_PARTITION_JSON_BUF
3217#define PC_PARTITION_JSON_BUF 1024
3218#endif
3219
3220/**
3221 * @brief Opt-in browser GPIO pin-mapper / diagnostics endpoint (PC_ENABLE_GPIO_MAP).
3222 *
3223 * Default off. When set, services/system/gpio_map serves a compile-time table of GPIO
3224 * pins (number, label, direction, live level) as JSON for a browser diag panel,
3225 * and accepts a control POST (`pin`, `level`) to drive an output. The live read /
3226 * write uses the Arduino digital API on ESP32; the JSON serializer and the control
3227 * parser are pure and host-testable.
3228 */
3229#ifndef PC_ENABLE_GPIO_MAP
3230#define PC_ENABLE_GPIO_MAP 0
3231#endif
3232
3233/** @brief Maximum GPIO pins the mapper reports (BSS table). */
3234#ifndef PC_GPIO_MAX
3235#define PC_GPIO_MAX 40
3236#endif
3237
3238/** @brief Stack buffer for the GPIO-map JSON (bytes). */
3239#ifndef PC_GPIO_JSON_BUF
3240#define PC_GPIO_JSON_BUF 1024
3241#endif
3242
3243/**
3244 * @brief Opt-in fire-and-forget UDP telemetry cast (PC_ENABLE_UDP_TELEMETRY).
3245 *
3246 * Default off. When set, services/iot/udp_telemetry casts metric lines (InfluxDB line
3247 * protocol: `measurement field=val,field2=val2`) to a configured collector over
3248 * UDP via pc_udp_sendto - zero-heap, fire-and-forget (no ACK, no retry), ideal
3249 * for shipping device metrics to Telegraf/InfluxDB/a log sink. The line builder is
3250 * pure and host-tested; only the send touches the network.
3251 */
3252#ifndef PC_ENABLE_UDP_TELEMETRY
3253#define PC_ENABLE_UDP_TELEMETRY 0
3254#endif
3255
3256/** @brief Stack buffer for one telemetry line (bytes). */
3257#ifndef PC_UDP_TELEMETRY_BUF
3258#define PC_UDP_TELEMETRY_BUF 256
3259#endif
3260
3261/**
3262 * @brief Opt-in StatsD metrics client (PC_ENABLE_STATSD).
3263 *
3264 * Default off. When set, services/iot/statsd pushes metrics in the StatsD wire format
3265 * (`name:value|type`, e.g. `api.hits:1|c`) over UDP to a StatsD-speaking collector -
3266 * Graphite/StatsD, Telegraf, Datadog, InfluxDB, etc. Counters, gauges (absolute + delta),
3267 * timings, and sets, with optional sample-rate (`|@0.1`) and DogStatsD tags (`|#env:prod`).
3268 * This is the push counterpart to the pull-based Prometheus `/metrics`. The line formatter
3269 * is pure and host-tested; only the send (pc_udp_sendto) touches the network. Zero heap.
3270 */
3271#ifndef PC_ENABLE_STATSD
3272#define PC_ENABLE_STATSD 0
3273#endif
3274
3275/** @brief Default StatsD collector UDP port (StatsD/Graphite standard). */
3276#ifndef PC_STATSD_PORT
3277#define PC_STATSD_PORT 8125
3278#endif
3279
3280/** @brief Stack buffer for one StatsD line (bytes; caps metric name + value + tags). */
3281#ifndef PC_STATSD_LINE_MAX
3282#define PC_STATSD_LINE_MAX 256
3283#endif
3284
3285/**
3286 * @brief Opt-in runtime heap/stack guardrails (PC_ENABLE_GUARDRAILS).
3287 *
3288 * Default off. When set, services/security/guardrails samples free heap, the heap low-water
3289 * mark, the largest free block (fragmentation), and the calling task's remaining
3290 * stack, and fires a callback when any crosses its threshold - a proactive
3291 * fail-safe hook beyond the passive numbers in /metrics. The threshold evaluator
3292 * and the JSON serializer are pure and host-tested; the sample reads esp_* / the
3293 * FreeRTOS stack high-water on ESP32.
3294 */
3295#ifndef PC_ENABLE_GUARDRAILS
3296#define PC_ENABLE_GUARDRAILS 0
3297#endif
3298
3299/** @brief Free-heap floor (bytes); below this trips the heap guardrail. */
3300#ifndef PC_GUARDRAIL_HEAP_MIN
3301#define PC_GUARDRAIL_HEAP_MIN 8192
3302#endif
3303
3304/** @brief Largest-free-block floor (bytes); below this trips the fragmentation guardrail. */
3305#ifndef PC_GUARDRAIL_FRAG_MIN_BLOCK
3306#define PC_GUARDRAIL_FRAG_MIN_BLOCK 4096
3307#endif
3308
3309/** @brief Task remaining-stack floor (bytes); below this trips the stack guardrail. */
3310#ifndef PC_GUARDRAIL_STACK_MIN
3311#define PC_GUARDRAIL_STACK_MIN 512
3312#endif
3313
3314/**
3315 * @brief Opt-in software watchdog: deadlock detection + fail-safe safe-state (PC_ENABLE_FAILSAFE).
3316 *
3317 * When set, services/system/failsafe provides a fixed registry of "lifelines" (a task / worker / control loop
3318 * that must check in within its deadline). pc_failsafe_check() detects one that stopped feeding (a
3319 * hang / deadlock) and fires a breach callback once per episode so the app can enter a known-safe
3320 * state. App-defined and per-lifeline, on top of the hardware task watchdog. Pure core, zero heap.
3321 * Default off.
3322 */
3323#ifndef PC_ENABLE_FAILSAFE
3324#define PC_ENABLE_FAILSAFE 0
3325#endif
3326
3327/** @brief Max monitored lifelines in the fail-safe registry (static, zero-heap). */
3328#ifndef PC_FAILSAFE_MAX_LIFELINES
3329#define PC_FAILSAFE_MAX_LIFELINES 8
3330#endif
3331
3332/**
3333 * @brief Opt-in dynamic sleep-cycle scheduler (PC_ENABLE_SLEEP_SCHED).
3334 *
3335 * When set, services/system/sleep_sched provides pc_sleep_next(): from the time since the last activity it
3336 * returns how long a low-power device should sleep (0 = stay awake), ramping the window from a floor up
3337 * to a ceiling the longer the idle streak runs. Pure decision core (the app applies the window via
3338 * light / modem / deep sleep). Complements services/radio_power. Default off.
3339 */
3340#ifndef PC_ENABLE_SLEEP_SCHED
3341#define PC_ENABLE_SLEEP_SCHED 0
3342#endif
3343
3344/**
3345 * @brief Opt-in flash wear-leveling slot selector (PC_ENABLE_WEARLEVEL).
3346 *
3347 * When set, services/storage/wearlevel provides pc_wearlevel_pick(): given per-slot write counts it returns
3348 * the least-worn slot to write next, so repeated flash/NVS writes spread evenly and the region ages
3349 * together instead of burning out one block. Pure core (the app owns the slots + persisted counts).
3350 * Default off.
3351 */
3352#ifndef PC_ENABLE_WEARLEVEL
3353#define PC_ENABLE_WEARLEVEL 0
3354#endif
3355
3356/**
3357 * @brief Opt-in SoC power governor (PC_ENABLE_POWER_MGMT).
3358 *
3359 * services/system/radio_power owns the radio and services/system/sleep_sched decides how long to sleep; neither
3360 * owns the SoC. When set, services/system/power_mgmt decides the CPU clock from load, die temperature and
3361 * the reset reason: idle work runs at the floor instead of spinning a 240 MHz core, a hot die clocks
3362 * down (with a lower restore threshold, so a part sitting at the limit does not oscillate), and a
3363 * board that just browned out comes back up at the floor for a settle window rather than slamming
3364 * into the load that collapsed its supply. It can also release the Bluetooth power domain on a build
3365 * that never uses BT. Pure decision core, host-tested; the binding only reads sensors and applies.
3366 * Default off.
3367 */
3368#ifndef PC_ENABLE_POWER_MGMT
3369#define PC_ENABLE_POWER_MGMT 0
3370#endif
3371
3372/** @brief CPU clock (MHz) when there is work to do. */
3373#ifndef PC_POWER_MHZ_MAX
3374#define PC_POWER_MHZ_MAX 240
3375#endif
3376
3377/** @brief CPU clock (MHz) when idle, thermally throttled, or recovering from a brownout. */
3378#ifndef PC_POWER_MHZ_MIN
3379#define PC_POWER_MHZ_MIN 80
3380#endif
3381
3382/** @brief Load percentage at/above which the ceiling clock is used. */
3383#ifndef PC_POWER_BUSY_PCT
3384#define PC_POWER_BUSY_PCT 40
3385#endif
3386
3387/** @brief Die temperature (C) at/above which the clock is throttled. */
3388#ifndef PC_POWER_TEMP_HOT_C
3389#define PC_POWER_TEMP_HOT_C 80
3390#endif
3391
3392/**
3393 * @brief Die temperature (C) at/below which the throttle is released.
3394 *
3395 * Deliberately below PC_POWER_TEMP_HOT_C: with a single threshold a part sitting exactly at the
3396 * limit would flap between ceiling and floor every tick, which is worse than either state.
3397 *
3398 * The gap has to be wider than the temperature swing the clock change *itself* causes, or the
3399 * governor oscillates no matter how correct the hysteresis is. Measured on an ESP32-S3: dropping
3400 * 240 -> 80 MHz cools the die about 2 C within one 500 ms tick, and going back up reheats it by the
3401 * same amount. A band narrower than that swing is self-sustaining - the throttle's own effect
3402 * carries the die back across the release threshold. The 10 C default clears it with room to spare.
3403 */
3404#ifndef PC_POWER_TEMP_COOL_C
3405#define PC_POWER_TEMP_COOL_C 70
3406#endif
3407
3408/** @brief How long (ms) to hold the floor clock after a brownout reset before ramping back up. */
3409#ifndef PC_POWER_RECOVER_MS
3410#define PC_POWER_RECOVER_MS 10000
3411#endif
3412
3413#if PC_ENABLE_POWER_MGMT && (PC_POWER_TEMP_COOL_C >= PC_POWER_TEMP_HOT_C)
3414#error "ProtoCore: PC_POWER_TEMP_COOL_C must be below PC_POWER_TEMP_HOT_C (hysteresis)"
3415#endif
3416
3417#if PC_ENABLE_POWER_MGMT && (PC_POWER_MHZ_MIN > PC_POWER_MHZ_MAX)
3418#error "ProtoCore: PC_POWER_MHZ_MIN must not exceed PC_POWER_MHZ_MAX"
3419#endif
3420
3421#if PC_ENABLE_POWER_MGMT && (PC_POWER_BUSY_PCT > 100)
3422#error "ProtoCore: PC_POWER_BUSY_PCT must be 0..100"
3423#endif
3424
3425/**
3426 * @brief Opt-in removable-storage hot-swap safeties (PC_ENABLE_HOTSWAP).
3427 *
3428 * An SD card is a connector, and it can be pulled mid-write. The failure is quiet: the driver still
3429 * reports a mounted volume, writes fail into a void, and code carries on believing it has storage.
3430 * When set, services/storage/hotswap runs a small state machine per volume (ABSENT / READY / FAULTED): a run
3431 * of consecutive I/O errors declares the medium gone and unmounts it immediately, `pc_hotswap_ready()`
3432 * becomes the fail-closed gate callers check before any filesystem call, and a periodic probe
3433 * remounts when a card comes back. The core is pure and takes an explicit `now`, so the whole state
3434 * machine is host-tested; mounting is three app callbacks, since how a volume mounts is the
3435 * application's business. Default off.
3436 */
3437#ifndef PC_ENABLE_HOTSWAP
3438#define PC_ENABLE_HOTSWAP 0
3439#endif
3440
3441/**
3442 * @brief Consecutive I/O failures that declare a removable volume gone.
3443 *
3444 * Not 1: a single failed write is not proof a card left (a transient bus error, a full volume), and
3445 * tearing down a working mount over one error would be its own bug. Any success resets the run.
3446 */
3447#ifndef PC_HOTSWAP_FAIL_THRESHOLD
3448#define PC_HOTSWAP_FAIL_THRESHOLD 3
3449#endif
3450
3451/** @brief Minimum gap between remount attempts while a volume is absent or faulted (ms). */
3452#ifndef PC_HOTSWAP_PROBE_MS
3453#define PC_HOTSWAP_PROBE_MS 2000
3454#endif
3455
3456#if PC_ENABLE_HOTSWAP && (PC_HOTSWAP_FAIL_THRESHOLD < 1 || PC_HOTSWAP_FAIL_THRESHOLD > 255)
3457#error "ProtoCore: PC_HOTSWAP_FAIL_THRESHOLD must be in [1, 255]"
3458#endif
3459
3460/**
3461 * @brief Opt-in network adaptation decisions (PC_ENABLE_NETADAPT).
3462 *
3463 * When set, services/net/netadapt provides two pure decisions: pc_netadapt_window() sizes the TCP
3464 * receive window from the free heap (bigger when RAM is plentiful, shrinking when tight), and
3465 * pc_netadapt_dhcp_fallback() decides when to give up on DHCP and use a static IP. The app applies
3466 * the results (lwIP window / netif config). Default off.
3467 */
3468#ifndef PC_ENABLE_NETADAPT
3469#define PC_ENABLE_NETADAPT 0
3470#endif
3471
3472/**
3473 * @brief Opt-in DShot ESC throttle protocol codec (PC_ENABLE_DSHOT).
3474 *
3475 * When set, services/peripherals/dshot provides pc_dshot_encode() / _decode(): the 16-bit DShot frame
3476 * (11-bit throttle/command + telemetry bit + 4-bit CRC), the bidirectional/extended inverted-CRC
3477 * variant, and the per-rate bit timing for an RMT driver. Pure codec (the app clocks it out via RMT).
3478 * Default off.
3479 */
3480#ifndef PC_ENABLE_DSHOT
3481#define PC_ENABLE_DSHOT 0
3482#endif
3483
3484/**
3485 * @brief Opt-in HART / HART-IP process-instrument protocol codec (PC_ENABLE_HART).
3486 *
3487 * When set, services/fieldbus/hart provides the HART command-frame codec (build/parse with the longitudinal XOR
3488 * checksum, short + long addressing) and the 8-octet HART-IP message header, so a device speaks HART
3489 * over UDP/TCP 5094 (front-end-free) or, with a HART FSK modem, over the 4-20 mA loop. Pure, host-tested.
3490 * Default off.
3491 */
3492#ifndef PC_ENABLE_HART
3493#define PC_ENABLE_HART 0
3494#endif
3495
3496/**
3497 * @brief Opt-in Network Time Security (NTS, RFC 8915) wire codec (PC_ENABLE_NTS).
3498 *
3499 * When set, services/timing_position/nts provides the NTS-KE record codec (build/parse the TLV records - next protocol,
3500 * AEAD, cookies, server/port) and the NTS NTP extension-field framing (Unique Identifier, Cookie,
3501 * Authenticator). Pure framing (the AES-SIV-CMAC-256 AEAD + TLS-exporter key derivation are the crypto
3502 * integration on top). Default off.
3503 */
3504#ifndef PC_ENABLE_NTS
3505#define PC_ENABLE_NTS 0
3506#endif
3507
3508/**
3509 * @brief Opt-in DDS / RTPS wire-protocol codec (PC_ENABLE_DDS).
3510 *
3511 * When set, services/iot/dds provides the RTPS (DDSI-RTPS) message + submessage framing: the 20-octet
3512 * header (magic / version / vendor / guidPrefix) and the typed submessages (INFO_TS, DATA, HEARTBEAT,
3513 * ACKNACK, ...) with the endianness flag, built by pc_rtps_header / _submessage and walked by
3514 * pc_rtps_parse. Pure framing (CDR payloads + SPDP/SEDP discovery layer on top). Default off.
3515 */
3516#ifndef PC_ENABLE_DDS
3517#define PC_ENABLE_DDS 0
3518#endif
3519
3520/**
3521 * @brief Opt-in XMPP (RFC 6120) stanza codec (PC_ENABLE_XMPP).
3522 *
3523 * When set, services/iot/xmpp builds correctly XML-escaped `<stream:stream>` / `<message>` / `<presence>` /
3524 * `<iq>` stanzas into a caller buffer and reads the stanza element name + an attribute value out of a
3525 * received stanza, so a device is an IoT XMPP client. Pure text framing (TLS/SASL ride the client TLS
3526 * path; the IoT XEPs layer inside `<iq>`). Default off.
3527 */
3528#ifndef PC_ENABLE_XMPP
3529#define PC_ENABLE_XMPP 0
3530#endif
3531
3532/**
3533 * @brief Opt-in raw Layer-2 Ethernet frame codec (PC_ENABLE_RAWL2).
3534 *
3535 * When set, services/fieldbus/rawl2 builds/parses Ethernet II + 802.1Q VLAN frames (no FCS - the MAC appends it;
3536 * pc_eth_fcs is provided for the cases that need it), so the app can inject/receive arbitrary L2
3537 * frames through the vendor L2 transmit path - the basis for the raw-L2 industrial protocols
3538 * (PROFINET DCP, GOOSE, POWERLINK). Pure codec, host-tested. Default off.
3539 */
3540#ifndef PC_ENABLE_RAWL2
3541#define PC_ENABLE_RAWL2 0
3542#endif
3543
3544/**
3545 * @brief Opt-in single-page-app micro-routing decision (PC_ENABLE_SPA_ROUTER).
3546 *
3547 * When set, services/web/spa_router provides pc_spa_route(): given a request path it returns whether to
3548 * serve a real asset file, serve the SPA shell (index.html) for a client-side route, or pass through to
3549 * the app's handlers under an API prefix - so a single-page UI's client routing works. Pure decision
3550 * core (the caller wires the result into serve_static / the router). Default off.
3551 */
3552#ifndef PC_ENABLE_SPA_ROUTER
3553#define PC_ENABLE_SPA_ROUTER 0
3554#endif
3555
3556/**
3557 * @brief Opt-in IEC 61850 GOOSE publisher codec (PC_ENABLE_GOOSE).
3558 *
3559 * When set, services/energy/goose builds the BER-encoded IECGoosePdu (gocbRef / timeAllowedToLive / datSet /
3560 * goID / t / stNum / sqNum / simulation / confRev / ndsCom / numDatSetEntries / allData) and wraps it in
3561 * the 8-octet GOOSE header + Ethernet frame (ethertype 0x88B8) for the fast raw-L2 substation-event
3562 * publish. Pure codec (allData is a caller-encoded BER blob; the raw-L2 transmit is the device step).
3563 * Default off.
3564 */
3565#ifndef PC_ENABLE_GOOSE
3566#define PC_ENABLE_GOOSE 0
3567#endif
3568
3569/**
3570 * @brief Opt-in MTConnect agent response codec (PC_ENABLE_MTCONNECT).
3571 *
3572 * When set, services/machine_tool/mtconnect builds the MTConnectStreams (current/sample) and MTConnectError XML
3573 * response documents (ANSI/MTC1.4) into a caller buffer - header with instanceId + nextSequence, then
3574 * per-DataItem Samples/Events/Condition observations - so the web server is an MTConnect agent over the
3575 * existing HTTP stack. Pure text framing (values XML-escaped). Default off.
3576 */
3577#ifndef PC_ENABLE_MTCONNECT
3578#define PC_ENABLE_MTCONNECT 0
3579#endif
3580
3581/**
3582 * @brief MTConnect rolling sample buffer sizing (PC_ENABLE_MTCONNECT).
3583 *
3584 * The agent retains the most recent ::PC_MTC_SAMPLE_BUFFER observations in a fixed ring so a
3585 * subscriber can replay them with the `sample` from/count long-poll cursor (MTC1.4 §6.7): a request
3586 * asks for observations starting at a sequence number, and the response header reports firstSequence /
3587 * lastSequence / nextSequence so the client knows what it received and where to resume. Each retained
3588 * observation stores its type / dataItemId / timestamp / value in fixed char fields; when the ring is
3589 * full the oldest is evicted and firstSequence advances. Zero-heap, compile-time sized; the buffer costs
3590 * ~PC_MTC_SAMPLE_BUFFER * (48 + the four string caps) bytes only where a pc_mtc_sample_buffer is used.
3591 */
3592#ifndef PC_MTC_SAMPLE_BUFFER
3593#define PC_MTC_SAMPLE_BUFFER 32 // observations retained for `sample` replay
3594#endif
3595#ifndef PC_MTC_STR_MAX
3596#define PC_MTC_STR_MAX 24 // max stored type / dataItemId length (excl NUL)
3597#endif
3598#ifndef PC_MTC_TS_MAX
3599#define PC_MTC_TS_MAX 32 // max stored ISO-8601 timestamp length (excl NUL)
3600#endif
3601#ifndef PC_MTC_VAL_MAX
3602#define PC_MTC_VAL_MAX 32 // max stored observation value length (excl NUL)
3603#endif
3604
3605/**
3606 * @brief Opt-in write-ahead store for atomic buffer-to-flash storage (PC_ENABLE_WAL).
3607 *
3608 * services/storage/wal is a power-loss-safe write-ahead log over any fs::FS backend (SD card, LittleFS): records
3609 * are CRC32-framed, a checkpoint is atomic via an A/B superblock, and a recovery scan on mount replays
3610 * past the last checkpoint and stops at the first bad CRC (the torn tail), bounding the loss window. Sized
3611 * from the measured SD envelope (docs/FEATURE_PERFORMANCE.md): append sequentially in ~32 KiB pages,
3612 * checkpoint every ~128-256 KiB (never scatter small durable writes). The substrate for on-device data
3613 * stores (dbm / sqlite / nosql). Zero heap. Default off.
3614 */
3615#ifndef PC_ENABLE_WAL
3616#define PC_ENABLE_WAL 0
3617#endif
3618#ifndef PC_WAL_PAGE_SIZE
3619#define PC_WAL_PAGE_SIZE 32768 // sequential write unit (the measured durable-throughput knee)
3620#endif
3621#ifndef PC_WAL_MAX_RECORD
3622#define PC_WAL_MAX_RECORD 4096 // largest single record payload
3623#endif
3624
3625/**
3626 * @brief Opt-in dbm: a log-structured hash key-value store on the WAL (PC_ENABLE_DBM, requires WAL).
3627 *
3628 * services/storage/dbm is a Bitcask-style key-value store: each put/delete appends one WAL record (so writes are
3629 * the WAL's fast sequential appends, not slow durable random writes), and an in-RAM open-addressed hash
3630 * index (fixed BSS, no heap) maps each live key to where its value sits in the log. Mount rebuilds the
3631 * index by scanning the WAL. Keys are bounded by PC_DBM_KEY_MAX, values by PC_DBM_VAL_MAX, and the
3632 * index holds up to PC_DBM_SLOTS live keys. Default off.
3633 */
3634#ifndef PC_ENABLE_DBM
3635#define PC_ENABLE_DBM 0
3636#endif
3637#ifndef PC_DBM_SLOTS
3638#define PC_DBM_SLOTS 256 // max live keys (in-RAM index capacity; open-addressed, keep load < ~0.7)
3639#endif
3640#ifndef PC_DBM_KEY_MAX
3641#define PC_DBM_KEY_MAX 32 // largest key in bytes
3642#endif
3643#ifndef PC_DBM_VAL_MAX
3644#define PC_DBM_VAL_MAX 256 // largest value in bytes
3645#endif
3646
3647/**
3648 * @brief Opt-in local JSON document store on the WAL (PC_ENABLE_DOCSTORE, requires DBM + WAL).
3649 *
3650 * services/storage/docstore is a small NoSQL document store: JSON documents addressed by an id, kept durably on
3651 * the write-ahead log. It is a thin layer over dbm (id = key, JSON body = value) and adds the document
3652 * capability - top-level field queries (find documents whose JSON field equals a value) via the zero-heap
3653 * JSON reader. Ids are bounded by PC_DBM_KEY_MAX, bodies by PC_DBM_VAL_MAX. Default off.
3654 */
3655#ifndef PC_ENABLE_DOCSTORE
3656#define PC_ENABLE_DOCSTORE 0
3657#endif
3658#ifndef PC_DOCSTORE_FIELD_MAX
3659#define PC_DOCSTORE_FIELD_MAX 128 // largest string field value a find can compare
3660#endif
3661
3662/**
3663 * @brief Opt-in SQLite3 on-disk file-format reader (PC_ENABLE_SQLITE).
3664 *
3665 * services/storage/sqlite parses the documented SQLite database file structure by hand - the 100-byte database
3666 * header, the b-tree page header, the record varint, and record serial types - so a device can read a
3667 * SQLite file (from pc_wal_fs / fs::FS) without the SQLite amalgamation (which needs a heap + stdio and does
3668 * not fit the no-stdlib zero-heap model). Read-first (a bounded writer is a later step); pure, host-tested
3669 * against real files from the sqlite3 CLI. Default off.
3670 */
3671#ifndef PC_ENABLE_SQLITE
3672#define PC_ENABLE_SQLITE 0
3673#endif
3674
3675/**
3676 * @brief Opt-in CNC RS-232 DNC drip-feed codec (PC_ENABLE_DNC).
3677 *
3678 * services/machine_tool/dnc is the transport-agnostic framing + tape-code layer that streams a G-code program
3679 * (RS-274 / ISO 6983) to a machine-tool controller over RS-232 or a socket: block framing with a `%`
3680 * rewind-stop, ISO 7-bit (ASCII, optional even parity) or EIA RS-244 (odd-parity punched-tape code)
3681 * character translation, a streaming block encoder + reassembling decoder, and XON/XOFF software
3682 * flow-control state. Pure codec (you own the UART / socket); host-tested. Default off.
3683 */
3684#ifndef PC_ENABLE_DNC
3685#define PC_ENABLE_DNC 0
3686#endif
3687
3688/**
3689 * @brief Largest G-code block (one line) the DNC decoder reassembles (PC_ENABLE_DNC).
3690 *
3691 * A block longer than this overflows the decoder's fixed line buffer and is dropped whole
3692 * (::DNC_EV_OVERFLOW) rather than truncated. Sized for a normal G-code line; raise it only for
3693 * unusually long blocks (many parameters). Zero heap - this is the static per-decoder buffer.
3694 */
3695#ifndef PC_DNC_LINE_MAX
3696#define PC_DNC_LINE_MAX 128
3697#endif
3698
3699/**
3700 * @brief Default leader/trailer runout length for the DNC encoder (PC_ENABLE_DNC).
3701 *
3702 * The number of NUL runout bytes ::pc_dnc_encode_leader emits before the program (and can emit after
3703 * it). The reader skips them until the first `%`. Traditional tape leaders were a few inches of
3704 * blank feed; 32 bytes is a serial-link equivalent. Overridable per call via DncCfg::leader_len.
3705 */
3706#ifndef PC_DNC_LEADER_LEN
3707#define PC_DNC_LEADER_LEN 32
3708#endif
3709
3710/**
3711 * @brief Safety cap on how many times the DNC stream engine polls the reverse channel while paused
3712 * by an XOFF, before giving up with an I/O error (PC_ENABLE_DNC).
3713 *
3714 * `dnc_stream` pauses on XOFF and polls `recv` for the XON that resumes it; a well-behaved transport
3715 * paces `recv` (blocks briefly when idle) so this cap is only a backstop against a `recv` that spins
3716 * returning no data forever. Raise it if a slow controller legitimately holds XOFF for a long time.
3717 */
3718#ifndef PC_DNC_XOFF_MAX_POLLS
3719#define PC_DNC_XOFF_MAX_POLLS 200000
3720#endif
3721
3722/**
3723 * @brief Opt-in TCP relay / DNAT port forwarding (PC_ENABLE_RELAY).
3724 *
3725 * services/net/relay is a bidirectional byte pump that publishes an internal `host:port` through the
3726 * server: an inbound connection is relayed to an origin (an outbound pc_client connection), moving
3727 * bytes both ways with backpressure and independent half-close, so the device fronts a service that
3728 * lives behind it. The engine is a pure step function over two send/recv seams (host-testable); the
3729 * app owns the two sockets. Default off.
3730 */
3731#ifndef PC_ENABLE_RELAY
3732#define PC_ENABLE_RELAY 0
3733#endif
3734
3735/**
3736 * @brief User-defined address:port -> hardware-bus bridge (services/net/iface_bridge).
3737 *
3738 * A configurable "device server": the app registers rules mapping a listen `x.x.x.x:nnnn` (TCP/UDP) to a
3739 * UART, SPI chip-select, or I2C address. A network client talking to the port is bridged to that bus -
3740 * raw stream passthrough for UART, or framed write-then-read transactions (uint16 write_len || uint16
3741 * read_len || write_bytes) for SPI/I2C. The rule table + transaction frame codec are a pure, zero-heap,
3742 * host-tested core; the bus I/O (Serial/SPI/Wire) and the PROTO_BRIDGE listener are the ESP32 step.
3743 * Default off.
3744 */
3745#ifndef PC_ENABLE_IFACE_BRIDGE
3746#define PC_ENABLE_IFACE_BRIDGE 0
3747#endif
3748
3749/** @brief Max concurrent address:port -> bus rules (services/net/iface_bridge). */
3750#ifndef PC_BRIDGE_MAX_RULES
3751#define PC_BRIDGE_MAX_RULES 8
3752#endif
3753
3754/**
3755 * @brief Max write / read payload (bytes) per TRANSACTION frame (services/net/iface_bridge).
3756 *
3757 * Bounds the per-transaction stack scratch used to clock an SPI/I2C write-then-read, and rejects a frame
3758 * whose write_len or read_len exceeds it. Device-server transactions are small register accesses, so the
3759 * default is modest; a frame over the cap closes the connection (protocol error). Keep it comfortably
3760 * under the transport RX ring so a whole frame can buffer before it is parsed.
3761 */
3762#ifndef PC_BRIDGE_TXN_MAX
3763#define PC_BRIDGE_TXN_MAX 256
3764#endif
3765
3766/** @brief STREAM (UART) pipe chunk size (bytes) for services/net/iface_bridge - one socket<->UART hop. */
3767#ifndef PC_BRIDGE_STREAM_CHUNK
3768#define PC_BRIDGE_STREAM_CHUNK 256
3769#endif
3770
3771/** @brief UART TRANSACTION read window (ms): how long a write-then-read waits for the read_len reply. */
3772#ifndef PC_BRIDGE_UART_TXN_MS
3773#define PC_BRIDGE_UART_TXN_MS 50
3774#endif
3775
3776/**
3777 * @brief GNSS RTK base station + NTRIP caster (services/timing_position/gnss).
3778 *
3779 * Turns the device into a differential-GNSS correction source: it surveys in a fixed antenna position and
3780 * serves RTCM 3.x corrections to rovers over the network as an NTRIP caster, so a rover applies them for
3781 * RTK / DGPS accuracy. The RTCM3 frame codec (0xD3 preamble, 10-bit length, CRC-24Q, message-type parse,
3782 * MSB-first bit I/O, station-reference 1005/1006 encode/decode) is a pure, zero-heap, host-tested core;
3783 * the caster server (rover connections + sourcetable) and the receiver bring-up (UBX / NMEA over UART) are
3784 * the ESP32 step. Generating RTCM3 *observation* (MSM) messages needs a receiver that outputs raw
3785 * measurements (u-blox RXM-RAWX: F9P / M8T class); a raw-less module (NEO-6/7, GT-U7) can still serve the
3786 * surveyed reference point + sourcetable. Default off.
3787 */
3788#ifndef PC_ENABLE_NTRIP_CASTER
3789#define PC_ENABLE_NTRIP_CASTER 0
3790#endif
3791
3792/** @brief Max concurrent rover connections a caster serves corrections to (services/timing_position/gnss). */
3793#ifndef PC_NTRIP_MAX_ROVERS
3794#define PC_NTRIP_MAX_ROVERS 4
3795#endif
3796
3797// The base surveys in from the receiver's GGA fixes, so the NTRIP caster needs the NMEA 0183 codec.
3798#define PC_NEED_NMEA0183 (PC_ENABLE_NMEA0183 || PC_ENABLE_NTRIP_CASTER)
3799
3800/** @brief Max length (incl. NUL) of an NTRIP mountpoint name the caster serves. */
3801#ifndef PC_NTRIP_MOUNT_MAX
3802#define PC_NTRIP_MOUNT_MAX 32
3803#endif
3804
3805/** @brief Max NTRIP client request size (bytes) the caster buffers while reading the request headers. */
3806#ifndef PC_NTRIP_REQ_MAX
3807#define PC_NTRIP_REQ_MAX 512
3808#endif
3809
3810/** @brief Max distinct mountpoints a single caster serves (each = one RTCM stream). */
3811#ifndef PC_NTRIP_MAX_MOUNTS
3812#define PC_NTRIP_MAX_MOUNTS 2
3813#endif
3814
3815/**
3816 * @brief Per-direction relay buffer size (bytes) for services/net/relay (PC_ENABLE_RELAY).
3817 *
3818 * Each active relay holds two buffers of this size (one per direction) for bytes read from one peer
3819 * but not yet accepted by the other (backpressure carry). Larger buffers raise throughput per step
3820 * (fewer cross-thread pc_conn_send marshals per KB) at the cost of RAM per concurrent relay
3821 * (2 * PC_RELAY_BUF * PC_RELAY_MAX_CONNS bytes).
3822 */
3823#ifndef PC_RELAY_BUF
3824#define PC_RELAY_BUF 2048
3825#endif
3826
3827/**
3828 * @brief Max pc_relay_step passes per poll for the relay listener (PC_ENABLE_RELAY).
3829 *
3830 * One poll drains up to this many PC_RELAY_BUF chunks per direction, so a single event forwards the
3831 * whole buffered origin RX ring (PC_CLIENT_RX_BUF) instead of one chunk - the difference between a
3832 * ~0.4 Mbps and a multi-Mbps port-forward. Bounded so one busy bridge cannot starve the others.
3833 */
3834#ifndef PC_RELAY_DRAIN_MAX
3835#define PC_RELAY_DRAIN_MAX 8
3836#endif
3837
3838/**
3839 * @brief Max published relay ports (bind table size) for the relay listener (PC_ENABLE_RELAY).
3840 *
3841 * Each pc_relay_publish() call binds one listener port to one origin `host:port`. This caps how
3842 * many distinct ports the device can front at once.
3843 */
3844#ifndef PC_RELAY_MAX_PUBLISH
3845#define PC_RELAY_MAX_PUBLISH 4
3846#endif
3847
3848/**
3849 * @brief Max concurrent relayed connections (bridge table size) for the relay listener
3850 * (PC_ENABLE_RELAY). Each holds a pc_relay (two PC_RELAY_BUF buffers) + an origin slot.
3851 */
3852#ifndef PC_RELAY_MAX_CONNS
3853#define PC_RELAY_MAX_CONNS 4
3854#endif
3855
3856/** @brief Max origin hostname length (bytes, incl. NUL) stored per published relay port. */
3857#ifndef PC_RELAY_HOST_MAX
3858#define PC_RELAY_HOST_MAX 64
3859#endif
3860
3861/** @brief Blocking connect timeout (ms) when the relay listener dials the origin on a new inbound. */
3862#ifndef PC_RELAY_CONNECT_MS
3863#define PC_RELAY_CONNECT_MS 5000
3864#endif
3865
3866/**
3867 * @brief Opt-in FTP client wire codec (PC_ENABLE_FTP).
3868 *
3869 * services/file_transfer/ftp is the pure protocol layer of an FTP client (RFC 959 + RFC 2428 EPSV/EPRT):
3870 * `pc_ftp_build_command` / `pc_ftp_build_port` / `pc_ftp_build_eprt` build control-channel commands,
3871 * `pc_ftp_parse_reply` detects a complete single- or multi-line 3-digit reply, and
3872 * `pc_ftp_parse_pasv` / `pc_ftp_parse_epsv` decode the data-channel address the server returns. So a
3873 * device can push/pull files - e.g. drip a `.nc` program to a CNC controller's FTP store. Pure
3874 * codec (you own the control + data sockets); host-tested. Default off.
3875 */
3876#ifndef PC_ENABLE_FTP
3877#define PC_ENABLE_FTP 0
3878#endif
3879
3880/**
3881 * @brief Suggested FTP control-command buffer size (PC_ENABLE_FTP).
3882 *
3883 * A convenience cap for callers sizing the buffer they hand `pc_ftp_build_command`; the builders
3884 * are all length-checked against the caller's `cap`, so this is only a sensible default. Large
3885 * enough for a RETR / STOR with a long path.
3886 */
3887#ifndef PC_FTP_CMD_MAX
3888#define PC_FTP_CMD_MAX 256
3889#endif
3890
3891/**
3892 * @brief Opt-in FTP client session driver (PC_ENABLE_FTP_SESSION, requires PC_ENABLE_FTP).
3893 *
3894 * services/file_transfer/ftp is deliberately pure - it owns no sockets. This is the other half: services/ftp_session
3895 * drives a real control connection through login -> TYPE I -> passive mode -> STOR -> QUIT over the
3896 * outbound client transport, opens the data connection the server names, and streams a payload pulled
3897 * from a caller-supplied source (so the bytes can come from a file, a log, or the core-dump partition
3898 * without the driver knowing). Separate from the codec gate because it drags in the TCP client and the
3899 * DNS resolver, which a build that only wanted the pure codec should not pay for. Default off.
3900 */
3901#ifndef PC_ENABLE_FTP_SESSION
3902#define PC_ENABLE_FTP_SESSION 0
3903#endif
3904
3905/**
3906 * @brief Control-reply accumulator for the FTP session driver (PC_ENABLE_FTP_SESSION).
3907 *
3908 * services/ftp_session buffers a whole control reply here before parsing it. Multiline greetings
3909 * and FEAT listings are the large cases; a reply that will not fit is treated as malformed rather
3910 * than waited on forever.
3911 */
3912#ifndef PC_FTP_REPLY_BUF
3913#define PC_FTP_REPLY_BUF 512
3914#endif
3915
3916/** @brief Bytes staged per data-channel write when the session driver streams a payload. */
3917#ifndef PC_FTP_CHUNK
3918#define PC_FTP_CHUNK 512
3919#endif
3920
3921/** @brief Per-step timeout for the FTP session driver: connect, and each control reply. */
3922#ifndef PC_FTP_TIMEOUT_MS
3923#define PC_FTP_TIMEOUT_MS 8000
3924#endif
3925
3926#if PC_ENABLE_FTP_SESSION && !PC_ENABLE_FTP
3927#error "ProtoCore: PC_ENABLE_FTP_SESSION requires PC_ENABLE_FTP (it drives that codec)"
3928#endif
3929
3930#if PC_ENABLE_FTP_SESSION && (PC_FTP_REPLY_BUF < 128)
3931#error "ProtoCore: PC_FTP_REPLY_BUF must be >= 128 (a multiline greeting needs room)"
3932#endif
3933
3934#if PC_ENABLE_FTP_SESSION && (PC_FTP_CHUNK < 64)
3935#error "ProtoCore: PC_FTP_CHUNK must be >= 64"
3936#endif
3937
3938/**
3939 * @brief Opt-in HTTP Cache-Control directive helpers (PC_ENABLE_HTTP_CACHE).
3940 *
3941 * services/web/httpcache is the origin-side of edge caching (RFC 9111 + RFC 8246 + RFC 5861): a
3942 * structured `Cache-Control` builder (`cache_control_build` + first-class presets like
3943 * `cache_immutable_asset` / `cache_shared`) so app routes emit correct, edge-cacheable responses
3944 * (hand the value to PC::set_cache_control()), a tolerant directive parser
3945 * (`cache_control_parse`), and the RFC 9111 freshness-lifetime calculation. Pure text, host-tested.
3946 * Groundwork for the CDN roadmap; the caching tier itself is a separate piece. Default off.
3947 */
3948#ifndef PC_ENABLE_HTTP_CACHE
3949#define PC_ENABLE_HTTP_CACHE 0
3950#endif
3951
3952/**
3953 * @brief Opt-in CDN edge-cache tier (PC_ENABLE_EDGE_CACHE, requires HTTP_CACHE).
3954 *
3955 * services/web/edge_cache is the caching reverse-proxy edge that services/web/httpcache is the origin-side
3956 * groundwork for: a device sits in front of a remote upstream origin, fetches a response once, and
3957 * serves subsequent hits from a bounded local store - honoring `Cache-Control` / `Expires` / `ETag` /
3958 * `Last-Modified`, revalidating stale entries with conditional requests (`If-None-Match` /
3959 * `If-Modified-Since` -> 304), and serving `Range` / `206` straight from the cache. A two-tier store:
3960 * bounded RAM (L1, hot) plus an optional dbm/WAL-backed SD tier (L2, persistent, when PC_ENABLE_DBM
3961 * is set). Misses/revalidations fetch the origin asynchronously (the client request is suspended and
3962 * resumed from the poll loop, never stalling the worker) and always fail open. Zero heap. Default off.
3963 */
3964#ifndef PC_ENABLE_EDGE_CACHE
3965#define PC_ENABLE_EDGE_CACHE 0
3966#endif
3967#if PC_ENABLE_EDGE_CACHE && !PC_ENABLE_HTTP_CACHE
3968#error "PC_ENABLE_EDGE_CACHE requires PC_ENABLE_HTTP_CACHE"
3969#endif
3970#if PC_ENABLE_EDGE_CACHE && !PC_ENABLE_HTTP_CLIENT
3971#error "PC_ENABLE_EDGE_CACHE requires PC_ENABLE_HTTP_CLIENT (it fetches the upstream origin)"
3972#endif
3973// Opt-in TLS upstream origins: when set, a mapped `https://` origin is fetched over the shared client-TLS
3974// session (pc_tls_csess) instead of being rejected. One outbound TLS origin fetch at a time (the session is
3975// a singleton, shared with MQTTS/wss); the handshake blocks the worker briefly at connect (like the MQTT/WS
3976// clients). Needs the TLS engine + the ~48 KB arena - an S3 / PSRAM board is recommended.
3977#ifndef PC_ENABLE_EDGE_ORIGIN_TLS
3978#define PC_ENABLE_EDGE_ORIGIN_TLS 0
3979#endif
3980#if PC_ENABLE_EDGE_ORIGIN_TLS && !PC_ENABLE_EDGE_CACHE
3981#error "PC_ENABLE_EDGE_ORIGIN_TLS requires PC_ENABLE_EDGE_CACHE"
3982#endif
3983#if PC_ENABLE_EDGE_ORIGIN_TLS && !PC_ENABLE_TLS
3984#error "PC_ENABLE_EDGE_ORIGIN_TLS requires PC_ENABLE_TLS (the client-TLS engine)"
3985#endif
3986/* Derived sizing for the edge cache. Macros, not constexpr: PC_EDGE_FETCH_BUF's default is
3987 * computed from PC_EDGE_MESH_RESP_MAX below and the requirement is enforced with an #error,
3988 * and the preprocessor can evaluate neither `constexpr` nor `sizeof`. SRCBANNED rule 18. */
3989
3990/** @brief Worst-case serialized L2 entry (edge_sd_serialize). */
3991#define PC_EDGE_SD_VALUE_MAX \
3992 (1 /*version*/ + 2 /*status*/ + 2 /*body_len*/ + 7u * 2u /*str lengths*/ + PC_EDGE_KEY_MAX + PC_EDGE_CTYPE_MAX + \
3993 PC_EDGE_ETAG_MAX + PC_EDGE_LASTMOD_MAX + PC_EDGE_CENC_MAX + PC_EDGE_VARY_MAX + PC_EDGE_VARY_MAX + \
3994 PC_EDGE_BODY_MAX)
3995
3996/** @brief Fixed timing trailer prepended to a mesh entry frame (age propagation). */
3997#define PC_EDGE_MESH_TRAILER (8 /*date*/ + 8 /*expires*/ + 4 /*lifetime_s*/ + 4 /*age_hdr*/ + 4 /*age*/)
3998
3999/** @brief Worst-case mesh entry frame (trailer + a full serialized entry). */
4000#define PC_EDGE_MESH_ENTRY_MAX (PC_EDGE_MESH_TRAILER + PC_EDGE_SD_VALUE_MAX)
4001
4002/** @brief Worst-case mesh response frame (header + entry on a HIT). */
4003#define PC_EDGE_MESH_RESP_MAX (2 + 1 + 1 + 2 + PC_EDGE_MESH_ENTRY_MAX)
4004
4005/** @brief Worst-case mesh request frame (bounded request-header snapshot for Vary matching). */
4006#define PC_EDGE_MESH_REQ_MAX (2 + 1 + 1 + 32 + 2 + PC_EDGE_KEY_MAX + 2 + PC_MESH_HDRS_MAX)
4007
4008/* A fetch slot reuses its origin buffer for the mesh query (the two phases never overlap),
4009 * so with the mesh on the buffer must also hold a mesh response. Deriving the floor here is
4010 * what makes PC_ENABLE_EDGE_MESH work out of the box instead of failing to compile. */
4011#if PC_ENABLE_EDGE_MESH
4012#define PC_EDGE_FETCH_BUF_MIN ((PC_EDGE_MESH_RESP_MAX) > 2560 ? (PC_EDGE_MESH_RESP_MAX) : 2560)
4013#else
4014#define PC_EDGE_FETCH_BUF_MIN 2560
4015#endif
4016
4017// PC_EDGE_CACHE_SLOTS and PC_EDGE_BODY_MAX come from board_drivers/board_profiles/ (classic floor, raised
4018// per chip/PSRAM by board_profile.h above); override with -D as usual.
4019#ifndef PC_EDGE_KEY_MAX
4020#define PC_EDGE_KEY_MAX 128 // largest canonical cache key (method\nhost\npath[\nquery])
4021#endif
4022#ifndef PC_EDGE_VARY_MAX
4023#define PC_EDGE_VARY_MAX 64 // stored Vary field-name list / captured request values (each)
4024#endif
4025
4026/** @brief Stored Content-Type to replay. */
4027#ifndef PC_EDGE_CTYPE_MAX
4028#define PC_EDGE_CTYPE_MAX 64
4029#endif
4030
4031/** @brief Stored validator (ETag, quotes included). */
4032#ifndef PC_EDGE_ETAG_MAX
4033#define PC_EDGE_ETAG_MAX 64
4034#endif
4035
4036/** @brief Stored Last-Modified (RFC 1123 date). */
4037#ifndef PC_EDGE_LASTMOD_MAX
4038#define PC_EDGE_LASTMOD_MAX 40
4039#endif
4040
4041/** @brief Stored Content-Encoding to replay (e.g. gzip). */
4042#ifndef PC_EDGE_CENC_MAX
4043#define PC_EDGE_CENC_MAX 32
4044#endif
4045#ifndef PC_EDGE_MAP_MAX
4046#define PC_EDGE_MAP_MAX 4 // path-prefix -> origin route mappings
4047#endif
4048#ifndef PC_EDGE_ORIGIN_URL_MAX
4049#define PC_EDGE_ORIGIN_URL_MAX 128 // largest origin base URL in a route mapping
4050#endif
4051// PC_EDGE_FETCH_SLOTS comes from board_drivers/board_profiles/ (classic floor, raised per chip/PSRAM).
4052#ifndef PC_EDGE_FETCH_BUF
4053#define PC_EDGE_FETCH_BUF PC_EDGE_FETCH_BUF_MIN // per-fetch origin-response accumulation buffer
4054#endif
4055#ifndef PC_EDGE_FETCH_TIMEOUT_MS
4056#define PC_EDGE_FETCH_TIMEOUT_MS 8000 // origin fetch deadline before fail-open
4057#endif
4058#ifndef PC_EDGE_DEFAULT_TTL_S
4059#define PC_EDGE_DEFAULT_TTL_S 60 // fallback freshness when no directive and no wall clock
4060#endif
4061// L2 (SD) tier: when PC_ENABLE_DBM is also set, the edge cache spills evicted entries to a dbm store
4062// (edge_cache_sd) so the cached set survives a reboot. Each entry serializes to ~ its body plus ~470 B of
4063// response metadata; for a full-body spill, PC_DBM_VAL_MAX must be >= that size (>= PC_EDGE_BODY_MAX
4064// + ~470). Entries that do not fit simply stay L1-only, so a small PC_DBM_VAL_MAX is safe but persists
4065// less. The L2 key is the 32-byte cache-key digest, so PC_DBM_KEY_MAX must be >= 32 (its default).
4066
4067/**
4068 * @brief Opt-in mesh (sibling-cache) distribution for the edge cache (PC_ENABLE_EDGE_MESH).
4069 *
4070 * Lets a fleet of edge nodes share one warm cache: on a full local miss, a node queries its configured
4071 * sibling peers (over a plaintext ConnProto::PROTO_MESH TCP link) before hitting the origin, and pulls a
4072 * fresh copy from whichever peer has it - so the origin is fetched once per fleet, not once per node. Pull
4073 * (read-through) only: no push, no invalidation protocol, no consistency window - a stale sibling copy
4074 * self-expires by its own TTL and the requester re-checks freshness on arrival. The transfer carries the
4075 * object plus its freshness/age (RFC 9111 age propagation), so a sibling-fresh object serves for its
4076 * remaining lifetime with zero origin contact. A serving node answers only from its local store (one hop,
4077 * never re-querying its own origin/peers, so the fleet cannot loop). Peers are a static list
4078 * (pc_edge_cache_add_peer); auto-discovery is a follow-up. Zero heap. Default off.
4079 */
4080#ifndef PC_ENABLE_EDGE_MESH
4081#define PC_ENABLE_EDGE_MESH 0
4082#endif
4083#if PC_ENABLE_EDGE_MESH && !PC_ENABLE_EDGE_CACHE
4084#error "PC_ENABLE_EDGE_MESH requires PC_ENABLE_EDGE_CACHE"
4085#endif
4086// PC_MESH_MAX_PEERS and PC_MESH_MAX_CONNS come from board_drivers/board_profiles/ (classic floor, raised
4087// per chip/PSRAM).
4088#ifndef PC_MESH_QUERY_MS
4089#define PC_MESH_QUERY_MS 300 // per-peer query deadline before moving on (miss) / to the origin
4090#endif
4091#ifndef PC_MESH_HOST_MAX
4092#define PC_MESH_HOST_MAX 64 // largest sibling peer host string
4093#endif
4094#ifndef PC_MESH_HDRS_MAX
4095#define PC_MESH_HDRS_MAX \
4096 384 // request-header snapshot carried to a peer so it can match Vary variants
4097 // (headers past the cap are dropped -> at worst a safe mesh miss, never wrong content)
4098#endif
4099
4100/**
4101 * @brief Opt-in SMB2 client (PC_ENABLE_SMB).
4102 *
4103 * services/file_transfer/smb is an SMB2 client (MS-SMB2) so a device can read/write files on a Windows share -
4104 * e.g. a CNC controller's program store. The full read/write-a-file path: the Direct-TCP transport
4105 * frame + SMB2 sync header, NEGOTIATE, the two-round NTLMv2 SESSION_SETUP (NTLM digests MD4/MD5/
4106 * HMAC-MD5, the NTLMv2 response, the NTLMSSP messages, SPNEGO wrapping), TREE_CONNECT, CREATE, READ,
4107 * WRITE, and CLOSE. smb_client ties the codecs into the exchange over a send/recv seam (host-tested
4108 * with a scripted mock server); you own the TCP socket (pc_client). All little-endian. Default off.
4109 */
4110#ifndef PC_ENABLE_SMB
4111#define PC_ENABLE_SMB 0
4112#endif
4113
4114/**
4115 * @brief SMB2 client work-buffer size (bytes) for smb_client's request/response framing.
4116 *
4117 * Two buffers of this size live on the stack during a call, plus a few half-size scratch buffers for
4118 * the NTLM auth tokens, so the engine needs roughly 4x this in stack. 1024 covers the NEGOTIATE ->
4119 * SESSION_SETUP -> TREE_CONNECT -> CREATE handshake; raise it if a server's SPNEGO/target-info token
4120 * or your share path is unusually large.
4121 */
4122#ifndef PC_SMB_BUF
4123#define PC_SMB_BUF 1024
4124#endif
4125
4126/**
4127 * @brief Opt-in SAE J2735 V2X codec (PC_ENABLE_J2735).
4128 *
4129 * When set, services/transportation/j2735 provides the ASN.1 UPER (Unaligned Packed Encoding Rules) bit-level primitive
4130 * codec (constrained INTEGER / BOOLEAN / bit fields) and, on top of it, the J2735 BSMcore safety-message
4131 * block (msgCnt / id / secMark / lat / long / elev / speed / heading) encode + decode, for connected-
4132 * vehicle messaging. Pure codec (the DSRC / C-V2X radio is an external module). Default off.
4133 */
4134#ifndef PC_ENABLE_J2735
4135#define PC_ENABLE_J2735 0
4136#endif
4137
4138/**
4139 * @brief Opt-in NEMA TS 2 traffic-cabinet SDLC frame codec (PC_ENABLE_NEMA_TS2).
4140 *
4141 * When set, services/transportation/nema_ts2 builds/validates the TS 2 SDLC bus frames ([address][control][frame-type]
4142 * [data][CRC-16/X-25]) that link a traffic-signal controller to the MMU, BIUs, and detector racks. Pure
4143 * codec (the synchronous serial PHY + BIU timing are hardware-gated). Default off.
4144 */
4145#ifndef PC_ENABLE_NEMA_TS2
4146#define PC_ENABLE_NEMA_TS2 0
4147#endif
4148
4149/**
4150 * @brief Opt-in GE Fanuc SNP (Series Ninety Protocol) serial frame codec (PC_ENABLE_SNP).
4151 *
4152 * When set, services/fieldbus/snp builds/validates the SNP master-slave serial frame ([control][length][data]
4153 * [arithmetic-sum BCC]) for reading/writing registers on a GE Fanuc Series 90 (90-30/90-70) PLC over
4154 * RS-485. Pure codec (the UART transport + SNP-X session are the device step). Default off.
4155 */
4156#ifndef PC_ENABLE_SNP
4157#define PC_ENABLE_SNP 0
4158#endif
4159
4160/**
4161 * @brief Opt-in AutomationDirect / Koyo DirectNET serial frame codec (PC_ENABLE_DIRECTNET).
4162 *
4163 * When set, services/fieldbus/directnet builds/validates the DirectNET master-slave serial frames - the header
4164 * (SOH + slave/type/address/blocks ASCII-hex + ETB + LRC) and the data frame (STX + data + ETX + LRC) -
4165 * for V-memory read/write on an AutomationDirect DirectLOGIC PLC. Pure codec (the UART transport +
4166 * ACK/NAK handshake are the device step). Default off.
4167 */
4168#ifndef PC_ENABLE_DIRECTNET
4169#define PC_ENABLE_DIRECTNET 0
4170#endif
4171
4172/**
4173 * @brief Opt-in IEEE 2030.5 (Smart Energy Profile 2.0) resource codec (PC_ENABLE_SEP2).
4174 *
4175 * When set, services/energy/sep2 builds the core 2030.5 XML resource documents (DeviceCapability, EndDevice,
4176 * DERControl) in the urn:ieee:std:2030.5:ns namespace, so the web server is a 2030.5 smart-grid
4177 * server/client over the existing HTTP stack (DER dispatch / curtailment). Pure text framing. Default off.
4178 */
4179#ifndef PC_ENABLE_SEP2
4180#define PC_ENABLE_SEP2 0
4181#endif
4182
4183/**
4184 * @brief Opt-in PROFINET DCP (Discovery and Configuration Protocol) frame codec (PC_ENABLE_PROFINET).
4185 *
4186 * When set, services/fieldbus/profinet builds/parses the DCP frames (10-octet header + option/suboption blocks)
4187 * PROFINET uses to discover and name IO-Devices over raw L2 (ethertype 0x8892) - Identify request/
4188 * response and Set (assign NameOfStation / IP). Pure codec (the raw-L2 transmit via services/fieldbus/rawl2 +
4189 * the vendor Ethernet driver is the device step). Default off.
4190 */
4191#ifndef PC_ENABLE_PROFINET
4192#define PC_ENABLE_PROFINET 0
4193#endif
4194
4195/**
4196 * @brief Opt-in NTCIP transportation-device object identifiers (PC_ENABLE_NTCIP).
4197 *
4198 * When set, services/transportation/ntcip provides the NTCIP (National Transportation Communications for ITS Protocol)
4199 * object OID definitions for the common device classes - NTCIP 1202 (actuated signal controller: phases,
4200 * timing, live states) and 1203 (dynamic message sign) - plus an OID builder, so an app exposes them via
4201 * the shipped SNMP agent (services/net/snmp). Pure OID data. Default off.
4202 */
4203#ifndef PC_ENABLE_NTCIP
4204#define PC_ENABLE_NTCIP 0
4205#endif
4206
4207/**
4208 * @brief Opt-in OpenADR 3.0 (Automated Demand Response) JSON codec (PC_ENABLE_OPENADR).
4209 *
4210 * When set, services/energy/openadr builds the OpenADR 3.0 event (a demand-response signal: programID +
4211 * eventName + interval payload points) and report (a VEN reading back to the VTN) JSON objects into a
4212 * caller buffer, over the existing HTTP client/server + OAuth2. Pure JSON framing. Default off.
4213 */
4214#ifndef PC_ENABLE_OPENADR
4215#define PC_ENABLE_OPENADR 0
4216#endif
4217
4218/**
4219 * @brief Opt-in IEC 61850 MMS PDU codec (PC_ENABLE_MMS).
4220 *
4221 * When set, services/energy/mms builds/parses the MMS (ISO 9506) confirmed-request/response Read PDUs
4222 * (BER-encoded, the ACSI client/server core of IEC 61850) - pc_mms_read_request builds a Read of a
4223 * named Data Object, pc_mms_read_response the data reply. Carried over ISO-on-TCP (TPKT + COTP via
4224 * the shipped services/fieldbus/cotp) on port 102. Pure BER codec. Default off.
4225 */
4226#ifndef PC_ENABLE_MMS
4227#define PC_ENABLE_MMS 0
4228#endif
4229
4230/**
4231 * @brief Opt-in CC-Link (CLPA) cyclic fieldbus frame codec (PC_ENABLE_CCLINK).
4232 *
4233 * When set, services/fieldbus/cclink builds/validates the CC-Link cyclic frame ([station][command][RX/RY bit
4234 * data][RWr/RWw word data][sum checksum]) a Mitsubishi CC-Link master exchanges with remote stations
4235 * over RS-485, plus bit/word process-image accessors. Pure codec (the RS-485 timing + CC-Link IE Field
4236 * PHY are hardware-gated). Default off.
4237 */
4238#ifndef PC_ENABLE_CCLINK
4239#define PC_ENABLE_CCLINK 0
4240#endif
4241
4242/**
4243 * @brief Opt-in Ethernet POWERLINK (EPSG) basic frame codec (PC_ENABLE_POWERLINK).
4244 *
4245 * When set, services/fieldbus/powerlink builds/parses the EPL basic frames ([messageType][dest][source][payload])
4246 * of the isochronous managed-node cycle - SoC (start of cycle), PReq (poll request), PRes (poll
4247 * response with process data), SoA (start of async) - over raw L2 (ethertype 0x88AB, on the shipped
4248 * services/fieldbus/rawl2). Pure codec (the raw-L2 transmit + isochronous timing are the device step). Default off.
4249 */
4250#ifndef PC_ENABLE_POWERLINK
4251#define PC_ENABLE_POWERLINK 0
4252#endif
4253
4254/**
4255 * @brief Opt-in SERCOS III motion-bus telegram codec (PC_ENABLE_SERCOS).
4256 *
4257 * When set, services/fieldbus/sercos builds/parses the SERCOS III MDT/AT telegrams (type + phase + cycle + cyclic
4258 * device data) the real-time drive/motion bus exchanges over raw L2 (ethertype 0x88CD, on the shipped
4259 * services/fieldbus/rawl2), plus the IDN (IDentification Number) encode/decode for drive-parameter addressing.
4260 * Pure codec (the isochronous timing + ring topology are hardware-gated). Default off.
4261 */
4262#ifndef PC_ENABLE_SERCOS
4263#define PC_ENABLE_SERCOS 0
4264#endif
4265
4266/**
4267 * @brief Opt-in PROFIBUS-DP FDL telegram codec (PC_ENABLE_PROFIBUS).
4268 *
4269 * When set, services/fieldbus/profibus builds/validates the PROFIBUS-DP FDL telegrams - SD1 (no-data: SD1 DA SA
4270 * FC FCS ED) and SD2 (variable-data: SD2 LE LEr SD2 DA SA FC data FCS ED, arithmetic-sum FCS) - a
4271 * Siemens DP master exchanges with slaves over RS-485 (the DP-V0 cyclic I/O exchange). Pure codec (the
4272 * RS-485 timing + DP state machine are the device step). Default off.
4273 */
4274#ifndef PC_ENABLE_PROFIBUS
4275#define PC_ENABLE_PROFIBUS 0
4276#endif
4277
4278/**
4279 * @brief Opt-in LonWorks / LON-IP (ISO/IEC 14908) network-variable codec (PC_ENABLE_LONWORKS).
4280 *
4281 * When set, services/fieldbus/lonworks builds/parses the LonTalk network-variable PDU ([msg-code][14-bit
4282 * selector][value]) that a building-automation device exchanges - over LON/IP (14908-4) UDP, so no
4283 * Neuron chip is needed - plus the common SNVT scalar encodings (SNVT_temp, SNVT_switch). Pure codec
4284 * (the UDP transport is the shipped UDP layer). Default off.
4285 */
4286#ifndef PC_ENABLE_LONWORKS
4287#define PC_ENABLE_LONWORKS 0
4288#endif
4289
4290/**
4291 * @brief Opt-in Modbus Plus HDLC token-bus frame codec (PC_ENABLE_MBPLUS).
4292 *
4293 * When set, services/fieldbus/mbplus builds/validates the Modbus Plus HDLC frame (7E addr ctrl payload CRC-16/X-25
4294 * 7E) that Schneider's token-passing peer bus exchanges, plus the token-rotation helper (next station in
4295 * the logical ring). Reuses the shipped Modbus PDU model for the data. Pure codec (the 1 Mbit/s bus is
4296 * hardware-gated). Default off.
4297 */
4298#ifndef PC_ENABLE_MBPLUS
4299#define PC_ENABLE_MBPLUS 0
4300#endif
4301
4302/**
4303 * @brief Opt-in INTERBUS summation-frame fieldbus codec (PC_ENABLE_INTERBUS).
4304 *
4305 * When set, services/fieldbus/interbus assembles/disassembles the INTERBUS summation frame (loopback word +
4306 * per-device 16-bit process-image slices + CRC-16/CCITT FCS) of the Phoenix Contact ring fieldbus,
4307 * where every device is a shift-register slice of one circulating frame. Pure codec (the physical ring
4308 * clocking is hardware-gated). Default off.
4309 */
4310#ifndef PC_ENABLE_INTERBUS
4311#define PC_ENABLE_INTERBUS 0
4312#endif
4313
4314/**
4315 * @brief Opt-in ICCP / TASE.2 (IEC 60870-6) inter-control-center telemetry codec (PC_ENABLE_ICCP).
4316 *
4317 * When set, services/energy/iccp builds the TASE.2 Data_Value BER structures - StateQ (a discrete state +
4318 * quality) and RealQ (a scaled real + quality), each with an optional timestamp - the indication points
4319 * a control center transfers as MMS Reads (on the shipped services/energy/mms + services/fieldbus/cotp). Pure BER
4320 * codec. Default off.
4321 */
4322#ifndef PC_ENABLE_ICCP
4323#define PC_ENABLE_ICCP 0
4324#endif
4325
4326/**
4327 * @brief Opt-in IEEE 1609 WAVE (WSMP + 1609.2 envelope) codec (PC_ENABLE_WAVE).
4328 *
4329 * When set, services/transportation/wave builds/parses the IEEE 1609 vehicular-radio framing that carries J2735: the
4330 * 1609.3 WSMP header (version + P-encoded PSID + length + payload) and the 1609.2 secured-message
4331 * envelope header (version + content type). Pairs with services/j2735. Pure codec (the DSRC / C-V2X
4332 * radio is an external module). Default off.
4333 */
4334#ifndef PC_ENABLE_WAVE
4335#define PC_ENABLE_WAVE 0
4336#endif
4337
4338/**
4339 * @brief Opt-in UTMC (Urban Traffic Management and Control) common-database codec (PC_ENABLE_UTMC).
4340 *
4341 * When set, services/transportation/utmc builds/parses the UTMC common-database HTTP+XML messages - a UTMCRequest for
4342 * an object id and a UTMCResponse carrying the object value + a data-quality flag + a timestamp - the UK
4343 * modular framework for sharing traffic data across municipal systems, over the existing HTTP server.
4344 * Pure text framing. Default off.
4345 */
4346#ifndef PC_ENABLE_UTMC
4347#define PC_ENABLE_UTMC 0
4348#endif
4349
4350/**
4351 * @brief Opt-in OCIT-Outstations message codec (PC_ENABLE_OCIT).
4352 *
4353 * When set, services/transportation/ocit builds/parses the OCIT (DE/AT/CH road-traffic-control) object messages
4354 * ([msg-type][object-type][instance][data-type][value]) between central traffic computers and field
4355 * controllers / detectors, with typed values (bool / byte / u16 / u32 / octets). Pure codec (the OCIT
4356 * transport is the shipped transport). Default off.
4357 */
4358#ifndef PC_ENABLE_OCIT
4359#define PC_ENABLE_OCIT 0
4360#endif
4361
4362/**
4363 * @brief Opt-in ATC (Advanced Traffic Controller) field-I/O interop snapshot (PC_ENABLE_ATC).
4364 *
4365 * When set, services/machine_tool/atc exposes this device's field-I/O (a fixed table of named input/output points it
4366 * already gathers via the NTCIP / NEMA-TS2 / gpio services) to an ATC Linux engine over the existing
4367 * HTTP surface: pc_atc_snapshot_json serializes the FIO map as JSON, and pc_atc_set_output drives
4368 * an output point from an ATC command. Pure interop codec (ATC is a platform spec, not a wire protocol).
4369 * Default off.
4370 */
4371#ifndef PC_ENABLE_ATC
4372#define PC_ENABLE_ATC 0
4373#endif
4374
4375/**
4376 * @brief Opt-in southbound protocol-driver framework (PC_ENABLE_SOUTHBOUND).
4377 *
4378 * The uniform seam every field-device driver plugs into so the app polls/drives any southbound device
4379 * (a Modbus slave, a BACnet controller, a raw sensor over SPI/I2C/UART) through one facade: register a
4380 * SouthboundDriver (a read/write/read_block/write_block vtable + its transport ctx), then address points
4381 * by driver name via pc_southbound_read / _write / _read_block / _write_block. The block calls are the
4382 * atomic multi-point (register-matrix) path. Bounded registry (PC_SOUTHBOUND_MAX_DRIVERS, default 8),
4383 * no heap; Modbus master is the one such driver today. Default off.
4384 */
4385#ifndef PC_ENABLE_SOUTHBOUND
4386#define PC_ENABLE_SOUTHBOUND 0
4387#endif
4388
4389/**
4390 * @brief Opt-in ESP32 panic / exception decoder for a live diagnostics panel (PC_ENABLE_EXC_DECODER).
4391 *
4392 * When set, services/system/exc_decoder parses a captured Guru Meditation panic dump (the cause, the register
4393 * PC + EXCVADDR, and the backtrace PC:SP frames) into a structured ExcInfo and serializes it as JSON for
4394 * a "/exception" panel; the browser or a build server resolves the PCs to file:line against the firmware
4395 * ELF (addr2line lives off-device). Pure, no heap/stdlib. Default off.
4396 */
4397#ifndef PC_ENABLE_EXC_DECODER
4398#define PC_ENABLE_EXC_DECODER 0
4399#endif
4400
4401/**
4402 * @brief Chunk the core-dump image is streamed out of flash in (PC_EXC_COREDUMP_CHUNK).
4403 *
4404 * pc_exc_coredump_save() copies the partition to a file this many bytes at a time from a stack
4405 * buffer, so a dump of any size costs no heap and never has to fit RAM.
4406 */
4407#ifndef PC_EXC_COREDUMP_CHUNK
4408#define PC_EXC_COREDUMP_CHUNK 512
4409#endif
4410
4411#if PC_ENABLE_EXC_DECODER && (PC_EXC_COREDUMP_CHUNK < 64)
4412#error "ProtoCore: PC_EXC_COREDUMP_CHUNK must be >= 64"
4413#endif
4414
4415/**
4416 * @brief Opt-in HTTP delivery optimizations (PC_ENABLE_HTTP_DELIVERY).
4417 *
4418 * Three pure cores for cheaper HTTP serving, each a real web standard: RFC 5861 stale-while-revalidate
4419 * (pc_delivery_swr decision + pc_delivery_cache_control header), RFC 7233 byte-range delta/offset
4420 * fetch (pc_delivery_range parse of X-Y / X- / -N + pc_delivery_content_range for a 206), and a
4421 * versioned service-worker precache manifest (pc_delivery_sw_manifest). No heap/stdlib. Default off.
4422 */
4423#ifndef PC_ENABLE_HTTP_DELIVERY
4424#define PC_ENABLE_HTTP_DELIVERY 0
4425#endif
4426
4427/**
4428 * @brief Most asset paths a service-worker precache manifest may list (PC_DELIVERY_PRECACHE_MAX).
4429 */
4430#ifndef PC_DELIVERY_PRECACHE_MAX
4431#define PC_DELIVERY_PRECACHE_MAX 16
4432#endif
4433
4434/**
4435 * @brief Buffer the precache manifest JSON is built into (PC_DELIVERY_MANIFEST_BUF).
4436 *
4437 * Must hold `{"version":"..","precache":[..]}` for PC_DELIVERY_PRECACHE_MAX paths; the manifest
4438 * route answers 500 rather than truncating if it does not fit.
4439 */
4440#ifndef PC_DELIVERY_MANIFEST_BUF
4441#define PC_DELIVERY_MANIFEST_BUF 512
4442#endif
4443
4444#if PC_ENABLE_HTTP_DELIVERY && (PC_DELIVERY_PRECACHE_MAX < 1)
4445#error "ProtoCore: PC_DELIVERY_PRECACHE_MAX must be >= 1"
4446#endif
4447#if PC_ENABLE_HTTP_DELIVERY && (PC_DELIVERY_MANIFEST_BUF < 64)
4448#error "ProtoCore: PC_DELIVERY_MANIFEST_BUF must be >= 64"
4449#endif
4450
4451/**
4452 * @brief Opt-in hardware-health diagnostics (PC_ENABLE_HW_HEALTH).
4453 *
4454 * Four pure decision cores fed with samples the app reads from the hardware: a power-rail voltage-drop
4455 * logger (pc_hwhealth_rail_sample tracks worst droop + sag/brownout counts), a SPI-bus CRC audit with
4456 * hysteretic clock backoff (pc_hwhealth_spi_result halves/doubles the clock on fail/ok streaks), a
4457 * GPIO short-circuit test (pc_hwhealth_gpio_short: driven vs readback), and a capacitor-leakage diag
4458 * (pc_hwhealth_cap_leak: measured vs expected RC decay). No heap/stdlib. Default off.
4459 */
4460#ifndef PC_ENABLE_HW_HEALTH
4461#define PC_ENABLE_HW_HEALTH 0
4462#endif
4463
4464/**
4465 * @brief Opt-in adaptive mDNS beacon scheduling (PC_ENABLE_MDNS_ADAPTIVE).
4466 *
4467 * Pure scheduling decisions on top of the shipped mDNS service: pc_mdns_beacon_adapt backs the
4468 * announce interval off toward a ceiling under RF contention and recovers it when the air is quiet,
4469 * pc_mdns_refresh_interval gives the TTL/2 continuous-refresher cadence, pc_mdns_beacon_due says
4470 * when an announce is due, and pc_mdns_beacon_presleep_due says whether to announce before a sleep
4471 * window that would otherwise let the record lapse. Wrap-safe time math, no heap/stdlib. Default off.
4472 */
4473#ifndef PC_ENABLE_MDNS_ADAPTIVE
4474#define PC_ENABLE_MDNS_ADAPTIVE 0
4475#endif
4476
4477/**
4478 * @brief Opt-in dynamic socket recycling: an LRU connection-slot pool (PC_ENABLE_SOCKPOOL).
4479 *
4480 * The transport-pool half of the adaptive-networking work: services/net/sockpool keeps a fixed table of
4481 * connection slots and, when saturated, recycles the least-recently-used slot for a new peer
4482 * (pc_sockpool_acquire returns the evicted id so the transport closes it), plus touch / release /
4483 * find. The app owns the real sockets; this owns which slot a connection lives in and which to reclaim
4484 * under pressure. No heap/stdlib. Default off.
4485 */
4486#ifndef PC_ENABLE_SOCKPOOL
4487#define PC_ENABLE_SOCKPOOL 0
4488#endif
4489
4490/**
4491 * @brief Opt-in buffer placement policy (DRAM vs PSRAM) + SPI DMA ping-pong manager (PC_ENABLE_PSRAM_POOL).
4492 *
4493 * Pure buffer-management decisions for a PSRAM-equipped ESP32: pc_psram_place picks DRAM vs PSRAM for
4494 * a buffer by size, DMA requirement, and free-heap headroom (large/cold to PSRAM, small/hot + DMA to
4495 * DRAM, always leaving an internal-DRAM reserve), and pc_pingpong_* keeps the classic SPI DMA
4496 * double-buffer bookkeeping (CPU fills one buffer while DMA drains the other; swap flips their roles).
4497 * The actual heap_caps_calloc is the app's. No heap/stdlib. Default off.
4498 */
4499#ifndef PC_ENABLE_PSRAM_POOL
4500#define PC_ENABLE_PSRAM_POOL 0
4501#endif
4502
4503/**
4504 * @brief Opt-in dual-stack Happy Eyeballs destination selection (PC_ENABLE_HAPPY_EYEBALLS).
4505 *
4506 * The client-side IPv6/IPv4 fallback decision on top of the shipped pc_ip: pc_he_pref scores a
4507 * destination (RFC 6724 scope + family), pc_he_order sorts a candidate list and interleaves the
4508 * address families (RFC 8305) so successive connection attempts alternate v6/v4, and
4509 * pc_he_attempt_due gates the next attempt by the Connection Attempt Delay. Fast IPv6 when it works,
4510 * quick fallback to IPv4 when it does not. Needs PC_ENABLE_IPV6 to matter. No heap/stdlib. Default off.
4511 */
4512#ifndef PC_ENABLE_HAPPY_EYEBALLS
4513#define PC_ENABLE_HAPPY_EYEBALLS 0
4514#endif
4515
4516/**
4517 * @brief Opt-in 802.11 sniffer / traffic analyzer (PC_ENABLE_WIFI_SNIFFER).
4518 *
4519 * The decode + decision layer for a promiscuous-mode WiFi sniffer: pc_wifi_parse decodes an 802.11
4520 * MAC header (frame-control type/subtype + flags and the addresses whose roles depend on ToDS/FromDS),
4521 * pc_wifi_stats_* tallies frames by type for a traffic panel, and pc_wifi_should_roam decides when
4522 * a candidate AP is enough stronger (RSSI hysteresis) to justify channel-agility roaming. On top of
4523 * that, pc_wifi_scan_* is the channel-hop dwell schedule and pc_wifi_survey_* is the per-channel
4524 * RSSI/traffic survey that supplies the roam candidate. With PC_ENABLE_PROMISC also set, the live
4525 * binding (pc_wifi_sniffer_begin / _tick / _end) drives the promiscuous-capture owner
4526 * (services/radio/promisc) so a channel-hopping sniff runs on hardware. No heap/stdlib. Default off.
4527 */
4528#ifndef PC_ENABLE_WIFI_SNIFFER
4529#define PC_ENABLE_WIFI_SNIFFER 0
4530#endif
4531
4532/**
4533 * @brief Channels tracked by the WiFi sniffer's per-channel survey (PC_WIFI_SNIFFER_MAX_CHANNELS).
4534 *
4535 * 14 covers the full 2.4 GHz channel plan (1-14); lower it to the channels actually swept to shrink
4536 * the survey table (each entry is 11 bytes).
4537 */
4538#ifndef PC_WIFI_SNIFFER_MAX_CHANNELS
4539#define PC_WIFI_SNIFFER_MAX_CHANNELS 14
4540#endif
4541
4542#if PC_ENABLE_WIFI_SNIFFER && ((PC_WIFI_SNIFFER_MAX_CHANNELS < 1) || (PC_WIFI_SNIFFER_MAX_CHANNELS > 14))
4543#error "ProtoCore: PC_WIFI_SNIFFER_MAX_CHANNELS must be 1..14"
4544#endif
4545
4546/**
4547 * @brief Opt-in multi-interface egress selection / failover policy (PC_ENABLE_LINK_MANAGER).
4548 *
4549 * The policy that drives which interface carries traffic once a device has more than one (a wired
4550 * Ethernet PHY alongside WiFi STA / softAP): services/system/link_manager keeps a small table of interfaces
4551 * (kind + priority + up/down) and deterministically selects the best link that is up, escalating to a
4552 * higher-priority interface when it comes up and failing over when it drops, reporting only real
4553 * transitions so the app reconfigures the netif once. The PHY bring-up stays the app's. No
4554 * heap/stdlib. Default off.
4555 */
4556#ifndef PC_ENABLE_LINK_MANAGER
4557#define PC_ENABLE_LINK_MANAGER 0
4558#endif
4559
4560/**
4561 * @brief Opt-in CC1101 sub-GHz radio driver (PC_ENABLE_CC1101).
4562 *
4563 * A gateway radio plugin (PC_ENABLE_GATEWAY) for the TI CC1101 300-928 MHz transceiver over SPI:
4564 * services/radio/cc1101 drives the chip's SPI header protocol (config registers, command strobes, status
4565 * registers, TX/RX FIFO) - reset + apply a SmartRF register table + set channel + verify VERSION
4566 * (pc_cc1101_init), send a variable-length packet (pc_cc1101_send), poll TX-done, enter RX, and read a packet
4567 * with appended RSSI/LQI (pc_cc1101_recv), plus the RSSI-to-dBm decode. The huge modem config is a
4568 * caller-supplied register table. Host-tested against a mock; the RF link needs the module. Default off.
4569 */
4570#ifndef PC_ENABLE_CC1101
4571#define PC_ENABLE_CC1101 0
4572#endif
4573
4574/**
4575 * @brief Opt-in FDC2114/2214 capacitance-to-digital field sensor (PC_ENABLE_FDC2214).
4576 *
4577 * A field-perturbation sensing peripheral: services/peripherals/fdc2214 decodes the FDC2x14's 28-bit conversion
4578 * result (a capacitance shift moves the LC-tank frequency, giving contactless proximity / liquid-level /
4579 * material sensing) - pc_fdc2214_data combines the register pair, pc_fdc2214_error pulls the flags,
4580 * pc_fdc2214_sensor_freq_hz scales to frequency, and pc_fdc2214_build_config emits a single-channel
4581 * bring-up; the ESP32 binding replays it and reads the channel over I2C. Pure codec host-tested. Default off.
4582 */
4583#ifndef PC_ENABLE_FDC2214
4584#define PC_ENABLE_FDC2214 0
4585#endif
4586
4587/**
4588 * @brief Opt-in LDC1614 inductance-to-digital field sensor (PC_ENABLE_LDC1614).
4589 *
4590 * A field-perturbation sensing peripheral: services/peripherals/ldc1614 decodes the LDC1614's 28-bit conversion
4591 * result (a nearby conductor changes the coil inductance via eddy currents, giving contactless metal
4592 * proximity / displacement / EM-field sensing) - pc_ldc1614_data combines the register pair, pc_ldc1614_error
4593 * pulls the flags, pc_ldc1614_sensor_freq_hz scales to frequency, and pc_ldc1614_build_config emits a
4594 * single-channel bring-up; the ESP32 binding replays it and reads the channel over I2C. Pure codec
4595 * host-tested. Default off.
4596 */
4597#ifndef PC_ENABLE_LDC1614
4598#define PC_ENABLE_LDC1614 0
4599#endif
4600
4601/**
4602 * @brief Opt-in VL53L0X optical time-of-flight ranging sensor (PC_ENABLE_VL53L0X).
4603 *
4604 * A field-perturbation sensing peripheral for contactless distance / gesture: services/peripherals/vl53l0x decodes
4605 * the ST VL53L0X ranging registers - pc_vl53l0x_range_mm combines the range byte pair, pc_vl53l0x_data_ready
4606 * decodes the interrupt-status byte, and pc_vl53l0x_range_valid checks the device range-status field; the
4607 * ESP32 binding verifies the model id, starts continuous ranging, and reads the distance over I2C.
4608 * Default-settings ranging (ST's tuning blob is not applied). Pure codec host-tested. Default off.
4609 */
4610#ifndef PC_ENABLE_VL53L0X
4611#define PC_ENABLE_VL53L0X 0
4612#endif
4613
4614/**
4615 * @brief Opt-in receive-only radio channel sniffer to pcap (PC_ENABLE_RADIO_SNIFF).
4616 *
4617 * Feeds frames pulled off the air by the RF gateway drivers (CC1101 / LoRa / 802.15.4) in receive-only
4618 * mode into the capture pipeline: services/radio/radio_sniff wraps each 802.15.4 MAC frame in the Wireshark
4619 * TAP pseudo-header (carrying per-frame RSSI + channel) and a pcap record so the forwarded stream is a
4620 * valid .pcap. pc_radiosniff_global writes the DLT-TAP global header and pc_radiosniff_tap_record
4621 * writes one record. Pure framing (no heap/stdlib); the radio drivers own the receive. Default off.
4622 */
4623#ifndef PC_ENABLE_RADIO_SNIFF
4624#define PC_ENABLE_RADIO_SNIFF 0
4625#endif
4626
4627/**
4628 * @brief Opt-in Bluetooth ATT protocol codec + GATT characteristic bridge (PC_ENABLE_BLE_GATT).
4629 *
4630 * The wire protocol under GATT for bridging the on-chip BLE radio to the web: services/radio/ble_gatt builds
4631 * and parses the common ATT PDUs (read / write / notify / error, Bluetooth Core Vol 3 Part F) and
4632 * serializes a GATT characteristic table as JSON for the web stack (att_read_req / att_write_req /
4633 * att_notify / att_error_rsp / att_parse / pc_gatt_char_json). The BLE stack owns the radio; this owns the
4634 * ATT bytes + the northbound JSON. Pure, no heap/stdlib. Default off.
4635 */
4636#ifndef PC_ENABLE_BLE_GATT
4637#define PC_ENABLE_BLE_GATT 0
4638#endif
4639
4640/**
4641 * @brief Opt-in TLS version negotiation + pinned cipher-suite policy (PC_ENABLE_TLS_POLICY).
4642 *
4643 * A policy layer on top of the mbedTLS-backed transport TLS (which already runs the 1.2 / 1.3 record +
4644 * handshake): services/security/tls_policy pins the version to an audited [min,max] and makes the negotiated
4645 * version observable (pc_tls_negotiate_version / pc_tls_version_name), and pins the cipher suites
4646 * to an audited allowlist selected by server preference (pc_tls_select_cipher), with an AEAD-only
4647 * classifier (pc_tls_is_aead) for a hardened profile. Pure, host-tested; the app feeds the results to
4648 * the mbedTLS config. Default off.
4649 */
4650#ifndef PC_ENABLE_TLS_POLICY
4651#define PC_ENABLE_TLS_POLICY 0
4652#endif
4653
4654/**
4655 * @brief Opt-in Wi-SUN FAN border-router connector (PC_ENABLE_WISUN).
4656 *
4657 * Wi-SUN FAN is an IPv6/UDP/CoAP mesh terminated by a border router, so the connector rides the existing
4658 * IP stack rather than driving a radio: services/radio/wisun keeps a table of FAN nodes (their pc_ip addresses +
4659 * join state) behind the border router and builds the CoAP client requests to their resources
4660 * (pc_wisun_build_coap frames an RFC 7252 header + Uri-Path options + payload; the CoAP service ships only a
4661 * server). pc_wisun_nodes_json exposes the mesh to the web. The app sends the built PDU over pc_udp; the
4662 * chosen devboard only sets which border router you point at. Pure, no heap/stdlib. Default off.
4663 */
4664#ifndef PC_ENABLE_WISUN
4665#define PC_ENABLE_WISUN 0
4666#endif
4667
4668/**
4669 * @brief Opt-in fixed-RAM rotating log buffer with severity traps (PC_ENABLE_LOGBUF).
4670 *
4671 * Default off. When set, services/system/logbuf keeps the last PC_LOG_LINES log lines
4672 * in a fixed ring (oldest pruned on overflow - no heap, bounded), dumps them
4673 * oldest-first for a `/logs` endpoint, and fires a trap callback when a line is
4674 * logged at/above a severity threshold (forward criticals as an SNMP trap /
4675 * webhook). The ring + trap logic is pure and host-tested.
4676 */
4677#ifndef PC_ENABLE_LOGBUF
4678#define PC_ENABLE_LOGBUF 0
4679#endif
4680
4681/** @brief Number of log lines retained in the ring. */
4682#ifndef PC_LOG_LINES
4683#define PC_LOG_LINES 32
4684#endif
4685
4686/** @brief Maximum length of one stored log line (bytes, including null). */
4687#ifndef PC_LOG_LINE_LEN
4688#define PC_LOG_LINE_LEN 96
4689#endif
4690
4691/**
4692 * @brief Compile-time severity floor for the PC_LOG* macros (shared_primitives/log.h).
4693 *
4694 * The values are ordered low -> high and match pc_log_level's, so a level is usable both in the
4695 * preprocessor (which cannot see a constexpr) and as the runtime argument. PC_NONE sits above ERROR
4696 * so that the default discards everything.
4697 */
4698#define PC_LOG_LEVEL_DEBUG 0
4699#define PC_LOG_LEVEL_INFO 1
4700#define PC_LOG_LEVEL_WARN 2
4701#define PC_LOG_LEVEL_ERROR 3
4702#define PC_LOG_LEVEL_NONE 4
4703
4704/**
4705 * @brief Lowest severity the PC_LOG* macros emit code for.
4706 *
4707 * A call below this floor is discarded by the preprocessor: no call, no formatting, and no format
4708 * string left in flash, because the discarded form only names its arguments inside `sizeof` - an
4709 * unevaluated context that still type-checks them. So instrumentation can be left in the source
4710 * permanently and costs exactly nothing in a build that does not want it, which is the point.
4711 *
4712 * Defaults to PC_NONE: opt in per build (e.g. -DPC_LOG_LEVEL=PC_LOG_LEVEL_WARN).
4713 */
4714#ifndef PC_LOG_LEVEL
4715#define PC_LOG_LEVEL PC_LOG_LEVEL_NONE
4716#endif
4717
4718#if (PC_LOG_LEVEL < PC_LOG_LEVEL_DEBUG) || (PC_LOG_LEVEL > PC_LOG_LEVEL_NONE)
4719#error "ProtoCore: PC_LOG_LEVEL must be one of the PC_LOG_LEVEL_* constants"
4720#endif
4721
4722/**
4723 * @brief Opt-in schema-driven config export / restore (PC_ENABLE_CONFIG_IO).
4724 *
4725 * Default off. Requires PC_ENABLE_CONFIG_STORE. The app declares a fixed schema
4726 * (key + type); services/storage/config_io serializes the current values to a portable
4727 * `key=value` text blob (backup / migrate) and parses one back into the store
4728 * (restore / bulk template). Schema-driven rather than enumerating NVS, so it
4729 * stays deterministic and zero-heap; the serialize / parse is host-tested.
4730 */
4731#ifndef PC_ENABLE_CONFIG_IO
4732#define PC_ENABLE_CONFIG_IO 0
4733#endif
4734
4735/** @brief Authenticated OTA firmware update (streaming POST to the ESP32 Update API). */
4736#ifndef PC_ENABLE_OTA
4737#define PC_ENABLE_OTA 0
4738#endif
4739
4740/**
4741 * @brief Opt-in OTA rollback protection / soft-brick safeguard (PC_ENABLE_OTA_ROLLBACK).
4742 *
4743 * Default off. After an OTA update the new image boots in PENDING_VERIFY; this
4744 * service confirms it (esp_ota_mark_app_valid) once a self-test passes, or rolls
4745 * back to the previous image if the self-test fails or the confirm window elapses
4746 * without success - so a bad update self-heals instead of soft-bricking. The
4747 * decision logic is pure and host-tested; the commit / rollback use esp_ota_ops.
4748 * Requires the bootloader's app-rollback support (CONFIG_BOOTLOADER_APP_ROLLBACK_ENABLE).
4749 */
4750#ifndef PC_ENABLE_OTA_ROLLBACK
4751#define PC_ENABLE_OTA_ROLLBACK 0
4752#endif
4753
4754/** @brief Confirm window (ms): a pending image not confirmed within this rolls back. */
4755#ifndef PC_OTA_CONFIRM_WINDOW_MS
4756#define PC_OTA_CONFIRM_WINDOW_MS 30000
4757#endif
4758
4759/**
4760 * @brief Opt-in TOTP two-factor auth (RFC 6238) (PC_ENABLE_TOTP).
4761 *
4762 * Default off. services/security/totp computes and verifies time-based one-time passwords
4763 * (HMAC-SHA1 over the existing SHA-1, Google Authenticator compatible) and decodes
4764 * base32 shared secrets, for a second factor on top of password / JWT auth. Pure
4765 * and host-tested against the RFC 6238 vectors; the verifier checks a +/- step
4766 * window for clock skew.
4767 */
4768#ifndef PC_ENABLE_TOTP
4769#define PC_ENABLE_TOTP 0
4770#endif
4771
4772/**
4773 * @brief Opt-in outbound webhooks / IFTTT (PC_ENABLE_WEBHOOK).
4774 *
4775 * Default off. Needs PC_ENABLE_HTTP_CLIENT to actually send: the API always
4776 * compiles, but without the HTTP client pc_webhook_post() returns -1.
4777 * services/net/webhook builds an IFTTT Maker URL and a value1/value2/value3 JSON
4778 * payload (pure, host-tested) and fires them - or any JSON to any URL - via the
4779 * outbound http_client (POST). Use it to
4780 * push an event from the device to IFTTT, a Slack/Discord hook, or your own API.
4781 */
4782#ifndef PC_ENABLE_WEBHOOK
4783#define PC_ENABLE_WEBHOOK 0
4784#endif
4785
4786/**
4787 * @brief Opt-in radio power controls (PC_ENABLE_RADIO_POWER).
4788 *
4789 * Default off. services/system/radio_power applies the WiFi modem-sleep mode and an
4790 * optional max-TX-power cap in one call - trade throughput/latency for lower average
4791 * power on a battery device. The mode names are host-tested; the apply needs a vendor
4792 * radio backend.
4793 */
4794#ifndef PC_ENABLE_RADIO_POWER
4795#define PC_ENABLE_RADIO_POWER 0
4796#endif
4797
4798/** @brief WiFi modem-sleep mode: 0 = none (max perf), 1 = min modem, 2 = max modem. */
4799#ifndef PC_RADIO_WIFI_PS
4800#define PC_RADIO_WIFI_PS 0
4801#endif
4802
4803/** @brief Max TX power cap in dBm (2..20); 0 = leave the platform default. */
4804#ifndef PC_RADIO_MAX_TX_DBM
4805#define PC_RADIO_MAX_TX_DBM 0
4806#endif
4807
4808/**
4809 * @brief Opt-in DNS resolver with answer verification (PC_ENABLE_DNS_RESOLVER).
4810 *
4811 * Default off. services/net/dns_resolver resolves a hostname to an IPv4 address (lwIP
4812 * dns_gethostbyname, marshalled to tcpip_thread like the http_client) and can
4813 * reject suspicious answers - 0.0.0.0, broadcast, loopback, multicast - which are
4814 * spoofing / DNS-rebinding indicators for a remote host. The address classifier /
4815 * verifier is pure and host-tested; the resolve is ESP32-only (blocking, so call
4816 * it off the request hot path).
4817 */
4818#ifndef PC_ENABLE_DNS_RESOLVER
4819#define PC_ENABLE_DNS_RESOLVER 0
4820#endif
4821
4822/** @brief DNS resolve timeout in milliseconds. */
4823#ifndef PC_DNS_TIMEOUT_MS
4824#define PC_DNS_TIMEOUT_MS 5000
4825#endif
4826
4827/**
4828 * @brief Tamper-evident audit log (PC_ENABLE_AUDIT_LOG).
4829 *
4830 * Default off. services/security/audit_log keeps an append-only, hash-chained security
4831 * log: each record carries SHA-256(prev_hash || fields), so altering, deleting,
4832 * or reordering any retained record breaks the chain (pc_audit_verify()
4833 * detects it). Storage is a fixed RAM ring of PC_AUDIT_LOG_ENTRIES records
4834 * (no heap); when it wraps, a moving anchor keeps the retained window verifiable.
4835 * Install a sink (pc_audit_set_sink) to forward every record at creation time
4836 * to a durable / remote store - SD-card file, syslog or HTTP log service, serial
4837 * console - preserving the same chain off-device. Pure and host-tested.
4838 */
4839#ifndef PC_ENABLE_AUDIT_LOG
4840#define PC_ENABLE_AUDIT_LOG 0
4841#endif
4842
4843// Ring depth and per-record message length are tunable in audit_log.h
4844// (PC_AUDIT_LOG_ENTRIES, PC_AUDIT_MSG_LEN); define them before include to
4845// override. The RAM cost is roughly PC_AUDIT_LOG_ENTRIES * (PC_AUDIT_MSG_LEN
4846// + 41) bytes.
4847
4848/**
4849 * @brief OpenID Connect ID-token verification, RS256 (PC_ENABLE_OIDC).
4850 *
4851 * Default off. services/security/oidc verifies an OIDC ID token (JWT) as a relying party:
4852 * requires alg RS256, selects the issuer key by kid from a JWKS, verifies the
4853 * RSASSA-PKCS1-v1.5 SHA-256 signature (real RSA modexp via ssh_rsa, mbedTLS-
4854 * accelerated on ESP32), and checks iss / aud / exp / nbf, extracting sub / email.
4855 * Pure and host-tested; the caller fetches + caches the JWKS over HTTPS (off the
4856 * request hot path) and passes the JSON in. Builds on the SSH RSA primitive, not
4857 * the HS256 JWT module (services/security/jwt), so the two are independent.
4858 */
4859#ifndef PC_ENABLE_OIDC
4860#define PC_ENABLE_OIDC 0
4861#endif
4862
4863/** @brief Max accepted OIDC ID-token length (also sizes the Authorization buffer). */
4864#ifndef PC_OIDC_MAX_LEN
4865#define PC_OIDC_MAX_LEN 1600
4866#endif
4867
4868/**
4869 * @brief Unified virtual filesystem wrapper (PC_ENABLE_VFS).
4870 *
4871 * Default off. services/storage/vfs exposes one small file API (open/read/write/close,
4872 * exists/size/remove/rename, whole-file helpers) over a pluggable backend, so a
4873 * feature can target storage without knowing the medium. A built-in zero-heap RAM
4874 * backend (fixed BSS pool - deterministic, host-identical) ships for scratch /
4875 * tests; an Arduino-FS backend (ESP32) wraps a real fs::FS (LittleFS / SD /
4876 * SPIFFS) for persistence. Mount one at startup; the API fails closed otherwise.
4877 * Pool dimensions are tunable in this config (PC_VFS_RAM_FILES, _RAM_FILE_SIZE,
4878 * _MAX_OPEN, _NAME_MAX).
4879 */
4880#ifndef PC_ENABLE_VFS
4881#define PC_ENABLE_VFS 0
4882#endif
4883
4884/**
4885 * @brief GraphQL query subset (PC_ENABLE_GRAPHQL).
4886 *
4887 * Default off. services/iot/graphql parses a GraphQL query into a fixed AST node pool
4888 * (no heap) and emits a `{"data":{...}}` response shaped exactly by the requested
4889 * selection. Schema-free: a field with a sub-selection is an object (the engine
4890 * recurses), a leaf field calls your single resolver, and arguments collected
4891 * along the path are handed to it. Supports nested selections, field arguments,
4892 * and the anonymous / `query` forms; mutations, subscriptions, fragments, and
4893 * variables are out of scope. Pure and host-tested; bounds are compile-time
4894 * (PC_GQL_* in this config). Serve it from a POST /graphql route.
4895 */
4896#ifndef PC_ENABLE_GRAPHQL
4897#define PC_ENABLE_GRAPHQL 0
4898#endif
4899
4900/**
4901 * @brief ESP-NOW peer messaging (PC_ENABLE_ESPNOW).
4902 *
4903 * Default off. services/radio/espnow wraps ESP-NOW connectionless peer-to-peer radio
4904 * messaging in a 3-byte typed envelope (magic + type + length) so a receiver can
4905 * demux by message type and reject a truncated frame, plus a bounded peer
4906 * registry (PC_ESPNOW_MAX_PEERS, no heap). The envelope codec + registry are
4907 * pure and host-tested; the radio path (begin / add_peer / send / broadcast over
4908 * esp_now, decoded frames to a callback) is ESP32-only and can bridge to
4909 * WebSocket/SSE. No stdlib.
4910 */
4911#ifndef PC_ENABLE_ESPNOW
4912#define PC_ENABLE_ESPNOW 0
4913#endif
4914
4915/**
4916 * @brief OAuth2 token-endpoint client (PC_ENABLE_OAUTH2).
4917 *
4918 * Default off. services/security/oauth2 obtains tokens - the counterpart to the OIDC
4919 * ID-token verifier. It builds the percent-encoded form body for the
4920 * authorization_code and refresh_token grants (RFC 6749), supporting a
4921 * confidential client (client_secret) or a public client with PKCE
4922 * (code_verifier, RFC 7636), and parses the JSON token response (reusing the
4923 * zero-heap JSON reader). The build + parse core is pure and host-tested; the POST
4924 * to the token endpoint uses the HTTP(S) client (needs PC_ENABLE_HTTP_CLIENT).
4925 * No heap, no stdlib.
4926 */
4927#ifndef PC_ENABLE_OAUTH2
4928#define PC_ENABLE_OAUTH2 0
4929#endif
4930
4931/**
4932 * @brief OPC UA Binary server (PC_ENABLE_OPCUA).
4933 *
4934 * Default off. services/fieldbus/opcua provides an OPC UA (IEC 62541) Binary server: the
4935 * little-endian built-in-type codec (incl. NodeId / ExtensionObject / DateTime /
4936 * Variant / DataValue / ReferenceDescription), UA-TCP (UACP) message framing, the
4937 * Hello/Acknowledge handshake, the SecureChannel (OpenSecureChannel, SecurityPolicy
4938 * None), the Session (CreateSession + ActivateSession), GetEndpoints, the Read, Write
4939 * and Browse services (registered resolvers map a NodeId to a value / accept a written
4940 * value / list child references), plus CloseSession + CloseSecureChannel and a
4941 * ServiceFault for unsupported services, served on TCP via ConnProto::PROTO_OPCUA
4942 * (`listen(4840, ConnProto::PROTO_OPCUA)`). The MSG framing is spec-faithful (incl.
4943 * SecureChannelId), so standard clients interoperate (verified with python asyncua:
4944 * connect + browse + read + write/read-back). All pure and host-tested. No heap, no stdlib.
4945 */
4946#ifndef PC_ENABLE_OPCUA
4947#define PC_ENABLE_OPCUA 0
4948#endif
4949
4950/**
4951 * @brief OPC UA Binary client (PC_ENABLE_OPCUA_CLIENT).
4952 *
4953 * Default off. Requires PC_ENABLE_OPCUA (shares the codec). services/pc_opcua_client
4954 * provides the client side of the OPC UA Binary protocol: request builders (Hello,
4955 * OpenSecureChannel, CreateSession, ActivateSession, Read, Browse, CloseSession,
4956 * CloseSecureChannel) and response parsers, reusing the opcua.h codec. It is
4957 * transport-agnostic - the app supplies the outbound socket (e.g. an Arduino
4958 * WiFiClient) and feeds bytes through these pure builders/parsers. No heap, no stdlib.
4959 */
4960#ifndef PC_ENABLE_OPCUA_CLIENT
4961#define PC_ENABLE_OPCUA_CLIENT 0
4962#endif
4963
4964/**
4965 * @brief umati - OPC UA for Machine Tools information model (PC_ENABLE_UMATI).
4966 *
4967 * Default off. Requires PC_ENABLE_OPCUA (builds on the OPC UA Binary server). services/machine_tool/umati
4968 * exposes the umati / OPC UA for Machine Tools companion model (OPC 40501-1, namespace
4969 * `http://opcfoundation.org/UA/MachineTool/`): a fixed MachineTool node hierarchy -
4970 * Identification, Monitoring (MachineTool / Channel / Spindle / Axis_X..Z), Production, and
4971 * Notification - served through the OPC UA Browse + Read resolvers out of a caller-owned
4972 * UmatiMachineTool struct you refresh each loop. Faithful BrowseNames per OPC 40501-1; a monitoring
4973 * (read-only) model any umati / OPC UA client browses and reads by BrowseName. No heap, no stdlib.
4974 */
4975#ifndef PC_ENABLE_UMATI
4976#define PC_ENABLE_UMATI 0
4977#endif
4978
4979/** @brief NamespaceIndex the umati MachineTool nodes live at (default 1). */
4980#ifndef PC_UMATI_NS
4981#define PC_UMATI_NS 1
4982#endif
4983
4984/**
4985 * @brief OPC UA for Robotics information model (PC_ENABLE_ROBOTICS).
4986 *
4987 * Default off. Requires PC_ENABLE_OPCUA (builds on the OPC UA Binary server). services/machine_tool/robotics
4988 * exposes the OPC UA for Robotics companion model (OPC 40010-1, namespace
4989 * `http://opcfoundation.org/UA/Robotics/`): a fixed MotionDeviceSystem node hierarchy -
4990 * MotionDevices (MotionDevice / ParameterSet / PC_ROBOTICS_AXES parametric Axes), Controllers
4991 * (Controller / Software), and SafetyStates (SafetyState / ParameterSet) - served through the OPC UA
4992 * Browse + Read resolvers out of a caller-owned RoboticsMotionDeviceSystem struct you refresh each loop.
4993 * Faithful BrowseNames per OPC 40010-1; a monitoring (read-only) model any OPC UA client browses and
4994 * reads by BrowseName. No heap, no stdlib. Same pattern as PC_ENABLE_UMATI.
4995 */
4996#ifndef PC_ENABLE_ROBOTICS
4997#define PC_ENABLE_ROBOTICS 0
4998#endif
4999
5000/** @brief NamespaceIndex the robotics MotionDeviceSystem nodes live at (default 1). */
5001#ifndef PC_ROBOTICS_NS
5002#define PC_ROBOTICS_NS 1
5003#endif
5004
5005/** @brief Number of Axes the robotics MotionDevice exposes (default 6; must fit PC_OPCUA_REF_MAX). */
5006#ifndef PC_ROBOTICS_AXES
5007#define PC_ROBOTICS_AXES 6
5008#endif
5009
5010/**
5011 * @brief EUROMAP 77 (OPC 40077) - OPC UA for injection moulding machines (IMM <-> MES) (PC_ENABLE_EUROMAP77).
5012 *
5013 * Default off. Requires PC_ENABLE_OPCUA (builds on the OPC UA Binary server). services/machine_tool/euromap77
5014 * exposes the EUROMAP 77 IMM_MES_Interface companion model (OPC 40077, namespace
5015 * `http://www.euromap.org/euromap77/`, enums from EUROMAP 83 / OPC 40083): a fixed node hierarchy -
5016 * MachineInformation, MachineStatus, and Jobs (ActiveJob + ActiveJobValues with the UInt64 production
5017 * counters) - served through the OPC UA Browse + Read resolvers out of a caller-owned EmImm struct you
5018 * refresh each loop. Faithful BrowseNames + a read-only monitoring model any OPC UA client browses and
5019 * reads by BrowseName. No heap, no stdlib. Same pattern as PC_ENABLE_UMATI / PC_ENABLE_ROBOTICS.
5020 */
5021#ifndef PC_ENABLE_EUROMAP77
5022#define PC_ENABLE_EUROMAP77 0
5023#endif
5024
5025/** @brief NamespaceIndex the EUROMAP 77 IMM_MES_Interface nodes live at (default 1). */
5026#ifndef PC_EM77_NS
5027#define PC_EM77_NS 1
5028#endif
5029
5030/**
5031 * @brief Streaming file upload: POST a body straight to a file on the filesystem.
5032 *
5033 * Default off. When set, src/services/file_transfer/upload_service/upload_service.h registers a POST route
5034 * that streams the request body directly into an Arduino FS file (LittleFS /
5035 * SPIFFS / SD) - the upload never has to fit in RAM. Reuses the same parser
5036 * streaming-body hook as OTA.
5037 *
5038 * For reliable streamed uploads the RX ring must hold at least one full TCP
5039 * receive window (RX_BUF_SIZE >= TCP_WND, ~5.7 KB by default): the transport
5040 * reopens the window only as the consumer drains the ring (ack-on-consume), so a
5041 * ring smaller than the window lets the peer overrun it and the transfer
5042 * deadlocks - you cannot advertise a window larger than your buffer. When a
5043 * streaming feature is enabled and RX_BUF_SIZE was left at its default, it is
5044 * automatically upsized below; an explicit RX_BUF_SIZE is honored as-is (set it
5045 * >= TCP_WND yourself). The 1024 default suits ordinary requests, not uploads.
5046 */
5047#ifndef PC_ENABLE_UPLOAD
5048#define PC_ENABLE_UPLOAD 0
5049#endif
5050
5051/**
5052 * @brief Internal: the parser's streaming-body machinery (OTA, file upload, WebDAV PUT).
5053 *
5054 * Each streams the request body to a sink instead of buffering it into body[]; the
5055 * parser support is shared and compiled when any of these features is enabled. The
5056 * sink is a single global hook, so only one streaming consumer is active per build
5057 * (the last to register wins) - do not combine OTA / upload / WebDAV streaming in
5058 * the same firmware.
5059 */
5060#if PC_ENABLE_OTA || PC_ENABLE_UPLOAD || PC_ENABLE_WEBDAV
5061#define PC_ENABLE_STREAM_BODY 1
5062#else
5063#define PC_ENABLE_STREAM_BODY 0
5064#endif
5065
5066// The RX-ring feature floors (streaming needs a full TCP window, SSH/TLS a full first flight) are
5067// resolved by board_drivers/board_profiles/derived_sizing.h, included at the end of this file once every feature
5068// flag is known - that is the sizing layer's job, not this file's.
5069
5070/** @brief First-boot WiFi provisioning: softAP + captive-portal credentials form. */
5071#ifndef PC_ENABLE_PROVISIONING
5072#define PC_ENABLE_PROVISIONING 0
5073#endif
5074
5075/**
5076 * @brief Syslog client (RFC 5424 over UDP).
5077 *
5078 * Default off. When set, the device can ship log lines to a remote syslog server
5079 * (e.g. rsyslog / journald / a SIEM) as RFC 5424 UDP datagrams via the
5080 * transport-layer UDP service - a zero-heap structured-logging sink for fleets
5081 * of constrained devices. See src/services/net/syslog/syslog.h.
5082 */
5083#ifndef PC_ENABLE_SYSLOG
5084#define PC_ENABLE_SYSLOG 0
5085#endif
5086
5087/** @brief Maximum formatted syslog datagram length in bytes (RFC 5424 line). */
5088#ifndef PC_SYSLOG_MSG_MAX
5089#define PC_SYSLOG_MSG_MAX 256
5090#endif
5091
5092/** @brief Maximum syslog HOSTNAME / APP-NAME field length (including NUL). */
5093#ifndef PC_SYSLOG_FIELD_MAX
5094#define PC_SYSLOG_FIELD_MAX 32
5095#endif
5096
5097/** @brief Default syslog collector UDP port (RFC 5426 well-known 514; overridable at runtime
5098 * via pc_syslog_init and here for a non-standard collector). */
5099#ifndef PC_SYSLOG_DEFAULT_PORT
5100#define PC_SYSLOG_DEFAULT_PORT 514
5101#endif
5102
5103/**
5104 * @brief JWT bearer-token authentication (HS256).
5105 *
5106 * Default off. When set, src/services/security/jwt/jwt.h verifies `Authorization: Bearer
5107 * <jwt>` tokens signed with HMAC-SHA-256 (reusing the SSH crypto layer) and can
5108 * read integer claims (e.g. `exp`) so a handler/middleware can gate routes on a
5109 * stateless token. Signature verification is constant-time.
5110 */
5111#ifndef PC_ENABLE_JWT
5112#define PC_ENABLE_JWT 0
5113#endif
5114
5115/** @brief Maximum accepted JWT length in bytes (header.payload.signature). */
5116#ifndef PC_JWT_MAX_LEN
5117#define PC_JWT_MAX_LEN 512
5118#endif
5119
5120/**
5121 * @brief Outbound HTTP(S) client (raw lwIP, optional client-side mbedTLS).
5122 *
5123 * Default off. When set, src/services/net/http_client/http_client.h can issue a
5124 * blocking GET/POST to a remote server: it resolves the host (DNS), opens a raw
5125 * lwIP TCP connection (https:// goes through client-side mbedTLS over the same
5126 * static arena as the server TLS), sends the request, and returns the status +
5127 * body in caller buffers. For webhooks, telemetry push, REST calls from the
5128 * device. The request builder + response parser are host-testable; the transport
5129 * is ESP32-only.
5130 */
5131#ifndef PC_ENABLE_HTTP_CLIENT
5132#define PC_ENABLE_HTTP_CLIENT 0
5133#endif
5134
5135/** @brief HTTPS client support inside the HTTP client (needs PC_ENABLE_TLS). */
5136#ifndef PC_ENABLE_HTTP_CLIENT_TLS
5137#define PC_ENABLE_HTTP_CLIENT_TLS 0
5138#endif
5139
5140/** @brief Receive buffer (and max response size) for the outbound HTTP client, bytes. */
5141#ifndef PC_HTTP_CLIENT_BUF_SIZE
5142#define PC_HTTP_CLIENT_BUF_SIZE 2048
5143#endif
5144
5145/**
5146 * @brief Ciphertext receive-ring size for the https:// client, bytes.
5147 *
5148 * The lwIP recv callback feeds TLS wire bytes into this draining ring while the
5149 * TLS engine pulls and decrypts them, so it holds only the in-flight (not yet
5150 * decrypted) ciphertext: a multi-KB handshake flight fits without loss thanks to
5151 * the refuse-and-redeliver backpressure. Must exceed one TCP segment (TCP_MSS,
5152 * ~1460) or a full segment could never fit. Only used when
5153 * PC_ENABLE_HTTP_CLIENT_TLS is set.
5154 */
5155#ifndef PC_HTTP_CLIENT_CT_BUF_SIZE
5156#define PC_HTTP_CLIENT_CT_BUF_SIZE 4096
5157#endif
5158
5159/** @brief Outbound HTTP client connect/response timeout in milliseconds. */
5160#ifndef PC_HTTP_CLIENT_TIMEOUT_MS
5161#define PC_HTTP_CLIENT_TIMEOUT_MS 8000
5162#endif
5163
5164/**
5165 * @brief Outbound SMTP client (RFC 5321) for device email alerts (services/net/smtp).
5166 *
5167 * A blocking one-shot `smtp_send()`: EHLO, optional AUTH LOGIN, MAIL FROM / RCPT TO /
5168 * DATA over the shared client transport (`pc_client`), with implicit TLS (SMTPS, e.g.
5169 * :465) when the message config sets `tls` and PC_ENABLE_TLS is on. Zero heap; the
5170 * dialogue engine (`smtp_run`) takes a send/recv seam so it is host-tested without lwIP.
5171 * "SMS fallback" rides on top - most carriers accept an email-to-SMS gateway address.
5172 */
5173#ifndef PC_ENABLE_SMTP
5174#define PC_ENABLE_SMTP 0
5175#endif
5176
5177/**
5178 * @brief Secure SMTP: run the mail client over client-side TLS (needs PC_ENABLE_TLS).
5179 *
5180 * Covers both SmtpSecurity::SMTP_TLS (implicit, port 465) and SmtpSecurity::SMTP_STARTTLS (the
5181 * in-band upgrade on the submission port, 587). Separate from PC_ENABLE_SMTP because the plain
5182 * codec needs neither the TLS stack nor the client-session singleton.
5183 */
5184#ifndef PC_ENABLE_SMTP_TLS
5185#define PC_ENABLE_SMTP_TLS 0
5186#endif
5187
5188#if PC_ENABLE_SMTP_TLS && !PC_ENABLE_SMTP
5189#error "ProtoCore: PC_ENABLE_SMTP_TLS requires PC_ENABLE_SMTP"
5190#endif
5191
5192/** @brief Max length of one SMTP command / address line (bytes, incl. CRLF). */
5193#ifndef PC_SMTP_LINE_MAX
5194#define PC_SMTP_LINE_MAX 256
5195#endif
5196
5197/** @brief Max size of the assembled DATA payload (headers + dot-stuffed body), bytes. */
5198#ifndef PC_SMTP_MSG_MAX
5199#define PC_SMTP_MSG_MAX 2048
5200#endif
5201
5202/** @brief Max size of one (possibly multi-line) server reply held while parsing, bytes. */
5203#ifndef PC_SMTP_REPLY_MAX
5204#define PC_SMTP_REPLY_MAX 512
5205#endif
5206
5207/** @brief SMTP connect / per-reply timeout in milliseconds. */
5208#ifndef PC_SMTP_TIMEOUT_MS
5209#define PC_SMTP_TIMEOUT_MS 10000
5210#endif
5211
5212/** @brief Ciphertext receive-ring size for SMTPS, bytes (only used when the message is TLS). */
5213#ifndef PC_SMTP_CT_BUF_SIZE
5214#define PC_SMTP_CT_BUF_SIZE 4096
5215#endif
5216
5217/**
5218 * @brief MQTT 3.1.1 publish/subscribe client (raw lwIP, optional MQTTS over TLS).
5219 *
5220 * Default off. When set, src/services/iot/mqtt/mqtt.h provides a persistent outbound
5221 * client: connect to a broker, PUBLISH (QoS 0/1/2) and SUBSCRIBE to topics, receive
5222 * incoming messages via a callback, with keep-alive pings - the dominant IoT
5223 * messaging pattern, for telemetry push and remote command. The packet codec is
5224 * host-testable; the transport (DNS + raw lwIP TCP, MQTTS via client-side mbedTLS)
5225 * is ESP32-only. Full QoS 0/1/2 (outbound DUP retransmit, inbound QoS-2
5226 * de-duplication by packet id) and Last-Will are supported.
5227 */
5228#ifndef PC_ENABLE_MQTT
5229#define PC_ENABLE_MQTT 0
5230#endif
5231
5232/** @brief MQTTS: run the MQTT client over client-side TLS (needs PC_ENABLE_TLS). */
5233#ifndef PC_ENABLE_MQTT_TLS
5234#define PC_ENABLE_MQTT_TLS 0
5235#endif
5236
5237/**
5238 * @brief MQTT packet buffer size in bytes (bounds one outgoing/incoming packet).
5239 *
5240 * Two buffers of this size live in BSS (one tx, one rx). Must hold the largest
5241 * CONNECT/PUBLISH the client sends and the largest incoming PUBLISH it accepts
5242 * (topic + payload + a few header bytes); larger incoming packets are dropped.
5243 */
5244#ifndef PC_MQTT_BUF_SIZE
5245#define PC_MQTT_BUF_SIZE 1024
5246#endif
5247
5248/** @brief Default MQTT keep-alive interval in seconds (PINGREQ cadence / CONNECT field). */
5249#ifndef PC_MQTT_KEEPALIVE_S
5250#define PC_MQTT_KEEPALIVE_S 30
5251#endif
5252
5253/** @brief Ciphertext receive-ring size for MQTTS (draining ring; must exceed one TCP_MSS). */
5254#ifndef PC_MQTT_CT_BUF_SIZE
5255#define PC_MQTT_CT_BUF_SIZE 4096
5256#endif
5257
5258/** @brief Maximum inbound MQTT topic length (including NUL) delivered to the callback. */
5259#ifndef PC_MQTT_MAX_TOPIC
5260#define PC_MQTT_MAX_TOPIC 128
5261#endif
5262
5263/**
5264 * @brief Outbound QoS 1/2 in-flight slots (unacknowledged messages held for DUP retransmit).
5265 *
5266 * Each slot stores its serialized packet (up to PC_MQTT_INFLIGHT_BUF bytes) until
5267 * the broker acknowledges it; a publish is refused when all slots are busy. The pool
5268 * costs PC_MQTT_MAX_INFLIGHT * (PC_MQTT_INFLIGHT_BUF + a few bytes) of BSS.
5269 */
5270#ifndef PC_MQTT_MAX_INFLIGHT
5271#define PC_MQTT_MAX_INFLIGHT 4
5272#endif
5273
5274/** @brief Stored-packet size per in-flight QoS 1/2 slot (caps a retransmittable PUBLISH). */
5275#ifndef PC_MQTT_INFLIGHT_BUF
5276#define PC_MQTT_INFLIGHT_BUF 256
5277#endif
5278
5279/** @brief Retransmit timeout (ms) for an unacknowledged in-flight QoS 1/2 message. */
5280#ifndef PC_MQTT_RETRANSMIT_MS
5281#define PC_MQTT_RETRANSMIT_MS 5000
5282#endif
5283
5284/** @brief Inbound QoS 2 packet-id de-duplication ring depth (PUBREC-acknowledged, awaiting PUBREL). */
5285#ifndef PC_MQTT_RX_QOS2_SLOTS
5286#define PC_MQTT_RX_QOS2_SLOTS 8
5287#endif
5288
5289/**
5290 * @brief Outbound WebSocket client (RFC 6455 over raw lwIP, optional wss:// TLS).
5291 *
5292 * Default off. When set, src/services/net/ws_client/ws_client.h connects to a remote
5293 * WebSocket endpoint (ws://, or wss:// over client-side mbedTLS), performs the
5294 * RFC 6455 client handshake (Sec-WebSocket-Key/Accept), and sends masked text /
5295 * binary frames + receives server frames via a callback - for streaming to cloud
5296 * dashboards or bidirectional control. The frame/handshake codec is host-testable.
5297 */
5298#ifndef PC_ENABLE_WS_CLIENT
5299#define PC_ENABLE_WS_CLIENT 0
5300#endif
5301
5302/** @brief wss://: run the WebSocket client over client-side TLS (needs PC_ENABLE_TLS). */
5303#ifndef PC_ENABLE_WS_CLIENT_TLS
5304#define PC_ENABLE_WS_CLIENT_TLS 0
5305#endif
5306
5307/** @brief WebSocket client send/receive buffer size in bytes (bounds one frame). */
5308#ifndef PC_WS_CLIENT_BUF_SIZE
5309#define PC_WS_CLIENT_BUF_SIZE 1024
5310#endif
5311
5312/** @brief Ciphertext receive-ring size for wss:// (draining ring; must exceed one TCP_MSS). */
5313#ifndef PC_WS_CLIENT_CT_BUF_SIZE
5314#define PC_WS_CLIENT_CT_BUF_SIZE 4096
5315#endif
5316
5317/**
5318 * @brief Internal: client-side TLS engine is compiled (HTTPS client, MQTTS, wss client, and/or a TLS edge-cache
5319 * origin).
5320 *
5321 * The outbound HTTP client (one-shot exchange) and the MQTT / WebSocket clients
5322 * and the edge cache's TLS origin fetch (persistent sessions) share the same
5323 * client mbedTLS code in pc_tls - the CA/pin trust config, the BIO typedefs,
5324 * and the session API - gated by this.
5325 */
5326#if PC_ENABLE_HTTP_CLIENT_TLS || PC_ENABLE_MQTT_TLS || PC_ENABLE_WS_CLIENT_TLS || PC_ENABLE_EDGE_ORIGIN_TLS || \
5327 PC_ENABLE_SMTP_TLS
5328#define PC_ENABLE_CLIENT_TLS 1
5329#else
5330#define PC_ENABLE_CLIENT_TLS 0
5331#endif
5332
5333// The outbound clients (pc_client) resolve hostnames through the shared DNS
5334// resolver (pc_dns_resolver_resolve), so enabling any client implies the resolver - one
5335// owner of the gethostbyname-marshal pattern instead of a private copy per client.
5336// PC_NEED_CLIENT marks when the client transport is actually used; the
5337// pc_client translation unit compiles its body only then (a server-only Arduino
5338// build that does not enable a client must not reference the resolver symbols).
5339// Every feature that drives the outbound client transport must pull it in: the direct callers
5340// (http_client / mqtt / ws_client / relay / smtp / ssh port-forward) and the seam-based engines
5341// whose shipped example binds the seam to pc_client (smb / dnc). Miss one and its pc_client_open
5342// resolves to the !NEED stub that returns -1, so the feature silently never connects on device.
5343#if PC_ENABLE_HTTP_CLIENT || PC_ENABLE_MQTT || PC_ENABLE_WS_CLIENT || PC_ENABLE_RELAY || PC_ENABLE_SMTP || \
5344 PC_SSH_PORT_FORWARD || PC_ENABLE_SMB || PC_ENABLE_DNC || PC_ENABLE_FTP_SESSION || PC_ENABLE_SSH_CLIENT
5345#define PC_NEED_CLIENT 1
5346#endif
5347#ifndef PC_NEED_CLIENT
5348#define PC_NEED_CLIENT 0
5349#endif
5350
5351// The client dials by name, so anything that needs the client needs the resolver.
5352#define PC_NEED_DNS_RESOLVER (PC_ENABLE_DNS_RESOLVER || PC_NEED_CLIENT)
5353
5354// ---------------------------------------------------------------------------
5355// Full Authorization-header capture (internal)
5356// ---------------------------------------------------------------------------
5357// Digest auth and JWT bearer tokens both carry an Authorization value far longer
5358// than MAX_VAL_LEN, so the parser captures the whole header into a dedicated
5359// per-request buffer (HttpReq::authorization) when either feature is enabled.
5360
5361/** @brief True when the parser must capture the full Authorization header value. */
5362#if PC_ENABLE_AUTH || PC_ENABLE_JWT || PC_ENABLE_OIDC
5363#define PC_CAPTURE_AUTH_HEADER 1
5364#else
5365#define PC_CAPTURE_AUTH_HEADER 0
5366#endif
5367
5368/**
5369 * @brief Capacity of HttpReq::authorization (full Authorization header value).
5370 *
5371 * Sized to the largest enabled consumer: a Digest header (DIGEST_AUTH_HDR_MAX), a
5372 * `Bearer <jwt>` HS256 token (PC_JWT_MAX_LEN), or a `Bearer <id_token>` OIDC
5373 * RS256 token (PC_OIDC_MAX_LEN), each plus the scheme.
5374 */
5375#if PC_ENABLE_OIDC
5376#define PC_AUTH_HDR_CAP_OIDC (PC_OIDC_MAX_LEN + 16)
5377#else
5378#define PC_AUTH_HDR_CAP_OIDC 0
5379#endif
5380#if PC_ENABLE_JWT
5381#define PC_AUTH_HDR_CAP_JWT (PC_JWT_MAX_LEN + 16)
5382#else
5383#define PC_AUTH_HDR_CAP_JWT 0
5384#endif
5385#define PC_AUTH_HDR_CAP_M1 (PC_AUTH_HDR_CAP_JWT > DIGEST_AUTH_HDR_MAX ? PC_AUTH_HDR_CAP_JWT : DIGEST_AUTH_HDR_MAX)
5386#define PC_AUTH_HDR_CAP (PC_AUTH_HDR_CAP_OIDC > PC_AUTH_HDR_CAP_M1 ? PC_AUTH_HDR_CAP_OIDC : PC_AUTH_HDR_CAP_M1)
5387
5388/** @brief Runtime stats endpoint (uptime, request/error counts, pool usage, heap). */
5389#ifndef PC_ENABLE_STATS
5390#define PC_ENABLE_STATS 0
5391#endif
5392
5393/**
5394 * @brief Transport-layer observability: connection event hook + counters.
5395 *
5396 * Default off (zero cost when unset - the notify points compile to nothing).
5397 * When set, the transport (L4) fires an application callback on every connection
5398 * state transition - pc_conn_on_event(slot, old_state, new_state, reason) - and
5399 * maintains lock-free counters (accepts, closes by reason, idle timeouts, RX
5400 * backpressure events, dropped deferred events, and a live ConnState::CONN_CLOSING gauge)
5401 * readable via pc_conn_counters_get(). This is the only state-transition trace the
5402 * L4/L5 core exposes; pair it with PC_ENABLE_STATS for request-level metrics.
5403 */
5404#ifndef PC_ENABLE_OBSERVABILITY
5405#define PC_ENABLE_OBSERVABILITY 0
5406#endif
5407
5408/**
5409 * @brief Prometheus `/metrics` endpoint (text exposition format 0.0.4).
5410 *
5411 * Default off (requires PC_ENABLE_STATS for the underlying counters). When
5412 * set, PC::metrics() emits the runtime stats as Prometheus metrics
5413 * (`pc_uptime_seconds`, `pc_http_requests_total`,
5414 * `pc_http_responses_total{class=...}`, `pc_active_connections`,
5415 * `pc_free_heap_bytes`, ...) so a Prometheus server can scrape the device.
5416 */
5417#ifndef PC_ENABLE_METRICS
5418#define PC_ENABLE_METRICS 0
5419#endif
5420
5421/**
5422 * @brief Browser "web serial" terminal over WebSocket (src/services/web/web_terminal).
5423 *
5424 * Serves a self-contained terminal page and a WebSocket endpoint: device output
5425 * is broadcast to all connected browsers, browser input is delivered to a
5426 * command callback. Requires PC_ENABLE_WEBSOCKET. Default off.
5427 */
5428#ifndef PC_ENABLE_WEB_TERMINAL
5429#define PC_ENABLE_WEB_TERMINAL 0
5430#endif
5431
5432/**
5433 * @brief Stack scratch for pc_web_terminal_frame()/println() line building.
5434 *
5435 * One formatted terminal line must fit in this many bytes (longer is truncated).
5436 * Allocated on the stack only during the call - no persistent RAM cost.
5437 */
5438#ifndef TERM_TX_BUF_SIZE
5439#define TERM_TX_BUF_SIZE 256
5440#endif
5441
5442/**
5443 * @brief Conditional GET (ETag + Last-Modified) for served files.
5444 *
5445 * When set, serve_file()/serve_static() emit a strong `ETag` (from file size +
5446 * mtime) and a `Last-Modified` date, and answer a conditional request with
5447 * `304 Not Modified` when either the client's `If-None-Match` matches the ETag or
5448 * - per RFC 9110, only if no `If-None-Match` is present - its `If-Modified-Since`
5449 * is not older than the file. Saves bandwidth on repeat fetches of static assets.
5450 * (If-Modified-Since needs a real wall clock for the file mtime; with no clock the
5451 * date validator is skipped and the ETag validator still works.)
5452 */
5453#ifndef PC_ENABLE_ETAG
5454#define PC_ENABLE_ETAG 0
5455#endif
5456
5457/**
5458 * @brief Expose a diagnostic JSON endpoint via server.diag().
5459 *
5460 * Disabled by default - enabling it exposes compile-time configuration
5461 * (buffer sizes, feature flags) which could aid an attacker. Only
5462 * enable in development or behind an authenticated route.
5463 *
5464 * When enabled, PC_DIAG_JSON is a compile-time string constant you can
5465 * serve from any route handler:
5466 * @code
5467 * server.on("/diag", HttpMethod::HTTP_GET, [](uint8_t id, HttpReq *) {
5468 * server.diag(id); // convenience wrapper
5469 * // or:
5470 * server.send(id, 200, "application/json", PC_DIAG_JSON);
5471 * });
5472 * @endcode
5473 */
5474#ifndef PC_ENABLE_DIAG
5475#define PC_ENABLE_DIAG 0
5476#endif
5477
5478/**
5479 * @brief HTTP/1.1 persistent connections (keep-alive).
5480 *
5481 * Default off (every response carries `Connection: close` and the connection is
5482 * closed after one request - the long-standing behavior). When set to 1, a
5483 * cleanly-parsed request is answered with `Connection: keep-alive` and the slot
5484 * is recycled for the next request on the same socket: HTTP/1.1 keeps the
5485 * connection open unless the client sends `Connection: close`; HTTP/1.0 closes
5486 * unless the client sends `Connection: keep-alive`. Error responses (400/413/414
5487 * and any non-ParseState::PARSE_COMPLETE path) always close, since the next request boundary
5488 * is unknown. Idle keep-alive connections are still reclaimed by the existing
5489 * conn_timeout sweep, and each connection serves at most
5490 * PC_KEEPALIVE_MAX_REQUESTS requests before a deliberate close.
5491 */
5492#ifndef PC_ENABLE_KEEPALIVE
5493#define PC_ENABLE_KEEPALIVE 1
5494#endif
5495
5496/**
5497 * @brief Maximum requests served on one keep-alive connection before it is closed.
5498 *
5499 * A fairness bound so a single client cannot hold a connection slot
5500 * indefinitely with a steady request stream. After this many responses the
5501 * server emits `Connection: close` and drops the link; the client simply
5502 * reconnects. Only meaningful when PC_ENABLE_KEEPALIVE is set.
5503 */
5504#ifndef PC_KEEPALIVE_MAX_REQUESTS
5505#define PC_KEEPALIVE_MAX_REQUESTS 100
5506#endif
5507
5508/**
5509 * @brief HTTP/2 (RFC 9113) over the version-agnostic request/response core.
5510 *
5511 * Default off. When set, the server negotiates HTTP/2 via TLS ALPN ("h2") and speaks the binary
5512 * framing + HPACK header compression (RFC 7541) on top of the same routes/handlers as HTTP/1.1
5513 * (the response serializer is version-neutral). The HPACK codec and the frame layer are pure and
5514 * host-tested; the connection/stream state machine plugs in as a ProtoHandler.
5515 */
5516#ifndef PC_ENABLE_HTTP2
5517#define PC_ENABLE_HTTP2 0
5518#endif
5519
5520/**
5521 * @brief Per-connection HPACK dynamic-table size in bytes (our decoder; advertised to the peer
5522 * as SETTINGS_HEADER_TABLE_SIZE). RFC 7541's default is 4096; lower it to save per-connection
5523 * RAM (each active HTTP/2 connection holds one table).
5524 */
5525#ifndef PC_HPACK_TABLE_BYTES
5526#define PC_HPACK_TABLE_BYTES 4096
5527#endif
5528
5529/** @brief Max HPACK dynamic-table entries (>= PC_HPACK_TABLE_BYTES / 32, the min entry size). */
5530#ifndef PC_HPACK_MAX_ENTRIES
5531#define PC_HPACK_MAX_ENTRIES 128
5532#endif
5533
5534/**
5535 * @brief Largest HTTP/2 frame we accept, in bytes (advertised as SETTINGS_MAX_FRAME_SIZE). RFC
5536 * 9113 requires accepting at least 16384; a whole frame is buffered for reassembly, so this
5537 * (plus the HPACK table) sets the per-HTTP/2-connection RAM. Range: [16384, 16777215].
5538 */
5539#ifndef PC_H2_MAX_FRAME
5540#define PC_H2_MAX_FRAME 16384
5541#endif
5542
5543/** @brief Max concurrent HTTP/2 streams per connection (advertised as MAX_CONCURRENT_STREAMS). */
5544#ifndef PC_H2_MAX_STREAMS
5545#define PC_H2_MAX_STREAMS 8
5546#endif
5547
5548/**
5549 * @brief Header-block reassembly buffer for HTTP/2 requests that span HEADERS + CONTINUATION
5550 * frames (a single END_HEADERS frame decodes in place and needs no copy). Caps the compressed
5551 * request-header size; a larger block is rejected (RFC 9113 sec 6.10).
5552 */
5553#ifndef PC_H2_HDR_BLOCK
5554#define PC_H2_HDR_BLOCK 4096
5555#endif
5556
5557/**
5558 * @brief Place the HTTP/2 connection-engine pool in external PSRAM (ESP32).
5559 *
5560 * Each HTTP/2 connection needs a ~28 KB engine, so the pool (MAX_CONNS of them) does not fit the
5561 * ~122 KB internal DRAM alongside a TLS server - HTTP/2 therefore requires PSRAM. Set this to 1
5562 * on a PSRAM board (S3 / P4 / WROVER) to move the pool to external RAM via `EXT_RAM_BSS_ATTR`.
5563 * Like PC_TLS_ARENA_IN_PSRAM it needs a framework built with
5564 * `CONFIG_SPIRAM_ALLOW_BSS_SEG_EXTERNAL_MEMORY=y` (the stock arduino-esp32 core ships it off; see
5565 * tools/psram/README.md). A compile-time guard rejects PC_ENABLE_HTTP2 without this on ARDUINO.
5566 */
5567#ifndef PC_H2_POOL_IN_PSRAM
5568#define PC_H2_POOL_IN_PSRAM 0
5569#endif
5570
5571/**
5572 * @brief HTTP/3 (RFC 9114) over QUIC (RFC 9000) - implemented, host-tested end-to-end (HW verification pending).
5573 *
5574 * Default off. HTTP/3 runs over QUIC (a reliable transport over UDP) with QPACK (RFC 9204)
5575 * header compression and its own binary framing. The full stack is in place and exercised by a
5576 * host end-to-end test - the QUIC variable-length integer (RFC 9000 sec 16), packet protection +
5577 * framing, the TLS 1.3-in-QUIC handshake, the transport connection engine, and the HTTP/3 + QPACK
5578 * codecs. On-device (ESP32) HW verification and an example are still pending. Like HTTP/2 this is a
5579 * PSRAM-class feature.
5580 */
5581#ifndef PC_ENABLE_HTTP3
5582#define PC_ENABLE_HTTP3 0
5583#endif
5584
5585/**
5586 * @brief DTLS 1.3 datagram security (RFC 9147) - the record layer.
5587 *
5588 * DTLS 1.3 secures datagram (UDP) transports - CoAP-over-DTLS and other constrained-device
5589 * telemetry - reusing the hand-rolled TLS 1.3 handshake crypto that already backs HTTP/3. This
5590 * flag gates the DTLS 1.3 **record layer** (pc_dtls_record): the DTLSCiphertext unified header,
5591 * per-record AEAD protection (AEAD_AES_128_GCM), the RFC 9147 sequence-number encryption,
5592 * sequence-number reconstruction, and the anti-replay window; the **handshake framing and
5593 * reliability** layer (pc_dtls_handshake, RFC 9147 §5 + §7): the 12-byte handshake header,
5594 * overlap-tolerant message reassembly, the ACK message, and the stateless HelloRetryRequest
5595 * cookie; and the **server handshake state machine** (pc_dtls_conn, RFC 9147 §5-6): the
5596 * one-round-trip full handshake (TLS_AES_128_GCM_SHA256 / X25519 / Ed25519), epoch 0->2->3
5597 * transitions, reusing the TLS 1.3 messages + key schedule (pc_tls13_msg / pc_tls13_kdf). The
5598 * HelloRetryRequest cookie round-trip and ACK/timeout retransmission, plus a CoAPs front-end, are
5599 * the following phases. Enabling this also compiles the shared pc_hkdf / aes128gcm / pc_tls13_*
5600 * primitives (otherwise gated behind HTTP/3). Default off.
5601 */
5602#ifndef PC_ENABLE_DTLS
5603#define PC_ENABLE_DTLS 0
5604#endif
5605
5606/**
5607 * @brief TLS Raw Public Keys (RFC 7250) - present a bare public key instead of an X.509 certificate.
5608 *
5609 * When a client offers the @c server_certificate_type extension (IANA 20) with RawPublicKey(2), the
5610 * server answers with that certificate type in EncryptedExtensions and sends a Certificate message
5611 * whose entry is a DER @c SubjectPublicKeyInfo (the 44-byte Ed25519 SPKI) rather than an X.509 chain -
5612 * the same Ed25519 key still signs CertificateVerify, so there is no security downgrade, only a
5613 * smaller handshake with no cert parsing. A cert-less credential is the natural fit for provisioned,
5614 * key-pinned ESP32 fleets and the RFC 7252 sec 9 CoAP-over-DTLS RawPublicKey profile. This is
5615 * server-side only (the server presents an RPK); the handshake never requests a client certificate.
5616 * Additive: a client that does not offer the extension still gets the X.509 certificate. Wired into
5617 * the DTLS 1.3 handshake; the shared TLS 1.3 codec also carries it for a future HTTP/3 use. Default off.
5618 */
5619#ifndef PC_ENABLE_TLS_RPK
5620#define PC_ENABLE_TLS_RPK 0
5621#endif
5622
5623// Internal request-dispatch slots appended to the connection pool for non-TCP transports.
5624// HTTP/3 runs over QUIC/UDP and has no accept-time TCP slot, but it reuses the same request
5625// pipeline (match_and_execute + send), which is indexed by a connection-pool slot. One reserved
5626// slot at index MAX_CONNS lets an HTTP/3 request run through that pipeline. The TCP accept path only
5627// ever scans [0, MAX_CONNS), and this slot is driven synchronously by the HTTP/3 poll on the worker
5628// thread, so there is no accept race. CONN_POOL_SLOTS sizes conn_pool / http_pool / the per-slot
5629// response-header buffer; every TCP loop still bounds itself with MAX_CONNS.
5630#if PC_ENABLE_HTTP3
5631#define PC_INTERNAL_SLOTS 1
5632#define PC_H3_DISPATCH_SLOT MAX_CONNS ///< reserved conn-pool slot an HTTP/3 request dispatches through
5633#else
5634#define PC_INTERNAL_SLOTS 0
5635#endif
5636#define CONN_POOL_SLOTS (MAX_CONNS + PC_INTERNAL_SLOTS)
5637
5638/** @brief UDP port the HTTP/3 (QUIC) server binds by default (used by PC::pc_h3_cert). */
5639#ifndef PC_HTTP3_PORT
5640#define PC_HTTP3_PORT 443
5641#endif
5642
5643/**
5644 * @brief Maximum bytes of one QUIC/TLS handshake CRYPTO flight (RFC 9001).
5645 *
5646 * The server's second flight - EncryptedExtensions + Certificate + CertificateVerify + Finished -
5647 * is assembled whole before it is fragmented into CRYPTO frames across Handshake packets. The
5648 * Certificate (a DER X.509 chain) dominates the size, so this bounds the certificate the server can
5649 * present. The default fits a single Ed25519 leaf certificate comfortably; raise it for a chain.
5650 */
5651#ifndef PC_H3_CRYPTO_BUF
5652#define PC_H3_CRYPTO_BUF 2048
5653#endif
5654
5655/**
5656 * @brief Maximum concurrent request streams per HTTP/3 connection.
5657 *
5658 * Bounds the per-connection QUIC stream table (client-initiated bidirectional request streams plus
5659 * the handful of unidirectional control / QPACK streams). Each slot is small; 8 matches the HTTP/2
5660 * default (PC_H2_MAX_STREAMS).
5661 */
5662#ifndef PC_H3_MAX_STREAMS
5663#define PC_H3_MAX_STREAMS 8
5664#endif
5665
5666/**
5667 * @brief HTTP Range requests / 206 Partial Content (requires PC_ENABLE_FILE_SERVING or
5668 * PC_ENABLE_EDGE_CACHE).
5669 *
5670 * Default off. When set, serve_file() / serve_static() and the CDN edge cache honor a single-range
5671 * `Range: bytes=...` request header: they answer `206 Partial Content` with a `Content-Range` header
5672 * and stream only the requested bytes (file serving seeks the file; the edge cache windows the cached
5673 * body), advertise `Accept-Ranges: bytes` on full responses, and answer an unsatisfiable range with
5674 * `416 Range Not Satisfiable`. This enables resumable downloads and media seeking. Multi-range
5675 * (multipart/byteranges) requests are not supported - the server falls back to a full 200 response,
5676 * which is RFC 7233 §3.1 compliant. The parser is shared (server/http_range.h).
5677 */
5678#ifndef PC_ENABLE_RANGE
5679#define PC_ENABLE_RANGE 0
5680#endif
5681
5682/**
5683 * @brief Enforce the RFC 7230 §5.4 Host-header requirement (default on).
5684 *
5685 * When 1, an HTTP/1.1 request that lacks a Host header - or carries more than
5686 * one - is rejected with 400 Bad Request. When 0, the Host header is not
5687 * required (useful for constrained clients or test harnesses that feed bare
5688 * request lines). The multiple-Host rule and Content-Length validation are
5689 * always active regardless of this flag.
5690 */
5691#ifndef PC_ENFORCE_HOST_HEADER
5692#define PC_ENFORCE_HOST_HEADER 1
5693#endif
5694
5695/**
5696 * @brief Allow SSH password authentication (default on).
5697 *
5698 * Set to 0 to harden the SSH server to publickey-only authentication
5699 * (RFC 4252 §7): the "password" method is then refused outright and is not
5700 * advertised in the USERAUTH_FAILURE method list. Publickey auth is always
5701 * available regardless of this flag.
5702 */
5703#ifndef PC_SSH_ALLOW_PASSWORD
5704#define PC_SSH_ALLOW_PASSWORD 1
5705#endif
5706
5707/**
5708 * @brief SSH keyboard-interactive authentication (RFC 4256), default off.
5709 *
5710 * Adds the "keyboard-interactive" method alongside password/publickey. On selection the server sends
5711 * one SSH_MSG_USERAUTH_INFO_REQUEST with a single non-echoed "Password: " prompt and verifies the
5712 * client's SSH_MSG_USERAUTH_INFO_RESPONSE through the same ::pc_ssh_auth_set_password_cb callback -
5713 * so it is the challenge-response face of password auth (the common OpenSSH `-o
5714 * PreferredAuthentications=keyboard-interactive` / PAM-password case), not a second credential store.
5715 * Requires PC_ENABLE_SSH and, because it is password-backed, PC_SSH_ALLOW_PASSWORD.
5716 */
5717#ifndef PC_ENABLE_SSH_KEYBOARD_INTERACTIVE
5718#define PC_ENABLE_SSH_KEYBOARD_INTERACTIVE 0
5719#endif
5720
5721/**
5722 * @brief Maximum failed SSH authentication attempts per connection.
5723 *
5724 * RFC 4252 §4 permits the server to disconnect after a small bounded number of
5725 * failed USERAUTH_REQUESTs. After this many SSH_MSG_USERAUTH_FAILURE responses
5726 * on one connection the server sends SSH_MSG_DISCONNECT and drops the link.
5727 * (The publickey "would-be-accepted" probe and a SUCCESS do not count.)
5728 */
5729#ifndef SSH_MAX_AUTH_ATTEMPTS
5730#define SSH_MAX_AUTH_ATTEMPTS 6
5731#endif
5732
5733// ---------------------------------------------------------------------------
5734// Listener pool
5735// ---------------------------------------------------------------------------
5736
5737/** @brief Maximum number of simultaneously active listener ports. */
5738#ifndef MAX_LISTENERS
5739#define MAX_LISTENERS 3
5740#endif
5741
5742/**
5743 * @brief Maximum simultaneously bound UDP ports (transport-layer UDP service).
5744 *
5745 * Sizes the fixed pool in udp.cpp. One slot per bound port, e.g. SNMP
5746 * (:161) and the captive-portal DNS responder (:53). Costs only a few pointers
5747 * of BSS each.
5748 */
5749#ifndef PC_MAX_UDP_LISTENERS
5750#define PC_MAX_UDP_LISTENERS 2
5751#endif
5752
5753/**
5754 * @brief Shared receive-scratch size for the transport-layer UDP service.
5755 *
5756 * One static buffer (lwIP delivers a single datagram at a time) into which each
5757 * incoming datagram is copied before the handler runs. Must hold the largest
5758 * datagram any UDP service expects (SNMP messages are the largest user).
5759 */
5760#ifndef PC_UDP_RX_BUF_SIZE
5761#define PC_UDP_RX_BUF_SIZE 1472
5762#endif
5763
5764/**
5765 * @brief Opt-in global accept-rate throttle (connection-flood defense).
5766 *
5767 * Default off (zero cost / no behavior change). When set to 1 the accept
5768 * callback rejects new connections once more than PC_ACCEPT_THROTTLE_MAX
5769 * have been accepted within a PC_ACCEPT_THROTTLE_WINDOW_MS fixed window
5770 * (global across all listeners, two static counters - no per-IP table). This
5771 * bounds connection churn (e.g. reconnect brute-force) on top of the bounded
5772 * connection pool and the per-connection auth limits. mitigate finer-grained /
5773 * per-IP attacks at the network layer.
5774 */
5775#ifndef PC_ENABLE_ACCEPT_THROTTLE
5776#define PC_ENABLE_ACCEPT_THROTTLE 0
5777#endif
5778
5779/** @brief Max accepted connections per throttle window (see PC_ENABLE_ACCEPT_THROTTLE). */
5780#ifndef PC_ACCEPT_THROTTLE_MAX
5781#define PC_ACCEPT_THROTTLE_MAX 20
5782#endif
5783
5784/** @brief Throttle window length in milliseconds (see PC_ENABLE_ACCEPT_THROTTLE). */
5785#ifndef PC_ACCEPT_THROTTLE_WINDOW_MS
5786#define PC_ACCEPT_THROTTLE_WINDOW_MS 1000
5787#endif
5788
5789/**
5790 * @brief Opt-in per-IP accept-rate throttle (connection-flood defense, keyed by source IPv4).
5791 *
5792 * Default off (zero cost / no behavior change). Complements the global accept
5793 * throttle: the accept callback rejects a new connection once one source IPv4
5794 * address has opened more than PC_PER_IP_THROTTLE_MAX connections within a
5795 * PC_PER_IP_THROTTLE_WINDOW_MS fixed window. A fixed BSS table of
5796 * PC_PER_IP_THROTTLE_SLOTS buckets tracks the most-recently-seen source
5797 * addresses; when a new address arrives and the table is full, an expired or
5798 * least-recently-started bucket is reused, so memory stays bounded (no heap).
5799 *
5800 * This bounds reconnect/brute-force churn from a single host (the gap left by the
5801 * global throttle, which cannot tell one noisy client from many). It is
5802 * best-effort: an attacker spreading across many source addresses can still churn
5803 * the bounded connection pool, so combine it with the global throttle and
5804 * network-layer filtering.
5805 */
5806#ifndef PC_ENABLE_PER_IP_THROTTLE
5807#define PC_ENABLE_PER_IP_THROTTLE 0
5808#endif
5809
5810/** @brief Number of source IPv4 addresses tracked by the per-IP throttle (BSS bucket table). */
5811#ifndef PC_PER_IP_THROTTLE_SLOTS
5812#define PC_PER_IP_THROTTLE_SLOTS 16
5813#endif
5814
5815/** @brief Max accepted connections per window from one source IP (see PC_ENABLE_PER_IP_THROTTLE). */
5816#ifndef PC_PER_IP_THROTTLE_MAX
5817#define PC_PER_IP_THROTTLE_MAX 10
5818#endif
5819
5820/** @brief Per-IP throttle window length in milliseconds (see PC_ENABLE_PER_IP_THROTTLE). */
5821#ifndef PC_PER_IP_THROTTLE_WINDOW_MS
5822#define PC_PER_IP_THROTTLE_WINDOW_MS 10000
5823#endif
5824
5825// ---------------------------------------------------------------------------
5826// Source-IP allowlist (accept-time firewall; PC_ENABLE_IP_ALLOWLIST)
5827// ---------------------------------------------------------------------------
5828
5829/**
5830 * @brief Opt-in source-IP allowlist (accept-time firewall, IPv4 and IPv6).
5831 *
5832 * Default off (zero cost / no behavior change). When set, the accept callback
5833 * drops any connection whose source address is not contained in a configured
5834 * CIDR rule (add rules with listener_ip_allow_add_cidr("192.168.1.0/24") /
5835 * "2001:db8::/32"). Matching is a full-address prefix compare per family, so a v4
5836 * peer never matches a v6 rule and vice versa. An empty allowlist allows
5837 * everything, so enabling the feature before adding rules never locks the device
5838 * out. Rules live in a fixed BSS table of PC_IP_ALLOWLIST_SLOTS entries (no heap).
5839 *
5840 * This is a coarse first-line filter - a spoofed source address can still pass
5841 * it - so combine it with the accept throttles and network-layer filtering.
5842 */
5843#ifndef PC_ENABLE_IP_ALLOWLIST
5844#define PC_ENABLE_IP_ALLOWLIST 0
5845#endif
5846
5847/** @brief Number of CIDR rules the source-IP allowlist can hold (BSS table). */
5848#ifndef PC_IP_ALLOWLIST_SLOTS
5849#define PC_IP_ALLOWLIST_SLOTS 8
5850#endif
5851
5852// ---------------------------------------------------------------------------
5853// Trusted reverse-proxy forwarded-client resolution (PC_ENABLE_FORWARDED_TRUST)
5854// ---------------------------------------------------------------------------
5855
5856/**
5857 * @brief Believe a `Forwarded` / `X-Forwarded-For` client address only from a trusted upstream.
5858 *
5859 * Default off. A forwarded header is client-spoofable, so it is honored only when the connection's
5860 * real TCP peer matches a configured trusted-proxy CIDR (register one with
5861 * `pc_forwarded_trust_add_cidr("10.0.0.0/8")`). When set, the per-IP auth lockout keys on the
5862 * recovered original client address behind such a proxy instead of the proxy's shared TCP address, so
5863 * one abusive client cannot lock out every client behind the proxy, while a direct (untrusted) peer's
5864 * spoofed header is ignored. The accept-time throttle and the IP allowlist deliberately stay on the
5865 * real TCP source. Requires PC_ENABLE_AUTH_LOCKOUT.
5866 */
5867#ifndef PC_ENABLE_FORWARDED_TRUST
5868#define PC_ENABLE_FORWARDED_TRUST 0
5869#endif
5870
5871/** @brief Number of trusted-upstream CIDR rules the forwarded-client resolver holds (BSS table). */
5872#ifndef PC_TRUSTED_PROXY_MAX
5873#define PC_TRUSTED_PROXY_MAX 2
5874#endif
5875
5876// ---------------------------------------------------------------------------
5877// Brute-force auth lockout (per-source-IP; PC_ENABLE_AUTH_LOCKOUT)
5878// ---------------------------------------------------------------------------
5879
5880/**
5881 * @brief Opt-in per-IP brute-force lockout for HTTP auth (requires PC_ENABLE_AUTH).
5882 *
5883 * Default off (zero cost / no behavior change). When set, the auth gate counts
5884 * consecutive failed authentications per source address (IPv4 or IPv6, keyed on
5885 * the full address) in a fixed BSS table; after
5886 * PC_AUTH_LOCKOUT_THRESHOLD failures the address is locked out for
5887 * PC_AUTH_LOCKOUT_BASE_MS, doubling on each further failure up to
5888 * PC_AUTH_LOCKOUT_MAX_MS. A locked address gets 429 (Retry-After) with no
5889 * credential check; a successful auth clears it. Bounded memory (no heap); the
5890 * table evicts idle, then least-recently-used, addresses when full.
5891 */
5892#ifndef PC_ENABLE_AUTH_LOCKOUT
5893#define PC_ENABLE_AUTH_LOCKOUT 0
5894#endif
5895
5896/** @brief Number of source IPs the auth lockout tracks (BSS bucket table). */
5897#ifndef PC_AUTH_LOCKOUT_SLOTS
5898#define PC_AUTH_LOCKOUT_SLOTS 16
5899#endif
5900
5901/** @brief Consecutive failed auths from one IP before it is locked out. */
5902#ifndef PC_AUTH_LOCKOUT_THRESHOLD
5903#define PC_AUTH_LOCKOUT_THRESHOLD 5
5904#endif
5905
5906/** @brief First lockout duration in ms; doubles on each further failure. */
5907#ifndef PC_AUTH_LOCKOUT_BASE_MS
5908#define PC_AUTH_LOCKOUT_BASE_MS 1000
5909#endif
5910
5911/** @brief Maximum lockout duration in ms (the exponential backoff cap). */
5912#ifndef PC_AUTH_LOCKOUT_MAX_MS
5913#define PC_AUTH_LOCKOUT_MAX_MS 300000
5914#endif
5915
5916// ---------------------------------------------------------------------------
5917// CSRF protection (PC_ENABLE_CSRF)
5918// ---------------------------------------------------------------------------
5919
5920/**
5921 * @brief Opt-in CSRF protection for state-changing HTTP requests.
5922 *
5923 * Default off (zero cost / no behavior change). When set, every POST / PUT /
5924 * PATCH / DELETE must carry a valid `X-CSRF-Token` header (a stateless,
5925 * HMAC-signed token); requests without one get 403 Forbidden. GET / HEAD /
5926 * OPTIONS are exempt (they are not state-changing). Clients fetch a token from
5927 * the built-in `GET /csrf` endpoint, which also sets it as the `csrf` cookie.
5928 * No server-side session storage - the token self-validates against an HMAC
5929 * secret seeded from the hardware RNG at begin(); it is independent of
5930 * PC_ENABLE_AUTH.
5931 */
5932#ifndef PC_ENABLE_CSRF
5933#define PC_ENABLE_CSRF 0
5934#endif
5935
5936// ---------------------------------------------------------------------------
5937// Telnet sizing constants (PC_ENABLE_TELNET must be 1)
5938// ---------------------------------------------------------------------------
5939
5940/** @brief Maximum simultaneous Telnet connections. */
5941#ifndef MAX_TELNET_CONNS
5942#define MAX_TELNET_CONNS 2
5943#endif
5944
5945/** @brief Stack buffer for one Telnet I/O chunk. */
5946#ifndef TELNET_BUF_SIZE
5947#define TELNET_BUF_SIZE 256
5948#endif
5949
5950// ---------------------------------------------------------------------------
5951// SSH sizing constants (PC_ENABLE_SSH must be 1)
5952// ---------------------------------------------------------------------------
5953
5954/** @brief Maximum simultaneous SSH connections. */
5955#ifndef MAX_SSH_CONNS
5956#define MAX_SSH_CONNS 1
5957#endif
5958
5959/**
5960 * @brief Maximum concurrent SSH channels per connection (RFC 4254 multiplexing).
5961 *
5962 * Default 1 - one "session" channel per connection, byte-for-byte the original
5963 * single-channel behavior. Raise it to multiplex several channels (e.g. several
5964 * concurrent shells/exec, or - with the forwarding build flags - tunnels) over one
5965 * SSH connection; each channel gets its own id, window, and peer state. Fixed BSS
5966 * (ssh_chan[MAX_SSH_CONNS][PC_SSH_MAX_CHANNELS]), no heap.
5967 */
5968#ifndef PC_SSH_MAX_CHANNELS
5969#define PC_SSH_MAX_CHANNELS 1
5970#endif
5971#if PC_SSH_MAX_CHANNELS < 1
5972#error "ProtoCore: PC_SSH_MAX_CHANNELS must be >= 1"
5973#endif
5974
5975/**
5976 * @brief SSH TCP port forwarding (`direct-tcpip`, i.e. `ssh -L`). Default off.
5977 *
5978 * When set, the SSH server can open an outbound TCP connection to a client-named
5979 * host:port and bridge bytes between that socket and the SSH channel - the
5980 * `ssh_forward` owner does the I/O via the outbound client transport (pc_client),
5981 * so it needs `PC_CLIENT_CONNS >= PC_SSH_FWD_MAX` and a channel pool
5982 * (`PC_SSH_MAX_CHANNELS > 1`) to be useful. Forwarding is still opt-in at
5983 * runtime: nothing is forwarded until the application calls `pc_ssh_forward_begin()`.
5984 * Off = the channel codec refuses every `direct-tcpip` open (no open relay).
5985 */
5986#ifndef PC_SSH_PORT_FORWARD
5987#define PC_SSH_PORT_FORWARD 0
5988#endif
5989
5990/** @brief Maximum concurrent forwarded TCP connections (must be <= PC_CLIENT_CONNS). */
5991#ifndef PC_SSH_FWD_MAX
5992#define PC_SSH_FWD_MAX 2
5993#endif
5994
5995/** @brief Maximum forward target hostname length including null terminator. */
5996#ifndef PC_SSH_FWD_HOST_MAX
5997#define PC_SSH_FWD_HOST_MAX 64
5998#endif
5999
6000/** @brief Blocking connect timeout (ms) when opening a forward target. */
6001#ifndef PC_SSH_FWD_CONNECT_MS
6002#define PC_SSH_FWD_CONNECT_MS 3000
6003#endif
6004
6005/** @brief Max bytes moved per forward channel per poll, target -> client (<= SSH_PKT_BUF_SIZE). */
6006#ifndef PC_SSH_FWD_CHUNK
6007#define PC_SSH_FWD_CHUNK 1024
6008#endif
6009
6010/**
6011 * @brief Maximum concurrent remote-forward listeners (`ssh -R` / `tcpip-forward`).
6012 *
6013 * Each accepted client that requests remote forwarding can bind up to this many
6014 * ports on the device; each binding consumes one `listener_pool[]` slot, so
6015 * `MAX_LISTENERS` must have that much headroom above the app's own listeners.
6016 * Remote forwarding shares `PC_SSH_PORT_FORWARD` (compiled in) and is inert
6017 * until `pc_ssh_forward_begin()`.
6018 */
6019#ifndef PC_SSH_RFWD_MAX
6020#define PC_SSH_RFWD_MAX 1
6021#endif
6022
6023/**
6024 * @brief Maximum concurrent bridged connections across all remote forwards.
6025 *
6026 * Each connection accepted on a forwarded port occupies one transport `conn_pool`
6027 * slot plus one SSH channel (so it needs `PC_SSH_MAX_CHANNELS` headroom) and one
6028 * entry here while it is bridged back to the client.
6029 */
6030#ifndef PC_SSH_RFWD_BRIDGE_MAX
6031#define PC_SSH_RFWD_BRIDGE_MAX 2
6032#endif
6033
6034/** @brief Packet assembly buffer per SSH connection (bytes). */
6035#ifndef SSH_PKT_BUF_SIZE
6036#define SSH_PKT_BUF_SIZE 2048
6037#endif
6038
6039/**
6040 * @brief SFTP server subsystem over SSH (SSH_FXP_* v3, draft-ietf-secsh-filexfer-02). Default off.
6041 *
6042 * When set, an SSH client's `subsystem` request for "sftp" opens an SFTP session over the channel and the
6043 * device serves files from an fs::FS mount (SD / LittleFS) bound via pc_ssh_sftp_begin(fs, root):
6044 * open/read/write/opendir/readdir/stat/mkdir/rmdir/remove/rename/realpath, with a fixed handle table and
6045 * streamed reads/writes (zero heap). The standards-track southbound path for secure file / G-code push over
6046 * the one authenticated SSH port. Requires PC_ENABLE_SSH + PC_ENABLE_FILE_SERVING (the fs::FS seam).
6047 */
6048#ifndef PC_ENABLE_SSH_SFTP
6049#define PC_ENABLE_SSH_SFTP 0
6050#endif
6051
6052/**
6053 * @brief SCP server over SSH (the legacy RCP protocol via `exec "scp -t/-f"`). Default off.
6054 *
6055 * When set, `scp file board:/path` (sink) and `scp board:/path file` (source) transfer a file to/from the
6056 * fs::FS mount bound by pc_ssh_sftp_begin. Reuses the SFTP fs binding + path-traversal guard. v1 carries its
6057 * error/ack bytes inline on the channel (no CHANNEL_EXTENDED_DATA). Requires SSH + FILE_SERVING.
6058 */
6059#ifndef PC_ENABLE_SSH_SCP
6060#define PC_ENABLE_SSH_SCP 0
6061#endif
6062
6063/** @brief Max concurrent open SFTP handles (files + dirs) per SSH connection. */
6064#ifndef PC_SFTP_MAX_HANDLES
6065#define PC_SFTP_MAX_HANDLES 4
6066#endif
6067
6068/** @brief SFTP packet-assembly buffer per SFTP channel (bytes); bounds one non-streamed request/response. */
6069#ifndef PC_SFTP_PKT_BUF
6070#define PC_SFTP_PKT_BUF 2048
6071#endif
6072
6073/**
6074 * @brief Largest PC_SSH_FXP_DATA payload returned for one READ (a short read - the client re-requests). Kept
6075 * within one SSH packet (SSH_PKT_BUF_SIZE minus framing), so bump SSH_PKT_BUF_SIZE too for throughput.
6076 */
6077#ifndef PC_SFTP_MAX_READ
6078#define PC_SFTP_MAX_READ 1024
6079#endif
6080
6081/** @brief Largest absolute path the SFTP/SCP server resolves (mount root + request path). */
6082#ifndef PC_SFTP_PATH_MAX
6083#define PC_SFTP_PATH_MAX 256
6084#endif
6085
6086/**
6087 * @brief SSH server-to-client compression (`zlib@openssh.com` / `zlib`, RFC 4253 sec 6.2). Default off.
6088 *
6089 * When set, the server advertises `zlib@openssh.com` (delayed, OpenSSH's default) and `zlib` for the
6090 * SERVER->CLIENT direction and, once active, compresses every outbound packet payload with a
6091 * context-takeover DEFLATE stream (a persistent sliding window carried across packets, sync-flushed
6092 * per packet - RFC 1951 / RFC 1950). Client->server stays `none`: SSH negotiates each direction
6093 * independently, and the inbound direction (keystrokes / uploads to the device) is tiny and, because
6094 * OpenSSH compresses outbound with Z_PARTIAL_FLUSH, would need a far larger resumable inflate engine
6095 * for little gain. `ssh -o Compression=yes` still gets real compression on the high-volume direction.
6096 *
6097 * PSRAM-class: each connection holds a compressor (~window + hash tables, tens of KB). A compile-time
6098 * guard rejects this on ARDUINO without PC_SSH_ZLIB_IN_PSRAM. Requires PC_ENABLE_SSH.
6099 */
6100#ifndef PC_ENABLE_SSH_ZLIB
6101#define PC_ENABLE_SSH_ZLIB 0
6102#endif
6103
6104/**
6105 * @brief Place the per-connection SSH compression state in external PSRAM (ESP32).
6106 *
6107 * Like PC_H2_POOL_IN_PSRAM / PC_TLS_ARENA_IN_PSRAM: moves the compressor pool
6108 * (MAX_SSH_CONNS of them) to external RAM via `EXT_RAM_BSS_ATTR`. Needs a framework built with
6109 * `CONFIG_SPIRAM_ALLOW_BSS_SEG_EXTERNAL_MEMORY=y` (tools/psram/README.md).
6110 */
6111#ifndef PC_SSH_ZLIB_IN_PSRAM
6112#define PC_SSH_ZLIB_IN_PSRAM 0
6113#endif
6114
6115/**
6116 * @brief Acknowledge placing the SSH compressor in internal DRAM (no PSRAM).
6117 *
6118 * The per-connection compressor is ~48 KB. With MAX_SSH_CONNS=1 and no TLS server it fits internal
6119 * DRAM on a roomy chip (S3 / P4). Rather than force PSRAM, this mirrors PC_TLS_ACK_MULTI_CONN_DRAM:
6120 * set it to 1 to consciously accept the internal-DRAM cost when PC_SSH_ZLIB_IN_PSRAM is off. The
6121 * build otherwise fails fast on ARDUINO with guidance (below) instead of a raw linker overflow.
6122 */
6123#ifndef PC_SSH_ZLIB_ACK_DRAM
6124#define PC_SSH_ZLIB_ACK_DRAM 0
6125#endif
6126
6127/**
6128 * @brief SSH s2c DEFLATE sliding-window size in bytes (max back-reference distance). Power of two,
6129 * 256..32768. Larger = better ratio + more per-connection RAM (the compressor holds a window-sized
6130 * work buffer + a window-sized hash chain). The client always allocates a 32 KB inflate window, so
6131 * any value here interoperates; 8 KB is a good ratio/RAM balance for terminal + command output.
6132 */
6133#ifndef PC_SSH_ZLIB_WINDOW
6134#define PC_SSH_ZLIB_WINDOW 8192
6135#endif
6136
6137/**
6138 * @brief Largest uncompressed payload the s2c compressor accepts in one call (bytes). Outbound SSH
6139 * payloads are bounded by SSH_PKT_BUF_SIZE; this sizes the compressor's history+input work buffer.
6140 */
6141#ifndef PC_SSH_ZLIB_MAX_IN
6142#define PC_SSH_ZLIB_MAX_IN 2048
6143#endif
6144
6145/** @brief Maximum SSH username length including null terminator. */
6146#ifndef SSH_MAX_USERNAME_LEN
6147#define SSH_MAX_USERNAME_LEN 32
6148#endif
6149
6150/** @brief Maximum SSH password length including null terminator. */
6151#ifndef SSH_MAX_PASSWORD_LEN
6152#define SSH_MAX_PASSWORD_LEN 64
6153#endif
6154
6155/**
6156 * @brief Size in bytes of the shared per-dispatch scratch arena.
6157 *
6158 * Codec / protocol handlers borrow transient working memory from this single BSS
6159 * arena (see server/mmgr/scratch.h) instead of each feature owning a
6160 * dedicated buffer. The session layer empties it before every event dispatch, so
6161 * it only needs to hold the *peak concurrent* scratch of any one dispatch, not
6162 * the sum across features. Tune from the scratch_high_water() reading on a real
6163 * workload; an over-budget borrow fails closed (scratch_alloc returns nullptr).
6164 */
6165#ifndef PC_SCRATCH_ARENA_SIZE
6166#define PC_SCRATCH_ARENA_SIZE 8192
6167#endif
6168
6169/**
6170 * @brief Compile the library's internal debug checks (default 0 = off).
6171 *
6172 * Deliberately NOT keyed on NDEBUG. Whether NDEBUG is defined is a property of whichever toolchain
6173 * happens to build the library - the Arduino ESP32 core does not define it - so keying on it means
6174 * nobody actually chose. This is the switch we control.
6175 *
6176 * What it enables today: the pools' owner tripwire, which records the first execution context to
6177 * touch each slot and asserts every later borrow matches. That catches a borrow crossing tasks - the
6178 * one way the lock-free single-accessor invariant can break - and turns a silent cross-core race into
6179 * an immediate failure.
6180 *
6181 * Measured cost on an ESP32-S3 at 240 MHz: ~52 cycles per pool entry point, and a
6182 * mark + alloc + release touches three of them, so roughly 156 cycles on every borrow. Worth paying
6183 * while chasing a memory bug; not worth shipping.
6184 */
6185#ifndef PC_DEBUG_CHECKS
6186#define PC_DEBUG_CHECKS 0
6187#endif
6188
6189/**
6190 * @brief Worst-case bytes each module borrows from the secure pool in a single call.
6191 *
6192 * Declared here because PC_SECURE_ARENA_SIZE below is derived from them and this is the one
6193 * place that can see them all - a module header cannot host its own, since every module header
6194 * includes this file. Each value is PROVED where the struct lives: the owning .cpp carries a
6195 * static_assert(sizeof(X) <= PC_WORK_X), so a working set that grows past its declaration fails
6196 * the build naming itself, rather than exhausting the pool at run time.
6197 *
6198 * These are SIZES, not offsets. Nothing here couples one module to another: each is a term in a
6199 * sum, order is irrelevant, and adding a module shifts no one. That is the difference from the
6200 * crypto_work region map these replaced.
6201 *
6202 * Values are what the ESP32 toolchain reported for the real structs, rounded up.
6203 */
6204#ifndef PC_WORK_BIGNUM_HW
6205#define PC_WORK_BIGNUM_HW 1024
6206#endif
6207#ifndef PC_WORK_BIGNUM_SW
6208#define PC_WORK_BIGNUM_SW 1408
6209#endif
6210// AES-256-GCM keyed context. Sized per vendor for the same reason as the bignum working set above: one
6211// backend or the other is compiled, never both. It matters more here than it did as a transient
6212// borrow - a consumer now embeds two of these per connection (send and receive) for the life of the
6213// key, so one flat figure sized for the largest backend is RAM every target pays and only one uses.
6214// The static_assert in each backend is what keeps these honest against a vendor header we do not own.
6215#ifndef PC_WORK_AESGCM_HW
6216#define PC_WORK_AESGCM_HW 416 // mbedtls_gcm_context measures 392 on the S3
6217#endif
6218#ifndef PC_WORK_AESGCM_SW
6219#define PC_WORK_AESGCM_SW 640 // GcmWork is 608: AES-256 round keys + the 4-bit GHASH table
6220#endif
6221#ifndef PC_WORK_AESGCM
6222#if PC_HAS_HW_AESGCM
6223#define PC_WORK_AESGCM PC_WORK_AESGCM_HW
6224#else
6225#define PC_WORK_AESGCM PC_WORK_AESGCM_SW
6226#endif
6227#endif
6228#ifndef PC_WORK_AESCCM
6229#define PC_WORK_AESCCM 448
6230#endif
6231// AES-128 single-block context (QUIC/DTLS header + sequence-number protection). Kept per key for the
6232// same reason as the AEAD contexts, though the win is smaller: measured on an S3, rebuilding it per
6233// record costs ~556 cycles plus a pool borrow and wipe. (The ECB block it protects is ~7,842 cycles on
6234// its own - one HW-AES operation - which is now the larger per-packet cost in QUIC and DTLS.)
6235#ifndef PC_WORK_AES128_HW
6236#define PC_WORK_AES128_HW 288 // mbedtls_aes_context: nr + rk + buf[68]
6237#endif
6238#ifndef PC_WORK_AES128_SW
6239#define PC_WORK_AES128_SW 176 // uint32_t rk[44]
6240#endif
6241#ifndef PC_WORK_AES128
6242#if PC_HAS_HW_AESGCM
6243#define PC_WORK_AES128 PC_WORK_AES128_HW
6244#else
6245#define PC_WORK_AES128 PC_WORK_AES128_SW
6246#endif
6247#endif
6248
6249// AES-128-GCM keyed context, sized per vendor exactly as PC_WORK_AESGCM above and for the same reason:
6250// one backend is compiled, and a consumer holding a context per direction should not carry storage for
6251// the backend it did not build.
6252#ifndef PC_WORK_AES128GCM_HW
6253#define PC_WORK_AES128GCM_HW 416 // mbedtls_gcm_context measures 392 on the S3
6254#endif
6255#ifndef PC_WORK_AES128GCM_SW
6256#define PC_WORK_AES128GCM_SW 576 // Aes128GcmWork is 560: AES-128 round keys + the 4-bit GHASH table
6257#endif
6258#ifndef PC_WORK_AES128GCM
6259#if PC_HAS_HW_AESGCM
6260#define PC_WORK_AES128GCM PC_WORK_AES128GCM_HW
6261#else
6262#define PC_WORK_AES128GCM PC_WORK_AES128GCM_SW
6263#endif
6264#endif
6265#ifndef PC_WORK_CHACHAPOLY
6266#define PC_WORK_CHACHAPOLY 64
6267#endif
6268#ifndef PC_WORK_CHACHA20
6269#define PC_WORK_CHACHA20 192
6270#endif
6271#ifndef PC_WORK_AES256CTR
6272#define PC_WORK_AES256CTR 384
6273#endif
6274#ifndef PC_WORK_HMAC_SHA256
6275#define PC_WORK_HMAC_SHA256 288 // 272 on the SSH example, 276 with DTLS+PQC (flags change the SHA ctx); headroom to 288
6276#endif
6277#ifndef PC_WORK_POLY1305
6278#define PC_WORK_POLY1305 80
6279#endif
6280#ifndef PC_WORK_MD
6281#define PC_WORK_MD 96
6282#endif
6283
6284/**
6285 * @brief Size in bytes of the per-slot SECURE pool (see server/mmgr/secure.h), DERIVED.
6286 *
6287 * Not a chosen number. Every borrow from this pool is a working set some module declares - see the
6288 * PC_WORK_* constants, each proved against its struct's sizeof by a static_assert in the module that
6289 * owns it. The floor is the sum of the ones a build actually compiles.
6290 *
6291 * A sum, not a deepest-nest figure. The sum is a strict upper bound - correct however those working
6292 * sets nest under one another - whereas a nest depth is only correct while the call graph stays as it
6293 * is. This value must not be wrong, so it buys certainty with a little slack.
6294 *
6295 * A module whose working set grows past its declaration fails the build, naming itself, instead of
6296 * exhausting the pool at run time. Override PC_SECURE_ARENA_SIZE to pin a size regardless.
6297 */
6298#ifndef PC_SECURE_ARENA_SIZE
6299
6300// The modexp dominates: one backend or the other is compiled, never both.
6301#if PC_HAS_HW_BIGNUM
6302#define PC_SECURE_WORK_BIGNUM PC_WORK_BIGNUM_HW
6303#else
6304#define PC_SECURE_WORK_BIGNUM PC_WORK_BIGNUM_SW
6305#endif
6306
6307// Feature-gated terms: a build pays only for the code it compiled.
6308#if PC_ENABLE_SSH || PC_ENABLE_SSH_CLIENT || PC_ENABLE_TLS || PC_ENABLE_HTTP3 || PC_ENABLE_DTLS
6309#define PC_SECURE_WORK_AEAD (PC_WORK_AESGCM + PC_WORK_CHACHAPOLY + PC_WORK_CHACHA20 + PC_WORK_POLY1305)
6310#define PC_SECURE_WORK_MAC PC_WORK_HMAC_SHA256
6311#else
6312#define PC_SECURE_WORK_AEAD 0
6313#define PC_SECURE_WORK_MAC 0
6314#endif
6315
6316#if PC_ENABLE_SMB
6317#define PC_SECURE_WORK_SMB (PC_WORK_AESCCM + PC_WORK_AES128GCM + PC_WORK_MD)
6318#else
6319#define PC_SECURE_WORK_SMB 0
6320#endif
6321
6322#if PC_ENABLE_SSH || PC_ENABLE_SSH_CLIENT
6323#define PC_SECURE_WORK_SSHCIPHER PC_WORK_AES256CTR
6324#else
6325#define PC_SECURE_WORK_SSHCIPHER 0
6326#endif
6327
6328#define PC_SECURE_ARENA_SIZE \
6329 (PC_SECURE_WORK_BIGNUM + PC_SECURE_WORK_AEAD + PC_SECURE_WORK_MAC + PC_SECURE_WORK_SMB + \
6330 PC_SECURE_WORK_SSHCIPHER + 256) // + 256: alignment round-up across the individual borrows
6331#endif
6332
6333// ---------------------------------------------------------------------------
6334// Static RAM (BSS) usage table
6335// ---------------------------------------------------------------------------
6336//
6337// All library memory is in BSS - allocated at link time, zero-initialized by
6338// the C runtime, never heap-allocated after begin(). The table below shows
6339// the contribution of every feature at its default constant values.
6340//
6341// Sizes are for ESP32 (32-bit pointers, int = 4 B). Where a size depends on
6342// a macro the formula is given so you can compute the impact of any change.
6343//
6344// ┌──────────────────────────────┬──────────────────────────────────────────────────────────────┬──────────┐
6345// │ Symbol / pool │ Size formula │ Default │
6346// ├──────────────────────────────┼──────────────────────────────────────────────────────────────┼──────────┤
6347// │ TRANSPORT LAYER (always on) │ │ │
6348// │ conn_pool[MAX_CONNS] │ MAX_CONNS × (RX_BUF_SIZE + 22) │ 4 168 B │
6349// │ listener_pool[MAX_LISTENERS]│ MAX_LISTENERS × (StaticQueue_t≈48 + EVT_QUEUE_DEPTH×12 + 18)│ 654 B │
6350// │ conn_timeout_ms │ 4 B │ 4 B │
6351// │ TRANSPORT SUBTOTAL │ │ 4 826 B │
6352// ├──────────────────────────────┼──────────────────────────────────────────────────────────────┼──────────┤
6353// │ HTTP PRESENTATION (always on)│ │ │
6354// │ http_pool[MAX_CONNS] │ MAX_CONNS × (MAX_PATH_LEN + MAX_QUERY_LEN │ │
6355// │ │ + MAX_HEADERS×(MAX_KEY_LEN+MAX_VAL_LEN) │ │
6356// │ │ + MAX_QUERY_PARAMS×(QUERY_KEY_LEN+QUERY_VAL_LEN) │ │
6357// │ │ + BODY_BUF_SIZE + 50) │ 6 668 B │
6358// │ HTTP SUBTOTAL │ │ 6 668 B │
6359// ├──────────────────────────────┼──────────────────────────────────────────────────────────────┼──────────┤
6360// │ WEBSOCKET (PC_ENABLE_WEBSOCKET=1) │ │
6361// │ ws_pool[MAX_WS_CONNS] │ MAX_WS_CONNS × (WS_FRAME_SIZE + 29) │ 1 082 B │
6362// ├──────────────────────────────┼──────────────────────────────────────────────────────────────┼──────────┤
6363// │ SSE (PC_ENABLE_SSE=1) │ │ │
6364// │ pc_sse_pool[MAX_SSE_CONNS] │ MAX_SSE_CONNS × (MAX_PATH_LEN + 3) │ 134 B │
6365// ├──────────────────────────────┼──────────────────────────────────────────────────────────────┼──────────┤
6366// │ SSH (PC_ENABLE_SSH=1) │ │ │
6367// │ ssh_pool[MAX_SSH_CONNS] │ MAX_SSH_CONNS × (SSH_PKT_BUF_SIZE + 22) │ 2 070 B │
6368// │ ssh_keys[MAX_SSH_CONNS] │ MAX_SSH_CONNS × sizeof(SshKeyMat) │ 1187 B │
6369// │ └─ SshKeyMat (all builds) │ 2×aes_key[32] + 2×aes_iv[16] + 2×mac_key[64] │ │
6370// │ │ + 2×chacha_key[64] + 3 flags = 355 B, plus the two keyed │ │
6371// │ │ GCM contexts 2×PC_WORK_AESGCM (832 B on a vendor AEAD). │ │
6372// │ │ The contexts buy ~9,200 cycles per packet - a FIXED cost │ │
6373// │ │ that dominates small interactive traffic (see aesgcm.h). │ │
6374// │ │ CTR still rebuilds its schedule in scratch per packet. │ │
6375// │ ssh_dh[MAX_SSH_CONNS] │ MAX_SSH_CONNS × (3×pc_bignum[256] + H[32] + 1) │ 801 B │
6376// │ crypto_work[] │ PC_CRYPTO_WORK_SIZE (scratch, wiped after each use) │ 2 144 B │
6377// │ SSH SUBTOTAL │ │ 5 370 B │
6378// ├──────────────────────────────┼──────────────────────────────────────────────────────────────┼──────────┤
6379// │ GRAND TOTAL (all features) │ │ ≈18 KB │
6380// └──────────────────────────────┴──────────────────────────────────────────────────────────────┴──────────┘
6381//
6382// ESP32 has 320 KB of SRAM; the library uses ~5–18 KB depending on features.
6383// Stack usage is separate; the largest frame is during SSH DH key exchange
6384// (~256 B for the pc_bignum private scalar on the call stack before it is
6385// zeroed by ssh_dh_finish()).
6386//
6387// SSH KEY MATERIAL IS NOT IN THE TABLE ABOVE intentionally:
6388// - The RSA host private key is NEVER stored in any static array. It is
6389// loaded from NVS into a local stack frame at sign time, used once, then
6390// explicitly zeroed (volatile memset) before the function returns.
6391// - AES session keys and HMAC keys live in ssh_keys[] (above), which is a
6392// separate BSS symbol from ssh_pool[]. Physical separation means a
6393// buffer overflow in the packet receive path (ssh_pool[].pkt_buf) cannot
6394// reach the key material without crossing a distinct linker symbol - a
6395// significant barrier against heap/BSS spray attacks.
6396// - The DH ephemeral private scalar y lives in ssh_dh[].y and is zeroed
6397// immediately after the shared secret K is derived.
6398// - crypto_work[] is zeroed via pc_secure_wipe() after every use so that
6399// bignum intermediates (including partial products that contain key
6400// material) do not persist in memory.
6401
6402// ---------------------------------------------------------------------------
6403// Runtime configuration struct
6404// ---------------------------------------------------------------------------
6405
6406/**
6407 * @brief Runtime-tunable server parameters.
6408 *
6409 * Can be declared as `const PROGMEM` (flash) or as a mutable variable (RAM).
6410 * Pass a pointer to PC::begin() or DeterministicAsyncTCP::init().
6411 */
6413{
6414 /** Milliseconds of inactivity before a connection is force-closed. */
6416};
6417
6418// ---------------------------------------------------------------------------
6419// Protocol identifier
6420// ---------------------------------------------------------------------------
6421
6422/**
6423 * @brief Application protocol spoken on a listener port or connection slot.
6424 *
6425 * Stored in both Listener::proto and TcpConn::proto. The session layer uses
6426 * this to route events to the correct protocol handler without branching on
6427 * port numbers.
6428 *
6429 * All values are always present regardless of feature flags - the enum is
6430 * part of the listener API. Feature flags gate the implementation, not the
6431 * identifier.
6432 */
6433enum class ConnProto : uint8_t
6434{
6435 PROTO_NONE = 0, ///< Unassigned slot.
6436 PROTO_HTTP = 1, ///< HTTP/1.1 with optional WS and SSE upgrades.
6437 PROTO_TELNET = 2, ///< Telnet (RFC 854).
6438 PROTO_SSH = 3, ///< SSH (RFC 4253/4252/4254).
6439 PROTO_MODBUS = 4, ///< Modbus TCP slave (Modbus Application Protocol).
6440 PROTO_OPCUA = 5, ///< OPC UA Binary (UA-TCP) server.
6441 PROTO_SSH_RFWD = 6, ///< SSH remote-forward listener (ssh -R): accepts bridge to a forwarded-tcpip channel.
6442 PROTO_RELAY = 7, ///< TCP relay / DNAT (PC_ENABLE_RELAY): bridge to an origin pc_client connection.
6443 PROTO_BRIDGE = 8, ///< address:port -> hardware bus (PC_ENABLE_IFACE_BRIDGE): UART/SPI/I2C device server.
6444 PROTO_NTRIP_CASTER = 9, ///< NTRIP caster (PC_ENABLE_NTRIP_CASTER): serves RTCM3 corrections to rovers.
6445 PROTO_MESH = 10, ///< Edge-cache sibling link (PC_ENABLE_EDGE_MESH): answers a peer's content-addressed query.
6446};
6447
6448/**
6449 * @brief Network interface a connection arrived on (for per-route filtering).
6450 *
6451 * Stamped onto each TcpConn at accept time by comparing the connection's local
6452 * IP to the softAP IP (see PC::set_ap_ip()). Used to gate routes to
6453 * the station or softAP interface only (PC::on(..., pc_iface)).
6454 */
6455enum class pc_iface : uint8_t
6456{
6457 PC_IFACE_ANY = 0, ///< Unknown / no filter (matches any interface).
6458 PC_IFACE_STA = 1, ///< Station interface (joined to an AP / your LAN).
6459 PC_IFACE_AP = 2, ///< softAP interface (clients joined to the device).
6460 PC_IFACE_ETH = 3, ///< Ethernet interface (wired PHY).
6461};
6462
6463// ---------------------------------------------------------------------------
6464// Diagnostic JSON string (only defined when PC_ENABLE_DIAG == 1)
6465// ---------------------------------------------------------------------------
6466// PC_DIAG_JSON is a compile-time string literal - zero runtime cost.
6467// Adjacent string literals are concatenated by the compiler; PC_STR()
6468// stringifies an integer macro value without evaluating it twice.
6469
6470#if PC_ENABLE_DIAG
6471
6472#define _PC_STR_(x) #x
6473#define _PC_STR(x) _PC_STR_(x)
6474
6475#if PC_ENABLE_WEBSOCKET
6476#define _PC_F_WS "true"
6477#else
6478#define _PC_F_WS "false"
6479#endif
6480
6481#if PC_ENABLE_SSE
6482#define _PC_F_SSE "true"
6483#else
6484#define _PC_F_SSE "false"
6485#endif
6486
6487#if PC_ENABLE_MULTIPART
6488#define _PC_F_MP "true"
6489#else
6490#define _PC_F_MP "false"
6491#endif
6492
6493#if PC_ENABLE_FILE_SERVING
6494#define _PC_F_FS "true"
6495#else
6496#define _PC_F_FS "false"
6497#endif
6498
6499#if PC_ENABLE_AUTH
6500#define _PC_F_AUTH "true"
6501#else
6502#define _PC_F_AUTH "false"
6503#endif
6504
6505// Every PC_ENABLE_* defaults to 0/1, so a two-level token-paste stringifies each to "true"/"false" and
6506// the protocol keys below stay one line each (no 5-line #if per flag). Lets tooling (pentesting/pc_pentest.py)
6507// auto-detect which protocols are built and select the applicable attacks without a manual --all.
6508#define _PC_BOOL_0 "false"
6509#define _PC_BOOL_1 "true"
6510#define _PC_FB2(v) _PC_BOOL_##v
6511#define _PC_FB(v) _PC_FB2(v)
6512
6513#define PC_DIAG_JSON \
6514 "{" \
6515 "\"lib\":\"ProtoCore\"," \
6516 "\"features\":{" \
6517 "\"websocket\":" _PC_F_WS "," \
6518 "\"sse\":" _PC_F_SSE "," \
6519 "\"multipart\":" _PC_F_MP "," \
6520 "\"file_serving\":" _PC_F_FS "," \
6521 "\"auth\":" _PC_F_AUTH "," \
6522 "\"webdav\":" _PC_FB( \
6523 PC_ENABLE_WEBDAV) "," \
6524 "\"coap\":" _PC_FB(PC_ENABLE_COAP) "," \
6525 "\"snmp\":" _PC_FB( \
6526 PC_ENABLE_SNMP) "," \
6527 "\"opcua\":" _PC_FB(PC_ENABLE_OPCUA) "," \
6528 "\"umati\":" _PC_FB( \
6529 PC_ENABLE_UMATI) "," \
6530 "\"modbus\":" _PC_FB( \
6531 PC_ENABLE_MODBUS) "," \
6532 "\"mqtt\":" _PC_FB(PC_ENABLE_MQTT) "," \
6533 "\"mtconnect\":" _PC_FB(PC_ENABLE_MTCONNECT) "," \
6534 "\"redis\":" _PC_FB(PC_ENABLE_REDIS) "," \
6535 "\"ftp\":" _PC_FB(PC_ENABLE_FTP) "," \
6536 "\"smtp\":" _PC_FB(PC_ENABLE_SMTP) "," \
6537 "\"smb\":" _PC_FB(PC_ENABLE_SMB) "," \
6538 "\"syslog\":" _PC_FB(PC_ENABLE_SYSLOG) "," \
6539 "\"pc_ntp_server\":" _PC_FB( \
6540 PC_ENABLE_NTP_SERVER) "," \
6541 "\"dns_server\":" _PC_FB(PC_ENABLE_DNS_SERVER) "," \
6542 "\"nats\":" _PC_FB( \
6543 PC_ENABLE_NATS) "," \
6544 "\"stomp\":" _PC_FB( \
6545 PC_ENABLE_STOMP) "," \
6546 "\"statsd\":" _PC_FB(PC_ENABLE_STATSD) "," \
6547 "\"jwt\":" _PC_FB(PC_ENABLE_JWT) "," \
6548 "\"tls\":" _PC_FB( \
6549 PC_ENABLE_TLS) "," \
6550 "\"http2\":" _PC_FB(PC_ENABLE_HTTP2) "," \
6551 "\"http3\":" _PC_FB(PC_ENABLE_HTTP3) "," \
6552 "\"ssh\":" _PC_FB( \
6553 PC_ENABLE_SSH) "," \
6554 "\"ws_deflate\":" _PC_FB( \
6555 PC_ENABLE_WS_DEFLATE) "," \
6556 "\"range\":" _PC_FB( \
6557 PC_ENABLE_RANGE) "," \
6558 "\"csrf\":" _PC_FB(PC_ENABLE_CSRF) "," \
6559 "\"accept_throttle\":" _PC_FB(PC_ENABLE_ACCEPT_THROTTLE) "," \
6560 "\"per_ip_throttle\":" _PC_FB(PC_ENABLE_PER_IP_THROTTLE) "," \
6561 "\"auth_lockout\":" _PC_FB( \
6562 PC_ENABLE_AUTH_LOCKOUT) "}," \
6563 "\"config\":{" \
6564 "\"MAX_CONNS\":" _PC_STR( \
6565 MAX_CONNS) "," \
6566 "\"RX_BUF_SIZE\":" _PC_STR( \
6567 RX_BUF_SIZE) "," \
6568 "\"BODY_BUF_SIZE\":" _PC_STR(BODY_BUF_SIZE) "," \
6569 "\"MAX_ROUTES\":" _PC_STR(MAX_ROUTES) "," \
6570 "\"MAX_" \
6571 "HEADERS\"" \
6572 ":" _PC_STR( \
6573 MAX_HEADERS) "," \
6574 "\"MAX_PATH_" \
6575 "LEN\"" \
6576 ":" _PC_STR(MAX_PATH_LEN) "," \
6577 "\"MAX_KEY_LEN\":" _PC_STR(MAX_KEY_LEN) "," \
6578 "\"MAX_VAL_LEN\":" _PC_STR(MAX_VAL_LEN) "," \
6579 "\"MAX_QUERY_LEN\":" _PC_STR(MAX_QUERY_LEN) "," \
6580 "\"MAX_QUERY_PARAMS\":" _PC_STR(MAX_QUERY_PARAMS) "," \
6581 "\"CONN_TIMEOUT_MS\":" _PC_STR(CONN_TIMEOUT_MS) "," \
6582 "\"RESP_HDR_BUF_SIZE\":" _PC_STR(RESP_HDR_BUF_SIZE) "," \
6583 "\"WS_HDR_BUF_SIZE\":" _PC_STR( \
6584 WS_HDR_BUF_SIZE) "," \
6585 "\"CORS_HDR_BUF_SIZE\":" _PC_STR( \
6586 CORS_HDR_BUF_SIZE) "," \
6587 "\"EVT_QUEUE_DEPTH\":" _PC_STR( \
6588 EVT_QUEUE_DEPTH) "}" \
6589 "}"
6590
6591#endif // PC_ENABLE_DIAG
6592
6593// ---------------------------------------------------------------------------
6594// Compile-time sanity checks
6595// ---------------------------------------------------------------------------
6596// These produce a clear #error message in the compiler output rather than a
6597// cryptic linker failure or silent misbehavior.
6598
6599#if EVT_QUEUE_DEPTH < MAX_CONNS * 4
6600#error "ProtoCore: EVT_QUEUE_DEPTH must be >= MAX_CONNS * 4 to absorb event bursts without blocking lwIP"
6601#endif
6602
6603#if MAX_CONNS < 1
6604#error "ProtoCore: MAX_CONNS must be >= 1"
6605#endif
6606
6607#if MAX_CONNS > 255
6608#error "ProtoCore: MAX_CONNS must be <= 255 (slot IDs are uint8_t)"
6609#endif
6610
6611#if PC_ENABLE_WEBSOCKET && PC_ENABLE_SSE
6612#if MAX_WS_CONNS + MAX_SSE_CONNS > MAX_CONNS
6613#error "ProtoCore: MAX_WS_CONNS + MAX_SSE_CONNS must not exceed MAX_CONNS"
6614#endif
6615#elif PC_ENABLE_WEBSOCKET
6616#if MAX_WS_CONNS > MAX_CONNS
6617#error "ProtoCore: MAX_WS_CONNS must not exceed MAX_CONNS"
6618#endif
6619#elif PC_ENABLE_SSE
6620#if MAX_SSE_CONNS > MAX_CONNS
6621#error "ProtoCore: MAX_SSE_CONNS must not exceed MAX_CONNS"
6622#endif
6623#endif
6624
6625#if BODY_BUF_SIZE < 1
6626#error "ProtoCore: BODY_BUF_SIZE must be >= 1"
6627#endif
6628
6629#if BODY_BUF_SIZE > RX_BUF_SIZE
6630#error "ProtoCore: BODY_BUF_SIZE must not exceed RX_BUF_SIZE (parser reads from the ring buffer)"
6631#endif
6632
6633#if PC_ENABLE_FILE_SERVING && FILE_CHUNK_SIZE > RX_BUF_SIZE
6634#error "ProtoCore: FILE_CHUNK_SIZE must not exceed RX_BUF_SIZE"
6635#endif
6636
6637#if MAX_KEY_LEN < 4
6638#error "ProtoCore: MAX_KEY_LEN must be >= 4 (minimum valid HTTP header name length)"
6639#endif
6640
6641#if MAX_VAL_LEN < 1
6642#error "ProtoCore: MAX_VAL_LEN must be >= 1"
6643#endif
6644
6645#if MAX_PATH_LEN < 2
6646#error "ProtoCore: MAX_PATH_LEN must be >= 2 (minimum: \"/\")"
6647#endif
6648
6649#if MAX_ROUTES < 1
6650#error "ProtoCore: MAX_ROUTES must be >= 1"
6651#endif
6652
6653#if MAX_MIDDLEWARE < 1
6654#error "ProtoCore: MAX_MIDDLEWARE must be >= 1"
6655#endif
6656
6657#if CHUNK_BUF_SIZE < 16
6658#error "ProtoCore: CHUNK_BUF_SIZE must be >= 16"
6659#endif
6660
6661#if JSON_MAX_DEPTH < 1
6662#error "ProtoCore: JSON_MAX_DEPTH must be >= 1"
6663#endif
6664
6665#if RE_MAX_STEPS < 64
6666#error "ProtoCore: RE_MAX_STEPS must be >= 64"
6667#endif
6668
6669// RSA-2048 verification (OIDC / SSH host key / JWKS) runs on a worker task and consumes
6670// ~7 KB of stack via the mbedTLS bignum modexp. Enforce the documented floor so a lowered
6671// worker stack is caught at build time instead of overflowing on the first verify.
6672#if PC_ENABLE_OIDC && !PC_ENABLE_SSH && (PC_WORKER_TASK_STACK < PC_WORKER_STACK_RSA_MIN)
6673#error \
6674 "ProtoCore: PC_WORKER_TASK_STACK is below PC_WORKER_STACK_RSA_MIN; RSA-2048 verification (OIDC) needs ~7 KB of worker stack - raise PC_WORKER_TASK_STACK (>= 8192) or marshal RSA verifies onto a dedicated larger-stack task"
6675#endif
6676
6677// SSH additionally can negotiate curve25519-sha256 + ssh-ed25519, whose software field
6678// arithmetic peaks at ~10.5 KB of worker stack (deeper than the RSA path). Enforce the
6679// higher floor so a lowered stack is caught at build time instead of tripping the task
6680// stack canary on the first modern-crypto handshake.
6681#if (PC_ENABLE_SSH || PC_ENABLE_SSH_CLIENT || PC_ENABLE_HTTP3) && (PC_WORKER_TASK_STACK < PC_WORKER_STACK_CURVE_MIN)
6682#error \
6683 "ProtoCore: PC_WORKER_TASK_STACK is below PC_WORKER_STACK_CURVE_MIN; SSH (server or reverse-SSH client) and HTTP/3 (QUIC TLS-1.3) curve25519/ed25519 need ~10.5 KB of worker stack - raise PC_WORKER_TASK_STACK (>= 12288) or marshal the handshake onto a dedicated larger-stack task"
6684#endif
6685
6686// The PQ/T hybrid KEX (PC_ENABLE_PQC_KEX) runs ML-KEM-768 Encaps in the handshake path, whose NTT
6687// + sampling peak at ~7 KB of worker stack on top of the classical curve/ed25519 work. Enforce a
6688// higher floor so it is caught at build time rather than overflowing on the first hybrid handshake.
6689#ifndef PC_WORKER_STACK_PQC_MIN
6690#define PC_WORKER_STACK_PQC_MIN 16384
6691#endif
6692#if PC_ENABLE_PQC_KEX && !PC_ENABLE_SSH_SNTRUP761 && (PC_ENABLE_SSH || PC_ENABLE_SSH_CLIENT || PC_ENABLE_HTTP3) && \
6693 (PC_WORKER_TASK_STACK < PC_WORKER_STACK_PQC_MIN)
6694#error \
6695 "ProtoCore: PC_WORKER_TASK_STACK is below PC_WORKER_STACK_PQC_MIN; the ML-KEM-768 hybrid KEX (PC_ENABLE_PQC_KEX) needs ~7 KB more worker stack - raise PC_WORKER_TASK_STACK (>= 16384) or marshal the handshake onto a dedicated larger-stack task"
6696#endif
6697
6698// sntrup761x25519-sha512 (PC_ENABLE_SSH_SNTRUP761, on by default with the hybrid) is heavier than
6699// ML-KEM: the server runs Encaps (~22 KB), the reverse-SSH client runs KeyGen+Decaps whose FO
6700// re-encrypt peaks ~32 KB. Enforce the matching floor so it is caught at build time.
6701#ifndef PC_WORKER_STACK_SNTRUP_MIN
6702#if defined(PC_ENABLE_SSH_CLIENT) && PC_ENABLE_SSH_CLIENT
6703#define PC_WORKER_STACK_SNTRUP_MIN 40960
6704#else
6705#define PC_WORKER_STACK_SNTRUP_MIN 32768
6706#endif
6707#endif
6708// sntrup761x25519-sha512 is an SSH key exchange; only an SSH server / reverse-SSH client runs its heavy
6709// KeyGen/Encaps/Decaps on the worker stack. HTTP/3's PQC is the lighter ML-KEM hybrid, so an HTTP/3-only
6710// build (PC_ENABLE_SSH_SNTRUP761 defaults on with PQC but is dormant without SSH) must not trip this.
6711#if PC_ENABLE_SSH_SNTRUP761 && (PC_ENABLE_SSH || PC_ENABLE_SSH_CLIENT) && \
6712 (PC_WORKER_TASK_STACK < PC_WORKER_STACK_SNTRUP_MIN)
6713#error \
6714 "ProtoCore: PC_WORKER_TASK_STACK is below PC_WORKER_STACK_SNTRUP_MIN; sntrup761x25519-sha512 needs ~32 KB worker stack for the server Encaps and ~40 KB for the reverse-SSH client KeyGen+Decaps - raise PC_WORKER_TASK_STACK (>= 32768 server / 40960 client), set PC_ENABLE_SSH_SNTRUP761 0 to keep ML-KEM only, or marshal the handshake onto a dedicated larger-stack task"
6715#endif
6716
6717#if PC_ENABLE_TLS
6718#if MAX_TLS_CONNS < 1 || MAX_TLS_CONNS > MAX_CONNS
6719#error "ProtoCore: MAX_TLS_CONNS must be between 1 and MAX_CONNS"
6720#endif
6721#if PC_TLS_ARENA_SIZE < 8192
6722#error "ProtoCore: PC_TLS_ARENA_SIZE is far too small for a TLS handshake"
6723#endif
6724// Concurrent TLS guard: the whole arena is static .bss and the ESP32 internal
6725// dram0_0_seg ceiling is only ~122 KB (ROM-reserved at both ends), so a 2nd
6726// connection's arena overflows the link. Reject MAX_TLS_CONNS > 1 with a clear
6727// message unless the arena is offloaded to PSRAM or the build was consciously sized -
6728// far friendlier than the raw "region `dram0_0_seg' overflowed" linker error.
6729#if defined(ARDUINO) && (MAX_TLS_CONNS > 1) && !PC_TLS_ARENA_IN_PSRAM && !PC_TLS_ACK_MULTI_CONN_DRAM
6730#error \
6731 "ProtoCore: MAX_TLS_CONNS > 1 - the static TLS arena will not fit the ~122 KB internal dram0_0_seg. Pick a path (docs/KNOWN_LIMITATIONS.md): set PC_TLS_ARENA_IN_PSRAM=1 on a PSRAM board, OR shrink records via a custom ESP-IDF build (CONFIG_MBEDTLS_SSL_IN/OUT_CONTENT_LEN + PC_TLS_MAX_FRAG_LEN), OR reclaim internal DRAM; then set PC_TLS_ACK_MULTI_CONN_DRAM=1 to confirm."
6732#endif
6733#endif
6734
6735// HTTP/2's per-connection engine pool (~MAX_CONNS x 28 KB) cannot fit internal DRAM alongside
6736// TLS, so it must live in PSRAM. Fail fast with guidance instead of the raw linker overflow.
6737#if PC_ENABLE_HTTP2 && defined(ARDUINO) && !PC_H2_POOL_IN_PSRAM
6738#error \
6739 "ProtoCore: PC_ENABLE_HTTP2 needs PSRAM - the HTTP/2 engine pool (~MAX_CONNS x 28 KB) overflows the ~122 KB internal dram0_0_seg alongside TLS. Set PC_H2_POOL_IN_PSRAM=1 on a PSRAM board (S3 / P4 / WROVER) built with CONFIG_SPIRAM_ALLOW_BSS_SEG_EXTERNAL_MEMORY=y (tools/psram/README.md)."
6740#endif
6741
6742#if PC_ENABLE_SNMP
6743#if SNMP_MAX_OID_LEN < 4
6744#error "ProtoCore: SNMP_MAX_OID_LEN must be >= 4"
6745#endif
6746#if SNMP_MAX_MIB_ENTRIES < 1
6747#error "ProtoCore: SNMP_MAX_MIB_ENTRIES must be >= 1"
6748#endif
6749#if SNMP_MAX_VARBINDS < 1
6750#error "ProtoCore: SNMP_MAX_VARBINDS must be >= 1"
6751#endif
6752#if SNMP_MSG_BUF_SIZE < 484
6753#error "ProtoCore: SNMP_MSG_BUF_SIZE must be >= 484 (RFC 1157 minimum)"
6754#endif
6755#endif
6756
6757#if PC_ENABLE_COAP
6758#if PC_COAP_MAX_RESOURCES < 1
6759#error "ProtoCore: PC_COAP_MAX_RESOURCES must be >= 1"
6760#endif
6761#if PC_COAP_MAX_PATH < 2
6762#error "ProtoCore: PC_COAP_MAX_PATH must be >= 2 (minimum: \"/\")"
6763#endif
6764#if PC_COAP_MAX_PAYLOAD < 1
6765#error "ProtoCore: PC_COAP_MAX_PAYLOAD must be >= 1"
6766#endif
6767#if PC_COAP_MSG_BUF_SIZE < (PC_COAP_MAX_PAYLOAD + 16)
6768#error \
6769 "ProtoCore: PC_COAP_MSG_BUF_SIZE must be >= PC_COAP_MAX_PAYLOAD + 16 (header + token + Content-Format option + payload marker)"
6770#endif
6771#endif
6772
6773#if PC_ENABLE_AUTH && MAX_AUTH_LEN < 2
6774#error "ProtoCore: MAX_AUTH_LEN must be >= 2 when PC_ENABLE_AUTH is set"
6775#endif
6776
6777#if PC_ENABLE_PER_IP_THROTTLE
6778#if PC_PER_IP_THROTTLE_SLOTS < 1
6779#error "ProtoCore: PC_PER_IP_THROTTLE_SLOTS must be >= 1 when PC_ENABLE_PER_IP_THROTTLE is set"
6780#endif
6781#if PC_PER_IP_THROTTLE_MAX < 1
6782#error "ProtoCore: PC_PER_IP_THROTTLE_MAX must be >= 1 when PC_ENABLE_PER_IP_THROTTLE is set"
6783#endif
6784#endif
6785
6786#if PC_ENABLE_IP_ALLOWLIST && PC_IP_ALLOWLIST_SLOTS < 1
6787#error "ProtoCore: PC_IP_ALLOWLIST_SLOTS must be >= 1 when PC_ENABLE_IP_ALLOWLIST is set"
6788#endif
6789
6790#if PC_ENABLE_AUTH_LOCKOUT
6791#if !PC_ENABLE_AUTH
6792#error "ProtoCore: PC_ENABLE_AUTH_LOCKOUT requires PC_ENABLE_AUTH"
6793#endif
6794#if PC_AUTH_LOCKOUT_SLOTS < 1
6795#error "ProtoCore: PC_AUTH_LOCKOUT_SLOTS must be >= 1 when PC_ENABLE_AUTH_LOCKOUT is set"
6796#endif
6797#if PC_AUTH_LOCKOUT_THRESHOLD < 1
6798#error "ProtoCore: PC_AUTH_LOCKOUT_THRESHOLD must be >= 1 when PC_ENABLE_AUTH_LOCKOUT is set"
6799#endif
6800#if PC_AUTH_LOCKOUT_BASE_MS < 1 || PC_AUTH_LOCKOUT_MAX_MS < PC_AUTH_LOCKOUT_BASE_MS
6801#error "ProtoCore: need 1 <= PC_AUTH_LOCKOUT_BASE_MS <= PC_AUTH_LOCKOUT_MAX_MS"
6802#endif
6803// The backoff doubles a uint32 capped at MAX_MS, so MAX_MS must leave headroom for
6804// one more shift (cap <= 0x80000000 => cap<<1 fits in uint32 without overflow).
6805#if PC_AUTH_LOCKOUT_MAX_MS > 0x80000000
6806#error "ProtoCore: PC_AUTH_LOCKOUT_MAX_MS must be <= 0x80000000 (2147483648)"
6807#endif
6808#endif
6809
6810#if PC_ENABLE_FORWARDED_TRUST
6811#if !PC_ENABLE_AUTH_LOCKOUT
6812#error "ProtoCore: PC_ENABLE_FORWARDED_TRUST requires PC_ENABLE_AUTH_LOCKOUT"
6813#endif
6814#if PC_TRUSTED_PROXY_MAX < 1
6815#error "ProtoCore: PC_TRUSTED_PROXY_MAX must be >= 1 when PC_ENABLE_FORWARDED_TRUST is set"
6816#endif
6817#endif
6818
6819#if PC_ENABLE_WEBDAV
6820#if !PC_ENABLE_FILE_SERVING
6821#error "ProtoCore: PC_ENABLE_WEBDAV requires PC_ENABLE_FILE_SERVING"
6822#endif
6823#if PC_WEBDAV_BUF_SIZE < 256
6824#error "ProtoCore: PC_WEBDAV_BUF_SIZE must be >= 256"
6825#endif
6826#if PC_METHOD_BUF_SIZE < 10
6827#error "ProtoCore: PC_METHOD_BUF_SIZE must be >= 10 when PC_ENABLE_WEBDAV is set (PROPPATCH)"
6828#endif
6829#endif
6830
6831#if PC_NEED_MODBUS
6832#if PC_MODBUS_COILS < 1 || PC_MODBUS_DISCRETE_INPUTS < 1 || PC_MODBUS_HOLDING_REGS < 1 || PC_MODBUS_INPUT_REGS < 1
6833#error "ProtoCore: each PC_MODBUS_* table size must be >= 1 when PC_ENABLE_MODBUS is set"
6834#endif
6835#endif
6836
6837#if PC_ENABLE_KEEPALIVE && PC_KEEPALIVE_MAX_REQUESTS < 1
6838#error "ProtoCore: PC_KEEPALIVE_MAX_REQUESTS must be >= 1 when PC_ENABLE_KEEPALIVE is set"
6839#endif
6840
6841#if PC_ENABLE_RANGE && !PC_ENABLE_FILE_SERVING && !PC_ENABLE_EDGE_CACHE
6842#error "ProtoCore: PC_ENABLE_RANGE requires PC_ENABLE_FILE_SERVING or PC_ENABLE_EDGE_CACHE"
6843#endif
6844
6845#if PC_ENABLE_MTLS && !PC_ENABLE_TLS
6846#error "ProtoCore: PC_ENABLE_MTLS requires PC_ENABLE_TLS"
6847#endif
6848
6849#if PC_ENABLE_TLS_RESUMPTION && !PC_ENABLE_TLS
6850#error "ProtoCore: PC_ENABLE_TLS_RESUMPTION requires PC_ENABLE_TLS"
6851#endif
6852
6853#if PC_ENABLE_METRICS && !PC_ENABLE_STATS
6854#error "ProtoCore: PC_ENABLE_METRICS requires PC_ENABLE_STATS"
6855#endif
6856
6857#if PC_ENABLE_HTTP_CLIENT_TLS && !PC_ENABLE_TLS
6858#error "ProtoCore: PC_ENABLE_HTTP_CLIENT_TLS requires PC_ENABLE_TLS"
6859#endif
6860
6861#if PC_ENABLE_MQTT_TLS && !PC_ENABLE_TLS
6862#error "ProtoCore: PC_ENABLE_MQTT_TLS requires PC_ENABLE_TLS"
6863#endif
6864
6865#if PC_ENABLE_WS_CLIENT_TLS && !PC_ENABLE_TLS
6866#error "ProtoCore: PC_ENABLE_WS_CLIENT_TLS requires PC_ENABLE_TLS"
6867#endif
6868
6869#if PC_ENABLE_SNMP_TRAP && !PC_ENABLE_SNMP
6870#error "ProtoCore: PC_ENABLE_SNMP_TRAP requires PC_ENABLE_SNMP"
6871#endif
6872
6873#if PC_ENABLE_COAP_OBSERVE && !PC_ENABLE_COAP
6874#error "ProtoCore: PC_ENABLE_COAP_OBSERVE requires PC_ENABLE_COAP"
6875#endif
6876
6877#if PC_ENABLE_TLS_RPK && !(PC_ENABLE_DTLS || PC_ENABLE_HTTP3)
6878#error "ProtoCore: PC_ENABLE_TLS_RPK requires PC_ENABLE_DTLS or PC_ENABLE_HTTP3"
6879#endif
6880
6881#if PC_ENABLE_COAP_BLOCK
6882#if !PC_ENABLE_COAP
6883#error "ProtoCore: PC_ENABLE_COAP_BLOCK requires PC_ENABLE_COAP"
6884#endif
6885#if PC_COAP_BLOCK_SZX_MAX > 6
6886#error "ProtoCore: PC_COAP_BLOCK_SZX_MAX must be <= 6 (block size 2^(SZX+4); SZX 7 is reserved)"
6887#endif
6888#if PC_COAP_MSG_BUF_SIZE < ((1 << (PC_COAP_BLOCK_SZX_MAX + 4)) + 16)
6889#error "ProtoCore: PC_COAP_MSG_BUF_SIZE must hold one full block (2^(PC_COAP_BLOCK_SZX_MAX+4)) + 16 header/option bytes"
6890#endif
6891#if PC_COAP_BLOCK1_MAX < (1 << (PC_COAP_BLOCK_SZX_MAX + 4))
6892#error "ProtoCore: PC_COAP_BLOCK1_MAX must be >= one block (2^(PC_COAP_BLOCK_SZX_MAX+4))"
6893#endif
6894#endif
6895
6896// --- feature dependency guards (centralized; see the BUILD-FLAG DEPENDENCY TREE
6897// near the top of this file). A child feature requires its parent(s). ---
6898
6899#if PC_ENABLE_WS_DEFLATE && !PC_ENABLE_WEBSOCKET
6900#error "ProtoCore: PC_ENABLE_WS_DEFLATE requires PC_ENABLE_WEBSOCKET"
6901#endif
6902
6903#if PC_ENABLE_CIA402 && !PC_ENABLE_CANOPEN
6904#error "ProtoCore: PC_ENABLE_CIA402 requires PC_ENABLE_CANOPEN"
6905#endif
6906
6907#if PC_ENABLE_SSH_ZLIB && !PC_ENABLE_SSH
6908#error "ProtoCore: PC_ENABLE_SSH_ZLIB requires PC_ENABLE_SSH"
6909#endif
6910
6911#if PC_ENABLE_SSH_KEYBOARD_INTERACTIVE && !PC_ENABLE_SSH
6912#error "ProtoCore: PC_ENABLE_SSH_KEYBOARD_INTERACTIVE requires PC_ENABLE_SSH"
6913#endif
6914#if PC_ENABLE_SSH_KEYBOARD_INTERACTIVE && !PC_SSH_ALLOW_PASSWORD
6915#error \
6916 "ProtoCore: PC_ENABLE_SSH_KEYBOARD_INTERACTIVE is password-backed - it verifies the response through the password callback, so PC_SSH_ALLOW_PASSWORD must stay 1 (or drop keyboard-interactive for publickey-only)"
6917#endif
6918
6919#if PC_ENABLE_SSH_SFTP && !PC_ENABLE_SSH
6920#error "ProtoCore: PC_ENABLE_SSH_SFTP requires PC_ENABLE_SSH"
6921#endif
6922#if PC_ENABLE_SSH_SFTP && !PC_ENABLE_FILE_SERVING
6923#error "ProtoCore: PC_ENABLE_SSH_SFTP requires PC_ENABLE_FILE_SERVING (the fs::FS seam)"
6924#endif
6925#if PC_ENABLE_SSH_SCP && !PC_ENABLE_SSH
6926#error "ProtoCore: PC_ENABLE_SSH_SCP requires PC_ENABLE_SSH"
6927#endif
6928#if PC_ENABLE_SSH_SCP && !PC_ENABLE_FILE_SERVING
6929#error "ProtoCore: PC_ENABLE_SSH_SCP requires PC_ENABLE_FILE_SERVING (the fs::FS seam)"
6930#endif
6931
6932#if PC_ENABLE_SSH_ZLIB
6933// Window must be a power of two in [256, 32768] (32 KB is zlib's max; the client's inflate window).
6934#if (PC_SSH_ZLIB_WINDOW & (PC_SSH_ZLIB_WINDOW - 1)) != 0 || PC_SSH_ZLIB_WINDOW < 256 || PC_SSH_ZLIB_WINDOW > 32768
6935#error "ProtoCore: PC_SSH_ZLIB_WINDOW must be a power of two in [256, 32768]"
6936#endif
6937// Positions index a uint16 hash chain, so window + max-input must stay under 65535.
6938#if (PC_SSH_ZLIB_WINDOW + PC_SSH_ZLIB_MAX_IN) > 65534
6939#error "ProtoCore: PC_SSH_ZLIB_WINDOW + PC_SSH_ZLIB_MAX_IN must be <= 65534"
6940#endif
6941// The compressor must accept a full packet payload, else a max-size outbound packet would fail to
6942// compress and drop, desyncing the stateful stream.
6943#if PC_SSH_ZLIB_MAX_IN < SSH_PKT_BUF_SIZE
6944#error "ProtoCore: PC_SSH_ZLIB_MAX_IN must be >= SSH_PKT_BUF_SIZE"
6945#endif
6946// The per-connection compression pool is ~80 KB: the s2c deflate work buffer + hash chain (~48 KB)
6947// plus the c2s 32 KB context-takeover inflate window. On ARDUINO pick a path: offload it to PSRAM
6948// (PC_SSH_ZLIB_IN_PSRAM, a PSRAM board built with the BSS-in-PSRAM core), or acknowledge the
6949// internal-DRAM cost (PC_SSH_ZLIB_ACK_DRAM, fine for MAX_SSH_CONNS=1 without TLS on a roomy S3 / P4).
6950// Fail fast with guidance instead of a raw linker overflow.
6951#if defined(ARDUINO) && !PC_SSH_ZLIB_IN_PSRAM && !PC_SSH_ZLIB_ACK_DRAM
6952#error \
6953 "ProtoCore: PC_ENABLE_SSH_ZLIB - the per-connection compression pool is ~80 KB (s2c deflate ~48 KB + the c2s 32 KB inflate window). Set PC_SSH_ZLIB_IN_PSRAM=1 on a PSRAM board (S3 / P4 / WROVER, core built with CONFIG_SPIRAM_ALLOW_BSS_SEG_EXTERNAL_MEMORY=y, tools/psram/README.md), OR set PC_SSH_ZLIB_ACK_DRAM=1 to accept the internal-DRAM cost (fits MAX_SSH_CONNS=1 without TLS on a roomy chip)."
6954#endif
6955#endif
6956
6957#if PC_ENABLE_WEB_TERMINAL && !PC_ENABLE_WEBSOCKET
6958#error "ProtoCore: PC_ENABLE_WEB_TERMINAL requires PC_ENABLE_WEBSOCKET"
6959#endif
6960
6961#if PC_ENABLE_DASHBOARD && !PC_ENABLE_SSE
6962#error "ProtoCore: PC_ENABLE_DASHBOARD requires PC_ENABLE_SSE"
6963#endif
6964
6965#if PC_ENABLE_SNMP_V3 && !PC_ENABLE_SNMP
6966#error "ProtoCore: PC_ENABLE_SNMP_V3 requires PC_ENABLE_SNMP"
6967#endif
6968
6969#if PC_ENABLE_OPCUA_CLIENT && !PC_ENABLE_OPCUA
6970#error "ProtoCore: PC_ENABLE_OPCUA_CLIENT requires PC_ENABLE_OPCUA (the shared OPC UA codec)"
6971#endif
6972
6973#if PC_ENABLE_UMATI && !PC_ENABLE_OPCUA
6974#error "ProtoCore: PC_ENABLE_UMATI requires PC_ENABLE_OPCUA (it builds on the OPC UA server)"
6975#endif
6976
6977#if PC_ENABLE_ROBOTICS && !PC_ENABLE_OPCUA
6978#error "ProtoCore: PC_ENABLE_ROBOTICS requires PC_ENABLE_OPCUA (it builds on the OPC UA server)"
6979#endif
6980
6981#if PC_ENABLE_EUROMAP77 && !PC_ENABLE_OPCUA
6982#error "ProtoCore: PC_ENABLE_EUROMAP77 requires PC_ENABLE_OPCUA (it builds on the OPC UA server)"
6983#endif
6984
6985#if PC_ENABLE_CONFIG_IO && !PC_ENABLE_CONFIG_STORE
6986#error "ProtoCore: PC_ENABLE_CONFIG_IO requires PC_ENABLE_CONFIG_STORE"
6987#endif
6988
6989#if PC_ENABLE_HTTP_CLIENT_TLS && !PC_ENABLE_HTTP_CLIENT
6990#error "ProtoCore: PC_ENABLE_HTTP_CLIENT_TLS requires PC_ENABLE_HTTP_CLIENT"
6991#endif
6992
6993#if PC_ENABLE_MQTT_TLS && !PC_ENABLE_MQTT
6994#error "ProtoCore: PC_ENABLE_MQTT_TLS requires PC_ENABLE_MQTT"
6995#endif
6996
6997#if PC_ENABLE_WS_CLIENT_TLS && !PC_ENABLE_WS_CLIENT
6998#error "ProtoCore: PC_ENABLE_WS_CLIENT_TLS requires PC_ENABLE_WS_CLIENT"
6999#endif
7000
7001#if PC_ENABLE_WEBSOCKET && WS_FRAME_SIZE < 2
7002#error "ProtoCore: WS_FRAME_SIZE must be >= 2 when PC_ENABLE_WEBSOCKET is set"
7003#endif
7004
7005#if PC_ENABLE_SSE && SSE_BUF_SIZE < 8
7006#error "ProtoCore: SSE_BUF_SIZE must be >= 8 when PC_ENABLE_SSE is set"
7007#endif
7008
7009#if PC_ENABLE_MULTIPART && MAX_MULTIPART_PARTS < 1
7010#error "ProtoCore: MAX_MULTIPART_PARTS must be >= 1 when PC_ENABLE_MULTIPART is set"
7011#endif
7012
7013#if RESP_HDR_BUF_SIZE < 128
7014#error "ProtoCore: RESP_HDR_BUF_SIZE must be >= 128 (status line + headers + CORS block)"
7015#endif
7016
7017#if PC_ENABLE_WEBSOCKET && WS_HDR_BUF_SIZE < 128
7018#error "ProtoCore: WS_HDR_BUF_SIZE must be >= 128 when PC_ENABLE_WEBSOCKET is set"
7019#endif
7020
7021#if CORS_HDR_BUF_SIZE < 64
7022#error "ProtoCore: CORS_HDR_BUF_SIZE must be >= 64"
7023#endif
7024
7025#if RESP_HDR_BUF_SIZE < (CORS_HDR_BUF_SIZE + EXTRA_HDR_BUF_SIZE + 96)
7026#error \
7027 "ProtoCore: RESP_HDR_BUF_SIZE must be >= CORS_HDR_BUF_SIZE + EXTRA_HDR_BUF_SIZE + 96 (status line + CORS block + custom-header block are injected into response headers)"
7028#endif
7029
7030// ===========================================================================
7031// Feature tuning knobs (grouped and gated by feature)
7032// ===========================================================================
7033//
7034// One place to turn every tunable knob - buffer sizes, table depths, limits, thresholds -
7035// so you never have to open a feature header. Each is an override-able default (set it in
7036// your build flags to change it); the owning module includes this header and uses the
7037// value. An optional feature's group is wrapped in that feature's PC_ENABLE_* flag
7038// (resolved above), so a knob only exists when its feature is built.
7039//
7040// What is NOT here: protocol- and algorithm-fixed constants (wire opcodes, magic bytes,
7041// crypto digest / block sizes, spec-mandated PDU / field widths, the deflate/inflate
7042// scratch sizes a static_assert pins to the table layout). Those are not knobs - changing
7043// them breaks conformance - so they stay in their feature file next to the code they bind.
7044
7045// -- Core: protocol dispatch + shared outbound transport (always built) --
7046/** @brief Size of the protocol-handler dispatch table; must exceed the largest ConnProto id. */
7047#ifndef PC_PROTO_MAX
7048#define PC_PROTO_MAX 11
7049#endif
7050// proto_register / proto_get index this table by ConnProto id, so it must be wide enough for every id.
7051static_assert((unsigned)ConnProto::PROTO_MESH < PC_PROTO_MAX, "PC_PROTO_MAX must exceed the largest ConnProto id");
7052/** @brief Reverse-SSH tunnel: max concurrent forwarded-tcpip channels bridged at once. A relay that
7053 * forwards to a web UI opens one channel per inbound TCP connection, so this bounds concurrency. */
7054#if PC_ENABLE_SSH_CLIENT
7055#ifndef PC_SSH_CLIENT_MAX_CHANNELS
7056#define PC_SSH_CLIENT_MAX_CHANNELS 4
7057#endif
7058#if PC_SSH_CLIENT_MAX_CHANNELS < 1
7059#error "ProtoCore: PC_SSH_CLIENT_MAX_CHANNELS must be >= 1"
7060#endif
7061#endif
7062
7063/** @brief Number of simultaneous outbound client connections (BSS pool size). */
7064#ifndef PC_CLIENT_CONNS
7065#if PC_ENABLE_SSH_CLIENT
7066// The reverse-SSH tunnel holds the relay connection plus one local bridge per forwarded channel, so
7067// the pool must cover 1 + PC_SSH_CLIENT_MAX_CHANNELS or channels past the pool fail to bridge.
7068#define PC_CLIENT_CONNS (1 + PC_SSH_CLIENT_MAX_CHANNELS)
7069#else
7070#define PC_CLIENT_CONNS 2
7071#endif
7072#endif
7073
7074// The FTP session driver holds a control connection and a data connection at the same time; with a
7075// smaller pool the data open would fail after login and every transfer would abort mid-session.
7076#if PC_ENABLE_FTP_SESSION && (PC_CLIENT_CONNS < 2)
7077#error "ProtoCore: PC_ENABLE_FTP_SESSION needs PC_CLIENT_CONNS >= 2 (control + data)"
7078#endif
7079
7080// The reverse-SSH tunnel needs the relay connection plus one local bridge per forwarded channel.
7081#if PC_ENABLE_SSH_CLIENT && (PC_CLIENT_CONNS < 1 + PC_SSH_CLIENT_MAX_CHANNELS)
7082#error \
7083 "ProtoCore: PC_ENABLE_SSH_CLIENT needs PC_CLIENT_CONNS >= 1 + PC_SSH_CLIENT_MAX_CHANNELS (relay + one local bridge per channel)"
7084#endif
7085
7086/**
7087 * @brief Per-connection wire receive ring size (bytes).
7088 *
7089 * Holds plaintext (plain) or ciphertext (TLS). The transport ACKs on consume
7090 * (pc_client_read reopens the window), so for a large inbound transfer to never
7091 * stall the ring must hold a full TCP receive window: keep PC_CLIENT_RX_BUF >=
7092 * TCP_WND (~5.7 KB). The 8192 default clears that and a multi-KB TLS handshake
7093 * flight; a ring below TCP_WND can deadlock a sustained download (the peer would be
7094 * allowed to send more than the ring holds). Must exceed one TCP segment (TCP_MSS).
7095 */
7096#ifndef PC_CLIENT_RX_BUF
7097#define PC_CLIENT_RX_BUF 8192
7098#endif
7099
7100// -- SSH (network_drivers/presentation/ssh; the codec compiles when the SSH sources are
7101// built, so its knobs are always defined) --
7102/** @brief Initial receive window the SSH server advertises (RFC 4254 §5.1). */
7103#ifndef SSH_CHAN_WINDOW
7104#define SSH_CHAN_WINDOW 32768u
7105#endif
7106/**
7107 * @brief Maximum SSH channel data payload the server advertises it can receive per message.
7108 *
7109 * This is what a peer may put in one SSH_MSG_CHANNEL_DATA, so it MUST fit one inbound SSH packet: the
7110 * transport rejects any packet larger than SSH_PKT_BUF_SIZE, so advertising more than that (minus the
7111 * channel-data + packet framing + MAC + padding) makes a peer that sends a bigger message - e.g. an SFTP
7112 * WRITE - trip the packet-too-large check and drop the connection. Derived from SSH_PKT_BUF_SIZE so it scales
7113 * when that buffer is raised (e.g. for higher SFTP throughput). Interactive shells never approach it.
7114 */
7115#ifndef SSH_CHAN_MAX_PACKET
7116#define SSH_CHAN_MAX_PACKET (SSH_PKT_BUF_SIZE - 64u)
7117#endif
7118/**
7119 * @brief Re-key when either packet sequence number reaches this value.
7120 *
7121 * RFC 4253 §9 recommends re-keying after ~1 GB or one hour. A packet-count proxy is used
7122 * here; the default is well below SSH_SEQ_CLOSE_THRESHOLD so a re-key always happens before
7123 * the sequence number can wrap.
7124 */
7125#ifndef SSH_REKEY_PACKET_THRESHOLD
7126#define SSH_REKEY_PACKET_THRESHOLD 0x40000000u
7127#endif
7128/**
7129 * @brief Elapsed-time re-key trigger in milliseconds (RFC 4253 §9: "after each hour"). Default 1 hour.
7130 *
7131 * A server-initiated re-key fires when either this much time or SSH_REKEY_PACKET_THRESHOLD packets have
7132 * passed since the last KEX, whichever comes first. Set to 0 to disable the time trigger (packet-count
7133 * only). Measured with the pluggable clock (pc_millis()).
7134 */
7135#ifndef SSH_REKEY_TIME_MS
7136#define SSH_REKEY_TIME_MS 3600000u
7137#endif
7138/** @brief Max stored user name (RFC 4252 imposes no limit; we cap for BSS). */
7139#ifndef SSH_AUTH_USER_MAX
7140#define SSH_AUTH_USER_MAX 32
7141#endif
7142/** @brief Max stored password length. */
7143#ifndef SSH_AUTH_PASS_MAX
7144#define SSH_AUTH_PASS_MAX 64
7145#endif
7146/**
7147 * @brief Max stored size of the CLIENT KEXINIT payload (I_C, for the exchange hash).
7148 *
7149 * A modern OpenSSH client's KEXINIT (post-quantum KEX names + cert host-key algs + EtM
7150 * MACs + ext-info-c) runs well past 1 KB, so this must be large enough to hold it - a
7151 * smaller bound silently rejects real clients at key exchange. The packet layer already
7152 * caps any single packet at SSH_PKT_BUF_SIZE.
7153 */
7154#ifndef SSH_KEXINIT_MAX
7155#define SSH_KEXINIT_MAX 2048
7156#endif
7157
7158#if PC_ENABLE_AUDIT_LOG
7159// -- Audit log (services/security/audit_log) --
7160#ifndef PC_AUDIT_LOG_ENTRIES
7161#define PC_AUDIT_LOG_ENTRIES 32 ///< RAM ring depth (records retained for query/verify).
7162#endif
7163#ifndef PC_AUDIT_MSG_LEN
7164#define PC_AUDIT_MSG_LEN 48 ///< Max message bytes per record (truncated to fit).
7165#endif
7166#endif // PC_ENABLE_AUDIT_LOG
7167
7168#if PC_ENABLE_DEVICENET
7169// -- DeviceNet (services/fieldbus/devicenet) --
7170#ifndef PC_DEVICENET_MSG_MAX
7171#define PC_DEVICENET_MSG_MAX 256 ///< max reassembled fragmented message
7172#endif
7173#endif // PC_ENABLE_DEVICENET
7174
7175#if PC_ENABLE_ESPNOW
7176// -- ESP-NOW (services/radio/espnow) --
7177#ifndef PC_ESPNOW_MAX_PEERS
7178#define PC_ESPNOW_MAX_PEERS 8 ///< Bounded peer registry size.
7179#endif
7180#endif // PC_ENABLE_ESPNOW
7181
7182#if PC_ENABLE_GRAPHQL
7183// -- GraphQL (services/iot/graphql) --
7184#ifndef PC_GQL_MAX_NODES
7185#define PC_GQL_MAX_NODES 48 ///< Max fields across the whole query.
7186#endif
7187#ifndef PC_GQL_MAX_ARGS
7188#define PC_GQL_MAX_ARGS 24 ///< Max arguments across the whole query.
7189#endif
7190#ifndef PC_GQL_MAX_DEPTH
7191#define PC_GQL_MAX_DEPTH 6 ///< Max selection-set nesting depth.
7192#endif
7193#ifndef PC_GQL_NAME_MAX
7194#define PC_GQL_NAME_MAX 32 ///< Max field / argument name length.
7195#endif
7196#ifndef PC_GQL_PATH_MAX
7197#define PC_GQL_PATH_MAX 96 ///< Max dotted path length passed to the resolver.
7198#endif
7199#ifndef PC_GQL_STRBUF
7200#define PC_GQL_STRBUF 256 ///< Pool for decoded string-argument bytes.
7201#endif
7202#endif // PC_ENABLE_GRAPHQL
7203
7204#if PC_NEED_J1939
7205// -- J1939 (services/j1939; also built when NMEA 2000 is enabled) --
7206#ifndef PC_J1939_TP_MAX
7207#define PC_J1939_TP_MAX 256 ///< max reassembled TP message (spec allows up to 1785); sized down for RAM
7208#endif
7209#endif // PC_NEED_J1939
7210
7211#if PC_NEED_NMEA0183
7212// -- NMEA 0183 (services/timing_position/nmea0183) --
7213#ifndef PC_NMEA0183_MAX_FIELDS
7214#define PC_NMEA0183_MAX_FIELDS 26 ///< max comma-separated fields (incl. the address field)
7215#endif
7216#endif // PC_NEED_NMEA0183
7217
7218#if PC_ENABLE_UBX
7219// -- UBX (services/timing_position/ubx) --
7220#ifndef PC_UBX_MAX_PAYLOAD
7221#define PC_UBX_MAX_PAYLOAD 256 ///< max UBX payload the stream demux buffers (NAV-PVT is 92; longer frames are skipped)
7222#endif
7223#endif // PC_ENABLE_UBX
7224
7225#if PC_ENABLE_NMEA2000
7226// -- NMEA 2000 (services/timing_position/nmea2000) --
7227#ifndef PC_N2K_FP_MAX
7228#define PC_N2K_FP_MAX 223 ///< Fast Packet max payload (6 in frame 0 + 31 x 7)
7229#endif
7230#endif // PC_ENABLE_NMEA2000
7231
7232#if PC_ENABLE_OAUTH2
7233// -- OAuth2 (services/security/oauth2) --
7234#ifndef PC_OAUTH2_TOKEN_LEN
7235#define PC_OAUTH2_TOKEN_LEN 768 ///< access_token / id_token buffer (JWTs are large).
7236#endif
7237#ifndef PC_OAUTH2_RT_LEN
7238#define PC_OAUTH2_RT_LEN 256 ///< refresh_token buffer.
7239#endif
7240#ifndef PC_OAUTH2_BODY_BUF
7241#define PC_OAUTH2_BODY_BUF 1024 ///< token-request body buffer.
7242#endif
7243#ifndef PC_OAUTH2_RESP_BUF
7244#define PC_OAUTH2_RESP_BUF 2048 ///< token-endpoint response buffer.
7245#endif
7246#endif // PC_ENABLE_OAUTH2
7247
7248#if PC_ENABLE_OIDC
7249// -- OIDC (services/security/oidc) --
7250// PC_OIDC_MAX_LEN is declared unconditionally with PC_ENABLE_OIDC above, because PC_AUTH_HDR_CAP
7251// sizes the Authorization buffer from it and that runs before this block.
7252#ifndef PC_OIDC_SUB_LEN
7253#define PC_OIDC_SUB_LEN 64 ///< Captured `sub` claim buffer.
7254#endif
7255#ifndef PC_OIDC_EMAIL_LEN
7256#define PC_OIDC_EMAIL_LEN 96 ///< Captured `email` claim buffer.
7257#endif
7258#ifndef PC_OIDC_KID_LEN
7259#define PC_OIDC_KID_LEN 80 ///< Max `kid` length.
7260#endif
7261#ifndef PC_OIDC_JWKS_MAX
7262#define PC_OIDC_JWKS_MAX 16384 ///< Max JWKS document scanned; exceeds any real multi-key set, bounds the parse.
7263#endif
7264#endif // PC_ENABLE_OIDC
7265
7266#if PC_ENABLE_OPCUA
7267// -- OPC UA (services/fieldbus/opcua) --
7268#ifndef PC_OPCUA_BUF
7269#define PC_OPCUA_BUF 8192 ///< Server's advertised buffer / max-message size for the handshake.
7270#endif
7271#ifndef PC_OPCUA_READ_MAX
7272#define PC_OPCUA_READ_MAX 8 ///< max NodesToRead handled per ReadRequest.
7273#endif
7274#ifndef PC_OPCUA_BROWSE_MAX
7275#define PC_OPCUA_BROWSE_MAX 4 ///< max NodesToBrowse handled per BrowseRequest.
7276#endif
7277#ifndef PC_OPCUA_REF_MAX
7278#define PC_OPCUA_REF_MAX 8 ///< max references returned per browsed node.
7279#endif
7280#ifndef PC_OPCUA_WRITE_MAX
7281#define PC_OPCUA_WRITE_MAX 8 ///< max NodesToWrite handled per WriteRequest.
7282#endif
7283
7284// An Axes Browse returns one reference per axis, so the axis count must fit a single Browse response.
7285#if PC_ENABLE_ROBOTICS && (PC_ROBOTICS_AXES > PC_OPCUA_REF_MAX)
7286#error "ProtoCore: PC_ROBOTICS_AXES must be <= PC_OPCUA_REF_MAX (an Axes Browse returns one reference per axis)"
7287#endif
7288// Advertised server identity (endpoint descriptions), overridable per deployment; the app may
7289// also set these at runtime via pc_opcua_set_endpoint_url() / the OpcUaServerInfo it passes.
7290#ifndef PC_OPCUA_DEFAULT_ENDPOINT
7291#define PC_OPCUA_DEFAULT_ENDPOINT "opc.tcp://localhost:4840" ///< default endpoint URL.
7292#endif
7293#ifndef PC_OPCUA_DEFAULT_APP_URI
7294#define PC_OPCUA_DEFAULT_APP_URI "urn:det:opcua:server" ///< default ApplicationUri.
7295#endif
7296#ifndef PC_OPCUA_DEFAULT_APP_NAME
7297#define PC_OPCUA_DEFAULT_APP_NAME "pc_opcua_server" ///< default ApplicationName.
7298#endif
7299#endif // PC_ENABLE_OPCUA
7300
7301#if PC_ENABLE_PROVISIONING
7302// -- Wi-Fi provisioning credential store (services/system/provisioning_service) --
7303// The NVS namespace and its keys, overridable per deployment (e.g. to avoid an NVS-namespace
7304// collision with the application's own store).
7305#ifndef PC_PROV_NVS_NAMESPACE
7306#define PC_PROV_NVS_NAMESPACE "wifi_prov" ///< NVS namespace holding the saved credentials.
7307#endif
7308#ifndef PC_PROV_KEY_SSID
7309#define PC_PROV_KEY_SSID "ssid" ///< NVS key + HTML form field for the SSID.
7310#endif
7311#ifndef PC_PROV_KEY_PSK
7312#define PC_PROV_KEY_PSK "psk" ///< NVS key + HTML form field for the pre-shared key.
7313#endif
7314#endif // PC_ENABLE_PROVISIONING
7315
7316#if PC_ENABLE_VFS
7317// -- Virtual filesystem (services/storage/vfs) --
7318#ifndef PC_VFS_RAM_FILES
7319#define PC_VFS_RAM_FILES 4 ///< RAM backend: number of files.
7320#endif
7321#ifndef PC_VFS_RAM_FILE_SIZE
7322#define PC_VFS_RAM_FILE_SIZE 1024 ///< RAM backend: max bytes per file.
7323#endif
7324#ifndef PC_VFS_MAX_OPEN
7325#define PC_VFS_MAX_OPEN 4 ///< Concurrent open handles (per backend).
7326#endif
7327#ifndef PC_VFS_NAME_MAX
7328#define PC_VFS_NAME_MAX 48 ///< Max path length (RAM backend).
7329#endif
7330#endif // PC_ENABLE_VFS
7331
7332// Final sizing pass: raise buffers to the floors the enabled features require (every PC_ENABLE_*
7333// flag is resolved by this point). Kept in the board-profile layer, not inline above.
7334#include "board_drivers/board_profiles/derived_sizing.h" // PC_ALLOW_LATE_INCLUDE: ordered - derives sizes from the PC_ENABLE_* flags resolved above
7335
7336#endif
Per-variant default sizing: pick sane PC_* defaults for the target board.
Feature-driven sizing resolution - the per-variant sizing layer's final pass.
ConnProto
Application protocol spoken on a listener port or connection slot.
@ PROTO_NONE
Unassigned slot.
@ PROTO_HTTP
HTTP/1.1 with optional WS and SSE upgrades.
@ PROTO_OPCUA
OPC UA Binary (UA-TCP) server.
@ PROTO_TELNET
Telnet (RFC 854).
@ PROTO_MESH
Edge-cache sibling link (PC_ENABLE_EDGE_MESH): answers a peer's content-addressed query.
@ PROTO_MODBUS
Modbus TCP slave (Modbus Application Protocol).
@ PROTO_BRIDGE
address:port -> hardware bus (PC_ENABLE_IFACE_BRIDGE): UART/SPI/I2C device server.
@ PROTO_SSH_RFWD
SSH remote-forward listener (ssh -R): accepts bridge to a forwarded-tcpip channel.
@ PROTO_RELAY
TCP relay / DNAT (PC_ENABLE_RELAY): bridge to an origin pc_client connection.
@ PROTO_SSH
SSH (RFC 4253/4252/4254).
@ PROTO_NTRIP_CASTER
NTRIP caster (PC_ENABLE_NTRIP_CASTER): serves RTCM3 corrections to rovers.
#define PC_PROTO_MAX
Size of the protocol-handler dispatch table; must exceed the largest ConnProto id.
pc_iface
Network interface a connection arrived on (for per-route filtering).
@ PC_IFACE_AP
softAP interface (clients joined to the device).
@ PC_IFACE_ETH
Ethernet interface (wired PHY).
@ PC_IFACE_STA
Station interface (joined to an AP / your LAN).
@ PC_IFACE_ANY
Unknown / no filter (matches any interface).
Runtime-tunable server parameters.