ProtoCore / features
254 / 254

Foundation17

Config IOPC_ENABLE_CONFIG_IO
Opt-in schema-driven config export / restore. Default off. Requires CONFIG_STORE. The app declares a fixed schema (key + type); services/storage/config_io serializes the current values to a portable `key=value` text blob (backup / migrate) and parses one back into the store (restore / bulk template). Schema-driven rather than enumerating NVS, so it stays deterministic and zero-heap; the serialize / parse is host-tested.
Config StorePC_ENABLE_CONFIG_STORE
Typed NVS configuration store (WiFi creds, IP config,... as blobs). When set, src/services/storage/config_store/config_store.h provides a typed key/value API (string / u32 / blob) that routes core settings into the ESP32's native NVS partition (via `Preferences`) instead of a JSON file on the filesystem - which survives FS corruption and is the corruption-resistant home for credentials. On host builds it is backed by a fixed in-memory table so the typed contract is unit-testable. Default off.
Device IDPC_ENABLE_DEVICE_ID
Stable device UUID derived from the chip MAC (RFC 4122 v5). When set, src/server/signaling/device_id.h derives a deterministic v5 UUID from a MAC (via the library's SHA-1) - a storage-free, stable identity for mDNS hostnames, MQTT client IDs, etc. The MAC->UUID core is host-testable; pc_device_uuid() reads the ESP32 factory MAC. Default off.
DMA Peripheral IngestPC_ENABLE_DMA
Opt-in DMA ingest / egress path (v5 milestone). Default off. mmgr/dma moves peripheral bytes (UART / I2C / SPI) between the wire and a static buffer while the CPU is free, then a DMA-complete event carries the result up - the high-throughput field-bus ingest path (RS-485 UART, CAN over SPI, IO-Link). RX is double-buffered (ping-pong): the completed buffer is handed to a callback while the engine fills the other; the canonical callback posts the bytes into the preempting work queue so a high-priority task processes them off the interrupt (see Preempting Work Queue). Zero heap (static PC_DMA_CHANNELS channels x PC_DMA_BUF_SIZE buffers), fail-closed (one TX in flight per channel, feeds past the staging capacity are rejected). PC_DMA_SIMULATE (default on) routes transfers through an in-memory ingress/egress simulator - feed bytes in, capture bytes out, optional TX->RX loopback - so the whole pipeline runs with no physical loopback wire, on the host bench and on-device; a real silicon driver plugs into the pc_dma_hw_* hooks when the flag is off. Host-tested (mmgr/dma) and HW-verified on a DevKitV1: 2.2M+ frames ingested with zero integrity errors while an HTTP server was stress-loaded on the same core, no heap growth. See src/mmgr/dma.h.
Exception DecoderPC_ENABLE_EXC_DECODER
Opt-in ESP32 panic / exception decoder for a live diagnostics panel, plus core-dump recovery. Default off. server/exc_decoder parses a captured Guru Meditation panic dump (the cause, the register PC + EXCVADDR, and the backtrace PC:SP frames) into a structured ExcInfo and serializes it as JSON for a "/exception" panel; the browser or a build server resolves the PCs to file:line against the firmware ELF (addr2line lives off-device). That decoder is pure, no heap/stdlib. **Core-dump partition recovery** closes the gap that a panic reboots the device and takes the console with it: ESP-IDF also writes a core dump to flash, which survives, so the next boot recovers it - `pc_exc_coredump_present` reports a stored image (checksum-verified) with its size/address, `pc_exc_coredump_summary` fills the _same_ ExcInfo so a crash recovered from flash renders through the same panel as a live capture, `pc_exc_coredump_save(fs, path)` streams the image out of flash to SD/LittleFS in `PC_EXC_COREDUMP_CHUNK` pieces (no heap, any dump size), and `pc_exc_coredump_erase` clears the partition once the copy is safe. Offload is **transport-agnostic**: `pc_exc_coredump_read(offset, buf, len)` is the seam every consumer pulls through (a range past the image end is refused rather than returning trailing flash), so the same image goes to a filesystem, up an FTP data connection (`PC_ENABLE_FTP_SESSION`), or into an HTTP body without this owner knowing about any of them - which matters because a dump that only ever reaches the device's own SD card is still lost with the device. The summary is **architecture-honest**: Xtensa's windowed ABI yields a real on-device backtrace, while RISC-V (C3/C6/H2/P4) carries only a stack dump - so there `frame_count` is 0 and the trap cause/value are reported rather than inventing frames, which is exactly why the raw offload matters on those parts. The saved file is the raw IDF flash image (24-byte header, then an ET_CORE ELF at offset 24, then a checksum), consumed as `esp-coredump ... -t raw`. **HW-verified on an ESP32-P4** saving to SD: a deliberate null dereference was recovered after reboot as `{"cause":"pc_worker","pc":"0x4000004e","excvaddr":"0x00000000","backtrace":[]}` - the fault address exactly the null pointer - a 12100-byte image was written to the card, and a following clean boot returned `{}`, proving the erase. **Also HW-verified on an ESP32-S3** (the Xtensa arm, and the FTP transport): the recovered summary carried a real 10-frame backtrace whose PCs `addr2line` resolves to the true crash path (`crash_handler` -> `dispatch_matched_route` -> `match_and_execute` -> `http_poll_slot`), the 21540-byte image was streamed off the device to a live FTP server, and Espressif's own `esp-coredump info_corefile -t raw` then parsed that uploaded file and reported `exccause 0x1d (StoreProhibitedCause)`, `excvaddr 0x0`, `pc <crash_handler(uint8_t, HttpReq*)+33>` and crashed task `'pc_worker'` - independently confirming both the image format and the task name this decoder reports. Example CoreDump. See src/server/exc_decoder.h.
Failsafe WatchdogPC_ENABLE_FAILSAFE
Opt-in software watchdog: deadlock detection + fail-safe safe-state. When set, server/failsafe provides a fixed registry of "lifelines" (a task / worker / control loop that must check in within its deadline). pc_failsafe_check() detects one that stopped feeding (a hang / deadlock) and fires a breach callback once per episode so the app can enter a known-safe state. App-defined and per-lifeline, on top of the hardware task watchdog. Pure core, zero heap. Default off.
GPIO MapPC_ENABLE_GPIO_MAP
Opt-in browser GPIO pin-mapper / diagnostics endpoint. Default off. When set, server/signaling/gpio_map serves a compile-time table of GPIO pins (number, label, direction, live level) as JSON for a browser diag panel, and accepts a control POST (`pin`, `level`) to drive an output. The live read / write uses the Arduino digital API on ESP32; the JSON serializer and the control parser are pure and host-testable.
GuardrailsPC_ENABLE_GUARDRAILS
Opt-in runtime heap/stack guardrails. Default off. When set, services/security/guardrails samples free heap, the heap low-water mark, the largest free block (fragmentation), and the calling task's remaining stack, and fires a callback when any crosses its threshold - a proactive fail-safe hook beyond the passive numbers in /metrics. The threshold evaluator and the JSON serializer are pure and host-tested; the sample reads esp_* / the FreeRTOS stack high-water on ESP32.
Hardware HealthPC_ENABLE_HW_HEALTH
Opt-in hardware-health diagnostics. Four pure decision cores fed with samples the app reads from the hardware: a power-rail voltage-drop logger (pc_hwhealth_rail_sample tracks worst droop + sag/brownout counts), a SPI-bus CRC audit with hysteretic clock backoff (pc_hwhealth_spi_result halves/doubles the clock on fail/ok streaks), a GPIO short-circuit test (pc_hwhealth_gpio_short: driven vs readback), and a capacitor-leakage diag (pc_hwhealth_cap_leak: measured vs expected RC decay). No heap/stdlib. Default off.
MNTPC_ENABLE_MNT
Mounted storage. Default off. server/filesystem/mnt says _what is mounted_: a pluggable backend vtable (open/read/write/close/seek, exists/size/remove/rename, mkdir/rmdir/stat, opendir/readdir) that a feature reaches through the filesystem accessor (server/filesystem/filesystem.h), so it can target storage without knowing the medium. The accessor owns the mount root and the resolved path and rejects `..`, so a protocol server never builds a path of its own. A built-in zero-heap RAM backend (fixed BSS pool - deterministic, host-identical) ships for scratch / tests, which is what lets the file-transfer servers run under a native test; an Arduino-FS backend (board layer, board_drivers/hal/esp) wraps a real fs::FS (LittleFS / SD / SPIFFS) for persistence. Mount one at startup; the API fails closed otherwise. Pool dimensions are tunable in protocore_config.h (PC_MNT_RAM_FILES, _RAM_FILE_SIZE, _MAX_OPEN, _NAME_MAX).
Preempting Work QueuePC_ENABLE_PREEMPT_QUEUE
Opt-in real-time ingest primitive (v5 milestone). Default off. Fixed-capacity queues, each feeding one dedicated core-pinned task: a producer posts a fixed-size item from a task (back or urgent-front, each with a wait timeout) or from an ISR (interrupt-safe via xQueueSendFromISR + portYIELD_FROM_ISR), and the scheduler preempts straight to the processing task so the work runs immediately instead of on the next tick - the clean ISR-to-"process now" hand-off for DMA-complete / GPIO / bus events. There are named lanes: one USER lane exposed to the app (the no-arg pc_pq_* API drives it) plus internal DMA / FORWARD / DEVICE lanes for the library's own real-time work. The internal lanes run above the user lane (base PC_PQ_INTERNAL_PRIORITY, DMA highest) and below the lwIP tcpip / WiFi tasks, so internal ingest always preempts user work without starving networking; pc_pq_lane_priority() reports the ordering. Zero heap (static FreeRTOS queue storage per lane, compile-time PC_PQ_DEPTH / PC_PQ_ITEM_SIZE / PC_PQ_STACK; a lane's task stack is created only when it starts, so unused lanes cost only their queue storage), fail-closed on a full queue, no hot-path locks so latency stays bounded. pc_pq_high_water_lane() reports the peak depth for sizing. HW-verified on a DevKitV1 (~12 us ISR-to-handler latency; the DMA and USER lanes ran continuously with zero errors under an HTTP flood); host-tested via the per-lane ring core (services/system/preempt_queue). See src/services/system/preempt_queue/preempt_queue.h.
PSRAM PoolPC_ENABLE_PSRAM_POOL
Opt-in buffer placement policy (DRAM vs PSRAM) + SPI DMA ping-pong manager. Pure buffer-management decisions for a PSRAM-equipped ESP32: pc_psram_place picks DRAM vs PSRAM for a buffer by size, DMA requirement, and free-heap headroom (large/cold to PSRAM, small/hot + DMA to DRAM, always leaving an internal-DRAM reserve), and pc_pingpong_* keeps the classic SPI DMA double-buffer bookkeeping (CPU fills one buffer while DMA drains the other; swap flips their roles). The actual heap_caps_calloc is the app's. No heap/stdlib. Default off.
RTCPC_ENABLE_RTC
I2C real-time-clock driver (DS1307 / DS3231). Default off. services/peripherals/rtc reads and sets a battery-backed RTC over I2C (Wire, address 0x68), so the device knows the correct wall-clock time the instant it boots - offline, across power loss - with no network. It is the ideal middle of a time-source chain (GPS -> RTC -> upstream NTP), feeding `pc_time_now()` and the NTP server; `pc_rtc_time_source()` plugs straight into pc_time_source_add(). The BCD <-> Unix-epoch conversion of the seven time registers (12/24-hour, leap years, the clock-halt / century bit masks, range validation) is pure and host-tested across the chip's 2000-2099 range (that round-trip test caught a real 32-bit overflow past 2038); only the register read/write touches I2C. Zero heap. Example Rtc. See src/services/peripherals/rtc/rtc.h.
Sleep SchedulerPC_ENABLE_SLEEP_SCHED
Opt-in dynamic sleep-cycle scheduler. When set, server/sleep_sched provides pc_sleep_next(): from the time since the last activity it returns how long a low-power device should sleep (0 = stay awake), ramping the window from a floor up to a ceiling the longer the idle streak runs. Pure decision core (the app applies the window via light / modem / deep sleep). Complements services/radio_power. Default off.
SouthboundPC_ENABLE_SOUTHBOUND
Opt-in southbound protocol-driver framework. The uniform seam every field-device driver plugs into so the app polls/drives any southbound device (a Modbus slave, a BACnet controller, a raw sensor over SPI/I2C/UART) through one facade: register a SouthboundDriver (a read/write/read_block/write_block vtable + its transport ctx), then address points by driver name via pc_southbound_read / _write / _read_block / _write_block. The block calls are the atomic multi-point (register-matrix) path. Bounded registry (PC_SOUTHBOUND_MAX_DRIVERS, default 8), no heap; Modbus master is the one such driver today. Default off.
Time SourcePC_ENABLE_TIME_SOURCE
Multi-source time fallback (NTP / RTC / GPS /... by priority). When set, src/services/timing_position/time_source/time_source.h provides a small registry of user-defined time sources, each a callback returning Unix epoch seconds (0 when that source has no valid time). pc_time_now() queries them in priority order (lowest value first) and returns the first valid result, so the device falls back automatically when its preferred clock is unavailable. Pure and zero-heap (a fixed source table); host-testable. Default off.
Wear LevelingPC_ENABLE_WEARLEVEL
Opt-in flash wear-leveling slot selector. When set, server/filesystem/wearlevel provides pc_wearlevel_pick(): given per-slot write counts it returns the least-worn slot to write next, so repeated flash/NVS writes spread evenly and the region ages together instead of burning out one block. Pure core (the app owns the slots + persisted counts). Default off.

Network (L3)6

Dns ResolverPC_ENABLE_DNS_RESOLVER
Opt-in DNS resolver with answer verification. Default off. network_drivers/network/dns_resolver resolves a hostname to an IPv4 address (lwIP dns_gethostbyname, marshalled to tcpip_thread like the http_client) and can reject suspicious answers - 0.0.0.0, broadcast, loopback, multicast - which are spoofing / DNS-rebinding indicators for a remote host. The address classifier / verifier is pure and host-tested; the resolve is ESP32-only (blocking, so call it off the request hot path).
Happy EyeballsPC_ENABLE_HAPPY_EYEBALLS
Opt-in dual-stack Happy Eyeballs destination selection. The client-side IPv6/IPv4 fallback decision on top of the shipped pc_ip: pc_he_pref scores a destination (RFC 6724 scope + family), pc_he_order sorts a candidate list and interleaves the address families (RFC 8305) so successive connection attempts alternate v6/v4, and pc_he_attempt_due gates the next attempt by the Connection Attempt Delay. Fast IPv6 when it works, quick fallback to IPv4 when it does not. Needs PC_ENABLE_IPV6 to matter. No heap/stdlib. Default off.
IPv6PC_ENABLE_IPV6
Dual-stack IPv6. Default off. The TCP and UDP listeners already bind `IPADDR_TYPE_ANY`, so the server answers over IPv6 the moment the interface has a v6 address; `PC_ENABLE_IPV6` turns IPv6 on for the Wi-Fi netif (`init_ipv6_physical()` -> SLAAC: a `fe80::` link-local address plus a global one if a router advertises a prefix), and `net_global_ipv6()` reads the acquired global address from lwIP. The pc_ip address core (`network_drivers/network/ip.h`) is one family-tagged type for both v4 and v6, with RFC 4291 text parsing (`::` zero-compression, embedded-v4 `::ffff:a.b.c.d`), RFC 5952 canonical formatting, and scope classification (loopback / link-local / private-ULA / multicast / global). The address core is pure and host-tested (`native_ip`); the netif bring-up is ESP32-only. Example IPv6. Requires an lwIP built with `LWIP_IPV6=1` (the stock Arduino-ESP32 core ships it). HW-verified (2026-07-19) on an ESP32-S3: SLAAC formed a link-local + ULA + a router-advertised global address, and the dual-stack `:80` listener answered real HTTP `GET`s over IPv6 from an on-link host.
Link ManagerPC_ENABLE_LINK_MANAGER
Opt-in multi-interface egress selection / failover policy. The policy that drives which interface carries traffic once a device has more than one (a wired Ethernet PHY alongside WiFi STA / softAP): server/signaling/link_manager keeps a small table of interfaces (kind + priority + up/down) and deterministically selects the best link that is up, escalating to a higher-priority interface when it comes up and failing over when it drops, reporting only real transitions so the app reconfigures the netif once. The PHY bring-up (esp_eth) stays the app's. No heap/stdlib. Default off.
Network AdaptationPC_ENABLE_NETADAPT
Opt-in network adaptation decisions. When set, services/net/netadapt provides two pure decisions: pc_netadapt_window() sizes the TCP receive window from the free heap (bigger when RAM is plentiful, shrinking when tight), and pc_netadapt_dhcp_fallback() decides when to give up on DHCP and use a static IP. The app applies the results (lwIP window / netif config). Default off.
Proxy ProtocolPC_ENABLE_PROXY_PROTOCOL
HAProxy PROXY protocol codec - recover the real client IPv4 when the server sits behind a load balancer / reverse proxy that prepends a PROXY header. Default off. services/net/proxy_protocol provides `proxy_parse` (detects + decodes a v1 text `PROXY TCP4 ...\r\n` header or a v2 binary header - 12-octet signature + ver_cmd / family / address block - and reports the bytes to skip before the real stream) plus `proxy_v1_build` / `proxy_v2_build` for a TCP/IPv4 header. Handles the library's IPv4 family; IPv6 / UNIX / LOCAL headers parse to their length but yield no addresses. Format per the HAProxy PROXY protocol spec; pure and host-tested. See src/services/net/proxy_protocol/proxy_protocol.h.

Transport (L4)9

Accept ThrottlePC_ENABLE_ACCEPT_THROTTLE
Opt-in global accept-rate throttle (connection-flood defense). Default off (zero cost / no behavior change). When set to 1 the accept callback rejects new connections once more than PC_ACCEPT_THROTTLE_MAX have been accepted within a PC_ACCEPT_THROTTLE_WINDOW_MS fixed window (global across all listeners, two static counters - no per-IP table). This bounds connection churn (e.g. reconnect brute-force) on top of the bounded connection pool and the per-connection auth limits. mitigate finer-grained / per-IP attacks at the network layer.
IP AllowlistPC_ENABLE_IP_ALLOWLIST
Opt-in source-IP allowlist (accept-time firewall, keyed by source IPv4). Default off (zero cost / no behavior change). When set, the accept callback drops any connection whose source address does not match a configured CIDR rule (see listener_ip_allow_add()). An empty allowlist allows everything, so enabling the feature before adding rules never locks the device out. Rules live in a fixed BSS table of PC_IP_ALLOWLIST_SLOTS entries (no heap). This is a coarse first-line filter - a spoofed source address can still pass it - so combine it with the accept throttles and network-layer filtering.
Keep-AlivePC_ENABLE_KEEPALIVE
HTTP/1.1 persistent connections (keep-alive). Default on: a cleanly-parsed request is answered with `Connection: keep-alive` and the slot is recycled for the next request on the same socket: HTTP/1.1 keeps the connection open unless the client sends `Connection: close`; HTTP/1.0 closes unless the client sends `Connection: keep-alive`. Set `PC_ENABLE_KEEPALIVE=0` for the legacy behavior (every response carries `Connection: close` and the connection is closed after one request). The default connection pool is `MAX_CONNS=8` to give a persistent-connection workload headroom above its peak concurrency. Error responses (400/413/414 and any non-PARSE_COMPLETE path) always close, since the next request boundary is unknown. Idle keep-alive connections are still reclaimed by the existing conn_timeout sweep, and each connection serves at most PC_KEEPALIVE_MAX_REQUESTS requests before a deliberate close.
MTLSPC_ENABLE_MTLS
Mutual TLS - require and verify a client certificate (mTLS). Default off. When set (requires TLS), the server can be given a trust-anchor CA via PC::tls_require_client_cert(): the TLS handshake then demands a client certificate chaining to that CA (MBEDTLS_SSL_VERIFY_REQUIRED) and aborts the connection if the client presents none or an untrusted one. The verified peer's subject DN is available to handlers via PC::tls_client_subject(). Strong transport-level client authentication with no passwords.
Per IP ThrottlePC_ENABLE_PER_IP_THROTTLE
Opt-in per-IP accept-rate throttle (connection-flood defense, keyed by source IPv4). Default off (zero cost / no behavior change). Complements the global accept throttle: the accept callback rejects a new connection once one source IPv4 address has opened more than PC_PER_IP_THROTTLE_MAX connections within a PC_PER_IP_THROTTLE_WINDOW_MS fixed window. A fixed BSS table of PC_PER_IP_THROTTLE_SLOTS buckets tracks the most-recently-seen source addresses; when a new address arrives and the table is full, an expired or least-recently-started bucket is reused, so memory stays bounded (no heap). This bounds reconnect/brute-force churn from a single host (the gap left by the global throttle, which cannot tell one noisy client from many). It is best-effort: an attacker spreading across many source addresses can still churn the bounded connection pool, so combine it with the global throttle and network-layer filtering.
Socket PoolPC_ENABLE_SOCKPOOL
Opt-in dynamic socket recycling: an LRU connection-slot pool. The transport-pool half of the adaptive-networking work: services/net/sockpool keeps a fixed table of connection slots and, when saturated, recycles the least-recently-used slot for a new peer (pc_sockpool_acquire returns the evicted id so the transport closes it), plus touch / release / find. The app owns the real sockets; this owns which slot a connection lives in and which to reclaim under pressure. No heap/stdlib. Default off.
TLSPC_ENABLE_TLS
TLS (HTTPS/WSS) via mbedTLS with a static memory pool (ESP32-only). When set, the server can accept TLS connections using mbedTLS configured with MBEDTLS_MEMORY_BUFFER_ALLOC_C over a fixed BSS arena (PC_TLS_ARENA_SIZE) - no system heap, so the determinism guarantee is preserved. The TLS engine is compiled only on Arduino/ESP32 (mbedTLS is not part of the native build). Default off.
TLS PolicyPC_ENABLE_TLS_POLICY
Opt-in TLS version negotiation + pinned cipher-suite policy. A policy layer on top of the mbedTLS-backed transport TLS (which already runs the 1.2 / 1.3 record + handshake): services/security/tls_policy pins the version to an audited [min,max] and makes the negotiated version observable (pc_tls_negotiate_version / pc_tls_version_name), and pins the cipher suites to an audited allowlist selected by server preference (pc_tls_select_cipher), with an AEAD-only classifier (pc_tls_is_aead) for a hardened profile. Pure, host-tested; the app feeds the results to the mbedTLS config. Default off.
TLS ResumptionPC_ENABLE_TLS_RESUMPTION
TLS session resumption via RFC 5077 session tickets (requires TLS). Default off. When set, the TLS 1.2 server issues encrypted session tickets and accepts them on reconnect, so a returning client completes an abbreviated handshake (no certificate or full key exchange) - much faster and far less CPU than the ~RSA/ECDHE full handshake. Resumption is stateless: the session state lives in the client's ticket, sealed with a server-held key, so there is no growing per-session cache (the determinism / zero-heap-growth guarantee holds; only a small fixed ticket key and a little arena headroom are added). The ticket key rotates automatically on the PC_TLS_TICKET_LIFETIME_S schedule. Needs the mbedTLS build to provide MBEDTLS_SSL_TICKET_C (stock arduino-esp32 does).

Session (L5)5

