ProtoCore v0.0.2
Deterministic, zero-heap network stack for embedded targets
Loading...
Searching...
No Matches
bacnet.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 bacnet.cpp
6 * @brief BACnet/IP BVLC + NPDU builder + parser (pure, host-tested).
7 */
8
10
11#if PC_ENABLE_BACNET
12
13#include <string.h>
14
15size_t pc_bvlc_build(uint8_t *buf, size_t cap, uint8_t function, const uint8_t *npdu, size_t pc_npdu_len)
16{
17 if (!buf || (pc_npdu_len && !npdu))
18 {
19 return 0;
20 }
21 size_t total = BVLC_HEADER_SIZE + pc_npdu_len;
22 if (total > 0xFFFF || total > cap)
23 {
24 return 0;
25 }
26 buf[0] = BVLC_TYPE_BIP;
27 buf[1] = function;
28 buf[2] = (uint8_t)(total >> 8); // length, big-endian, the whole BVLL
29 buf[3] = (uint8_t)(total & 0xFF);
30 if (pc_npdu_len)
31 {
32 memcpy(buf + BVLC_HEADER_SIZE, npdu, pc_npdu_len);
33 }
34 return total;
35}
36
37bool pc_bvlc_parse(const uint8_t *buf, size_t len, uint8_t *function, const uint8_t **npdu, size_t *pc_npdu_len)
38{
39 if (!buf || len < BVLC_HEADER_SIZE || buf[0] != BVLC_TYPE_BIP)
40 {
41 return false;
42 }
43 size_t total = ((size_t)buf[2] << 8) | buf[3];
44 if (total < BVLC_HEADER_SIZE || total > len)
45 {
46 return false;
47 }
48 if (function)
49 {
50 *function = buf[1];
51 }
52 if (npdu)
53 {
54 *npdu = buf + BVLC_HEADER_SIZE;
55 }
56 if (pc_npdu_len)
57 {
58 *pc_npdu_len = total - BVLC_HEADER_SIZE;
59 }
60 return true;
61}
62
63size_t pc_npdu_build(uint8_t *buf, size_t cap, bool expecting_reply, uint8_t priority, bool has_dest, uint16_t dnet,
64 const uint8_t *dadr, uint8_t dadr_len, uint8_t hop_count, const uint8_t *apdu, size_t apdu_len)
65{
66 if (!buf || (apdu_len && !apdu) || (dadr_len && !dadr))
67 {
68 return 0;
69 }
70 size_t need = 2 + apdu_len; // version + control + apdu
71 if (has_dest)
72 {
73 need += 2 + 1 + dadr_len + 1; // DNET + DLEN + DADR + hop count
74 }
75 if (need > cap)
76 {
77 return 0;
78 }
79
80 size_t p = 0;
81 buf[p++] = NPDU_VERSION;
82 uint8_t control = (uint8_t)(priority & NPCI_PRIORITY_MASK);
83 if (expecting_reply)
84 {
85 control |= NPCI_EXPECTING_REPLY;
86 }
87 if (has_dest)
88 {
89 control |= NPCI_DEST_PRESENT;
90 }
91 buf[p++] = control;
92 if (has_dest)
93 {
94 buf[p++] = (uint8_t)(dnet >> 8);
95 buf[p++] = (uint8_t)(dnet & 0xFF);
96 buf[p++] = dadr_len;
97 if (dadr_len)
98 {
99 memcpy(buf + p, dadr, dadr_len);
100 p += dadr_len;
101 }
102 buf[p++] = hop_count; // follows the (absent) source fields when a destination is present
103 }
104 if (apdu_len)
105 {
106 memcpy(buf + p, apdu, apdu_len);
107 p += apdu_len;
108 }
109 return p;
110}
111
112bool pc_npdu_parse(const uint8_t *buf, size_t len, NpduInfo *out)
113{
114 if (!buf || !out || len < 2 || buf[0] != NPDU_VERSION)
115 {
116 return false;
117 }
118 uint8_t control = buf[1];
119 size_t p = 2;
120
121 out->control = control;
122 out->network_message = (control & NPCI_NETWORK_MSG) != 0;
123 out->dest_present = (control & NPCI_DEST_PRESENT) != 0;
124 out->src_present = (control & NPCI_SRC_PRESENT) != 0;
125 out->dnet = 0;
126 out->snet = 0;
127 out->hop_count = 0;
128
129 if (out->dest_present)
130 {
131 if (p + 3 > len)
132 {
133 return false;
134 }
135 out->dnet = (uint16_t)((buf[p] << 8) | buf[p + 1]);
136 uint8_t dlen = buf[p + 2];
137 p += 3 + dlen;
138 if (p > len)
139 {
140 return false;
141 }
142 }
143 if (out->src_present)
144 {
145 if (p + 3 > len)
146 {
147 return false;
148 }
149 out->snet = (uint16_t)((buf[p] << 8) | buf[p + 1]);
150 uint8_t slen = buf[p + 2];
151 p += 3 + slen;
152 if (p > len)
153 {
154 return false;
155 }
156 }
157 if (out->dest_present) // the hop count follows the source fields
158 {
159 if (p + 1 > len)
160 {
161 return false;
162 }
163 out->hop_count = buf[p++];
164 }
165 out->apdu = buf + p;
166 out->apdu_len = len - p;
167 return true;
168}
169
170// Encode a BACnet tagged unsigned integer (tag octet = tag-number<<4 | class | length), using the minimal
171// number of value octets (big-endian). @p context selects the tag class: a context tag ORs 0x08 into the tag
172// octet, an application tag does not. Callers pass values that fit the 4-bit length field (<= 4 octets).
173static size_t bacnet_put_tagged_uint(uint8_t *buf, uint8_t tag_number, uint32_t value, bool context)
174{
175 uint8_t v[4];
176 size_t vlen = 0;
177 if (value == 0)
178 {
179 v[vlen++] = 0;
180 }
181 else
182 {
183 for (int shift = 24; shift >= 0; shift -= 8) // big-endian, dropping the leading zero octets
184 {
185 uint8_t b = (uint8_t)(value >> shift);
186 if (vlen == 0 && b == 0)
187 {
188 continue;
189 }
190 v[vlen++] = b;
191 }
192 }
193 size_t p = 0;
194 buf[p++] = (uint8_t)(((uint32_t)tag_number << 4) | (context ? 0x08u : 0x00u) | (uint8_t)vlen);
195 for (size_t i = 0; i < vlen; i++)
196 {
197 buf[p++] = v[i];
198 }
199 return p;
200}
201
202size_t pc_apdu_build_who_is(uint8_t *buf, size_t cap, uint32_t low_limit, uint32_t high_limit, bool has_limits)
203{
204 if (!buf)
205 {
206 return 0;
207 }
208 uint8_t tmp[16]; // worst case: 2 header + 2 * (1 tag + 3 value) = 10
209 size_t p = 0;
210 tmp[p++] = (uint8_t)(BACNET_PDU_UNCONFIRMED_REQUEST << 4); // 0x10, no flags
211 tmp[p++] = BACNET_SVC_UN_WHO_IS; // service choice 8
212 if (has_limits)
213 {
214 if (low_limit > BACNET_MAX_INSTANCE || high_limit > BACNET_MAX_INSTANCE || low_limit > high_limit)
215 {
216 return 0;
217 }
218 p += bacnet_put_tagged_uint(tmp + p, 0, low_limit, true);
219 p += bacnet_put_tagged_uint(tmp + p, 1, high_limit, true);
220 }
221 if (cap < p)
222 {
223 return 0;
224 }
225 memcpy(buf, tmp, p);
226 return p;
227}
228
229size_t pc_apdu_build_i_am(uint8_t *buf, size_t cap, uint32_t device_instance, uint32_t max_apdu, uint8_t segmentation,
230 uint16_t vendor_id)
231{
232 if (!buf || device_instance > BACNET_MAX_INSTANCE || segmentation > 3)
233 {
234 return 0;
235 }
236 uint8_t tmp[24]; // worst case: 2 header + 5 oid + 5 max-apdu + 2 seg + 5 vendor = 19
237 size_t p = 0;
238 tmp[p++] = (uint8_t)(BACNET_PDU_UNCONFIRMED_REQUEST << 4); // 0x10, no flags
239 tmp[p++] = BACNET_SVC_UN_I_AM; // service choice 0
240 // I-Am device object identifier: application tag 12, a 4-octet (object-type << 22) | instance.
241 tmp[p++] = 0xC4; // application tag 12, length 4
242 uint32_t oid = ((uint32_t)BACNET_OBJ_DEVICE << 22) | device_instance;
243 tmp[p++] = (uint8_t)(oid >> 24);
244 tmp[p++] = (uint8_t)(oid >> 16);
245 tmp[p++] = (uint8_t)(oid >> 8);
246 tmp[p++] = (uint8_t)oid;
247 p += bacnet_put_tagged_uint(tmp + p, 2, max_apdu, false); // max APDU length accepted (unsigned)
248 tmp[p++] = 0x91; // segmentation supported: application tag 9 (enumerated), length 1
249 tmp[p++] = segmentation;
250 p += bacnet_put_tagged_uint(tmp + p, 2, vendor_id, false); // vendor id (unsigned)
251 if (cap < p)
252 {
253 return 0;
254 }
255 memcpy(buf, tmp, p);
256 return p;
257}
258
259size_t pc_apdu_build_read_property(uint8_t *buf, size_t cap, uint8_t invoke_id, uint8_t max_resp, uint16_t object_type,
260 uint32_t object_instance, uint32_t property_id)
261{
262 if (!buf || object_instance > BACNET_MAX_INSTANCE || object_type > 0x3FFu) // object type is 10 bits
263 {
264 return 0;
265 }
266 uint8_t tmp[16]; // 4 header + 5 object-id (tag + 4) + 5 property (tag + <= 4) = 14 worst case
267 size_t p = 0;
268 tmp[p++] = (uint8_t)(BACNET_PDU_CONFIRMED_REQUEST << 4); // 0x00, unsegmented
269 tmp[p++] = max_resp; // max segments accepted / max APDU length accepted
270 tmp[p++] = invoke_id;
271 tmp[p++] = BACNET_SVC_CONF_READ_PROPERTY; // service choice 12
272 // Object identifier: context tag 0, a 4-octet (object-type << 22) | instance.
273 tmp[p++] = 0x0C; // context tag 0, length 4
274 uint32_t oid = ((uint32_t)object_type << 22) | object_instance;
275 tmp[p++] = (uint8_t)(oid >> 24);
276 tmp[p++] = (uint8_t)(oid >> 16);
277 tmp[p++] = (uint8_t)(oid >> 8);
278 tmp[p++] = (uint8_t)oid;
279 // Property identifier: context tag 1 (enumerated), minimal-length.
280 p += bacnet_put_tagged_uint(tmp + p, 1, property_id, true);
281 if (cap < p)
282 {
283 return 0;
284 }
285 memcpy(buf, tmp, p);
286 return p;
287}
288
289// Confirmed-Request APDU header: flags + the max-segs/max-apdu octet + invoke id, then a segment
290// sequence/window pair when segmented, then the service choice. Advances *p; false on a short buffer.
291static bool apdu_parse_confirmed_request(const uint8_t *apdu, size_t len, BacnetApdu *out, size_t *p)
292{
293 out->segmented = (apdu[0] & BACNET_APDU_SEG) != 0;
294 out->more_follows = (apdu[0] & BACNET_APDU_MOR) != 0;
295 out->sa = (apdu[0] & BACNET_APDU_SA) != 0;
296 if (len < *p + 2) // max-segs/max-apdu octet + invoke id
297 {
298 return false;
299 }
300 out->invoke_id = apdu[*p + 1]; // apdu[1] is max segments / max APDU, apdu[2] is the invoke id
301 *p += 2;
302 if (out->segmented) // a segmented request carries a sequence number + proposed window size
303 {
304 if (len < *p + 2)
305 {
306 return false;
307 }
308 *p += 2;
309 }
310 if (len < *p + 1)
311 {
312 return false;
313 }
314 out->service_choice = apdu[(*p)++];
315 return true;
316}
317
318// Complex-ACK APDU header: flags + invoke id, then a segment sequence/window pair when segmented, then
319// the service-ACK choice. Advances *p; false on a short buffer.
320static bool apdu_parse_complex_ack(const uint8_t *apdu, size_t len, BacnetApdu *out, size_t *p)
321{
322 out->segmented = (apdu[0] & BACNET_APDU_SEG) != 0;
323 out->more_follows = (apdu[0] & BACNET_APDU_MOR) != 0;
324 if (len < *p + 1) // invoke id
325 {
326 return false;
327 }
328 out->invoke_id = apdu[(*p)++];
329 if (out->segmented)
330 {
331 if (len < *p + 2)
332 {
333 return false;
334 }
335 *p += 2;
336 }
337 if (len < *p + 1)
338 {
339 return false;
340 }
341 out->service_choice = apdu[(*p)++];
342 return true;
343}
344
345bool pc_apdu_parse(const uint8_t *apdu, size_t len, BacnetApdu *out)
346{
347 if (!apdu || !out || len < 1)
348 {
349 return false;
350 }
351 memset(out, 0, sizeof(*out));
352 out->pdu_type = (uint8_t)(apdu[0] >> 4);
353 size_t p = 1;
354 switch (out->pdu_type)
355 {
356 case BACNET_PDU_CONFIRMED_REQUEST:
357 if (!apdu_parse_confirmed_request(apdu, len, out, &p))
358 {
359 return false;
360 }
361 break;
362 case BACNET_PDU_UNCONFIRMED_REQUEST:
363 if (len < p + 1)
364 {
365 return false;
366 }
367 out->service_choice = apdu[p++];
368 break;
369 case BACNET_PDU_SIMPLE_ACK:
370 if (len < p + 2) // invoke id + service-ACK choice
371 {
372 return false;
373 }
374 out->invoke_id = apdu[p++];
375 out->service_choice = apdu[p++];
376 break;
377 case BACNET_PDU_COMPLEX_ACK:
378 if (!apdu_parse_complex_ack(apdu, len, out, &p))
379 {
380 return false;
381 }
382 break;
383 default:
384 return false; // segment-ack / error / reject / abort are not decoded here
385 }
386 out->service_data = (p < len) ? apdu + p : nullptr;
387 out->service_data_len = len - p;
388 return true;
389}
390
391#endif // PC_ENABLE_BACNET
BACnet/IP BVLC + NPDU codec (PC_ENABLE_BACNET) - zero-heap framing for the ASHRAE 135 building-automa...