Skip to content

About

No description or website provided.

Topics

Resources

Stars

1 star

Watchers

0 watching

Forks

Latest commit

Β 

History

37 Commits

Folders and files

Repository files navigation

BACnet B-ASC (Application Specific Controller) - C++ example

A minimal, copy-paste-friendly example showing how to implement the BACnet B-ASC (BACnet Application Specific Controller) device profile in C++ using the CAS BACnet Stack. It listens on BACnet/IP (UDP 47808), answers ReadProperty, accepts WriteProperty to its commandable outputs, responds to DeviceCommunicationControl, and is discoverable via Who-Is / I-Am. It announces itself with an I-Am heartbeat every 5 s - the message a management station's DeviceCommunicationControl visibly silences and lets resume. Each output does what the winning command says (a heating valve, a relay and a drive, each reporting back through an input), and a disconnected valve shows up as a fault. The valve, relay and drive 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.5.0, built and verified against CAS BACnet Stack 6.0.23 (6.x @ 69b6249f), at Protocol_Revision 30, with the vendored common/ helper at v3.5.0. Running the example prints all three - if what it prints disagrees with this line, trust the program and check CHANGELOG.md.

Protocol_Revision is the revision of the ASHRAE 135 standard a device claims to conform to. It is a number every BACnet device advertises, and it changes real behaviour: at revision 20 and above, for example, the deprecated plain disable form of DeviceCommunicationControl must be rejected (see DeviceCommunicationControl below).

DeviceCommunicationControl (the B-ASC addition)

DeviceCommunicationControl lets a management station tell a device to go quiet - useful to silence a misbehaving or noisy device during commissioning - and later to resume. The request carries:

  • an enable/disable choice,
  • an optional time duration (minutes) after which the device re-enables on its own, and
  • an optional password.

The CAS BACnet Stack runs the actual enable/disable state machine and the re-enable timer; this example's callback (DeviceCommunicationControl in main.cpp) just validates the password and logs what was asked.

Concretely, the callback:

  1. Rejects a request for any device instance other than this one, naming optional-functionality-not-supported on the wire.
  2. Compares the supplied password to the configured one (g_dccPassword, set with --dcc-password) by length first, then bytes - never strcmp, because a BACnet CharacterString may legitimately contain an embedded NUL that strcmp would silently stop at. A device with no configured password (the shipped default) accepts any request. A mismatch is rejected with *errorCode = ERROR_CODE_PASSWORD_FAILURE, which the stack pairs with Error Class = SECURITY (clause 16.1.1.3.1).
  3. On a correct (or absent) password, accepts and logs the requested action - enable, the deprecated disable, or disable-initiation - and the optional time duration.
  4. Sets *errorCode on every false return, with no exception. Unlike the SetProperty* callbacks (which have a sensible writeAccessDenied fallback), DeviceCommunicationControl has no default: the stack presets *errorCode to success (84) and, on false, answers Error Class = SECURITY when the code is password-failure or Error Class = SERVICES otherwise. Returning false without setting *errorCode puts the literal nonsense "Error Code = success(84)" on the wire - main.cpp never does that.

Protocol_Revision >= 20 note: the plain disable value (stop initiating and responding) is deprecated. At Protocol_Revision 30 the stack rejects it with service-request-denied, as 135-2024 clause 16.1.2 requires ("If the request is valid and the 'Enable/Disable' parameter is the deprecated value DISABLE, the request shall be ignored..."). The modern choice is disable-initiation. So only enable and disable-initiation take effect. The callback still logs what the client asked for on the deprecated path, because the rejection happens in the stack after the callback returns.

What you can observe. Clause 16.1.2 stops a device initiating messages other than I-Am and I-Have sent in answer to Who-Is and Who-Has - so disable-initiation is only visible on a device that initiates something. This one does: an unsolicited I-Am heartbeat every 5 s (HeartbeatTick(), section 2d of main.cpp; I-Am is the service DM-DDB-B has the device initiate, so the BACnet Surface does not grow). After a disable-initiation the heartbeat stops - the stack refuses to send it - while ReadProperty, WriteProperty and Who-Is are still answered. After an enable, or when the request's time duration runs out, it resumes. The example keeps no communication state of its own: it asks for a heartbeat every 5 s and the stack decides. A product would announce far less often (minutes, or not at all); 5 s keeps the demonstration watchable.

