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