ProtoCore v0.0.1
Deterministic, zero-heap network stack for embedded targets
Loading...
Searching...
No Matches
thread.h
Go to the documentation of this file.
1// Copyright (C) 2026 Douglas Quigg (dstroy0) <dquigg123@gmail.com>
2// SPDX-License-Identifier: AGPL-3.0-or-later
3
4/**
5 * @file thread.h
6 * @brief Thread spinel / HDLC-lite framing codec (PC_ENABLE_THREAD) - OpenThread RCP.
7 *
8 * The HDLC-lite framing that carries spinel frames to an OpenThread radio co-processor (an
9 * nRF52840 / EFR32 RCP) over UART - an 802.15.4 / Thread mesh bridged to IP and the web.
10 * HDLC-lite wraps each spinel frame by appending an FCS, byte-stuffing the reserved bytes,
11 * and terminating with a Flag:
12 *
13 * [spinel payload | FCS(lo,hi)] --byte-stuffed--> ... | 0x7E
14 *
15 * The FCS is the HDLC frame check sequence, **CRC-16/X-25** (poly 0x1021 reflected, init
16 * 0xFFFF, reflected in/out, final XOR 0xFFFF), transmitted low byte first. The reserved
17 * bytes stuffed (as 0x7D, byte XOR 0x20) are the Flag 0x7E, the Escape 0x7D, XON 0x11, and
18 * XOFF 0x13.
19 *
20 * pc_spinel_frame_encode() wraps a payload; pc_spinel_frame_decode() finds the flag, removes the
21 * stuffing, and verifies the FCS. pc_spinel_fcs() is the shared checksum. The spinel command
22 * inside (a property get/set/insert, an 802.15.4 stream) is the application's. Pure - you
23 * carry the bytes over your UART - so it is fully host-testable.
24 *
25 * @author Douglas Quigg (dstroy0)
26 * @date 2026
27 */
28
29#ifndef PROTOCORE_THREAD_H
30#define PROTOCORE_THREAD_H
31
32#include "protocore_config.h"
33
34#if PC_ENABLE_THREAD
35
36#include <stddef.h>
37#include <stdint.h>
38
39/** @brief HDLC-lite markers. */
40struct ThreadHdlc
41{
42 static constexpr uint8_t HDLC_FLAG = 0x7E; ///< frame delimiter
43 static constexpr uint8_t HDLC_ESCAPE = 0x7D; ///< byte-stuffing escape
44};
45
46/** @brief Common spinel commands (the property accessors a gateway uses). */
47struct SpinelCmd
48{
49 static constexpr uint8_t SPINEL_CMD_NOOP = 0;
50 static constexpr uint8_t SPINEL_CMD_RESET = 1;
51 static constexpr uint8_t SPINEL_CMD_PROP_VALUE_GET = 2;
52 static constexpr uint8_t SPINEL_CMD_PROP_VALUE_SET = 3;
53 static constexpr uint8_t SPINEL_CMD_PROP_VALUE_INSERT = 4;
54 static constexpr uint8_t SPINEL_CMD_PROP_VALUE_REMOVE = 5;
55 static constexpr uint8_t SPINEL_CMD_PROP_VALUE_IS = 6; ///< an async property update from the NCP
56 static constexpr uint8_t SPINEL_CMD_PROP_VALUE_INSERTED = 7; ///< a list property gained an entry
57 static constexpr uint8_t SPINEL_CMD_PROP_VALUE_REMOVED = 8; ///< a list property lost an entry
58};
59
60/**
61 * @brief The spinel property ids a Thread/802.15.4 gateway reads or writes (subset of the
62 * spinel property registry, grouped CORE / PHY / MAC / NET / IPv6 / STREAM).
63 */
64struct SpinelProp
65{
66 // Core (SPINEL_PROP_CORE__BEGIN = 0)
67 static constexpr uint32_t SPINEL_PROP_LAST_STATUS = 0; ///< 'i' last operation status
68 static constexpr uint32_t SPINEL_PROP_PROTOCOL_VERSION = 1; ///< 'ii' major, minor
69 static constexpr uint32_t SPINEL_PROP_NCP_VERSION = 2; ///< 'U' NCP version string
70 static constexpr uint32_t SPINEL_PROP_INTERFACE_TYPE = 3; ///< 'i' 3 = Thread
71 static constexpr uint32_t SPINEL_PROP_VENDOR_ID = 4; ///< 'i'
72 static constexpr uint32_t SPINEL_PROP_CAPS = 5; ///< 'A(i)' capability list
73 static constexpr uint32_t SPINEL_PROP_INTERFACE_COUNT = 6; ///< 'C'
74 static constexpr uint32_t SPINEL_PROP_HWADDR = 8; ///< 'E' factory EUI64
75 static constexpr uint32_t SPINEL_PROP_LOCK = 9; ///< 'b'
76 // PHY (SPINEL_PROP_PHY__BEGIN = 0x20)
77 static constexpr uint32_t SPINEL_PROP_PHY_ENABLED = 0x20; ///< 'b'
78 static constexpr uint32_t SPINEL_PROP_PHY_CHAN = 0x21; ///< 'C' 802.15.4 channel
79 static constexpr uint32_t SPINEL_PROP_PHY_CHAN_SUPPORTED = 0x22; ///< 'A(C)'
80 static constexpr uint32_t SPINEL_PROP_PHY_FREQ = 0x23; ///< 'L' kHz
81 static constexpr uint32_t SPINEL_PROP_PHY_TX_POWER = 0x25; ///< 'c' dBm
82 static constexpr uint32_t SPINEL_PROP_PHY_RSSI = 0x26; ///< 'c' dBm
83 // MAC (SPINEL_PROP_MAC__BEGIN = 0x30)
84 static constexpr uint32_t SPINEL_PROP_MAC_SCAN_STATE = 0x30; ///< 'C'
85 static constexpr uint32_t SPINEL_PROP_MAC_SCAN_MASK = 0x31; ///< 'A(C)'
86 static constexpr uint32_t SPINEL_PROP_MAC_SCAN_PERIOD = 0x32; ///< 'S' ms/channel
87 static constexpr uint32_t SPINEL_PROP_MAC_15_4_LADDR = 0x34; ///< 'E' extended (long) address
88 static constexpr uint32_t SPINEL_PROP_MAC_15_4_SADDR = 0x35; ///< 'S' short address
89 static constexpr uint32_t SPINEL_PROP_MAC_15_4_PANID = 0x36; ///< 'S' PAN id
90 // NET (SPINEL_PROP_NET__BEGIN = 0x40)
91 static constexpr uint32_t SPINEL_PROP_NET_SAVED = 0x40; ///< 'b'
92 static constexpr uint32_t SPINEL_PROP_NET_IF_UP = 0x41; ///< 'b'
93 static constexpr uint32_t SPINEL_PROP_NET_STACK_UP = 0x42; ///< 'b'
94 static constexpr uint32_t SPINEL_PROP_NET_ROLE = 0x43; ///< 'C' 0 detached,1 child,2 router,3 leader
95 static constexpr uint32_t SPINEL_PROP_NET_NETWORK_NAME = 0x44; ///< 'U'
96 static constexpr uint32_t SPINEL_PROP_NET_XPANID = 0x45; ///< 'D' 8-byte extended PAN id
97 static constexpr uint32_t SPINEL_PROP_NET_NETWORK_KEY = 0x46; ///< 'D' 16-byte network key
98 // IPv6 (SPINEL_PROP_IPV6__BEGIN = 0x60)
99 static constexpr uint32_t SPINEL_PROP_IPV6_LL_ADDR = 0x60; ///< '6' link-local
100 static constexpr uint32_t SPINEL_PROP_IPV6_ML_ADDR = 0x61; ///< '6' mesh-local
101 // Stream (SPINEL_PROP_STREAM__BEGIN = 0x70)
102 static constexpr uint32_t SPINEL_PROP_STREAM_DEBUG = 0x70; ///< 'U' debug text
103 static constexpr uint32_t SPINEL_PROP_STREAM_RAW = 0x71; ///< 'dD' raw 802.15.4 frame + metadata
104 static constexpr uint32_t SPINEL_PROP_STREAM_NET = 0x72; ///< 'dD' IPv6 datagram + metadata
105};
106
107/** @brief spinel `LAST_STATUS` codes (a subset - the ones a gateway acts on). */
108struct SpinelStatus
109{
110 static constexpr uint32_t SPINEL_STATUS_OK = 0;
111 static constexpr uint32_t SPINEL_STATUS_FAILURE = 1;
112 static constexpr uint32_t SPINEL_STATUS_UNIMPLEMENTED = 2;
113 static constexpr uint32_t SPINEL_STATUS_INVALID_ARGUMENT = 3;
114 static constexpr uint32_t SPINEL_STATUS_INVALID_STATE = 4;
115 static constexpr uint32_t SPINEL_STATUS_INVALID_COMMAND = 5;
116 static constexpr uint32_t SPINEL_STATUS_INVALID_INTERFACE = 6;
117 static constexpr uint32_t SPINEL_STATUS_INTERNAL_ERROR = 7;
118 static constexpr uint32_t SPINEL_STATUS_SECURITY_ERROR = 8;
119 static constexpr uint32_t SPINEL_STATUS_PARSE_ERROR = 9;
120 static constexpr uint32_t SPINEL_STATUS_IN_PROGRESS = 10;
121 static constexpr uint32_t SPINEL_STATUS_NOMEM = 11;
122 static constexpr uint32_t SPINEL_STATUS_BUSY = 12;
123 static constexpr uint32_t SPINEL_STATUS_PROP_NOT_FOUND = 13;
124 static constexpr uint32_t SPINEL_STATUS_DROPPED = 14;
125 static constexpr uint32_t SPINEL_STATUS_EMPTY = 15;
126 static constexpr uint32_t SPINEL_STATUS_RESET_POWER_ON = 112; ///< 0x70..0x77 are reset causes
127};
128
129// --- Header byte (bit7 = flag, bits6-4 = interface id, bits3-0 = transaction id) --------
130
131/** @brief Build a spinel header byte for interface @p iid and transaction @p tid (tid 0 = no response wanted). */
132inline uint8_t pc_spinel_header(uint8_t iid, uint8_t tid)
133{
134 return (uint8_t)(0x80 | ((iid & 0x03) << 4) | (tid & 0x0F));
135}
136/** @brief The transaction id carried in header byte @p h. */
137inline uint8_t pc_spinel_header_tid(uint8_t h)
138{
139 return (uint8_t)(h & 0x0F);
140}
141/** @brief The interface id carried in header byte @p h. */
142inline uint8_t pc_spinel_header_iid(uint8_t h)
143{
144 return (uint8_t)((h >> 4) & 0x03);
145}
146
147/** @brief HDLC frame check sequence: CRC-16/X-25 over @p buf. */
148uint16_t pc_spinel_fcs(const uint8_t *buf, uint16_t len);
149
150// --- Spinel command layer (rides inside a decoded HDLC frame's payload) ---------------
151
152/**
153 * @brief Encode a spinel packed unsigned integer (7 bits/byte, little-endian, high bit =
154 * continuation) into @p out.
155 * @return the number of bytes written (1..5), or 0 if it would not fit @p cap.
156 */
157uint8_t pc_spinel_pack_uint(uint32_t value, uint8_t *out, uint8_t cap);
158
159/**
160 * @brief Decode a spinel packed unsigned integer from the front of @p raw.
161 * @return the bytes consumed (> 0, value in @p value), 0 if more bytes are needed, or -1 if
162 * the encoding overflows a uint32.
163 */
164int pc_spinel_unpack_uint(const uint8_t *raw, uint8_t len, uint32_t *value);
165
166/**
167 * @brief Build a spinel property-command payload (`header | CMD | PROP | value`) - the
168 * content of an HDLC frame - into @p out. CMD and PROP are packed integers.
169 * @return the payload length, or 0 if it would not fit @p cap.
170 */
171uint16_t pc_spinel_command_build(uint8_t header, uint32_t cmd, uint32_t prop, const uint8_t *value, uint16_t value_len,
172 uint8_t *out, uint16_t cap);
173
174/**
175 * @brief Parse a spinel property-command payload (from a decoded HDLC frame).
176 * @param[out] header the flag/iid/tid header byte.
177 * @param[out] cmd the command (unpacked).
178 * @param[out] prop the property id (unpacked).
179 * @param[out] value the first value byte (points into @p payload).
180 * @param[out] value_len the value length.
181 * @return the value offset (> 0), or -1 if the header / command / property is malformed.
182 */
183int pc_spinel_command_parse(const uint8_t *payload, uint16_t len, uint8_t *header, uint32_t *cmd, uint32_t *prop,
184 const uint8_t **value, uint16_t *value_len);
185
186// --- Spinel value semantics (the typed datatypes carried in a property value) -----------
187//
188// spinel encodes a property value as a sequence of typed fields (spinel datatype format):
189// b bool, C uint8, c int8, S uint16-LE, s int16-LE, L uint32-LE, l int32-LE, i packed-uint,
190// E EUI64 (8B), 6 IPv6 (16B), U UTF8 (NUL-terminated), D data (rest), d data (uint16-LE len).
191// A cursor reads/writes those fields in order (an array A(t) is just a field read in a loop,
192// a struct T(...) is its fields in sequence). Any out-of-bounds read/write latches `err` so a
193// caller can run a whole sequence and check once at the end.
194
195/** @brief A read cursor over a spinel property value. */
196struct SpinelReader
197{
198 const uint8_t *buf; ///< the value bytes
199 uint16_t len; ///< value length
200 uint16_t off; ///< next unread offset
201 bool err; ///< set once any read runs past the end / is malformed
202};
203
204/** @brief A write cursor building a spinel property value into a caller buffer. */
205struct SpinelWriter
206{
207 uint8_t *buf; ///< output buffer
208 uint16_t cap; ///< output capacity
209 uint16_t off; ///< bytes written so far
210 bool err; ///< set once any write would overflow @c cap
211};
212
213void pc_spinel_reader_init(SpinelReader *r, const uint8_t *value, uint16_t len);
214bool pc_spinel_get_bool(SpinelReader *r, bool *out);
215bool pc_spinel_get_u8(SpinelReader *r, uint8_t *out);
216bool pc_spinel_get_i8(SpinelReader *r, int8_t *out);
217bool pc_spinel_get_u16(SpinelReader *r, uint16_t *out);
218bool pc_spinel_get_i16(SpinelReader *r, int16_t *out);
219bool pc_spinel_get_u32(SpinelReader *r, uint32_t *out);
220bool pc_spinel_get_i32(SpinelReader *r, int32_t *out);
221bool pc_spinel_get_uint(SpinelReader *r, uint32_t *out); ///< packed 'i'
222bool pc_spinel_get_eui64(SpinelReader *r, const uint8_t **out8);
223bool pc_spinel_get_ipv6(SpinelReader *r, const uint8_t **out16);
224/** @brief UTF8 'U': @p out points into the value, @p out_len excludes the NUL; advances past it. */
225bool pc_spinel_get_utf8(SpinelReader *r, const char **out, uint16_t *out_len);
226/** @brief Data 'D' (to end of value): @p out points into the value, @p out_len is the remainder. */
227bool pc_spinel_get_data(SpinelReader *r, const uint8_t **out, uint16_t *out_len);
228/** @brief Data 'd' (uint16-LE length prefix): reads the count, then that many bytes. */
229bool pc_spinel_get_data_wlen(SpinelReader *r, const uint8_t **out, uint16_t *out_len);
230/** @brief True if every read so far stayed in bounds. */
231bool pc_spinel_reader_ok(const SpinelReader *r);
232
233void pc_spinel_writer_init(SpinelWriter *w, uint8_t *out, uint16_t cap);
234bool pc_spinel_put_bool(SpinelWriter *w, bool v);
235bool pc_spinel_put_u8(SpinelWriter *w, uint8_t v);
236bool pc_spinel_put_i8(SpinelWriter *w, int8_t v);
237bool pc_spinel_put_u16(SpinelWriter *w, uint16_t v);
238bool pc_spinel_put_i16(SpinelWriter *w, int16_t v);
239bool pc_spinel_put_u32(SpinelWriter *w, uint32_t v);
240bool pc_spinel_put_i32(SpinelWriter *w, int32_t v);
241bool pc_spinel_put_uint(SpinelWriter *w, uint32_t v); ///< packed 'i'
242bool pc_spinel_put_eui64(SpinelWriter *w, const uint8_t *v8);
243bool pc_spinel_put_ipv6(SpinelWriter *w, const uint8_t *v16);
244bool pc_spinel_put_utf8(SpinelWriter *w, const char *s); ///< 'U' incl. the NUL
245bool pc_spinel_put_data(SpinelWriter *w, const uint8_t *d, uint16_t n); ///< 'D' raw bytes
246bool pc_spinel_put_data_wlen(SpinelWriter *w, const uint8_t *d, uint16_t n); ///< 'd' uint16-LE len + bytes
247/** @brief The finished value length, or 0 if any write overflowed. */
248uint16_t pc_spinel_writer_len(const SpinelWriter *w);
249
250// --- Property registry (id -> name + primary datatype) ----------------------------------
251
252/** @brief A registry entry: a property id, its human name, and its primary spinel datatype char. */
253struct SpinelPropInfo
254{
255 uint32_t id;
256 const char *name;
257 char type; ///< the leading spinel datatype ('U','i','C','c','S','E','6','b','D', or '.')
258};
259
260/** @brief Look up a property's registry entry, or nullptr if it is not in the registry. */
261const SpinelPropInfo *pc_spinel_prop_lookup(uint32_t id);
262/** @brief A property's human name, or "UNKNOWN" if unregistered. */
263const char *pc_spinel_prop_name(uint32_t id);
264/** @brief A `LAST_STATUS` code's human name, or "UNKNOWN" if unregistered. */
265const char *pc_spinel_status_name(uint32_t status);
266
267/**
268 * @brief Encode an HDLC-lite frame: @p payload + FCS, byte-stuffed, flag-terminated.
269 * @return the encoded frame length, or 0 if @p len exceeds PC_THREAD_MAX_DATA or the
270 * stuffed frame would not fit @p cap.
271 */
272uint16_t pc_spinel_frame_encode(const uint8_t *payload, uint16_t len, uint8_t *out, uint16_t cap);
273
274/**
275 * @brief Decode one HDLC-lite frame from the front of @p raw: find the flag, remove the
276 * stuffing, verify the FCS, and copy the spinel payload to @p payload.
277 * @param[out] pay_len set to the payload length.
278 * @return the bytes consumed up to and including the flag (> 0), 0 if no flag is present yet
279 * (need more), or -1 if the frame is malformed (too short, bad FCS, dangling escape,
280 * or the payload overflows @p pay_cap) - the caller drops one byte and retries.
281 */
282int pc_spinel_frame_decode(const uint8_t *raw, uint16_t len, uint8_t *payload, uint16_t pay_cap, uint16_t *pay_len);
283
284#endif // PC_ENABLE_THREAD
285
286#endif // PROTOCORE_THREAD_H
User-facing configuration for ProtoCore.