A minimal, copy-paste-friendly example showing how to implement the BACnet B-SS (Smart Sensor) device profile in Go, using the CAS BACnet Stack through cgo. It listens on BACnet/IP (UDP 47808), answers ReadProperty, and is discoverable via Who-Is / I-Am. Its readings follow a sensor Simulation (labelled below), and it reports a broken sensor the BACnet way.
This is a faithful Go port of the reworked BACnetProfileExample-B-SS-CPP (v1.3.0): the same Device, objects, properties, Simulation, Failure Modes, test-only hook and command-line options, proven by the same Wire Tests, which cannot tell the two apart on the wire.
- 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.1.0, built and verified against CAS BACnet Stack 6.0.23 (
6.x@64b2cfac5, theReleaseDllshared library built from the submodule, its header included in place by cgo), at Protocol_Revision 30, with the Gocommon/helper at v1.1.0 andwiretest/at 0.7.0. Running the example prints all three versions - if what it prints disagrees with this line, trust the program and checkCHANGELOG.md.
B-SS (BACnet Smart Sensor), defined in Annex L of ANSI/ASHRAE 135, is the simplest BACnet device profile - the standard describes it as "a simple sensing device with very limited resources." It is meant for inexpensive, fixed-function sensors (temperature, humidity, occupancy, a contact, ...) that mostly just need to report what they measure when asked.
A B-SS device answers ReadProperty and is discoverable. It does not have to support WriteProperty (a smart sensor is read-only), alarming / event reporting, scheduling, or trending, and this example implements none of them on purpose.
But it is still a full BACnet device. Even the simplest profile must present the standard object model - a Device object, a Network Port object (every device needs one), and its sensor objects - and each object must expose all of its required properties. The CAS BACnet Stack generates most of those automatically (Object_Identifier, Object_Type, Status_Flags, Object_List, Protocol_*, ...); this example supplies the handful that are application-specific. The result is conformant for Protocol_Revision 30. docs/PICS.md lists every property and who answers it.
Device 389001 "Chipkin Example B-SS" (Vendor 389 - Chipkin Automation Systems)
β
βββ Analog Input 1 "Bronze" Present_Value 21.5 -> ramps 18.0 .. 26.0 (REAL, degrees Celsius)
βββ Binary Input 1 "Emerald" Present_Value toggles every 3 s (0 = inactive / 1 = active)
βββ Multi-State Input 1 "Hot Pink" Present_Value 1 -> 2 -> 3 every 4 s (state, 1..3)
βββ Network Port 1 "Vermilion" the BACnet/IP port (required on every device)
What a B-SS is for, in one sentence, from Annex L.4.5 of ANSI/ASHRAE 135-2024:
"Ability to provide values for any of its BACnet objects upon request." A
controller or workstation (the Peer - any BACnet client, or the Python
client in tests/) reads the sensor and gets what it is
measuring now:
- The Peer finds the sensor with Who-Is and reads its
Object_List. - It reads Analog Input 1
Present_Value- say22.0- and again a few seconds later -23.5. The reading follows the (simulated) room temperature, 0.5 C a second. Binary Input 1 and Multi-State Input 1 change too. - Failure Mode - a broken sensor. When the sensor wire breaks, Analog
Input 1
Reliabilityreadsopen-loop(4) and the FAULT bit ofStatus_Flagsis TRUE (clause 12.2.7 derives FAULT from Reliability). The reading stops changing. Repair the wire andReliabilityreturns tono-fault-detected, FAULT clears, and the reading moves again. - Failure Mode - a request it cannot serve. Reading an object or property
the sensor does not have, or an array element past the end, gets the error
clause 15.5.1.3.1 names (
unknown-object,unknown-property,invalid-array-index); a WriteProperty gets a Rejectunrecognized-service, because a B-SS is read-only.
A real smart sensor samples a thermistor, a contact and a selector switch. This
example has none of them, so it simulates them - non-BACnet scaffolding,
outside the BACnet Surface, in section 2 of main.go
(sensorSimulationTick(), called beside BACnetStack_Tick()). Once a second:
- the temperature (Analog Input 1) moves 0.5 C, ramping up to 26.0 and back down to 18.0;
- the contact (Binary Input 1) toggles every 3 s;
- the selector (Multi-State Input 1) steps 1 -> 2 -> 3 -> 1 every 4 s.
The broken wire is a test-only hook, not a feature: if the environment
variable CAS_EXAMPLE_TEST_SENSOR_FAULT_FILE names a file, the wire counts as
broken for as long as that file exists. The Wire Tests create and delete it.
Nothing on the network can break the sensor.
To port the example to hardware, replace sensorSimulationTick() with your own
sampling (on a timer or another goroutine - never block the BACnet callbacks on
slow I/O), and set Reliability from your hardware's own diagnostics.
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 | β |
| DM-DDB-B | Device Management - Dynamic Device Binding - B | β |
| DM-DOB-B | Device Management - Dynamic Object Binding - B | β |
| Service | Notes |
|---|---|
| ReadProperty | Responds to property reads (DS-RP-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 | 389001 | Chipkin Example B-SS |
| Analog Input | 1 | Bronze |
| Binary Input | 1 | Emerald |
| Multi-state Input | 1 | Hot Pink |
| 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 (tracking the 6.x branch). The Go
edition loads the stack at run time as a shared library
(CASBACnetStack_x64_Release.dll on Windows / libCASBACnetStack_x64_Release.so
on Linux) that you build from that submodule - this repository and its releases
never contain it. 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.
This is a self-contained project. It ships:
main.go- the example device (section 2 is the sensor Simulation);property_dispatch.go- its section 3, theGet*Propertycallbacks.cgobridge/- the only package withimport "C": thin Go wrappers for theBACnetStack_*calls, the BACnet/IP transport callbacks, and the callback trampolines (see How the Go binding works).common/- the cgo-free Go helper (UDP socket, IPv4, command line, device state, BACnet constants).tests/- the Wire Tests (tests/README.md), copied unchanged from the C++ edition;wiretest/- the series' shared Wiretest Harness 0.7.0, vendored verbatim.docs/PICS.md- the conformance statement;docs/objects.json- the input to its objects-and-properties generator.submodules/cas-bacnet-stack/- the CAS BACnet Stack as a git submodule (private; requires a license - see above). cgo includes itssource/CASBACnetStackDLL.hin place; its MSVC / g++ build produces the shared library, in its ownbin/folder.
- Go 1.20 or later (verified with go1.21.6), with cgo
(
CGO_ENABLED=1). - A C compiler for cgo. On Windows that is MinGW-w64 gcc (for example
from MSYS2, or the one bundled with Strawberry Perl - verified with
x86_64-w64-mingw32gcc 13.2.0), separate from the MSVC toolchain that builds the stack's DLL. On Linux, gcc. - To build the stack's shared library: on Windows, Visual Studio Build Tools (MSBuild, C++); on Linux, a C++17 g++.
- Python 3 (standard library only) to run the Wire Tests.
- Git (to fetch the stack submodule).
git clone --recursive https://github.com/chipkin/BACnetProfileExample-B-SS-Go.git
cd BACnetProfileExample-B-SS-GoAlready cloned without --recursive? Run git submodule update --init --recursive.
The library goes into the submodule's own bin/ folder, where the example
finds it at run time when it is run from this checkout. Do not commit it to this
repository (it is gitignored, and never part of a release).
Windows - the stack's MSVC project has a ReleaseDll|x64 configuration
that builds exactly this DLL, under exactly this name (a customer-interface
build: no test-tool exports):
& "C:\Program Files (x86)\Microsoft Visual Studio\2022\BuildTools\MSBuild\Current\Bin\amd64\MSBuild.exe" `
submodules\cas-bacnet-stack\projects\msvs\CASBACnetStack\CASBACnetStack.vcxproj `
-p:Configuration=ReleaseDll -p:Platform=x64 -m
# -> submodules\cas-bacnet-stack\bin\CASBACnetStack_x64_Release.dll(Adjust the MSBuild path to your Visual Studio / Build Tools install.)
Linux - build the shared library straight from the stack's sources:
mkdir -p submodules/cas-bacnet-stack/bin
g++ -shared -fPIC -std=c++17 -O2 \
-I submodules/cas-bacnet-stack/source \
submodules/cas-bacnet-stack/source/*.cpp \
-o submodules/cas-bacnet-stack/bin/libCASBACnetStack_x64_Release.soOnly the Release library is used: the adapter loads CASBACnetStack_x64_Release
(BACNET_LIB_FILE), and there is no DebugDll configuration.
CGO_ENABLED=1 go build -o build/BACnetExampleBSS . # Linux$env:CGO_ENABLED = 1; go build -o build\BACnetExampleBSS.exe . # Windows (gcc on PATH)build/BACnetExampleBSS is the name the Wire Tests look for by default (the
C++ edition's name). go vet . ./cgobridge ./common should be clean; avoid
./..., which walks into the submodule's own Go files.
The executable loads the stack library at run time (not the operating system at start-up), from the first of these folders that has it:
- the run directory - the folder that holds the executable;
- in a development checkout,
submodules/cas-bacnet-stack/bin/(found by looking upwards from the executable's folder) - sobuild/BACnetExampleBSSin this checkout needs no set-up.
A release of this example ships the executable without the stack library (it is a commercially licensed product). Run on its own, it stops with this message on stderr and exit code 1:
ERROR: This example cannot run without the CAS BACnet Stack library.
Missing file: CASBACnetStack_x64_Release.dll
Looked in: C:\BACnetExampleBSS
The CAS BACnet Stack is a commercially licensed (paid) library from Chipkin.
It is not included in this release, so the example cannot start without it.
- To license the CAS BACnet Stack: https://store.chipkin.com/contact-us
- Licensed customers: build the library from the cas-bacnet-stack submodule
(see README.md) and place CASBACnetStack_x64_Release.dll next to this executable.
Licensed customers build the library (see Build) and put
CASBACnetStack_x64_Release.dll (Windows) / libCASBACnetStack_x64_Release.so
(Linux) next to the executable. --help and --version work without it.
./build/BACnetExampleBSS --port 47821 --deviceID 12345Expected output:
BACnet B-SS (Smart Sensor) Example - Go v1.1.0
CAS BACnet Stack version: 6.0.23.0
Common helper (common/) version: 1.1.0
FYI: Listening for BACnet/IP on UDP port 47821 (Network Port 1).
TX 21 bytes to <your-subnet-broadcast>:47821 (broadcast) (Network Port 1) - Unconfirmed: I-Am
FYI: Device 12345 ("Chipkin Example B-SS") ready. Vendor ID 389. Press 'h' then Enter for help.
The device listens on UDP 47808 (BACnet/IP) by default. Allow that port
through your firewall. To use a different port, pass --port.
| Option | Default | Meaning |
|---|---|---|
--port <n> |
47808 |
UDP port to listen on (BACnet/IP). |
--deviceID <n> |
389001 |
The device's BACnet instance number (BACnet requires this to be configurable). |
--help, -h |
- | Show usage and exit. |
--version |
- | Print the example, stack and common/ versions, then exit. |
The console stays in its normal line mode, so press the key and then Enter (the C++ edition reads single raw key presses - Go's standard library has no portable way to; see TUTORIAL.md):
| Key | Action |
|---|---|
h |
Show the version information and this command list. |
q |
Quit. |
u |
Nudge the simulated Analog Input 1 (Bronze) reading up by 1.1 (the C++ edition's up arrow). |
d |
Nudge it down by 1.1 (the C++ edition's down arrow). |
The Simulation carries on ramping from the nudged value, and a client
re-reading it sees the change. Ctrl+C also shuts the device down cleanly. With
stdin a pipe (as the Wire Tests run it), a q written to it quits at once.
Use a BACnet client such as the CAS BACnet Explorer:
- Discover - send a Who-Is. The device replies with I-Am from instance 389001 (vendor 389). It also broadcasts an I-Am at start-up.
- Browse the object model - the device shows five objects: the Device
(
Chipkin Example B-SS), the three sensors, and the Network Port (Vermilion). Reading the Device'sObject_Listreturns all five. - Read the Device - ReadProperty
389001->Object_Namereturns"Chipkin Example B-SS";Protocol_Revisionreturns30;Descriptionreturns the profile description string. - Read a sensor - ReadProperty Analog Input
1->Present_Valuereturns the current simulated temperature (21.5at start-up, then 0.5 higher each second); read it again and it has moved.Unitsreturnsdegrees-Celsius;Reliabilityreturnsno-fault-detected;Status_Flagsreturns all FALSE;Out_Of_Servicereturnsfalse;Object_Namereturns"Bronze". Repeat for Binary Input1("Emerald", hasPolarity) and Multi-State Input1("Hot Pink", hasNumber_Of_States= 3). Every required property of every object is readable. - Confirm the profile boundary - a WriteProperty to any object is rejected. That is correct: a B-SS Smart Sensor is read-only.
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.
The scripts are the C++ edition's tests/, unchanged: the Go build is a
single executable at the path they expect (build/BACnetExampleBSS).
| 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. |
sensing_test.py |
The Profile Function. Reads the three sensors again and again for 12 s; the readings follow the Simulation (0.5 C steps, a toggle every 3 s, states in order). Fails against a sensor that serves constants. |
failure_modes_test.py |
The Failure Modes. Breaks the simulated sensor wire: Reliability open-loop, Status_Flags FAULT, a frozen reading; repairs it: all clear, the reading moves. Then the errors for an unknown object / property / array index, and the Reject for a WriteProperty. |
Run them all after the build (the executable finds the library in the
submodule's bin/, see Run):
python tests/run_wire_tests.py # default --exe build/BACnetExampleBSS(.exe), --port 47821
python tests/run_wire_tests.py --port 47821 --exe build/BACnetExampleBSSIt ends with ALL WIRE TESTS PASSED (exit 0); about 25 seconds.
tests/README.md says what each check proves. Verified on
Windows only (3 runs in a row, 36/36 checks each); this port has not been
run on Linux. CI (.github/workflows/release.yml) runs them on Linux.
If you have read the C++ edition, the object model and callback shapes are the same; this section is only about what is mechanically different because Go reaches the stack through cgo.
- The stack's own adapter, in place, loaded at run time.
cgobridge/stack_adapter.ccompiles the stack's generated C++ adapter (submodules/cas-bacnet-stack/adapters/cpp/) straight from the submodule in its DLL mode: everyBACnetStack_*is a function pointer thatLoadBACnetFunctions()binds withLoadLibrary/GetProcAddress(Windows) ordlopen/dlsym(Linux), after checking the stack's API version - the same call the C++ edition makes first thing inmain(). The executable has no load-time dependency on the stack library (its import table does not list it), so a missing library is reported by the program itself (see Run). Nothing from the stack is copied into this repository. - Callback trampolines. cgo cannot hand a Go function to C, so every
callback the stack calls goes C wrapper (
cgobridge/trampolines.c, the header's exact signature) ->//exported Go function (cgobridge/callbacks.go) -> the Go functionmain.goregistered. The stack's own Go adapter (adapters/golang/CASBACnetStackAdapter.go) holds this same pattern, but at this Stack Pin it does not compile and is apackage mainsample, so this example carries its own - marked as a workaround, see TODO.md. - Package-level state. The callbacks are top-level functions reading
common.DeviceStatethrough a mutex (common.WithState) rather than variables local tomain()- seecommon/device_state.go. - A standard-library transport.
common/simple_udp.goowns the UDP socket (net.ListenUDP); the stack pulls one datagram per receive callback, as in every edition.
The CAS BACnet Stack supports every standardized device profile in ASHRAE 135-2024 Annex L, and there is one example repository per profile. 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 | 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 | 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. "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, TODO.md and AGENTS.md.