The example ships with no password (accept any request). Start it with --dcc-password <string> to require one; a missing or wrong password is then rejected with Error Class = SECURITY, Error Code = password-failure. In your product, set the password from your own configuration.

The device this example creates

Device 389003  "Chipkin Example B-ASC"   (Vendor 389 - Chipkin Automation Systems)
    β”‚
    β”œβ”€β”€ Analog Input  1       "Bronze"      Present_Value  21.5 -> follows the setpoint  (REAL, degrees Celsius; read-only)
    β”œβ”€β”€ Binary Input  1       "Emerald"     Present_Value  follows Binary Output 1      (0 = inactive / 1 = active; read-only)
    β”œβ”€β”€ Multi-State Input 1   "Hot Pink"    Present_Value  follows Multi-State Output 1 (state, 1..3; read-only)
    β”œβ”€β”€ Analog Output 1       "Chartreuse"  Present_Value  20.0    (REAL setpoint, 10.0 .. 30.0 C; WRITABLE, commandable)
    β”œβ”€β”€ Binary Output 1       "Fuchsia"     Present_Value  inactive  (0/1 relay; WRITABLE, commandable)
    β”œβ”€β”€ Multi-State Output 1  "Indigo"      Present_Value  1       (speed, 1..3; WRITABLE, commandable)
    └── Network Port 1        "Vermilion"   the BACnet/IP port     (required on every device)

The three input objects (Bronze, Emerald, Hot Pink) are the shared minimum every example in this series carries; here each is the feedback of one output. The three output objects (Chartreuse, Fuchsia, Indigo) are commandable via a Priority_Array. Object names follow this series' colour-naming convention (Device is always "Chipkin Example B-ASC").

The scenario: a controller a management station can silence

What a B-ASC is for, from Annex L.4.3 of ANSI/ASHRAE 135-2024: a B-SA's "Ability to allow modification of some or all of its BACnet objects by another device", plus "Ability to respond to communication control messages". Against a Peer - a controller and a management station (any BACnet client, or the Python client in tests/):

  1. Commanded. The Peer writes Analog Output 1 = 24.0 at priority 8: the temperature (Analog Input 1) climbs to 24.0. A write at a lower priority changes nothing; relinquishing hands the output to the next command, then to Relinquish_Default (20.0). Binary and Multi-State Output 1 work the same way, with Binary Input 1 and Multi-State Input 1 as their feedback - exactly as in the B-SA example.
  2. Silenced. The management station sends DeviceCommunicationControl disable-initiation with the password: the I-Am heartbeat stops. The controller still answers ReadProperty, still obeys WriteProperty, and still answers a Who-Is with an I-Am.
  3. Resumed. enable brings the heartbeat back. A disable-initiation for a time duration (1 minute, say) ends by itself: the heartbeat resumes when the minute is up, with no enable sent.
  4. Failure Mode - a request it must refuse. A wrong or missing password gets Error security / password-failure; the deprecated plain disable gets Error services / service-request-denied (clause 16.1.1.3.1). Neither silences anything. Out-of-range writes get value-out-of-range; a write to an input gets write-access-denied.
  5. Failure Mode - a disconnected actuator. Analog Output 1 Reliability reads no-output and Status_Flags FAULT is TRUE; commands are still accepted, but the temperature does not move until the valve is reconnected.

The actuator Simulation

The same as the B-SA example's: non-BACnet scaffolding, outside the BACnet Surface, in section 1b of main.cpp (ActuatorSimulationTick(), called beside BACnetStack_Tick()). Once a second the heating valve moves the temperature (Analog Input 1) up to 1.0 C toward the winning Analog Output 1 setpoint, the relay's feedback contact (Binary Input 1) takes the winning Binary Output 1 command, and the drive's speed (Multi-State Input 1) takes the winning Multi-State Output 1 command. "Winning" is ResolvedCommand(), the same answer the stack gives for Present_Value (clause 19.2).

The disconnected valve is a test-only hook, not a feature: if the environment variable CAS_EXAMPLE_TEST_ACTUATOR_FAULT_FILE names a file, the valve actuator counts as disconnected for as long as that file exists. Nothing on the network can disconnect it. The I-Am heartbeat is not Simulation: it is a real BACnet message, and the one this example initiates.

What this example supports

The example implements exactly the capabilities below - and nothing more, which is the point of a profile example. These capabilities satisfy the B-ASC (Application Specific Controller) profile; because B-ASC's BIBBs are a superset of the B-GENERAL baseline, a conformant B-ASC device necessarily satisfies B-GENERAL too. That is subsumption, not a second claim: this repository still claims exactly one profile.

