ProtoCore v0.0.2
Deterministic, zero-heap network stack for embedded targets
Loading...
Searching...
No Matches
goose.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 goose.cpp
6 * @brief IEC 61850 GOOSE publisher codec (see goose.h).
7 */
8
10
11#if PC_ENABLE_GOOSE
12
13#include <string.h>
14
15namespace
16{
17// Number of octets to encode a BER definite length.
18size_t len_octets(size_t len)
19{
20 if (len < 0x80)
21 {
22 return 1;
23 }
24 size_t n = 0;
25 for (size_t v = len; v; v >>= 8)
26 {
27 n++;
28 }
29 return 1 + n;
30}
31
32// Write a BER length at p; returns octets written.
33size_t write_len(uint8_t *p, size_t len)
34{
35 if (len < 0x80)
36 {
37 p[0] = (uint8_t)len;
38 return 1;
39 }
40 size_t nb = 0;
41 for (size_t v = len; v; v >>= 8)
42 {
43 nb++;
44 }
45 p[0] = (uint8_t)(0x80 | nb);
46 for (size_t i = 0; i < nb; i++)
47 {
48 p[1 + i] = (uint8_t)(len >> (8 * (nb - 1 - i)));
49 }
50 return 1 + nb;
51}
52
53// Append a TLV. Returns false on overflow.
54bool tlv(uint8_t *out, size_t cap, size_t *n, uint8_t tag, const uint8_t *val, size_t val_len)
55{
56 size_t need = 1 + len_octets(val_len) + val_len;
57 if (*n + need > cap)
58 {
59 return false;
60 }
61 out[(*n)++] = tag;
62 *n += write_len(out + *n, val_len);
63 if (val_len)
64 {
65 memcpy(out + *n, val, val_len);
66 *n += val_len;
67 }
68 return true;
69}
70
71// Append a BER INTEGER (unsigned value, minimal length, leading 0x00 if the MSB is set).
72bool pc_ber_int(uint8_t *out, size_t cap, size_t *n, uint8_t tag, uint32_t v)
73{
74 uint8_t buf[5];
75 size_t k = 0;
76 // big-endian minimal
77 if (v == 0)
78 {
79 buf[k++] = 0;
80 }
81 else
82 {
83 uint8_t tmp[4];
84 size_t t = 0;
85 for (uint32_t x = v; x; x >>= 8)
86 {
87 tmp[t++] = (uint8_t)x;
88 }
89 if (tmp[t - 1] & 0x80)
90 {
91 buf[k++] = 0x00; // keep it positive
92 }
93 for (size_t i = 0; i < t; i++)
94 {
95 buf[k++] = tmp[t - 1 - i];
96 }
97 }
98 return tlv(out, cap, n, tag, buf, k);
99}
100
101bool pc_ber_str(uint8_t *out, size_t cap, size_t *n, uint8_t tag, const char *s)
102{
103 return tlv(out, cap, n, tag, (const uint8_t *)(s ? s : ""), s ? strnlen(s, cap + 1) : 0);
104}
105
106bool pc_ber_bool(uint8_t *out, size_t cap, size_t *n, uint8_t tag, bool v)
107{
108 uint8_t b = v ? 0xFF : 0x00;
109 return tlv(out, cap, n, tag, &b, 1);
110}
111
112// Read one definite-length BER TLV at *p within [*p, end). Advances *p past the value on success.
113// Rejects truncation and indefinite / over-4-octet lengths (a GOOSE PDU never needs them).
114bool ber_read(const uint8_t *end, const uint8_t **p, uint8_t *tag, const uint8_t **vptr, size_t *vlen)
115{
116 const uint8_t *q = *p;
117 if (q + 2 > end)
118 {
119 return false;
120 }
121 uint8_t t = *q++;
122 size_t len = *q++;
123 if (len & 0x80)
124 {
125 size_t nb = len & 0x7F;
126 if (nb == 0 || nb > 4 || q + nb > end)
127 {
128 return false;
129 }
130 len = 0;
131 for (size_t i = 0; i < nb; i++)
132 {
133 len = (len << 8) | *q++;
134 }
135 }
136 if (len > (size_t)(end - q))
137 {
138 return false;
139 }
140 *tag = t;
141 *vptr = q;
142 *vlen = len;
143 *p = q + len;
144 return true;
145}
146
147// A big-endian unsigned BER INTEGER value (a leading 0x00 sign octet folds in harmlessly).
148uint32_t ber_uint(const uint8_t *v, size_t n)
149{
150 uint32_t x = 0;
151 for (size_t i = 0; i < n; i++)
152 {
153 x = (x << 8) | v[i];
154 }
155 return x;
156}
157} // namespace
158
159size_t pc_goose_pdu(const pc_goose *g, uint8_t *out, size_t cap)
160{
161 if (!g || !out)
162 {
163 return 0;
164 }
165 const size_t RESERVE = 4; // 0x61 tag + up to 3 length octets
166 if (cap < RESERVE)
167 {
168 return 0;
169 }
170
171 size_t n = RESERVE; // build the content after the reserved header area
172 static const uint8_t ZT[8] = {0};
173 bool ok = pc_ber_str(out, cap, &n, 0x80, g->gocb_ref) && pc_ber_int(out, cap, &n, 0x81, g->time_allowed_to_live) &&
174 pc_ber_str(out, cap, &n, 0x82, g->dat_set) && pc_ber_str(out, cap, &n, 0x83, g->go_id) &&
175 tlv(out, cap, &n, 0x84, g->t ? g->t : ZT, 8) && pc_ber_int(out, cap, &n, 0x85, g->st_num) &&
176 pc_ber_int(out, cap, &n, 0x86, g->sq_num) && pc_ber_bool(out, cap, &n, 0x87, g->simulation) &&
177 pc_ber_int(out, cap, &n, 0x88, g->conf_rev) && pc_ber_bool(out, cap, &n, 0x89, g->nds_com) &&
178 pc_ber_int(out, cap, &n, 0x8A, g->num_entries) && tlv(out, cap, &n, 0xAB, g->all_data, g->all_data_len);
179 if (!ok)
180 {
181 return 0;
182 }
183
184 size_t content_len = n - RESERVE;
185 size_t hdr = 1 + len_octets(content_len);
186 // Move the content down so the 0x61 tag + length sit immediately before it (no gap).
187 memmove(out + hdr, out + RESERVE, content_len);
188 out[0] = 0x61;
189 write_len(out + 1, content_len);
190 return hdr + content_len;
191}
192
193size_t pc_goose_frame(const uint8_t *dst, const uint8_t *src, uint16_t appid, const pc_goose *g, uint8_t *out,
194 size_t cap)
195{
196 if (!dst || !src || !g || !out || cap < 22)
197 {
198 return 0;
199 }
200 // Ethernet II header (ethertype 0x88B8 = GOOSE).
201 memcpy(out, dst, 6);
202 memcpy(out + 6, src, 6);
203 out[12] = 0x88;
204 out[13] = 0xB8;
205 // GOOSE header: APPID(2), length(2, filled below), reserved1(2), reserved2(2).
206 out[14] = (uint8_t)(appid >> 8);
207 out[15] = (uint8_t)appid;
208 out[18] = 0;
209 out[19] = 0;
210 out[20] = 0;
211 out[21] = 0;
212
213 size_t pdu = pc_goose_pdu(g, out + 22, cap - 22);
214 if (!pdu)
215 {
216 return 0;
217 }
218 uint16_t goose_len = (uint16_t)(8 + pdu); // GOOSE header (8) + APDU
219 out[16] = (uint8_t)(goose_len >> 8);
220 out[17] = (uint8_t)goose_len;
221 return 22 + pdu;
222}
223
224bool pc_goose_parse_frame(const uint8_t *buf, size_t len, pc_goose_rx *out)
225{
226 if (!buf || !out || len < 24) // 14 Ethernet + 8 GOOSE header + a minimal PDU
227 {
228 return false;
229 }
230 if (buf[12] != 0x88 || buf[13] != 0xB8) // GOOSE ethertype
231 {
232 return false;
233 }
234 memset(out, 0, sizeof(*out));
235 out->appid = (uint16_t)((buf[14] << 8) | buf[15]);
236
237 const uint8_t *end = buf + len;
238 const uint8_t *p = buf + 22; // the IECGoosePdu begins after the 8-octet GOOSE header
239 uint8_t tag = 0;
240 const uint8_t *v = nullptr;
241 size_t vlen = 0;
242 if (!ber_read(end, &p, &tag, &v, &vlen) || tag != 0x61) // the outer IECGoosePdu SEQUENCE
243 {
244 return false;
245 }
246
247 const uint8_t *inner_end = v + vlen;
248 const uint8_t *ip = v;
249 while (ip < inner_end)
250 {
251 if (!ber_read(inner_end, &ip, &tag, &v, &vlen))
252 {
253 return false;
254 }
255 switch (tag)
256 {
257 case 0x80:
258 out->gocb_ref = (const char *)v;
259 out->gocb_ref_len = vlen;
260 break;
261 case 0x81:
262 out->time_allowed_to_live = ber_uint(v, vlen);
263 break;
264 case 0x82:
265 out->dat_set = (const char *)v;
266 out->dat_set_len = vlen;
267 break;
268 case 0x83:
269 out->go_id = (const char *)v;
270 out->go_id_len = vlen;
271 break;
272 case 0x84:
273 if (vlen == 8)
274 {
275 out->t = v;
276 }
277 break;
278 case 0x85:
279 out->st_num = ber_uint(v, vlen);
280 break;
281 case 0x86:
282 out->sq_num = ber_uint(v, vlen);
283 break;
284 case 0x87:
285 out->simulation = (vlen >= 1 && v[0] != 0);
286 break;
287 case 0x88:
288 out->conf_rev = ber_uint(v, vlen);
289 break;
290 case 0x89:
291 out->nds_com = (vlen >= 1 && v[0] != 0);
292 break;
293 case 0x8A:
294 out->num_entries = ber_uint(v, vlen);
295 break;
296 case 0xAB:
297 out->all_data = v;
298 out->all_data_len = vlen;
299 break;
300 default:
301 break; // unknown / future tags are skipped
302 }
303 }
304 return true;
305}
306
307#endif // PC_ENABLE_GOOSE
IEC 61850 GOOSE publisher + subscriber codec (PC_ENABLE_GOOSE).