ProtoCore v0.0.2
Deterministic, zero-heap network stack for embedded targets
Loading...
Searching...
No Matches
dnp3.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 dnp3.cpp
6 * @brief DNP3 (IEEE 1815) data-link frame builder + parser (pure, host-tested).
7 */
8
10#include "shared_primitives/crc.h" // PC_CRC16_DNP
11#include "shared_primitives/endian.h" // pc_rd16le / pc_rd32le
12
13#if PC_ENABLE_DNP3
14
15#include <string.h>
16
17uint16_t pc_dnp3_crc(const uint8_t *data, size_t len)
18{
19 // The DNP3 link-layer block check: reflected poly 0xA6BC = reflect(0x3D65), init 0, final XOR
20 // 0xFFFF - cataloged as CRC-16/DNP. test_crc diffs the shared engine against the loop that used
21 // to live here over every length 0..64, so this is byte-identical to it.
22 return (uint16_t)pc_crc(&PC_CRC16_DNP, data, len);
23}
24
25// Append a CRC over [data, data+n) low octet first.
26static size_t put_crc(uint8_t *p, const uint8_t *data, size_t n)
27{
28 uint16_t crc = pc_dnp3_crc(data, n);
29 p[0] = (uint8_t)(crc & 0xFF);
30 p[1] = (uint8_t)(crc >> 8);
31 return DNP3_CRC_LEN;
32}
33
34size_t pc_dnp3_build_frame(uint8_t *buf, size_t cap, uint8_t control, uint16_t dest, uint16_t src,
35 const uint8_t *user_data, size_t user_data_len)
36{
37 if (!buf || (user_data_len && !user_data) || user_data_len > DNP3_MAX_USER_DATA)
38 {
39 return 0;
40 }
41 size_t nblocks = (user_data_len + DNP3_BLOCK_LEN - 1) / DNP3_BLOCK_LEN;
42 size_t total = DNP3_HEADER_BLOCK_LEN + user_data_len + nblocks * DNP3_CRC_LEN;
43 if (total > cap)
44 {
45 return 0;
46 }
47
48 size_t p = 0;
49 buf[p++] = DNP3_START0;
50 buf[p++] = DNP3_START1;
51 buf[p++] = (uint8_t)(DNP3_LEN_OVERHEAD + user_data_len); // LEN counts CTRL + DEST + SRC + data
52 buf[p++] = control;
53 buf[p++] = (uint8_t)(dest & 0xFF); // LE
54 buf[p++] = (uint8_t)(dest >> 8);
55 buf[p++] = (uint8_t)(src & 0xFF);
56 buf[p++] = (uint8_t)(src >> 8);
57 p += put_crc(buf + p, buf, DNP3_HEADER_LEN); // header CRC over the header octets
58
59 size_t off = 0;
60 while (off < user_data_len)
61 {
62 size_t blk = user_data_len - off;
63 if (blk > DNP3_BLOCK_LEN)
64 {
65 blk = DNP3_BLOCK_LEN;
66 }
67 memcpy(buf + p, user_data + off, blk);
68 put_crc(buf + p + blk, buf + p, blk); // CRC over this block's data
69 p += blk + DNP3_CRC_LEN;
70 off += blk;
71 }
72 return total;
73}
74
75bool pc_dnp3_parse_frame(const uint8_t *buf, size_t len, Dnp3Frame *out, uint8_t *out_user, size_t out_cap,
76 size_t *out_user_len)
77{
78 if (!buf || !out || len < DNP3_HEADER_BLOCK_LEN)
79 {
80 return false;
81 }
82 if (buf[0] != DNP3_START0 || buf[1] != DNP3_START1)
83 {
84 return false;
85 }
86 uint8_t length = buf[2];
87 if (length < DNP3_LEN_OVERHEAD) // LEN must at least cover CTRL + DEST + SRC
88 {
89 return false;
90 }
91 size_t user_len = (size_t)length - DNP3_LEN_OVERHEAD;
92 if (user_len > DNP3_MAX_USER_DATA) // GCOVR_EXCL_BR_LINE unreachable: the line-76 guard above already
93 // forced length into [DNP3_LEN_OVERHEAD, 255] (length is a uint8_t, so
94 // 255 is a hard ceiling, not just an untested one), so this unsigned
95 // subtraction cannot underflow and user_len = length-DNP3_LEN_OVERHEAD is
96 // confined to [0, 250] = [0, DNP3_MAX_USER_DATA]; the true arm has no
97 // reachable value of length to take it with
98 {
99 return false; // GCOVR_EXCL_LINE unreachable: see the branch comment above - user_len can never exceed
100 // DNP3_MAX_USER_DATA given length's uint8_t domain plus the line-76 lower-bound guard
101 }
102
103 uint16_t hcrc = pc_dnp3_crc(buf, DNP3_HEADER_LEN);
104 if ((uint16_t)(buf[DNP3_HEADER_LEN] | (buf[DNP3_HEADER_LEN + 1] << 8)) != hcrc)
105 {
106 return false; // header CRC mismatch
107 }
108
109 size_t nblocks = (user_len + DNP3_BLOCK_LEN - 1) / DNP3_BLOCK_LEN;
110 size_t total = DNP3_HEADER_BLOCK_LEN + user_len + nblocks * DNP3_CRC_LEN;
111 if (total > len)
112 {
113 return false; // frame not fully buffered
114 }
115
116 if (user_len && (!out_user || user_len > out_cap))
117 {
118 return false;
119 }
120
121 size_t p = DNP3_HEADER_BLOCK_LEN;
122 size_t off = 0;
123 while (off < user_len)
124 {
125 size_t blk = user_len - off;
126 if (blk > DNP3_BLOCK_LEN)
127 {
128 blk = DNP3_BLOCK_LEN;
129 }
130 uint16_t bcrc = pc_dnp3_crc(buf + p, blk);
131 if ((uint16_t)(buf[p + blk] | (buf[p + blk + 1] << 8)) != bcrc)
132 {
133 return false; // block CRC mismatch
134 }
135 memcpy(out_user + off, buf + p, blk);
136 p += blk + DNP3_CRC_LEN;
137 off += blk;
138 }
139
140 out->length = length;
141 out->control = buf[3];
142 out->dest = (uint16_t)(buf[4] | (buf[5] << 8));
143 out->src = (uint16_t)(buf[6] | (buf[7] << 8));
144 if (out_user_len)
145 {
146 *out_user_len = user_len;
147 }
148 return true;
149}
150
151// --- transport function (IEEE 1815 ยง8.2) ---
152
153uint8_t pc_dnp3_transport_header(bool fir, bool fin, uint8_t seq)
154{
155 return (uint8_t)((fin ? DNP3_TR_FIN : 0u) | (fir ? DNP3_TR_FIR : 0u) | (seq & DNP3_TR_SEQ_MASK));
156}
157
158size_t pc_dnp3_build_transport_segment(uint8_t *out, size_t cap, bool fir, bool fin, uint8_t seq,
159 const uint8_t *app_data, size_t app_len)
160{
161 if (!out || (app_len && !app_data) || app_len > DNP3_TR_MAX_APP)
162 {
163 return 0;
164 }
165 if (cap < 1 + app_len)
166 {
167 return 0;
168 }
169 out[0] = pc_dnp3_transport_header(fir, fin, seq);
170 if (app_len)
171 {
172 memcpy(out + 1, app_data, app_len);
173 }
174 return 1 + app_len;
175}
176
177void pc_dnp3_transport_rx_init(Dnp3TransportRx *r, uint8_t *buf, size_t cap)
178{
179 if (!r)
180 {
181 return;
182 }
183 r->buf = buf;
184 r->cap = cap;
185 r->len = 0;
186 r->expect_seq = 0;
187 r->active = false;
188 r->done = false;
189}
190
191int pc_dnp3_transport_feed(Dnp3TransportRx *r, const uint8_t *user, size_t user_len)
192{
193 if (!r || !r->buf || !user || user_len < 1)
194 {
195 return DNP3_TR_IGNORED;
196 }
197 uint8_t hdr = user[0];
198 bool fir = (hdr & DNP3_TR_FIR) != 0;
199 bool fin = (hdr & DNP3_TR_FIN) != 0;
200 uint8_t seq = (uint8_t)(hdr & DNP3_TR_SEQ_MASK);
201 const uint8_t *app = user + 1;
202 size_t app_len = user_len - 1;
203
204 if (fir) // a first segment starts (or restarts) the fragment
205 {
206 r->len = 0;
207 r->active = true;
208 r->done = false;
209 r->expect_seq = seq;
210 }
211 else
212 {
213 if (!r->active) // a continuation with no fragment in progress
214 {
215 return DNP3_TR_IGNORED;
216 }
217 if (seq != r->expect_seq) // out of sequence: abandon and discard
218 {
219 r->active = false;
220 return DNP3_TR_IGNORED;
221 }
222 }
223
224 if (app_len > r->cap - r->len) // would overflow the fragment buffer
225 {
226 r->active = false;
227 return DNP3_TR_ERROR;
228 }
229 if (app_len)
230 {
231 memcpy(r->buf + r->len, app, app_len);
232 }
233 r->len += app_len;
234 r->expect_seq = (uint8_t)((r->expect_seq + 1) & DNP3_TR_SEQ_MASK);
235 if (fin)
236 {
237 r->active = false;
238 r->done = true;
239 return DNP3_TR_COMPLETE;
240 }
241 return DNP3_TR_PROGRESS;
242}
243
244uint8_t pc_dnp3_app_control(bool fir, bool fin, bool con, bool uns, uint8_t seq)
245{
246 return (uint8_t)((fir ? DNP3_AC_FIR : 0u) | (fin ? DNP3_AC_FIN : 0u) | (con ? DNP3_AC_CON : 0u) |
247 (uns ? DNP3_AC_UNS : 0u) | (seq & DNP3_AC_SEQ_MASK));
248}
249
250size_t pc_dnp3_build_app_request(uint8_t *out, size_t cap, uint8_t app_control, uint8_t fc, const uint8_t *objects,
251 size_t obj_len)
252{
253 if (!out || (obj_len && !objects) || cap < 2 + obj_len)
254 {
255 return 0;
256 }
257 out[0] = app_control;
258 out[1] = fc;
259 if (obj_len)
260 {
261 memcpy(out + 2, objects, obj_len);
262 }
263 return 2 + obj_len;
264}
265
266size_t pc_dnp3_build_app_response(uint8_t *out, size_t cap, uint8_t app_control, uint8_t fc, uint16_t iin,
267 const uint8_t *objects, size_t obj_len)
268{
269 if (!out || (obj_len && !objects) || cap < 4 + obj_len)
270 {
271 return 0;
272 }
273 out[0] = app_control;
274 out[1] = fc;
275 out[2] = (uint8_t)iin; // IIN1, little-endian
276 out[3] = (uint8_t)(iin >> 8); // IIN2
277 if (obj_len)
278 {
279 memcpy(out + 4, objects, obj_len);
280 }
281 return 4 + obj_len;
282}
283
284bool pc_dnp3_parse_app_header(const uint8_t *frag, size_t len, Dnp3AppHeader *out)
285{
286 if (!frag || !out || len < 2)
287 {
288 return false;
289 }
290 uint8_t ac = frag[0];
291 uint8_t fc = frag[1];
292 bool is_response = (fc == DNP3_FC_RESPONSE || fc == DNP3_FC_UNSOLICITED_RESPONSE);
293 size_t hdr_len = is_response ? 4u : 2u;
294 if (len < hdr_len) // a response needs the two IIN octets
295 {
296 return false;
297 }
298 out->app_control = ac;
299 out->fir = (ac & DNP3_AC_FIR) != 0;
300 out->fin = (ac & DNP3_AC_FIN) != 0;
301 out->con = (ac & DNP3_AC_CON) != 0;
302 out->uns = (ac & DNP3_AC_UNS) != 0;
303 out->seq = (uint8_t)(ac & DNP3_AC_SEQ_MASK);
304 out->fc = fc;
305 out->is_response = is_response;
306 out->iin = is_response ? (uint16_t)(frag[2] | (frag[3] << 8)) : 0u;
307 out->obj_len = len - hdr_len;
308 out->objects = out->obj_len ? frag + hdr_len : nullptr;
309 return true;
310}
311
312size_t pc_dnp3_build_object_header_range(uint8_t *buf, size_t cap, uint8_t group, uint8_t variation, uint32_t start,
313 uint32_t stop)
314{
315 if (!buf || stop < start)
316 {
317 return 0;
318 }
319 uint8_t range_code = 0;
320 size_t range_len = 0;
321 if (stop <= 0xFFu) // stop is the larger index, so it decides the width
322 {
323 range_code = DNP3_RANGE_START_STOP_1;
324 range_len = 2;
325 }
326 else if (stop <= 0xFFFFu)
327 {
328 range_code = DNP3_RANGE_START_STOP_2;
329 range_len = 4;
330 }
331 else
332 {
333 range_code = DNP3_RANGE_START_STOP_4;
334 range_len = 8;
335 }
336 size_t total = 3 + range_len;
337 if (cap < total)
338 {
339 return 0;
340 }
341 buf[0] = group;
342 buf[1] = variation;
343 buf[2] = range_code; // prefix code 0 (no per-object index prefix) | range specifier
344 if (range_code == DNP3_RANGE_START_STOP_1)
345 {
346 buf[3] = (uint8_t)start;
347 buf[4] = (uint8_t)stop;
348 }
349 else if (range_code == DNP3_RANGE_START_STOP_2)
350 {
351 pc_wr16le(buf + 3, (uint16_t)start);
352 pc_wr16le(buf + 5, (uint16_t)stop);
353 }
354 else
355 {
356 pc_wr32le(buf + 3, start);
357 pc_wr32le(buf + 7, stop);
358 }
359 return total;
360}
361
362size_t pc_dnp3_build_object_header_all(uint8_t *buf, size_t cap, uint8_t group, uint8_t variation)
363{
364 if (!buf || cap < 3)
365 {
366 return 0;
367 }
368 buf[0] = group;
369 buf[1] = variation;
370 buf[2] = DNP3_RANGE_NO_RANGE; // qualifier 0x06: all objects, no range field
371 return 3;
372}
373
374size_t pc_dnp3_build_crob(uint8_t *buf, size_t cap, uint8_t op_type, uint8_t tcc, bool clear, uint8_t count,
375 uint32_t on_time_ms, uint32_t off_time_ms)
376{
377 if (!buf || op_type > 0x0Fu || tcc > 0x03u || cap < DNP3_CROB_LEN)
378 {
379 return 0;
380 }
381 // Control code: op-type (bits 0-3) | clear (bit 5) | trip-close (bits 6-7). The queue bit (0x10) is obsolete.
382 buf[0] = (uint8_t)((op_type & 0x0Fu) | (clear ? 0x20u : 0x00u) | (uint8_t)((tcc & 0x03u) << 6));
383 buf[1] = count;
384 pc_wr32le(buf + 2, on_time_ms);
385 pc_wr32le(buf + 6, off_time_ms);
386 buf[10] = 0x00; // status: 0 in a request (the outstation reports the result in its response)
387 return DNP3_CROB_LEN;
388}
389
390size_t pc_dnp3_build_aob32(uint8_t *buf, size_t cap, int32_t value)
391{
392 if (!buf || cap < DNP3_AOB_LEN)
393 {
394 return 0;
395 }
396 pc_wr32le(buf, (uint32_t)value); // 32-bit signed setpoint, little-endian (two's complement)
397 buf[4] = 0x00; // control status: 0 in a request (the outstation reports the result)
398 return DNP3_AOB_LEN;
399}
400
401size_t pc_dnp3_build_aob_float(uint8_t *buf, size_t cap, float value)
402{
403 if (!buf || cap < DNP3_AOB_LEN)
404 {
405 return 0;
406 }
407 uint32_t bits;
408 memcpy(&bits, &value, 4); // the IEEE-754 bit pattern, written little-endian (endian-safe)
409 pc_wr32le(buf, bits);
410 buf[4] = 0x00; // control status: 0 in a request
411 return DNP3_AOB_LEN;
412}
413
414bool pc_dnp3_parse_object_header(const uint8_t *buf, size_t len, Dnp3ObjectHeader *out)
415{
416 if (!buf || !out || len < 3) // group + variation + qualifier
417 {
418 return false;
419 }
420 uint8_t range_code = (uint8_t)(buf[2] & DNP3_QUAL_RANGE_MASK);
421 size_t p = 3;
422 uint32_t start = 0;
423 uint32_t stop = 0;
424 uint32_t count = 0;
425 bool is_count = false;
426 switch (range_code)
427 {
428 case DNP3_RANGE_START_STOP_1:
429 if (len < p + 2)
430 {
431 return false;
432 }
433 start = buf[p];
434 stop = buf[p + 1];
435 p += 2;
436 count = stop - start + 1;
437 break;
438 case DNP3_RANGE_START_STOP_2:
439 if (len < p + 4)
440 {
441 return false;
442 }
443 start = pc_rd16le(buf + p);
444 stop = pc_rd16le(buf + p + 2);
445 p += 4;
446 count = stop - start + 1;
447 break;
448 case DNP3_RANGE_START_STOP_4:
449 if (len < p + 8)
450 {
451 return false;
452 }
453 start = pc_rd32le(buf + p);
454 stop = pc_rd32le(buf + p + 4);
455 p += 8;
456 count = stop - start + 1;
457 break;
458 case DNP3_RANGE_NO_RANGE:
459 break; // all objects; no range field follows
460 case DNP3_RANGE_COUNT_1:
461 if (len < p + 1)
462 {
463 return false;
464 }
465 count = buf[p];
466 p += 1;
467 is_count = true;
468 break;
469 case DNP3_RANGE_COUNT_2:
470 if (len < p + 2)
471 {
472 return false;
473 }
474 count = pc_rd16le(buf + p);
475 p += 2;
476 is_count = true;
477 break;
478 case DNP3_RANGE_COUNT_4:
479 if (len < p + 4)
480 {
481 return false;
482 }
483 count = pc_rd32le(buf + p);
484 p += 4;
485 is_count = true;
486 break;
487 default:
488 return false; // an unsupported qualifier range form
489 }
490 out->group = buf[0];
491 out->variation = buf[1];
492 out->qualifier = buf[2];
493 out->prefix_code = (uint8_t)((buf[2] & DNP3_QUAL_PREFIX_MASK) >> DNP3_QUAL_PREFIX_SHIFT);
494 out->range_code = range_code;
495 out->is_count = is_count;
496 out->start = start;
497 out->stop = stop;
498 out->count = count;
499 out->objects = (p < len) ? buf + p : nullptr;
500 out->objects_len = len - p;
501 return true;
502}
503
504#endif // PC_ENABLE_DNP3
Parameterized CRC engine - one source of truth for every cyclic redundancy check.
constexpr pc_crc_params PC_CRC16_DNP
CRC-16/DNP (DNP3 link-layer block check). check = 0xEA82. Used by services/dnp3.
Definition crc.h:192
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
DNP3 (IEEE 1815) data-link frame codec (PC_ENABLE_DNP3) - zero-heap builder + CRC-validating parser f...
Fixed-width integer serializers into a raw uint8_t* buffer - one source of truth.
uint32_t pc_rd32le(const uint8_t *p)
Read a little-endian u32 at p.
Definition endian.h:66
size_t pc_wr32le(uint8_t *p, uint32_t v)
Write v little-endian at p.
Definition endian.h:40
uint16_t pc_rd16le(const uint8_t *p)
Read a little-endian u16 at p.
Definition endian.h:60
size_t pc_wr16le(uint8_t *p, uint16_t v)
Write v little-endian at p.
Definition endian.h:32