BIBBs (BACnet Interoperability Building Blocks)

BIBB Description Supported
DS-RP-B Data Sharing - ReadProperty - B βœ…
DS-WP-B Data Sharing - WriteProperty - B βœ…
DM-DCC-B Device Management - DeviceCommunicationControl - B βœ…
DM-DDB-B Device Management - Dynamic Device Binding - B βœ…
DM-DOB-B Device Management - Dynamic Object Binding - B βœ…

Services (executed / B-side)

Service Notes
ReadProperty Responds to property reads (DS-RP-B).
WriteProperty Accepts writes to the commandable outputs' Present_Value (DS-WP-B).
DeviceCommunicationControl Stops/resumes communication, optionally timed/passworded (DM-DCC-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 types

Object type Instance Name Access
Device 389003 Chipkin Example B-ASC -
Analog Input 1 Bronze read-only
Binary Input 1 Emerald read-only
Multi-State Input 1 Hot Pink read-only
Analog Output 1 Chartreuse writable (commandable)
Binary Output 1 Fuchsia writable (commandable)
Multi-State Output 1 Indigo writable (commandable)
Network Port 1 Vermilion -

Every required property of every object, and who answers it, is in docs/PICS.md.

Requires the CAS BACnet Stack (licensed product)

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.

What's in this repository

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 (see Wire Tests), and wiretest/, the series' shared Python harness they are written against (vendored, like common/).
  • 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.

Prerequisites

  • A C++17 compiler (MSVC, GCC, or Clang).
  • CMake >= 3.15.
  • Git (to fetch the stack submodule).

Windows

Linux / macOS

  • Debian/Ubuntu: sudo apt install build-essential cmake git
  • macOS: xcode-select --install and brew install cmake

Build

CMake only, and the same two commands on every platform:

git clone --recursive https://github.com/chipkin/BACnetProfileExample-B-ASC-CPP.git
cd BACnetProfileExample-B-ASC-CPP

cmake -B build -S .
cmake --build build --config Release

Already 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.

Run

# Linux / macOS
./build/BACnetExampleBASC

# Windows
.\build\Release\BACnetExampleBASC.exe

Expected output:

BACnet B-ASC (Application Specific Controller) Example - C++ v1.5.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 389003 ("Chipkin Example B-ASC") ready. Vendor ID 389. Press 'h' for help.

The TX line is the start-up I-Am the device broadcasts to announce itself; the same line appears every 5 s after that, for the heartbeat (and stops while a disable-initiation is in force). 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; a WriteProperty to an output prints a line such as WriteProperty: Analog Output 1 (Chartreuse) <- 24.00 @ priority 8, and a DeviceCommunicationControl prints e.g. DeviceCommunicationControl: disable-initiation (keep responding) (indefinitely).

The device listens on UDP 47808 (BACnet/IP). Allow that port through your firewall. To use a different port, pass --port (see below).

A few red Error: lines at start-up are expected and are not your bug - it is the stack's own debug logging of the device hearing its own broadcast I-Am (on a quiet network there may be none). TUTORIAL.md explains it.

Command-line options

Option Default Meaning
--port <n> 47808 UDP port to listen on (BACnet/IP).
--deviceID <n> 389003 The device's BACnet instance number (BACnet requires this to be configurable).
--deviceName <text> Chipkin Example B-ASC 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.
--dcc-password <string> none Require this password on DeviceCommunicationControl. Prints FYI: DeviceCommunicationControl requires a password at start-up (never the password).
--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.

Interactive commands

While the example runs, these keys are available:

Key Action
h Show the version information and this command list.
q Quit.
up arrow Disturb the simulated temperature (Analog Input 1, Bronze) up by 1.1.
down arrow Disturb the simulated temperature (Analog Input 1, Bronze) down by 1.1.

The up/down keys push the simulated temperature off its setpoint; within a few seconds the valve brings it back, and a client re-reading Analog Input 1 sees both.

Verify

Use a BACnet client such as the CAS BACnet Explorer:

  1. Discover - send a Who-Is. The device replies with I-Am from instance 389003 (vendor 389). It also broadcasts an I-Am at start-up.
  2. Browse the object model - the device shows eight objects: the Device (Chipkin Example B-ASC), three inputs, three outputs, and the Network Port (Vermilion). Reading the Device's Object_List returns all eight.
  3. Read the Device - ReadProperty 389003 -> Object_Name returns "Chipkin Example B-ASC"; Protocol_Revision returns 30; Description returns the profile description string.
  4. Read a sensor - ReadProperty Analog Input 1 -> Present_Value returns 20.0 once the valve has settled at the default setpoint (it starts at 21.5); Units returns degrees-Celsius. Repeat for Binary Input 1 ("Emerald") and Multi-State Input 1 ("Hot Pink").
  5. Command an output - WriteProperty Analog Output 1 Present_Value = 24.0 at priority 8; re-read Present_Value (24.0) and Priority_Array[8] (24.0), and watch Analog Input 1 climb to 24.0; write NULL at priority 8 to relinquish; Present_Value returns to 20.0 (its Relinquish_Default), and so does Analog Input 1. Repeat for Binary/Multi-State Output (watch Binary / Multi-State Input 1). A write to a read-only input, or an out-of-range value, is rejected.
  6. Communication control (the B-ASC test) - watch the console (or a capture of the subnet broadcast): a TX ... (broadcast) ... I-Am line every 5 s. Send a DeviceCommunicationControl with disable-initiation: the device SimpleACKs, the heartbeat lines stop, and it keeps answering reads and writes. Send enable to resume. Sending the deprecated plain disable returns service-request-denied at Protocol_Revision 30 - that is correct. Restart the example with --dcc-password s3cret: a request with no password or the wrong one is rejected with security / password-failure, and one with s3cret is SimpleACKed.

For a property-by-property review against the conformance statement, see TUTORIAL.md.

Wire Tests

The checks above, automated: a Python BACnet client (the Peer) drives the running example over BACnet/IP from outside it. Python 3 standard library only.

Script What it proves
discovery_test.py Who-Is, the Device's Object_List, then for every object its Property_List and a ReadProperty of every property it lists - every read comes back as a value.
configuration_test.py The Device's identity is configurable at start-up: with --deviceID 389903 --deviceName "Chipkin Example B-ASC Unit 2", Who-Is, ReadProperty Object_Name and Who-Has by name all see the configured instance and name (and 389003 is gone); without them, the defaults; an empty or 256-byte name keeps the default with a warning, a 255-byte one is kept whole. --help lists exactly the options the example reads, and --xml prints XML.
commanding_test.py Commands each output at two priorities; Present_Value, Priority_Array and Current_Command_Priority follow the winning command, and the feedback input follows it too (the B-SA Profile Function, which a B-ASC includes).
failure_modes_test.py A disconnected valve (no-output, FAULT, command accepted but not obeyed), out-of-range writes, a write to an input.
dcc_test.py The B-ASC Profile Function. With --dcc-password: the heartbeat runs; wrong / missing password and deprecated disable are refused and it keeps running; disable-initiation stops it while ReadProperty, WriteProperty and Who-Is are still answered; enable resumes it; a 1-minute disable-initiation ends by itself. About 2.5 minutes.

The heartbeat is a subnet broadcast. On Linux/macOS dcc_test.py hears it on the wire; on Windows a second socket cannot share the example's port, so there it counts the example's own TX ... (broadcast) ... I-Am console lines (on Linux it checks both). Run them all:

python3 tests/run_wire_tests.py --exe build/BACnetExampleBASC          # Linux/macOS
python tests\run_wire_tests.py --exe build\Release\BACnetExampleBASC.exe   # Windows

It ends with ALL WIRE TESTS PASSED (exit 0). CI runs it on Linux after the smoke test. tests/README.md says what each check proves.

The BACnet profile example series

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.

Controllers (Annex L.4)

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

Life safety controllers (Annex L.5)

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

Access control controllers (Annex L.6)

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

Lighting controllers (Annex L.11)

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

Elevator controllers (Annex L.13)

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

Authentication and authorization (Annex L.14)

Profile C++ Node.js C# Rust Python Go
B-AS Authorization Server B-AS-CPP β›” blocked on stack #2043 Ask Ask Ask Ask Ask

Miscellaneous (Annex L.7, combinable with any one family)

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) β€” β€” β€” β€” β€”

Operator interfaces and workstations (Annex L.1–L.3, L.9–L.10, L.12)

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.

References

See also TUTORIAL.md, docs/PICS.md, CHANGELOG.md, and AGENTS.md.

About

No description or website provided.

Topics

Resources

Stars

1 star

Watchers

0 watching

Forks

Releases

Packages

Contributors

Languages