SSHPC_ENABLE_SSH
SSH server support (RFC 4253/4252/4254). Channels are multiplexed per connection (`PC_SSH_MAX_CHANNELS`, default 1), each routed by its recipient id with its own flow-control window. Beyond `session` channels, `direct-tcpip` (the `ssh -L` local-forward request) is parsed and routed through a normalized forwarding seam: the codec extracts the target host:port and routes channel data but does no TCP I/O itself. With `PC_SSH_PORT_FORWARD` set, the `ssh_forward` owner plugs into that seam and does the I/O - it opens the outbound TCP through the client transport (pc_client) and bridges bytes both ways, with an optional target policy callback, a per-poll target-to-client pump bounded by the channel window, and EOF/CLOSE propagation. Forwarding is opt-in twice over (compiled out by default, and inert until the app calls `pc_ssh_forward_begin()`) because any authenticated client could otherwise reach arbitrary hosts; with it off every `direct-tcpip` open is refused (no open relay).
SSH CompressionPC_ENABLE_SSH_ZLIB
SSH `zlib@openssh.com` / `zlib` compression (RFC 4253 sec 6.2), **both directions**. When set (and SSH is on) the server advertises `zlib@openssh.com` (delayed, OpenSSH's default) and `zlib` for the client-to-server and server-to-client lists alike, negotiating each independently. Server->client compresses every outbound payload through a single context-takeover DEFLATE stream (network_drivers/presentation/ssh/transport/ssh_zlib) - a persistent sliding window carried across packets, sync-flushed per packet, wrapped as a zlib stream - which a standard `inflate()` (OpenSSH) decodes. Client->server decompresses every inbound payload through the matching resumable inflate (network_drivers/presentation/ssh/transport/ssh_inflate): OpenSSH compresses its outbound with `Z_PARTIAL_FLUSH`, whose DEFLATE blocks end mid-byte and spill into the next packet, so the decoder carries the bit position + a 32 KB context-takeover window across packets and commits only whole blocks (it retains the un-decoded flush-block tail and re-decodes it next packet, so no mid-block Huffman/symbol state persists). `zlib@openssh.com` starts each direction after `SSH_MSG_USERAUTH_SUCCESS`; plain `zlib` at NEWKEYS. Both directions are one owner (network_drivers/presentation/ssh/transport/ssh_comp) the packet layer asks per packet - compress on send, decompress on receive - and inbound decompression is capped at the uncompressed payload limit so a decompression bomb fails closed. PSRAM-class (~80 KB/connection incl. the 32 KB inflate window): on ESP32 set `PC_SSH_ZLIB_IN_PSRAM=1` on a PSRAM board or `PC_SSH_ZLIB_ACK_DRAM=1` to accept the internal-DRAM cost. Default off. The s2c stream is HW-verified against OpenSSH 10.3 (`ssh -o Compression=yes`); the c2s inflate is verified byte-exact against golden vectors produced by real zlib (windowBits 15, `Z_PARTIAL_FLUSH` per packet; tools/gen_ssh_inflate_vectors.py), with HW interop against OpenSSH pending.
SSH SCPPC_ENABLE_SSH_SCP
SCP server over SSH - the legacy rcp protocol via `exec "scp -t <path>"` (requires SSH + Mnt). Default off. `scp localfile admin@device:/path` drops a file onto the mounted filesystem (`pc_mnt_mount` + `pc_fs_begin(root)`), with the server started by `pc_ssh_scp_begin()`: the SINK direction (client -> device). The binding drives the rcp handshake over the exec channel - send a ready ack, read the `C<mode> <size> <name>` control line, ack it, stream the data straight to the file, read the trailing end-of-record byte, final ack - reaching storage through the same filesystem accessor SFTP uses, so the mount root and the `..` traversal guard are shared rather than reimplemented; error/ack bytes ride the main channel (no CHANNEL_EXTENDED_DATA). One file per transfer (no `-r`). The rcp codec (network_drivers/application/scp) is pure and host-tested (native_scp: command + control-line parse/build, malformed rejection); the binding is HW-verified against the OpenSSH `scp` client on an ESP32-S3 with an SD card (a 60 KB upload byte-exact). Follow-ups: the SOURCE direction (`scp device:/path localfile`, device -> client - use `sftp get` meanwhile), recursive directory transfers, and the graceful teardown noted under SSH SFTP.
SSH SFTPPC_ENABLE_SSH_SFTP
SFTP v3 server subsystem over SSH (draft-ietf-secsh-filexfer-02; requires SSH + Mnt). Default off. A client's `subsystem "sftp"` request opens an SFTP session and the device serves files from the mounted filesystem (`pc_mnt_mount` + `pc_fs_begin(root)`; the RAM backend, or SD / LittleFS / SPIFFS via `pc_mnt_fs`), started with `pc_ssh_sftp_begin()` - the standards-track southbound path for securely dropping files (e.g. NC / G-code programs) onto the device over the one authenticated SSH port. The channel layer recognizes the subsystem request, tags the channel, and routes its bytes to the binding; the binding accumulates `SSH_FXP_*` request packets from the channel byte stream and executes each against the filesystem, reusing the same file operations WebDAV and the static file server use: OPEN/CLOSE, READ (a short DATA the client re-requests), WRITE (streamed straight to the file as fragments arrive, so a transfer is never buffered whole), OPENDIR/READDIR (with `ls -l`-style longnames), STAT/LSTAT/FSTAT, MKDIR/RMDIR/REMOVE/RENAME, REALPATH, and SETSTAT (accepted, best-effort). Handles are a fixed BSS table (`PC_SFTP_MAX_HANDLES`), 4-byte indices; a request path never reaches storage as a string the server built - it goes to `server/filesystem/filesystem.h`, which owns the mount root, frames the whole path once, and rejects `..` before any filesystem call; zero heap beyond the backend's own file handles. The wire codec (network_drivers/application/sftp) is pure and host-tested (native_ssh_sftp: frame round-trips, ATTRS + age of every field, length-framing, longname); the fs binding is HW-verified against the OpenSSH `sftp` client on an ESP32-S3 with an SD card (mkdir / put / ls / get / rename / rm, a 60 KB file byte-exact). Note: a `READ` returns at most one SSH packet of data (`PC_SFTP_MAX_READ`, kept within `SSH_PKT_BUF_SIZE`); raise both for throughput. Follow-ups: the SCP SOURCE (download) direction and a graceful `SSH_MSG_DISCONNECT` on teardown.
TelnetPC_ENABLE_TELNET
Telnet server support (RFC 854 / IAC option negotiation).

Presentation (L6)18

AuthPC_ENABLE_AUTH
HTTP Basic Authentication per-route.
Auth LockoutPC_ENABLE_AUTH_LOCKOUT
Opt-in per-IP brute-force lockout for HTTP auth (requires AUTH). Default off (zero cost / no behavior change). When set, the auth gate counts consecutive failed authentications per source IPv4 in a fixed BSS table; after PC_AUTH_LOCKOUT_THRESHOLD failures the address is locked out for PC_AUTH_LOCKOUT_BASE_MS, doubling on each further failure up to PC_AUTH_LOCKOUT_MAX_MS. A locked address gets 429 (Retry-After) with no credential check; a successful auth clears it. Bounded memory (no heap); the table evicts idle, then least-recently-used, addresses when full.
CBORPC_ENABLE_CBOR
Zero-heap CBOR (RFC 8949) encoder for compact binary payloads. Default off. When set, network_drivers/presentation/codec/cbor/cbor.h provides a writer that serializes ints, strings, byte strings, arrays, maps, booleans, null, and float32 into a caller-provided buffer - a compact binary alternative to the JSON writer for telemetry. Pure, no heap, host-tested against the RFC 8949 vectors.
CloudEventsPC_ENABLE_CLOUDEVENTS
CloudEvents v1.0 (CNCF) event envelope. Default off. services/iot/cloudevents makes a device's events interoperable with serverless / event-mesh consumers: `pc_cloudevents_build_json()` emits a structured `application/cloudevents+json` envelope (the required `id` / `source` / `type` plus optional `subject` / `datacontenttype` / `data`) over the JSON writer, and `pc_cloudevents_from_headers()` reads an inbound binary-mode event's `ce-*` headers. Pure and host-tested. See src/services/cloudevents.h.
HTTP DeliveryPC_ENABLE_HTTP_DELIVERY
Opt-in HTTP delivery optimizations that make a slow origin acceptable to a browser. Default off. **Stale-while-revalidate (RFC 5861):** `pc_delivery_swr` decides FRESH / serve-stale-and-revalidate / EXPIRED from an age against `max-age` + `stale-while-revalidate`, and `pc_delivery_cache_control` builds the matching header - wired into serving by `PC::set_cache_control_swr(max_age_s, swr_s)`, so every `serve_file` / `serve_static` response carries `public, max-age=N, stale-while-revalidate=M` and the header can never drift from the decision. **Service worker:** `pc_delivery_sw_manifest` emits the versioned `{"version":..,"precache":[..]}` document, and `pc_delivery_serve_sw(srv, paths, n, version)` registers `/sw.js` (a flash-resident worker shipped through the web-asset pipeline) plus `/precache.json`; the worker precaches the shell and then serves it stale-while-revalidate client-side, naming its cache after the version so a bump invalidates the old shell exactly once, and the manifest route answers 500 rather than serving truncated JSON if it would not fit `PC_DELIVERY_MANIFEST_BUF` (`PC_DELIVERY_PRECACHE_MAX` paths). **Byte ranges are NOT in this service** - `network_drivers/application/http_range.h` (`http_parse_byte_range`, `PC_ENABLE_RANGE`) is the single owner of the RFC 7233 range math and is already wired into static file serving and the edge cache, emitting `Accept-Ranges`, the 206 `Content-Range`, and a 416 `bytes */size`; a duplicate parser here was removed rather than given a second call site. Pure cores host-tested (`native_http_delivery`) and **HW-verified on an ESP32-P4 serving from SD**: `bytes=10-19` / `bytes=-5` / `bytes=995-` each returned byte-exact 206 payloads with the right `Content-Range`, an out-of-range request returned 416 `bytes */1000`, and served files carried the SWR header. Example HttpDelivery. See src/services/file_transfer/http_delivery/http_delivery.h.
HTTP/1.1 Parseralways on
RFC 7230 request parser - validates method, path, header names and values byte-by-byte before storing anything. Always on.
HTTP/2PC_ENABLE_HTTP2
HTTP/2 (RFC 9113) over the version-agnostic request/response core. Default off. Negotiated by TLS ALPN ("h2", falling back to "http/1.1"); an h2 connection speaks the binary framing + HPACK header compression on top of the same routes and handlers as HTTP/1.1 (the response serializer branches on the connection's protocol). The three layers are pure and host-tested against the RFCs: **HPACK** (RFC 7541 - static + dynamic table, prefix-integer / string / canonical-Huffman coding; `native_hpack` vs the Appendix C vectors), the **frame layer** (RFC 9113 sec 4/6 - the 9-byte header + SETTINGS / WINDOW_UPDATE / RST_STREAM / GOAWAY / PING / HEADERS / DATA; `native_h2frame`), and the **connection/stream engine** (preface, SETTINGS exchange, per-stream HEADERS->request + DATA/flow-control, PING/RST/GOAWAY, reassembly; `native_h2conn` drives a full request/response cycle). A connection's engine is ~28 KB, so it does not fit internal DRAM alongside TLS: **HTTP/2 requires PSRAM** (set PC_H2_POOL_IN_PSRAM=1 on an S3 / P4 / WROVER; a compile-time guard enforces it). See src/network_drivers/presentation/http/http2/.
HTTP/3PC_ENABLE_HTTP3
HTTP/3 (RFC 9114) over QUIC (RFC 9000) - implemented, host-tested end-to-end. Default off. HTTP/3 runs over QUIC (a reliable transport over UDP) with QPACK (RFC 9204) header compression and its own binary framing. The full stack is in place and exercised by a host end-to-end test: a QUIC client completes the TLS 1.3-in-QUIC handshake, sends a QPACK-encoded HTTP/3 GET, and verifies the 1-RTT response - covering the QUIC variable-length integer (RFC 9000 sec 16), packet protection + framing, the TLS 1.3 handshake, the transport connection engine, and the HTTP/3 + QPACK codecs. On-device (ESP32) HW verification and an example are still pending. Like HTTP/2 this is a PSRAM-class feature.
JSONalways on
Zero-heap JSON writer/reader (json.h) for request bodies and responses. Always on.
JWTPC_ENABLE_JWT
JWT bearer-token authentication (HS256). Default off. When set, src/services/security/jwt/jwt.h verifies `Authorization: Bearer <jwt>` tokens signed with HMAC-SHA-256 (reusing the SSH crypto layer) and can read integer claims (e.g. `exp`) so a handler/middleware can gate routes on a stateless token. Signature verification is constant-time.
MessagePackPC_ENABLE_MSGPACK
Zero-heap MessagePack encoder and decoder for compact binary payloads. Default off. When set, network_drivers/presentation/codec/msgpack/msgpack.h provides a writer that serializes ints, strings, byte strings, arrays, maps, booleans, nil, and float32 into a caller-provided buffer, plus a cursor decoder (pc_msgpack_peek / pc_msgpack_read_\*, no-copy strings) over a caller buffer - the MessagePack-format sibling of the CBOR / JSON readers and writers. Pure, no heap, host-tested against the spec encodings and round-trip.
MultipartPC_ENABLE_MULTIPART
multipart/form-data body parser.
ProtobufPC_ENABLE_PROTOBUF
Protocol Buffers wire codec. Default off. services/iot/protobuf is a zero-heap streaming Protobuf encoder + cursor reader over caller buffers (the same shape as the CBOR / MessagePack codecs): a writer for varint / ZigZag / fixed32 / fixed64 / length-delimited fields (`pc_pb_uint64`, `pc_pb_sint64`, `pc_pb_fixed32`, `pc_pb_fixed64`, `pc_pb_float`, `pc_pb_double`, `pc_pb_bytes`, `pc_pb_string`; embedded messages are built into a sub-buffer and added with `pc_pb_bytes`), and a reader (`pc_pb_read_field`) that decodes one field at the buffer head and reports bytes consumed, with ZigZag / float / double value decoders. Host-tested against the spec vectors. This is the standalone Protobuf deliverable; gRPC (framed Protobuf over HTTP/2) is gated on the HTTP/2 roadmap item. See src/services/iot/protobuf/protobuf.h.
SenMLPC_ENABLE_SENML
SenML (RFC 8428) measurement-pack builder. Default off; implies CBOR. services/iot/senml is a zero-heap SenML-JSON + SenML-CBOR encoder over the shipped JSON / CBOR writers: the caller fills a `SenmlRecord` array (optional base name / base time, name, unit, one value - number / string / boolean, optional time) and `pc_senml_json_build` / `pc_senml_cbor_build` emit the whole pack. SenML-JSON uses the text labels (bn/n/u/v/vs/vb/t); SenML-CBOR uses the integer labels (n=0, u=1, v=2, ..., bn=-2, bt=-3). Numbers that are integral are emitted as integers so timestamps keep full precision. On the consuming side, `pc_senml_resolve` (RFC 8428 §4.6) folds the base name and base time across a pack into `SenmlResolved` records - each with the full concatenated name and the absolute (base + record) time, the base fields carried forward until a later record overrides them - so a receiver gets standalone measurements. The standard measurement format for CoAP / LwM2M / HTTP telemetry; verified against the RFC example, and the resolver host-tested against a base-name/base-time pack. See src/services/iot/senml/senml.h.
SSEPC_ENABLE_SSE
Server-Sent Events push support.
Web TerminalPC_ENABLE_WEB_TERMINAL
Browser "web serial" terminal over WebSocket (src/services/web/web_terminal). Serves a self-contained terminal page and a WebSocket endpoint: device output is broadcast to all connected browsers, browser input is delivered to a command callback. Requires WEBSOCKET. Default off.
WebSocketPC_ENABLE_WEBSOCKET
WebSocket support (RFC 6455 framing + SHA-1/base64 handshake).
WS DeflatePC_ENABLE_WS_DEFLATE
WebSocket permessage-deflate (RFC 7692) - bidirectional compression. When set (and WEBSOCKET is on), the server negotiates the `permessage-deflate` extension and both decompresses inbound compressed (RSV1) messages via a bounded INFLATE (network_drivers/presentation/inflate._) and compresses outbound data frames via a bounded DEFLATE (network_drivers/presentation/deflate._); both borrow their table scratch from the shared per-dispatch arena. The extension is negotiated with `{client,server}_no_context_takeover` so every message (de)compresses independently - no window is carried between messages. An outbound frame that would not shrink is sent uncompressed (the per-message RSV1 flag permits this). Default off.

Web & HTTP16

Chunked Responsesalways on
Streaming / chunked responses of unbounded length in constant memory via send_chunked(). Always on.
CORSalways on
Cross-origin resource sharing with automatic preflight handling. Always on.
DashboardPC_ENABLE_DASHBOARD
Real-time SVG dashboard (DASHBOARD; requires SSE). Default off. Serves a self-contained, hand-rolled SVG dashboard page whose widgets are declared in a fixed compile-time pc_widget table (zero-heap, deterministic). The page fetches the widget layout as JSON and subscribes to an SSE stream of live values; pc_dashboard_set() + pc_dashboard_publish() push the current readings. The widget-table -> JSON serializers are host-testable; WebSocket controls are a follow-up.
Edge CachePC_ENABLE_EDGE_CACHE
Opt-in CDN edge-cache tier (requires HTTP Cache). Default off. services/web/edge_cache is the caching reverse-proxy edge that services/web/httpcache is the origin-side groundwork for: the device sits in front of a remote upstream origin, fetches a response once, and serves subsequent hits from a bounded local store - honoring `Cache-Control` / `Expires` / `ETag` / `Last-Modified`, revalidating stale entries with conditional requests (`If-None-Match` / `If-Modified-Since` -> 304), and serving `Range` / `206 Partial Content` straight from the cache with the existing constant-memory send-pump. A two-tier store: bounded RAM (L1, hot, LRU + TTL) plus an optional dbm/WAL-backed SD tier (L2, persistent across reboot, when DBM is enabled). A miss or a stale-entry revalidation fetches the origin asynchronously - the client request is suspended and resumed from the server poll loop, so the worker never stalls - and every failure path (miss, full cache, origin down, oversize) fails open to the origin. The deterministic cache key is method + host + path (+ optional query), with a SHA-256 digest for the L2 key and `Vary` handled as a secondary key; explicit purge (single + prefix/wildcard) and stats round it out. Registered as a middleware via `pc_edge_cache_enable(server)` + `pc_edge_cache_map(prefix, origin)`; zero heap, all buffers fixed. The freshness/validator/key/store logic is pure and host-tested (native_edge_cache); the async origin fetch + serve are HW-verified against a real origin; the L2 SD tier's entry serialization, spill/promote, reboot survival, and purge are host-tested over a RAM WalDev (native_edge_cache_sd) via `pc_edge_cache_bind_sd`. When `PC_ENABLE_RANGE` is set, a cached object also answers a single-range `Range` request with `206 Partial Content` + `Content-Range` (or `416`) and advertises `Accept-Ranges: bytes`, streaming just the window through the same shared parser (server/http_range) the file server uses.
Edge Cache MeshPC_ENABLE_EDGE_MESH
Opt-in mesh (sibling-cache) distribution for the edge cache (requires Edge Cache). Default off. Lets a fleet of edge nodes share one warm cache: on a full local miss a node queries its configured sibling peers (over a plaintext `ConnProto::PROTO_MESH` TCP link) before hitting the origin, and pulls a fresh copy from whichever peer has it - so the origin is fetched once per fleet, not once per node. Pull (read-through) only: no push, no invalidation protocol, no cross-node consistency window - a stale sibling copy self-expires by its own TTL and the requester re-checks freshness on arrival. The transfer carries the object plus its freshness/age (RFC 9111 §4.2.3 age propagation, reusing the same `edge_current_age()` math as the single-node serve), so a sibling-fresh object serves for its remaining lifetime with zero origin contact. The wire frame reuses the shared `edge_sd` entry serializer plus a small timing trailer; the requester ships a bounded snapshot of its request headers so the peer re-runs the exact `edge_store_find` Vary matcher (a header past `PC_MESH_HDRS_MAX` degrades to a safe miss, never wrong content). A serving node (`pc_edge_cache_mesh_serve`, after `server.listen(port, ConnProto::PROTO_MESH)`) answers only from its LOCAL cache - one hop, never re-querying its own origin or peers, so the fleet cannot loop. The peer query runs as a pre-origin phase of the same async fetch slot (reusing that slot's origin buffer, so no extra per-slot memory) and is pumped from the server poll loop, so it never stalls the worker; a peer MISS or an exhausted peer list transitions the slot to the ordinary origin fetch. Peers are a static list (`pc_edge_cache_add_peer(host, port)`, up to `PC_MESH_MAX_PEERS`, tried in series, first hit wins). The wire codec + peer-query state machine are pure and host-tested (native_edge_mesh: frame round-trips, age propagation, HIT/MISS/timeout/close), the two-node integration is HW-verified on ESP32-S3 rigs (example 80). Follow-ups: a TLS sibling link, UDP-broadcast peer auto-discovery, and push replication with invalidation.
ETagPC_ENABLE_ETAG
Conditional GET via ETag for served files. When set, serve_file()/serve_static() emit a strong `ETag` (derived from the file size + last-modified time) and answer a matching `If-None-Match` with `304 Not Modified`, saving bandwidth on repeat fetches of static assets.
File ServingPC_ENABLE_FILE_SERVING
Static file serving via Arduino FS (LittleFS, SPIFFS, SD).
HTTP CachePC_ENABLE_HTTP_CACHE
HTTP `Cache-Control` directive helpers (RFC 9111 + RFC 8246 + RFC 5861). Default off. services/web/httpcache is the origin-side of edge caching so app routes emit correct, edge-cacheable responses (the groundwork for the CDN roadmap; the caching tier itself is a separate piece). `cache_control_build` serializes a `pc_cache_control` struct into the canonical directive string (hand it to `set_cache_control()`), with first-class presets for the common cases - `cache_immutable_asset` (`public, max-age=..., immutable`), `cache_shared` (distinct browser vs shared-cache TTL via `s-maxage`), `cache_revalidatable` (`stale-while-revalidate`), and `cache_no_store`. `cache_control_parse` is a tolerant reader (case-insensitive names, bare or quoted delta-seconds, unknown directives ignored) covering the response directives plus `no-cache` / `must-revalidate` / `proxy-revalidate` / `must-understand` / the extensions and the request directives (`only-if-cached` / `max-stale` / `min-fresh`). `cache_freshness_lifetime` implements the RFC 9111 sec 4.2.1 first-match precedence (s-maxage for a shared cache, then max-age, then `Expires` minus `Date`). Directive set + freshness precedence verified against RFC 9111; pure and host-tested with a build->parse round-trip. See src/services/web/httpcache/httpcache.h.
Middlewarealways on
Composable use() pipeline with a fixed-window rate limiter. Always on.
RangePC_ENABLE_RANGE
HTTP Range requests / 206 Partial Content for served files. Default off. When set (requires FILE_SERVING), serve_file() / serve_static() honor a single-range `Range: bytes=...` request header: they answer `206 Partial Content` with a `Content-Range` header and stream only the requested bytes (seeking the file to the start offset), advertise `Accept-Ranges: bytes` on full responses, and answer an unsatisfiable range with `416 Range Not Satisfiable`. This enables resumable downloads and media seeking. Multi-range (multipart/byteranges) requests are not supported - the server falls back to a full 200 response, which is RFC 7233 §3.1 compliant.
Routingalways on
Exact, wildcard (/*), :param path parameters, bounded allocation-free regex routes, and per-interface STA/softAP route filters. Always on.
SPA RouterPC_ENABLE_SPA_ROUTER
Opt-in single-page-app micro-routing decision. When set, services/web/spa_router provides pc_spa_route(): given a request path it returns whether to serve a real asset file, serve the SPA shell (index.html) for a client-side route, or pass through to the app's handlers under an API prefix - so a single-page UI's client routing works. Pure decision core (the caller wires the result into serve_static / the router). **Fallback HMI**: on a machine-control device the SPA is a convenience, not the contract, so `pc_spa_route_ex()` takes a `pc_spa_ctx` (shell available? client will script? device degraded?) and resolves a client route to `PC_SPA_SERVE_FALLBACK` - a plain server-rendered control page needing no JavaScript and no asset files - whenever the SPA cannot serve it. Two rules are what make that worth having: the API prefix **keeps passing through** in fallback mode (the page own controls post to those endpoints, so cutting them off would leave an operator unable to actuate anything), and asset requests are never rewritten (handing back HTML where the browser asked for CSS). **Conditional UI streaming**: that page is a fragment table, each entry with a predicate, emitted by `pc_ui_stream_next()` into a buffer of any size - it resumes mid-fragment, so a page far larger than the buffer streams out in pieces, and predicates run as the stream reaches each fragment rather than all up front, so a long render reflects the state that holds when it gets there. Host-tested (`native_spa_router`, 16 cases including byte-identical output across chunk sizes 1..40). **HW-verified on an ESP32-S3**: healthy routes served the shell; `?nojs=1`, a missing shell asset, and a degraded device each served the fallback; and actuating through the still-live API while in fallback mode flipped two predicates in opposite directions on the next render (`ALARM ACTIVE` appeared, `Status: normal` disappeared) - only observable because the API kept working when the SPA did not. Example SpaFallback. Default off.
Templatingalways on
{{var}} response templating via send_template(). Always on.
ThemesPC_ENABLE_THEMES
Embed the theme stylesheet library as runtime-selectable blobs (default off). Off by default: build-time theme injection (`<!--#theme NAME-->`) costs nothing extra, but embedding the whole library for runtime switching links every theme's CSS into flash (~1 KB each). When set, application/binary_asset_blobs.{h,cpp} exposes `pc_theme_css(name)` + the registry `PC_THEME_BLOBS`, so a route (e.g. `/themes/<name>.css`) or a picker can switch themes live. Regenerate with `web_assets/wizard/gen_theme_blobs.py` after adding a theme.
UploadPC_ENABLE_UPLOAD
Streaming file upload: POST a body straight to a file on the filesystem. Default off. When set, src/services/upload_service.h registers a POST route that streams the request body directly into an Arduino FS file (LittleFS / SPIFFS / SD) - the upload never has to fit in RAM. Reuses the same parser streaming-body hook as OTA. Enabling upload (or OTA / WebDAV) raises RX_BUF_SIZE to the streaming floor of 8192 automatically (in board_drivers/board_profiles/derived_sizing.h, regardless of a board profile's smaller pin), comfortably above a full TCP receive window - the transport refuses-and-redelivers a segment that will not fit the receive ring (lossless backpressure), so a ring below one window would otherwise stall. Override RX_BUF_SIZE higher for very large windows; the 1024 base suits ordinary requests, not uploads.
WebDAVPC_ENABLE_WEBDAV
WebDAV server (RFC 4918, class 1 + class 2 locking) over the file system. Default off. When set (requires FILE_SERVING), dav() mounts an FS subtree that answers the WebDAV methods - OPTIONS, PROPFIND (Depth 0/1), PROPPATCH, GET, HEAD, PUT, DELETE, MKCOL, COPY, MOVE, and LOCK/UNLOCK - so a client (rclone, cadaver, curl, or a mounted network drive) can browse and edit files. PROPFIND returns a 207 Multi-Status document built into a fixed buffer (PC_WEBDAV_BUF_SIZE); a Depth-1 listing is capped at PC_WEBDAV_MAX_ENTRIES children. PROPPATCH returns a 207 with each requested property refused 403 Forbidden (read-only live properties, no dead-property store) so Explorer/Finder - which PROPPATCH a timestamp right after a PUT - do not error on a 405. PUT streams the request body straight to the file (via the shared streaming-body sink), so an upload is not bounded by BODY_BUF_SIZE. Locks are enforced by a small fixed lock table (PC_DAV_LOCK_MAX): LOCK stores an exclusive or shared token (Depth 0 or infinity), a write (PUT/DELETE/MKCOL/COPY/MOVE) to a locked resource without the matching token in its If header is refused 423 Locked, and UNLOCK requires the token. Each lock carries a Second-3600 timeout and is swept once it expires; a LOCK presenting the token in its If header refreshes it. See docs/SECURITY.md before exposing it.

Auth, Identity & Security5

Audit LogPC_ENABLE_AUDIT_LOG
Tamper-evident audit log. Default off. services/security/audit_log keeps an append-only, hash-chained security log: each record carries SHA-256(prev_hash || fields), so altering, deleting, or reordering any retained record breaks the chain (pc_audit_verify() detects it). Storage is a fixed RAM ring of PC_AUDIT_LOG_ENTRIES records (no heap); when it wraps, a moving anchor keeps the retained window verifiable. Install a sink (pc_audit_set_sink) to forward every record at creation time to a durable / remote store - SD-card file, syslog or HTTP log service, serial console - preserving the same chain off-device. Pure and host-tested.
CSRFPC_ENABLE_CSRF
Opt-in CSRF protection for state-changing HTTP requests. Default off (zero cost / no behavior change). When set, every POST / PUT / PATCH / DELETE must carry a valid `X-CSRF-Token` header (a stateless, HMAC-signed token); requests without one get 403 Forbidden. GET / HEAD / OPTIONS are exempt (they are not state-changing). Clients fetch a token from the built-in `GET /csrf` endpoint, which also sets it as the `csrf` cookie. No server-side session storage - the token self-validates against an HMAC secret seeded from the hardware RNG at begin(); it is independent of AUTH.
OAuth2PC_ENABLE_OAUTH2
OAuth2 token-endpoint client. Default off. services/security/oauth2 obtains tokens - the counterpart to the OIDC ID-token verifier. It builds the percent-encoded form body for the authorization_code and refresh_token grants (RFC 6749), supporting a confidential client (client_secret) or a public client with PKCE (code_verifier, RFC 7636), and parses the JSON token response (reusing the zero-heap JSON reader). The build + parse core is pure and host-tested; the POST to the token endpoint uses the HTTP(S) client (needs HTTP_CLIENT). No heap, no stdlib.
OIDCPC_ENABLE_OIDC
OpenID Connect ID-token verification, RS256. Default off. services/security/oidc verifies an OIDC ID token (JWT) as a relying party: requires alg RS256, selects the issuer key by kid from a JWKS, verifies the RSASSA-PKCS1-v1.5 SHA-256 signature (real RSA modexp via ssh_rsa, mbedTLS- accelerated on ESP32), and checks iss / aud / exp / nbf, extracting sub / email. Pure and host-tested; the caller fetches + caches the JWKS over HTTPS (off the request hot path) and passes the JSON in. Builds on the SSH RSA primitive, not the HS256 JWT module (services/security/jwt), so the two are independent.
TOTPPC_ENABLE_TOTP
Opt-in TOTP two-factor auth (RFC 6238). Default off. services/security/totp computes and verifies time-based one-time passwords (HMAC-SHA1 over the existing SHA-1, Google Authenticator compatible) and decodes base32 shared secrets, for a second factor on top of password / JWT auth. Pure and host-tested against the RFC 6238 vectors; the verifier checks a +/- step window for clock skew.

IoT, Messaging & APIs16

AMQPPC_ENABLE_AMQP
AMQP 0-9-1 frame codec - the RabbitMQ wire protocol. Default off. services/iot/amqp lets a device be an AMQP client over the outbound client transport: `pc_amqp_protocol_header` writes the `"AMQP" 0 0 9 1` preamble, `pc_amqp_build_frame` / `pc_amqp_parse_frame` build and validate a frame (type + channel + 4-octet size + payload + the mandatory 0xCE frame-end), `pc_amqp_build_method` / `pc_amqp_parse_method` handle a METHOD frame's class-id / method-id / arguments, `pc_amqp_build_content_header` frames the content HEADER frame that follows a Basic.Publish (class-id + weight + the 8-octet body size + property flags + an optional property list) so a device can publish a message body, and `pc_amqp_build_heartbeat` emits a keep-alive. Pure and host-tested; the method-argument field encoding and the connection state are the application's. See src/services/iot/amqp/amqp.h.
CoAPPC_ENABLE_COAP
CoAP server (RFC 7252) over UDP/5683. A zero-heap Constrained Application Protocol endpoint: a fixed resource table dispatched against the request's Uri-Path, with a pure host-testable message codec (parse/build) and an ESP32 UDP binding via the transport-layer UDP service. **Message de-duplication (RFC 7252 sec 4.5)** is built in: a retransmitted Confirmable request is re-answered from a small cache keyed on the full (source endpoint, Message-ID) - never a hash, so two peers cannot collide - _without_ re-running its handler, so a client's CON retransmission cannot execute a non-idempotent request (POST/PUT/DELETE) twice (`PC_COAP_DEDUP_ENTRIES`, default 4, freshness `PC_COAP_DEDUP_LIFETIME_MS` = EXCHANGE_LIFETIME; set entries 0 to compile it out). Separate (deferred) responses and CON retransmission are non-goals by design: the server answers in-line (a request is served before its handler returns) and sends notifications Non-confirmable, so it never emits a Confirmable message to retransmit. Default off; the codec is otherwise unit-tested standalone (env:native_coap).
CoAP BlockPC_ENABLE_COAP_BLOCK
CoAP block-wise transfer - RFC 7959 (requires COAP). Default off. When set, the server understands the Block2 (descriptive, responses) and Block1 (control, request uploads) options: - Block2: a representation larger than one block, or any GET that carries a Block2 option, is served one block at a time. A constrained client requests a small block size (SZX) and pages through with ascending block numbers; the server re-renders the (idempotent) resource and slices out the asked-for block, setting the More bit until the last. - Block1: a POST/PUT payload larger than one block is reassembled into a single BSS buffer. Each non-final block is acknowledged 2.31 Continue; the final block dispatches the handler with the whole reassembled payload. One block-wise transfer is reassembled at a time (deterministic, single buffer); an out-of-order or oversized block yields 4.08 / 4.13. Size1/Size2 options and the /.well-known/core listing are out of scope.
CoAP ObservePC_ENABLE_COAP_OBSERVE
CoAP resource observation - RFC 7641 (requires COAP). Default off. When set, a client GET with the Observe option registers as an observer of a resource; the application calls pc_coap_notify(path) to push the resource's current representation to every observer (a CoAP notification from the server port with an increasing Observe sequence). Observers are dropped on a deregister GET, a client RST, or send failure.
DDS-RTPSPC_ENABLE_DDS
Opt-in DDS / RTPS wire-protocol codec. When set, services/iot/dds provides the RTPS (DDSI-RTPS) message + submessage framing: the 20-octet header (magic / version / vendor / guidPrefix) and the typed submessages (INFO_TS, DATA, HEARTBEAT, ACKNACK, ...) with the endianness flag, built by pc_rtps_header / _submessage and walked by pc_rtps_parse. Pure framing (CDR payloads + SPDP/SEDP discovery layer on top). Default off.
GraphQLPC_ENABLE_GRAPHQL
GraphQL query subset. Default off. services/iot/graphql parses a GraphQL query into a fixed AST node pool (no heap) and emits a `{"data":{...}}` response shaped exactly by the requested selection. Schema-free: a field with a sub-selection is an object (the engine recurses), a leaf field calls your single resolver, and arguments collected along the path are handed to it. Supports nested selections, field arguments, and the anonymous / `query` forms; mutations, subscriptions, fragments, and variables are out of scope. Pure and host-tested; bounds are compile-time (PC_GQL_* in protocore_config.h). Serve it from a POST /graphql route.
gRPC-WebPC_ENABLE_GRPC_WEB
gRPC-Web message framing. Default off. services/iot/grpcweb is a zero-heap length-prefixed frame builder + parser for gRPC-Web, the HTTP/1.1-reachable subset of gRPC (gRPC proper needs HTTP/2): `pc_grpcweb_frame_message` wraps a Protobuf message in the 5-octet `[flags][length BE32]` prefix, `pc_grpcweb_frame_trailer` emits the 0x80 trailers frame (`grpc-status` / `grpc-message`), and `pc_grpcweb_parse` reads one frame back (with `pc_grpcweb_trailer_status` and `pc_grpcweb_trailer_message` pulling the status code and the human-readable error text out of a trailers frame, so a client sees why a call failed). Wraps the Protobuf codec (PC_ENABLE_PROTOBUF) and rides the shipped HTTP/1.1 server/client. Pure and host-tested. See src/services/iot/grpcweb/grpcweb.h.
LwM2MPC_ENABLE_LWM2M
OMA LwM2M TLV codec. Default off. services/iot/lwm2m is a zero-heap writer + cursor reader for the LwM2M `application/vnd.oma.lwm2m+tlv` resource encoding carried over the shipped CoAP service for device management: `pc_lwm2m_tlv_write` (with typed `pc_lwm2m_tlv_write_int` shortest-form / `pc_lwm2m_tlv_write_bool` / `pc_lwm2m_tlv_write_string` / `pc_lwm2m_tlv_write_float` helpers), `pc_lwm2m_tlv_read`, and `pc_lwm2m_tlv_value_int`. Handles 8-/16-bit identifiers, inline / 8- / 16- / 24-bit lengths, and the Object-Instance / Resource / Multiple-Resource / Resource-Instance kinds; type-byte layout verified against the LwM2M spec. Pure and host-tested. The registration interface and the standard object model layer on top. See src/services/iot/lwm2m/pc_lwm2m_tlv.h.
MQTTPC_ENABLE_MQTT
MQTT 3.1.1 publish/subscribe client (raw lwIP, optional MQTTS over TLS). Default off. When set, src/services/iot/mqtt/mqtt.h provides a persistent outbound client: connect to a broker, PUBLISH (QoS 0/1/2) and SUBSCRIBE to topics, receive incoming messages via a callback, with keep-alive pings - the dominant IoT messaging pattern, for telemetry push and remote command. The packet codec is host-testable; the transport (DNS + raw lwIP TCP, MQTTS via client-side mbedTLS) is ESP32-only. Full QoS 0/1/2 (outbound DUP retransmit, inbound QoS-2 de-duplication by packet id) and Last-Will are supported.
MQTT SNPC_ENABLE_MQTT_SN
MQTT-SN v1.2 wire codec. Default off. services/iot/mqtt/mqtt_sn is a zero-heap codec for MQTT for Sensor Networks - the UDP / non-TCP MQTT variant for constrained, lossy links (numeric topic IDs instead of strings, gateway discovery, sleeping-client keep-alive). Builders for CONNECT / REGISTER / PUBLISH / SUBSCRIBE (by name or pre-defined id) / PINGREQ / DISCONNECT / SEARCHGW, plus `pc_mqttsn_parse_header()` (the 1- and 3-octet Length forms, big-endian fields) and typed parsers for CONNACK / REGACK / PUBACK / SUBACK / PUBLISH / REGISTER, with a `pc_mqttsn_make_flags()` helper (DUP / QoS / retain / will / clean / TopicIdType). Wire bytes verified against the spec and the Eclipse Paho reference; pure and host-tested. The datagram send (pc_udp_sendto), topic-ID registry, and sleep / retransmit state are the application's. See src/services/iot/mqtt/mqtt_sn.h.
MQTT TLSPC_ENABLE_MQTT_TLS
MQTTS: run the MQTT client over client-side TLS (needs TLS).
NATSPC_ENABLE_NATS
NATS client protocol codec - the text-based NATS pub/sub messaging protocol. Default off. services/iot/nats lets a device be a NATS client over the outbound client transport: builders for `CONNECT`, `PUB` (with optional reply-to), `HPUB` (a NATS 2.2+ publish carrying a header block), `SUB` (with optional queue group), `UNSUB`, `PING`, and `PONG`, plus `pc_nats_parse` which decodes an inbound `MSG` / `HMSG` / `INFO` / `PING` / `PONG` / `+OK` / `-ERR` (a MSG yields subject / sid / reply-to / payload; an HMSG additionally exposes its header block via the two length fields). Line-oriented (CRLF, space-delimited); only PUB / HPUB / MSG / HMSG carry a payload. Pure and host-tested; the connection and subscription state are the application's. See src/services/iot/nats/nats.h.
SparkplugPC_ENABLE_SPARKPLUG
Sparkplug B payload + topic codec - the Eclipse Sparkplug B industrial-IoT format over MQTT. Default off; implies PROTOBUF (the payload is a Protobuf message). services/iot/sparkplug builds the topic (`pc_spb_build_topic` -> `spBv1.0/group/type/node[/device]`) and serializes the payload over the protobuf codec: `pc_spb_build_metric` emits one Tahu Metric (name / alias / timestamp / datatype + a value - int / long / float / double / boolean / string), and `pc_spb_build_payload` wraps the timestamp + metrics + seq. On the subscriber side, `pc_spb_parse_payload` decodes a received payload's timestamp + sequence number, `pc_spb_payload_next_metric` iterates its metric sub-messages, and `pc_spb_parse_metric` decodes each into an `SpbMetricDecoded` (name, alias, timestamp, datatype, and the value oneof) - so a device consumes NDATA / DDATA from a broker, not only publishes. Field numbers and datatype codes are verified against the Eclipse Tahu sparkplug_b.proto, and a build -> decode round-trip is host-tested. Pure; publish / subscribe the payload with the MQTT client. See src/services/iot/sparkplug/sparkplug.h.
StompPC_ENABLE_STOMP
STOMP 1.2 frame codec. Default off. services/iot/stomp is a zero-heap codec for the Simple/Streaming Text Oriented Messaging Protocol: `pc_stomp_build_frame()` writes a frame (command + escaped `key:value` headers + blank line + NUL-terminated body) and `pc_stomp_parse_frame()` is a non-mutating cursor reporting the command, header key/value slices, and body (honoring `content-length`, tolerating `\r\n` line endings, and skipping broker heart-beats), with `pc_stomp_header()` lookup and `pc_stomp_unescape()` for the header escapes (`\r` `\n` `\c` `\\`). Drives CONNECT / SEND / SUBSCRIBE / MESSAGE / ACK against a broker (ActiveMQ / RabbitMQ / Artemis) over the shipped outbound client transport, or STOMP-over-WebSocket via the WS client. Pure and host-tested; the connection and subscription state are the application's. See src/services/stomp.h.
WAMPPC_ENABLE_WAMP
WAMP messaging codec. Default off. services/iot/wamp is a zero-heap codec for the Web Application Messaging Protocol (unified RPC + PubSub over WebSocket, subprotocol `wamp.2.json`): builders for HELLO / SUBSCRIBE / UNSUBSCRIBE / PUBLISH / CALL / REGISTER / UNREGISTER / YIELD / GOODBYE (JSON arrays emitted via the shared JsonWriter - Options/Details default to `{}`, Arguments / ArgumentsKw are passed as JSON literals), plus a nesting-aware positional parser (`pc_wamp_get_type`, `pc_wamp_get_uint`, `pc_wamp_get_uri`, `pc_wamp_element`) that pulls the message type, ids, and URIs out of an inbound WELCOME / SUBSCRIBED / EVENT / RESULT / INVOCATION / ERROR. Message codes verified against the WAMP spec; pure and host-tested. It rides the shipped WebSocket layer; the session / subscription / registration tables are the application's. See src/services/iot/wamp/wamp.h.
XMPPPC_ENABLE_XMPP
Opt-in XMPP (RFC 6120) stanza codec. When set, services/iot/xmpp builds correctly XML-escaped `<stream:stream>` / `<message>` / `<presence>` / `<iq>` stanzas into a caller buffer and reads the stanza element name + an attribute value out of a received stanza, so a device is an IoT XMPP client. Pure text framing (TLS/SASL ride the client TLS path; the IoT XEPs layer inside `<iq>`). Default off.

Industrial & Fieldbus31

ADS (Beckhoff)PC_ENABLE_ADS
Beckhoff ADS / AMS protocol codec (TwinCAT PC-based control over TCP 48898). Default off. services/fieldbus/ads builds + parses the AMS/TCP + AMS-header frames the TwinCAT router speaks (little-endian throughout, target-before-source AMSNetId+port addressing, a command id + state flags + cbData + invoke id): `pc_ads_build_*` emit complete request frames for ReadDeviceInfo, Read, Write, ReadWrite, ReadState, WriteControl, and Add/DeleteDeviceNotification, and `pc_ads_parse_*` decode the matching responses - including the DeviceNotification stamp/sample stream (`pc_ads_parse_notification` walks every sample via a callback). ReadWrite drives symbol-by-name access (write the name to index group 0xF003 to get a handle, then read/write the value through 0xF005). AMS header field order + command ids + state-flag bits verified against the Beckhoff InfoSys AMS/ADS specification; payload layouts cross-checked with Beckhoff's own open-source ADS library, pyads, and Apache PLC4X. Pure codec, host-tested (`native_ads`); the caller owns the TCP socket and the AMS route registration on the target router. The most widely used PC-based-control protocol and a machine-tool data source via TwinCAT NC/CNC. See src/services/fieldbus/ads/ads.h.
BACnetPC_ENABLE_BACNET
BACnet/IP BVLC + NPDU codec - the ASHRAE 135 building-automation network framing over UDP (47808). Default off. services/fieldbus/bacnet provides `pc_bvlc_build` / `pc_bvlc_parse` for the BACnet/IP virtual-link envelope (type 0x81, function such as Original-Unicast-NPDU 0x0A / Original-Broadcast-NPDU 0x0B, 2-octet length) and `pc_npdu_build` / `pc_npdu_parse` for the network layer (version 0x01 + the NPCI control octet + optional DNET/DLEN/DADR destination addressing + hop count), slicing out the APDU. `pc_apdu_parse` then decodes the APDU header into a `BacnetApdu` - the PDU type (confirmed / unconfirmed request, simple / complex ACK), the segmentation flags, the invoke id, and the service choice, with the service parameters sliced out (segmented requests skip the sequence-number + window octets) - so an app dispatches on the BACnet service. For the request side, `pc_apdu_build_who_is` frames a Who-Is unconfirmed-request APDU (service choice 8) - either the 2-octet unbounded form that every device answers, or, with a device-instance search range, the low / high limits appended as minimal-length context-tagged unsigned ints (tags 0 and 1) - so a device discovers others on the internetwork, and `pc_apdu_build_i_am` frames the matching I-Am answer (service choice 0) carrying the device object identifier, the max APDU length accepted, the segmentation-supported enumeration, and the vendor id as application-tagged values. `pc_apdu_build_read_property` frames a ReadProperty confirmed-request APDU (service choice 12) - the workhorse a client sends to read one property of one object: the confirmed-request header (the invoke id and the max-segments / max-APDU-length octet), then the object identifier as context tag 0 and the property identifier as context tag 1, with named object-type (`BACNET_OBJ_*`) and property-identifier (`BACNET_PROP_*`) constants - so a master reads, say, an analog input's present-value without hand-packing the tags. Layout verified against ASHRAE 135 Annex J / Clause 6 / Clause 15 / Clause 20; pure and host-tested. The remaining service parameters (a ReadProperty-ACK's returned value, and the other confirmed services, encoded as ASN.1 tagged data) layer on top. See src/services/fieldbus/bacnet/bacnet.h.
CANopenPC_ENABLE_CANOPEN
CANopen (CiA 301) message codec. Default off. services/fieldbus/canopen is a zero-heap builder + parser for the CANopen messaging set over classic CAN frames (`shared_primitives/can.h`): NMT node control, SYNC, TIME, the heartbeat / boot-up (NMT error control), EMCY, PDO process data, and expedited SDO read / write / abort. The 11-bit COB-ID is a 4-bit function code plus a 7-bit node id, so `pc_canopen_build_*` compute it and `pc_canopen_parse` classifies a received frame back to its function + node; `pc_canopen_parse_sdo_response` decodes a server's expedited upload value, download acknowledge, or abort code. `pc_canopen_build_time` / `pc_canopen_parse_time` carry the CiA 301 §7.2.6 TIME message as a `CanopenTime` (the TIME_OF_DAY structure: 28-bit milliseconds after midnight plus days since the CANopen epoch of January 1, 1984), masking off the reserved top 4 bits of the millisecond field on both build and parse. **Segmented SDO** (CiA 301 §7.2.4.3) handles objects larger than the 4-octet expedited limit: `pc_canopen_build_sdo_download_init` names the object + total size, `pc_canopen_build_sdo_download_segment` / `pc_canopen_build_sdo_upload_segment_req` carry / request the 7-octet segments with the alternating toggle bit and last-segment flag, `pc_canopen_parse_sdo_segment` decodes a segment either direction, and `CanopenSdoReasm` accumulates an upload's segments into a caller buffer (checking the toggle sequence and buffer bound). The object dictionary itself is the application's; block SDO is not covered. Identifier allocation verified against CiA 301, and the segmented transfer round-trip host-tested; pure. Drive it from the ESP32 TWAI peripheral or an MCP2515 over SPI to bridge a CANopen field bus onto Wi-Fi. See src/services/fieldbus/canopen/canopen.h.
CC-LinkPC_ENABLE_CCLINK
Opt-in CC-Link (CLPA) cyclic fieldbus frame codec. When set, services/fieldbus/cclink builds/validates the CC-Link cyclic frame ([station][command][RX/RY bit data][RWr/RWw word data][sum checksum]) a Mitsubishi CC-Link master exchanges with remote stations over RS-485, plus bit/word process-image accessors. Pure codec (the RS-485 timing + CC-Link IE Field PHY are hardware-gated). Default off.
CiA 402PC_ENABLE_CIA402
CiA 402 / IEC 61800-7-201 drive + motion profile over CANopen. Default off (requires CANOPEN). services/fieldbus/cia402 is the standardized servo / stepper drive profile: `pc_cia402_state` decodes the power state machine from the Statusword (the CiA 402 mask/value table - Not ready to switch on / Switch on disabled / Ready to switch on / Switched on / Operation enabled / Quick stop active / Fault reaction active / Fault), `pc_cia402_controlword` and `pc_cia402_enable_sequence` produce the Controlword commands (Shutdown 0x06, Switch on 0x07, Enable operation 0x0F, Quick stop 0x02, Fault reset 0x80) that walk an axis to Operation Enabled, and the `pc_cia402_sdo_set_*` / `pc_cia402_pack_command` / `pc_cia402_unpack_status` helpers write the Controlword, Modes of Operation (PP / PV / PT / HM / IP / CSP / CSV / CST), and target/actual position-velocity-torque objects (0x6040 / 0x6041 / 0x6060 / 0x607A / 0x60FF / 0x6071 / 0x6064 / ...) through the shipped CANopen SDO / PDO codec. State masks + command values + object indices verified against IEC 61800-7-201 and multiple drive vendors' tables; pure and host-tested (`native_cia402`). Turns the ESP32 CAN stack (TWAI or MCP2515) into a motion master; close the loop with a services/system/control PID. See src/services/fieldbus/cia402/cia402.h.
CIPPC_ENABLE_CIP
CIP (Common Industrial Protocol) message codec - the message that rides inside an EtherNet/IP Unconnected Data item (PC_ENABLE_ENIP). Default off. services/fieldbus/cip builds the request - `pc_cip_build_epath` encodes a class/instance/attribute EPATH from logical segments (`0x20 | type | format`, 8- or 16-bit ids), `pc_cip_build_get_attr_single` / `pc_cip_build_set_attr_single` (the read and the write, service 0x0E / 0x10, the write appending the attribute value octets), `pc_cip_build_get_attr_all` (Get_Attributes_All, service 0x01, a class/instance request with no attribute segment that reads every attribute of an object at once - e.g. the whole Identity object) / `pc_cip_build_request` prepend the service code + path size - and `pc_cip_parse_response` splits a reply into service / general status / additional status / data. Service codes and the segment encoding are verified against the Wireshark CIP dissector. Pure and host-tested; wrap the request with `pc_eip_build_send_rr_data` for a working CIP read / write path. See src/services/fieldbus/cip/cip.h.
ControlPC_ENABLE_CONTROL
Closed-loop control law: a zero-heap, FPU-accelerated PID controller. Default off. services/system/control provides a single-precision-float PID (`pid_init` / `pid_update`) with the corrections that matter on real hardware - derivative-on-measurement (a setpoint step produces no derivative kick) with an optional single-pole low-pass, output clamping, anti-windup by conditional integration (the integrator freezes while saturated instead of winding up) plus a hard integral clamp, and a feed-forward term - and inline control-law primitives (`control_clamp` / `control_deadband` / `control_slew` / `control_lpf`). The maths is single-precision end to end so it runs on the ESP32 / ESP32-S3 FPU (`madd.s` fused multiply-add, never the soft-float double path), and `pid_update` is defined `inline` so the whole law folds into the caller. The derivative's `/dt` is the one divide - ~58 of the ~166 CCOUNT cycles per `pid_update` on the ESP32-S3, because the LX7 FPU has no divide instruction (`a/b` becomes a `__divsf3` call) - so for a fixed-rate loop `pid_set_rate(p, dt)` caches `1/dt` and `pid_update_fixed()` runs the hot path with no divide at all (**97.4 cyc, ~41% faster**; a compile-time-constant dt folds the divide away too). `pid_update_n` runs a batch of axes off one control tick. Pure maths, host-tested (`native_control`); see docs/FEATURE_PERFORMANCE.md for the bare-op and PID cycle counts. Pair it with a plant it can command (a services/fieldbus/cia402 drive, a dshot ESC, a heater PWM) and tune the gains offline by replaying the device's run log through `tools/pid_tune.py`. See src/services/system/control/control.h.
COTPPC_ENABLE_COTP
TPKT (RFC 1006) + COTP (ISO 8073 / X.224 class 0) frame codec - the "ISO transport on TCP" foundation under S7comm and IEC 61850 MMS. Default off. services/fieldbus/cotp provides `pc_tpkt_build` / `pc_tpkt_parse` for the 4-octet TPKT envelope (version 3 + 16-bit length), `pc_cotp_build_dt` for a Data TPDU (`LI 0xF0 EOT|NR` + user data), `pc_cotp_build_cr` for a Connection Request (destination/source refs, class 0, the TPDU-size parameter, plus caller TSAP parameters), `pc_cotp_build_cc` for the server-side Connection Confirm that answers a CR (echoing the peer's source reference as the destination reference), and `pc_cotp_parse` which reports the TPDU type and the DT user data or the CR/CC refs - so the device can accept an incoming COTP connection, not only initiate one. Layout verified against RFC 1006 / ISO 8073; pure and host-tested. See src/services/fieldbus/cotp/cotp.h.
DeviceNetPC_ENABLE_DEVICENET
DeviceNet link-adaptation codec. Default off. services/fieldbus/devicenet is the CAN-specific layer of "CIP over CAN": `pc_devicenet_encode_id` / `pc_devicenet_decode_id` pack and unpack the 11-bit DeviceNet identifier as a Message Group (1..4) + Message ID + MAC ID (the four identifier ranges 0x000-0x3FF / 0x400-0x5FF / 0x600-0x7BF / 0x7C0-0x7EF), `pc_devicenet_msg_header` / `pc_devicenet_frag_octet` build the explicit-message header and fragmentation octets, `pc_devicenet_build_explicit` emits a single-frame explicit message, `pc_devicenet_build_fragment` emits one fragment of a longer message (the sender complement: header + fragmentation octet + up to 6 data octets, split as first / middle / last), and `pc_devicenet_frag_feed` reassembles a fragmented body (first / middle / last, modulo-64 count) up to `PC_DEVICENET_MSG_MAX` octets. The CIP application layer (services / EPATH / data) is the same one EtherNet/IP uses, so build the message body with the existing cip codec (`PC_ENABLE_CIP`). Identifier allocation + fragmentation verified against the ODVA DeviceNet spec; pure and host-tested. Drive it from the ESP32 TWAI peripheral or an MCP2515 over SPI to bridge a DeviceNet segment onto Wi-Fi. See src/services/fieldbus/devicenet/devicenet.h.
DF1PC_ENABLE_DF1
Allen-Bradley DF1 full-duplex frame codec. Default off. services/fieldbus/df1 is a zero-heap framing + DLE byte-stuffing + BCC/CRC codec for the Rockwell serial PLC data-link layer (pub. 1770-6.5.16): `pc_df1_build_frame` wraps application data in `DLE STX ... DLE ETX` (a data byte equal to DLE/0x10 is doubled) with either a BCC (the 2's complement of the modulo-256 data sum) or a CRC-16/ARC (poly 0x8005, init 0, over the data plus the ETX, sent low byte first), and `pc_df1_parse_frame` validates the check and un-stuffs the data. Vectors verified against the manual (BCC 0x20->0xE0, CRC "123456789"->0xBB3D). Pure and host-tested; the PCCC application header lives inside the data. See src/services/fieldbus/df1/df1.h.
DirectNETPC_ENABLE_DIRECTNET
Opt-in AutomationDirect / Koyo DirectNET serial frame codec. When set, services/fieldbus/directnet builds/validates the DirectNET master-slave serial frames - the header (SOH + slave/type/address/blocks ASCII-hex + ETB + LRC) and the data frame (STX + data + ETX + LRC) - for V-memory read/write on an AutomationDirect DirectLOGIC PLC. Pure codec (the UART transport + ACK/NAK handshake are the device step). Default off.
DMX512PC_ENABLE_DMX
DMX512 + RDM (ANSI E1.20) lighting codec. Default off. services/peripherals/dmx covers stage / architectural lighting over RS-485: `pc_dmx_build` and `pc_dmx_get_channel` assemble and read the positional DMX512 slot packet (a start code, 0x00 for dimmer data, followed by up to 512 channel slots - no checksum or in-frame addressing), and the RDM (Remote Device Management) functions build / parse the addressed management packet that shares the wire: `pc_rdm_build` / `pc_rdm_parse` carry 48-bit source / destination UIDs (`pc_rdm_uid`), a transaction number, a command class (GET / SET / DISCOVERY) + parameter id, optional parameter data, and the 16-bit additive checksum (`pc_rdm_checksum`), with named command-class / response-type / PID constants. `pc_rdm_decode_disc_response` decodes the DISC_UNIQUE_BRANCH discovery reply - which is not a normal RDM packet but a 0xFE preamble + 0xAA separator followed by the 6 UID octets and the checksum each sent as two copies OR'd with 0xAA / 0x55 - recovering each octet as the AND of its two copies and verifying the checksum, so the controller learns a responder's UID during device discovery; `pc_rdm_build_disc_response` frames that same reply from a responder's UID (the encoding complement), so a device on the bus answers a discovery branch. `pc_rdm_build_device_info` / `pc_rdm_parse_device_info` pack and decode the DEVICE_INFO (PID 0x0060) parameter block - the 19-octet big-endian descriptor every RDM responder must answer (RDM protocol version, device model, product category, software version, DMX footprint / personality / start address, and sub-device and sensor counts) - so a controller identifies and patches a device without hand-packing the E1.20 Table A-15 fields. RDM packet layout + checksum + discovery encoding + the DEVICE_INFO field order verified against ANSI E1.20 (cross-checked with the Wireshark RDM dissector); pure and host-tested. Drive a MAX485-class transceiver on a UART (250 kbit/s, 8N2; the break is the application's) and bridge a lighting rig onto Wi-Fi. See src/services/peripherals/dmx/dmx.h.
EtherNet/IPPC_ENABLE_ENIP
EtherNet/IP encapsulation codec - the ODVA EtherNet/IP transport (TCP/UDP 44818) that carries CIP. Default off. services/fieldbus/enip provides `pc_eip_build` / `pc_eip_parse` for the 24-octet encapsulation header (little-endian command / length / session handle / status / sender context / options), `pc_eip_build_register_session` to open a session and `pc_eip_build_unregister_session` to close it (the session handle in the header, no command data), `pc_eip_build_send_rr_data` / `pc_eip_parse_send_rr_data` to wrap + unwrap a CIP message as an unconnected message via the Common Packet Format (a Null Address item plus an Unconnected Data item), and `pc_eip_build_list_identity` / `pc_eip_parse_list_identity` for device discovery (the header-only ListIdentity request an originator broadcasts, and a decoder for the List Identity response item that lifts out the device's vendor id / device type / product code / revision / status / serial number / product name / state - the 16-octet CIP socket address, whose fields are network-order unlike the little-endian encapsulation, is skipped rather than reinterpreted). Commands and CPF item types verified against the Wireshark ENIP dissector; pure and host-tested. The CIP object model inside the Unconnected Data item layers on top. See src/services/fieldbus/enip/enip.h.
FINSPC_ENABLE_FINS
Omron FINS frame codec. Default off. services/fieldbus/fins is a zero-heap command/response builder + parser for the Factory Interface Network Service (FINS/UDP): `pc_fins_build_command`, `pc_fins_build_memory_area_read`, and `pc_fins_build_memory_area_write` emit the 10-octet routing header (ICF/RSV/GCT, destination + source net/node/unit, SID) plus the MRC/SRC command code and parameters (the write command appends its data words after the same area / address / count parameters as the read), `pc_fins_build_run` / `pc_fins_build_stop` frame the RUN (0401, into MONITOR or RUN mode) and STOP (0402, into PROGRAM mode) operating-mode commands so a device can start / stop the PLC, and `pc_fins_parse_command` / `pc_fins_parse_response` read them back (the response MRES/SRES end code included). Talks to an Omron PLC over the shipped UDP transport (pc_udp_sendto). Header layout verified against the FINS spec; pure and host-tested. See src/services/fieldbus/fins/fins.h.
HARTPC_ENABLE_HART
Opt-in HART / HART-IP process-instrument protocol codec. When set, services/fieldbus/hart provides the HART command-frame codec (build/parse with the longitudinal XOR checksum, short + long addressing) and the 8-octet HART-IP message header (both directions: `pc_hartip_build_header` frames a request/response/publish, and `pc_hartip_parse_header` decodes a received message - version / type / id / status / sequence / byte-count - and exposes its payload slice, e.g. the token-passing PDU, rejecting a truncated message whose declared length exceeds the buffer), so a device speaks HART over UDP/TCP 5094 (front-end-free) or, with a HART FSK modem, over the 4-20 mA loop. Pure, host-tested. Default off.
Host LinkPC_ENABLE_HOSTLINK
Omron Host Link (C-mode) frame codec. Default off. services/fieldbus/hostlink is a zero-heap ASCII command/response codec for Omron's serial host-link protocol (the RS-232/485 sibling of FINS): `pc_hostlink_build` emits `@UU` + 2-char header code + text + FCS + `*`CR, and `pc_hostlink_parse` FCS-validates and splits a frame (`pc_hostlink_end_code` reads a response's end code). For the most common operations, `pc_hostlink_build_read` frames an RD (DM-area read) command from a beginning word address + word count (formatting the two 4-digit decimal fields), and `pc_hostlink_read_word` extracts a word value (4 hex digits after the end code) from the RD response - so an app reads a PLC register without hand-formatting the ASCII; `pc_hostlink_build_write` frames the complementary WR (DM-area write) command (a 4-digit beginning word address followed by one 4-hex-char value per word), and its response is just an end code (read via `pc_hostlink_end_code`). The FCS is the 8-bit XOR from `@` through the text, rendered as two hex digits (verified against the `@00RD00000010` -> `57` example). Pure and host-tested; the UART transport is the application's. See src/services/fieldbus/hostlink/hostlink.h.
INTERBUSPC_ENABLE_INTERBUS
Opt-in INTERBUS summation-frame fieldbus codec. When set, services/fieldbus/interbus assembles/disassembles the INTERBUS summation frame (loopback word + per-device 16-bit process-image slices + CRC-16/CCITT FCS) of the Phoenix Contact ring fieldbus, where every device is a shift-register slice of one circulating frame. Pure codec (the physical ring clocking is hardware-gated). Default off.
IO-LinkPC_ENABLE_IOLINK
IO-Link (SDCI, IEC 61131-9) data-link message codec. Default off. services/fieldbus/iolink implements the point-to-point smart-sensor link's data-link message layer: `pc_iol_mc` (with decoders) builds the M-sequence Control octet (read/write, communication channel, address), `pc_iol_ckt` builds a master message's checksum / M-sequence-type octet, `pc_iol_cks` builds a device reply's checksum / status octet (Event + PD-valid flags), and `pc_iol_checksum6` / `pc_iol_finalize` / `pc_iol_verify` implement the SDCI message checksum straight from the IO-Link Interface and System Specification v1.1.4 Annex A.1.6 (a 0x52 seed XORed octet by octet with the check octet's checksum bits zeroed, then the 8-to-6-bit compression of equation A.1). The checksum is verified against a vector hand-derived from the spec formula. Lay the per-type M-sequence and ISDU octets out per your device profile, then finalize / verify with this codec. Pure and host-tested. The wire is a UART through an IO-Link transceiver (MAX14819 / L6360 class); bridge sensor data onto Wi-Fi. See src/services/fieldbus/iolink/iolink.h.
LonWorksPC_ENABLE_LONWORKS
Opt-in LonWorks / LON-IP (ISO/IEC 14908) network-variable codec. When set, services/fieldbus/lonworks builds/parses the LonTalk network-variable PDU ([msg-code][14-bit selector][value]) that a building-automation device exchanges - over LON/IP (14908-4) UDP, so no Neuron chip is needed - plus the common SNVT scalar encodings (SNVT_temp, SNVT_switch). Pure codec (the UDP transport is the shipped UDP layer). Default off.
MELSECPC_ENABLE_MELSEC
Mitsubishi MELSEC MC protocol (binary 3E frame) codec. Default off. services/fieldbus/melsec builds + parses the QnA-compatible binary 3E frames for MELSEC PLCs over TCP/UDP: `pc_melsec_build_read` emits a batch-read (word units) request (little-endian fields, request subheader 0x5000, command 0x0401, a device code - D 0xA8 / M 0x90 / X / Y / R / ... - plus a 24-bit head device number and a point count), `pc_melsec_build_write` emits the symmetric batch-write (command 0x1401) with the same device / head / point parameters followed by the write data words (two little-endian octets per point, the request-length field extended to cover them), and `pc_melsec_parse_response` validates the 0xD000 response and reports the end code (0x0000 = success) and the read data. Frame layout + device codes verified against a third-party MC implementation; pure and host-tested. Completes the major-vendor PLC read set alongside FINS / Host Link (Omron), DF1 (Allen-Bradley), and S7comm (Siemens). See src/services/fieldbus/melsec/melsec.h.
ModbusPC_ENABLE_MODBUS
Modbus TCP slave/server (Modbus Application Protocol v1.1b3) on TCP/502. Default off. When set, listen(502, PROTO_MODBUS) serves a fixed data model (coils, discrete inputs, holding + input registers, all in BSS) over Modbus TCP: Read/Write Coils (FC 1/5/15), Read Discrete Inputs (FC 2), Read/Write Holding Registers (FC 3/6/16), and Read Input Registers (FC 4). The codec (MBAP framing + PDU dispatch) is pure and host-tested; the TCP transport is ESP32-only. The application reads/writes the model with the accessor functions and is notified of client writes via pc_modbus_on_write(). Modbus has no authentication or encryption - run it only on a trusted control network.
Modbus MasterPC_ENABLE_MODBUS_MASTER
Opt-in Modbus master codec + register scanner. Default off. services/fieldbus/modbus/pc_modbus_master builds Modbus TCP read-request ADUs and parses the responses (register values or exception), so an app can poll / auto-discover a slave's registers. Pure and host-tested as a full round-trip against the slave codec (pc_modbus_process_adu); the actual send is the app's TCP.
Modbus PlusPC_ENABLE_MBPLUS
Opt-in Modbus Plus HDLC token-bus frame codec. When set, services/fieldbus/mbplus builds/validates the Modbus Plus HDLC frame (7E addr ctrl payload CRC-16/X-25 7E) that Schneider's token-passing peer bus exchanges, plus the token-rotation helper (next station in the logical ring). Reuses the shipped Modbus PDU model for the data. Pure codec (the 1 Mbit/s bus is hardware-gated). Default off.
Modbus RTUPC_ENABLE_MODBUS_RTU
Modbus RTU framing (serial / RS-485). Default off; implies MODBUS. Adds the RTU ADU codec `pc_modbus_rtu_process_adu()` - a `[slave addr][PDU][CRC16]` frame (CRC16-Modbus, little-endian) wrapped around the existing host-tested PDU dispatch: a CRC mismatch or a non-matching unit address is dropped silently (no reply, per the spec), and a broadcast (address 0) is processed with no reply. Pure and host-tested; feed it from a UART/RS-485 driver (the serial transport, framed by the 3.5-character inter-frame idle, is the application's). See src/services/fieldbus/modbus/modbus.h.
POWERLINKPC_ENABLE_POWERLINK
Opt-in Ethernet POWERLINK (EPSG) basic frame codec. When set, services/fieldbus/powerlink builds/parses the EPL basic frames ([messageType][dest][source][payload]) of the isochronous managed-node cycle - SoC (start of cycle), PReq (poll request), PRes (poll response with process data), SoA (start of async, `pc_epl_soa`), and ASnd (asynchronous send, `pc_epl_asnd`, unicast or broadcast) - over raw L2 (ethertype 0x88AB, on the shipped services/fieldbus/rawl2). Pure codec (the raw-L2 transmit + isochronous timing are the device step). Default off.
PROFIBUSPC_ENABLE_PROFIBUS
Opt-in PROFIBUS-DP FDL telegram codec. When set, services/fieldbus/profibus builds/validates the PROFIBUS-DP FDL telegrams - SD1 (no-data: SD1 DA SA FC FCS ED), SD2 (variable-data: SD2 LE LEr SD2 DA SA FC data FCS ED, arithmetic-sum FCS), and SD3 (fixed 8-octet data: SD3 DA SA FC data[8] FCS ED) - a Siemens DP master exchanges with slaves over RS-485 (the DP-V0 cyclic I/O exchange). Pure codec (the RS-485 timing + DP state machine are the device step). Default off.
PROFINETPC_ENABLE_PROFINET
Opt-in PROFINET DCP (Discovery and Configuration Protocol) frame codec. When set, services/fieldbus/profinet builds/parses the DCP frames (10-octet header + option/suboption blocks) PROFINET uses to discover and name IO-Devices over raw L2 (ethertype 0x8892) - Identify request/ response and Set (assign NameOfStation / IP). Pure codec (the raw-L2 transmit via services/fieldbus/rawl2 + esp_eth is the device step). Default off.
S7commPC_ENABLE_S7COMM
Siemens S7comm PDU codec. Default off. services/fieldbus/s7comm builds + parses the S7-300/400 communication PDUs carried inside a COTP Data TPDU (PC_ENABLE_COTP) over ISO-on-TCP (port 102): `pc_s7_build_setup` (Setup Communication), `pc_s7_build_read_request` (Read Var with S7-ANY items over the DB / I / Q / M areas, encoding the byte address as a 24-bit bit-address), `pc_s7_build_write_request` (Write Var - the same S7-ANY item specs plus a data section carrying, per item, a return code + data transport size + the value bytes, applying the same length-in-bits rule and even-item padding as the response reader), `pc_s7_parse_header`, and `pc_s7_read_next_item` which walks the response data items honoring the length-in-bits transport sizes (BIT/BYTE/INT) and the even-item padding. All constants (protocol id 0x32, ROSCTR, function/area/transport codes) are verified against the Wireshark S7comm dissector. Pure and host-tested; wrap the PDU with `pc_cotp_build_dt` + `pc_tpkt_build`. See src/services/fieldbus/s7comm/s7comm.h.
SDI-12PC_ENABLE_SDI12
SDI-12 sensor-bus codec. Default off. services/peripherals/sdi12 is a zero-heap command / response codec for the 1200-baud single-wire ASCII bus used by environmental / agricultural sensors (soil moisture, water level, weather). `pc_sdi12_build` and the `pc_sdi12_build_measure` / `_measure_additional` / `_concurrent` / `_concurrent_additional` / `_continuous` / `_verify` / `_data` / `_identify` / `_ack` / `_change_address` / `_query_address` helpers emit the standard `<addr><command>!` requests (`_measure_additional` / `_concurrent_additional` frame the `aM<n>!` / `aC<n>!` secondary measurement sets (1..9) a multi-parameter sensor exposes beyond the primary `aM!` / `aC!`, `_continuous` frames the `aR<n>!` / `aRC<n>!` continuous-measurement command whose sensor returns values with no service-request delay, and `_verify` the `aV!` start-verification command); `pc_sdi12_parse_measure` reads the `atttn` measurement response (seconds-until-ready + value count, both the 1-digit `aM!` and 2-digit `aC!` forms); `pc_sdi12_parse_values` splits a data response into floats; `pc_sdi12_parse_identify` decodes the `aI!` identify response into an `Sdi12Identity` (the fixed-width SDI-12 version, vendor, model, and sensor-version fields), so a bus scan reports what each sensor is; and `pc_sdi12_crc16` / `pc_sdi12_crc_encode` / `pc_sdi12_check_crc` implement the SDI-12 CRC (poly 0xA001, encoded as 3 printable octets) for the CRC-protected `aMC!` / `aCC!` variants. Command set + CRC verified against the SDI-12 specification; pure and host-tested. Drive the single 1200-baud line over a UART and bridge sensor readings onto Wi-Fi. See src/services/peripherals/sdi12/sdi12.h.
SERCOS IIIPC_ENABLE_SERCOS
Opt-in SERCOS III motion-bus telegram codec. When set, services/fieldbus/sercos builds/parses the SERCOS III MDT/AT telegrams (type + phase + cycle + cyclic device data) the real-time drive/motion bus exchanges over raw L2 (ethertype 0x88CD, on the shipped services/fieldbus/rawl2), plus the IDN (IDentification Number) encode/decode for drive-parameter addressing. Pure codec (the isochronous timing + ring topology are hardware-gated). Default off.
SNPPC_ENABLE_SNP
Opt-in GE Fanuc SNP (Series Ninety Protocol) serial frame codec. When set, services/fieldbus/snp builds/validates the SNP master-slave serial frame ([control][length][data] [arithmetic-sum BCC]) for reading/writing registers on a GE Fanuc Series 90 (90-30/90-70) PLC over RS-485. Pure codec (the UART transport + SNP-X session are the device step). Default off.

SCADA, Energy & Monitoring10

C37.118PC_ENABLE_C37118
IEEE C37.118.2 synchrophasor frame codec. Default off. services/energy/c37118 is a zero-heap builder + CRC-validating parser for the PMU / PDC wide-area measurement wire protocol: `pc_c37118_build_frame` emits a `SYNC FRAMESIZE IDCODE SOC FRACSEC DATA CHK` frame (CHK = CRC-CCITT) for any payload, `pc_c37118_build_command` handles the fixed Command frame, and `pc_c37118_parse_frame` validates the CRC and reports the frame type / id / timestamp / payload slice (with `pc_c37118_parse_command`). `pc_c37118_decode_stat` decodes the 16-bit STAT word that opens every data frame (IEEE C37.118.2-2011 Table 6) into a `C37118Stat` - the data-valid and PMU-error flags, the UTC sync / data-sorting / trigger / config-change / data-modified bits, the PMU time quality, the unlocked-time code, and the trigger reason (with named constants) - so a consumer knows whether a measurement is trustworthy and in sync before using it. CRC verified against the canonical CRC-CCITT-FALSE check value; pure and host-tested. The fixed phasor configuration / data model layered on the framing is a future addition. See src/services/energy/c37118/c37118.h.
DNP3PC_ENABLE_DNP3
DNP3 (IEEE 1815) data-link frame codec. Default off. services/energy/dnp3 is a zero-heap builder + CRC-validating parser for the SCADA / utility outstation data-link layer: `pc_dnp3_build_frame` emits the `0x0564 LEN CTRL DEST SRC CRC` header block plus the CRC'd 16-octet user-data blocks, and `pc_dnp3_parse_frame` validates the header and every block CRC (CRC-16/DNP, verified against the canonical 0xEA82 check value) and de-blocks the user data. Addresses are little-endian. On top of the link layer, the **transport function** (IEEE 1815 §8.2) reassembles application fragments: `pc_dnp3_build_transport_segment` prepends the 1-octet transport header (FIR / FIN flags + a 6-bit sequence) to up to 249 application octets, and `Dnp3TransportRx` + `pc_dnp3_transport_feed` accumulate a fragment across segments, starting on a FIR, checking the sequence increments, finishing on a FIN, and rejecting an out-of-sequence segment or a buffer overflow. On top of that, the **application layer** (IEEE 1815 §4.2.2) frames the reassembled fragment's header: `pc_dnp3_app_control` composes the Application Control octet (FIR / FIN / CON / UNS + a 4-bit sequence), `pc_dnp3_build_app_request` emits an Application Control + function code + object octets, `pc_dnp3_build_app_response` inserts the two little-endian Internal Indication (IIN) octets a response carries, and `pc_dnp3_parse_app_header` decodes a fragment header into a `Dnp3AppHeader` (the control flags + sequence, the function code, and - for a RESPONSE / UNSOLICITED_RESPONSE - the 16-bit IIN, with a pointer to the object octets that follow), with named constants for the function codes and every IIN bit. `pc_dnp3_parse_object_header` then decodes an object header (IEEE 1815 §4.3) into a `Dnp3ObjectHeader` - the object group + variation, the qualifier (its index-size / prefix code and range specifier code split out), and the range: the start / stop indexes of a start-stop range (1/2/4-octet forms) or the object count of a count range (1/2/4-octet), the all-objects no-range form, with a pointer to the object data that follows - so a consumer knows which points a request or response references. For the request side, `pc_dnp3_build_object_header_range` frames a start-stop object header, automatically picking the smallest of the 1/2/4-octet range forms that holds the stop index, and `pc_dnp3_build_object_header_all` frames the all-objects (qualifier 0x06) header - so a master builds, say, a Class-0 poll or a contiguous point-range read without hand-assembling the qualifier. `pc_dnp3_build_crob` encodes a Control Relay Output Block (group 12 variation 1) - the 11-octet control object a SELECT / OPERATE carries to command a binary output: the control code (operation type - pulse/latch on/off - the clear bit, and the trip/close code), the count, the on / off time in milliseconds (little-endian), and the status octet - so a master builds a relay control without hand-packing the control-code bits. `pc_dnp3_build_aob32` and `pc_dnp3_build_aob_float` encode the analog counterpart, an Analog Output Block (group 41 variation 1 for a signed 32-bit value, variation 3 for a 32-bit float) - the setpoint object a SELECT / OPERATE carries to command an analog output: the value (little-endian) followed by a status octet - so a master commands an analog point (a valve position, a control loop setpoint) without hand-packing the object. Pure codec, host-tested. See src/services/energy/dnp3/dnp3.h.
GOOSEPC_ENABLE_GOOSE
Opt-in IEC 61850 GOOSE publisher + subscriber codec. When set, services/energy/goose builds the BER-encoded IECGoosePdu (gocbRef / timeAllowedToLive / datSet / goID / t / stNum / sqNum / simulation / confRev / ndsCom / numDatSetEntries / allData) and wraps it in the 8-octet GOOSE header + Ethernet frame (ethertype 0x88B8) for the fast raw-L2 substation-event publish. On the receive side, `pc_goose_parse_frame` subscribes: it validates the ethertype, walks the BER definite-length TLVs of the IECGoosePdu, and lifts every field into a `pc_goose_rx` (the numeric stNum / sqNum / timeAllowedToLive / confRev / numDatSetEntries, the booleans, the borrowed gocbRef / datSet / goID strings, the 8-octet UtcTime, and the allData slice), skipping unknown/future tags - so a device reacts to a protection trip, not just publishes one. Pure codec (allData is a caller-encoded BER blob; the raw-L2 transmit / receive is the device step). Default off.
ICCPPC_ENABLE_ICCP
Opt-in ICCP / TASE.2 (IEC 60870-6) inter-control-center telemetry codec. When set, services/energy/iccp builds the TASE.2 Data_Value BER structures - StateQ (a discrete state + quality) and RealQ (a scaled real + quality), each with an optional timestamp - the indication points a control center transfers as MMS Reads (on the shipped services/energy/mms + services/fieldbus/cotp). Pure BER codec. Default off.
IEC 60870PC_ENABLE_IEC60870
IEC 60870-5-101 / -104 telecontrol (SCADA) codec. Default off. services/energy/iec60870 covers the utility-SCADA protocol in both transports: the -104 APCI over TCP (`pc_iec104_build_i` / `_s` / `_u` and `pc_iec104_parse` for the I / S / U formats - numbered information transfer with 15-bit send/receive sequence numbers, the supervisory acknowledge, and the STARTDT / STOPDT / TESTFR unnumbered commands), the shared ASDU header (`pc_iec_asdu_build_header` / `pc_iec_asdu_parse_header` - type id, variable structure qualifier, cause of transmission, common address) with the 3-octet Information Object Address (`pc_iec_put_ioa` / `pc_iec_get_ioa`), and the -101 FT1.2 serial link frames (`pc_iec101_build_fixed` / `_variable` and `pc_iec101_parse`, 8-bit sum checksum). Named type-id and cause-of-transmission constants are provided, plus typed information objects for the common type ids: `pc_iec_io_build_sp` / `_parse_sp` (single-point M_SP_NA_1 - IOA + SPI + quality), `pc_iec_io_build_float` / `_parse_float` (short-float measured value M_ME_NC_1 - IOA + IEEE-754 float + QDS), `pc_iec_io_build_scaled` / `_parse_scaled` (scaled measured value M_ME_NB_1 - IOA + a signed 16-bit value + QDS, the integer-range companion to the short float), `pc_iec_io_build_normalized` / `_parse_normalized` (normalized measured value M_ME_NA_1 - IOA + a signed 16-bit NVA + QDS, exposed as the fraction NVA/32768 in [-1, +1), the fixed-point companion completing the measured-value trio), `pc_iec_io_build_counter` / `_parse_counter` (integrated totals M_IT_NA_1 - IOA + a Binary Counter Reading: a signed 32-bit counter value followed by a sequence-notation octet carrying the 5-bit sequence number and the carry / counter-adjusted / invalid flags, the metering companion for energy and pulse totals), `pc_iec_io_build_sc` / `_parse_sc` (single command C_SC_NA_1 - IOA + SCS + select/execute), `pc_iec_io_build_dp` / `_parse_dp` (double-point information M_DP_NA_1 - IOA + the 2-bit DPI value + quality), and `pc_iec_io_build_dc` / `_parse_dc` (double command C_DC_NA_1 - IOA + the 2-bit DCS state + the 5-bit qualifier + select/execute); the remaining per-type elements are the application's. Frame + APCI + ASDU layout verified against IEC 60870-5-101/-104, and the information objects round-trip host-tested; pure. Run -104 over the shipped TCP stack or -101 over a UART / RS-485 transceiver to bridge an RTU or outstation onto Wi-Fi. See src/services/energy/iec60870/iec60870.h.
M-BusPC_ENABLE_MBUS
Wired M-Bus (Meter-Bus, EN 13757) frame codec. Default off. services/fieldbus/mbus is a zero-heap builder + parser for the M-Bus link-layer frames used by utility meters (water / gas / heat / electricity): `pc_mbus_build_ack` (the single-character 0xE5), `pc_mbus_build_short` (`10 C A CS 16`), and `pc_mbus_build_long` (`68 L L 68 C A CI ... CS 16`, with convenience `pc_mbus_build_snd_nke` / `pc_mbus_build_req_ud2` / `pc_mbus_build_req_ud1`, the last requesting class-1 alarm data where REQ_UD2 requests routine class-2 data), plus `pc_mbus_parse` which validates the start / stop octets, the doubled length, and the 8-bit sum checksum. `pc_mbus_parse_var_header` decodes the 12-octet fixed header that opens a variable-data response (CI 0x72) into an `MbusVarHeader` - the meter's identification serial (from the 4-octet BCD), the 3-letter manufacturer code (unpacked from the 2-octet FLAG value), the version, the medium / device type (named `MBUS_MEDIUM_*` codes), and the access number / status / signature - so an app identifies the meter before reading its records. `pc_mbus_record_next` walks the EN 13757-3 variable-data records that follow, skipping the DIFE / VIFE extension chains and decoding the data length from the DIF coding (incl. the LVAR variable form). Each record's value then decodes into a usable number and unit: `pc_mbus_record_value_int` reads the integer (little-endian, sign-extended) and BCD (with the 0xF negative-sign nibble) codings into an int64, `pc_mbus_record_value_real` reads the REAL32 coding into a float, and `pc_mbus_vif_decode` maps a VIF octet to its physical unit (Wh / J / m³ / kg / W / m³·h⁻¹ / °C / K / bar) and the base-10 exponent to apply, so an app gets, say, 12345 × 10⁻³ m³. Frame formats + checksum verified against EN 13757-2, and the value / VIF decoding against worked examples; pure and host-tested. Talk to the powered two-wire bus over a UART through an M-Bus level converter (a TSS721-based master) and bridge meter readings onto Wi-Fi. See src/services/fieldbus/mbus/mbus.h.
MMSPC_ENABLE_MMS
Opt-in IEC 61850 MMS PDU codec. When set, services/energy/mms builds/parses the MMS (ISO 9506) confirmed-request/response Read PDUs (BER-encoded, the ACSI client/server core of IEC 61850) - pc_mms_read_request builds a Read of a named Data Object, pc_mms_read_response the data reply. Carried over ISO-on-TCP (TPKT + COTP via the shipped services/fieldbus/cotp) on port 102. Pure BER codec. Default off.
OpenADRPC_ENABLE_OPENADR
Opt-in OpenADR 3.0 (Automated Demand Response) JSON codec. When set, services/energy/openadr builds the OpenADR 3.0 event (a demand-response signal: programID + eventName + interval payload points) and report (a VEN reading back to the VTN) JSON objects into a caller buffer, over the existing HTTP client/server + OAuth2. Pure JSON framing. Default off.
SEP2PC_ENABLE_SEP2
Opt-in IEEE 2030.5 (Smart Energy Profile 2.0) resource codec. When set, services/energy/sep2 builds the core 2030.5 XML resource documents (DeviceCapability, EndDevice, DERControl) in the urn:ieee:std:2030.5:ns namespace, so the web server is a 2030.5 smart-grid server/client over the existing HTTP stack (DER dispatch / curtailment). Pure text framing. Default off.
SunSpecPC_ENABLE_SUNSPEC
SunSpec Modbus device-information-model codec. Default off. services/energy/sunspec is a zero-heap codec for the SunSpec Alliance register maps layered on the holding-register model: a model-chain walker (`pc_sunspec_check_marker` / `pc_sunspec_begin` / `pc_sunspec_next_model` - verify the `SunS` 0x53756E53 marker, then iterate each model's id / length / body to the 0xFFFF end model) plus typed point readers (`pc_sunspec_u16`, `pc_sunspec_i16`, `pc_sunspec_u32`, `pc_sunspec_i32`, `pc_sunspec_string`), and a map writer (`pc_sunspec_write_marker`, `pc_sunspec_write_model_header`, the point writers, `pc_sunspec_write_end_model`) for a device exposing its own map. Makes a solar inverter / meter / battery interoperable. Marker and header format verified against the SunSpec spec; pure and host-tested. Pairs with the Modbus service. See src/services/energy/sunspec/sunspec.h.

Machine Tools & OT5

DNC (CNC drip-feed)PC_ENABLE_DNC
CNC RS-232 DNC (Distributed Numerical Control) drip-feed codec. Default off. services/machine_tool/dnc is the transport-agnostic framing + tape-code layer that streams a G-code program (RS-274 / ISO 6983) to a machine-tool controller over RS-232 or a socket. It carries the two historical tape codes: ISO 7-bit / ASCII (RS-358; End-of-Block = LF, program marker = `%`, optional even parity in bit 7) and the EIA RS-244 punched-tape code (a distinct odd-parity 8-track encoding with parity in channel 5, End-of-Block = 0x80, the rewind-stop mapped to EIA End-of-Record 0x0B, uppercase-only). `pc_dnc_iso_to_eia` / `pc_dnc_eia_to_iso` translate a character in either direction (fail-closed on a non-representable character); `pc_dnc_encode_block` frames one G-code line (characters + End-of-Block), `pc_dnc_encode_marker` writes the `%` program start/end, and `pc_dnc_encode_leader` the NUL runout. `DncDecoder` is a byte-at-a-time reassembler that strips parity / translates, skips runout, reports the `%` program markers, and delivers each block as an ASCII line (fail-closed, dropping an over-long block whole rather than truncating). `DncFlow` tracks XON/XOFF (DC1/DC3) software flow control on the reverse channel so the send pump pauses when the controller's buffer fills - kept separate from the forward stream, since the EIA data byte for `3` is 0x13 (DC3). The EIA table is validated by an odd-parity + exact-inverse host guardrail, and full encode -> decode program round-trips pass for both codes. On top of the codec, **`dnc_stream`** (src/services/machine_tool/dnc/dnc_stream) is the drip-feed engine: it streams a whole program - leader, `%` start marker, one block per source line, `%` end marker, trailer - over a send/recv seam, pausing on a reverse-channel XOFF and waiting for XON before the next write. Because it is transport-agnostic (the seam is two function pointers), the same engine drip-feeds over a raw TCP socket ("Ethernet DNC") or a UART; it is host-tested end to end with a scripted mock controller that decodes the streamed program back to lines and exercises the XOFF/XON pause-resume path (`native_dnc`, 22 cases). Pure and host-tested; you own the UART / socket. See src/services/machine_tool/dnc/dnc.h and dnc_stream.h.
MTConnectPC_ENABLE_MTCONNECT
Opt-in MTConnect agent response codec. When set, services/machine_tool/mtconnect builds the MTConnectStreams (current/sample) and MTConnectError XML response documents (ANSI/MTC1.4) into a caller buffer - header with instanceId + nextSequence, then per-DataItem Samples/Events/Condition observations - so the web server is an MTConnect agent over the existing HTTP stack. Pure text framing (values XML-escaped). Default off.
OPC-UAPC_ENABLE_OPCUA
OPC UA Binary server. Default off. services/fieldbus/opcua provides an OPC UA (IEC 62541) Binary server: the little-endian built-in-type codec (incl. NodeId / ExtensionObject / DateTime / Variant / DataValue / ReferenceDescription), UA-TCP (UACP) message framing, the Hello/Acknowledge handshake (with a transport-level `ERR` message - `pc_opcua_build_error` - the server can return before closing when a Hello is malformed or a buffer size is unacceptable, carrying an OPC UA StatusCode + reason string), the SecureChannel (OpenSecureChannel, SecurityPolicy None), the Session (CreateSession + ActivateSession), GetEndpoints, the Read, Write and Browse services (registered resolvers map a NodeId to a value / accept a written value / list child references), plus CloseSession + CloseSecureChannel and a ServiceFault for unsupported services, served on TCP via PROTO_OPCUA (`listen(4840, PROTO_OPCUA)`). The MSG framing is spec-faithful (incl. SecureChannelId), so standard clients interoperate (verified with python asyncua: connect + browse + read + write/read-back). All pure and host-tested. No heap, no stdlib.
OPC-UA ClientPC_ENABLE_OPCUA_CLIENT
OPC UA Binary client. Default off (requires OPC-UA, shares the codec). services/fieldbus/opcua_client builds the client-side requests (Hello, OpenSecureChannel, CreateSession, ActivateSession, Read, Browse, CloseSession, CloseSecureChannel) and parses the server responses, reusing the opcua.h codec. Transport-agnostic - the app supplies the outbound socket (e.g. an Arduino WiFiClient). No heap, no stdlib.
umati (OPC UA for Machine Tools)PC_ENABLE_UMATI
umati / OPC UA for Machine Tools information model (OPC 40501-1). Default off (requires OPC-UA). services/machine_tool/umati exposes a fixed MachineTool node hierarchy - Identification, Monitoring (MachineTool / Channel / Spindle / Axis_X..Z), Production, and Notification - through the OPC UA Browse + Read resolvers, served out of a caller-owned UmatiMachineTool struct you refresh each loop. Faithful BrowseNames per OPC 40501-1 (namespace `http://opcfoundation.org/UA/MachineTool/`); a read-only monitoring model any umati / OPC UA client browses and reads by BrowseName. No heap, no stdlib.

Transportation & ITS10

ATCPC_ENABLE_ATC
Opt-in ATC (Advanced Traffic Controller) field-I/O interop snapshot. When set, services/machine_tool/atc exposes this device's field-I/O (a fixed table of named input/output points it already gathers via the NTCIP / NEMA-TS2 / gpio services) to an ATC Linux engine over the existing HTTP surface: pc_atc_snapshot_json serializes the FIO map as JSON, and pc_atc_set_output drives an output point from an ATC command. Pure interop codec (ATC is a platform spec, not a wire protocol). Default off.
J1939PC_ENABLE_J1939
SAE J1939 codec. Default off. services/fieldbus/j1939 is a zero-heap codec for the heavy-duty-vehicle / agriculture / marine / genset CAN higher-layer protocol over 29-bit extended frames (`shared_primitives/can.h`): `pc_j1939_encode_id` / `pc_j1939_decode_id` pack and unpack the priority / PGN / source / destination identifier (both the PDU1 peer-to-peer and PDU2 broadcast forms), `pc_j1939_build_message` emits single frames, `pc_j1939_build_request` and `pc_j1939_build_address_claim` (with `pc_j1939_build_name` for the 64-bit NAME) handle the Request PGN and Address Claimed messages, and the Transport Protocol (BAM announce + TP.DT data packets) reassembles multi-packet messages up to `PC_J1939_TP_MAX` octets via `pc_j1939_tp_feed`. Typed decoders lift the two canonical engine PGNs into engineering units (SAE J1939-71): `pc_j1939_decode_eec1` (EEC1 / PGN 61444 - engine speed at 0.125 rpm/bit and the driver's-demand / actual percent torque at the -125 offset) `pc_j1939_decode_et1` (ET1 / PGN 65262 - coolant, fuel, and oil temperature), `pc_j1939_decode_lfe` (LFE / PGN 65266 - engine fuel rate at 0.05 L/h/bit, instantaneous and average fuel economy at 1/512 km/L, and throttle position), `pc_j1939_decode_amb` (AMB / PGN 65269 - barometric pressure at 0.5 kPa/bit and the cab-interior / ambient-air / air-inlet / road-surface temperatures), `pc_j1939_decode_ic1` (IC1 / PGN 65270 - the particulate-trap-inlet / boost / air-inlet / air-filter / coolant-filter pressures, the intake-manifold temperature, and the exhaust gas temperature at 0.03125 degC/bit), `pc_j1939_decode_vd` (VD / PGN 65248 - the trip and total vehicle distance at 0.125 km/bit, held as double so a full 32-bit odometer keeps its precision), and `pc_j1939_decode_ccvs` (CCVS / PGN 65265 - the wheel-based vehicle speed at 1/256 km/h per bit plus the 2-bit cruise-control-active state; only the two signals with cross-source-verified bit positions are decoded, the many discrete switches in this PGN left to the caller), each applying the SPN scale + offset and clearing a validity flag for a raw in the not-available range. `pc_j1939_decode_dm1` decodes DM1 (PGN 65226 - Active Diagnostic Trouble Codes, J1939-73): the four lamp statuses (malfunction / red-stop / amber-warning / protect) and the list of active DTCs (each a suspect parameter number + failure mode identifier + occurrence count, unpacked by SPN conversion method 4), taking the raw body so it works on a single frame or a Transport-Protocol-reassembled buffer and skipping the all-zero "no active fault" placeholder. Identifier layout, NAME bit fields, and the TP control bytes verified against SAE J1939-21 / -81, and the EEC1/ET1 decoders against worked examples (1500 rpm, 90 degC coolant); pure and host-tested. Drive it from the ESP32 TWAI peripheral or an MCP2515 over SPI to bridge a J1939 bus onto Wi-Fi. See src/services/fieldbus/j1939/j1939.h.
J2735PC_ENABLE_J2735
Opt-in SAE J2735 V2X codec. When set, services/transportation/j2735 provides the ASN.1 UPER (Unaligned Packed Encoding Rules) bit-level primitive codec (constrained INTEGER / BOOLEAN / bit fields) and, on top of it, the J2735 BSMcore safety-message block (msgCnt / id / secMark / lat / long / elev / speed / heading) encode + decode, for connected- vehicle messaging. Pure codec (the DSRC / C-V2X radio is an external module). Default off.
NEMA TS2PC_ENABLE_NEMA_TS2
Opt-in NEMA TS 2 traffic-cabinet SDLC frame codec. When set, services/transportation/nema_ts2 builds/validates the TS 2 SDLC bus frames ([address][control][frame-type] [data][CRC-16/X-25]) that link a traffic-signal controller to the MMU, BIUs, and detector racks. Pure codec (the synchronous serial PHY + BIU timing are hardware-gated). Default off.
NMEA 0183PC_ENABLE_NMEA0183
NMEA 0183 sentence codec. Default off. services/timing_position/nmea0183 is a zero-heap codec for the marine / GPS ASCII protocol (sentences like `$GPGGA,123519,4807.038,N,...*47`): `pc_nmea0183_build` emits a sentence (adding the `$`, the XOR checksum, and CR/LF), `pc_nmea0183_checksum` computes the XOR check, `pc_nmea0183_parse` validates the `*HH` checksum and splits the comma-separated fields (deriving the talker id + sentence type from the address field), and `pc_nmea0183_field_float` / `pc_nmea0183_field_int` decode field values (the field substrings are delimited so `pc_strtof` / `pc_strtol` stop cleanly). Typed decoders for the two common GPS sentences lift a parsed sentence into a position struct: `pc_nmea0183_parse_gga` (fix data - time, ddmm.mmmm latitude/longitude converted to signed decimal degrees, fix quality, satellite count, HDOP, MSL altitude) and `pc_nmea0183_parse_rmc` (recommended minimum - the A/V validity, time + date, position, speed over ground in knots, and course). `pc_nmea0183_parse_gsv` decodes the variable-length GSV (satellites in view): the sentence count / index / total-in-view header and up to four per-satellite records (PRN, elevation, azimuth, and SNR with a valid flag that a blank field - a satellite in view but not tracked - clears). `pc_nmea0183_parse_zda` decodes ZDA (time + date): the UTC time, the calendar day / month and - unlike RMC - the full four-digit year, plus the optional local-zone hours / minutes offset (which reads back 0 when the receiver leaves it blank), so ZDA is the sentence to read for wall-clock time sync. `pc_nmea0183_parse_vtg` decodes VTG (track made good + ground speed): the course over ground in degrees true and magnetic, the speed over ground in both knots and km/h, and the optional NMEA 2.3+ mode indicator (which reads back `\0` on an older receiver) - the sentence to read for heading and speed. `pc_nmea0183_parse_gsa` decodes GSA (DOP + active satellites): the manual/automatic selection mode, the 2D/3D fix type, the list of satellite PRNs actually used in the solution (blank slots skipped), and all three dilution-of-precision values (PDOP / HDOP / VDOP) - the fix-geometry companion to GSV's satellites-in-view. `pc_nmea0183_parse_mwv` decodes MWV (wind speed + angle): the wind angle, the relative/true reference, the wind speed with its units (km/h / m/s / knots), and the A/V status - the standard wind-instrument sentence, the ASCII companion to the NMEA 2000 wind PGN. `pc_nmea0183_parse_dpt` decodes DPT (depth of water): the depth relative to the transducer, the transducer offset (positive to the waterline, negative to the keel, so the app can report depth below surface or below keel), and the optional maximum-range-scale field - the ASCII companion to the NMEA 2000 water-depth PGN. `pc_nmea0183_parse_hdg` decodes HDG (heading + deviation + variation): the magnetic sensor heading plus the deviation and variation (each folded to a signed value, East positive / West negative, so true heading = heading + deviation + variation) - the compass sentence, distinct from VTG's GPS course over ground. `pc_nmea0183_parse_gll` decodes GLL (geographic position - latitude / longitude): the position converted to signed decimal degrees, the UTC time, the A/V validity, and the optional NMEA 2.3+ FAA mode indicator (which reads back `\0` on an older receiver) - the minimal position report, where GGA adds fix quality and RMC adds velocity. `pc_nmea0183_parse_vhw` decodes VHW (water speed + heading): the vessel's heading in degrees true and magnetic and its speed through the water in both knots and km/h - the log / paddlewheel sentence, distinct from VTG's GPS speed over ground (the difference between the two is the current / leeway). `pc_nmea0183_parse_vlw` decodes VLW (distance traveled through the water): the total cumulative and the trip (since-reset) water distance in nautical miles - the marine odometer, the through-water distance companion to VHW's speed. Sentence framing + checksum verified against the NMEA 0183 standard (the canonical GGA example checks to 0x47), and the GGA/RMC decoders against the textbook 48.1173 N / 11.5167 E example; pure and host-tested. GPS / marine receivers are cheap UART breakouts, so this is a plain HardwareSerial link; bridge position / wind / depth data onto Wi-Fi. See src/services/timing_position/nmea0183/nmea0183.h.
NMEA 2000PC_ENABLE_NMEA2000
NMEA 2000 codec. Default off; implies J1939 (NMEA 2000 is J1939 at the transport layer). services/timing_position/nmea2000 is a zero-heap codec for the marine instrumentation network over CAN: it reuses the J1939 29-bit identifier codec and adds the NMEA-specific Fast Packet transport. `pc_n2k_fastpacket_num_frames` sizes a transfer, `pc_n2k_fastpacket_build_frame` emits frame N of a 9..223-octet message (a control octet of sequence counter + frame counter, the first frame carrying the total length and 6 data octets, continuations carrying 7), and `pc_n2k_fastpacket_feed` reassembles a sequence (matching source / PGN / sequence counter, rejecting out-of-order frames and ignoring interleaved sequences); `pc_n2k_build_single` wraps a single-frame message. Typed decoders lift common single-frame PGNs into engineering units: `pc_n2k_decode_position_rapid` (PGN 129025 - latitude / longitude at 1e-7 deg/bit into decimal degrees), `pc_n2k_decode_cog_sog_rapid` (PGN 129026 - course over ground at 0.0001 rad, speed over ground at 0.01 m/s, and the true/magnetic course reference), `pc_n2k_decode_engine_rapid` (PGN 127488 - engine instance, engine speed at 0.25 rpm/bit, boost pressure at 100 Pa/bit, and the signed tilt/trim percent), `pc_n2k_decode_engine_dynamic` (PGN 127489, the Fast Packet engine-monitoring record decoded from the reassembled body - oil pressure / temperature, coolant temperature / pressure, alternator voltage, fuel rate, total engine hours, fuel pressure, the two discrete-status bitfields, and the signed engine load / torque percent), `pc_n2k_decode_wind_data` (PGN 130306 - wind speed at 0.01 m/s, angle at 0.0001 rad, and the reference), `pc_n2k_decode_water_depth` (PGN 128267 - depth below the transducer at 0.01 m and the transducer offset at 0.001 m), `pc_n2k_decode_vessel_heading` (PGN 127250 - heading / deviation / variation at 0.0001 rad and the true/magnetic reference), `pc_n2k_decode_rudder` (PGN 127245 - the rudder instance, the direction order, and the commanded angle + actual position at 0.0001 rad/bit), `pc_n2k_decode_attitude` (PGN 127257 - the vessel's yaw / pitch / roll, each signed at 0.0001 rad/bit), `pc_n2k_decode_speed` (PGN 128259 - the through-water and over-ground speed at 0.01 m/s and the water-speed sensor type), `pc_n2k_decode_temperature` (PGN 130312 - instance, source, and the actual / set temperatures, carried in Kelvin at 0.01 K/bit and exposed in Celsius), `pc_n2k_decode_battery_status` (PGN 127508 - a battery bank's instance, voltage at 0.01 V/bit, the signed current at 0.1 A/bit, and the temperature carried in Kelvin and exposed in Celsius), `pc_n2k_decode_fluid_level` (PGN 127505 - a tank's instance + fluid type packed in one octet, the fill level as a percentage at 0.004 %/bit, and the total capacity in litres at 0.1 L/bit), and `pc_n2k_decode_actual_pressure` (PGN 130314 - a measured pressure's instance, its source - atmospheric / water / oil / fuel / ... - and the value in pascals at 0.1 Pa/bit, signed), each clearing a validity flag for a not-available raw (0x7FFFFFFF for the signed position and pressure, 0x7F for the signed tilt/trim, all-ones for a course / speed / engine / wind / depth / heading field). Fast Packet framing verified against the NMEA 2000 / J1939 layout, and the PGN decoders against worked examples; pure and host-tested. Drive it from the ESP32 TWAI peripheral or an MCP2515 over SPI to bridge an NMEA 2000 backbone (GPS, wind, depth, engine PGNs) onto Wi-Fi. See src/services/timing_position/nmea2000/nmea2000.h.
NTCIPPC_ENABLE_NTCIP
Opt-in NTCIP transportation-device object identifiers. When set, services/transportation/ntcip provides the NTCIP (National Transportation Communications for ITS Protocol) object OID definitions for the common device classes - NTCIP 1202 (actuated signal controller: phases, timing, live states) and 1203 (dynamic message sign) - plus an OID builder, so an app exposes them via the shipped SNMP agent (services/net/snmp). Pure OID data. Default off.
OCITPC_ENABLE_OCIT
Opt-in OCIT-Outstations message codec. When set, services/transportation/ocit builds/parses the OCIT (DE/AT/CH road-traffic-control) object messages ([msg-type][object-type][instance][data-type][value]) between central traffic computers and field controllers / detectors, with typed values (bool / byte / u16 / u32 / octets). Pure codec (the OCIT transport is the shipped transport). Default off.
UTMCPC_ENABLE_UTMC
Opt-in UTMC (Urban Traffic Management and Control) common-database codec. When set, services/transportation/utmc builds/parses the UTMC common-database HTTP+XML messages - a UTMCRequest for an object id and a UTMCResponse carrying the object value + a data-quality flag + a timestamp - the UK modular framework for sharing traffic data across municipal systems, over the existing HTTP server. Pure text framing. Default off.
WAVEPC_ENABLE_WAVE
Opt-in IEEE 1609 WAVE (WSMP + 1609.2 envelope) codec. When set, services/transportation/wave builds/parses the IEEE 1609 vehicular-radio framing that carries J2735: the 1609.3 WSMP header (version + P-encoded PSID + length + payload) and the 1609.2 secured-message envelope header (version + content type). Pairs with services/j2735. Pure codec (the DSRC / C-V2X radio is an external module). Default off.

Clients & Gateways9

FTP clientPC_ENABLE_FTP
FTP client wire codec (RFC 959 + RFC 2428). Default off. services/file_transfer/ftp is the pure protocol layer of an FTP client so a device can push/pull files - e.g. drip a `.nc` program to a CNC controller's FTP program store (Fanuc / Haas / Mazak / Heidenhain all expose one), fetch a config, or archive a log. `pc_ftp_build_command` frames any control-channel command (`VERB` or `VERB arg` + CRLF - USER / PASS / TYPE / CWD / RETR / STOR / LIST / SIZE / DELE / PASV / EPSV / QUIT / ...); `pc_ftp_build_port` and `pc_ftp_build_eprt` build the active-mode data-address commands (RFC 2428 EPRT for IPv4 / IPv6). `pc_ftp_parse_reply` detects a complete reply at the head of the control buffer - single line `NNN<SP>text` or a multiline `NNN-...` block ended by the same code followed by a space - and reports the 3-digit code + the bytes consumed (so pipelined replies advance cleanly); `pc_ftp_parse_pasv` decodes the `227 ...(h1,h2,h3,h4,p1,p2)` passive address and `pc_ftp_parse_epsv` the `229 ...(|||port|)` extended-passive port. Reply / PASV / EPSV formats verified against authentic strings captured from a live FTP server; pure and host-tested (the control + data sockets are the application's). See src/services/file_transfer/ftp/ftp.h.
HTTP ClientPC_ENABLE_HTTP_CLIENT
Outbound HTTP(S) client (raw lwIP, optional client-side mbedTLS). Default off. When set, src/services/net/http_client/http_client.h can issue a blocking GET/POST to a remote server: it resolves the host (DNS), opens a raw lwIP TCP connection (https:// goes through client-side mbedTLS over the same static arena as the server TLS), sends the request, and returns the status + body in caller buffers. For webhooks, telemetry push, REST calls from the device. The request builder + response parser are host-testable; the transport is ESP32-only.
HTTP Client TLSPC_ENABLE_HTTP_CLIENT_TLS
HTTPS client support inside the HTTP client (needs TLS).
Relay (TCP forward / DNAT)PC_ENABLE_RELAY
TCP relay / DNAT port forwarding. Default off. services/net/relay publishes an internal `host:port` through the server: an inbound (accepted) connection is relayed to an origin (an outbound `pc_client` connection to the internal service), moving bytes in both directions so the device fronts a service that lives behind it. `pc_relay_step` is a pure, non-blocking byte pump over two send/recv seams: the app calls it each poll tick until it returns `PC_RELAY_DONE`, then closes both sockets. It handles **backpressure** (a `send` seam that accepts a partial write carries the rest in a per-direction buffer and retries before reading more) and **independent half-close** (each direction finishes when its source hits EOF and drains; the opposite peer's optional `shutdown` seam is then called once to propagate the FIN, and the relay is done only when both directions have finished). Zero heap - each active relay owns two `PC_RELAY_BUF` buffers. Transports that report a close out of band (an `on_close` event rather than a short read - e.g. the server's `pc_conn`) call `pc_relay_note_eof` to finish a direction cleanly. Host-tested with two mock sockets covering a bidirectional transfer, the backpressure carry, half-close with shutdown propagation, a large multi-step byte-exact transfer, a seam error, and the out-of-band EOF path (`native_relay`, 6 cases). The server-side listener (src/services/net/relay/relay_listener) wires it in: after `server.listen(port, PROTO_RELAY)` you call `pc_relay_publish(listener_id, origin_host, origin_port)`, and a `PROTO_RELAY` connection handler dials the origin through `pc_client` on each inbound accept, pumps `pc_relay_step` from the server's poll loop, and tears both sockets down on close - so the app publishes an internal service in two calls (a fixed static bind/bridge table, zero heap; opt-in twice - compiled out by default and inert until you publish). Verified on ESP32 via the `PortForward` example build. See src/services/net/relay/relay.h and relay_listener.h.
SMBPC_ENABLE_SMB
SMB2 client wire codec (MS-SMB2). Default off. network_drivers/application/smb is the pure protocol layer of an SMB2 client so a device can read/write files on a Windows share - e.g. a CNC controller's `.nc` program store (Fanuc / Haas / Mazak / Heidenhain expose one). Increment 1 (no crypto): `pc_smb2_transport_frame` / `pc_smb2_transport_len` handle the Direct-TCP transport header (`0x00` + a 24-bit big-endian length on port 445); `pc_smb2_build_header` / `pc_smb2_parse_header` build and validate the 64-byte little-endian SMB2 sync header (ProtocolId `FE 53 4D 42` + StructureSize 64, exposing the command / status / MessageId / TreeId / SessionId); `pc_smb2_build_negotiate` offers the dialect list (SMB 2.0.2 / 2.1 / 3.0 / 3.0.2) with the client GUID; and `pc_smb2_parse_negotiate_response` extracts the chosen dialect, server GUID, max transact/read/write sizes, and the SPNEGO/NTLM security token (bounds-checked against the message). All fields little-endian; you own the TCP socket. Field layout verified against MS-SMB2 §2.2.1.2 / §2.2.3 / §2.2.4. Increment 2 adds the **NTLM digests** (src/crypto/md): MD4 (RFC 1320 - the NT hash primitive, `MD4(UTF-16LE(password))`), MD5 (RFC 1321), and HMAC-MD5 (RFC 2104 - the NTLMv2 MAC), streaming + zero-heap, KAT-verified against the RFC test vectors and the well-known NT hash of "password". (MD4/MD5 are cryptographically broken and are included only because NTLM requires them on the wire.) Increment 3 is the **NTLMv2 response** (src/network_drivers/application/smb/ntlm, MS-NLMP §3.3.2): `pc_ntlm_nt_hash` (`MD4(UTF-16LE(password))`), `pc_ntlm_ntowfv2` (`HMAC-MD5(NThash, UTF-16LE(Uppercase(user)+domain))`), and `pc_ntlm_v2_response`, which builds `temp` (version + timestamp + client challenge + the server's target-info AV_PAIRs) and computes the NTProofStr / NtChallengeResponse / SessionBaseKey from the server challenge - verified byte-for-byte against the MS-NLMP §4.2 worked example. Increment 4 is the **NTLMSSP message codec** (src/network_drivers/application/smb/ntlmssp, MS-NLMP §2.2.1): `pc_ntlmssp_build_negotiate` (type 1), `pc_ntlmssp_parse_challenge` (type 2 - extracts the flags, the 8-byte server challenge, and the target-info AV_PAIRs, bounds-checked), and `pc_ntlmssp_build_authenticate` (type 3 - lays out the LM/NT responses + domain/user/workstation in the little-endian Len/MaxLen/Offset payload fields); an end-to-end test parses a CHALLENGE, computes the NTLMv2 response, and confirms the AUTHENTICATE carries it. Increment 5 is the **SPNEGO GSS-API wrapping** (src/network_drivers/application/smb/spnego, RFC 4178): a zero-heap definite-length DER codec that wraps the NTLMSSP NEGOTIATE token in a GSS-API `InitialContextToken` (`[APPLICATION 0]` + the SPNEGO OID `1.3.6.1.5.5.2` + a `NegTokenInit` advertising the NTLM mech OID `1.3.6.1.4.1.311.2.2.10`), extracts the CHALLENGE from the server's `NegTokenResp` (skipping negState / supportedMech), and wraps the AUTHENTICATE in a reply `NegTokenResp` - the tokens SMB2 SESSION_SETUP carries. Verified byte-exact against a hand-computed vector, round-trip, and independently via `openssl asn1parse`. Increment 6 is the **SMB2 SESSION_SETUP framing** (src/network_drivers/application/smb/smb2, MS-SMB2 §2.2.5/§2.2.6): `pc_smb2_build_session_setup` builds the request (SecurityMode + PreviousSessionId + the SPNEGO security buffer at offset 88, echoing the server SessionId on the second round) and `pc_smb2_parse_session_setup_response` parses the response (StructureSize 9, SessionFlags, and the server's security buffer, bounds-checked) - the caller reads the SessionId and the `STATUS_MORE_PROCESSING_REQUIRED` / `SUCCESS` status from the header. An end-to-end test routes a full auth round through the framing (wrap an NTLMSSP NEGOTIATE in SPNEGO, frame it, then unwind a server response back through framing -> SPNEGO -> NTLMSSP and recover the server challenge intact). Increment 7 adds the **file commands** (src/network_drivers/application/smb/smb2, MS-SMB2 §2.2.9-§2.2.16): `pc_smb2_build_tree_connect` / `pc_smb2_parse_tree_connect_response` connect to a `\\server\share` UNC path (the TreeId comes back in the response header, the ShareType tells disk vs pipe vs print); `pc_smb2_build_create` / `pc_smb2_parse_create_response` open or create a file with a DesiredAccess / ShareAccess / CreateDisposition / CreateOptions set, returning the 16-byte FileId handle and the EndofFile size; and `pc_smb2_build_close` / `pc_smb2_parse_close_response` release the handle. Increment 8 completes the client with **READ / WRITE** (src/network_drivers/application/smb/smb2, MS-SMB2 §2.2.19-§2.2.22): `pc_smb2_build_read` / `pc_smb2_parse_read_response` read a length of bytes at a file offset (the response data is returned bounds-checked as a pointer into the message), and `pc_smb2_build_write` / `pc_smb2_parse_write_response` write a data buffer at an offset (the response reports the byte count). All little-endian, field layout verified against the MS-SMB2 request/response structures. Pure and host-tested (`native_smb`, 36 cases across the codec). This is the full read/write-a-file-on-a-share codec: NEGOTIATE -> SESSION_SETUP (NTLMv2 over SPNEGO) -> TREE_CONNECT -> CREATE -> READ / WRITE -> CLOSE. On top of the codecs, **`smb_client`** (src/network_drivers/application/smb/smb_client) is the dialogue engine that runs the exchange: `smb_open` drives NEGOTIATE, the two-round NTLMv2 SESSION_SETUP (building the NTLMSSP tokens, wrapping them in SPNEGO, echoing the server SessionId, extracting the server's MsvAvTimestamp for the NTLMv2 response), TREE_CONNECT to `\\server\share`, and CREATE - returning an `SmbHandle` (session / tree / file id + file size); `smb_close` releases it. `smb_read` and `smb_write` then move the file bytes on that handle, looping the READ / WRITE commands in `PC_SMB_BUF`-sized chunks (read stops at a short read / STATUS_END_OF_FILE; write grows the cached file size), giving a device a small POSIX-like open/read/write/close surface. It works against a send/recv seam (Direct-TCP framing handled internally), so the whole exchange - handshake and file transfer - is host-tested end to end with a scripted mock SMB2 server (`native_smb`, 46 cases total, including a byte-exact write-then-read round trip) and rides `pc_client` on the device. Increment 9 adds **SMB 2.x message signing**: the shared `pc_sha256` / `pc_hmac_sha256` (FIPS 180-4 + RFC 2104, KAT-verified against the FIPS and RFC 4231 vectors) from src/crypto, and `pc_smb2_sign` / `pc_smb2_verify` (MS-SMB2 §3.1.4.1/§3.1.5.1) set the `SMB2_FLAGS_SIGNED` flag, HMAC-SHA256 the message with its 16-byte Signature field zeroed, and write / constant-time-verify the MAC's first 16 octets - verified against a reference signature computed with Python's hmac/hashlib plus tamper / wrong-key / short-message rejection. Increment 10 wires the signer into **`smb_client`**: `smb_open` reads the server's NEGOTIATE SecurityMode and, when signing is required, derives the SMB 2.x session key (the NTLMv2 SessionBaseKey), signs the round-2 SESSION_SETUP, and marks the `SmbHandle` signing-active (unless the session is GUEST/NULL, MS-SMB2 §3.2.5.3.1); every later request (TREE_CONNECT / CREATE / READ / WRITE / CLOSE) is then HMAC-SHA256 signed and every response verified, failing closed on a missing or wrong signature - host-tested end to end with the mock server re-deriving the same key as a reference signing peer (a full signed read/write/close round trip, a tampered-response rejection, and an unsigned-when-not-required regression). Increment 11 adds the **SMB 3.1.1 negotiate-context codec** (src/network_drivers/application/smb/smb2, MS-SMB2 §2.2.3 / §2.2.3.1 / §2.2.4): `pc_smb2_build_negotiate_311` offers the dialect list through 0x0311 with the mandatory PREAUTH_INTEGRITY_CAPABILITIES context (SHA-512 + a client salt) and a SIGNING_CAPABILITIES context advertising HMAC-SHA256, each 8-byte aligned; `pc_smb2_parse_negotiate_contexts` walks the response NegotiateContextList (located by NegotiateContextOffset / NegotiateContextCount) and extracts the negotiated preauth hash + salt, the signing algorithm, and the cipher, with every context header and its data bounds-checked (`test_smb2`). Increment 12 adds the **SP800-108 counter-mode KDF** (src/crypto/kdf, `pc_kdf_ctr_hmac_sha256`): HMAC-SHA256 as the PRF with a 32-bit counter before the fixed input (NIST SP800-108 §5.1), the primitive SMB 3.x uses to derive its signing and encryption keys (MS-SMB2 §3.1.4.2). The caller passes the assembled `Label || 0x00 || Context || [L]` fixed input, so the primitive stays independent of the per-key label choices; it is verified against the NIST CAVP KBKDF (KDFCTR) vectors, including a two-block case that exercises the counter loop and truncation. Increment 13 adds **SHA-512** (src/crypto/sha512, FIPS 180-4): streaming `pc_sha512_init` / `_update` / `_final` plus a one-shot `pc_sha512`, the hash the SMB 3.1.1 preauth-integrity chain runs over the handshake messages, KAT-verified against the FIPS 180-4 vectors (including the 112-byte two-block example) with a streaming-equals-one-shot check. These primitives are assembled into the full 3.1.1 flow (the preauth-integrity hash chain over the concatenated NEGOTIATE / SESSION_SETUP messages + the per-key label/context assembly feeding the KDF + AES-CMAC signing, plus the NTLMSSP MIC), HW-verified against a real Samba. Increment 14 adds **SMB 3.x transport encryption** (MS-SMB2 §2.2.41 / §3.1.4.3-4): all four SMB 3.1.1 ciphers - AES-128-GCM / AES-256-GCM (src/crypto/aes128gcm, src/crypto/aesgcm) and AES-128-CCM / AES-256-CCM (the new src/crypto/aesccm, NIST SP800-38C) - wired into the TRANSFORM_HEADER codec (`pc_smb2_encrypt` / `pc_smb2_decrypt` dispatch on the negotiated cipher, selecting the 16/32-byte key and 12/11-byte GCM/CCM nonce), the §2.2.3.1.2 ENCRYPTION_CAPABILITIES negotiate (offer all four, and advertise SMB2_GLOBAL_CAP_ENCRYPTION so a required-encryption server negotiates a cipher), and the SP800-108 cipher-key derivation (SMBC2SCipherKey / SMBS2CCipherKey, [L]=128 or 256). `smb_client` derives the keys at SESSION_SETUP and encrypts every request / decrypts every response from TREE_CONNECT on, driven by a monotonic per-session nonce (never reused, so no replay); a **client-forced** mode (`SmbConfig.encrypt`, like smbclient `-e`) reaches a share whose server requires encryption. The CCM / 256-GCM primitives are KAT-verified against pyca/cryptography, and the whole path is HW-verified against a real Samba `smb encrypt = required` share with every cipher reading byte-exact. See src/network_drivers/application/smb/smb_client.h.
SMTPPC_ENABLE_SMTP
Outbound SMTP client (RFC 5321) for device email alerts. Default off. services/net/smtp runs a blocking one-shot send over the shared outbound client transport (pc_client): read the greeting, EHLO, optional AUTH LOGIN (username/password base64-encoded), then MAIL FROM / RCPT TO / DATA the message and QUIT. The body is dot-stuffed (RFC 5321 sec 4.5.2) and CRLF-normalized so it can never end early, and the whole exchange is zero-heap (fixed PC_SMTP_* buffers). `SmtpConfig.security` selects how the connection is secured - a `SmtpSecurity` enum (PLAIN / TLS / STARTTLS) rather than a bool, so "implicit and explicit at once" is unrepresentable. Implicit TLS (SMTPS, port 465) encrypts from the first byte; **STARTTLS** (RFC 3207, the submission port 587) issues STARTTLS after the first EHLO, upgrades the transport in place, and reissues EHLO because RFC 3207 sec 4.2 requires discarding capabilities learned in the clear. STARTTLS **fails closed**: if it is configured and the server does not advertise it, the send aborts with `SMTP_ERR_NO_STARTTLS` _before_ AUTH, so an attacker stripping the capability line cannot downgrade the exchange into sending credentials in plaintext - and the capability is matched as a whole keyword, so "STARTTLSX" does not count. The upgrade is invisible to the dialogue engine: the transport owns one send/recv pair that flips to TLS underneath it, so no path can keep writing plaintext after the handshake. Both need `PC_ENABLE_SMTP_TLS` (which is what pulls in the client-TLS session). The dialogue engine (smtp_run) is written against a send/recv seam, so the full protocol - reply codes, AUTH, dot-stuffing, the terminating dot - is host-tested with a scripted mock server (env:native_smtp). SMS fallback needs no extra code (an email-to-SMS carrier gateway address). **HW-verified on an ESP32-S3** against a live aiosmtpd submission server: the device connected in the clear to :587, saw STARTTLS advertised, upgraded, reissued EHLO encrypted, and the server logged `DELIVERED from=esp32@... bytes=183 tls=True` with the message body intact. Getting there fixed three latent bugs in a TLS path that had **never been compiled** - a missing `<mbedtls/ssl.h>` include, SMTP being absent from the `PC_ENABLE_CLIENT_TLS` derivation (so the session API resolved to stubs), and BIO callbacks dereferencing a `ctx` the TLS layer never passes them. Example SmtpAlert ships a from-scratch beginner walkthrough (including standing up a Postfix server). See src/services/net/smtp/smtp.h.
WebhookPC_ENABLE_WEBHOOK
Opt-in outbound webhooks / IFTTT. Default off. Requires HTTP_CLIENT. services/net/webhook builds an IFTTT Maker URL and a value1/value2/value3 JSON payload (pure, host-tested) and fires them - or any JSON to any URL - via the outbound http_client (POST). Use it to push an event from the device to IFTTT, a Slack/Discord hook, or your own API.
WS ClientPC_ENABLE_WS_CLIENT
Outbound WebSocket client (RFC 6455 over raw lwIP, optional wss:// TLS). Default off. When set, src/services/net/ws_client/ws_client.h connects to a remote WebSocket endpoint (ws://, or wss:// over client-side mbedTLS), performs the RFC 6455 client handshake (Sec-WebSocket-Key/Accept), and sends masked text / binary frames + receives server frames via a callback - for streaming to cloud dashboards or bidirectional control. The frame/handshake codec is host-testable.
WS Client TLSPC_ENABLE_WS_CLIENT_TLS
wss://: run the WebSocket client over client-side TLS (needs TLS).

Storage & Database5

DBM Key-Value StorePC_ENABLE_DBM
Opt-in log-structured hash key-value store on the write-ahead log (services/dbm; requires WAL). Default off. A Bitcask-style store: each put/delete appends one WAL record (so every write is one of the WAL's fast sequential appends, never a slow durable random write), and an in-RAM open-addressed hash index - a fixed BSS array of PC_DBM_SLOTS slots, no heap - maps each live key to where its value sits in the log. get re-reads the value straight from the log; open rebuilds the index by scanning the WAL and replaying puts and deletes in order, so the live key set is exactly what survived the last mount. Keys are bounded by PC_DBM_KEY_MAX, values by PC_DBM_VAL_MAX; writes are batched and made durable by a checkpoint (pc_dbm_sync). Pure and host-tested over a RAM-backed device (overwrite, tombstone resurrection, persistence across a remount with and without checkpoint, collisions, index-full fail-closed, bounds, max-value round-trip).
Document StorePC_ENABLE_DOCSTORE
Local JSON document store on the write-ahead log (services/docstore; requires DBM + WAL). Default off. A small NoSQL document store: JSON documents addressed by an id, kept durably on the WAL. It is a thin layer over dbm (the id is the key, the JSON body is the value), so it inherits dbm's zero-heap index, WAL persistence, and crash recovery; what it adds is the document capability - top-level field queries (`pc_docstore_find_str` / `_find_int` / `_find_bool` scan the live documents and match those whose JSON field equals a value, like a small `find({field: value})`) using the zero-heap JSON reader. put/get/delete forward to dbm; writes are batched and made durable by a checkpoint. Ids are bounded by PC_DBM_KEY_MAX, bodies by PC_DBM_VAL_MAX. Pure and host-tested over a RAM-backed device (put/get/delete, string/int/bool field finds, persistence and query across a remount, and early-stop). See src/services/storage/docstore/docstore.h.
RedisPC_ENABLE_REDIS
Redis RESP2/RESP3 wire codec. Default off. services/iot/redis_resp lets a device drive a Redis server over the shipped outbound client transport: `pc_resp_encode_command` builds a command (an array of bulk strings, binary-safe via explicit arg lengths) and `pc_resp_parse` is a streaming cursor reply decoder covering RESP2 (simple / error / integer / bulk / array / nil) plus the RESP3 additions (null / boolean / double / big number / bulk error / verbatim / map / set / push); aggregates report a child count and the caller walks each child, so nested replies decode with no heap. Pure and host-tested against Redis spec vectors and verified live against a real redis-server (SET/GET, arrays, and a RESP3 map from HELLO 3); the connection is the application's. See src/services/redis_resp.h.
SQLitePC_ENABLE_SQLITE
Opt-in SQLite3 on-disk file-format reader (services/storage/sqlite). Default off. This is file-format access, not the SQLite library: the documented SQLite3 database file structure parsed by hand - the 100-byte database header, the b-tree page header, the record varint, and record serial types - so a device can read a SQLite file (from pc_wal_fs / fs::FS) without the SQLite amalgamation, which needs a heap and stdio and does not fit the no-stdlib zero-heap model. It reads rows: cell pointers, the leaf-table cell (rowid + payload + overflow detection), a record cursor that yields each column's serial type and value bytes (with int/float decoders), and a multi-page table cursor that walks an interior b-tree over a table's rootpage in rowid order using a bounded descent stack and two page buffers (it pulls pages through a reader callback, so it works over a RAM image, pc_wal_fs, or fs::FS). Read-first (a bounded writer and overflow-page chains are later steps); pure (you hand it page bytes, it does no I/O) and host-tested against real databases produced by the sqlite3 CLI - reading the actual pc_sqlite_schema row column-by-column and scanning a 40-row, two-level b-tree - plus varint and serial-type spec vectors.
Write-Ahead LogPC_ENABLE_WAL
Opt-in write-ahead store for atomic buffer-to-flash storage, the substrate for on-device data stores (dbm / sqlite / nosql). services/storage/wal frames each record with a CRC-32 (pc_wal_record_encode), and pc_wal_replay walks a journal image on mount and stops at the first record with a bad magic, a failed CRC, or a truncated tail - the torn write a power loss leaves - so a crash costs at most the last un-checkpointed record. On top of that codec, pc_wal_store adds a mountable durable log over a block-device seam (WalDev, three function pointers - so it binds to any fs::FS: SD card or LittleFS): records append sequentially and are committed in bulk by pc_wal_store_checkpoint, which flips an A/B superblock (the single durable pointer move); mount picks the newest valid superblock and replays the tail past it, so records appended after the last checkpoint are still recovered and a torn superblock falls back to the other copy. Built to the SD-over-SPI envelope measured in docs/FEATURE_PERFORMANCE.md (append sequentially in ~32 KiB pages, checkpoint every ~128-256 KiB; never scatter small durable writes). Pure and host-tested (CRC-32 check vector, torn/truncated-tail recovery, checkpoint durability, superblock fallback), and pc_wal_fs.h binds the seam to a real fs::FS file (preallocated, random-access over File::seek + File::flush). The whole path is hardware-verified on an SD card over SPI: checkpoint recovery, torn-tail drop, byte-level payload persistence, and survival across a chip reset all pass. Zero heap. Default off.

Time & Discovery7

Adaptive mDNSPC_ENABLE_MDNS_ADAPTIVE
Opt-in adaptive mDNS beacon scheduling. Pure scheduling decisions on top of the shipped mDNS service: pc_mdns_beacon_adapt backs the announce interval off toward a ceiling under RF contention and recovers it when the air is quiet, pc_mdns_refresh_interval gives the TTL/2 continuous-refresher cadence, pc_mdns_beacon_due says when an announce is due, and pc_mdns_beacon_presleep_due says whether to announce before a sleep window that would otherwise let the record lapse. Wrap-safe time math, no heap/stdlib. **The device binding** (`pc_mdns_adaptive_begin/_tick/_end`, needs `PC_ENABLE_MDNS` + `PC_ENABLE_PROMISC`) ties the three shipped pieces together: promiscuous capture pinned to the station's own channel supplies a live frame count, `pc_mdns_contention_sample` turns that running total into a per-window contention value (host-tested incl. counter- and clock-wrap and uint16 saturation), the beacon scheduler turns that into an interval, and the announce is a TXT re-apply on the running responder - which re-announces on every PCB with no goodbye and no re-probe, a refresh not an evict. The contention signal is a **radio-layer callback, not a second socket on UDP 5353**: a 5353 co-bind turns the ESP-IDF responder announce-only (it browses but stops resolving), whereas promiscuous capture never touches the responder's sockets. The backoff ceiling is capped at ~7/8 of the TTL regardless of the requested `max_interval_ms`, because announcing slower than the TTL would let the record lapse - the adaptive range is fundamentally `[TTL/2, ~TTL)`, so a longer TTL buys more room. Host-tested (`native_mdns_adaptive`, 14 cases). **HW-verified on an ESP32-S3** against avahi: the record kept resolving (A + SRV + TXT) while capture ran - the exact case the 5353 approach broke - real ambient frames drove the interval off its base and it capped at the safe ceiling below the TTL, and the refresher re-announced on the adaptive schedule. It owns promiscuous mode, so it cannot run alongside the wifi_sniffer live channel-hop binding. Example MdnsAdaptive. Default off.
DNS ServerPC_ENABLE_DNS_SERVER
Authoritative DNS server on UDP/53. Default off. services/net/dns_server answers A/IN queries from a small fixed table of `name -> IPv4` records you register with `pc_dns_server_add()`, so devices on an offline / air-gapped LAN can use names (`printer.lan`) instead of raw IPs - a companion to the NTP server for self-hosted infrastructure. It parses the question, looks the name up case-insensitively, and on a hit appends one answer record using DNS name compression (a 2-byte pointer back to the question); an unknown name returns NXDOMAIN and non-A queries return no answer. The response builder (`pc_dns_server_build_response`) is pure and host-tested against the wire format; the binding is the transport UDP service. Zero heap. This is a general resolver, distinct from the provisioning captive-portal DNS (which points every name at the softAP) - do not enable both. Example DnsServer. See src/services/net/dns_server/dns_server.h.
MDNSPC_ENABLE_MDNS
mDNS / DNS-SD advertisement (`name.local` + `_http._tcp`) via ESPmDNS.
NTPPC_ENABLE_NTP
SNTP wall-clock time sync via the ESP-IDF SNTP client.
NTP ServerPC_ENABLE_NTP_SERVER
NTP/SNTP time server (RFC 5905 / RFC 4330 server mode) on UDP/123. Default off. network_drivers/application/ntp_server turns the device into a local time source: it answers client NTP requests from its own clock, so an offline or air-gapped LAN can keep its devices in sync without reaching the public pool. The 48-octet response builder (`pc_ntp_server_build_response`) is pure - it echoes the request's protocol version, copies the client's transmit timestamp into the origin field (so the client can compute round-trip delay), and stamps reference/receive/transmit times - and is host-tested against the wire format. `pc_ntp_server_begin(stratum, refid)` binds the port via the transport UDP service and drives it from `pc_time_now()` (seconds) plus a `pc_millis()`-derived sub-second fraction; while the device has no time it stays silent rather than serve a wrong clock. Pair it with a GPS receiver (parsed via the NMEA 0183 codec into a stratum-1 time source) and an upstream-NTP fallback for a self-hosted, offline-capable time server. Example NtpServer. See src/network_drivers/application/ntp_server/ntp_server.h.
NTSPC_ENABLE_NTS
Opt-in Network Time Security (NTS, RFC 8915) wire codec. When set, network_drivers/application/nts provides the NTS-KE record codec (build/parse the TLV records - next protocol, AEAD, cookies, server/port) and the NTS NTP extension-field framing (Unique Identifier, Cookie, Authenticator). Pure framing (the AES-SIV-CMAC-256 AEAD + TLS-exporter key derivation are the crypto integration on top). Default off.
PTPPC_ENABLE_PTP
PTP / IEEE 1588-2008 (PTPv2) message codec + ordinary-clock slave math. Default off. The Precision Time Protocol disciplines LAN clocks to sub-microsecond accuracy by exchanging timestamped messages; network_drivers/application/ptp is a zero-heap codec for the PTPv2 wire format: `pc_ptp_build_header` / `pc_ptp_parse_header` frame the 34-octet common header (all multi-octet fields big-endian), `pc_ptp_ts_write` / `pc_ptp_ts_read` handle the 10-octet timestamp (48-bit seconds + 32-bit nanoseconds), and `pc_ptp_build_sync` / `_delay_req` / `_follow_up` / `_delay_resp` plus `pc_ptp_parse_timestamp_msg` / `_parse_delay_resp` / `_parse_announce` build and decode the five messages a two-step ordinary clock needs (each build stamps the type-specific messageType / control / messageLength for you). `pc_ptp_compute` derives the slave's offsetFromMaster and meanPathDelay from the four transfer timestamps t1..t4 (`offset = ((t2-t1) - (t4-t3)) / 2`, `delay = ((t2-t1) + (t4-t3)) / 2`). The **P2P peer-delay mechanism** (IEEE 1588-2008 §11.4) is also supported: `pc_ptp_build_pdelay_req` / `_pdelay_resp` / `_pdelay_resp_follow_up` and their parsers build and decode the Pdelay_Req / Pdelay_Resp / Pdelay_Resp_Follow_Up messages, and `pc_ptp_compute_link_delay` computes the per-link meanLinkDelay (`((t4-t1) - (t3-t2)) / 2`) that a transparent/boundary clock measures to each neighbor independently of the master offset. Header field order, timestamp layout, and message bodies verified against IEEE 1588-2008 clause 13 and interop-checked against Wireshark's PTP dissector (every built message - Announce/Sync/Follow_Up/Delay_Req/Delay_Resp - decodes with the correct type, length, clock identity, and grandmaster-quality fields); pure and host-tested (`native_ptp`), and the slave is **HW-verified against linuxptp `ptp4l`** over a LAN (an ESP32-S3 on Wi-Fi ran the full Announce/Sync/Follow_Up/Delay_Req/Delay_Resp exchange with a real grandmaster, decoding its Announce and computing offset + path delay each cycle). Supports both roles: an ordinary-clock **slave** and a **grandmaster** (`pc_ptp_build_announce` advertises clockClass / timeSource so the device can hand out GPS/RTC-sourced time). The UDP transport (event port 319, general port 320, multicast 224.0.1.129) and the local timestamping are the application's. Example Ptp is an ordinary-clock slave (`PTP_MASTER 1` makes it a grandmaster). See src/network_drivers/application/ptp/ptp.h.

Observability & Telemetry14

DiagPC_ENABLE_DIAG
Expose a diagnostic JSON endpoint via `diag(slot_id)`, which reports which features are enabled and how the buffers are sized. Disabled by default - enabling it exposes compile-time configuration (buffer sizes, feature flags) which could aid an attacker. Only enable in development or behind an authenticated route. The document is a frame spec (`DIAG_DOC` in src/protocore.c): each feature flag renders as `true` or `false` and each sizing constant as a decimal, so every value in the report is a compile-time constant and cannot drift from the build. `diag()` borrows the worst-case 975 bytes from the plaintext arena, fills the spec with `pc_frame_build()`, sends the result as `application/json`, and releases the borrow; a failed borrow or build answers 503 with an empty body, so no partial document reaches the wire. Host-tested (`native_diag`). See src/protocore.c.
Flow ExportPC_ENABLE_FLOW_EXPORT
Flow-record export codec. Default off. services/net/flow_export is a zero-heap exporter-side codec for on-device flow accounting in three formats: NetFlow v5 (the fixed 24-octet header + 48-octet record, via `flow_v5_write_header` / `flow_v5_write_record`), NetFlow v9 (RFC 3954), and IPFIX (RFC 7011). The v9 / IPFIX side is a small cursor (`FlowWriter`): begin the message (`flow_ipfix_begin` / `flow_v9_begin`), emit a Template (`flow_export_template`), open a matching Data Set and append records (`flow_export_data_begin` / `flow_export_data_record` / `flow_export_data_end`), then `flow_export_finish()` patches the IPFIX message length or the NetFlow v9 record count (and pads each v9 FlowSet to a 4-octet boundary). Field offsets verified against RFC 7011 / RFC 3954 / the published v5 layout; pure and host-tested. The flow cache (5-tuple + counters) and the UDP send (pc_udp_sendto) are the application's. See src/services/net/flow_export/flow_export.h.
Log-BufferPC_ENABLE_LOGBUF
Opt-in fixed-RAM rotating log buffer with severity traps. Default off. When set, server/logbuf keeps the last PC_LOG_LINES log lines in a fixed ring (oldest pruned on overflow - no heap, bounded), dumps them oldest-first for a `/logs` endpoint, and fires a trap callback when a line is logged at/above a severity threshold (forward criticals as an SNMP trap / webhook). The ring + trap logic is pure and host-tested.
MetricsPC_ENABLE_METRICS
Prometheus `/metrics` endpoint (text exposition format 0.0.4). Default off (requires STATS for the underlying counters). When set, PC::metrics() emits the runtime stats as Prometheus metrics (`pc_uptime_seconds`, `pc_http_requests_total`, `pc_http_responses_total{class=...}`, `pc_active_connections`, `pc_free_heap_bytes`,...) so a Prometheus server can scrape the device.
ObservabilityPC_ENABLE_OBSERVABILITY
Transport-layer observability: connection-event hook + counters. Default off (zero cost when unset - the notify points compile to nothing). When set, the transport (L4) fires an application callback on every connection state transition - `pc_conn_on_event(slot, old_state, new_state, reason)` - and maintains lock-free counters (accepts, closes by reason, idle timeouts, RX backpressure events, dropped deferred events, and a live CONN_CLOSING gauge) readable via `pc_conn_counters_get()`. The only state-transition trace the L4/L5 core exposes; pair it with STATS for request-level metrics.
Partition MonitorPC_ENABLE_PARTITION_MONITOR
Opt-in flash partition-map monitor endpoint. Default off. When set, services/storage/partition_monitor reports the device's flash partition table (label, kind, type / subtype, offset, size, and which app slot is running) as JSON, for diagnostics and OTA dashboards. The partition walk uses esp_partition / esp_ota_ops; the JSON serializer and the kind classifier are pure and host-testable.
SNMPPC_ENABLE_SNMP
SNMP agent (v1/v2c, + v3 USM when SNMP_V3) over lwIP UDP. Zero-heap ASN.1 BER codec + a fixed MIB table on UDP/161. Default off. The BER codec itself is gated by this flag and is otherwise unit-tested standalone (env:native_snmp).
SNMP TrapPC_ENABLE_SNMP_TRAP
Outbound SNMP notifications - traps and informs (requires SNMP). Default off. When set, src/services/net/snmp/pc_snmp_notify.h sends SNMPv2c (and, with SNMP_V3, SNMPv3 USM) Trap / InformRequest PDUs to a manager over UDP - so the agent can push alerts instead of only answering polls. Reuses the BER codec and the transport-layer UDP service; the PDU builder is host-testable.
SNMP V3PC_ENABLE_SNMP_V3
Add SNMPv3 USM (auth via HMAC-SHA, privacy via AES-128-CFB). Default off.
StatsPC_ENABLE_STATS
Runtime stats endpoint (uptime, request/error counts, pool usage, heap).
StatsDPC_ENABLE_STATSD
StatsD metrics client - push counters/gauges/timings/sets to a StatsD collector. Default off. services/iot/statsd emits metrics in the StatsD wire format (`name:value|type`, e.g. `api.hits:1|c`) over UDP via pc_udp_sendto to any StatsD-speaking backend (Graphite/StatsD, Telegraf, Datadog, InfluxDB), with counters (`c`), gauges (`g`, absolute or a signed `+`/`-` delta), timings (`ms`), and sets (`s`), plus an optional sample rate (`|@0.1`) and DogStatsD tags (`|#env:prod`). This is the push counterpart to the pull-based Prometheus `/metrics` endpoint - useful behind NAT/firewalls where nothing can reach in to scrape. The value/rate are rendered without printf float/64-bit formatting; the line builder (`pc_statsd_format`) is pure and host-tested, and the emit helpers are host-tested through the transport UDP capture seam. Zero heap. Example StatsdMetrics. See src/services/iot/statsd/statsd.h.
SyslogPC_ENABLE_SYSLOG
Syslog client (RFC 5424 over UDP). Default off. When set, the device can ship log lines to a remote syslog server (e.g. rsyslog / journald / a SIEM) as RFC 5424 UDP datagrams via the transport-layer UDP service - a zero-heap structured-logging sink for fleets of constrained devices. See src/services/net/syslog/syslog.h.
TelemetryPC_ENABLE_TELEMETRY
Telemetry math helpers (moving-window stats, rate-of-change, totalizer). Default off. When set, src/services/iot/telemetry/telemetry.h provides zero-heap pure-computation helpers over caller-supplied storage: a moving-window stats accumulator (mean / variance / stddev / min / max), a derivative / rate-of- change tracker, and a trapezoidal run-time totalizer. No ESP32 dependency, so the whole cluster is host-testable; it feeds dashboards, alert triggers, and odometer-style counters.
UDP TelemetryPC_ENABLE_UDP_TELEMETRY
Opt-in fire-and-forget UDP telemetry cast. Default off. When set, services/iot/udp_telemetry casts metric lines (InfluxDB line protocol: `measurement field=val,field2=val2`) to a configured collector over UDP via pc_udp_sendto - zero-heap, fire-and-forget (no ACK, no retry), ideal for shipping device metrics to Telegraf/InfluxDB/a log sink. The line builder is pure and host-tested; only the send touches the network.

Firmware & System3

OTAPC_ENABLE_OTA
Authenticated OTA firmware update (streaming POST to the ESP32 Update API).
OTA RollbackPC_ENABLE_OTA_ROLLBACK
Opt-in OTA rollback protection / soft-brick safeguard. Default off. After an OTA update the new image boots in PENDING_VERIFY; this service confirms it (esp_ota_mark_app_valid) once a self-test passes, or rolls back to the previous image if the self-test fails or the confirm window elapses without success - so a bad update self-heals instead of soft-bricking. The decision logic is pure and host-tested; the commit / rollback use esp_ota_ops. Requires the bootloader's app-rollback support (CONFIG_BOOTLOADER_APP_ROLLBACK_ENABLE).
ProvisioningPC_ENABLE_PROVISIONING
First-boot WiFi provisioning: softAP + captive-portal credentials form.

Application (L7) - Other37

AD9238PC_ENABLE_AD9238
SPI configuration-port codec for the AD9238 - and the shared Analog Devices high-speed-ADC SPI register map it belongs to. Default off. The AD9238 (12-bit, 20/40/65 MSPS dual ADC) exposes two interfaces that must not be confused: the **sample data path** is a parallel CMOS/LVDS bus running far beyond what an MCU can bit-bang, so it is out of scope here (see `reverse_engineering/` for the FPGA/CPLD-buffered burst-drain architecture), and the low-speed 3-wire **SPI configuration port** (SCLK / SDIO / CSB, MSB first) that controls power-down, output data format, output test patterns, and offset trim. services/peripherals/ad9238 is that control-port codec: `pc_ad9238_build_*` frame the 16-bit instruction word (R/W + 2-bit byte-count + 13-bit address) and the shadow-register "device update" transfer that this whole ADI high-speed-ADC generation shares, per the AD9238 Rev. D datasheet SPI Register Map, with fail-closed input validation (null buffer, undersized cap, zero/oversize byte count, out-of-range address). Pure codec (builds/parses byte sequences); the SPI clocking and the per-register bit-field meanings are the application's / the datasheet's - the same contract as services/instrumentation/scpi and services/gpib. Host-tested (`native_ad9238`). See src/services/peripherals/ad9238/ad9238.h.
DiffServ QoS MarkingPC_ENABLE_DIFFSERV
DiffServ QoS marking (RFC 2474): stamp the 6-bit DSCP into the DS field (the high 6 bits of the IPv4 TOS / IPv6 Traffic-Class byte) of outbound traffic so a QoS-aware network - and the Wi-Fi driver's 802.11e WMM access-category mapping - can prioritize safety / real-time packets over best-effort. Default off (zero cost: the marking code and the DSCP state compile out). `network_drivers/transport/diffserv.h` gives three levels of control, coarse to fine: `pc_set_default_dscp()` marks every outbound TCP connection (accepted + client); `pc_listen_set_dscp(port, dscp)` overrides that for one listener's connections; and `pc_conn_set_dscp(slot, dscp)` re-tags one live connection with any DSCP - real per-flow QoS, or arbitrarily tagging traffic for network testing. `pc_udp_set_dscp()` marks outbound UDP datagrams. Convenience code points are defined (`PC_DSCP_EF` 46 expedited forwarding, `PC_DSCP_CS6` 48 network control, `PC_DSCP_AF41`/`AF31`); any 0-63 value is accepted (0 = best-effort). The DSCP is written to the pcb's TOS field on tcpip_thread as the connection is accepted / connected (or per outbound UDP datagram), so nothing is added to the send hot path; marking applies from the connection's first data segment (the SYN-ACK stays best-effort - it is emitted by lwIP before any app callback, and ESP32 lwIP does not inherit the listen-pcb TOS; HW-tested by stamping the listen pcb and confirming the SYN-ACK is still tos 0x0). Host-tested (`native_diffserv`) and HW-verified on an ESP32-P4, read straight off the wire with tcpdump: the server default marked the `:80` HTTP response Expedited-Forwarding (tos 0xb8); a per-listener override on `:8080` marked its responses AF41 (tos 0x88); a per-connection re-tag marked its flow Class-Selector 6 (tos 0xc0); and outbound UDP datagrams were sent marked CS6 (the same `pcb->tos` path as the verified TCP marking). Example DiffServ. See src/network_drivers/transport/diffserv.h.
DTLS 1.3PC_ENABLE_DTLS
DTLS 1.3 (RFC 9147) datagram security for UDP transports - the secure datagram counterpart to the TLS 1.3 that already backs HTTP/3, aimed at CoAP-over-DTLS and constrained-device telemetry. Default off. This flag currently gates the DTLS 1.3 **record layer** (network_drivers/presentation/security/dtls/pc_dtls_record): the DTLSPlaintext classic header (initial flight + alerts) and the DTLSCiphertext unified header with per-record AEAD (AEAD_AES_128_GCM), the RFC 9147 §4.2.3 sequence-number encryption (AES-ECB mask over the ciphertext sample) and §4.2.2 reconstruction, the TLS 1.3 AEAD nonce, and the §4.5.1 anti-replay sliding window. Record and key derivation are reused from the hand-rolled TLS 1.3 stack (pc_quic_hkdf HKDF-Expand-Label, pc_quic_aead AES-128-GCM), so enabling this also compiles those primitives (otherwise gated behind HTTP/3). The record layer is byte-exact against an independent HKDF + AES-128-GCM + AES-ECB reconstruction (host-tested, `native_dtls`). This flag also gates the **handshake framing and reliability** layer (network_drivers/presentation/security/dtls/pc_dtls_handshake, RFC 9147 §5 + §7): the 12-byte DTLS handshake header, overlap-tolerant message reassembly (§5.4), the ACK message (content type 26, §7), and the stateless HelloRetryRequest cookie (§5.1) that binds the client address into an HMAC-SHA256 for return-routability; its cookie wire format is byte-exact against an independent Python-stdlib HMAC-SHA256 (host-tested, `native_dtls_hs`). It also gates the **server handshake state machine** (network_drivers/presentation/security/dtls/pc_dtls_conn, RFC 9147 §5-6): the one-round-trip full handshake (TLS_AES_128_GCM_SHA256 / X25519 / Ed25519, no PSK / 0-RTT / client auth), transport-neutral like pc_coap_server_process - ClientHello in, then ServerHello (epoch 0) and the encrypted EncryptedExtensions / Certificate / CertificateVerify / Finished flight (epoch 2), the client Finished verified, and epoch 0→2→3 key transitions. It reuses the TLS 1.3 messages + key schedule (pc_tls13_msg, pc_tls13_kdf, guards lifted to HTTP/3 or DTLS). Proven end-to-end (`native_dtls_conn`) and, most importantly, **against a real reference implementation**: the wolfSSL DTLS 1.3 client completes a full handshake and an application-data round trip with the server (test/servers/pc_dtls_wolfssl), both offering X25519 directly and through a HelloRetryRequest group renegotiation. DTLS 1.3 derives every secret and record key with the RFC 9147 §5.9 `dtls13` HKDF-Expand-Label prefix (not TLS 1.3's `tls13 `), modelled as a `Tls13Kdf` variant bound once into the key schedule. When a client offers X25519 in supported_groups but sends no X25519 key_share, the server renegotiates the group with a **HelloRetryRequest** carrying an address-bound, stateless cookie (§5.1) and restarts the transcript with the §4.4.1 message_hash before spending any asymmetric crypto. Lost flights are recovered by a caller-driven **retransmission timer** (§5.8): `pc_dtls_conn_timeout_ms` / `pc_dtls_conn_on_timeout` re-send the whole flight with fresh record sequence numbers on an exponential backoff (capped, with a retransmit ceiling that abandons a dead handshake), inbound ACKs cancel it, and a retransmitted client Finished (whose ACK was lost) is re-acknowledged. The **CoAP-over-DTLS front-end** (services/iot/coap, when PC_ENABLE_COAP is also set) secures CoAP over UDP end to end: the bridge (coaps/pc_coaps_process) drives the DtlsConn handshake and then unwraps each epoch-3 application record, answers it with pc_coap_server_process(), and re-wraps the response (proven host-side, `native_coaps`, replay-drop included); and coaps_server binds UDP 5684 (coaps://) to a per-peer DtlsConn pool - routing each datagram to its connection by peer address, driving the handshakes and the retransmission timer from a single pc_coaps_server_poll(), and reaping idle connections (host-tested `native_coaps_server`: peer routing, poll-driven retransmission, idle reap; example CoapSecure). Pure, zero-heap, not the mbedTLS TCP-TLS engine. See src/network_drivers/presentation/security/dtls/pc_dtls_conn.h.
Edge Cache Origin TLSPC_ENABLE_EDGE_ORIGIN_TLS
Opt-in TLS upstream origins for the edge cache (requires Edge Cache + TLS). Default off. When set, a mapped `https://` origin is fetched over the shared client-TLS session (`pc_tls_csess`, the same one MQTTS / wss use) layered over `pc_client` via mbedTLS BIO callbacks, selected per route by a scheme flag on the route map; without it an `https://` origin is rejected. The handshake blocks briefly in the transport's `open` (like `pc_client_open`'s connect and the MQTT/WS clients), and a `pc_tls_client_session_active()` guard fails open rather than tearing down a live shared session, so one TLS origin fetch runs at a time. Verification is off by default (encrypt-only, no authentication); `pc_edge_cache_set_origin_ca(pem, len)` enables full chain + hostname verification and `pc_edge_cache_set_origin_pin(sha256)` pins the cert (both write the shared client-TLS trust store, so they also apply to MQTTS / wss / the HTTP client). HW-verified on an ESP32-S3 against a real https origin: encrypt-only fetch byte-exact, a correct CA verifies and a wrong CA is rejected (fail-open), Range/`206` works over the TLS-fetched body. Note: the TLS engine adds a ~48 KB arena (an S3 / PSRAM board is recommended), and on mbedtls v2 (espressif32 default) an IP-address origin needs a CN-matching cert (IP-address SANs are not matched; DNS origins work normally). Follow-ups: an async handshake so a cold https MISS never blocks the worker, and a multi-session pool for more than one concurrent TLS fetch.
EUROMAP 77 (OPC UA for injection moulding)PC_ENABLE_EUROMAP77
EUROMAP 77 / OPC 40077 IMM_MES_Interface information model (OPC UA for injection moulding machines <-> MES, enums from EUROMAP 83 / OPC 40083). Default off (requires OPC-UA). services/machine_tool/euromap77 exposes a fixed IMM_MES_Interface node hierarchy - MachineInformation, MachineStatus, and Jobs (ActiveJob + ActiveJobValues with the UInt64 production counters JobCycleCounter / *PartsCounter / MachineCycleCounter) - through the OPC UA Browse + Read resolvers, served out of a caller-owned EmImm struct you refresh each loop. Faithful BrowseNames per the EUROMAP 77 NodeSet (namespace `http://www.euromap.org/euromap77/`); the counters are 64-bit (adds Int64/UInt64 to the OPC UA Variant codec). A read-only monitoring model any OPC UA / MES client browses and reads by BrowseName. The plastics-industry twin of umati / robotics. No heap, no stdlib.
FANUC FOCASPC_ENABLE_FOCAS
FANUC FOCAS Ethernet protocol codec (FANUC CNC data over TCP 8193). Default off. services/machine_tool/focas builds + parses the FOCAS wire frames a FANUC control speaks (big-endian throughout, a 10-octet magic/version/type/length envelope + payload): `pc_focas_build_open`/`_close` drive the session handshake (FRAME_DST open, empty close), `pc_focas_build_request` emits the generic command frame (a 6-octet function selector + five signed 32-bit arguments + optional trailing data), and typed wrappers cover SysInfo, alarm status, CNC parameters, macro variables, position/axis data, and the actual feedrate / spindle speed. `pc_focas_parse_frame` validates the envelope, `pc_focas_parse_response` decodes the echoed selector + FOCAS return code + data, `pc_focas_parse_sysinfo` decodes ODBSYS, `pc_focas_parse_alarm` reads the alarm bitmask, and `pc_focas_decode8` / `pc_focas_value_f` decode the FANUC 8-octet `data / base^exp` numeric encoding used by positions, feeds, and macros. Frame layout, selector encoding, the SysInfo/alarm response layouts, and the value encoding were reverse-engineered by and cross-checked against diohpix/pyfanuc (the FOCAS wire protocol is not officially published; the proprietary fwlib32 library is not required or used). Pure codec, host-tested (`native_focas`); the caller owns the TCP socket and drives the open -> command -> close sequence. FANUC is the most widely deployed CNC control, so this is a direct machine-tool data source. See src/services/machine_tool/focas/focas.h.
FANUC Stream Motion (J519)PC_ENABLE_FANUC_J519
FANUC Stream Motion (option J519) UDP codec - the robot counterpart to [FOCAS](#fanuc-focas), which speaks to FANUC CNCs. Default off. Stream Motion is the real-time external motion interface on R-30iB / R-30iA robot controllers: an external controller streams joint (or Cartesian) setpoints over UDP 60015 at the controller's interpolation rate (typically 125 / 250 Hz) and the robot answers every command with its measured state, so a device can close a motion loop around a FANUC arm or simply log it at rate. services/machine_tool/fanuc_j519 is a zero-heap, symmetric codec for that wire protocol. **Framing**: every packet opens with an 8-octet header (a `u32` packet type + a `u32` version) and - unlike FOCAS, which is big-endian throughout - **every multi-octet field is little-endian**, with all real-valued fields IEEE-754 binary32. **PC -> robot**: `pc_j519_build_start` / `_build_stop` emit the 8-octet stream delimiters, `pc_j519_build_motion` emits the 64-octet Motion Command (sequence number, last-data flag, the read-/write-I/O selectors with their port class, index, mask and value, the data style, and the 9 setpoints), and `pc_j519_build_request` emits the 16-octet Request for a motion-limit table. **robot -> PC**: `pc_j519_parse_status` decodes the 132-octet Robot Status (echoed sequence number, the READY / command-received / SYSRDY / in-motion status bits, the read-back I/O value, the controller timestamp, and three 9-axis binary32 blocks - Cartesian pose, joint pose, and per-axis motor current), and `pc_j519_parse_ack` decodes the 184-octet Ack (axis number, threshold type, maximum Cartesian speed, and the two 20-entry threshold tables at no load and at maximum load). The mirrored half (`pc_j519_build_status` / `_build_ack`, `pc_j519_parse_motion` / `_parse_request`) is present too, so the device can stand in as a robot simulator for bench work and every build -> parse round trip is exact. **A note on the type word**: it is reused across directions - type 0 is _Start_ from the PC but _Robot Status_ from the robot, and type 3 is _Request_ from the PC but _Ack_ from the robot - so direction is encoded in the function name rather than a runtime flag, and each parser accepts only its packet's exact length (8 vs 132, 16 vs 184), which is what actually separates the two. Field offsets, packet sizes, type codes, the I/O-type and threshold-type enumerations, and the status bit assignments were taken from the public Wireshark dissector `fanuc-stream-motion/packet-fanuc-stream-motion-j519` - the same class of public reference the FOCAS codec was cross-checked against; no FANUC source, header, or option documentation is used or required. Pure codec, host-tested (`native_fanuc_j519`, 13 cases - every field offset byte-checked, symmetric round trips, and the length/type guards); the caller owns the UDP socket and the real-time cadence, and HW verification against a real controller is still outstanding. See src/services/machine_tool/fanuc_j519/fanuc_j519.h.
Forwarded TrustPC_ENABLE_FORWARDED_TRUST
Trusted-reverse-proxy resolution of the forwarded client address (requires AUTH_LOCKOUT). Default off. A `Forwarded` (RFC 7239) / `X-Forwarded-For` header is client-spoofable, so behind a reverse proxy the per-IP auth lockout keys on the original client address the proxy reports ONLY when the request's real TCP peer matches a trusted-upstream CIDR you register with `pc_forwarded_trust_add_cidr("10.0.0.0/8")`; a direct or untrusted peer's header is ignored, and any malformed / obfuscated / unspecified token falls back to the real TCP peer. So one abusive client behind a proxy cannot lock out every client sharing the proxy's address, and no client can spoof its way out of, or another address into, a lockout. The accept-time throttle and the IP allowlist deliberately stay on the real TCP source. The trusted-CIDR table is a fixed BSS array of PC_TRUSTED_PROXY_MAX entries (no heap); the resolver is pure and host-tested (native_forwarded_trust) and HW-verified on an ESP32-S3 (a forwarded client is locked out independently of the TCP source and of other forwarded clients). See src/services/security/forwarded_trust/forwarded_trust.h.
FTP client sessionalways on
`PC_ENABLE_FTP_SESSION` (requires `PC_ENABLE_FTP`)
GPIBPC_ENABLE_GPIB
GPIB-over-LAN (Prologix-style) controller command codec. Default off. services/instrumentation/gpib is a zero-heap codec for the Prologix-compatible `++` command set that drives a bench of legacy IEEE-488 (GPIB) instruments through a Prologix GPIB-Ethernet / GPIB-USB adapter (raw socket on TCP 1234) - the bridge into pre-LAN test gear that will never speak SCPI-over-TCP directly. It builds the control commands (`pc_gpib_command` for the generic form, plus typed `pc_gpib_addr` / `pc_gpib_read` / `pc_gpib_spoll` / `pc_gpib_eos` helpers - the `++eos` mapping 0=CR+LF / 1=CR / 2=LF / 3=None), builds an escaped data line (`pc_gpib_build_data` - a leading ESC before each CR / LF / ESC / `+` byte in the payload, then an unescaped newline, verified byte-exact against the manual's binary example), classifies a line as command-vs-data (`pc_gpib_is_command`), and parses the responses (`pc_gpib_parse_decimal` for the serial-poll status byte / SRQ / address, `pc_gpib_parse_addr`, `pc_gpib_parse_version`). Command set + escaping + port verified against the Prologix GPIB-Ethernet / GPIB-USB controller manuals (cross-checked with AR488). Carries SCPI ([PC_ENABLE_SCPI](#scpi)) to the instrument as data. Pure codec, host-tested; the socket / serial link to the adapter is the application's. See src/services/instrumentation/gpib/gpib.h.
Haas MDCPC_ENABLE_HAAS_MDC
Haas Machine Data Collection (MDC) Q-command codec. Default off. services/machine_tool/haas_mdc is a zero-heap codec for the documented Haas Automation MDC protocol - the `?Q` query set a Haas CNC mill / lathe control answers over RS-232 (7-E-1, XON/XOFF) or a raw TCP socket (Setting 143, default port 5051) - so a device becomes a fixed-BSS CNC data collector that fans machine status into HTTP / MQTT (alongside the shipped Fanuc [FOCAS](#fanuc-focas), [MELSEC](#melsec), [S7comm](#s7comm) and [DNC](#dnc-cnc-drip-feed) machine-tool codecs). As the collector it builds the numbered queries (`pc_haas_mdc_build_q` for `?Q100` serial / `?Q101` software version / `?Q102` model / `?Q104` mode / `?Q300` power-on time / `?Q500` program+status+parts / ..., taking a `HaasQ` value or a raw command number) and the macro/system-variable read (`pc_haas_mdc_build_var` -> `?Q600 <var>`); then it parses the reply (`pc_haas_mdc_parse` locates the CSV payload framed between STX (0x02) and ETB (0x17) - scanning, never by offset - and splits it into trimmed fields), with typed decoders on top: `pc_haas_mdc_value` (the value of a simple `LABEL, value` reply such as Q100/Q104), `pc_haas_mdc_parse_status` (the two-branch Q500 - `PROGRAM, Oxxxxx, <status>, PARTS, n` when idle/running vs `STATUS, BUSY` mid-operation), `pc_haas_mdc_parse_macro` (`MACRO, <var>, <value>`), and `pc_haas_mdc_is_error` (the `UNKNOWN` reply to an unsupported / lowercase command). It also de-multiplexes the unprompted `DPRNT(...)` lines a running G-code program pushes on the same link (`pc_haas_mdc_dprnt_line` - raw CRLF text with no STX/ETB, optionally bracketed by DC2 (POPEN) / DC4 (PCLOS)). Request framing, the STX/ETB response wrapper, and the Q100/Q104/Q500/Q600 field layouts are verified against the Haas MDC service manual (Setting 143) + the Q-command table and cross-checked byte-for-byte against a production Haas serial adapter. Pure codec, host-tested (`native_haas_mdc`, 10 cases); the serial / TCP link to the control is the application's. See src/services/machine_tool/haas_mdc/haas_mdc.h.
HiSLIPPC_ENABLE_HISLIP
HiSLIP (High-Speed LAN Instrument Protocol, IVI-6.1) message codec. Default off. services/instrumentation/hislip is a zero-heap codec for the IVI Foundation's modern LXI instrument transport on TCP port 4880 - the successor to VXI-11 that carries SCPI ([PC_ENABLE_SCPI](#scpi)) at higher throughput over two TCP channels (a synchronous SCPI command/response stream and an asynchronous out-of-band control channel), correlated by a 16-bit SessionID. `pc_hislip_build_header` / `pc_hislip_parse_header` frame the fixed 16-byte header (`"HS"` prologue + message type + control code + 32-bit MessageParameter + 64-bit PayloadLength, all big-endian); `pc_hislip_build_initialize` / `_initialize_response` / `_async_initialize` / `_async_initialize_response` (+ the matching parsers) drive the two-channel handshake (the MessageParameter carries `(protocol version << 16) | vendor id`, then the negotiated version + SessionID); and `pc_hislip_build_data` frames a Data / DataEND message carrying a SCPI payload keyed by a MessageID (`pc_hislip_next_message_id` implements the initial-`0xFFFFFF00`, increment-by-2 rule). The full `HislipMsg` message-type enum (0-38, including the HiSLIP 2.0 TLS / SASL additions), the header byte layout, and the handshake vectors are verified against IVI-6.1 (cross-checked with the Wireshark dissector, MSL-equipment, and PyHiSLIP). Pure codec, host-tested; the two TCP connections are the application's. See src/services/instrumentation/hislip/hislip.h.
HMMDPC_ENABLE_HMMD
Waveshare HMMD 24 GHz mmWave human micro-motion radar (UART). Default off. The HMMD (Waveshare's FMCW micro-motion module, built on the S3KM1110 / SXKMxxx0 radar SoC) is a close relative of the [LD2410](#ld2410) and shares its framing exactly - a report frame `F4 F3 F2 F1 | len(2) | ... | F8 F7 F6 F5` and a command frame `FD FC FB FA | len(2) | word(2) | [value] | 04 03 02 01`, with every multi-octet field little-endian. Where the LD2410 splits moving vs stationary energy across 9 range gates, the HMMD reports a single detection flag, one target distance, and the per-gate energy of **16** gates: it is a micro-motion detector, so a still person breathing is the case it exists to catch. A report payload is a fixed 35 octets (detect 1 + distance 2 + 16 gates x 2), making a whole frame 45 - which agrees exactly with the reference library's own `kMaxFrameLength`, a useful cross-check on the layout. Unlike the LD2410 there is no intra-payload head/tail marker or check byte, so the header, the footer, and the length agreeing with the buffer are the whole of the validation. `pc_hmmd_parse_report` decodes one frame and `HmmdStream` reassembles frames byte-by-byte from the UART with resync on dropped bytes or noise (fixed buffer, no heap), mirroring `Ld2410Stream` including the widen-before-compare guard that stops an absurd length field from wrapping the bounds check. The command encoders cover open / close command mode (words 0x00FF with value 0x0001, and 0x00FE) plus the firmware-version (0x0000), serial-number (0x0011), parameter-config (0x0008) and register (0x0002) reads, over a public `pc_hmmd_cmd_build` since the module's register and parameter maps are larger than the handful worth naming; the register selector payload is passed through verbatim rather than modelled, because the reference does not specify that map and inventing one would be a guess. `pc_hmmd_parse_ack` decodes replies, handing back the payload whole rather than splitting off a status word - the reference only establishes that the ACK echoes the request's command octet, which is exactly what `pc_hmmd_ack_matches` tests. The module's bare GPIO **OUT** pin carries no protocol at all; feed it to `PresenceCore` from [RCWL-0516](#rcwl-0516) for the same debounced, hold-extended presence, an application-level wiring choice so this service takes no dependency on that one. Framing, payload layout, command words and the open/close encoding come from the public `2Grey/s3km1110` reference library; no vendor SDK is used or required. Pure codec, host-tested (`native_hmmd`, 11 cases); only the UART read/write touches hardware, and it has not been verified against a physical module. See src/services/peripherals/hmmd/hmmd.h.
Hot-Swap StoragePC_ENABLE_HOTSWAP
Opt-in safeties for removable storage that can vanish mid-write. Default off. An SD card is a connector, and the failure when it leaves is a quiet one: the driver still reports a mounted volume, every write fails into nothing, and code that does not check the return carries on believing it has storage. services/storage/hotswap runs one state machine per volume - `ABSENT` (nothing mounted, no filesystem call is safe) / `READY` / `FAULTED` (was mounted, I/O is failing, unmounted and awaiting a remount probe). `pc_hotswap_ready()` is the **fail-closed gate** callers check before any filesystem call, and `pc_hotswap_io(ok)` reports every outcome; `PC_HOTSWAP_FAIL_THRESHOLD` (default 3) consecutive failures declare the medium gone and unmount it immediately so nothing can write through a stale handle, while a probe every `PC_HOTSWAP_PROBE_MS` remounts a card that comes back. The threshold is deliberate: a single failed write is not proof of removal (a transient bus error, a full volume) and tearing down a working mount over one error would be its own bug, so any success resets the run and intermittent noise never accumulates into a false removal. A volume starts ABSENT rather than READY (assuming storage exists because the code compiled is how silent data loss happens at boot), and present-but-unmountable stays ABSENT because a card that will not mount is not storage. The fail counter saturates rather than wrapping, and probe pacing is wrap-safe across a `millis()` rollover. Core is pure and takes an explicit `now`, so the whole machine is host-tested with a synthetic clock (`native_hotswap`, 20 cases); mounting is three app callbacks (mount / unmount / optional card-detect), since how a volume mounts is the application's business. **HW-verified on an ESP32-P4** with a real SD card: three good writes left it `ready`/`faults:0`, an unmount underneath the app made the next three writes really fail and the **third** faulted it (`{"storage":"faulted","mounts":1,"faults":1}`), the following write was **refused** with `storage not ready` rather than attempted, and the probe then remounted it by itself (`{"storage":"ready","mounts":2,"faults":1}`). Serialized as `{"storage":..,"mounts":N,"faults":N}` for a /health panel. Example HotSwapStorage. See src/services/storage/hotswap/hotswap.h.
IKEv2PC_ENABLE_IKEV2
IKEv2 (RFC 7296) message + payload codec. Default off. services/security/ikev2 is a zero-heap builder / parser for the Internet Key Exchange v2 wire format that negotiates IPsec security associations over UDP 500 / 4500 (NAT-T) - it is **tier 1 (the pure framing)** of an IKEv2 / IPsec stack, the standards-track "secure machine bridge over untrusted networks" southbound: the Diffie-Hellman math, the SKEYSEED / SK\_\* key derivation, the SK AEAD, and the IKE_SA_INIT -> IKE_AUTH state machine are later tiers that reuse the crypto the library already ships. It builds / parses the **28-octet IKE header** (`pc_ike_hdr_build` / `_parse` - initiator / responder SPIs, next payload, version, exchange type, flags, message id, length, plus `pc_ike_set_length` to backfill the whole-message length) and walks the **generic payload chain** (`pc_ike_payload_iter_init` / `pc_ike_payload_next` - forward-linked by each payload's next-payload field, with `pc_ike_payload_build` for a raw payload). Typed builders + parsers cover the IKE_SA_INIT / IKE_AUTH payload set: **SA** (`pc_ike_sa_build` writes one proposal with its transforms - ENCR / PRF / INTEG / D-H / ESN - encoding the key-length attribute, and `pc_ike_sa_first_proposal` + `pc_ike_transform_iter_init` / `pc_ike_transform_next` decode the proposal -> transform tree back, reading the key-length attribute), **KE** (`pc_ike_ke_build` / `_parse` - D-H group + key data), **Nonce** (Ni / Nr), **IDi / IDr** (`pc_ike_id_build` / `_parse`), **CERT / CERTREQ** (`pc_ike_cert_build`), **AUTH** (`pc_ike_auth_build` / `_parse` - PSK / RSA / digital-signature methods), **N notify** (`pc_ike_notify_build` / `_parse` - protocol + optional SPI + 16-bit type + data), **D delete** (`pc_ike_delete_build` / `_parse`), **TSi / TSr** traffic selectors (`pc_ike_ts_build` / `pc_ike_ts_count` / `pc_ike_ts_get` - IPv4 / IPv6 address ranges + port ranges), and the **SK** encrypted-payload envelope (`pc_ike_sk_build` / `_parse` frames IV + ciphertext + ICV so a later tier plugs the AEAD in). Every parser bounds-checks and fails closed on a malformed length. The header, payload, and SA / proposal / transform layouts (including the key-length transform attribute) are verified against RFC 7296 + the IANA registry and cross-checked byte-for-byte against scapy's IKEv2 codec. Pure codec, host-tested (`native_ikev2`, 16 cases); the UDP transport, the crypto, and the SA state machine are the application's / later tiers. The protocol constants are **scoped** (`enum class`) and the types flow through the API - `IkePayloadType`, `IkeExchange`, `IkeProtocol`, `IkeIdType`, `IkeAuthMethod`, `IkeTransformType`, `IkeTsType` appear on the struct fields and builder / parser signatures, with a cast only where an octet is written to or read from the wire. That is not cosmetic: RFC 7296 numbers the exchange and payload registries into the same range, so `IKE_SA_INIT` (34) collides with `IKE_PL_KE` (34), `IKE_AUTH` (35) with `IKE_PL_IDI` (35), and so on for every exchange value. Passing the wrong one used to compile silently and emit a structurally valid message a peer would parse and misinterpret; it is now a compile error. See src/services/security/ikev2/ikev2.h.
Interface BridgePC_ENABLE_IFACE_BRIDGE
Opt-in user-defined address:port -> hardware-bus bridge, a configurable "device server". The app registers rules mapping a listen `x.x.x.x:nnnn` (TCP/UDP) to a UART, an SPI chip-select, or an I2C address (`pc_iface_bridge_map(ip, port, proto, target)`), so a network client talking to that port is transparently bridged to the bus: raw bidirectional stream passthrough for UART (a ser2net-style serial device server), or framed write-then-read transactions (`uint16 write_len || uint16 read_len || write_bytes`, big-endian) for the master-initiated SPI / I2C buses, with the bus address / chip-select / clock / mode taken from the rule's target. The fixed-capacity rule table (keyed by port+proto, carrying the full pc_ip bind address, never a flattened one) and the transaction frame codec are a pure, zero-heap, host-tested core (services/net/iface_bridge); the bus I/O (Serial / SPI / Wire) and the PROTO_BRIDGE connection handler are the ESP32 step (iface_bridge_hw), wired up in two calls: `server.listen(port, ConnProto::PROTO_BRIDGE)` then `pc_iface_bridge_publish(listener_id, port, proto, target)`. UART stream mode is a ser2net-style raw pipe pumped by the server poll loop; SPI/I2C transaction mode peeks a whole frame out of the connection ring, clocks it against the bus (I2C uses a repeated-start read), and returns the read bytes. Default off. See src/services/net/iface_bridge/iface_bridge_hw.h and examples/L7-Application/InterfaceBridge.
Logging macrosPC_LOG_LEVEL
Abstract logging whose disabled levels cost nothing at all. Instrumentation is only worth leaving in the source permanently if a build that does not want it pays nothing for it - not a branch, not a call, and not a format string sitting in flash, where a runtime `if (level >= threshold)` still links every message. So the filter is the preprocessor: `PC_LOGD` / `PC_LOGI` / `PC_LOGW` / `PC_LOGE` below `PC_LOG_LEVEL` expand to a form that names its arguments only inside `sizeof(...)`, an unevaluated context, which emits no code and no string literal yet still runs the compiler's printf format checking over them and marks the arguments used (so a variable read only by a log does not warn). Enable the level and the same line starts logging, with no source change. Defaults to `PC_LOG_LEVEL_NONE`, so it is opt-in per build (`-DPC_LOG_LEVEL=PC_LOG_LEVEL_WARN`); at NONE the implementation compiles to nothing at all, defining no symbols and no BSS. An emitted line is formatted once into a `PC_LOG_LINE_LEN` stack buffer and handed to server/logbuf's ring (when `PC_ENABLE_LOGBUF` is on) and to a sink registered with `pc_log_set_sink()` (Serial, syslog, a websocket console). **Measured, not asserted**: a translation unit with four discarded log statements compiles byte-identical to the same function with none (7 bytes .text, 87 total in both), a bad format at a discarded level is still diagnosed by `-Wformat`, and a discarded call never evaluates its arguments. Host-tested (`native_log`); dogfooded by services/ftp_session, whose `PC_LOGD` trace is what localized a real transport-gating bug on hardware. See src/shared_primitives/log.h.
LSV/2PC_ENABLE_LSV2
Heidenhain LSV/2 telegram codec. Default off. services/machine_tool/lsv2 is a zero-heap codec for the LSV/2 protocol Heidenhain TNC controls (iTNC 530, TNC 320 / 620 / 640, ...) speak for DNC and data access over a serial link or, as implemented here, LSV/2-over-TCP (default port 19000) - so a device becomes a fixed-BSS collector for the common European CNC control, alongside the shipped Fanuc [FOCAS](#fanuc-focas), [Haas MDC](#haas-mdc), [MELSEC](#melsec), [S7comm](#s7comm) and [DNC](#dnc-cnc-drip-feed) machine-tool codecs. **Framing** (byte-exact, both directions): a telegram is a 4-byte big-endian payload-length prefix, a 4-character ASCII command / response mnemonic, then the payload - the length counts the payload only (the mnemonic is not included), so a telegram with no payload is exactly 8 bytes on the wire (a bare `T_OK` acknowledgement is `00 00 00 00 'T' '_' 'O' 'K'`). `pc_lsv2_build` frames an arbitrary mnemonic + payload and `pc_lsv2_parse` slices one complete telegram off a byte stream (reporting the consumed byte count so a caller can re-frame the rest). **Typed builders** cover the common requests: `pc_lsv2_build_login` / `pc_lsv2_build_logout` (the `A_LG` / `A_LO` privilege-group access with the `INSPECT` / `FILE` / `DNC` / `MONITOR` / `DIAGNOSTICS` / `PLCDEBUG` groups + optional password), `pc_lsv2_build_filename` (the null-terminated-filename file commands - `R_FL` load, `C_FL` send, `C_FD` delete, `C_DC` change dir, `C_DM` / `C_DD` make / delete dir), and `pc_lsv2_build_run_info` (`R_RI` with a 2-byte big-endian run-info selector: execution state / selected program / override / program state). **Response readers** decode the reply mnemonic (`pc_lsv2_is_ok` for `T_OK`, `pc_lsv2_is_error` for the `T_ER` / `T_BD` errors, `pc_lsv2_error` for their two-byte error-class + error-code, and `pc_lsv2_is` for the `S_*` data replies). The telegram framing, mnemonic set, and the login / filename / run-info payload layouts are cross-checked byte-for-byte against the pyLSV2 reference (drunsinn/pyLSV2). Pure codec, host-tested (`native_lsv2`, 12 cases); the serial / TCP link to the control is the application's. See src/services/machine_tool/lsv2/lsv2.h.
NTRIP CasterPC_ENABLE_NTRIP_CASTER
Opt-in GNSS RTK base station + NTRIP caster (services/timing_position/gnss). Default off (implies `PC_ENABLE_NMEA0183`). Turns the device into a differential-GNSS correction source: it surveys in a fixed antenna position and serves RTCM 3.x corrections to rovers over the network (the NTRIP protocol), so a rover applies them for RTK / DGPS accuracy instead of the ~2.5 m of a bare receiver. Three pure, zero-heap, host-tested cores plus a ConnProto listener: (1) the RTCM3 codec (services/timing_position/gnss/rtcm3) - the transport frame (0xD3 preamble, 6 reserved + 10-bit length, payload, 24-bit CRC-24Q), MSB-first bit I/O, and the Stationary Antenna Reference Point messages 1005 (no height) / 1006 (with antenna height) that advertise the base's surveyed ECEF position (38-bit signed coordinates at 0.0001 m resolution), verified byte-for-byte against pyrtcm; (2) the survey-in core (services/timing_position/gnss/pc_gnss_survey) - the exact WGS84 geodetic&lt;-&gt;ECEF transform (matched against pyproj), a shifted-origin position averager with a 3-D accuracy estimate and a min-observations / accuracy-limit convergence gate, and a GGA-fix fold (ellipsoidal height = MSL + geoid separation); (3) the NTRIP caster protocol (services/timing_position/gnss/pc_ntrip_caster) - rover request parsing (mountpoint, NTRIP 1.0 / 2.0 version, optional HTTP Basic auth), the stream-accept / error / 401 responses, and the RTCM source table (STR records + ENDSOURCETABLE). The `ConnProto::PROTO_NTRIP_CASTER` listener (services/timing_position/gnss/pc_ntrip_caster_listener) answers rovers and fans RTCM corrections out to every subscriber, published like the relay: `server.listen(2101, ConnProto::PROTO_NTRIP_CASTER)` then `pc_ntrip_caster_add_mount()` / `pc_ntrip_caster_broadcast()`. Example NtripCaster runs a base (survey-in + caster) and a rover (NTRIP client that CRC-validates and decodes the 1005) on two boards. Generating RTCM3 _observation_ messages (the MSM sets 1074/1077/1084/... that let a rover fix carrier-phase ambiguities for centimeter RTK) requires a receiver that outputs raw measurements (u-blox RXM-RAWX: F9P / M8T class); a raw-less module (NEO-6/7, GT-U7) can still survey in and serve the reference point + sourcetable. See src/services/timing_position/gnss/pc_ntrip_caster_listener.h.
OPC UA Robotics (MotionDevice)PC_ENABLE_ROBOTICS
OPC UA for Robotics information model (OPC 40010-1). Default off (requires OPC-UA). services/machine_tool/robotics exposes a fixed MotionDeviceSystem node hierarchy - MotionDevices (MotionDevice / ParameterSet / parametric Axes), Controllers (Controller / Software), and SafetyStates (SafetyState / ParameterSet) - through the OPC UA Browse + Read resolvers, served out of a caller-owned RoboticsMotionDeviceSystem struct you refresh each loop. The axis count is parametric (`PC_ROBOTICS_AXES`, default 6): each bound axis becomes an Axis_k object with ActualPosition / ActualSpeed / ActualAcceleration / MotionProfile. Faithful BrowseNames per OPC 40010-1 (namespace `http://opcfoundation.org/UA/Robotics/`); a read-only monitoring model any OPC UA client browses and reads by BrowseName. The twin of umati (machine tools). No heap, no stdlib.
PackMLPC_ENABLE_PACKML
PackML / OMAC packaging-machine state model. Default off. services/machine_tool/packml is a zero-heap implementation of the ISA-TR88.00.02 (ISBN 978-1-64331-224-8, "Machine and Unit States: An Implementation Example of ISA-88") PackML state model that packaging and process machines expose so a line controller - usually over OPC UA - can command and observe them uniformly. Two layers. The **pure state engine**: `pc_packml_command(state, cmd)` applies a control command (Reset / Start / Stop / Hold / Unhold / Suspend / Unsuspend / Abort / Clear) and returns the resulting state across the 17-state model (the wait states Stopped / Idle / Execute / Held / Suspended / Complete / Aborted plus the transient "acting" states Starting / Resetting / Holding / Unholding / Suspending / Unsuspending / Completing / Stopping / Aborting / Clearing), with Stop and Abort as the near-universal transitions to the shutdown and fault branches; `pc_packml_state_complete(state)` auto-advances an acting state to its target when its action finishes (Starting -> Execute, Aborting -> Aborted, ...); `pc_packml_execute_complete(state)` ends a production run (Execute -> Completing); and `pc_packml_command_valid` / `pc_packml_is_acting` / `pc_packml_state_name` / `pc_packml_command_name` classify and label. The `PackMlState` enum's underlying value is the ISA-TR88.00.02 StateCurrent wire number (Stopped=2, Execute=6, Aborted=9, Complete=17, ...) so a PackTags Status.StateCurrent needs no translation. The **owned service** (`pc_packml_svc_*`) layers the PackTags on top: it holds the current state + unit mode (Producing / Maintenance / Manual), advances on command / state-complete, tracks the ProdProcessedCount / ProdDefectiveCount production counters (only while executing), enforces the unit-mode-change rule (allowed only in a stable non-producing state), reflects the commanded MachSpeed as MachSpeedActual while in Execute, and reports the StateCurrentTime / AccTimeSinceReset timers off the pluggable clock. Pure and host-tested (`native_packml`, 16 cases covering the full transition graph, command validity, counters, mode rules, speed, and timers) and HW-verified on an ESP32-P4 - the whole state graph, the fault branch (Abort -> Aborted -> Clear -> Stopped), the production counters, and illegal-command rejection (HTTP 409, state unchanged) driven live over the example's HTTP routes; the OPC UA / tag transport is the application's. See src/services/machine_tool/packml/packml.h.
Post-Quantum Hybrid KEXPC_ENABLE_PQC_KEX
Post-quantum / traditional hybrid key exchange: ML-KEM-768 (FIPS 203) combined with X25519. Closes the harvest-now-decrypt-later gap - OpenSSH 9.9+ and current browsers now DEFAULT to a hybrid group, so without this the device negotiates DOWN to classical X25519. When set (and SSH is on) the server advertises `mlkem768x25519-sha256` (draft-ietf-sshm-mlkem-hybrid-kex) first in its SSH KEX list and, on selection, ML-KEM-Encaps to the client's key + X25519, combining `K = SHA256(K_PQ || K_CL)` per the RFC 9370 concatenation combiner. As a server the device is the KEM responder (Encaps); as a reverse-SSH CLIENT (`PC_ENABLE_SSH_CLIENT`) it is the KEM initiator, so KeyGen and Decaps also ship - Decaps carries the full constant-time Fujisaki-Okamoto transform (re-encrypt m' under the embedded ek, select the real key vs the implicit-reject key `J(z || ct)` under a constant-time ciphertext compare, FIPS 203 sec 6.3). The ML-KEM core (network_drivers/presentation/pqc) is a software NTT over q=3329 with Montgomery reduction plus a Keccak/SHA-3/SHAKE sponge (FIPS 202); zero heap, peak ~7 KB (Encaps) to ~9 KB (Decaps) of worker stack (raise `PC_WORKER_TASK_STACK` to >= `PC_WORKER_STACK_PQC_MIN` = 16384). KeyGen, Encaps and Decaps are all byte-exact against the FIPS 203 reference (kyber-py): keygen `(d,z)->(ek,dk)`, encaps `(ek,m)->(ct,ss)`, decaps `(dk,ct)->ss`, the initiator/responder round-trip, and the FO implicit-reject. Wired into both transports from the one core: the SSH key exchange (`PC_ENABLE_SSH`, `mlkem768x25519-sha256`, K = SHA256(K_PQ || K_CL), K as an RFC 4251 string) and the HTTP/3 QUIC TLS 1.3 handshake (`PC_ENABLE_HTTP3`, the **X25519MLKEM768** group, IANA 0x11ec, ML-KEM-first client/server shares and a 64-byte ML-KEM || X25519 secret into the key schedule). A PQC-capable peer (OpenSSH 9.9+, current browsers) negotiates the hybrid; others fall back to classical X25519. Default off.
Power GovernorPC_ENABLE_POWER_MGMT
Opt-in SoC power governor. Default off. network_drivers/physical/radio_power owns the radio and server/sleep_sched decides how _long_ to sleep; neither owns the SoC, which is where the rest of the power budget goes. `pc_power_plan()` decides the CPU clock every tick from three inputs, with a deliberate precedence - **brownout beats thermal beats load**, because a board that cannot hold its supply must not be clocked up merely because it is busy, and neither must a hot one. **Scaling**: an idle server drops to `PC_POWER_MHZ_MIN` instead of spinning a 240 MHz core to poll a quiet socket. **Thermal throttle**: a hot die clocks down, and the restore threshold is lower than the throttle threshold - the caller feeds the previous tick's `throttled` flag back in, which is what supplies the hysteresis. **Brownout recovery**: after an `ESP_RST_BROWNOUT` reset (read once and latched at boot) the part holds the floor clock for `PC_POWER_RECOVER_MS` rather than slamming back into the load that collapsed the rail and boot-looping. **Gating**: `pc_power_gate_bt()` disables and releases the Bluetooth controller's power domain on a build that never uses BT, which draws current whether or not anything is connected. A part with no usable internal sensor (classic ESP32) reports `INT16_MIN`, treated as "no reading" rather than ice-cold, so it neither throttles nor silently releases one. Pure decision core taking every input explicitly, host-tested (`native_power_mgmt`, 19 cases incl. a 20-tick feedback loop asserting exactly one throttle transition). **HW-verified on an ESP32-S3** with its real die sensor: the Bluetooth domain was released, an idle server clocked itself 240 -> 80 MHz, `GET /busy` took it to 240 MHz with the die rising 36 -> 38 C and back, and with the thresholds moved to a reachable band the throttle engaged at exactly `temp_hot_c` and released at exactly `temp_cool_c`. That run also produced a tuning rule now documented at the flag: **the hysteresis band must be wider than the temperature swing the clock change itself causes** (measured ~2 C for 240 -> 80 MHz on an S3 within one 500 ms tick), or the throttle's own effect carries the die back across the release threshold and it self-sustains regardless of how correct the comparison is - which is why the default band is 10 C. Example PowerGovernor. See src/server/power_mgmt.h.
RCWL-0516PC_ENABLE_RCWL0516
RCWL-0516 microwave Doppler presence sensor, and the shared one-GPIO presence facade. Default off. The RCWL-0516 (RCWL-9196 controller + MMBR941M RF amp, ~3.18 GHz Doppler) has no data protocol at all - a single 3.3 V **OUT** pin that latches HIGH on a moving reflector and returns LOW once its own retrigger window expires - so the entire problem is timing, not bytes, and a bare `digitalRead()` gets it wrong in two ways this fixes. **Chatter**: the pin is comparator-driven, so around the detection threshold it flickers and one person walking past becomes a burst of presence events; a level must therefore hold for `PC_RCWL0516_DEBOUNCE_MS` (default 50) before it is believed. **Gaps**: the module drops OUT between retriggers, so a present-but-briefly-still person reads as absent for a moment; presence is therefore held for `PC_RCWL0516_HOLD_MS` (default 2000, matching the module's ~2 s retrigger window) past the last believed-HIGH sample, turning a stream of retriggers into one continuous occupied span instead of a flapping boolean. `pc_presence_core_update` feeds one sample and returns the resulting presence, `pc_presence_core_get` reads it without sampling, and `pc_presence_take_event` consumes a changed-flag exactly once per transition so a caller publishes one event per edge rather than re-publishing a level every poll. The core is level-driven rather than edge-driven, so a missed poll delays a transition instead of losing it, and it starts **absent** - presence is only reported once actually observed. Pure and taking an explicit `now` like [Hot-Swap Storage](#hot-swap-storage), so the whole machine is host-tested against a synthetic clock with no GPIO and no real time (`native_rcwl0516`, 10 cases incl. chatter rejection, retrigger-gap bridging, and a `millis()` rollover); every elapsed-time test is an unsigned difference, which is what makes it wrap-safe. `PresenceCore` is deliberately sensor-agnostic - the RCWL-0516 is just its first user via `pc_rcwl0516_core_init`, and an HMMD OUT pin, a PIR, or an HB100 reuse the same core by supplying their own two constants. Not HW-verified against a physical module. See src/services/peripherals/rcwl0516/rcwl0516.h.
Safety SCL (IEC 61784-3)PC_ENABLE_SAFETY_SCL
IEC 61784-3 black-channel Safety Communication Layer primitives. Default off. The functional-safety profiles - PROFIsafe (IEC 61784-3-3), CIP Safety (-3-2), FSoE (-3-12), IO-Link Safety - all work the same way: they treat the underlying fieldbus as an untrusted "black channel" and layer their own end-to-end checks on top, so the transport may corrupt, lose, duplicate, delay or reorder frames without defeating the safety function. Three mechanisms do that work and are common to every profile: a **CRC signature** over the safety payload, a **monitoring / consecutive counter**, and a **receive watchdog**. services/machine_tool/safety_scl lands the counter state machine and the watchdog, plus the fail-safe state machine that combines them with the caller's signature verdict, so each profile's codec composes these instead of reimplementing them. `pc_scl_on_frame` accepts a frame only when the signature is good **and** the counter is exactly the expected next value - which is what makes one comparison sufficient for four distinct failure modes, since a lost or inserted frame runs the counter ahead, a duplicate repeats it, and a reordered pair sends it backwards. `pc_scl_poll` runs the receive watchdog every cycle including cycles with no frame, because a silent channel is itself a fault rather than a stale-but-happy reading; it does not arm until the first frame has been accepted, since a connection that has never received anything is starting up rather than timing out. **Fail-safe latches**: any failure puts the connection in `FAILSAFE` and it stays there until an explicit `pc_scl_reset` - a safety layer that silently reheals would let an intermittent fault present as a working link, which is precisely what a SIL rating exists to exclude, so even a subsequent perfectly good frame must not revive it. The first fault is preserved rather than overwritten by later ones, and the accepted/rejected tallies survive a reset so a flapping link stays visible. `pc_scl_next_counter` is exposed for the sender so both ends wrap a narrow consecutive number identically - the commonest way a hand-rolled counter desynchronizes after its first wrap. **This module deliberately does not compute the CRC**: every profile defines its own polynomial, width, seed and the order payload / counter / connection identifier are fed in, those constants live in paid standards, and a guessed CRC in a safety layer would look authoritative while silently failing to detect the very corruption it exists to catch - so the caller computes its profile's CRC and passes the verdict in as `signature_ok`, and this module owns the consequence, which genuinely is profile-independent. Pure and taking an explicit `now` like [Hot-Swap Storage](#hot-swap-storage), so the whole machine is host-tested against a synthetic clock (`native_safety_scl`, 16 cases incl. all four black-channel failure modes, a `millis()` rollover, and the no-silent-reheal latch); every elapsed-time test is an unsigned difference, which is what makes the watchdog wrap-safe. No heap, no stdlib. See src/services/machine_tool/safety_scl/safety_scl.h.
SCPIPC_ENABLE_SCPI
SCPI / IEEE 488.2 instrument-control codec. Default off. services/instrumentation/scpi is a zero-heap codec for the text command language nearly every modern bench instrument speaks (DMMs, oscilloscopes, power supplies, function/arbitrary generators, SMUs, spectrum/network analyzers, electronic loads) over a raw TCP socket on port 5025 (also USBTMC / VXI-11 / HiSLIP / serial). The codec is symmetric: as a **controller** it builds command lines with `pc_scpi_build` (a `:`-hierarchy header + comma-separated params + newline) and `pc_scpi_common` (the 13 mandatory IEEE 488.2 commands `*IDN?` / `*RST` / `*CLS` / `*ESR?` / `*STB?` / ...), then parses replies - `pc_scpi_parse_number` (NR1/NR2/NR3, hand-rolled, no stdlib), `_parse_bool` (`1`/`0`/`ON`/`OFF`), `_parse_string` (quote-stripping with doubled-quote collapse), and `_parse_block` (the definite `#<n><len><data>` and indefinite `#0<data>` arbitrary block used for waveform captures); as an **instrument** it carries the IEEE 488.2 status model - `ScpiStatus` (the Status Byte, Standard Event Status Register + its enable mask, the Service Request Enable mask, and the FIFO error/event queue) with `pc_scpi_push_error` (latching the CME/EXE/DDE/QYE/... ESR class bit from the error-number range and replacing the tail with -350 "Queue overflow" when full), `pc_scpi_pop_error` (the `SYSTem:ERRor?` action, `0,"No error"` when empty), `pc_scpi_stb` (EAV/ESB/MSS computation), and `pc_scpi_cls` (`*CLS`) - plus `pc_scpi_match`, a SCPI short/long-form + numeric-suffix header matcher for dispatching an incoming command against a pattern like `"SYSTem:ERRor?"`. Register bits, error-number classes, and the exact standard error strings are verified against the SCPI-1999 standard and IEEE 488.2-1992. Pure codec, host-tested; the TCP/USB/serial transport is the application's. See src/services/instrumentation/scpi/scpi.h.
SEN0192PC_ENABLE_SEN0192
DFRobot SEN0192 10.525 GHz microwave Doppler motion sensor (single digital OUT line). Default off. Unlike a PIR it senses movement through thin non-metal enclosures and is unaffected by ambient light or temperature; unlike the LD2410 it carries no protocol - just one digital line - so services/peripherals/sen0192 tracks that line as a debounced presence signal: presence asserts on an active sample and is held for `PC_SEN0192_HOLD_MS` after the last active sample (so brief gaps between Doppler returns don't make presence flap), clears after it, and counts clear-to-present edges. The presence state machine (`Sen0192Motion`) is pure - it takes a sampled level and a timestamp, needing no clock or GPIO - and host-tested (`native_sen0192`); the ESP32 binding reads `PC_SEN0192_PIN` each poll via pc_millis() and only that read touches hardware. The OUT pin, hold window, and polarity are ServerConfig knobs (`PC_SEN0192_PIN` / `PC_SEN0192_HOLD_MS` / `PC_SEN0192_ACTIVE_HIGH`). Example Sen0192 lights the onboard LED on motion. See src/services/peripherals/sen0192/sen0192.h.
SIMATIC SerialPC_ENABLE_SIMATIC
Siemens SIMATIC serial point-to-point codec (3964R + RK512). Default off. services/fieldbus/simatic is a zero-heap implementation of the pre-Ethernet Siemens point-to-point link the S5/S7 PtP CP modules (CP 341 / CP 441 / CP 524 / CP 525) speak - so a device becomes a fixed-BSS collector/agent for legacy Siemens controls, alongside the shipped Fanuc [FOCAS](#fanuc-focas), [Haas MDC](#haas-mdc), [MELSEC](#melsec), [S7comm](#s7comm) and [DNC](#dnc-cnc-drip-feed) machine-tool codecs. Two layers. **3964R** (the byte-oriented, half-duplex link protocol): `pc_3964r_build_block` frames a block as the DLE-doubled payload + `DLE ETX` + (on the "R" variant) the BCC - the longitudinal **XOR** (even parity, not DF1's 2's-complement sum) over the stuffed data and the `DLE ETX`, so a doubled DLE pair cancels (`0x10 ^ 0x10 = 0`); `pc_3964r_parse_block` un-stuffs and validates, fail-closed on bad framing / a lone DLE / a missing terminator / a BCC mismatch / overflow. On top, an owned link state machine (`Simatic3964Ctx`, no file-scope mutable) drives the interactive handshake: the sender emits `STX` and waits for the receiver's connect `DLE` within **QVZ**, sends the block, waits for the end `DLE`/`NAK`, and repeats the block up to 6× then the connection up to 6× on `NAK`/timeout; the receiver replies `DLE` to a partner `STX`, collects the block enforcing the **ZVZ** inter-character timeout, and answers `DLE` (ok) or `NAK` (bad); on a simultaneous `STX` collision the **low-priority** station yields to receive (a per-context priority bit). Timers run off the pluggable `pc_millis()` (injectable for host tests); no `delay()`. **RK512** (the computer-link telegrams carried as the 3964R block payload): `pc_rk512_build_send` / `pc_rk512_build_fetch` build the fixed header (command + coordination byte + area code + DB number + big-endian start address + word count) plus the big-endian data words for SEND, and `pc_rk512_parse_header` / `pc_rk512_parse_reaction` decode a request header and a reaction telegram (status word 0 = ok, plus any FETCH-returned words). The 3964R control-char handshake, QVZ/ZVZ timeouts, priority arbitration and the XOR-BCC follow the Siemens "3964(R) transmission protocol" manual; the RK512 header field order + big-endian words follow the "RK 512 computer link" manual (the exact area/command byte values are pinned per CP-module doc). The two layers are cleanly separated (RK512 never touches DLE/STX; 3964R never interprets the telegram). Pure codec + one owned link context, host-tested (`native_simatic`, 14 cases across framing, the link state machine, and RK512) and cross-checked byte-for-byte against an independent python 3964R+RK512 reference implementation (`test/servers/peers/simatic_peer.py`); the RS-232 / RS-485 UART is the application's. MPI (token-bus network layer) and the generic PtP IF are a separate, larger follow-on. See src/services/fieldbus/simatic/simatic.h.
SMTP TLSPC_ENABLE_SMTP_TLS
Secure SMTP: run the mail client over client-side TLS (needs TLS). Covers both `SmtpSecurity::SMTP_TLS` (implicit, port 465) and `SmtpSecurity::SMTP_STARTTLS` (the RFC 3207 in-band upgrade on the submission port, 587). Separate from `PC_ENABLE_SMTP` because the plain codec needs neither the TLS stack nor the client-session singleton, and it is what pulls SMTP into the `PC_ENABLE_CLIENT_TLS` derivation - without that every `pc_tls_client_session_*` call resolves to a stub that always fails. See src/services/net/smtp/smtp.h.
SSH Keyboard-InteractivePC_ENABLE_SSH_KEYBOARD_INTERACTIVE
Keyboard-interactive user authentication (RFC 4256) alongside password/publickey. When set (and SSH is on) the server advertises `keyboard-interactive` in the `USERAUTH_FAILURE` method list; on selection it sends one `SSH_MSG_USERAUTH_INFO_REQUEST` with a single non-echoed `Password:` prompt and verifies the client's `SSH_MSG_USERAUTH_INFO_RESPONSE` through the same `pc_ssh_auth_set_password_cb` callback - so it is the challenge-response face of password auth (the common OpenSSH `-o PreferredAuthentications=keyboard-interactive` / PAM-password case), not a second credential store. Per-slot exchange state lives in the auth owner: an `INFO_RESPONSE` with no armed exchange (or a replayed one) is refused, and the response is wiped from the stack after every attempt. Because it is password-backed it requires `PC_SSH_ALLOW_PASSWORD`. Default off. HW-verified on an ESP32-P4 vs OpenSSH 10: authenticated via keyboard-interactive with a byte-exact channel echo.
SSH Reverse TunnelPC_ENABLE_SSH_CLIENT
Outbound SSH client + reverse tunnel - the mirror of the SSH server: the device dials OUT to a relay with a public address, authenticates, and requests a remote forward (`tcpip-forward`, RFC 4254 sec 7.1, the `ssh -R` seam), so a connection to the relay's forwarded port is tunnelled back to a local service on the device. This is how a device behind NAT / a firewall stays reachable over one authenticated SSH port (e.g. serve its web UI to the world via a cloud relay). Default off. The client negotiates the FULL modern suite and interoperates with any current SSH server (RFC 4253 sec 7.1 order): KEX `mlkem768x25519-sha256` (PQ/T hybrid, when `PC_ENABLE_PQC_KEX`) and `sntrup761x25519-sha512@openssh.com` (PQ/T hybrid, when `PC_ENABLE_SSH_SNTRUP761`; as the KEM initiator it runs sntrup761 KeyGen+Decaps and derives over SHA-512), `curve25519-sha256`, `ecdh-sha2-nistp256`, `diffie-hellman-group14-sha256`; host keys `ssh-ed25519` / `ecdsa-sha2-nistp256` / `rsa-sha2-512` / `rsa-sha2-256`, each verified against a caller-supplied pin (the SHA-256 of the relay's host-key blob - type-agnostic, no trust-on-first-use); ciphers `chacha20-poly1305@openssh.com` / `aes256-gcm@openssh.com` / `aes256-ctr` (+ hmac-sha2-256/512, ETM and E&M); the device authenticates with its own ssh-ed25519 key (publickey). Reuses the server's transport crypto through a role-aware packet layer (an `is_client` flag flips the send/receive key direction; the RFC 4253 sec 7.2 KDF letters are role-independent) - this file is the client state machine only, not a second crypto stack. Forwarded connections are bridged through a channel POOL (`PC_SSH_CLIENT_MAX_CHANNELS`, per-variant default) so concurrent / rapid requests each get a channel; each opens one local bridge connection, so the outbound pool auto-provisions `PC_CLIENT_CONNS >= 1 + PC_SSH_CLIENT_MAX_CHANNELS`. The tunnel runs in the caller's task and owns a dedicated pool slot (`PC_GHOST_WORKER_SLOT`); drive `pc_ssh_tunnel_begin()/poll()/end()` from one task with enough stack for the negotiated KEX (curve25519/ed25519 ~10.5 KB; the ML-KEM hybrid ~16 KB; the sntrup761 hybrid ~32 KB - a dedicated 40 KB task with sntrup761 enabled, else 20 KB, not `loop()`'s 8 KB). HW-verified against OpenSSH 10.0 on an ESP32-S3: curve25519 KEX + ed25519 pin/auth + `tcpip-forward` + the forwarded-tcpip bridge to the device's `:80`, concurrent and rapid-sequential requests returning the device's response byte-for-byte.
Streaming Request BodyPC_ENABLE_STREAM_BODY
Opt-in streaming delivery of large request bodies. Default off. Normally a request body is buffered whole and handed to the route once complete; with this flag the HTTP/1.1 parser instead drives per-chunk stream hooks (begin / data / abort) as body bytes arrive, so an arbitrarily large upload - a firmware image, a WebDAV `PUT`, a large multipart part - is written straight to its destination (flash, a file) as it streams, without ever holding the whole body in RAM. This preserves the zero-heap, bounded-memory contract for uploads that exceed any fixed buffer: memory use is one `RX_BUF_SIZE` window regardless of body size. The canonical consumer is a WebDAV `PUT` under a DAV mount (`dav_stream_put_begin` / `dav_stream_put_data` / abort trampolines wire the parser hook to the file), and the OTA update path uses the same seam. Because the body is never fully buffered, it raises the minimum `RX_BUF_SIZE` (see `src/board_drivers/board_profiles/derived_sizing.h`). The parser streaming hook is host-tested (`native_ota`); the FS-backed WebDAV PUT path is hardware-verified. See src/network_drivers/presentation/http/http_parser/http_parser.cpp.
TLS Raw Public KeysPC_ENABLE_TLS_RPK
Raw Public Keys (RFC 7250) for the hand-rolled TLS 1.3 stack (requires DTLS or HTTP/3). Default off. A cert-less TLS credential: when a client offers the `server_certificate_type` extension (IANA 20) with `RawPublicKey`(2), the server answers with that certificate type in EncryptedExtensions and sends a Certificate message whose entry is a bare DER `SubjectPublicKeyInfo` (the 44-byte Ed25519 SPKI, RFC 8410) instead of an X.509 chain. The same Ed25519 key still signs CertificateVerify, so there is no security downgrade - only a smaller handshake with no certificate to parse, the natural fit for provisioned, key-pinned ESP32 fleets and the RFC 7252 §9 CoAP-over-DTLS RawPublicKey profile. Purely additive and server-side: a client that does not offer the extension still receives the X.509 certificate, and the handshake never requests a client certificate. Wired into the DTLS 1.3 handshake (`pc_dtls_conn`); the shared TLS 1.3 codec (`pc_tls13_msg`) also carries it for a future HTTP/3 use. The SPKI encoder, the RawPublicKey Certificate, the `server_certificate_type` EncryptedExtensions, and the ClientHello parse are pure and host-tested (`native_dtls_tls13`), and an end-to-end DTLS 1.3 handshake completes with the server presenting the raw key and the client verifying CertificateVerify against it (`native_dtls_conn`). Verified **against two independent reference implementations**: the wolfSSL DTLS 1.3 client in RawPublicKey-only mode (`--rpk`, so it rejects an X.509 answer) and the CycloneSSL DTLS 1.3 client (whose RFC 7250 verify callback fires only on a real RawPublicKey) each complete a full handshake and an application-data round trip with the server (test/servers/pc_dtls_wolfssl, test/servers/cyclone_dtls) - proof the presented `SubjectPublicKeyInfo` is wire-conformant, not self-consistent. Pure, zero-heap. See src/network_drivers/presentation/http/http3/pc_tls13_msg.h.
Trace CapturePC_ENABLE_TRACE_CAPTURE
Pre/post-trigger sample-window assembler - the high-rate acquisition primitive that sits downstream of mmgr/dma on a sampling front end. Default off. `pc_tc_feed()` is called with every batch of samples as they arrive (most naturally from a DMA-complete handler) into a continuously running **pre-trigger ring** that always holds the most recent `pretrigger_samples`. When `pc_tc_trigger()` fires - a GPIO ISR, a software threshold detector, or an external front-end's own trigger line - the ring's current contents become the pre-trigger half of the emitted window and subsequent feeds fill the post-trigger half, so the window straddles the trigger instant exactly like a benchtop oscilloscope's pretrigger/posttrigger split, even though the trigger is only detected after the pre-trigger samples already went by. One capture in flight at a time, fail-closed: a trigger while a window is still filling is rejected and counted (`triggers_dropped`), never queued or stomped - the same determinism contract as mmgr/dma's one-TX-in-flight rule. Static storage, no heap. The natural source is an external ADC front-end (an AD9226/AD9238 draining over SPI or UART DMA) or a scope digitizer. Host-tested (`native_trace_capture`). See src/server/signaling/trace_capture.h.
UBX (u-blox binary)PC_ENABLE_UBX
u-blox UBX binary GNSS protocol codec - the binary companion to NMEA 0183 that u-blox receivers speak on the same UART. Default off. services/timing_position/ubx is a zero-heap codec for the UBX frame `B5 62 <class> <id> <len-LE> <payload> <CK_A> <CK_B>`: `pc_ubx_build` emits a frame (adding the sync chars, the little-endian length, and the 8-bit Fletcher checksum), `pc_ubx_build_poll` emits the zero-length poll request that asks a receiver to send a message, `pc_ubx_parse` validates one frame (sync chars, declared length against the buffer, and checksum) and exposes its class/id/payload, `pc_ubx_ack` decodes UBX-ACK-ACK / -NAK, and `pc_ubx_u16` / `pc_ubx_u32` / `pc_ubx_i32` read little-endian payload fields. `pc_ubx_parse_nav_pvt` decodes the all-in-one UBX-NAV-PVT navigation solution (the message most applications read) into a `pc_ubx_nav_pvt` - GPS/UTC time with validity + accuracy, fixType + gnssFixOK + satellite count, lat/lon (1e-7 deg) and ellipsoid/MSL height with horizontal/vertical accuracy, NED + ground velocity with heading, and pDOP - verified field-by-field against a u-blox-8 reference frame. `pc_ubx_parse_nav_sat` + `pc_ubx_nav_sat_get` decode the variable-length UBX-NAV-SAT (per-satellite signal + usage): the header gives the satellite count, then each block yields the GNSS/SV id, carrier-to-noise (dB-Hz), elevation/azimuth, pseudorange residual, and the flags (signal-quality indicator + the used-in-solution bit), with the declared length checked against the block count. `pc_ubx_parse_nav_timeutc` decodes UBX-NAV-TIMEUTC (the receiver's validated UTC time solution) into a `pc_ubx_nav_time_utc` - iTOW, the estimated time accuracy (ns) and the sub-second nano correction, the broken-down UTC year/month/day/hour/minute/second, and the validity byte (validTOW / validWKN / validUTC), where the validUTC bit means the leap-second count is resolved so the wall-clock fields are trustworthy - the message a PTP grandmaster or an SNTP source reads to discipline its clock. To configure the receiver, `pc_ubx_build_cfg_msg` emits a CFG-MSG that sets how often a given message is output (rate 0 disables it, N sends every Nth solution) and `pc_ubx_build_cfg_rate` a CFG-RATE that sets the measurement period + navigation rate (e.g. 200 ms / 5 Hz) and the time reference - so an app turns on NAV-PVT / NAV-SAT and dials the update rate, then decodes what it asked for. Because a u-blox module multiplexes UBX with ASCII NMEA on one link, `pc_ubx_stream_feed` is a byte-at-a-time demultiplexer: it pulls complete, checksum-valid UBX frames out of the stream and hands every non-UBX byte back to the caller for an NMEA line assembler (so one UART carries both, with an over-long-frame guard bounded by PC_UBX_MAX_PAYLOAD). Frame framing + Fletcher checksum verified against the published u-blox poll frames (UBX-MON-VER `B5 62 0A 04 00 00 0E 34`, UBX-CFG-PRT `... 06 18`); pure and host-tested (`native_ubx`). GNSS receivers are cheap UART breakouts, so this is a plain HardwareSerial link; send config/poll as UBX, read fixes as UBX or NMEA. Pairs with services/nmea0183. Example UbloxGnss. See src/services/timing_position/ubx/ubx.h.
VXI-11PC_ENABLE_VXI11
VXI-11 (TCP/IP Instrument Protocol) codec over ONC RPC / XDR. Default off. services/instrumentation/vxi11 is a zero-heap codec for the legacy LXI instrument transport that predates HiSLIP ([PC_ENABLE_HISLIP](#hislip)) - VXI-11 rides on ONC RPC (Sun RPC, RFC 5531) with XDR (RFC 4506) over TCP, carrying SCPI ([PC_ENABLE_SCPI](#scpi)). It provides the reusable ONC-RPC framing - `pc_rpc_record_mark` / `pc_rpc_parse_record_mark` (the TCP record-marking header) and `pc_rpc_parse_reply` (the accepted-reply header with AUTH_NONE) over big-endian, 4-byte-aligned, length-prefixed XDR (no sub-word types on the wire) - plus the DEVICE_CORE procedures: `pc_vxi11_build_create_link` / `_device_write` / `_device_read` / `_device_readstb` / `_device_clear` (empty the instrument's I/O buffers) / `_device_trigger` (the protocol-level `*TRG`) / `_destroy_link` (program `0x0607AF` v1) with their response parsers, and the portmapper `pc_vxi11_build_getport` call that maps the program to its dynamic TCP port (the instrument channel is not on a fixed port). Device_Flags (waitlock / end / termchrset), the read `reason` bits (REQCNT / CHR / END), and the Device_ErrorCode set are covered; `pc_vxi11_error_str` names an error. The XDR struct layouts, procedure numbers, and RPC headers are verified against the VXI-11 spec + RFC 5531 / 4506 / 1833 (cross-checked with python-vxi11 and the Wireshark dissector), with a byte-exact create_link vector. Pure codec, host-tested; the TCP connection is the application's. See src/services/instrumentation/vxi11/vxi11.h.
Wi-Fi RoamingPC_ENABLE_ROAMING
Wi-Fi roaming decision layer. Default off. network_drivers/network/roaming is the pure, stateless policy that decides whether and where to roam to a better access point, fusing three inputs into one decision: the current link's RSSI, a candidate neighbor list (from an 802.11k neighbor report or a scan), and an optional 802.11v BSS-Transition-Management hint from the network. `pc_roam_decide` returns a roam/stay decision with a target BSSID + channel and a reason, in priority order: a disassociation-imminent BTM forces a roam (to the network's preferred candidate if listed, else the strongest); a non-imminent BTM steering hint is honoured only when its preferred AP is not weaker than the current link; otherwise, when the current RSSI is at/below the caller's threshold and the strongest candidate beats it by at least the hysteresis margin, it roams to that candidate (the current AP is never chosen). This is the fusion layer above the simpler two-AP RSSI-hysteresis helper (`pc_wifi_should_roam`) in the sniffer: it adds the 802.11v steering path, the candidate-list selection, and the reason codes. `pc_roam_parse_neighbor_report` decodes the 802.11k Neighbor Report elements (id 52) an AP returns into that candidate list - each element's BSSID and operating channel, with the RSSI left unknown for the caller to fill after measuring - so a real neighbor report feeds straight into `pc_roam_decide`. `pc_roam_parse_btm_request` decodes an 802.11v BSS Transition Management Request action frame into the BTM hint: the Request Mode's disassociation-imminent flag and, when a preferred candidate list is present, the top-preference target's BSSID (found past the optional BSS Termination Duration and Session Information URL fields). Together the two parsers turn real 802.11k/v frames into the decision layer's inputs. Pure and host-tested (`native_roaming`); the scan / neighbor-report request and the 802.11r fast transition that executes the decision live in the Wi-Fi supplicant. See src/network_drivers/datalink/roaming.h.