A minimal, copy-paste-friendly example showing how to implement the BACnet B-SS (BACnet Smart Sensor) device profile in TypeScript on Node.js, using the CAS BACnet Stack. It listens on BACnet/IP (UDP 47808), answers ReadProperty requests, and is discoverable via Who-Is / I-Am. Its three sensor readings change over time the way real ones do, and a broken sensor shows up as a fault a client can read. The sensors themselves are a Simulation; everything BACnet does with their readings is real.
It is the Node.js edition of BACnetProfileExample-B-SS-CPP: the same device, the same objects and properties, the same Simulation and the same Wire Tests - a client cannot tell the two apart on the wire.
-
there is no prebuilt binary to download;
npm installcompiles the CAS BACnet Stack addon locally (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.1.0, built and verified against CAS BACnet Stack 6.0.23 (
6.x@64b2cfac5; its Node package@chipkin/cas-bacnet-stack6.1.2, built as the production addon), at Protocol_Revision 30, with the vendored Nodecommon/helper at v3.0.0, on Node.js 24.21. Running the example prints the example, stack andcommon/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, the device
does not keep handing out a number as if nothing happened: Analog Input 1
Reliabilityreadsopen-loop(4) and the FAULT bit ofStatus_Flagsis TRUE - clause 12.2.7: FAULT is "Logical TRUE (1) if the Reliability property is present and does not have a value of NO_FAULT_DETECTED". 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.ts
(SensorSimulationTick(), run 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 - 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.
These three are what the B-SS profile requires, and they are what this example implements. DS-RP-B is the profile's defining BIBB; the two DM BIBBs make the device discoverable.
| 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 (pinned to 6.x @ 64b2cfac5) and
consumed as an npm workspace: npm install compiles it from source into a
native Node addon and builds the package's TypeScript layer. 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.
This is a self-contained project. It ships:
main.ts- the example device.common/- the shared Node.js helper (UDP, callbacks, CLI, keyboard) vendored in.package.json/tsconfig.json/.npmrc- the build, the same on Windows, Linux and macOS..npmrcsetsbacnet_testtool=0, sonpm installbuilds the stack's production addon: the customer interface only, no test-tool exports.tests/- the Wire Tests, andwiretest/- the series' shared Python Wiretest Harness they are written against (vendored).docs/PICS.md- the conformance statement.docs/objects.json- the input to the objects-and-properties generator.submodules/cas-bacnet-stack/- the CAS BACnet Stack as a git submodule (private; requires a license - see above), consumed as an npm workspace. Its sources compile into a native Node addon, so there is no separate library to build, ship, or install. The Node adapter is used in place from the submodule (submodules/cas-bacnet-stack/adapters/node/); nothing of it is copied here.
The stack binary - the addon cas-bacnet-stack.node, written to
submodules/cas-bacnet-stack/build/Release/ - is never committed and never
released: .gitignore covers it, and a tagged release carries source only.
Build it yourself from the submodule with npm install.
- Node.js 24.21 or a later 24.x, with its npm (11.x). The stack's Node
package requires
>=24.21.0 <25. - A C++17 toolchain (node-gyp needs it to compile the stack addon).
- Git (to fetch the stack submodule), and Python 3 for the Wire Tests.
- Build tools - Visual Studio 2022 or 2026 (or their Build Tools) with the
"Desktop development with C++" workload, plus Python (the standard
node-gyp prerequisites). Node 24.21's own npm (11.19, node-gyp 12) finds
Visual Studio 2026 by itself; no
msvs_versionsetting is needed. - Use a short project path - the stack's MSVC intermediate paths mirror its source tree, and long paths can fail to compile.
- Debian/Ubuntu:
sudo apt install build-essential python3 git - macOS:
xcode-select --install
npm only, and the same commands on every platform:
git clone --recursive https://github.com/chipkin/BACnetProfileExample-B-SS-Node.git
cd BACnetProfileExample-B-SS-Node
npm install # compiles the CAS BACnet Stack addon (production build)
npm run build # compiles main.ts and common/ to dist/Already cloned without --recursive? Run git submodule update --init --recursive
first - the install needs the stack submodule.
The first
npm installtakes a few minutes (about 3 on a 12-core Windows machine): it compiles the entire CAS BACnet Stack into a native Node addon via node-gyp. npm printsUnknown project config "bacnet_testtool"on every run - that is npm noticing the.npmrcsetting it hands on to the stack's build, not an error. To confirm which addon you built:node -e "const s=require('@chipkin/cas-bacnet-stack');s.LoadBACnetFunctions();console.log(s.IsTestToolBuild())"printsfalsefor the production build.
npm start # defaults: device 389001, UDP 47808
npm start -- --port 47821 # non-default port (recommended while testing)
npm start -- --deviceID 12345 # your own device instance
npm start -- --helpnpm start runs node dist/main.js; run npm run build again after editing
main.ts. Expected output:
BACnet B-SS (Smart Sensor) Example - Node v1.1.0
CAS BACnet Stack version: 6.0.23.0
Common helper (common/) version: 3.0.0
FYI: Listening for BACnet/IP on UDP port 47808 (Network Port 1).
TX 21 bytes to <your-subnet-broadcast>:47808 (broadcast) (Network Port 1) - Unconfirmed: I-Am
FYI: Device 389001 ("Chipkin Example B-SS") ready. Vendor ID 389. Press 'h' for help.
Every BACnet frame the device receives or sends prints one RX ... / TX ...
line. 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/ 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 | Nudge the simulated Analog Input 1 (Bronze) reading up by 1.1. |
| down arrow | Nudge the simulated Analog Input 1 (Bronze) reading down by 1.1. |
The up/down keys move the simulated temperature; the Simulation carries on
ramping from the new value, and a client re-reading it sees the change. When
stdin is a pipe rather than a terminal the same characters work (q quits) -
that is how the Wire Tests stop the example.
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. The Network Port reports real BACnet/IP addressing -IP_Address,IP_Subnet_Mask,BACnet_IP_UDP_Port, and aMAC_Addressthe stack builds from them. - 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.
For a property-by-property review against the conformance statement, see TUTORIAL.md.
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. They are
the C++ edition's Wire Tests, unchanged except that --exe takes a command line
(tests/launch.py), so they prove the two editions behave the same.
| 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. |
Build (npm install && npm run build), then run them all:
python3 tests/run_wire_tests.py # Linux/macOS: starts "node dist/main.js"
python tests\run_wire_tests.py --port 47821 # WindowsIt ends with ALL WIRE TESTS PASSED (exit 0). --exe "<command>" starts the
example some other way (e.g. --exe "npx tsx main.ts").
tests/README.md says what each check proves.
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, and AGENTS.md.