ProtoCore v0.0.2
Deterministic, zero-heap network stack for embedded targets
Loading...
Searching...
No Matches
zigbee.cpp
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 zigbee.cpp
6 * @brief Zigbee EZSP / ASH framing codec - implementation.
7 *
8 * ASH (UG101): [control | payload | CRC16] byte-stuffed and Flag-terminated. The CRC is
9 * CRC-16/CCITT (poly 0x1021, init 0xFFFF); the reserved bytes 0x7E / 0x7D / 0x11 / 0x13 /
10 * 0x18 / 0x1A are escaped as 0x7D, (byte XOR 0x20).
11 */
12
14#include "shared_primitives/crc.h" // PC_CRC16_IBM_3740
15
16#if PC_ENABLE_ZIGBEE
17
18namespace
19{
20bool is_reserved(uint8_t b)
21{
22 return b == 0x7E || b == 0x7D || b == 0x11 || b == 0x13 || b == 0x18 || b == 0x1A;
23}
24
25// Append a byte to out with ASH stuffing; return false if it would overflow cap.
26bool put_stuffed(uint8_t *out, uint16_t *p, uint16_t cap, uint8_t b)
27{
28 if (is_reserved(b))
29 {
30 if (*p + 2 > cap)
31 {
32 return false;
33 }
34 out[(*p)++] = Ash::ASH_ESCAPE;
35 out[(*p)++] = (uint8_t)(b ^ 0x20);
36 }
37 else
38 {
39 if (*p + 1 > cap)
40 {
41 return false;
42 }
43 out[(*p)++] = b;
44 }
45 return true;
46}
47} // namespace
48
49uint16_t pc_ash_crc16(const uint8_t *buf, uint16_t len)
50{
51 // ASH uses CRC-CCITT (poly 0x1021, init 0xFFFF, unreflected), cataloged as CRC-16/IBM-3740.
52 // test_crc diffs the shared engine against the loop that used to live here over every length
53 // 0..64, so this is byte-identical to it.
54 return (uint16_t)pc_crc(&PC_CRC16_IBM_3740, buf, len);
55}
56
57uint16_t pc_ash_frame_encode(uint8_t control, const uint8_t *payload, uint16_t len, uint8_t *out, uint16_t cap)
58{
59 if (!out || len > PC_ZIGBEE_MAX_DATA || (payload == nullptr && len > 0))
60 {
61 return 0;
62 }
63 // CRC over control + payload. They are not contiguous in memory, which is what the engine's
64 // begin/update/final split is for - no scratch buffer to assemble them into.
65 uint32_t c = pc_crc_begin(&PC_CRC16_IBM_3740);
66 c = pc_crc_update(&PC_CRC16_IBM_3740, c, &control, 1);
67 c = pc_crc_update(&PC_CRC16_IBM_3740, c, payload, len);
68 const uint16_t crc = (uint16_t)pc_crc_final(&PC_CRC16_IBM_3740, c);
69
70 uint16_t p = 0;
71 if (!put_stuffed(out, &p, cap, control))
72 {
73 return 0;
74 }
75 for (uint16_t i = 0; i < len; i++)
76 {
77 if (!put_stuffed(out, &p, cap, payload[i]))
78 {
79 return 0;
80 }
81 }
82 if (!put_stuffed(out, &p, cap, (uint8_t)(crc >> 8)) || !put_stuffed(out, &p, cap, (uint8_t)(crc & 0xFF)))
83 {
84 return 0;
85 }
86 if (p + 1 > cap)
87 {
88 return 0;
89 }
90 out[p++] = Ash::ASH_FLAG; // the delimiter is never stuffed
91 return p;
92}
93
94int pc_ash_frame_decode(const uint8_t *raw, uint16_t len, uint8_t *control, uint8_t *payload, uint16_t pay_cap,
95 uint16_t *pay_len)
96{
97 if (!raw)
98 {
99 return 0;
100 }
101 // Find the frame delimiter.
102 uint16_t flag = 0;
103 while (flag < len && raw[flag] != Ash::ASH_FLAG)
104 {
105 flag++;
106 }
107 if (flag >= len)
108 {
109 return 0; // no complete frame yet
110 }
111
112 // Remove the byte-stuffing from raw[0, flag) into a fixed scratch: control + payload + CRC(2).
113 uint8_t un[PC_ZIGBEE_MAX_DATA + 3];
114 uint16_t n = 0;
115 for (uint16_t i = 0; i < flag; i++)
116 {
117 uint8_t b = raw[i];
118 if (b == Ash::ASH_ESCAPE)
119 {
120 if (++i >= flag)
121 {
122 return -1; // dangling escape
123 }
124 b = (uint8_t)(raw[i] ^ 0x20);
125 }
126 if (n >= sizeof(un))
127 {
128 return -1; // frame longer than we accept
129 }
130 un[n++] = b;
131 }
132 if (n < 3)
133 {
134 return -1; // need at least control + CRC(2)
135 }
136 uint16_t body = (uint16_t)(n - 2);
137 uint16_t crc = pc_ash_crc16(un, body);
138 if ((uint16_t)((un[n - 2] << 8) | un[n - 1]) != crc)
139 {
140 return -1; // CRC mismatch
141 }
142
143 uint16_t plen = (uint16_t)(body - 1); // minus the control byte
144 if (plen > pay_cap)
145 {
146 return -1; // caller buffer too small
147 }
148 if (control)
149 {
150 *control = un[0];
151 }
152 for (uint16_t i = 0; i < plen; i++)
153 {
154 payload[i] = un[1 + i];
155 }
156 if (pay_len)
157 {
158 *pay_len = plen;
159 }
160 return (int)(flag + 1); // consume up to and including the flag
161}
162
163#endif // PC_ENABLE_ZIGBEE
Parameterized CRC engine - one source of truth for every cyclic redundancy check.
uint32_t pc_crc_final(const pc_crc_params *p, uint32_t crc)
Finish a CRC: apply the output reflection and the final XOR.
Definition crc.h:142
uint32_t pc_crc_begin(const pc_crc_params *p)
Start a CRC.
Definition crc.h:108
constexpr pc_crc_params PC_CRC16_IBM_3740
CRC-16/IBM-3740 (often called CCITT-FALSE). check = 0x29B1.
Definition crc.h:184
uint32_t pc_crc(const pc_crc_params *p, const uint8_t *data, size_t len)
One-shot CRC of len octets at data.
Definition crc.h:158
uint32_t pc_crc_update(const pc_crc_params *p, uint32_t crc, const uint8_t *data, size_t len)
Fold len octets at data into the running register crc.
Definition crc.h:119
#define PC_ZIGBEE_MAX_DATA
Max ASH payload bytes (an EZSP frame; the ASH data field caps near 128).
Zigbee EZSP / ASH framing codec (PC_ENABLE_ZIGBEE) - Silicon Labs NCP.