ProtoCore v1.0.16
Deterministic, zero-heap network stack for embedded targets
Loading...
Searching...
No Matches
bacnet.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 bacnet.h
6 * @brief BACnet/IP BVLC + NPDU codec (PROTOCORE_ENABLE_BACNET) - zero-heap framing for the
7 * ASHRAE 135 building-automation network layer over UDP (default port 47808).
8 *
9 * Two stacked layers:
10 * - BVLC (Annex J): `Type(1)=0x81 Function(1) Length(2, big-endian, whole BVLL)` then the
11 * NPDU. Functions include Original-Unicast-NPDU (0x0A) and Original-Broadcast-NPDU (0x0B).
12 * - NPDU (Clause 6): `Version(1)=0x01 NPCI-Control(1)` then optional addressing. The NPCI
13 * control bits: 0x80 = network-layer message (else APDU), 0x20 = destination present
14 * (DNET(2) DLEN(1) DADR(DLEN); DLEN 0 = remote broadcast), 0x08 = source present (SNET(2)
15 * SLEN(1) SADR), 0x04 = expecting reply, low 2 bits = priority. A hop count octet follows
16 * the source fields when a destination is present. The APDU is whatever remains.
17 *
18 * The builders frame an APDU into a caller buffer (fail-closed); the parsers validate and
19 * report the slices. Layout verified against ASHRAE 135 Annex J / Clause 6.
20 *
21 * @author Douglas Quigg (dstroy0)
22 * @date 2026
23 */
24
25#ifndef PROTOCORE_BACNET_H
26#define PROTOCORE_BACNET_H
27
28#include "protocore_config.h" // the entry point: protocore_types.h for the widths
29
30#if PROTOCORE_ENABLE_BACNET
31
33
34// This module holds nothing between calls, so it carves no borrow and states none. An entry
35// takes one all the same, and never reads it, so every namespace in the tree is invoked the
36// same way.
37
38#define BVLC_TYPE_BIP 0x81 ///< BVLC Type: BACnet/IP
39#define BVLC_HEADER_SIZE 4 ///< type + function + 2-octet length
40
41// BVLC functions (Annex J).
42#define BVLC_FUNC_RESULT 0x00
43#define BVLC_FUNC_WRITE_BDT 0x01
44#define BVLC_FUNC_FORWARDED_NPDU 0x04
45#define BVLC_FUNC_REGISTER_FD 0x05
46#define BVLC_FUNC_ORIGINAL_UNICAST 0x0A
47#define BVLC_FUNC_ORIGINAL_BROADCAST 0x0B
48
49#define NPDU_VERSION 0x01 ///< ASCII 1, the only defined protocol version
50
51// NPCI control-octet bits (Clause 6.2.2).
52#define NPCI_NETWORK_MSG 0x80 ///< NSDU is a network-layer message (else an APDU)
53#define NPCI_DEST_PRESENT 0x20 ///< DNET / DLEN / DADR present
54#define NPCI_SRC_PRESENT 0x08 ///< SNET / SLEN / SADR present
55#define NPCI_EXPECTING_REPLY 0x04 ///< a reply is expected
56#define NPCI_PRIORITY_MASK 0x03 ///< message priority (low 2 bits)
57
58// Message priorities.
59#define NPDU_PRIO_NORMAL 0x00
60#define NPDU_PRIO_URGENT 0x01
61#define NPDU_PRIO_CRITICAL 0x02
62#define NPDU_PRIO_LIFE_SAFETY 0x03
63
64// PDU types (the high nibble of the first APDU octet).
65#define BACNET_PDU_CONFIRMED_REQUEST 0
66#define BACNET_PDU_UNCONFIRMED_REQUEST 1
67#define BACNET_PDU_SIMPLE_ACK 2
68#define BACNET_PDU_COMPLEX_ACK 3
69#define BACNET_PDU_SEGMENT_ACK 4
70#define BACNET_PDU_ERROR 5
71#define BACNET_PDU_REJECT 6
72#define BACNET_PDU_ABORT 7
73
74// PDU flags (the low nibble of the first octet, on confirmed-request / complex-ack).
75#define BACNET_APDU_SEG 0x08 ///< the message is segmented
76#define BACNET_APDU_MOR 0x04 ///< more segments follow
77#define BACNET_APDU_SA 0x02 ///< the sender accepts a segmented response (confirmed-request only)
78
79// Unconfirmed-request service choices (ASHRAE 135 §21).
80#define BACNET_SVC_UN_I_AM 0 ///< I-Am
81#define BACNET_SVC_UN_WHO_IS 8 ///< Who-Is
82
83// Confirmed-request service choices (ASHRAE 135 §15).
84#define BACNET_SVC_CONF_READ_PROPERTY 12 ///< ReadProperty
85
86#define BACNET_MAX_INSTANCE 0x3FFFFFu ///< maximum BACnet object / device instance (22-bit)
87
88// Object types (the 10-bit high field of a BACnetObjectIdentifier).
89#define BACNET_OBJ_ANALOG_INPUT 0 ///< object type: Analog Input
90#define BACNET_OBJ_ANALOG_OUTPUT 1 ///< object type: Analog Output
91#define BACNET_OBJ_ANALOG_VALUE 2 ///< object type: Analog Value
92#define BACNET_OBJ_BINARY_INPUT 3 ///< object type: Binary Input
93#define BACNET_OBJ_BINARY_OUTPUT 4 ///< object type: Binary Output
94#define BACNET_OBJ_BINARY_VALUE 5 ///< object type: Binary Value
95#define BACNET_OBJ_DEVICE 8 ///< object type: Device (used in the I-Am object identifier)
96
97// Common property identifiers (ASHRAE 135 §12).
98#define BACNET_PROP_OBJECT_NAME 77 ///< object-name property
99#define BACNET_PROP_PRESENT_VALUE 85 ///< present-value property
100
101/** @brief A parsed NPDU. @ref apdu points INTO the source buffer. */
102typedef struct
103{
104 uint8_t control;
105 proto_bool network_message; ///< control & 0x80
106 proto_bool dest_present;
107 uint16_t dnet;
108 proto_bool src_present;
109 uint16_t snet;
110 uint8_t hop_count; ///< valid when dest_present
111 const uint8_t *apdu;
112 size_t apdu_len;
113} NpduInfo;
114
115/** @brief A decoded APDU header (from Bacnet.apdu_parse). Service data points INTO the source buffer. */
116typedef struct
117{
118 uint8_t pdu_type; ///< PDU type (BACNET_PDU_*)
119 proto_bool segmented; ///< SEG flag (confirmed-request / complex-ack)
120 proto_bool more_follows; ///< MOR flag
121 proto_bool sa; ///< segmented-response-accepted flag (confirmed-request)
122 uint8_t invoke_id; ///< invoke id (confirmed-request / simple-ack / complex-ack)
123 uint8_t service_choice; ///< service choice
124 const uint8_t *service_data; ///< the service parameters after the header, or nullptr if none
125 size_t service_data_len; ///< octets remaining after the header
126} BacnetApdu;
127
128/** @brief What bvlc_build takes: buf, cap, function, npdu, npdu_len. */
129typedef struct
130{
131 uint8_t *buf;
132 size_t cap;
133 uint8_t function;
134 const uint8_t *npdu;
135 size_t npdu_len;
136} BacnetBvlcBuildArgs;
137
138/** @brief What bvlc_parse takes: buf, len, function, npdu, npdu_len. */
139typedef struct
140{
141 const uint8_t *buf;
142 size_t len;
143 uint8_t *function;
144 const uint8_t **npdu;
145 size_t *npdu_len;
146} BacnetBvlcParseArgs;
147
148/** @brief What npdu_build takes: buf, cap, expecting_reply, priority, ... */
149typedef struct
150{
151 uint8_t *buf;
152 size_t cap;
153 proto_bool expecting_reply;
154 uint8_t priority;
155 proto_bool has_dest;
156 uint16_t dnet;
157 const uint8_t *dadr;
158 uint8_t dadr_len;
159 uint8_t hop_count;
160 const uint8_t *apdu;
161 size_t apdu_len;
162} BacnetNpduBuildArgs;
163
164/** @brief What npdu_parse takes: buf, len, out. */
165typedef struct
166{
167 const uint8_t *buf;
168 size_t len;
169 NpduInfo *out;
170} BacnetNpduParseArgs;
171
172/** @brief What apdu_parse takes: apdu, len, out. */
173typedef struct
174{
175 const uint8_t *apdu;
176 size_t len;
177 BacnetApdu *out;
178} BacnetApduParseArgs;
179
180/** @brief What apdu_build_who_is takes: buf, cap, low_limit, ... */
181typedef struct
182{
183 uint8_t *buf;
184 size_t cap;
185 uint32_t low_limit;
186 uint32_t high_limit;
187 proto_bool has_limits;
188} BacnetApduBuildWhoIsArgs;
189
190/** @brief What apdu_build_i_am takes: buf, cap, device_instance, ... */
191typedef struct
192{
193 uint8_t *buf;
194 size_t cap;
195 uint32_t device_instance;
196 uint32_t max_apdu;
197 uint8_t segmentation;
198 uint16_t vendor_id;
199} BacnetApduBuildIAmArgs;
200
201/** @brief What apdu_build_read_property takes: buf, cap, invoke_id, ... */
202typedef struct
203{
204 uint8_t *buf;
205 size_t cap;
206 uint8_t invoke_id;
207 uint8_t max_resp;
208 uint16_t object_type;
209 uint32_t object_instance;
210 uint32_t property_id;
211} BacnetApduBuildReadPropertyArgs;
212
213/**
214 * @brief BACnet/IP BVLC + NPDU codec (PROTOCORE_ENABLE_BACNET) - zero-heap framing for the ASHRAE 135
215 * building-automation network layer over UDP (default port 47808).
216 *
217 * A caller sets the members a call takes, invokes it through ::Bacnet with the bytes it runs
218 * out of, and reads the outcome off the same handle.
219 *
220 * Bacnet.bvlc_build_args.buf = ...;
221 * Bacnet.bvlc_build_args.cap = ...;
222 * Bacnet.bvlc_build_args.function = ...;
223 * Bacnet.bvlc_build_args.npdu = ...;
224 * Bacnet.bvlc_build_args.npdu_len = ...;
225 * Bacnet.bvlc_build(work);
226 * // Bacnet.n is what the call reports
227 *
228 * @var BacnetNs::bvlc_build_args what bvlc_build takes: buf, cap, function, npdu, npdu_len
229 * @var BacnetNs::bvlc_parse_args what bvlc_parse takes: buf, len, function, npdu, npdu_len
230 * @var BacnetNs::npdu_build_args what npdu_build takes: buf, cap, expecting_reply, priority,
231 * @var BacnetNs::npdu_parse_args what npdu_parse takes: buf, len, out
232 * @var BacnetNs::apdu_parse_args what apdu_parse takes: apdu, len, out
233 * @var BacnetNs::apdu_build_who_is_args what apdu_build_who_is takes: buf, cap, low_limit,
234 * @var BacnetNs::apdu_build_i_am_args what apdu_build_i_am takes: buf, cap, device_instance,
235 * @var BacnetNs::apdu_build_read_property_args what apdu_build_read_property takes: buf, cap, invoke_id,
236 * @var BacnetNs::ok true iff len covers the header for a supported PDU type (confirmed ...
237 * @var BacnetNs::n the APDU length, or 0 on overflow, a limit above ...
238 * @var BacnetNs::bvlc_build wrap an NPDU in a BVLC envelope. Returns total octets, or 0 on ...
239 * @var BacnetNs::bvlc_parse parse a BVLC envelope; reports the function and the NPDU slice
240 * @var BacnetNs::npdu_build build an NPDU carrying apdu. With has_dest, the destination ...
241 * @var BacnetNs::npdu_parse parse + validate an NPDU (version, control, optional addressing) ...
242 * @var BacnetNs::apdu_parse decode an APDU header (PDU type, flags, invoke id, service choice) ...
243 * @var BacnetNs::apdu_build_who_is build a Who-Is unconfirmed-request APDU (service choice 8). With ...
244 * @var BacnetNs::apdu_build_i_am build an I-Am unconfirmed-request APDU (service choice 0) - a ...
245 * @var BacnetNs::apdu_build_read_property build a ReadProperty confirmed-request APDU (service choice 12) - ...
246 *
247 * @c work is bytes the CALLER holds. This module reads none of them: it carries nothing
248 * between calls, so there is no state to keep and nothing to wipe. The parameter is there so
249 * a caller drives every namespace the same way.
250 */
251typedef struct
252{
253 BacnetBvlcBuildArgs bvlc_build_args;
254 BacnetBvlcParseArgs bvlc_parse_args;
255 BacnetNpduBuildArgs npdu_build_args;
256 BacnetNpduParseArgs npdu_parse_args;
257 BacnetApduParseArgs apdu_parse_args;
258 BacnetApduBuildWhoIsArgs apdu_build_who_is_args;
259 BacnetApduBuildIAmArgs apdu_build_i_am_args;
260 BacnetApduBuildReadPropertyArgs apdu_build_read_property_args;
261 proto_bool ok;
262 size_t n;
263} BacnetVars;
264
265/** @brief The operands and the outcome. */
266extern BacnetVars BacnetV;
267
268/** @brief The entries. */
269typedef struct
270{
271 void (*const bvlc_build)(uint8_t *work);
272 void (*const bvlc_parse)(uint8_t *work);
273 void (*const npdu_build)(uint8_t *work);
274 void (*const npdu_parse)(uint8_t *work);
275 void (*const apdu_parse)(uint8_t *work);
276 void (*const apdu_build_who_is)(uint8_t *work);
277 void (*const apdu_build_i_am)(uint8_t *work);
278 void (*const apdu_build_read_property)(uint8_t *work);
279} BacnetNs;
280
281// What the table binds, defined once in the .c and taking one parameter each: everything
282// else an entry needs is an operand in BacnetV or a region of the borrow at a fixed offset.
283void protocore_bacnet_bvlc_build(uint8_t *work);
284void protocore_bacnet_bvlc_parse(uint8_t *work);
285void protocore_bacnet_npdu_build(uint8_t *work);
286void protocore_bacnet_npdu_parse(uint8_t *work);
287void protocore_bacnet_apdu_parse(uint8_t *work);
288void protocore_bacnet_apdu_build_who_is(uint8_t *work);
289void protocore_bacnet_apdu_build_i_am(uint8_t *work);
290void protocore_bacnet_apdu_build_read_property(uint8_t *work);
291
292// `static const`, initialised HERE rather than `extern` against a definition in the .c: a
293// const object whose initializer every translation unit can see is a COMPILE-TIME FACT, so
294// `Bacnet.bvlc_build(work)` resolves to a named function and becomes a DIRECT call. An extern table
295// leaves the call indirect and the symbol live at every level, -O2 -flto included.
296static const BacnetNs Bacnet __attribute__((unused)) = {
297 .bvlc_build = protocore_bacnet_bvlc_build,
298 .bvlc_parse = protocore_bacnet_bvlc_parse,
299 .npdu_build = protocore_bacnet_npdu_build,
300 .npdu_parse = protocore_bacnet_npdu_parse,
301 .apdu_parse = protocore_bacnet_apdu_parse,
302 .apdu_build_who_is = protocore_bacnet_apdu_build_who_is,
303 .apdu_build_i_am = protocore_bacnet_apdu_build_i_am,
304 .apdu_build_read_property = protocore_bacnet_apdu_build_read_property,
305};
306
308
309#endif // PROTOCORE_ENABLE_BACNET
310
311#endif // PROTOCORE_BACNET_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