ProtoCore v0.0.1
Deterministic, zero-heap network stack for embedded targets
Loading...
Searching...
No Matches
dnp3.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 dnp3.h
6 * @brief DNP3 (IEEE 1815) data-link frame codec (PC_ENABLE_DNP3) - zero-heap builder +
7 * CRC-validating parser for the SCADA / utility outstation link layer.
8 *
9 * A DNP3 data-link frame:
10 * @code
11 * 0x05 0x64 LEN CTRL DEST(2,LE) SRC(2,LE) CRC(2) // 10-octet header block
12 * [<=16 user-data octets][CRC(2)] ... // data blocks, each CRC'd
13 * @endcode
14 * - LEN counts CTRL + DEST + SRC + user data (the start word, LEN itself, and the CRCs are
15 * excluded), so LEN = 5 + user_data_len; min 5, max 255 (<= 250 user-data octets).
16 * - Addresses are little-endian (LSB first). User data is carried in blocks of up to 16
17 * octets, each followed by its own CRC; the header is its own CRC'd block.
18 * - CRC is CRC-16/DNP (poly 0x3D65, init 0x0000, reflected in/out, final XOR 0xFFFF),
19 * transmitted low octet first.
20 *
21 * This is the data-link layer (framing + CRC) plus the transport function (IEEE 1815 §8.2:
22 * segment header + fragment reassembly). The application layer (objects / function codes) is layered
23 * on the reassembled fragment.
24 *
25 * @author Douglas Quigg (dstroy0)
26 * @date 2026
27 */
28
29#ifndef PROTOCORE_DNP3_H
30#define PROTOCORE_DNP3_H
31
32#include "protocore_config.h"
33
34#if PC_ENABLE_DNP3
35
36#include <stddef.h>
37#include <stdint.h>
38
39#define DNP3_START0 0x05
40#define DNP3_START1 0x64
41#define DNP3_MAX_USER_DATA 250 ///< LEN max 255 minus the 5 header octets it counts
42
43// Frame geometry (octets). The header is one CRC'd block; user data is carried in
44// fixed-size CRC'd blocks. These are wire constants from IEEE 1815, not tunables.
45#define DNP3_BLOCK_LEN 16 ///< user-data octets per data block (the last may be shorter)
46#define DNP3_CRC_LEN 2 ///< CRC-16/DNP appended after each block, low octet first
47#define DNP3_HEADER_LEN 8 ///< header octets the header CRC covers: 0x0564 LEN CTRL DEST SRC
48#define DNP3_HEADER_BLOCK_LEN 10 ///< whole header block = DNP3_HEADER_LEN + DNP3_CRC_LEN
49#define DNP3_LEN_OVERHEAD 5 ///< octets LEN counts beyond user data: CTRL + DEST + SRC
50
51/** @brief CRC-16/DNP (poly 0x3D65, init 0, reflected, final XOR 0xFFFF). */
52uint16_t pc_dnp3_crc(const uint8_t *data, size_t len);
53
54/**
55 * @brief Build a complete data-link frame (header block + CRC'd data blocks).
56 * @param control link-layer control octet (DIR/PRM/FCB/FCV + function code).
57 * @param dest 16-bit destination address (written little-endian).
58 * @param src 16-bit source address (written little-endian).
59 * @return total octets written, or 0 on overflow / user_data_len > DNP3_MAX_USER_DATA.
60 */
61size_t pc_dnp3_build_frame(uint8_t *buf, size_t cap, uint8_t control, uint16_t dest, uint16_t src,
62 const uint8_t *user_data, size_t user_data_len);
63
64/** @brief A parsed data-link frame header (the user data is de-blocked separately). */
65struct Dnp3Frame
66{
67 uint8_t length; ///< the LEN field value
68 uint8_t control; ///< link-layer control octet
69 uint16_t dest;
70 uint16_t src;
71};
72
73/**
74 * @brief Parse + CRC-validate a frame, de-blocking the user data (per-block CRCs stripped).
75 * @param out_user receives the reassembled user data.
76 * @param out_cap capacity of @p out_user.
77 * @param out_user_len receives the user-data length.
78 * @return true on a complete, all-CRC-valid frame; false on a bad start word, an invalid
79 * LEN, truncation, a header or block CRC mismatch, or an out_user overflow.
80 */
81bool pc_dnp3_parse_frame(const uint8_t *buf, size_t len, Dnp3Frame *out, uint8_t *out_user, size_t out_cap,
82 size_t *out_user_len);
83
84// --- transport function (IEEE 1815 §8.2): reassemble application fragments from link user data ---
85//
86// Each data-link frame's user data begins with a 1-octet transport header - FIN (bit 7), FIR (bit 6), and
87// a 6-bit SEQUENCE - followed by up to 249 octets of the application fragment. A fragment starts with a
88// FIR segment, continues with sequence-incrementing segments, and ends with a FIN segment (a single-frame
89// fragment sets both). This layers on the de-blocked user data from pc_dnp3_parse_frame.
90
91#define DNP3_TR_FIN 0x80u ///< transport header: final segment of the fragment
92#define DNP3_TR_FIR 0x40u ///< transport header: first segment of the fragment
93#define DNP3_TR_SEQ_MASK 0x3Fu ///< transport header: 6-bit sequence number
94#define DNP3_TR_MAX_APP 249 ///< application octets per segment (250 user - 1 transport header)
95
96/** @brief Compose a transport header octet from the FIR / FIN flags and a 6-bit sequence. */
97uint8_t pc_dnp3_transport_header(bool fir, bool fin, uint8_t seq);
98
99/**
100 * @brief Build one transport segment (transport header + @p app_len application octets) into @p out.
101 * @return the segment length (1 + @p app_len), or 0 on a bad argument / overflow / @p app_len > 249.
102 */
103size_t pc_dnp3_build_transport_segment(uint8_t *out, size_t cap, bool fir, bool fin, uint8_t seq,
104 const uint8_t *app_data, size_t app_len);
105
106/** @brief Result of feeding a link frame's user data to the transport reassembler. */
107enum Dnp3TransportResult
108{
109 DNP3_TR_IGNORED = 0, ///< out-of-sequence / a continuation with no active fragment: discarded
110 DNP3_TR_PROGRESS, ///< a segment was accepted, more expected
111 DNP3_TR_COMPLETE, ///< the fragment is fully reassembled (see buf / len)
112 DNP3_TR_ERROR, ///< a buffer overflow (the fragment is abandoned)
113};
114
115/** @brief Transport-function reassembly state (one in-flight application fragment). */
116struct Dnp3TransportRx
117{
118 uint8_t *buf; ///< caller-provided accumulation buffer
119 size_t cap; ///< its capacity
120 size_t len; ///< application octets accumulated so far
121 uint8_t expect_seq; ///< the sequence the next segment must carry
122 bool active; ///< a fragment is in progress (a FIR was seen)
123 bool done; ///< the last segment (FIN) was accepted
124};
125
126/** @brief Begin transport reassembly with a caller-owned buffer. */
127void pc_dnp3_transport_rx_init(Dnp3TransportRx *r, uint8_t *buf, size_t cap);
128
129/**
130 * @brief Feed one link frame's de-blocked user data (transport header + application octets).
131 * @return a @ref Dnp3TransportResult. On DNP3_TR_COMPLETE the reassembled fragment is @c buf[0..len).
132 */
133int pc_dnp3_transport_feed(Dnp3TransportRx *r, const uint8_t *user, size_t user_len);
134
135// --- application layer (IEEE 1815 §4.2.2): the reassembled fragment's header ---
136//
137// An application fragment begins with a 1-octet Application Control (AC) and a 1-octet Function Code (FC).
138// A response (FC RESPONSE / UNSOLICITED_RESPONSE) inserts two Internal Indication (IIN) octets before the
139// object data; a request carries object headers immediately after the FC. This layers on the fragment that
140// pc_dnp3_transport_feed reassembles.
141
142// Application Control octet (IEEE 1815 §4.2.2.1).
143#define DNP3_AC_FIR 0x80u ///< first fragment of a multi-fragment response
144#define DNP3_AC_FIN 0x40u ///< final fragment
145#define DNP3_AC_CON 0x20u ///< confirmation of this fragment is requested
146#define DNP3_AC_UNS 0x10u ///< unsolicited response
147#define DNP3_AC_SEQ_MASK 0x0Fu ///< 4-bit application sequence number
148
149// Application function codes (IEEE 1815 Table 4-1, common subset).
150#define DNP3_FC_CONFIRM 0x00u
151#define DNP3_FC_READ 0x01u
152#define DNP3_FC_WRITE 0x02u
153#define DNP3_FC_SELECT 0x03u
154#define DNP3_FC_OPERATE 0x04u
155#define DNP3_FC_DIRECT_OPERATE 0x05u
156#define DNP3_FC_DIRECT_OPERATE_NR 0x06u ///< direct operate, no response
157#define DNP3_FC_COLD_RESTART 0x0Du
158#define DNP3_FC_WARM_RESTART 0x0Eu
159#define DNP3_FC_RESPONSE 0x81u
160#define DNP3_FC_UNSOLICITED_RESPONSE 0x82u
161
162// Internal Indication bits (IEEE 1815 §4.2.2.5), packed IIN1 in the low octet, IIN2 in the high octet.
163#define DNP3_IIN_BROADCAST 0x0001u ///< IIN1.0 request was broadcast
164#define DNP3_IIN_CLASS1_EVENTS 0x0002u ///< IIN1.1 class 1 events available
165#define DNP3_IIN_CLASS2_EVENTS 0x0004u ///< IIN1.2 class 2 events available
166#define DNP3_IIN_CLASS3_EVENTS 0x0008u ///< IIN1.3 class 3 events available
167#define DNP3_IIN_NEED_TIME 0x0010u ///< IIN1.4 time synchronization required
168#define DNP3_IIN_LOCAL_CONTROL 0x0020u ///< IIN1.5 some output point is in local mode
169#define DNP3_IIN_DEVICE_TROUBLE 0x0040u ///< IIN1.6 device trouble
170#define DNP3_IIN_DEVICE_RESTART 0x0080u ///< IIN1.7 device restarted
171#define DNP3_IIN_FUNC_NOT_SUPPORTED 0x0100u ///< IIN2.0 function code not supported
172#define DNP3_IIN_OBJECT_UNKNOWN 0x0200u ///< IIN2.1 requested object(s) unknown
173#define DNP3_IIN_PARAM_ERROR 0x0400u ///< IIN2.2 parameter error in the request
174#define DNP3_IIN_EVENT_OVERFLOW 0x0800u ///< IIN2.3 event buffer overflowed
175#define DNP3_IIN_ALREADY_EXECUTING 0x1000u ///< IIN2.4 operation already executing
176#define DNP3_IIN_CONFIG_CORRUPT 0x2000u ///< IIN2.5 configuration corrupt
177
178/** @brief A decoded application-fragment header (from pc_dnp3_parse_app_header). */
179struct Dnp3AppHeader
180{
181 uint8_t app_control; ///< raw Application Control octet
182 bool fir;
183 bool fin;
184 bool con;
185 bool uns;
186 uint8_t seq; ///< 4-bit application sequence
187 uint8_t fc; ///< function code
188 bool is_response; ///< true for RESPONSE / UNSOLICITED_RESPONSE (the two IIN octets are present)
189 uint16_t iin; ///< internal indications (IIN1 low octet, IIN2 high octet); 0 for a request
190 const uint8_t *objects; ///< pointer into the fragment at the first object header (or nullptr if none)
191 size_t obj_len; ///< object octets remaining after the header
192};
193
194/** @brief Compose an Application Control octet from the FIR/FIN/CON/UNS flags and a 4-bit sequence. */
195uint8_t pc_dnp3_app_control(bool fir, bool fin, bool con, bool uns, uint8_t seq);
196
197/**
198 * @brief Build an application request fragment: AC + FC + @p obj_len object octets.
199 * @return the fragment length (2 + @p obj_len), or 0 on a bad argument / overflow.
200 */
201size_t pc_dnp3_build_app_request(uint8_t *out, size_t cap, uint8_t app_control, uint8_t fc, const uint8_t *objects,
202 size_t obj_len);
203
204/**
205 * @brief Build an application response fragment: AC + FC + IIN (2 octets, little-endian) + object octets.
206 * @return the fragment length (4 + @p obj_len), or 0 on a bad argument / overflow.
207 */
208size_t pc_dnp3_build_app_response(uint8_t *out, size_t cap, uint8_t app_control, uint8_t fc, uint16_t iin,
209 const uint8_t *objects, size_t obj_len);
210
211/**
212 * @brief Decode an application fragment's header (AC + FC, plus IIN for a response) into @p out.
213 * @return true iff @p len covers the header (2 octets, or 4 for a response); false otherwise.
214 */
215bool pc_dnp3_parse_app_header(const uint8_t *frag, size_t len, Dnp3AppHeader *out);
216
217// --- object header (IEEE 1815 §4.3): group + variation + qualifier + range, after the function code ---
218
219// Qualifier octet: the index-size / prefix code (bits 6-4) selects any per-object index prefix; the range
220// specifier code (bits 3-0) selects the range field form.
221#define DNP3_QUAL_PREFIX_MASK 0x70u
222#define DNP3_QUAL_PREFIX_SHIFT 4u
223#define DNP3_QUAL_RANGE_MASK 0x0Fu
224
225// Range specifier codes (qualifier low nibble), common forms.
226#define DNP3_RANGE_START_STOP_1 0x00u ///< 1-octet start + stop indexes
227#define DNP3_RANGE_START_STOP_2 0x01u ///< 2-octet start + stop (little-endian)
228#define DNP3_RANGE_START_STOP_4 0x02u ///< 4-octet start + stop (little-endian)
229#define DNP3_RANGE_NO_RANGE 0x06u ///< all objects (no range field)
230#define DNP3_RANGE_COUNT_1 0x07u ///< 1-octet object count
231#define DNP3_RANGE_COUNT_2 0x08u ///< 2-octet object count (little-endian)
232#define DNP3_RANGE_COUNT_4 0x09u ///< 4-octet object count (little-endian)
233
234/** @brief A decoded object header (from pc_dnp3_parse_object_header). */
235struct Dnp3ObjectHeader
236{
237 uint8_t group; ///< object group
238 uint8_t variation; ///< object variation
239 uint8_t qualifier; ///< the raw qualifier octet
240 uint8_t prefix_code; ///< index-size / prefix code (qualifier bits 6-4)
241 uint8_t range_code; ///< range specifier code (qualifier bits 3-0)
242 bool is_count; ///< true when the range field is an object count; false for a start/stop range
243 uint32_t start; ///< range start index (start/stop forms; 0 otherwise)
244 uint32_t stop; ///< range stop index (start/stop forms; 0 otherwise)
245 uint32_t count; ///< object count (count forms, or stop-start+1 for a start/stop range)
246 const uint8_t *objects; ///< the object data following the header, or nullptr if none
247 size_t objects_len; ///< octets remaining after the header
248};
249
250/**
251 * @brief Decode an object header (group + variation + qualifier + range) at the start of @p buf.
252 * @return true iff the header and its range field fit in @p len, and the qualifier is a supported form
253 * (start-stop 0x00/0x01/0x02, no-range 0x06, or count 0x07/0x08/0x09); false otherwise.
254 */
255bool pc_dnp3_parse_object_header(const uint8_t *buf, size_t len, Dnp3ObjectHeader *out);
256
257/**
258 * @brief Build an object header addressing a contiguous index range [@p start, @p stop]: group + variation +
259 * a start-stop qualifier, picking the smallest of the 1 / 2 / 4-octet range forms (0x00 / 0x01 / 0x02)
260 * that holds @p stop. The per-object index-prefix code is 0 (no prefix). @return octets written (5, 7,
261 * or 11), or 0 on @p stop < @p start, a null buffer, or overflow.
262 */
263size_t pc_dnp3_build_object_header_range(uint8_t *buf, size_t cap, uint8_t group, uint8_t variation, uint32_t start,
264 uint32_t stop);
265
266/**
267 * @brief Build an all-objects object header (qualifier 0x06, no range field): group + variation + 0x06 - the
268 * common "read every object of this group" form (e.g. a Class-data poll of group 60). @return 3, or 0.
269 */
270size_t pc_dnp3_build_object_header_all(uint8_t *buf, size_t cap, uint8_t group, uint8_t variation);
271
272// --- Control Relay Output Block (group 12 variation 1, IEEE 1815 Table): the 11-octet control object a
273// SELECT / OPERATE carries to command a binary output (relay). ---
274
275#define DNP3_CROB_LEN 11 ///< octets in a g12v1 CROB object
276
277// Control-code operation type (the low nibble of the control-code octet).
278#define DNP3_CROB_OP_NUL 0x00u ///< no operation
279#define DNP3_CROB_OP_PULSE_ON 0x01u ///< energize for on-time, then de-energize
280#define DNP3_CROB_OP_PULSE_OFF 0x02u ///< de-energize for off-time, then energize
281#define DNP3_CROB_OP_LATCH_ON 0x03u ///< energize and hold
282#define DNP3_CROB_OP_LATCH_OFF 0x04u ///< de-energize and hold
283
284// Control-code trip/close code (bits 6-7 of the control-code octet).
285#define DNP3_CROB_TCC_NUL 0x00u
286#define DNP3_CROB_TCC_CLOSE 0x01u
287#define DNP3_CROB_TCC_TRIP 0x02u
288
289/**
290 * @brief Build a Control Relay Output Block (g12v1) object: control code (@p op_type in the low nibble, the
291 * @p clear bit, and @p tcc in bits 6-7), @p count, the on / off time in milliseconds (each a 4-octet
292 * little-endian field), and a status octet (0 in a request). This is the control object itself; wrap it
293 * with an object header + index prefix inside a SELECT / OPERATE request.
294 * @return DNP3_CROB_LEN (11), or 0 on a null buffer, an @p op_type > 0x0F, a @p tcc > 3, or an overflow.
295 */
296size_t pc_dnp3_build_crob(uint8_t *buf, size_t cap, uint8_t op_type, uint8_t tcc, bool clear, uint8_t count,
297 uint32_t on_time_ms, uint32_t off_time_ms);
298
299// --- Analog Output Block (group 41): the analog counterpart to the CROB - the setpoint object a SELECT /
300// OPERATE carries to command an analog output. Variation 1 is a 32-bit signed integer, variation 3 a 32-bit
301// float; each carries the value (little-endian) followed by a 1-octet control status (0 in a request). ---
302
303#define DNP3_AOB_LEN 5 ///< octets in a g41v1 (32-bit int) or g41v3 (32-bit float) Analog Output Block object
304
305/**
306 * @brief Build a 32-bit Analog Output Block (g41v1) object: a signed 32-bit setpoint value (little-endian,
307 * two's complement) followed by a status octet (0 in a request). Wrap it with an object header + index
308 * prefix inside a SELECT / OPERATE request; the outstation reports the result status in its response.
309 * @return DNP3_AOB_LEN (5), or 0 on a null buffer or an overflow.
310 */
311size_t pc_dnp3_build_aob32(uint8_t *buf, size_t cap, int32_t value);
312
313/**
314 * @brief Build a 32-bit floating-point Analog Output Block (g41v3) object: an IEEE-754 single-precision
315 * setpoint value (little-endian) followed by a status octet (0 in a request).
316 * @return DNP3_AOB_LEN (5), or 0 on a null buffer or an overflow.
317 */
318size_t pc_dnp3_build_aob_float(uint8_t *buf, size_t cap, float value);
319
320#endif // PC_ENABLE_DNP3
321
322#endif // PROTOCORE_DNP3_H
User-facing configuration for ProtoCore.