ProtoCore v0.0.1
Deterministic, zero-heap network stack for embedded targets
Loading...
Searching...
No Matches
bacnet.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 bacnet.h
6 * @brief BACnet/IP BVLC + NPDU codec (PC_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"
29
30#if PC_ENABLE_BACNET
31
32#include <stddef.h>
33#include <stdint.h>
34
35#define BVLC_TYPE_BIP 0x81 ///< BVLC Type: BACnet/IP
36#define BVLC_HEADER_SIZE 4 ///< type + function + 2-octet length
37
38// BVLC functions (Annex J).
39#define BVLC_FUNC_RESULT 0x00
40#define BVLC_FUNC_WRITE_BDT 0x01
41#define BVLC_FUNC_FORWARDED_NPDU 0x04
42#define BVLC_FUNC_REGISTER_FD 0x05
43#define BVLC_FUNC_ORIGINAL_UNICAST 0x0A
44#define BVLC_FUNC_ORIGINAL_BROADCAST 0x0B
45
46#define NPDU_VERSION 0x01 ///< ASCII 1, the only defined protocol version
47
48// NPCI control-octet bits (Clause 6.2.2).
49#define NPCI_NETWORK_MSG 0x80 ///< NSDU is a network-layer message (else an APDU)
50#define NPCI_DEST_PRESENT 0x20 ///< DNET / DLEN / DADR present
51#define NPCI_SRC_PRESENT 0x08 ///< SNET / SLEN / SADR present
52#define NPCI_EXPECTING_REPLY 0x04 ///< a reply is expected
53#define NPCI_PRIORITY_MASK 0x03 ///< message priority (low 2 bits)
54
55// Message priorities.
56#define NPDU_PRIO_NORMAL 0x00
57#define NPDU_PRIO_URGENT 0x01
58#define NPDU_PRIO_CRITICAL 0x02
59#define NPDU_PRIO_LIFE_SAFETY 0x03
60
61// ---- BVLC ----
62
63/** @brief Wrap an NPDU in a BVLC envelope. Returns total octets, or 0 on overflow. */
64size_t pc_bvlc_build(uint8_t *buf, size_t cap, uint8_t function, const uint8_t *npdu, size_t pc_npdu_len);
65
66/** @brief Parse a BVLC envelope; reports the function and the NPDU slice. */
67bool pc_bvlc_parse(const uint8_t *buf, size_t len, uint8_t *function, const uint8_t **npdu, size_t *pc_npdu_len);
68
69// ---- NPDU ----
70
71/**
72 * @brief Build an NPDU carrying @p apdu. With @p has_dest, the destination addressing
73 * (DNET / DLEN / DADR) and the hop count are emitted (DLEN 0 + @p dnet 0xFFFF is a
74 * remote/global broadcast).
75 */
76size_t pc_npdu_build(uint8_t *buf, size_t cap, bool expecting_reply, uint8_t priority, bool has_dest, uint16_t dnet,
77 const uint8_t *dadr, uint8_t dadr_len, uint8_t hop_count, const uint8_t *apdu, size_t apdu_len);
78
79/** @brief A parsed NPDU. @ref apdu points INTO the source buffer. */
80struct NpduInfo
81{
82 uint8_t control;
83 bool network_message; ///< control & 0x80
84 bool dest_present;
85 uint16_t dnet;
86 bool src_present;
87 uint16_t snet;
88 uint8_t hop_count; ///< valid when dest_present
89 const uint8_t *apdu;
90 size_t apdu_len;
91};
92
93/** @brief Parse + validate an NPDU (version, control, optional addressing) and slice the APDU. */
94bool pc_npdu_parse(const uint8_t *buf, size_t len, NpduInfo *out);
95
96// --- APDU header (the application layer sliced out by pc_npdu_parse) ---
97
98// PDU types (the high nibble of the first APDU octet).
99#define BACNET_PDU_CONFIRMED_REQUEST 0
100#define BACNET_PDU_UNCONFIRMED_REQUEST 1
101#define BACNET_PDU_SIMPLE_ACK 2
102#define BACNET_PDU_COMPLEX_ACK 3
103#define BACNET_PDU_SEGMENT_ACK 4
104#define BACNET_PDU_ERROR 5
105#define BACNET_PDU_REJECT 6
106#define BACNET_PDU_ABORT 7
107
108// PDU flags (the low nibble of the first octet, on confirmed-request / complex-ack).
109#define BACNET_APDU_SEG 0x08 ///< the message is segmented
110#define BACNET_APDU_MOR 0x04 ///< more segments follow
111#define BACNET_APDU_SA 0x02 ///< the sender accepts a segmented response (confirmed-request only)
112
113// Unconfirmed-request service choices (ASHRAE 135 §21).
114#define BACNET_SVC_UN_I_AM 0 ///< I-Am
115#define BACNET_SVC_UN_WHO_IS 8 ///< Who-Is
116
117// Confirmed-request service choices (ASHRAE 135 §15).
118#define BACNET_SVC_CONF_READ_PROPERTY 12 ///< ReadProperty
119
120#define BACNET_MAX_INSTANCE 0x3FFFFFu ///< maximum BACnet object / device instance (22-bit)
121
122// Object types (the 10-bit high field of a BACnetObjectIdentifier).
123#define BACNET_OBJ_ANALOG_INPUT 0 ///< object type: Analog Input
124#define BACNET_OBJ_ANALOG_OUTPUT 1 ///< object type: Analog Output
125#define BACNET_OBJ_ANALOG_VALUE 2 ///< object type: Analog Value
126#define BACNET_OBJ_BINARY_INPUT 3 ///< object type: Binary Input
127#define BACNET_OBJ_BINARY_OUTPUT 4 ///< object type: Binary Output
128#define BACNET_OBJ_BINARY_VALUE 5 ///< object type: Binary Value
129#define BACNET_OBJ_DEVICE 8 ///< object type: Device (used in the I-Am object identifier)
130
131// Common property identifiers (ASHRAE 135 §12).
132#define BACNET_PROP_OBJECT_NAME 77 ///< object-name property
133#define BACNET_PROP_PRESENT_VALUE 85 ///< present-value property
134
135/** @brief A decoded APDU header (from pc_apdu_parse). Service data points INTO the source buffer. */
136struct BacnetApdu
137{
138 uint8_t pdu_type; ///< PDU type (BACNET_PDU_*)
139 bool segmented; ///< SEG flag (confirmed-request / complex-ack)
140 bool more_follows; ///< MOR flag
141 bool sa; ///< segmented-response-accepted flag (confirmed-request)
142 uint8_t invoke_id; ///< invoke id (confirmed-request / simple-ack / complex-ack)
143 uint8_t service_choice; ///< service choice
144 const uint8_t *service_data; ///< the service parameters after the header, or nullptr if none
145 size_t service_data_len; ///< octets remaining after the header
146};
147
148/**
149 * @brief Decode an APDU header (PDU type, flags, invoke id, service choice) and slice the service data.
150 * @return true iff @p len covers the header for a supported PDU type (confirmed / unconfirmed request,
151 * simple / complex ACK); false for a short buffer or an unsupported type (segment-ack / error /
152 * reject / abort).
153 */
154bool pc_apdu_parse(const uint8_t *apdu, size_t len, BacnetApdu *out);
155
156/**
157 * @brief Build a Who-Is unconfirmed-request APDU (service choice 8). With @p has_limits, the device-instance
158 * search range is appended as context-tagged unsigned ints (tag 0 = low limit, tag 1 = high limit, each
159 * encoded minimal-length); without it, the APDU is the 2-octet unbounded form that every device answers.
160 * @return the APDU length, or 0 on overflow, a limit above BACNET_MAX_INSTANCE, or low > high.
161 */
162size_t pc_apdu_build_who_is(uint8_t *buf, size_t cap, uint32_t low_limit, uint32_t high_limit, bool has_limits);
163
164/**
165 * @brief Build an I-Am unconfirmed-request APDU (service choice 0) - a device's answer to Who-Is. Carries the
166 * device object identifier (@p device_instance, object type Device), the max APDU length accepted, the
167 * segmentation-supported enumeration (0..3), and the vendor id, each as an application-tagged value.
168 * @return the APDU length, or 0 on overflow, @p device_instance above BACNET_MAX_INSTANCE, or @p segmentation > 3.
169 */
170size_t pc_apdu_build_i_am(uint8_t *buf, size_t cap, uint32_t device_instance, uint32_t max_apdu, uint8_t segmentation,
171 uint16_t vendor_id);
172
173/**
174 * @brief Build a ReadProperty confirmed-request APDU (service choice 12) - the BACnet workhorse a client sends to
175 * read one property of one object. Frames the confirmed-request header (unsegmented; @p invoke_id and the
176 * @p max_resp octet - the max-segments-accepted / max-APDU-length-accepted field the peer echoes limits
177 * against), then the object identifier as context tag 0 ((@p object_type << 22) | @p object_instance, a
178 * 4-octet field) and the property identifier as context tag 1 (an enumerated value, minimal-length). The
179 * optional property-array-index (context tag 2) is not emitted - it applies only to array-typed properties.
180 * @return the APDU length, or 0 on overflow, @p object_instance above BACNET_MAX_INSTANCE, or @p object_type > 0x3FF.
181 */
182size_t pc_apdu_build_read_property(uint8_t *buf, size_t cap, uint8_t invoke_id, uint8_t max_resp, uint16_t object_type,
183 uint32_t object_instance, uint32_t property_id);
184
185#endif // PC_ENABLE_BACNET
186
187#endif // PROTOCORE_BACNET_H
User-facing configuration for ProtoCore.