A minimal, copy-paste-friendly example showing how to implement the BACnet B-ACDC (Access Control Door Controller) device profile in C++ using the CAS BACnet Stack. It listens on BACnet/IP (UDP 47808), answers ReadProperty and WriteProperty requests, is discoverable via Who-Is / I-Am, and runs one commandable Access Door the way a door controller does: a client locks, unlocks or pulse-unlocks it, the door re-locks itself when the pulse time runs out, and it reports when the door is forced open or held open too long. The lock relay and door contact it drives are a Simulation; everything BACnet does with them is real.
Download a prebuilt binary (Windows and Linux x64) - or build it yourself, see Build below.
- TUTORIAL.md - how to extend this example and how to review it for conformance. Read it when you start turning this into your own device.
- docs/PICS.md - the Protocol Implementation Conformance Statement: every object, every property, and who answers it.
Versions: this document describes example v1.3.0, built and verified against CAS BACnet Stack 6.0.23 (
6.x@69b6249f), at Protocol_Revision 30, with the vendoredcommon/helper at v3.5.0. Running the example prints all three - if what it prints disagrees with this line, trust the program and checkCHANGELOG.md.
Device 389011 "Chipkin Example B-ACDC" (Vendor 389 - Chipkin Automation Systems)
│
├── Analog Input 1 "Bronze" Present_Value 21.5 (REAL, degrees Celsius; read-only)
├── Binary Input 1 "Emerald" Present_Value active (0 = inactive / 1 = active; read-only)
├── Multi-State Input 1 "Hot Pink" Present_Value 1 (state, 1..3; read-only)
├── Access Door 1 "Cobalt" Present_Value lock (commandable - lock / unlock / pulse; re-locks itself)
└── Network Port 1 "Vermilion" the BACnet/IP port (required on every device)
The three sensor objects are read-only, exactly as in the
B-SS example. The
Access Door is the one object that makes this a B-ACDC: its Present_Value
is not stored directly, but resolved by the stack from a 16-slot
Priority_Array plus a Relinquish_Default of lock - a
WriteProperty(Present_Value, value, priority) sets a slot, NULL relinquishes
it, and the stack reports the highest-priority non-null slot (or
Relinquish_Default) as the effective value. A fire-alarm system can command
the door at a high priority and a badge reader at a low one, and BACnet decides
who wins. Present_Value is a BACnetDoorValue: 0 = lock, 1 = unlock,
2 = pulseUnlock, 3 = extendedPulseUnlock.
What a B-ACDC is for, in one sentence: it locks a door, unlocks it when told
to, and locks it again. This example does that against a Peer - any BACnet
client: an access control panel, a fire-alarm interface, a commissioning tool,
or the Python client in tests/:
- The Peer writes
Present_Value=pulseUnlockat priority 8 (what an access panel sends when someone badges in). The lock relay opens:Lock_Status=unlocked,Secured_Status=unsecured,Current_Command_Priority= 8. - Someone walks through:
Door_Status=opened. - After
Door_Pulse_Time(3.0 s by default) the controller relinquishes priority 8 by itself -Priority_Array[8]reads NULL,Present_Valuefalls back tolock, the relay locks. That is clause 12.26.16 of ANSI/ASHRAE 135-2024: the Present_Value "shall be automatically relinquished at the priority that established the PULSE_UNLOCK command".extendedPulseUnlockdoes the same onDoor_Extended_Pulse_Time. - Failure Mode - held open. If the door is still open
Door_Open_Too_Long_Timeafter it re-locked,Door_Alarm_StatereadsdoorOpenTooLong(12.26.19). Shut it and it returns tonormal. - Failure Mode - forced open. If the door opens while it is commanded
lock- nobody was let in -Door_Alarm_StatereadsforcedOpenuntil it shuts.
A pulse written below a higher-priority command (a fire-alarm unlock at
priority 5, say) is acknowledged and relinquished at once, as 12.26.4 requires;
a pulse above one wins while it runs and then hands the door back to it.
Every one of these steps is printed on the console as it happens.
The door does not send event notifications about its alarms: event
reporting is optional for a B-ACDC, and a door controller that reports access
events is a B-ACC / B-AACC. The alarm is in Door_Alarm_State for anyone who
reads it.
A real door controller has a lock relay (a GPIO output driving the strike or
maglock) and a door-position contact (a reed switch on a GPIO input). This
example has neither, so it simulates them - non-BACnet scaffolding,
outside the BACnet Surface, in section 1b of main.cpp:
- the lock relay follows the commanded
Present_Value(open forunlock/pulseUnlock/extendedPulseUnlock), and is whatLock_Statusreports; - the door contact is moved from the keyboard -
oopens and shuts the door,fforces it open - and is whatDoor_Statusreports; pis a badge reader wired to the controller: a localpulseUnlockat priority 8, the same command an access panel would send over BACnet.
To port the example to hardware, replace the relay and contact variables with
your GPIO and keep everything else. A client can also simulate the door
over BACnet, the standard way: write Out_Of_Service = TRUE and
Door_Status, Lock_Status and Door_Alarm_State become writable, and the
alarm logic responds to written values "as if those changes had occurred to
the physical door" (12.26.9). The Wire Tests use exactly that.
The example implements exactly the capabilities below - and nothing more, which is the point of a profile example.
| BIBB | Description | Supported |
|---|---|---|
| DS-RP-B | Data Sharing - ReadProperty - B | ✅ |
| DS-WP-B | Data Sharing - WriteProperty - B | ✅ |
| DS-ACAD-B | Data Sharing - Access Control Access Door - B (the profile's defining BIBB) | ✅ |
| DM-DDB-B | Device Management - Dynamic Device Binding - B | ✅ |
| DM-DOB-B | Device Management - Dynamic Object Binding - B | ✅ |
Deliberately not included - a B-ACDC does not require them, so a faithful profile example leaves them out: ReadPropertyMultiple, SubscribeCOV, alarms and events, scheduling, trending, and DeviceCommunicationControl. A door controller that also reports access events is a B-ACC / B-AACC - a different profile with its own example.
| Service | Notes |
|---|---|
| ReadProperty | Responds to property reads (DS-RP-B). |
| WriteProperty | Accepts a door command into the Priority_Array, the door's three timing properties, and Out_Of_Service (DS-WP-B). |
| Who-Is / I-Am | Answers Who-Is with I-Am, and broadcasts an I-Am on start-up (DM-DDB-B). |
| Who-Has / I-Have | Answers Who-Has with I-Have (DM-DOB-B). |
| Object type | Instance | Name |
|---|---|---|
| Device | 389011 | Chipkin Example B-ACDC |
| Analog Input | 1 | Bronze |
| Binary Input | 1 | Emerald |
| Multi-State Input | 1 | Hot Pink |
| Access Door | 1 | Cobalt |
| Network Port | 1 | Vermilion |
Every required property of every object, and who answers it, is in docs/PICS.md.
This example builds against the CAS BACnet Stack, which is a commercial Chipkin
product - it is not free or open source, and there is no public/trial build.
The stack is referenced here as the private git submodule
submodules/cas-bacnet-stack; you can only fetch and build it once you have a CAS
BACnet Stack license and access to that repository.
To get the CAS BACnet Stack (and access to build this example), contact Chipkin: https://store.chipkin.com/services/stacks/bacnet-stack or sales@chipkin.com.
You do not need a stack licence to read this example, or to run a prebuilt release binary. The licence is what lets you build it - that is the part the stack submodule gates.
This is a self-contained project. It ships:
main.cpp- the example device.common/- the shared helper (UDP, callbacks, CLI, keyboard) vendored in.CMakeLists.txt- the build, the same on Windows, Linux, and macOS.docs/PICS.md- the conformance statement.tests/- the Wire Tests (Python 3, standard library only), andwiretest/- the series' shared Wiretest Harness they are written against (vendored, likecommon/).submodules/cas-bacnet-stack/- the CAS BACnet Stack as a git submodule (private; requires a license - see above). Its sources are compiled into the executable, so there is no library or DLL to build, ship, or install.
- A C++17 compiler (MSVC, GCC, or Clang).
- CMake >= 3.15.
- Git (to fetch the stack submodule).
- C++ compiler - install Visual Studio Community (free) and select the "Desktop development with C++" workload.
- CMake - from https://cmake.org/download/, or
winget install Kitware.CMake.
- Debian/Ubuntu:
sudo apt install build-essential cmake git - macOS:
xcode-select --installandbrew install cmake
CMake only, and the same two commands on every platform:
git clone --recursive https://github.com/chipkin/BACnetProfileExample-B-ACDC-CPP.git
cd BACnetProfileExample-B-ACDC-CPP
cmake -B build -S .
cmake --build build --config ReleaseAlready cloned without --recursive? Run git submodule update --init --recursive
first - the build needs the stack submodule.
The first build takes a few minutes - it compiles the entire CAS BACnet Stack (~600 source files) into the executable. Rebuilds after that are incremental and take seconds.
If your CAS BACnet Stack lives somewhere other than the bundled submodule, point
CMake at it: cmake -B build -S . -D CAS_STACK_DIR=/path/to/cas-bacnet-stack.
# Linux / macOS
./build/BACnetExampleBACDC
# Windows
.\build\Release\BACnetExampleBACDC.exeExpected output:
BACnet B-ACDC (Access Control Door Controller) Example - C++ v1.3.0
CAS BACnet Stack version: 6.0.23.0
Common helper (common/) version: 3.5.0
FYI: Listening for BACnet/IP on UDP port 47808 (Network Port 1).
TX 21 bytes to 192.168.3.255:47808 (broadcast) (Network Port 1) - Unconfirmed: I-Am
FYI: Device 389011 ("Chipkin Example B-ACDC") ready. Vendor ID 389. Press 'h' for help.
FYI: Access Door 1 (Cobalt) starts lock, door shut. WriteProperty its Present_Value (0=lock, 1=unlock, 2=pulseUnlock, 3=extendedPulseUnlock) to command it.
FYI: the lock relay and door contact are a SIMULATION - keys o / f / p (h for help).
A badge (p) with the door opened (o) and left open prints:
Badge: Access Door 1 (Cobalt) <- pulseUnlock @ priority 8, re-locks in 3.0 s
Access Door 1 (Cobalt): Present_Value lock -> pulseUnlock
Simulation: lock relay energised (door unlocked) -> Lock_Status unlocked
Access Door 1 (Cobalt): Secured_Status unsecured
Simulation: door contact OPEN
Access Door 1 (Cobalt): Door_Status opened
Access Door 1 (Cobalt): pulseUnlock @ priority 8 expired - relinquished (12.26.16)
Access Door 1 (Cobalt): Present_Value pulseUnlock -> lock
Simulation: lock relay released (door locked) -> Lock_Status locked
Access Door 1 (Cobalt): Door_Alarm_State normal -> doorOpenTooLong
The TX line is the start-up I-Am the device broadcasts to announce itself. It
goes to the local subnet broadcast address (here 192.168.3.255, computed
from the Network Port's interface), not the global 255.255.255.255. As clients
talk to the device you'll see RX ... bytes from ... and TX ... bytes to ...
lines showing the traffic.
The device listens on UDP 47808 (BACnet/IP). Allow that port through your
firewall. To use a different port, pass --port (see below).
A wall of red
Error:lines at start-up is expected and is not your bug - it is the stack's own debug logging (the device hearing its own broadcast I-Am, and a one-time BACnet/SC UUID notice). TUTORIAL.md explains both.
| Option | Default | Meaning |
|---|---|---|
--port <n> |
47808 |
UDP port to listen on (BACnet/IP). |
--deviceID <n> |
389011 |
The device's BACnet instance number (BACnet requires this to be configurable). |
--deviceName <text> |
Chipkin Example B-ACDC |
The Device object's Object_Name, 1..255 bytes, no control characters. It must be unique across the BACnet internetwork, so give every unit its own (BACnet requires it to be configurable, like the instance). An invalid value prints a warning and keeps the default. |
--xml |
off | Print every received and sent BACnet message as an indented XML block instead of a one-line summary (very verbose; for protocol debugging). |
--help, -h |
- | Show usage and exit. |
--version |
- | Print the example, stack, and common/ helper versions, then exit. |
While the example runs, these keys are available:
| Key | Action |
|---|---|
h |
Show the version information and this command list. |
q |
Quit. |
| up arrow | Increase Analog Input 1 (Bronze) by 1.1. |
| down arrow | Decrease Analog Input 1 (Bronze) by 1.1. |
o |
Simulation: open / shut the door (the door contact). |
f |
Simulation: force the door open, whatever the lock is doing. |
p |
Simulation: present a badge - pulseUnlock at priority 8. |
Automated, on the wire - tests/ (Python 3 standard
library only). One command starts the door controller and drives it from a
Python client - the Peer:
python3 tests/run_wire_tests.py --exe build/BACnetExampleBACDCThey first discover the device the way a BACnet client does - every
property every object lists must read as a value - then check that pulseUnlock and extendedPulseUnlock are relinquished at
their own priority after their own times (Priority_Array slot NULL,
Present_Value lock, relay locked), priority arbitration between a pulse and
another command in both directions, refusal of a value that is not a
BACnetDoorValue, and both Failure Modes - forced open and held open too long -
plus the Simulation's own keys on Linux/macOS. Each check has been shown to
fail with its feature removed (the table at the end of
tests/README.md). A [KNOWN GAP] line is a stack defect,
listed in TODO.md. CI runs them on Linux after the smoke test.
By hand - with a BACnet client such as the CAS BACnet Explorer:
- Discover - send a Who-Is. The device replies with I-Am from instance 389011 (vendor 389). It also broadcasts an I-Am at start-up.
- Browse the object model - the device shows six objects: the Device
(
Chipkin Example B-ACDC), the three sensors, the Access Door (Cobalt), and the Network Port (Vermilion). Reading the Device'sObject_Listreturns all six. - Read the Access Door - ReadProperty
Access Door 1->Present_Valuereturnslock(nothing has commanded it, so it rests atRelinquish_Default). - Command it - WriteProperty
Access Door 1Present_Value=1(unlock) at priority 8 -> read it back:unlock.Lock_Statusnow readsunlockedandSecured_Statusreadsunsecured. - Confirm command arbitration - WriteProperty
Present_Value=0(lock) at priority 1 -> the door readslock: priority 1 outranks the priority-8 unlock, even though the unlock came later. - Relinquish - WriteProperty
Present_Value=NULLat priority 1 -> the door returns tounlock(slot 8 is still commanded). WritePropertyNULLat priority 8 too -> every slot is now null, so the door falls back toRelinquish_Default=lock. - Confirm validation - WriteProperty
Present_Value=9-> refused. (The stack at this pin answers Reject, not thevalue-out-of-rangeError the standard names - see TODO.md.) - Pulse - WriteProperty
Present_Value=2(pulseUnlock) at priority 8, readPresent_Valueat once (pulseUnlock) and again afterDoor_Pulse_Time(3.0 s):lock,Priority_Array[8]NULL. - The alarms - press
owith the door locked:Door_Alarm_StatereadsforcedOpen;oagain to shut it. Or badge (p), open (o) and waitDoor_Open_Too_Long_Time(30 s) past the re-lock:doorOpenTooLong.
For a property-by-property review against the conformance statement, see TUTORIAL.md.
There is one example repository per standardized device profile in ASHRAE 135-2024 Annex L. An example marked ⛔ is blocked: the stack cannot yet perform that profile's function through its customer interface, and the example's README says what it does today. Pick the profile your device claims, then the language you build in. "Ask" means the example hasn't been built yet for that language - contact Chipkin if you need one.
| Profile | C++ | Node.js | C# | Rust | Python | Go |
|---|---|---|---|---|---|---|
| B-SS Smart Sensor | B-SS-CPP | B-SS-Node | B-SS-CS | B-SS-Rust | B-SS-Python | B-SS-Go |
| B-SA Smart Actuator | B-SA-CPP | Ask | Ask | Ask | Ask | Ask |
| B-ASC Application Specific Controller | B-ASC-CPP | B-ASC-Node | Ask | Ask | Ask | Ask |
| B-AAC Advanced Application Controller | B-AAC-CPP | Ask | Ask | Ask | Ask | Ask |
| B-BC Building Controller | B-BC-CPP | Ask | Ask | Ask | Ask | Ask |
| Profile | C++ | Node.js | C# | Rust | Python | Go |
|---|---|---|---|---|---|---|
| B-LSC Life Safety Controller | B-LSC-CPP 🚧 | Ask | Ask | Ask | Ask | Ask |
| B-ALSC Advanced Life Safety Controller | B-ALSC-CPP | Ask | Ask | Ask | Ask | Ask |
| Profile | C++ | Node.js | C# | Rust | Python | Go |
|---|---|---|---|---|---|---|
| B-ACC Access Control Controller | B-ACC-CPP | Ask | Ask | Ask | Ask | Ask |
| B-AACC Advanced Access Control Controller | B-AACC-CPP | Ask | Ask | Ask | Ask | Ask |
| Profile | C++ | Node.js | C# | Rust | Python | Go |
|---|---|---|---|---|---|---|
| B-LD Lighting Device | B-LD-CPP | Ask | Ask | Ask | Ask | Ask |
| B-LS Lighting Supervisor | B-LS-CPP | Ask | Ask | Ask | Ask | Ask |
| Profile | C++ | Node.js | C# | Rust | Python | Go |
|---|---|---|---|---|---|---|
| B-EM Elevator Monitor | B-EM-CPP | Ask | Ask | Ask | Ask | Ask |
| B-EC Elevator Controller | B-EC-CPP | Ask | Ask | Ask | Ask | Ask |
| B-AEC Advanced Elevator Controller | B-AEC-CPP | Ask | Ask | Ask | Ask | Ask |
| Profile | C++ | Node.js | C# | Rust | Python | Go |
|---|---|---|---|---|---|---|
| B-AS Authorization Server | B-AS-CPP | Ask | Ask | Ask | Ask | Ask |
| Profile | C++ | Node.js | C# | Rust | Python | Go |
|---|---|---|---|---|---|---|
| B-BBMD Broadcast Management Device | B-BBMD-CPP | Ask | Ask | Ask | Ask | Ask |
| B-ACDC Access Control Door Controller | B-ACDC-CPP | Ask | Ask | Ask | Ask | Ask |
| B-ACCR Access Control Credential Reader | B-ACCR-CPP ⛔ blocked on stack #3934 | Ask | Ask | Ask | Ask | Ask |
| B-RTR Router | B-RTR-CPP | Ask | Ask | Ask | Ask | Ask |
| B-GW Gateway | B-GW-CPP | Ask | Ask | Ask | Ask | Ask |
| B-DAP Device Address Proxy | B-DAP-CPP ⛔ blocked on stack #2032 | Ask | Ask | Ask | Ask | Ask |
| B-SCHUB BACnet/SC Hub | B-SCHUB-CPP | Ask | Ask | Ask | Ask | Ask |
| B-GENERAL General device (Annex L.8) | (satisfied by every example above) | — | — | — | — | — |
Client-side profiles.
| Profile | C++ | Node.js | C# | Rust | Python | Go |
|---|---|---|---|---|---|---|
| B-OD Operator Display | B-OD-CPP | Ask | Ask | Ask | Ask | Ask |
| B-OWS Operator Workstation | planned | — | — | — | — | — |
| B-AWS Advanced Operator Workstation | planned | — | — | — | — | — |
| B-XAWS Extended Advanced Operator Workstation | planned | — | — | — | — | — |
| B-LSAP Life Safety Annunciator Panel | planned | — | — | — | — | — |
| B-LSWS Life Safety Workstation | planned | — | — | — | — | — |
| B-ALSWS Advanced Life Safety Workstation | planned | — | — | — | — | — |
| B-ACSD Access Control Security Display | planned | — | — | — | — | — |
| B-ACWS Access Control Workstation | planned | — | — | — | — | — |
| B-AACWS Advanced Access Control Workstation | planned | — | — | — | — | — |
| B-LOD Lighting Operator Display | planned | — | — | — | — | — |
| B-ALWS Advanced Lighting Workstation | planned | — | — | — | — | — |
| B-LCS Lighting Control Station | planned | — | — | — | — | — |
| B-ALCS Advanced Lighting Control Station | planned | — | — | — | — | — |
| B-ED Elevator Display | planned | — | — | — | — | — |
| B-EWS Elevator Workstation | planned | — | — | — | — | — |
| B-AEWS Advanced Elevator Workstation | planned | — | — | — | — | — |
🚧 = in progress. ⛔ = Blocked Example (blocked on the named stack gap). "Ask" = not yet built for that language; contact Chipkin if you need it. Profile definitions: ANSI/ASHRAE 135-2024 Annex L. BIBB definitions: Annex K. Get the stack: https://store.chipkin.com/services/stacks/bacnet-stack.
- ANSI/ASHRAE Standard 135 (BACnet) - the protocol standard. Object model (Clause 12), services (Clause 15), BACnet/IP (Annex J), device profiles (Annex L). Purchase / preview via the ASHRAE store.
- What is BACnet? - Chipkin's introduction: https://docs.chipkin.com/protocols/bacnet/.
- CAS BACnet Stack - product page and documentation: https://store.chipkin.com/services/stacks/bacnet-stack.
- CAS BACnet Explorer - client for testing this device: https://store.chipkin.com/products/tools/cas-bacnet-explorer.
- Shared helper used by this example -
common/README.md.
See also TUTORIAL.md, docs/PICS.md, CHANGELOG.md, and AGENTS.md.