diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml
index 9805249..ae95bf9 100644
--- a/.github/workflows/release.yml
+++ b/.github/workflows/release.yml
@@ -1,6 +1,7 @@
-# Build the B-RTR example against the CAS BACnet Stack STATIC library and publish a GitHub
-# Release (binaries + metrics.json) when a vX.Y.Z tag is pushed. PRs and pushes to main run
-# the same build + smoke test without publishing.
+# Build the B-RTR example and publish a GitHub Release (binaries + metrics.json) when a
+# vX.Y.Z tag is pushed. PRs and pushes to main run the same build + smoke test without
+# publishing. The build is exactly what README.md tells a customer to run: plain CMake in
+# the adapter's default SOURCE mode, which compiles the stack into the executable.
#
# The CAS BACnet Stack is a PRIVATE git submodule: the repository secret CAS_STACK_PAT must
# hold a PAT with read access to chipkin/cas-bacnet-stack.
@@ -24,17 +25,15 @@ jobs:
fail-fast: false
matrix:
include:
- # windows-2022, not windows-latest: the stack's vcxproj names the v143 (VS 2022)
- # toolset; newer images may not carry it.
+ # windows-2022 pins the VS 2022 (v143) toolset the released binaries are built
+ # with, so the published metrics stay comparable release to release.
- os: windows-2022
artifact: BACnetExampleBRTR-windows-x64
exe: build/Release/BACnetExampleBRTR.exe
- lib: submodules/cas-bacnet-stack/bin/CASBACnetStack_x64_Release.lib
metrics: metrics-windows.json
- os: ubuntu-latest
artifact: BACnetExampleBRTR-linux-x64
exe: build/BACnetExampleBRTR
- lib: submodules/cas-bacnet-stack/bin/libCASBACnetStack_x64_Release.a
metrics: metrics-linux.json
steps:
- name: Enable git long paths (Windows)
@@ -47,52 +46,23 @@ jobs:
submodules: recursive
token: ${{ secrets.CAS_STACK_PAT || github.token }}
- - name: Stack commit (cache key)
+ - name: Stack commit (recorded in metrics.json)
id: stack
shell: bash
run: echo "sha=$(git -C submodules/cas-bacnet-stack rev-parse HEAD)" >> "$GITHUB_OUTPUT"
- # The static library depends only on the stack commit, so it is cached on that SHA.
- # A moved pin therefore always rebuilds it - a stale library against a new header is
- # an ABI mismatch that links and misbehaves.
- - name: Restore the prebuilt static library
- id: libcache
- uses: actions/cache@v4
- with:
- path: submodules/cas-bacnet-stack/bin
- key: cas-stack-static-${{ runner.os }}-${{ steps.stack.outputs.sha }}
-
- - name: Build the CAS BACnet Stack static library (ReleaseLib, customer surface only)
- if: steps.libcache.outputs.cache-hit != 'true'
- shell: bash
- run: |
- set -e
- if [ "$RUNNER_OS" = "Windows" ]; then
- # microsoft/setup-msbuild's PATH update does not reach this Git-Bash step
- # (msbuild: command not found even after that step ran), so resolve it
- # directly the same way tools/build-stack-static.sh does locally.
- vswhere='/c/Program Files (x86)/Microsoft Visual Studio/Installer/vswhere.exe'
- MSBUILD=$("$vswhere" -latest -requires Microsoft.Component.MSBuild -find 'MSBuild\**\Bin\MSBuild.exe' | head -1)
- # The vcxproj names PlatformToolset=v145; windows-2022 only carries the VS 2022
- # v143 build tools, so force v143 (MSB8020 otherwise: "build tools for v145 ...
- # cannot be found"). CMake's own "Visual Studio 17 2022" generator resolves to
- # the same v143 toolset on this runner, so the two stay compiler-matched.
- "$MSBUILD" submodules/cas-bacnet-stack/projects/msvs/CASBACnetStack/CASBACnetStack.vcxproj -p:Configuration=ReleaseLib -p:Platform=x64 -p:PlatformToolset=v143 -m -v:minimal
- else
- make -C submodules/cas-bacnet-stack/projects/linux/Release/x64 -j"$(nproc)" all
- make -C submodules/cas-bacnet-stack/projects/linux/Release/x64 install
- fi
- ls -l "${{ matrix.lib }}"
-
- - name: Configure (STATIC link mode)
- run: cmake -B build -S . -DCMAKE_BUILD_TYPE=Release -DCAS_BACNET_STACK_LINK=STATIC
+ - name: Configure
+ run: cmake -B build -S . -DCMAKE_BUILD_TYPE=Release
+ # The README documents `cmake -B build -S .` with no link-mode flag, so assert the
+ # adapter's default really is SOURCE. If that default ever changes, the documented
+ # build silently starts needing a prebuilt library and fails for customers first.
- name: Confirm the link mode CMake actually used
shell: bash
run: |
mode=$(grep -E '^CAS_BACNET_STACK_LINK:' build/CMakeCache.txt | cut -d= -f2)
echo "CAS_BACNET_STACK_LINK=$mode"
- [ "$mode" = "STATIC" ] || { echo "FAIL: expected STATIC, got '$mode'"; exit 1; }
+ [ "$mode" = "SOURCE" ] || { echo "FAIL: expected SOURCE, got '$mode'"; exit 1; }
- name: Build
run: cmake --build build --config Release
@@ -101,7 +71,7 @@ jobs:
shell: bash
run: |
start=$(date +%s%3N)
- "${{ matrix.exe }}" --port 47821 > smoke.log 2>&1 &
+ "${{ matrix.exe }}" --port 47821 --port2 47822 > smoke.log 2>&1 &
PID=$!
ready_ms=""
for i in $(seq 1 100); do
@@ -128,7 +98,7 @@ jobs:
"example": "BACnetExampleBRTR",
"app_version": "$appver",
"os": "${{ matrix.os }}",
- "link_mode": "STATIC",
+ "link_mode": "SOURCE",
"stack_commit": "${{ steps.stack.outputs.sha }}",
"stack_version": "$stackver",
"binary_bytes": $bytes,
@@ -147,7 +117,8 @@ jobs:
run: |
mkdir -p dist
cp "${{ matrix.exe }}" "${{ matrix.metrics }}" dist/
- cp README.md LICENSE dist/ 2>/dev/null || true
+ cp README.md TUTORIAL.md LICENSE dist/ 2>/dev/null || true
+ cp docs/PICS.md dist/ 2>/dev/null || true
- name: Upload build artifact
uses: actions/upload-artifact@v4
diff --git a/AGENTS.md b/AGENTS.md
index a38f11a..5d12e47 100644
--- a/AGENTS.md
+++ b/AGENTS.md
@@ -2,7 +2,7 @@
Guidance for AI coding agents working in this repository. See
for the format. Human contributors should read
-[README.md](README.md) first.
+[README.md](README.md) first, then [TUTORIAL.md](TUTORIAL.md).
## What this project is
@@ -20,33 +20,47 @@ customer can learn from and copy-paste. Favour clarity over cleverness.
This repository is self-contained:
- `main.cpp` - the example device.
-- `common/` - the shared helper, vendored in-repo (not referenced via a path
- outside the repository).
+- `common/` - the shared helper (UDP, callbacks, CLI, keyboard) vendored in.
+- `README.md` - what this example is. Keep it short and about THIS example only.
+- `TUTORIAL.md` - how to extend and review the example: adding objects, the
+ F-MULTIPORT/F-ROUTER patterns, who serves which property, and
+ troubleshooting. Long-form material that would bloat the README belongs
+ here.
+- `docs/PICS.md` - the Protocol Implementation Conformance Statement. Its
+ objects-and-properties section is GENERATED from `docs/objects.json`; do not
+ hand-edit between the `OBJECTS-PROPERTIES` markers.
+- `docs/objects.json` - the input to that generator. Update it in the same change
+ as any `main.cpp` change that adds an object or a `GetProperty*` branch. It
+ now includes the Device object as its first entry.
+- `TODO.md` - the two filed stack gaps this example works around (DM-LM-B not
+ compiled into the customer build; inter-network forwarding not implemented
+ at this pin) and the third (`Routing_Table` read aborts). Read before
+ touching router behaviour.
- `submodules/cas-bacnet-stack/` - the **CAS BACnet Stack** as a git submodule
- (private). After cloning, run `git submodule update --init --recursive`.
+ (private; compiled from source). After cloning, run
+ `git submodule update --init --recursive`.
+
+The `PROFILE-TABLE` block in README.md is also generated, from the example-series
+repository's `docs/profile-table.md`. Edit it there, not here.
## Build
-This example links the CAS BACnet Stack as a prebuilt **STATIC** library - build
-the library once from the pinned submodule commit, then configure and build:
+Plain CMake, identical on every platform, in the adapter's default SOURCE mode
+(the stack's sources are compiled into the executable - no prebuilt library, no
+DLL, no per-platform pre-step):
```bash
git submodule update --init --recursive # once, if not cloned with --recursive
-tools/build-stack-static.sh BACnetProfileExample-B-RTR-CPP # from the series root
-cmake -B build -S . -DCAS_BACNET_STACK_LINK=STATIC
+cmake -B build -S .
cmake --build build --config Release
```
-The stack library build compiles the whole stack (~600 files) once and takes a
-few minutes; the example itself then builds in seconds, and later incremental
-rebuilds are fast. Use `-D CAS_STACK_DIR=...` only if your stack lives outside
-the bundled submodule. The adapter also offers a SOURCE mode (compiles the
-stack straight into the executable, no library build); this example builds and
-ships STATIC only.
-
-On Windows, if `msbuild`/CMake pick a toolset the stack's `.vcxproj` lacks,
-force `v143` on both: `TOOLSET=v143 tools/build-stack-static.sh ...` and
-`cmake -B build -S . -T v143 ...`.
+The first build compiles the whole stack (~600 files) and takes a few minutes;
+rebuilds after that are incremental and fast. Use `-D CAS_STACK_DIR=...` only if
+your stack lives outside the bundled submodule. Do not reintroduce a link-mode
+flag or a series-root build script into the documented build: a customer
+downloads this repository on its own and must be able to build it with the two
+commands above.
## Run
@@ -83,13 +97,19 @@ Interactive keys while running: `h` help, `q` quit, up/down nudge Analog Input 1
- **Do not claim cross-network forwarding works.** The pinned stack cannot
route between two ports of the same network type (stack issue #2037) - only
router *configuration* is verified. If a future stack pin fixes this,
- update README's gap notice, `TODO.md`, and the "Verify" section together.
+ update README's gap notice, `TUTORIAL.md`, `docs/PICS.md` section 9,
+ `TODO.md`, and the README's "Verify" section together.
- Every `GetProperty*` callback ends with `uint32_t* errorCode`. Leave it alone
on a catch-all decline (the stack's decline-and-fabricate default answers
required properties this app does not serve); set it only where this device
knows the read is wrong (`State_Text` out of range is the one case here).
- Match the surrounding code style: `const`-correct parameters, check every stack
return value, keep `main.cpp` linear and well-commented.
+- The `CHANGE ALL OF THIS BEFORE YOU SHIP` block in `main.cpp` carries a
+ per-constant comment on what to change it to (vendor ID, device name/
+ uniqueness, model, description, versions, device instance, and the two
+ network numbers). Keep those comments in sync with `docs/PICS.md` section 1
+ if the placeholder values change.
- **Never edit `common/` in this repo alone** - it is a vendored copy shared by
every example in the series, with its own version (`COMMON_VERSION`) and
changelog (`common/CHANGELOG.md`). To change it: edit it in
@@ -115,6 +135,9 @@ There are no unit tests; verification is behavioural:
6. **Do NOT assume cross-network forwarding works** - it currently does not
(see Conventions above); do not write a test that assumes it does without
first re-checking the stack gap is still open.
+7. If you changed the objects or their properties, regenerate `docs/PICS.md`
+ (`python tools/gen-objects-properties.py BACnetProfileExample-B-RTR-CPP` from
+ the series root) and confirm no row comes out flagged with ⚠.
## Releasing
@@ -123,6 +146,5 @@ then tag `vX.Y.Z`. The GitHub Actions workflow builds and publishes the release.
## License
-The example source code is dedicated to the public domain under
-[CC0-1.0](LICENSE). The CAS BACnet Stack is a separate, commercially licensed
-product and is not covered by that dedication.
+See [LICENSE](LICENSE). The CAS BACnet Stack is a separate, commercially
+licensed product and is not covered by it.
diff --git a/CHANGELOG.md b/CHANGELOG.md
index bb0be32..ed6b337 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -5,6 +5,67 @@ All notable changes to this project will be documented in this file.
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/),
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
+## [Unreleased]
+
+### Changed — documentation restructure, single-command build
+
+- **README.md cut down to what this example is.** It now covers only the
+ B-RTR profile, the device this example creates, the BIBBs/services/objects
+ it supports, how to build and run it, and how to verify it. It links to the
+ releases page and to the new TUTORIAL.md/docs/PICS.md near the top. Removed:
+ the "part of the series" / "you can start here" framing, the generic
+ explanation of what a device profile is, the detailed F-MULTIPORT/F-ROUTER
+ narrative (moved to TUTORIAL.md), the "Before you ship" table (moved into
+ `main.cpp` comments), the "Get the code" and "Link mode" sections, the
+ Troubleshooting table (moved to TUTORIAL.md), and the "Objects and
+ properties" section (moved to `docs/PICS.md`).
+- **New [TUTORIAL.md](TUTORIAL.md)** holds the long-form material the README
+ used to carry: extending the example (including the silent-failure warning
+ for adding a second object), the F-MULTIPORT and F-ROUTER walkthroughs
+ (including the router-forwarding limitation, carried over verbatim), what
+ each object type needs the application to serve, who serves what for a
+ representative commandable object, how to review a change against the
+ conformance statement, and troubleshooting.
+- **New [docs/PICS.md](docs/PICS.md)**, a Protocol Implementation Conformance
+ Statement in the ANSI/ASHRAE 135 Annex A shape (product description, profile
+ claimed, BIBBs, services, segmentation, object types, data link layer,
+ address binding, networking options, character sets), with the generated
+ objects-and-properties tables as its penultimate section. Section 9
+ (networking options) spells out precisely what router configuration does
+ and does not do at this stack pin - it does not claim cross-network
+ forwarding works.
+- **`docs/objects.json`'s Device entry now separates `stack` from `accepted`**,
+ matching the shape used across the series: device-wide facts the stack
+ computes (`Object_List`, `Protocol_Version`/`Revision`,
+ `Protocol_Services/Object_Types_Supported`, `Device_Address_Binding`) are
+ now labelled plain `stack` in the generated PICS instead of `stack default,
+ accepted`; the actual stack-configured defaults (APDU limits, segmentation,
+ system status, database revision) stay under `accepted`. Regenerated with
+ `tools/gen-objects-properties.py` - zero ⚠ rows.
+- **Build is `cmake -B build -S .` and nothing else, on every platform.** The
+ documented build no longer calls `tools/build-stack-static.sh`, which lives
+ in the example-series repository and is therefore not available to a
+ customer who downloads this repository on its own, and no longer links a
+ prebuilt STATIC library. The example now builds in the adapter's default
+ SOURCE mode: the stack's sources are compiled into the executable, so there
+ is no library to build first. `CMakeLists.txt`, `AGENTS.md` and the release
+ workflow were updated to match; the workflow no longer builds/caches the
+ STATIC library or carries matrix `lib:` entries, configures without a
+ link-mode flag, asserts `CAS_BACNET_STACK_LINK=SOURCE` (was `STATIC`), sets
+ `"link_mode": "SOURCE"` in the published metrics JSON, and packages
+ `TUTORIAL.md`/`docs/PICS.md` alongside the binary.
+- **The "Before you ship" guidance moved into `main.cpp`**, as a comment next
+ to each constant in the `CHANGE ALL OF THIS BEFORE YOU SHIP` block,
+ including the warning that `DEVICE_NAME` is a compile-time constant here and
+ must be made per-unit configurable (serial number, DIP switches, config
+ file, or a `--deviceName` argument) in a real product, and that the two
+ network numbers must be unique across the whole internetwork like the
+ device instance.
+- **Footprint table now documents the SOURCE-mode build.** The v1.0.0 numbers
+ were measured from a STATIC-linked build; the table now notes that the next
+ release refreshes them under the documented SOURCE build, matching the
+ series convention.
+
## [1.0.0] - unreleased
### Added
diff --git a/CMakeLists.txt b/CMakeLists.txt
index 10bce1f..b18c227 100644
--- a/CMakeLists.txt
+++ b/CMakeLists.txt
@@ -3,16 +3,15 @@
#
# This example links the CAS BACnet Stack through the CASBACnetStack::Adapter CMake
# target (submodules/cas-bacnet-stack/adapters/cpp) and calls the stack's C API
-# (BACnetStack_*, via CASBACnetStackAdapter.h) directly. This example builds and
-# ships in STATIC mode: CAS_BACNET_STACK_LINK=STATIC links a prebuilt
-# CASBACnetStack_x64_Release.lib / libCASBACnetStack_x64_Release.a, built first by
-# tools/build-stack-static.sh from the stack's own project files. (The adapter also
-# offers a SOURCE mode that compiles the stack's source/ straight into the
-# executable with no separate library build; this example is not built that way.)
+# (BACnetStack_*, via CASBACnetStackAdapter.h) directly. It builds and ships in the
+# adapter's default SOURCE mode: the stack's source/*.cpp are compiled straight into
+# the executable, so there is no library or DLL to build, ship, or install first and
+# the build is the same two commands on every platform. (The adapter also offers
+# STATIC and DLL modes - see its CMakeLists.txt - but they need a prebuilt library
+# and this example does not use them.)
#
# Build:
-# tools/build-stack-static.sh BACnetProfileExample-B-RTR-CPP (from the series root)
-# cmake -B build -S . -DCAS_BACNET_STACK_LINK=STATIC
+# cmake -B build -S .
# cmake --build build --config Release --parallel
# Run (from the example folder):
# ./build/BACnetExampleBRTR (Linux/macOS)
diff --git a/README.md b/README.md
index 1e68db6..c657f89 100644
--- a/README.md
+++ b/README.md
@@ -6,23 +6,22 @@ A minimal, copy-paste-friendly example showing how to implement the BACnet
It is a stand-alone BACnet router: it owns **two BACnet/IP Network Port
objects**, one per network it joins, and configures physical inter-datalink
routing between them, answers **ReadProperty**/**WriteProperty**, and is
-discoverable via **Who-Is / I-Am**.
+discoverable via **Who-Is / I-Am**. It is the series' canonical example for
+**F-ROUTER** and **F-MULTIPORT** (a device owning more than one Network Port).
-Part of the CAS BACnet Stack **BACnet profile example series** - one repository
-per BACnet device profile. This example claims **only** B-RTR, and is the
-series' canonical example for **F-ROUTER** and **F-MULTIPORT** (a device
-owning more than one Network Port).
+**[Download a prebuilt binary](https://github.com/chipkin/BACnetProfileExample-B-RTR-CPP/releases)**
+(Windows and Linux x64) - or build it yourself, see [Build](#build) below.
-Reading order: this repository stands on its own - **you can start here.** If you also want the
-gentler introductions to the shared sensor/actuator objects, [B-SS (Smart
-Sensor)](https://github.com/chipkin/BACnetProfileExample-B-SS-CPP) is the first example in the
-series; this one repeats everything it needs.
+- **[TUTORIAL.md](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](docs/PICS.md)** - the Protocol Implementation Conformance
+ Statement: every object, every property, and who answers it.
> **Versions:** this document describes **example v1.0.0**, built and verified
-> against **CAS BACnet Stack 6.0.21** (`6.x` @ `abd4cee1`), linked as a static
-> library, at **Protocol_Revision 24**, with the vendored `common/` helper at
-> **v2.5.0**. Running the example prints all three - if what it prints
-> disagrees with this line, trust the program and check `CHANGELOG.md`.
+> against **CAS BACnet Stack 6.0.21** (`6.x` @ `abd4cee1`), at
+> **Protocol_Revision 24**, with the vendored `common/` helper at **v2.5.0**.
+> Running the example prints all three - if what it prints disagrees with this
+> line, trust the program and check `CHANGELOG.md`.
> **IMPORTANT - read before you rely on this example for routing.** The pinned
> CAS BACnet Stack build genuinely accepts this device's router **configuration**
@@ -37,69 +36,6 @@ series; this one repeats everything it needs.
> Each port answers ReadProperty/WriteProperty directly and correctly on its own
> network - what does not work is forwarding traffic **between** the two networks.
-## Quickstart
-
-You need a CAS BACnet Stack licence and access to its private submodule (see
-[Requires the CAS BACnet Stack](#requires-the-cas-bacnet-stack-licensed-product)).
-Then:
-
-```bash
-git clone --recursive https://github.com/chipkin/BACnetProfileExample-B-RTR-CPP.git
-cd BACnetProfileExample-B-RTR-CPP
-tools/build-stack-static.sh BACnetProfileExample-B-RTR-CPP # from the series root; builds the static library
-cmake -B build -S . -DCAS_BACNET_STACK_LINK=STATIC
-cmake --build build --config Release
-./build/Release/BACnetExampleBRTR.exe # Windows; drop Release/ on Linux
-```
-
-The device binds two UDP ports, announces itself on both networks, and prints
-`Press 'h' for help`.
-
-> **You will see one or more red `Error:` lines at start-up. Most of the
-> device is fine** - see [Troubleshooting](#troubleshooting); the
-> `FindPortByNetworkType` / `BindRouterPort` lines specifically are the known,
-> documented routing gap above, not a bug in this example.
-
-## What is a B-RTR (Router) profile?
-
-A **device profile** is a standard "template" defined in Annex L of ANSI/ASHRAE
-135. It lists the capabilities a class of device must support so that any
-compliant client knows what to expect, and the BACnet Testing Laboratories (BTL)
-certify devices against it. (New to BACnet in general? See Chipkin's
-[What is BACnet?](https://docs.chipkin.com/protocols/bacnet/) guide.)
-
-**B-RTR (Router)** is a stand-alone BACnet router (ANSI/ASHRAE 135 clause 6.6):
-a device with two (or more) Network Port objects that forwards NPDUs between
-the BACnet networks they connect, and speaks the network-layer messages other
-devices and routers use to discover it and learn the internetwork's topology.
-
-**Reading the capability names.** Each capability below is a **BIBB** (BACnet
-Interoperability Building Block). Every BIBB name ends in **-A** or **-B**:
-**-A** = the device that *initiates* a request; **-B** = the device that
-*responds*. `DM-DDB-A` means this device also **initiates** Who-Is (not just
-answers it) - appropriate for a router, which is expected to actively discover
-devices on the networks it joins.
-
-**What the profile requires:**
-
-- **Data Sharing - ReadProperty - B side (DS-RP-B):** answer **ReadProperty**.
-- **Data Sharing - WriteProperty - B side (DS-WP-B):** accept **WriteProperty**
- to its objects.
-- **Device Management - Dynamic Device Binding - A/B (DM-DDB-A/B):** answer
- **Who-Is** with **I-Am**, broadcast an unsolicited I-Am on each of its
- networks at start-up, and (A side) itself send **Who-Is** to discover other
- devices.
-- **Device Management - Dynamic Object Binding - B side (DM-DOB-B):** answer
- **Who-Has** with **I-Have**.
-- **Network Management - Router Configuration - B side (NM-RC-B):** configure
- router ports/routes; answer **Who-Is-Router-To-Network** with
- **I-Am-Router-To-Network**; announce **Network-Number-Is** on each network.
-
-**What the profile does NOT require** - and this example omits on purpose:
-**alarming / event reporting**, **scheduling**, **trending**, and (see the
-gap notice above and `TODO.md`) **AddListElement/RemoveListElement (DM-LM-B)**,
-which the pinned stack's customer-facing build does not compile in.
-
## The device this example creates
```
@@ -116,64 +52,16 @@ Device 389018 "Rainbow" (Vendor 389 - Chipkin Automation Systems)
```
The three **input** objects and three **output** objects are the shared
-minimum/commandable pattern every example in this series carries (inherited
-from B-SA/B-ASC, minus B-ASC's DeviceCommunicationControl). The **two Network
-Ports** are this example's own contribution - see F-MULTIPORT/F-ROUTER below.
-
-## F-MULTIPORT: two Network Ports, two UDP sockets
-
-Every other example in this series owns exactly one Network Port and one UDP
-socket. This example is the series' first with two, and is canonical for that
-pattern (`common/` 2.3.0+): `CASExampleHelper::SetupUDP(port, networkPortInstance)`
-binds an additional socket keyed to a specific Network Port instance, and the
-shared receive/send callbacks (`common/CASExampleHelper.cpp`) dispatch on that
-instance internally - `main.cpp` just calls `SetupUDP` twice and
-`SendIAm(deviceInstance, networkPortInstance)` once per port. See
-`common/CHANGELOG.md`'s 2.3.0 entry and the "WHY THIS IS SAFE FOR SINGLE-PORT
-EXAMPLES" comment in `CASExampleHelper.cpp` for why this did not change any
-other (single-port) example's behaviour.
-
-Both ports run on **one host** and are distinguished purely by **UDP port
-number** (`--port` / `--port2`, default `--port + 1`) - they report the same
-`IP_Address`/`IP_Subnet_Mask` and different `BACnet_IP_UDP_Port` /
-`Network_Number`.
-
-## F-ROUTER: router configuration
-
-At start-up, `main.cpp`:
-
-1. `BACnetStack_AddRouterPort` once per Network Port, registering the network
- it directly connects (network 1 on Vermilion, network 2 on Vermilion 2).
-2. `BACnetStack_AddRouterRoute` for one DEMONSTRATION-ONLY network (3), one hop
- beyond port 2, with no next-hop MAC configured - this exercises the API; no
- device in this example series actually sits on network 3 (see `main.cpp`'s
- `DEMO_REMOTE_NETWORK` comment).
-3. `BACnetStack_SetRouterEnabled(deviceInstance, true)` - routing is opt-in
- even with ports configured.
-4. Broadcasts **I-Am-Router-To-Network** on both ports (`networkNumbers = NULL`,
- announcing this router's full configured routing table per the stack's
- `#1014` fix), and again whenever the **`r`** key is pressed.
-5. Broadcasts **Network-Number-Is** on both ports, announcing each network's
- own (configured) number.
-6. Broadcasts **Who-Is-Router-To-Network** (unrestricted) on both ports, the
- polite router start-up probe for any other router already present (none is
- expected to answer in this example).
-
-`BACnetStack_AddRouterPort`'s `networkType` parameter is the stack's own
-internal datalink enum (`BACnetPacket::NetworkType_IP = 0`), which is a
-**different enumeration** from the Network Port object's `Network_Type`
-property (`NETWORK_PORT_NETWORK_TYPE_IPV4 = 5`, used with
-`AddNetworkPortObject`) - passing the wrong one fails with "unknown network
-type"; see the `ROUTER_NETWORK_TYPE_IP` comment in `main.cpp`.
-
-**`Routing_Table` (cl. 12.56.62) is deliberately NOT enabled or claimed.**
-Verified on the wire: enabling it and reading it aborts the request, because
-the pinned stack's `BACnetBusinessLogic::UseCallbackGetList` has no case for
-Network Port/Routing_Table outside a test-tool build. See `TODO.md` and
-[stack issue #2038](https://github.com/chipkin/cas-bacnet-stack/issues/2038).
+minimum/commandable pattern every example in this series carries. The **two
+Network Ports** are this example's own contribution - see [TUTORIAL.md](TUTORIAL.md)
+for how `main.cpp` wires them up as F-MULTIPORT and configures the router
+(F-ROUTER).
## What this example supports
+The example implements exactly the capabilities below - and nothing more,
+which is the point of a profile example.
+
### BIBBs (BACnet Interoperability Building Blocks)
| BIBB | Description | Supported |
@@ -186,6 +74,9 @@ Network Port/Routing_Table outside a test-tool build. See `TODO.md` and
| NM-RC-B | Network Management - Router Configuration - B | 🟡 partial - see the gap notice above; configuration/announcement genuinely work, inter-network *forwarding* does not at this stack pin |
| DM-LM-B | Device Management - List Manipulation - B (AddListElement/RemoveListElement) | ❌ not implemented - `TODO.md`, [stack issue #2033](https://github.com/chipkin/cas-bacnet-stack/issues/2033) |
+This example does not implement DeviceCommunicationControl (DM-DCC-B),
+alarming, scheduling, or trending - not required by B-RTR.
+
### Services (executed / B-side)
| Service | Notes |
@@ -194,7 +85,7 @@ Network Port/Routing_Table outside a test-tool build. See `TODO.md` and
| WriteProperty (15) | Accepts writes to the commandable outputs' Present_Value (DS-WP-B). Verified with a live client. |
| Who-Is / I-Am | Answers Who-Is with I-Am; broadcasts I-Am on start-up on BOTH networks (DM-DDB-A/B). |
| Who-Has / I-Have | Answers Who-Has with I-Have (DM-DOB-B). |
-| Who-Is-Router-To-Network / I-Am-Router-To-Network | Sent on both ports at start-up and on the `r` key (NM-RC-B). Verified sent (TX on the wire); not verified that this device *answers* an incoming Who-Is-Router-To-Network with a matching reply - see Verify below. |
+| Who-Is-Router-To-Network / I-Am-Router-To-Network | Sent on both ports at start-up and on the `r` key (NM-RC-B). Verified sent (TX on the wire); not verified that this device *answers* an incoming Who-Is-Router-To-Network - see [Verify](#verify). |
| Network-Number-Is | Sent on both ports at start-up (NM-RC-B). |
| AddListElement (8) / RemoveListElement (9) | **Not implemented** - see `TODO.md`. |
@@ -212,25 +103,8 @@ Network Port/Routing_Table outside a test-tool build. See `TODO.md` and
| Network Port | 1 | Vermilion | network 1 |
| Network Port | 2 | Vermilion 2 | network 2 |
-## Before you ship
-
-This example is a tutorial, and it identifies itself as one. Everything in this
-table is read by clients and shown to the operator in **every discovery tool on
-the network**. Left as-is, your product appears on a real site announcing itself
-as a Chipkin demo. None of it is cosmetic.
-
-| Constant (`main.cpp`) | Ships as | Change it to |
-|---|---|---|
-| `VENDOR_IDENTIFIER` | `389` (Chipkin) | **Your** company's vendor ID. Assigned by ASHRAE, free: |
-| `VENDOR_NAME` | `Chipkin Automation Systems` | Your company name — must match the vendor ID above. |
-| `DEVICE_NAME` | `"Rainbow"` | Your device's `Object_Name`. **Must be unique across the BACnet internetwork.** |
-| `MODEL_NAME` | `CAS BACnet Stack Example - B-RTR` | Your model designation. |
-| `DEVICE_DESCRIPTION` | a description of *this example* | What your device actually is. |
-| `FIRMWARE_REVISION` / `APPLICATION_SOFTWARE_VERSION` | `1.0.0` | Your real versions. |
-| Device instance | `389018` (`--deviceID` overrides) | Must be unique on the internetwork. |
-| `NETWORK_NUMBER_PORT_1` / `NETWORK_NUMBER_PORT_2` | `1` / `2` | Your site's actual network numbers - these must be unique across the whole BACnet internetwork, same as a device instance. |
-
-`main.cpp` marks this block with a `CHANGE ALL OF THIS BEFORE YOU SHIP` banner.
+Every required property of every object, and who answers it, is in
+[docs/PICS.md](docs/PICS.md).
## Requires the CAS BACnet Stack (licensed product)
@@ -244,17 +118,22 @@ BACnet Stack license and access to that repository.
Chipkin:** or
sales@chipkin.com.
-You do not need a stack licence to *read* this example. Every file outside
-submodules/ is CC0 public domain.
+You do not need a stack licence to *read* this example, or to run a
+[prebuilt release binary](https://github.com/chipkin/BACnetProfileExample-B-RTR-CPP/releases).
+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.
- `submodules/cas-bacnet-stack/` - the **CAS BACnet Stack as a git submodule**
- (private; requires a license). Built into a prebuilt **STATIC** library by
- the stack's own project files (`tools/build-stack-static.sh`), then linked -
- no DLL is shipped.
+ (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
@@ -274,53 +153,28 @@ submodules/ is CC0 public domain.
- Debian/Ubuntu: `sudo apt install build-essential cmake git`
- macOS: `xcode-select --install` and `brew install cmake`
-## Get the code
+## Build
+
+CMake only, and the same two commands on every platform:
```bash
git clone --recursive https://github.com/chipkin/BACnetProfileExample-B-RTR-CPP.git
cd BACnetProfileExample-B-RTR-CPP
-# already cloned without --recursive? fetch the submodule:
-git submodule update --init --recursive
-```
-
-## Build
-
-This example links the CAS BACnet Stack as a prebuilt **STATIC** library. Build
-the library once from the pinned submodule commit, then configure and build the
-example against it:
-
-```bash
-tools/build-stack-static.sh BACnetProfileExample-B-RTR-CPP # from the series root
-cmake -B build -S . -DCAS_BACNET_STACK_LINK=STATIC
+cmake -B build -S .
cmake --build build --config Release
```
-> **The stack library build takes a few minutes** the first time - it compiles
-> the entire CAS BACnet Stack (~600 source files) once. The example itself then
-> builds in seconds and rebuilds incrementally. Build in parallel to cut that
-> down: `cmake --build build --config Release --parallel`.
->
-> On Windows, if `msbuild` picks a toolset the stack's `.vcxproj` does not have
-> installed, force the one Visual Studio 2022 ships (`v143`):
-> `TOOLSET=v143 tools/build-stack-static.sh BACnetProfileExample-B-RTR-CPP`, and
-> configure CMake with the matching generator toolset: `cmake -B build -S . -T v143 ...`.
+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`.
-### Link mode
-
-This example links the stack through the `CASBACnetStack::Adapter` CMake target
-in **STATIC** mode - `-DCAS_BACNET_STACK_LINK=STATIC` links the prebuilt
-`CASBACnetStack_x64_Release.lib` / `libCASBACnetStack_x64_Release.a` built by
-`tools/build-stack-static.sh` above. Every mode requires calling
-`LoadBACnetFunctions()` once at the top of `main()` before any other
-`BACnetStack_*` call. On MSVC the adapter also forces the static CRT (`/MT`) to
-match how the library is built. A **SOURCE** mode also exists (compiles the
-stack's `source/*.cpp` straight into the executable) - this example is built
-and published in **STATIC** mode only.
-
## Run
```bash
@@ -349,6 +203,12 @@ FYI: Device 389018 ("Rainbow") ready. Vendor ID 389. Routing network 1 (Vermilio
The device listens on UDP **47808** and **47809** by default. Allow both ports
through your firewall.
+> **A wall of red `Error:` lines at start-up is expected and is not your bug** -
+> it includes both ports hearing their own broadcast I-Am, a one-time BACnet/SC
+> UUID notice, and the `BindRouterPort`/`FindPortByNetworkType` lines from the
+> forwarding gap above. [TUTORIAL.md](TUTORIAL.md#troubleshooting) explains all
+> of them.
+
### Command-line options
| Option | Default | Meaning |
@@ -367,7 +227,7 @@ through your firewall.
| `q` | Quit. |
| up arrow | Increase Analog Input 1 (`Bronze`) by 1.1. |
| down arrow | Decrease Analog Input 1 (`Bronze`) by 1.1. |
-| `r` | Re-send I-Am-Router-To-Network on both ports now (`common/` 2.4.0+). |
+| `r` | Re-send I-Am-Router-To-Network on both ports now. |
## Verify
@@ -395,278 +255,139 @@ both work.
- **Cross-network forwarding.** A ReadProperty sent to a device reachable only
through the OTHER Network Port is **not** routed - see the gap notice at the
top of this README and `TODO.md` ([stack issue #2037](https://github.com/chipkin/cas-bacnet-stack/issues/2037)).
- This was the runbook's own wire-verification target for this example and it
- genuinely fails at this stack pin; it is not a gap in this example's code.
- **`Routing_Table` readback.** Deliberately not enabled - see `TODO.md`
([stack issue #2038](https://github.com/chipkin/cas-bacnet-stack/issues/2038)).
- **This device answering an incoming Who-Is-Router-To-Network.** Only the
- outbound (start-up / `r`-key) direction was exercised in verification; a
- peer router's unsolicited query was not tested against this build.
+ outbound (start-up / `r`-key) direction was exercised in verification.
- **The `DEMO_REMOTE_NETWORK` (3) route.** No device sits on it; `AddRouterRoute`
accepting the call is confirmed, an actual forwarded packet is not (and
could not be, given the forwarding gap above).
-## Troubleshooting
+For a property-by-property review against the conformance statement, see
+[TUTORIAL.md](TUTORIAL.md).
-| Symptom | Cause / fix |
-|---------|-------------|
-| Start-up prints `BindRouterPort ... second port of networkType=[0] ... issue #304` and `FindPortByNetworkType ... Cannot attribute an incoming NPDU` | **Expected, not your bug.** The pinned stack cannot route between two ports of the same network type (both BACnet/IP here) - see the gap notice at the top of this README, `TODO.md`, and [stack issue #2037](https://github.com/chipkin/cas-bacnet-stack/issues/2037). Each port still works fine on its own network. |
-| ReadProperty of `Routing_Table` aborts | Expected - not enabled in this example; see `TODO.md` and [stack issue #2038](https://github.com/chipkin/cas-bacnet-stack/issues/2038). |
-| On start-up the app also prints a `UUID has not been set` line | Benign - the stack starts a BACnet/SC datalink this BACnet/IP-only example never configures. Same as every other example in the series. |
-| CMake error: *"CAS BACnet Stack adapter not found under: ..."* | Submodules not initialized. Run `git submodule update --init --recursive`. |
-| `Failed to bind UDP port ...` | Another program is using that port. Stop it, or pass a different `--port`/`--port2`. |
-| `--port2` equals `--port` | Rejected on purpose - they must be two different UDP ports (two different simulated networks). |
-| First build seems stuck for minutes | Normal - it's compiling ~600 stack files. Only the first build is slow. |
-| `git submodule update` fails with *Permission denied* / *repository not found* | The CAS BACnet Stack submodule is a **private** repo - see [Requires the CAS BACnet Stack](#requires-the-cas-bacnet-stack-licensed-product). |
+## Footprint
+
+Measured from the [v1.0.0](https://github.com/chipkin/BACnetProfileExample-B-RTR-CPP/releases/tag/v1.0.0) release CI.
+
+| Metric | Windows | Linux |
+|---|---|---|
+| Binary size | 3,263,488 bytes | 49,128 bytes |
+| SHA-256 (prefix) | `688a49a7a1ca5fb2` | `8842ac2760185932` |
+| Start-up time to `ready` | 200 ms | 109 ms |
+| Stack commit | `abd4cee1` (6.0.21) | `abd4cee1` (6.0.21) |
+| Link mode | STATIC | STATIC |
+| Compiler | Visual Studio 17 2022 | `/usr/bin/c++` |
+
+v1.0.0 was published from a STATIC-linked build; this table now documents the
+adapter's default **SOURCE** mode instead (see [Build](#build)), so the next
+release refreshes these numbers from the documented build.
+
+
## The BACnet profile example series
-The CAS BACnet Stack supports every standardized device profile in ASHRAE 135-2024 Annex L. One example repository per profile shows how. ✅ = the required BIBB (service) is supported by the CAS BACnet Stack; the **Example** column is the state of that profile's tutorial repository.
+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.
### Controllers (Annex L.4)
-| Profile | Example | Required BIBBs (services) |
+| Profile | C++ | Node.js |
|---|---|---|
-| **B-SS** Smart Sensor | [B-SS-CPP](https://github.com/chipkin/BACnetProfileExample-B-SS-CPP) ✅ | ✅ DS-RP-B · ✅ DM-DDB-B · ✅ DM-DOB-B |
-| **B-SA** Smart Actuator | [B-SA-CPP](https://github.com/chipkin/BACnetProfileExample-B-SA-CPP) ✅ | ✅ DS-RP-B · ✅ DS-WP-B · ✅ DM-DDB-B · ✅ DM-DOB-B |
-| **B-ASC** Application Specific Controller | [B-ASC-CPP](https://github.com/chipkin/BACnetProfileExample-B-ASC-CPP) ✅ · [B-ASC-Node](https://github.com/chipkin/BACnetProfileExample-B-ASC-Node) ✅ | ✅ DS-RP-B · ✅ DS-WP-B · ✅ DM-DDB-B · ✅ DM-DOB-B · ✅ DM-DCC-B |
-| **B-AAC** Advanced Application Controller | [B-AAC-CPP](https://github.com/chipkin/BACnetProfileExample-B-AAC-CPP) ✅ | ✅ DS-RP-B · ✅ DS-RPM-B · ✅ DS-WP-B · ✅ DS-WPM-B · ✅ AE-N-I-B · ✅ AE-ACK-B · ✅ AE-INFO-B · ✅ AE-CRL-B · ✅ SCHED-I-B · ✅ DM-DDB-A · ✅ DM-DDB-B · ✅ DM-DOB-B · ✅ DM-DCC-B · ✅ DM-TS-B / DM-UTC-B · ✅ DM-RD-B |
-| **B-BC** Building Controller | [B-BC-CPP](https://github.com/chipkin/BACnetProfileExample-B-BC-CPP) ✅ | ✅ DS-RP-A · ✅ DS-RP-B · ✅ DS-RPM-A · ✅ DS-RPM-B · ✅ DS-WP-A · ✅ DS-WP-B · ✅ DS-WPM-B · ✅ AE-N-I-B · ✅ AE-ACK-B · ✅ AE-INFO-B · ✅ AE-CRL-B · ✅ SCHED-E-B · ✅ T-VMT-I-B · ✅ T-ATR-B · ✅ DM-DDB-A · ✅ DM-DDB-B · ✅ DM-DOB-B · ✅ DM-DCC-B · ✅ DM-TS-B / DM-UTC-B · ✅ DM-RD-B · ✅ DM-BR-B |
+| **B-SS** Smart Sensor | [B-SS-CPP](https://github.com/chipkin/BACnetProfileExample-B-SS-CPP) | — |
+| **B-SA** Smart Actuator | [B-SA-CPP](https://github.com/chipkin/BACnetProfileExample-B-SA-CPP) | — |
+| **B-ASC** Application Specific Controller | [B-ASC-CPP](https://github.com/chipkin/BACnetProfileExample-B-ASC-CPP) | [B-ASC-Node](https://github.com/chipkin/BACnetProfileExample-B-ASC-Node) |
+| **B-AAC** Advanced Application Controller | [B-AAC-CPP](https://github.com/chipkin/BACnetProfileExample-B-AAC-CPP) | — |
+| **B-BC** Building Controller | [B-BC-CPP](https://github.com/chipkin/BACnetProfileExample-B-BC-CPP) | — |
### Life safety controllers (Annex L.5)
-| Profile | Example | Required BIBBs (services) |
+| Profile | C++ | Node.js |
|---|---|---|
-| **B-LSC** Life Safety Controller | [B-LSC-CPP](https://github.com/chipkin/BACnetProfileExample-B-LSC-CPP) 🚧 (blocked: [cas-bacnet-stack#2036](https://github.com/chipkin/cas-bacnet-stack/issues/2036)) | ✅ DS-RP-B · ✅ DS-RPM-B · ✅ DS-WP-B · ✅ DS-WPM-B · ✅ DS-COV-B · ✅ AE-LS-B · ✅ AE-ACK-B · ✅ AE-INFO-B · ✅ DM-DDB-A · ✅ DM-DDB-B · ✅ DM-DOB-B · ✅ DM-DCC-B · ✅ DM-TS-B / DM-UTC-B · ✅ DM-RD-B |
-| **B-ALSC** Advanced Life Safety Controller | [B-ALSC-CPP](https://github.com/chipkin/BACnetProfileExample-B-ALSC-CPP) ✅ | ✅ DS-RP-B · ✅ DS-RPM-B · ✅ DS-WP-B · ✅ DS-WPM-B · ✅ DS-COV-B · ✅ AE-LS-B · ✅ AE-ACK-B · ✅ AE-INFO-B · ✅ AE-EL-I-B · ✅ SCHED-I-B · ✅ DM-DDB-A · ✅ DM-DDB-B · ✅ DM-DOB-B · ✅ DM-DCC-B · ✅ DM-TS-B / DM-UTC-B · ✅ DM-RD-B |
+| **B-LSC** Life Safety Controller | [B-LSC-CPP](https://github.com/chipkin/BACnetProfileExample-B-LSC-CPP) 🚧 | — |
+| **B-ALSC** Advanced Life Safety Controller | [B-ALSC-CPP](https://github.com/chipkin/BACnetProfileExample-B-ALSC-CPP) | — |
### Access control controllers (Annex L.6)
-| Profile | Example | Required BIBBs (services) |
+| Profile | C++ | Node.js |
|---|---|---|
-| **B-ACC** Access Control Controller | [B-ACC-CPP](https://github.com/chipkin/BACnetProfileExample-B-ACC-CPP) ✅ | ✅ DS-RP-B · ✅ DS-RPM-B · ✅ DS-WP-B · ✅ DS-WPM-B · ✅ DS-COV-B · ✅ DS-ACUC-B · ✅ DS-ACSC-B · ☐ AE-AC-B ([cas-bacnet-stack#2044](https://github.com/chipkin/cas-bacnet-stack/issues/2044)) · ✅ AE-ACK-B · ✅ AE-INFO-B · ✅ AE-EL-I-B · ✅ SCHED-I-B · ✅ DM-DDB-A · ✅ DM-DDB-B · ✅ DM-DOB-B · ✅ DM-DCC-B · ✅ DM-TS-B / DM-UTC-B · ✅ DM-RD-B · ✅ DM-BR-B |
-| **B-AACC** Advanced Access Control Controller | [B-AACC-CPP](https://github.com/chipkin/BACnetProfileExample-B-AACC-CPP) ✅ | ✅ DS-RP-A · ✅ DS-RP-B · ✅ DS-RPM-A · ✅ DS-RPM-B · ✅ DS-WP-A · ✅ DS-WP-B · ✅ DS-WPM-B · ✅ DS-COV-A · ✅ DS-COV-B · ✅ DS-ACAD-A · ☐ DS-ACCDI-A · ✅ DS-ACUC-B · ✅ DS-ACSC-B · ☐ AE-AC-B ([cas-bacnet-stack#2044](https://github.com/chipkin/cas-bacnet-stack/issues/2044)) · ✅ AE-ACK-B · ✅ AE-INFO-B · ✅ AE-EL-I-B · ✅ SCHED-I-B · ✅ DM-DDB-A · ✅ DM-DDB-B · ✅ DM-DOB-B · ✅ DM-DCC-B · ✅ DM-TS-B / DM-UTC-B · ✅ DM-RD-B · ✅ DM-BR-B |
+| **B-ACC** Access Control Controller | [B-ACC-CPP](https://github.com/chipkin/BACnetProfileExample-B-ACC-CPP) | — |
+| **B-AACC** Advanced Access Control Controller | [B-AACC-CPP](https://github.com/chipkin/BACnetProfileExample-B-AACC-CPP) | — |
### Lighting controllers (Annex L.11)
-| Profile | Example | Required BIBBs (services) |
+| Profile | C++ | Node.js |
|---|---|---|
-| **B-LD** Lighting Device | [B-LD-CPP](https://github.com/chipkin/BACnetProfileExample-B-LD-CPP) ✅ | ✅ DS-RP-B · ✅ DS-WP-B · ✅ DS-LO-B / DS-BLO-B · ✅ DM-DDB-B · ✅ DM-DOB-B · ✅ DM-DCC-B · ✅ DM-TS-B / DM-UTC-B |
-| **B-LS** Lighting Supervisor | [B-LS-CPP](https://github.com/chipkin/BACnetProfileExample-B-LS-CPP) ✅ | ✅ DS-RP-B · ✅ DS-WP-A · ✅ DS-WP-B · ✅ DS-WG-E-B · ✅ DS-ALO-A · ✅ SCHED-E-B · ✅ DM-DDB-A · ✅ DM-DDB-B · ✅ DM-DOB-B · ✅ DM-DCC-B · ✅ DM-TS-B / DM-UTC-B |
+| **B-LD** Lighting Device | [B-LD-CPP](https://github.com/chipkin/BACnetProfileExample-B-LD-CPP) | — |
+| **B-LS** Lighting Supervisor | [B-LS-CPP](https://github.com/chipkin/BACnetProfileExample-B-LS-CPP) | — |
### Elevator controllers (Annex L.13)
-| Profile | Example | Required BIBBs (services) |
+| Profile | C++ | Node.js |
|---|---|---|
-| **B-EM** Elevator Monitor | [B-EM-CPP](https://github.com/chipkin/BACnetProfileExample-B-EM-CPP) ✅ | ✅ DS-RP-B · ✅ DS-RPM-B · ✅ DS-COV-B · ✅ DS-COVM-B · ✅ AE-N-I-B · ✅ AE-ACK-B · ✅ AE-INFO-B · ✅ DM-DDB-B · ✅ DM-DOB-B · ✅ DM-DCC-B |
-| **B-EC** Elevator Controller | [B-EC-CPP](https://github.com/chipkin/BACnetProfileExample-B-EC-CPP) ✅ | ✅ DS-RP-B · ✅ DS-RPM-B · ✅ DS-WP-B · ✅ DS-WPM-B · ✅ DS-COV-B · ✅ DS-COVM-B · ✅ AE-N-I-B · ✅ AE-ACK-B · ✅ AE-INFO-B · ✅ DM-DDB-A · ✅ DM-DDB-B · ✅ DM-DOB-B · ✅ DM-DCC-B · ✅ DM-TS-B / DM-UTC-B · ✅ DM-RD-B |
-| **B-AEC** Advanced Elevator Controller | [B-AEC-CPP](https://github.com/chipkin/BACnetProfileExample-B-AEC-CPP) ✅ | ✅ DS-RP-B · ✅ DS-RPM-B · ✅ DS-WP-B · ✅ DS-WPM-B · ✅ DS-COV-B · ✅ DS-COVM-B · ✅ AE-N-I-B · ✅ AE-ACK-B · ✅ AE-INFO-B · ✅ AE-EL-I-B · ✅ SCHED-I-B · ✅ DM-DDB-A · ✅ DM-DDB-B · ✅ DM-DOB-B · ✅ DM-DCC-B · ✅ DM-TS-B / DM-UTC-B · ✅ DM-OCD-B · ✅ DM-RD-B · ✅ DM-BR-B |
+| **B-EM** Elevator Monitor | [B-EM-CPP](https://github.com/chipkin/BACnetProfileExample-B-EM-CPP) | — |
+| **B-EC** Elevator Controller | [B-EC-CPP](https://github.com/chipkin/BACnetProfileExample-B-EC-CPP) | — |
+| **B-AEC** Advanced Elevator Controller | [B-AEC-CPP](https://github.com/chipkin/BACnetProfileExample-B-AEC-CPP) | — |
### Authentication and authorization (Annex L.14)
-| Profile | Example | Required BIBBs (services) |
+| Profile | C++ | Node.js |
|---|---|---|
-| **B-AS** Authorization Server | [B-AS-CPP](https://github.com/chipkin/BACnetProfileExample-B-AS-CPP) ✅ | ✅ DS-RP-B · ✅ DM-DDB-B · ✅ DM-DOB-B · ✅ DM-DCC-B · ☐ AA-AS-B ([cas-bacnet-stack#2043](https://github.com/chipkin/cas-bacnet-stack/issues/2043)) |
+| **B-AS** Authorization Server | [B-AS-CPP](https://github.com/chipkin/BACnetProfileExample-B-AS-CPP) | — |
### Miscellaneous (Annex L.7, combinable with any one family)
-| Profile | Example | Required BIBBs (services) |
+| Profile | C++ | Node.js |
|---|---|---|
-| **B-BBMD** Broadcast Management Device | [B-BBMD-CPP](https://github.com/chipkin/BACnetProfileExample-B-BBMD-CPP) ✅ | ✅ DS-RP-B · ✅ DS-WP-B · ✅ DM-DDB-B · ✅ DM-DOB-B · ✅ DM-DCC-B · ✅ NM-BBMDC-B |
-| **B-ACDC** Access Control Door Controller | [B-ACDC-CPP](https://github.com/chipkin/BACnetProfileExample-B-ACDC-CPP) ✅ | ✅ DS-RP-B · ✅ DS-WP-B · ✅ DS-ACAD-B · ✅ DM-DDB-B · ✅ DM-DOB-B |
-| **B-ACCR** Access Control Credential Reader | [B-ACCR-CPP](https://github.com/chipkin/BACnetProfileExample-B-ACCR-CPP) ✅ | ✅ DS-RP-B · ✅ DS-WP-B · ✅ DS-COV-B · ✅ DS-ACCDI-B · ✅ DM-DDB-B · ✅ DM-DOB-B |
-| **B-RTR** Router | [B-RTR-CPP](https://github.com/chipkin/BACnetProfileExample-B-RTR-CPP) ✅ | ✅ DS-RP-B · ✅ DS-WP-B · ✅ DM-DDB-A · ✅ DM-DOB-B · ☐ DM-LM-B · ✅ NM-RC-B |
-| **B-GW** Gateway | [B-GW-CPP](https://github.com/chipkin/BACnetProfileExample-B-GW-CPP) ✅ | ✅ DS-RP-B · ✅ DS-WP-B · ✅ DM-DDB-B · ✅ DM-DOB-B · ✅ DM-DCC-B · ✅ GW-EO-B / GW-VN-B |
-| **B-DAP** Device Address Proxy | [B-DAP-CPP](https://github.com/chipkin/BACnetProfileExample-B-DAP-CPP) ✅ | ✅ DS-RP-B · ✅ DS-WP-B · ✅ DM-DDB-B · ✅ DM-DOB-B · ✅ DM-DAB-B |
-| **B-SCHUB** BACnet/SC Hub | [B-SCHUB-CPP](https://github.com/chipkin/BACnetProfileExample-B-SCHUB-CPP) ✅ | ✅ DS-RP-B · ✅ DM-DDB-B · ✅ DM-DOB-B · ✅ DM-DCC-B · ✅ NM-SCH-B |
-| **B-GENERAL** General device (Annex L.8) | *(satisfied by every example above)* | ✅ DS-RP-B · ✅ DM-DDB-B · ✅ DM-DOB-B |
-
-### Operator interfaces and workstations (Annex L.1–L.3, L.9–L.10, L.12) — client-side profiles
-
-| Profile | Example | Required BIBBs (services) |
-|---|---|---|
-| **B-OD** Operator Display | [B-OD-CPP](https://github.com/chipkin/BACnetProfileExample-B-OD-CPP) ✅ | ✅ DS-RP-A · ✅ DS-RP-B · ✅ DS-WP-A · ✅ DS-V-A · ✅ DS-M-A · ✅ AE-N-A · ✅ AE-VN-A · ✅ DM-DDB-A · ✅ DM-DDB-B · ✅ DM-DOB-B |
-| **B-OWS** Operator Workstation | planned | ✅ DS-RP-A · ✅ DS-RP-B · ✅ DS-WP-A · ✅ DS-V-A · ✅ DS-M-A · ✅ AE-N-A · ✅ AE-ACK-A · ✅ AE-AS-A · ✅ AE-VM-A · ✅ AE-VN-A · ✅ SCHED-VM-A · ✅ T-V-A · ✅ DM-DDB-A · ✅ DM-DDB-B · ✅ DM-DOB-B · ✅ DM-MTS-A |
-| **B-AWS** Advanced Operator Workstation | planned | ✅ DS-RP-A · ✅ DS-RP-B · ✅ DS-RPM-A · ✅ DS-WP-A · ✅ DS-WPM-A · ✅ DS-AV-A · ✅ DS-AM-A · ✅ AE-N-A · ✅ AE-ACK-A · ✅ AE-AS-A · ✅ AE-AVM-A · ✅ AE-AVN-A · ✅ AE-ELVM-A · ✅ SCHED-AVM-A · ✅ T-AVM-A · ✅ DM-DDB-A · ✅ DM-DDB-B · ✅ DM-ANM-A · ✅ DM-ADM-A · ✅ DM-DOB-B · ✅ DM-DCC-A · ✅ DM-MTS-A · ✅ DM-OCD-A · ✅ DM-RD-A · ✅ DM-BR-A · ✅ DM-DDA-A · ✅ NM-CC-A · ✅ AR-AVM-A |
-| **B-XAWS** Extended Advanced Operator Workstation | planned | ✅ union of B-AWS + B-AACWS + B-ALWS + B-AEWS |
-| **B-LSAP** Life Safety Annunciator Panel | planned | ✅ DS-RP-A · ✅ DS-RP-B · ✅ DS-WP-A · ✅ DS-LSV-A · ✅ AE-N-A · ✅ AE-LS-A · ✅ AE-ACK-A · ✅ AE-LSVN-A |
-| **B-LSWS** Life Safety Workstation | planned | ✅ DS-RP-A · ✅ DS-RP-B · ✅ DS-RPM-A · ✅ DS-WP-A · ✅ DS-WPM-A · ✅ DS-LSV-A · ✅ DS-LSM-A · ✅ AE-N-A · ✅ AE-LS-A · ✅ AE-ACK-A · ✅ AE-AS-A · ✅ AE-LSVM-A · ✅ AE-LSAVN-A · ✅ AE-ELV-A · ✅ SCHED-VM-A · ✅ T-V-A · ✅ DM-DDB-A · ✅ DM-DDB-B · ✅ DM-ANM-A · ✅ DM-ADM-A · ✅ DM-DOB-B · ✅ DM-DCC-A · ✅ DM-MTS-A · ✅ DM-OCD-A · ✅ DM-RD-A · ✅ DM-BR-A |
-| **B-ALSWS** Advanced Life Safety Workstation | planned | ✅ DS-RP-A · ✅ DS-RP-B · ✅ DS-RPM-A · ✅ DS-WP-A · ✅ DS-WPM-A · ✅ DS-LSAV-A · ✅ DS-LSAM-A · ✅ AE-N-A · ✅ AE-LS-A · ✅ AE-ACK-A · ✅ AE-AS-A · ✅ AE-LSAVM-A · ✅ AE-LSAVN-A · ✅ AE-ELVM-A · ✅ SCHED-AVM-A · ✅ T-AVM-A · ✅ DM-DDB-A · ✅ DM-DDB-B · ✅ DM-ANM-A · ✅ DM-ADM-A · ✅ DM-DOB-B · ✅ DM-DCC-A · ✅ DM-MTS-A · ✅ DM-OCD-A · ✅ DM-RD-A · ✅ DM-BR-A · ✅ AR-AVM-A |
-| **B-ACSD** Access Control Security Display | planned | ✅ DS-RP-A · ✅ DS-RP-B · ✅ DS-RPM-A · ✅ DS-WP-A · ✅ DS-WPM-A · ✅ DS-ACV-A · ✅ DS-ACM-A · ✅ AE-N-A · ✅ AE-AC-A · ✅ AE-ACK-A · ✅ AE-AS-A · ✅ AE-ACAVN-A · ✅ AE-ELV-A · ✅ SCHED-VM-A · ✅ DM-DDB-A · ✅ DM-DDB-B · ✅ DM-DOB-B · ✅ DM-MTS-A |
-| **B-ACWS** Access Control Workstation | planned | ✅ DS-RP-A · ✅ DS-RP-B · ✅ DS-RPM-A · ✅ DS-WP-A · ✅ DS-WPM-A · ✅ DS-ACAV-A · ✅ DS-ACM-A · ✅ DS-ACUC-A · ✅ AE-N-A · ✅ AE-AC-A · ✅ AE-ACK-A · ✅ AE-AS-A · ✅ AE-ACVM-A · ✅ AE-ACAVN-A · ✅ AE-ELV-A · ✅ SCHED-VM-A · ✅ DM-DDB-A · ✅ DM-DDB-B · ✅ DM-ANM-A · ✅ DM-ADM-A · ✅ DM-DOB-B · ✅ DM-DCC-A · ✅ DM-MTS-A · ✅ DM-OCD-A · ✅ DM-RD-A · ✅ DM-BR-A |
-| **B-AACWS** Advanced Access Control Workstation | planned | ✅ DS-RP-A · ✅ DS-RP-B · ✅ DS-RPM-A · ✅ DS-WP-A · ✅ DS-WPM-A · ✅ DS-ACAV-A · ✅ DS-ACAM-A · ✅ DS-ACUC-A · ✅ DS-ACSC-A · ✅ AE-N-A · ✅ AE-AC-A · ✅ AE-ACK-A · ✅ AE-AS-A · ✅ AE-ACAVM-A · ✅ AE-ACAVN-A · ✅ AE-ELVM-A · ✅ SCHED-AVM-A · ✅ DM-DDB-A · ✅ DM-DDB-B · ✅ DM-ANM-A · ✅ DM-ADM-A · ✅ DM-DOB-B · ✅ DM-DCC-A · ✅ DM-MTS-A · ✅ DM-OCD-A · ✅ DM-RD-A · ✅ DM-BR-A · ✅ AR-AVM-A |
-| **B-LOD** Lighting Operator Display | planned | ✅ DS-RP-A · ✅ DS-RP-B · ✅ DS-WP-A · ✅ DS-LV-A · ✅ DS-WG-A · ✅ DS-ALO-A · ✅ DM-DDB-A · ✅ DM-DDB-B · ✅ DM-DOB-B |
-| **B-ALWS** Advanced Lighting Workstation | planned | ✅ DS-RP-A · ✅ DS-RP-B · ✅ DS-RPM-A · ✅ DS-WP-A · ✅ DS-WPM-A · ✅ DS-LAV-A · ✅ DS-LAM-A · ✅ DS-WG-A · ✅ DS-ALO-A · ✅ AE-N-A · ✅ AE-ACK-A · ✅ AE-AS-A · ✅ AE-AVM-A · ✅ AE-AVN-A · ✅ AE-ELVM-A · ✅ SCHED-AVM-A · ✅ T-AVM-A · ✅ DM-DDB-A · ✅ DM-DDB-B · ✅ DM-ANM-A · ✅ DM-ADM-A · ✅ DM-DOB-B · ✅ DM-DCC-A · ✅ DM-MTS-A · ✅ DM-OCD-A · ✅ DM-RD-A · ✅ DM-BR-A |
-| **B-LCS** Lighting Control Station | planned | ✅ DS-RP-A · ✅ DS-RP-B · ✅ DS-WP-A · ✅ DS-LO-A · ✅ DM-DDB-A · ✅ DM-DDB-B · ✅ DM-DOB-B · ✅ DM-DCC-B · ✅ DM-TS-B / DM-UTC-B |
-| **B-ALCS** Advanced Lighting Control Station | planned | ✅ DS-RP-A · ✅ DS-RP-B · ✅ DS-RPM-A · ✅ DS-WP-A · ✅ DS-WPM-A · ✅ DS-WG-A · ✅ DS-ALO-A · ✅ SCHED-E-B · ✅ DM-DDB-A · ✅ DM-DDB-B · ✅ DM-DOB-B · ✅ DM-DCC-B · ✅ DM-TS-B / DM-UTC-B |
-| **B-ED** Elevator Display | planned | ✅ DS-RP-A · ✅ DS-RP-B · ✅ DS-WP-A · ✅ DS-EV-A · ✅ AE-N-A · ✅ AE-ACK-A · ✅ AE-EVN-A · ✅ DM-DDB-A · ✅ DM-DDB-B · ✅ DM-DOB-B |
-| **B-EWS** Elevator Workstation | planned | ✅ DS-RP-A · ✅ DS-RP-B · ✅ DS-RPM-A · ✅ DS-WP-A · ✅ DS-WPM-A · ✅ DS-COVM-A · ✅ DS-EV-A · ✅ DS-EM-A · ✅ AE-N-A · ✅ AE-ACK-A · ✅ AE-AS-A · ✅ AE-EVM-A · ✅ AE-EAVN-A · ✅ SCHED-VM-A · ✅ T-V-A · ✅ DM-DDB-A · ✅ DM-DDB-B · ✅ DM-ANM-A · ✅ DM-ADM-A · ✅ DM-DOB-B · ✅ DM-DCC-A · ✅ DM-MTS-A |
-| **B-AEWS** Advanced Elevator Workstation | planned | ✅ DS-RP-A · ✅ DS-RP-B · ✅ DS-RPM-A · ✅ DS-WP-A · ✅ DS-WPM-A · ✅ DS-COVM-A · ✅ DS-EAV-A · ✅ DS-EAM-A · ✅ AE-N-A · ✅ AE-ACK-A · ✅ AE-AS-A · ✅ AE-EAVM-A · ✅ AE-EAVN-A · ✅ AE-ELVM-A · ✅ SCHED-AVM-A · ✅ T-AVM-A · ✅ DM-DDB-A · ✅ DM-DDB-B · ✅ DM-ANM-A · ✅ DM-ADM-A · ✅ DM-DOB-B · ✅ DM-DCC-A · ✅ DM-MTS-A · ✅ DM-OCD-A · ✅ DM-RD-A · ✅ DM-BR-A |
-
-Profile definitions: ANSI/ASHRAE 135-2024 Annex L. BIBB definitions: Annex K. Get the stack: .
-
+| **B-BBMD** Broadcast Management Device | [B-BBMD-CPP](https://github.com/chipkin/BACnetProfileExample-B-BBMD-CPP) | — |
+| **B-ACDC** Access Control Door Controller | [B-ACDC-CPP](https://github.com/chipkin/BACnetProfileExample-B-ACDC-CPP) | — |
+| **B-ACCR** Access Control Credential Reader | [B-ACCR-CPP](https://github.com/chipkin/BACnetProfileExample-B-ACCR-CPP) | — |
+| **B-RTR** Router | [B-RTR-CPP](https://github.com/chipkin/BACnetProfileExample-B-RTR-CPP) | — |
+| **B-GW** Gateway | [B-GW-CPP](https://github.com/chipkin/BACnetProfileExample-B-GW-CPP) | — |
+| **B-DAP** Device Address Proxy | [B-DAP-CPP](https://github.com/chipkin/BACnetProfileExample-B-DAP-CPP) | — |
+| **B-SCHUB** BACnet/SC Hub | [B-SCHUB-CPP](https://github.com/chipkin/BACnetProfileExample-B-SCHUB-CPP) | — |
+| **B-GENERAL** General device (Annex L.8) | *(satisfied by every example above)* | — |
-## Objects and properties
-
-
-Every object this example creates, and every REQUIRED property of each (per ANSI/ASHRAE 135-2024 clause 12 and the stack's `docs/property-profile-reference.md`), plus the optional properties the example turns on. **Served by** says who answers a ReadProperty: the **stack** generates it, or the **app** serves it from a `GetProperty*` callback in `main.cpp`. A ⚠ row is a required property the app does not serve and the stack would fill with a default - that is a defect, not a feature.
-
-### Device 389018 "Rainbow" - vendor 389 (Chipkin Automation Systems); instance configurable with --deviceID. The properties in 'accepted' are not served from a GetProperty callback because the stack itself is the source of truth for them - Protocol_Revision/Protocol_Version are stack build constants, Protocol_Services_Supported/Protocol_Object_Types_Supported are computed from the BACnetStack_SetServiceEnabled/AddObject calls this example already makes, Object_List and Device_Address_Binding are live stack-maintained tables, System_Status/Database_Revision/Max_APDU_Length_Accepted/Segmentation_Supported/APDU_Timeout/Number_Of_APDU_Retries are the stack's own configuration defaults for a device this example does not override
-
-| Property | Datatype | Served by | Writable |
-|---|---|---|:---:|
-| Object_Identifier | BACnetObjectIdentifier | stack | no |
-| Object_Name | CharacterString | app | no |
-| Object_Type | BACnetObjectType | stack | no |
-| System_Status | BACnetDeviceStatus | stack default, accepted (Generic Enumerated default: `0`) | no |
-| Vendor_Name | CharacterString | app | no |
-| Vendor_Identifier | Unsigned16 | app | no |
-| Model_Name | CharacterString | app | no |
-| Firmware_Revision | CharacterString | app | no |
-| Application_Software_Version | CharacterString | app | no |
-| Description *(optional, enabled)* | CharacterString | app | no |
-| Protocol_Version | Unsigned | stack default, accepted (`BACNET_PROTOCOL_VERSION`) | no |
-| Protocol_Revision | Unsigned | stack default, accepted (`BACNET_PROTOCOL_REVISION`) | no |
-| Protocol_Services_Supported | BACnetServicesSupported | stack default, accepted (computed from which services are enabled) | no |
-| Protocol_Object_Types_Supported | BACnetObjectTypesSupported | stack default, accepted (computed from which object types are supported) | no |
-| Object_List | BACnetARRAY[N] of BACnetObjectIdentifier | stack default, accepted (None known - a read fails with `unknown-property` or an empt) | no |
-| Max_APDU_Length_Accepted | Unsigned | stack default, accepted (`CAS_BACNET_DEVICE_DEFAULT_MAX_APDU_LENGTH_ACCEPTED`) | no |
-| Segmentation_Supported | BACnetSegmentation | stack default, accepted (`BACnetSegmentation::noSegmentation`) | no |
-| APDU_Timeout | Unsigned | stack default, accepted (`CAS_BACNET_DEVICE_DEFAULT_APDU_TIMEOUT`) | no |
-| Number_Of_APDU_Retries | Unsigned | stack default, accepted (`CAS_BACNET_DEVICE_DEFAULT_NUMBER_OF_APDU_RETRIES`) | no |
-| Device_Address_Binding | BACnetLIST of BACnetAddressBinding | stack default, accepted (the live Device_Address_Binding (DAB) table) | no |
-| Database_Revision | Unsigned | stack default, accepted (Generic UnsignedInteger default: `0`) | no |
-
-### Analog Input 1 "Bronze" - REAL, degrees Celsius; starts at 21.5; read-only
-
-| Property | Datatype | Served by | Writable |
-|---|---|---|:---:|
-| Object_Identifier | BACnetObjectIdentifier | stack | no |
-| Object_Name | CharacterString | app | no |
-| Object_Type | BACnetObjectType | stack | no |
-| Present_Value | Real | app | no |
-| Status_Flags | BACnetStatusFlags | stack | no |
-| Event_State | BACnetEventState | stack default, accepted (Generic Enumerated default: `0`) | no |
-| Out_Of_Service | Boolean | app | no |
-| Units | BACnetEngineeringUnits | app | no |
-
-### Binary Input 1 "Emerald" - starts inactive; read-only
-
-| Property | Datatype | Served by | Writable |
-|---|---|---|:---:|
-| Object_Identifier | BACnetObjectIdentifier | stack | no |
-| Object_Name | CharacterString | app | no |
-| Object_Type | BACnetObjectType | stack | no |
-| Present_Value | BACnetBinaryPV | app | no |
-| Status_Flags | BACnetStatusFlags | stack | no |
-| Event_State | BACnetEventState | stack default, accepted (Generic Enumerated default: `0`) | no |
-| Out_Of_Service | Boolean | app | no |
-| Polarity | BACnetPolarity | app | no |
-
-### Multi-state Input 1 "Hot Pink" - state 1 of 3: On, Off, Auto; read-only
-
-| Property | Datatype | Served by | Writable |
-|---|---|---|:---:|
-| Object_Identifier | BACnetObjectIdentifier | stack | no |
-| Object_Name | CharacterString | app | no |
-| Object_Type | BACnetObjectType | stack | no |
-| Present_Value | Unsigned | app | no |
-| Status_Flags | BACnetStatusFlags | stack | no |
-| Event_State | BACnetEventState | stack default, accepted (Generic Enumerated default: `0`) | no |
-| Out_Of_Service | Boolean | app | no |
-| Number_Of_States | Unsigned | app | no |
-| State_Text *(optional, enabled)* | BACnetARRAY[N] of CharacterString | app | no |
-
-### Analog Output 1 "Chartreuse" - REAL setpoint, default 20.0 C; commandable via a 16-slot Priority_Array (WriteProperty, DS-WP-B) - Present_Value/Priority_Array/Current_Command_Priority are resolved by the stack from the slots the app serves through GetPropertyReal/GetPropertyBool
-
-| Property | Datatype | Served by | Writable |
-|---|---|---|:---:|
-| Object_Identifier | BACnetObjectIdentifier | stack | no |
-| Object_Name | CharacterString | app | no |
-| Object_Type | BACnetObjectType | stack | no |
-| Present_Value | Real | stack | yes |
-| Status_Flags | BACnetStatusFlags | stack | no |
-| Event_State | BACnetEventState | stack default, accepted (Generic Enumerated default: `0`) | no |
-| Out_Of_Service | Boolean | app | no |
-| Units | BACnetEngineeringUnits | app | no |
-| Priority_Array | BACnetARRAY[16] of BACnetOptionalReal | stack | no |
-| Relinquish_Default | Real | app | no |
-| Current_Command_Priority | BACnetOptionalUnsigned | stack | no |
-
-### Binary Output 1 "Fuchsia" - active/inactive, default inactive; commandable via a 16-slot Priority_Array (WriteProperty, DS-WP-B) - Present_Value/Priority_Array/Current_Command_Priority are resolved by the stack from the slots the app serves through GetPropertyEnumerated/GetPropertyBool
-
-| Property | Datatype | Served by | Writable |
-|---|---|---|:---:|
-| Object_Identifier | BACnetObjectIdentifier | stack | no |
-| Object_Name | CharacterString | app | no |
-| Object_Type | BACnetObjectType | stack | no |
-| Present_Value | BACnetBinaryPV | stack | yes |
-| Status_Flags | BACnetStatusFlags | stack | no |
-| Event_State | BACnetEventState | stack default, accepted (Generic Enumerated default: `0`) | no |
-| Out_Of_Service | Boolean | app | no |
-| Polarity | BACnetPolarity | app | no |
-| Priority_Array | BACnetARRAY[16] of BACnetOptionalBinaryPV | stack | no |
-| Relinquish_Default | BACnetBinaryPV | app | no |
-| Current_Command_Priority | BACnetOptionalUnsigned | stack | no |
-
-### Multi-state Output 1 "Indigo" - state 1 of 3, default state 1; commandable via a 16-slot Priority_Array (WriteProperty, DS-WP-B) - Present_Value/Priority_Array/Current_Command_Priority are resolved by the stack from the slots the app serves through GetPropertyUnsignedInteger/GetPropertyBool
-
-| Property | Datatype | Served by | Writable |
-|---|---|---|:---:|
-| Object_Identifier | BACnetObjectIdentifier | stack | no |
-| Object_Name | CharacterString | app | no |
-| Object_Type | BACnetObjectType | stack | no |
-| Present_Value | Unsigned | stack | yes |
-| Status_Flags | BACnetStatusFlags | stack | no |
-| Event_State | BACnetEventState | stack default, accepted (Generic Enumerated default: `0`) | no |
-| Out_Of_Service | Boolean | app | no |
-| Number_Of_States | Unsigned | app | no |
-| Priority_Array | BACnetARRAY[16] of BACnetOptionalUnsigned | stack | no |
-| Relinquish_Default | Unsigned | app | no |
-| Current_Command_Priority | BACnetOptionalUnsigned | stack | no |
-
-### Network Port 1 "Vermilion" - BACnet/IP, network 1, configured (F-MULTIPORT/F-ROUTER port A). Network_Type/Protocol_Level are set from BACnetStack_AddNetworkPortObject()'s arguments at start-up (as is Network_Number = 1 / Network_Number_Quality = configured, verified readable on the wire; not listed as a separate row here because the stack's own property-profile-reference.md does not carry those two properties for the type-agnostic 'Network Port' heading this generator reads). Changes_Pending is likewise computed and answered natively. Reliability has no fault condition this example detects, so it is accepted at the generic default (normal). Routing_Table is deliberately NOT enabled - verified on the wire that the pinned stack cannot serve it outside a test-tool build (TODO.md, stack issue #2038).
-
-| Property | Datatype | Served by | Writable |
-|---|---|---|:---:|
-| Object_Identifier | BACnetObjectIdentifier | stack | no |
-| Object_Name | CharacterString | app | no |
-| Object_Type | BACnetObjectType | stack | no |
-| Status_Flags | BACnetStatusFlags | stack | no |
-| Reliability | BACnetReliability | stack default, accepted (Generic Enumerated default: `0`) | no |
-| Out_Of_Service | Boolean | app | no |
-| Network_Type | BACnetNetworkType | app | no |
-| Protocol_Level | BACnetProtocolLevel | app | no |
-| Changes_Pending | Boolean | app | no |
-
-### Network Port 2 "Vermilion 2" - BACnet/IP, network 2, configured (F-MULTIPORT/F-ROUTER port B). Same notes as Network Port 1 (Vermilion) apply, with Network_Number = 2. Physically routes to network 1 via BACnetStack_AddRouterPort/SetRouterEnabled, but see TODO.md and stack issue #2037: this stack pin cannot forward NPDUs between two ports of the same network type, so cross-network traffic is not verified end-to-end even though both ports individually answer ReadProperty/WriteProperty correctly.
-
-| Property | Datatype | Served by | Writable |
-|---|---|---|:---:|
-| Object_Identifier | BACnetObjectIdentifier | stack | no |
-| Object_Name | CharacterString | app | no |
-| Object_Type | BACnetObjectType | stack | no |
-| Status_Flags | BACnetStatusFlags | stack | no |
-| Reliability | BACnetReliability | stack default, accepted (Generic Enumerated default: `0`) | no |
-| Out_Of_Service | Boolean | app | no |
-| Network_Type | BACnetNetworkType | app | no |
-| Protocol_Level | BACnetProtocolLevel | app | no |
-| Changes_Pending | Boolean | app | no |
-
-
+### Operator interfaces and workstations (Annex L.1–L.3, L.9–L.10, L.12)
-## Footprint
+Client-side profiles.
-Measured from the [v1.0.0](https://github.com/chipkin/BACnetProfileExample-B-RTR-CPP/releases/tag/v1.0.0) release CI.
-
-| Metric | Windows | Linux |
+| Profile | C++ | Node.js |
|---|---|---|
-| Binary size | 3,263,488 bytes | 49,128 bytes |
-| SHA-256 (prefix) | `688a49a7a1ca5fb2` | `8842ac2760185932` |
-| Start-up time to `ready` | 200 ms | 109 ms |
-| Stack commit | `abd4cee1` (6.0.21) | `abd4cee1` (6.0.21) |
-| Link mode | STATIC | STATIC |
-| Compiler | Visual Studio 17 2022 | `/usr/bin/c++` |
+| **B-OD** Operator Display | [B-OD-CPP](https://github.com/chipkin/BACnetProfileExample-B-OD-CPP) | — |
+| **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. Profile definitions: ANSI/ASHRAE 135-2024 Annex L. BIBB definitions: Annex K. Get the stack: .
+
-
+## References
+
+- **ANSI/ASHRAE Standard 135** (BACnet) - the protocol standard. Object model
+ (Clause 12), services (Clause 15), routers (Clause 6.6), BACnet/IP (Annex J),
+ device profiles (Annex L). Purchase / preview via the
+ [ASHRAE store](https://www.ashrae.org/technical-resources/standards-and-guidelines).
+- **What is BACnet?** - Chipkin's introduction:
+ .
+- **CAS BACnet Stack** - product page and documentation:
+ .
+- **A BACnet client for testing this device** - [bacpypes3](https://github.com/JoelBender/bacpypes3)
+ or [YABE](https://sourceforge.net/projects/yetanotherbacnetexplorer/).
+- **Shared helper used by this example** - [`common/README.md`](common/README.md).
+
+See also [TUTORIAL.md](TUTORIAL.md), [docs/PICS.md](docs/PICS.md),
+[TODO.md](TODO.md), [CHANGELOG.md](CHANGELOG.md), and [AGENTS.md](AGENTS.md).
diff --git a/TUTORIAL.md b/TUTORIAL.md
new file mode 100644
index 0000000..0060818
--- /dev/null
+++ b/TUTORIAL.md
@@ -0,0 +1,312 @@
+# Tutorial - extending and reviewing the B-RTR example
+
+[README.md](README.md) says what this example *is*. This document is the
+*how*: how to extend it into your own router, what each object type needs the
+application to serve, how the two Network Ports and the router configuration
+fit together, how to review the result for conformance, and what goes wrong
+when you get it subtly right.
+
+Read this once before you start changing `main.cpp`. The most expensive
+mistake in this example is silent, and the section it lives in is
+[Add a second analog input](#add-a-second-analog-input).
+
+- [Extending the example](#extending-the-example)
+- [F-MULTIPORT: two Network Ports, two UDP sockets](#f-multiport-two-network-ports-two-udp-sockets)
+- [F-ROUTER: router configuration](#f-router-router-configuration)
+- [What each object type needs you to serve](#what-each-object-type-needs-you-to-serve)
+- [Who serves what: the application or the stack?](#who-serves-what-the-application-or-the-stack)
+- [Reviewing your device](#reviewing-your-device)
+- [Troubleshooting](#troubleshooting)
+
+## Extending the example
+
+The example is intentionally small so it's easy to change.
+
+**Change an object's value or name** - edit the constants / callbacks in
+`main.cpp` (e.g. the initial value of `g_analogInput1Value`, or the
+`"Bronze"` string in `GetPropertyCharString`).
+
+**Change the device identity before you ship** - vendor ID, vendor name,
+model name, description, firmware revision, device name and the two network
+numbers are all in the `CHANGE ALL OF THIS BEFORE YOU SHIP` block at the top
+of `main.cpp`, with a per-field comment on each saying what to change it to.
+That block is the authoritative checklist; it is in the source rather than
+here so it cannot be skipped by someone who only reads the code.
+
+### Add a second analog input
+
+Read this whole recipe before starting — the last step is the one that is
+easy to miss and the one BTL will fail you for.
+
+> **Why there are four edits, not three — and why skipping one is SILENT.**
+> Most of the `GetProperty*` callbacks match on **both** object type *and*
+> instance (`objectInstance == ANALOG_INPUT_INSTANCE`), so a new instance
+> falls through every one of them. `GetPropertyBool` is the exception for
+> `Out_Of_Service`: it matches on type only, so it works for a new instance
+> for free.
+>
+> Here is the part that matters, and it is the opposite of what most people
+> assume: falling through a callback does **not** reliably produce an error.
+> The stack errors only for the few properties it refuses to invent -
+> `Present_Value`, `Number_Of_States`, `Relinquish_Default`, and a Network
+> Port's `APDU_Length`. For everything else it **silently substitutes a
+> default**:
+>
+> | Property | If you forget to serve it | Loud? |
+> |---|---|:--:|
+> | `Present_Value` | Error (`read-access-denied`) | yes |
+> | `Object_Name` | reads back as the string **`"undefined"`** | **no** |
+> | `Units` | reads back as **`no-units` (95)** | **no** |
+> | `Out_Of_Service` | served on type alone — works by accident | n/a |
+>
+> **Doesn't the `errorCode` out-parameter fix this?** Only if you use it, and
+> only where it is right to. Each `GetProperty*` callback ends with a
+> `uint32_t* errorCode` that the stack presets to `success` and reads only
+> when you return `false`, so you *can* turn any decline into a chosen BACnet
+> error. But ending every callback with `*errorCode = unknown-property`
+> breaks the device: the stack's decline-and-fabricate path is what answers
+> required properties an application is not expected to serve — the Device's
+> `Max_APDU_Length_Accepted`, `APDU_Timeout` and `Number_Of_APDU_Retries`
+> among them. Name an error on the catch-all and those start failing instead
+> of answering. Set `errorCode` only where *this device* knows the read is
+> wrong; `main.cpp` does it in exactly one place, `State_Text` with an
+> out-of-range array index.
+>
+> It is worse than "wrong value": the object's `Property_List` **still
+> advertises `Units` (117)**. So the object actively claims to have the
+> property, and then answers with a default. Nothing on the wire says you
+> forgot anything.
+>
+> So a half-added object does not look broken; it looks **healthy**. Add two
+> of them and both report `Object_Name "undefined"` — duplicate object names
+> inside one device, which is a spec violation and a hard BTL failure that
+> every scan tool will render as a perfectly good object. **"It scanned OK"
+> is exactly the failure mode, not evidence against it.**
+
+```cpp
+// 1) a new instance number (in section 1).
+// Naming: a second object of a type is " 2" - so Analog Input 2 is
+// "Bronze 2", NOT a new colour. Each object TYPE owns one colour series-wide.
+static const uint32_t ANALOG_INPUT_2_INSTANCE = 2; // "Bronze 2"
+static float g_analogInput2Value = 23.1f; // its live value
+
+// 2) add the object (in main, next to the other BACnetStack_AddObject calls).
+// Check the return, like every other stack call in this file.
+if (!BACnetStack_AddObject(g_deviceInstance, OBJECT_TYPE_ANALOG_INPUT, ANALOG_INPUT_2_INSTANCE)) {
+ printf("Error: Failed to add Analog Input 2 (Bronze 2).\n");
+ return 1;
+}
+
+// 3) serve its Present_Value + Object_Name:
+// GetPropertyReal: AI/2 + Present_Value -> *value = g_analogInput2Value;
+// GetPropertyCharString: AI/2 + Object_Name -> "Bronze 2"
+
+// 4) DO NOT SKIP: serve its Units, in GetPropertyEnumerated.
+// Units is a REQUIRED property of an Analog Input. The existing check reads
+// `objectInstance == ANALOG_INPUT_INSTANCE`, which is instance 1 - so without
+// this, reading Analog Input 2's Units returns an ERROR and the object is
+// NON-CONFORMANT. It will still appear in the Object_List and its
+// Present_Value will read back perfectly, so the device looks healthy right
+// up until BTL certification.
+// GetPropertyEnumerated: AI/2 + Units -> *value = ENGINEERING_UNITS_DEGREES_CELSIUS;
+```
+
+Then re-run the README's Verify steps **against Analog Input 2**, not just
+Analog Input 1 — read every required property, on both networks, and **diff
+it against Analog Input 1**. Any property that comes back `"undefined"`,
+`no-units`, or `0` where object 1 returns something real is a step you
+missed. Because the failure is silent (see the table above), this diff is
+the only thing that catches it.
+
+## F-MULTIPORT: two Network Ports, two UDP sockets
+
+Most examples in this series own exactly one Network Port and one UDP socket.
+This example is the series' canonical example of owning two:
+`CASExampleHelper::SetupUDP(port, networkPortInstance)` binds an additional
+socket keyed to a specific Network Port instance, and the shared
+receive/send callbacks (`common/CASExampleHelper.cpp`) dispatch on that
+instance internally - `main.cpp` just calls `SetupUDP` twice and
+`SendIAm(deviceInstance, networkPortInstance)` once per port. See
+`common/CHANGELOG.md`'s 2.3.0 entry and the "WHY THIS IS SAFE FOR
+SINGLE-PORT EXAMPLES" comment in `CASExampleHelper.cpp` for why this did not
+change any single-port example's behaviour.
+
+Both ports run on **one host** and are distinguished purely by **UDP port
+number** (`--port` / `--port2`, default `--port + 1`) - they report the same
+`IP_Address`/`IP_Subnet_Mask` and different `BACnet_IP_UDP_Port` /
+`Network_Number`.
+
+Adding a third port to a device you build from this example follows the same
+pattern: a third `NETWORK_PORT_x_INSTANCE`, a third `SetupUDP(port,
+instance)` call, a third `SendIAm(deviceInstance, instance)` call, and (if it
+should also route) a third `BACnetStack_AddRouterPort`.
+
+## F-ROUTER: router configuration
+
+At start-up, `main.cpp`:
+
+1. `BACnetStack_AddRouterPort` once per Network Port, registering the network
+ it directly connects (network 1 on Vermilion, network 2 on Vermilion 2).
+2. `BACnetStack_AddRouterRoute` for one DEMONSTRATION-ONLY network (3), one
+ hop beyond port 2, with no next-hop MAC configured - this exercises the
+ API; no device in this example series actually sits on network 3 (see
+ `main.cpp`'s `DEMO_REMOTE_NETWORK` comment).
+3. `BACnetStack_SetRouterEnabled(deviceInstance, true)` - routing is opt-in
+ even with ports configured.
+4. Broadcasts **I-Am-Router-To-Network** on both ports (`networkNumbers =
+ NULL`, announcing this router's full configured routing table per the
+ stack's `#1014` fix), and again whenever the **`r`** key is pressed.
+5. Broadcasts **Network-Number-Is** on both ports, announcing each network's
+ own (configured) number.
+6. Broadcasts **Who-Is-Router-To-Network** (unrestricted) on both ports, the
+ polite router start-up probe for any other router already present (none
+ is expected to answer in this example).
+
+`BACnetStack_AddRouterPort`'s `networkType` parameter is the stack's own
+internal datalink enum (`BACnetPacket::NetworkType_IP = 0`), which is a
+**different enumeration** from the Network Port object's `Network_Type`
+property (`NETWORK_PORT_NETWORK_TYPE_IPV4 = 5`, used with
+`AddNetworkPortObject`) - passing the wrong one fails with "unknown network
+type"; see the `ROUTER_NETWORK_TYPE_IP` comment in `main.cpp`.
+
+> **The pinned CAS BACnet Stack genuinely accepts this router's
+> configuration** (`AddRouterPort`, `AddRouterRoute`, `SetRouterEnabled` all
+> succeed; `Network_Number` and `Network_Number_Quality` read back correctly
+> on both ports; I-Am-Router-To-Network / Who-Is-Router-To-Network /
+> Network-Number-Is all send) - but it does **NOT** forward NPDUs between the
+> two ports, because both are BACnet/IP and the stack holds only one
+> internal datalink instance per network type at this pin (a scope gap left
+> by stack issue #304, which fixed this only for BACnet/SC; see `TODO.md`
+> and [stack issue #2037](https://github.com/chipkin/cas-bacnet-stack/issues/2037)).
+> A client reachable only through the OTHER port is **not** actually routed
+> to. Each port answers ReadProperty/WriteProperty directly and correctly on
+> its own network - what does not work is forwarding traffic **between** the
+> two networks. Do not remove or soften this warning without first
+> re-verifying the stack gap is closed.
+
+**`Routing_Table` (cl. 12.56.62) is deliberately NOT enabled or claimed.**
+Verified on the wire: enabling it and reading it aborts the request, because
+the pinned stack's `BACnetBusinessLogic::UseCallbackGetList` has no case for
+Network Port/Routing_Table outside a test-tool build. See `TODO.md` and
+[stack issue #2038](https://github.com/chipkin/cas-bacnet-stack/issues/2038).
+
+**DM-LM-B (AddListElement/RemoveListElement, services 8/9) is not
+implemented.** The B-RTR profile card calls for it, and the stack genuinely
+implements both services end-to-end against a Group object's
+`List_Of_Group_Members` - but only when compiled with
+`STACK_OPTION_DM_LM_LIST_MANIPULATION`, which the customer-facing STATIC
+build of the pinned stack does not define. See `TODO.md` and
+[stack issue #2033](https://github.com/chipkin/cas-bacnet-stack/issues/2033)
+for the full verification trail. Do not add a Group object or claim these
+services until that stack gap is confirmed fixed.
+
+## What each object type needs you to serve
+
+The application must serve every REQUIRED property the stack does not
+generate. It differs per type — this is the checklist, so you do not have to
+infer it:
+
+| Object type | You must serve | Plus |
+|---|---|---|
+| Analog Input | `Present_Value` (Real), `Object_Name`, `Units` | — |
+| Binary Input | `Present_Value` (Enumerated), `Object_Name` | `Polarity` |
+| Multi-State Input | `Present_Value` (Unsigned), `Object_Name` | `Number_Of_States` |
+| Analog Output (commandable) | `Object_Name`, `Units`, `Relinquish_Default` | 16-slot Priority_Array via `GetPropertyReal`/`GetPropertyBool`; `Present_Value` resolved by the stack |
+| Binary Output (commandable) | `Object_Name`, `Polarity`, `Relinquish_Default` | 16-slot Priority_Array via `GetPropertyEnumerated`/`GetPropertyBool`; `Present_Value` resolved by the stack |
+| Multi-State Output (commandable) | `Object_Name`, `Number_Of_States`, `Relinquish_Default` | 16-slot Priority_Array via `GetPropertyUnsignedInteger`/`GetPropertyBool`; `Present_Value` resolved by the stack |
+| Network Port | `Object_Name`, `Network_Type`, `Protocol_Level`, `Changes_Pending`, `IP_Address`/`IP_Subnet_Mask`/`IP_Default_Gateway`, `BACnet_IP_UDP_Port` | one instance per network the device joins |
+
+## Who serves what: the application or the stack?
+
+The single most common question when reading this file is "who answers this
+property?" For Analog Output 1 (the representative commandable object), the
+whole picture:
+
+| Property | Served by | How |
+|---|---|---|
+| `Object_Identifier` | **stack** | generated from the object you added |
+| `Object_Type` | **stack** | generated |
+| `Object_List` | **stack** | generated (Device object) |
+| `Property_List` | **stack** | generated |
+| `Status_Flags` | **stack** | generated |
+| `Event_State` | **stack**, sort of | no intrinsic alarming here, so nothing serves it — it reads `normal` only because `normal` is the enumeration's zero value and the stack substitutes a datatype default. Correct by coincidence, not design. |
+| `Out_Of_Service` | **you** | `GetPropertyBool` — matched on object **type only** |
+| `Present_Value` | **stack** | resolved from the `Priority_Array` slots you serve; **writable** — `SetPropertyReal` stores the slot |
+| `Priority_Array` | **stack**, backed by **you** | `GetPropertyBool` returns whether each slot is set; `SetPropertyReal`/`SetPropertyNull` write/relinquish a slot |
+| `Current_Command_Priority` | **stack** | computed from the Priority_Array slots |
+| `Relinquish_Default` | **you** | `GetPropertyReal` |
+| `Object_Name` | **you** | `GetPropertyCharString` |
+| `Units` | **you** | `GetPropertyEnumerated` |
+
+Every object, not just this one, is in [docs/PICS.md](docs/PICS.md).
+
+Going beyond this (alarming/events, COV, scheduling, trending) means
+implementing a richer profile — see the series table in
+[README.md](README.md).
+
+## Reviewing your device
+
+After you have changed anything, review it against the conformance statement
+rather than against "it looked fine in the explorer":
+
+1. Regenerate [docs/PICS.md](docs/PICS.md) after editing `docs/objects.json`
+ (see [Keeping the PICS honest](#keeping-the-pics-honest) below). A ⚠ row
+ is a required property nothing serves.
+2. Read **every** property listed for **every** object with a BACnet client,
+ on **both** ports independently, and compare the value against the PICS.
+ `"undefined"`, `no-units` and `0` are the three shapes a missed callback
+ takes.
+3. Diff a new object of a type against the existing one of that type.
+ Anything that differs and shouldn't is a callback that matched on
+ instance.
+4. WriteProperty each commandable output at a priority, re-read it, then
+ write NULL to relinquish and confirm it falls back to
+ `Relinquish_Default`.
+5. Confirm the router announcement services fire: watch the TX log at
+ start-up and on the `r` key for `I-Am-Router-To-Network` and
+ `Network-Number-Is` on both ports.
+6. **Do not test, or claim, cross-network forwarding** — it does not work at
+ this stack pin (see [F-ROUTER: router configuration](#f-router-router-configuration)
+ above). A test that assumes it works will fail, correctly.
+7. Confirm services this device does **not** implement are still rejected —
+ AddListElement/RemoveListElement (services 8/9), and any alarm/event,
+ COV, or scheduling service.
+
+### Keeping the PICS honest
+
+`docs/PICS.md` is partly generated. `docs/objects.json` describes each
+object and who serves which property; the series tool regenerates the
+object tables from it plus the stack's own
+`docs/property-profile-reference.md` at the pinned commit:
+
+```bash
+python tools/gen-objects-properties.py BACnetProfileExample-B-RTR-CPP # rewrite
+python tools/gen-objects-properties.py BACnetProfileExample-B-RTR-CPP --check # fail if stale
+```
+
+(That tool lives in the example-series repository, not in this one. If you
+only have this repository, edit the generated block by hand and keep it
+matching the callbacks in `main.cpp`.)
+
+When you add an object or a property to `main.cpp`, update
+`docs/objects.json` in the same change and regenerate. The `app` list is
+what the callbacks serve; `accepted` is for a required property you
+deliberately leave to the stack's default, and each one needs a
+justification. Anything required, not in `app` and not in `accepted`, comes
+out as a ⚠ row - that is a defect, not a feature.
+
+## Troubleshooting
+
+| Symptom | Cause / fix |
+|---------|-------------|
+| Start-up prints `BindRouterPort ... second port of networkType=[0] ... issue #304` and `FindPortByNetworkType ... Cannot attribute an incoming NPDU` | **Expected, not your bug.** The pinned stack cannot route between two ports of the same network type (both BACnet/IP here) - see the F-ROUTER section above, `TODO.md`, and [stack issue #2037](https://github.com/chipkin/cas-bacnet-stack/issues/2037). Each port still works fine on its own network. |
+| ReadProperty of `Routing_Table` aborts, or fails with `unknown-property` | Expected - not enabled in this example; see `TODO.md` and [stack issue #2038](https://github.com/chipkin/cas-bacnet-stack/issues/2038). |
+| On start-up the app prints a wall of red `Error:` lines but the device works | **Expected — this is not your bug.** Several benign sources, all from the stack's own debug logging: (1) each port receives its **own** broadcast I-Am and logs a decode cascade — any BACnet/IP device that listens for broadcasts hears itself; (2) the `BindRouterPort`/`FindPortByNetworkType` lines above; (3) a one-time *"UUID has not been set. A UUID must be set for the BACnetSC device to start."* — the stack starts a BACnet/SC datalink this IP-only example never configures. |
+| CMake error: *"CAS BACnet Stack adapter not found under: ..."* | Submodules not initialized. Run `git submodule update --init --recursive` (or pass `-D CAS_STACK_DIR=...`). |
+| `CASBACnetStackDLL.h: No such file or directory` | Same - submodules not checked out. |
+| Windows: *"No CMAKE_CXX_COMPILER could be found"* | Install Visual Studio with the "Desktop development with C++" workload, then re-run from a fresh terminal. |
+| First build seems stuck for minutes | Normal - it's compiling ~600 stack files. Only the first build is slow. |
+| `Failed to bind UDP port ...` | Another program is using that port. Stop it, or pass a different `--port`/`--port2`. |
+| `--port2` equals `--port` | Rejected on purpose - they must be two different UDP ports (two different simulated networks). |
+| Client sends Who-Is but sees no I-Am | Firewall is blocking the UDP port, or the client and device are on different subnets (Who-Is is a broadcast). Allow both ports; test on the same subnet first. |
+| Replies show an unexpected device instance or vendor | Another BACnet device is already answering on this host/port. Stop the other device, or use `--port`/`--port2`. |
diff --git a/docs/PICS.md b/docs/PICS.md
new file mode 100644
index 0000000..5e7a814
--- /dev/null
+++ b/docs/PICS.md
@@ -0,0 +1,340 @@
+# BACnet Protocol Implementation Conformance Statement (PICS)
+
+For the **BACnet B-RTR (Router) C++ example** -
+see [README.md](../README.md).
+
+> This is the PICS **for the example as shipped**. It describes a tutorial
+> device announcing itself as a Chipkin demo, not a product. When you turn this
+> example into your own device, this document is one of the things you rewrite:
+> the vendor, model and version rows all come from the
+> `CHANGE ALL OF THIS BEFORE YOU SHIP` block at the top of `main.cpp`. The
+> example has **not** been submitted for BTL certification.
+
+## 1. Product description
+
+| | |
+|---|---|
+| **Vendor Name** | Chipkin Automation Systems |
+| **Vendor Identifier** | 389 |
+| **Product Name** | CAS BACnet Stack Example - B-RTR |
+| **Product Model Number** | CAS BACnet Stack Example - B-RTR |
+| **Application Software Version** | 1.0.0 |
+| **Firmware Revision** | 1.0.0 |
+| **BACnet Protocol Version** | 1 |
+| **BACnet Protocol Revision** | 24 |
+
+**Product Description:** a stand-alone BACnet router built on the CAS BACnet
+Stack. It owns two BACnet/IP Network Port objects, one per network it joins,
+and the shared minimum/commandable object pattern (three read-only inputs,
+three writable commandable outputs) used across this example series. It
+configures router ports and routes (`AddRouterPort`/`AddRouterRoute`) and
+announces itself as a router (`I-Am-Router-To-Network`,
+`Who-Is-Router-To-Network`, `Network-Number-Is`), but at the pinned stack
+commit it does **not** forward NPDUs between the two networks - see section 9
+below. It is a tutorial for implementers of the B-RTR profile.
+
+## 2. BACnet standardized device profile (Annex L)
+
+**B-RTR - BACnet Router.**
+
+This device claims exactly one profile. Because the B-RTR requirements are a
+superset of B-GENERAL's, a conformant B-RTR device also satisfies
+**B-GENERAL** (Annex L.8); that is subsumption, not a second claim.
+
+## 3. BIBBs supported (Annex K)
+
+| BIBB | Description | Supported |
+|---|---|:---:|
+| DS-RP-B | Data Sharing - ReadProperty - B | yes |
+| DS-WP-B | Data Sharing - WriteProperty - B | yes |
+| DM-DDB-A | Device Management - Dynamic Device Binding - A (initiates Who-Is) | yes |
+| DM-DDB-B | Device Management - Dynamic Device Binding - B | yes |
+| DM-DOB-B | Device Management - Dynamic Object Binding - B | yes |
+| NM-RC-B | Network Management - Router Configuration - B | partial - router configuration and announcement are implemented and verified; inter-network NPDU **forwarding** is not, at this stack pin (see section 9) |
+| DM-LM-B | Device Management - List Manipulation - B (AddListElement/RemoveListElement) | **no** - the customer-facing STATIC build of the pinned stack does not compile in `STACK_OPTION_DM_LM_LIST_MANIPULATION` ([TODO.md](../TODO.md), [stack issue #2033](https://github.com/chipkin/cas-bacnet-stack/issues/2033)) |
+
+No other BIBBs are supported. In particular this device does **not** support
+DS-RPM-B (ReadPropertyMultiple), DS-COV-B, any alarm and event (AE-*) BIBB,
+scheduling (SCHED-*), trending (T-*), or DM-DCC-B
+(DeviceCommunicationControl) - unlike this example's B-ASC seed, this profile
+omits DM-DCC-B.
+
+## 4. Application services supported
+
+| Service | Initiate | Execute |
+|---|:---:|:---:|
+| ReadProperty | no | **yes** |
+| WriteProperty | no | **yes** |
+| Who-Is | **yes** | **yes** |
+| I-Am | **yes** | - |
+| Who-Has | no | **yes** |
+| I-Have | **yes** | - |
+| Who-Is-Router-To-Network | **yes** | not verified (see section 9) |
+| I-Am-Router-To-Network | **yes** | - |
+| Network-Number-Is | **yes** | - |
+| AddListElement / RemoveListElement | no | no - not implemented (DM-LM-B, see above) |
+
+An unsolicited I-Am is broadcast on **both** networks at start-up (DM-DDB-A/B).
+I-Am-Router-To-Network and Network-Number-Is are likewise broadcast on both
+networks at start-up and again whenever the `r` key is pressed; this is the
+router's polite start-up announcement, not a response to a request.
+
+Any other confirmed service is rejected. That rejection is part of the
+profile boundary, not a limitation to work around.
+
+## 5. Segmentation capability
+
+Segmentation is **not supported** in either direction
+(`Segmentation_Supported` = `no-segmentation`). `Max_APDU_Length_Accepted` is
+1476 octets, the BACnet/IP maximum, on both Network Ports.
+
+## 6. Standard object types supported
+
+No object is dynamically creatable or deletable.
+
+| Object type | Instance | Object_Name | Writable | Optional properties supported |
+|---|:---:|---|:---:|---|
+| Device | 389018 | Rainbow | no | Description |
+| Analog Input | 1 | Bronze | no | - |
+| Binary Input | 1 | Emerald | no | - |
+| Multi-State Input | 1 | Hot Pink | no | State_Text |
+| Analog Output | 1 | Chartreuse | **yes** (commandable) | - |
+| Binary Output | 1 | Fuchsia | **yes** (commandable) | - |
+| Multi-State Output | 1 | Indigo | **yes** (commandable) | - |
+| Network Port | 1 | Vermilion | no | - |
+| Network Port | 2 | Vermilion 2 | no | - |
+
+The device instance is configurable at run time with `--deviceID` (BACnet
+requires the device instance to be configurable).
+
+## 7. Data link layer options
+
+**BACnet/IP (Annex J)**, two independent instances on one host, distinguished
+by UDP port: Network Port 1 (Vermilion) on `--port` (default 47808), Network
+Port 2 (Vermilion 2) on `--port2` (default `--port` + 1 = 47809).
+
+BBMD is not supported, Foreign Device registration is not supported, and
+BACnet/SC, MS/TP, Ethernet (Annex H) and PTP are not supported.
+
+## 8. Device address binding
+
+Static device binding is **not supported**. The device answers Who-Is/Who-Has
+and additionally initiates Who-Is (DM-DDB-A) to discover other devices; it
+does not otherwise bind a peer address ahead of time.
+
+## 9. Networking options
+
+**This device is a router** (two directly-connected BACnet/IP networks, one
+per Network Port) - but read this section precisely before relying on it.
+
+**Works and is verified on the wire, against this build:**
+
+- Router **configuration**: `BACnetStack_AddRouterPort` (both ports),
+ `BACnetStack_AddRouterRoute` (one demonstration-only static route to network
+ 3, one hop beyond port 2), and `BACnetStack_SetRouterEnabled` all succeed.
+- `Network_Number` / `Network_Number_Quality` read back correctly on both
+ Network Ports (`1`/`configured` and `2`/`configured`).
+- **I-Am-Router-To-Network** and **Network-Number-Is** transmission - both
+ confirmed sent (TX on the wire) on both ports at start-up and on the `r`
+ key.
+- ReadProperty and WriteProperty work correctly on **each network
+ independently** - a client on network 1 (port 47808) or network 2 (port
+ 47809) gets correct answers from this device directly.
+
+**Does NOT work at this stack pin - do not rely on these:**
+
+- **Inter-network NPDU forwarding.** A request addressed to a device reachable
+ only through the *other* Network Port is **not forwarded** between the two
+ networks. The pinned stack holds exactly one internal BACnet/IP datalink
+ instance regardless of how many router ports of that type exist, so it
+ cannot attribute inbound traffic to the correct egress port. This is the
+ single most important limitation of this example: it demonstrates router
+ **configuration and announcement**, not working packet forwarding. See
+ [TODO.md](../TODO.md) and
+ [stack issue #2037](https://github.com/chipkin/cas-bacnet-stack/issues/2037).
+- **`Routing_Table` (cl. 12.56.62) read.** Deliberately not enabled - verified
+ on the wire that reading it aborts the request outside a test-tool build of
+ the stack. See [TODO.md](../TODO.md) and
+ [stack issue #2038](https://github.com/chipkin/cas-bacnet-stack/issues/2038).
+- **This device answering an incoming Who-Is-Router-To-Network from a peer
+ router.** Only the outbound (start-up / `r`-key) direction has been
+ exercised against this build.
+- **The demonstration route to network 3.** `AddRouterRoute` accepting the
+ call is confirmed; no device sits on network 3 in this example series, and
+ an actual forwarded packet over that route is not demonstrated (and could
+ not be, given the forwarding gap above).
+
+Neither BBMD nor Foreign Device functionality is implemented by this router.
+
+## 10. Character sets supported
+
+UTF-8 (ANSI X3.4). Supporting a character set does not imply the device can
+handle data in all character sets.
+
+## 11. Objects and properties
+
+
+Every object this example creates, and every REQUIRED property of each (per ANSI/ASHRAE 135-2024 clause 12 and the stack's `docs/property-profile-reference.md`), plus the optional properties the example turns on. **Served by** says who answers a ReadProperty: the **stack** generates it, or the **app** serves it from a `GetProperty*` callback in `main.cpp`. A ⚠ row is a required property the app does not serve and the stack would fill with a default - that is a defect, not a feature.
+
+### Device 389018 "Rainbow" - vendor 389 (Chipkin Automation Systems); instance configurable with --deviceID. The stack rows are device-wide facts only the stack knows - the protocol version and revision it implements, the services and object types it was configured with, the live object list and address-binding table. The accepted rows are the stack's configured defaults for APDU limits, segmentation, system status and database revision; an application that answered them from its own constants could contradict the stack, so this example does not
+
+| Property | Datatype | Served by | Writable |
+|---|---|---|:---:|
+| Object_Identifier | BACnetObjectIdentifier | stack | no |
+| Object_Name | CharacterString | app | no |
+| Object_Type | BACnetObjectType | stack | no |
+| System_Status | BACnetDeviceStatus | stack default, accepted (Generic Enumerated default: `0`) | no |
+| Vendor_Name | CharacterString | app | no |
+| Vendor_Identifier | Unsigned16 | app | no |
+| Model_Name | CharacterString | app | no |
+| Firmware_Revision | CharacterString | app | no |
+| Application_Software_Version | CharacterString | app | no |
+| Description *(optional, enabled)* | CharacterString | app | no |
+| Protocol_Version | Unsigned | stack | no |
+| Protocol_Revision | Unsigned | stack | no |
+| Protocol_Services_Supported | BACnetServicesSupported | stack | no |
+| Protocol_Object_Types_Supported | BACnetObjectTypesSupported | stack | no |
+| Object_List | BACnetARRAY[N] of BACnetObjectIdentifier | stack | no |
+| Max_APDU_Length_Accepted | Unsigned | stack default, accepted (`CAS_BACNET_DEVICE_DEFAULT_MAX_APDU_LENGTH_ACCEPTED`) | no |
+| Segmentation_Supported | BACnetSegmentation | stack default, accepted (`BACnetSegmentation::noSegmentation`) | no |
+| APDU_Timeout | Unsigned | stack default, accepted (`CAS_BACNET_DEVICE_DEFAULT_APDU_TIMEOUT`) | no |
+| Number_Of_APDU_Retries | Unsigned | stack default, accepted (`CAS_BACNET_DEVICE_DEFAULT_NUMBER_OF_APDU_RETRIES`) | no |
+| Device_Address_Binding | BACnetLIST of BACnetAddressBinding | stack | no |
+| Database_Revision | Unsigned | stack default, accepted (Generic UnsignedInteger default: `0`) | no |
+| Property_List | BACnetARRAY[N] of BACnetPropertyIdentifier | stack | no |
+
+### Analog Input 1 "Bronze" - REAL, degrees Celsius; starts at 21.5; read-only
+
+| Property | Datatype | Served by | Writable |
+|---|---|---|:---:|
+| Object_Identifier | BACnetObjectIdentifier | stack | no |
+| Object_Name | CharacterString | app | no |
+| Object_Type | BACnetObjectType | stack | no |
+| Present_Value | Real | app | no |
+| Status_Flags | BACnetStatusFlags | stack | no |
+| Event_State | BACnetEventState | stack default, accepted (Generic Enumerated default: `0`) | no |
+| Out_Of_Service | Boolean | app | no |
+| Units | BACnetEngineeringUnits | app | no |
+| Property_List | BACnetARRAY[N] of BACnetPropertyIdentifier | stack | no |
+
+### Binary Input 1 "Emerald" - starts inactive; read-only
+
+| Property | Datatype | Served by | Writable |
+|---|---|---|:---:|
+| Object_Identifier | BACnetObjectIdentifier | stack | no |
+| Object_Name | CharacterString | app | no |
+| Object_Type | BACnetObjectType | stack | no |
+| Present_Value | BACnetBinaryPV | app | no |
+| Status_Flags | BACnetStatusFlags | stack | no |
+| Event_State | BACnetEventState | stack default, accepted (Generic Enumerated default: `0`) | no |
+| Out_Of_Service | Boolean | app | no |
+| Polarity | BACnetPolarity | app | no |
+| Property_List | BACnetARRAY[N] of BACnetPropertyIdentifier | stack | no |
+
+### Multi-state Input 1 "Hot Pink" - state 1 of 3: On, Off, Auto; read-only
+
+| Property | Datatype | Served by | Writable |
+|---|---|---|:---:|
+| Object_Identifier | BACnetObjectIdentifier | stack | no |
+| Object_Name | CharacterString | app | no |
+| Object_Type | BACnetObjectType | stack | no |
+| Present_Value | Unsigned | app | no |
+| Status_Flags | BACnetStatusFlags | stack | no |
+| Event_State | BACnetEventState | stack default, accepted (Generic Enumerated default: `0`) | no |
+| Out_Of_Service | Boolean | app | no |
+| Number_Of_States | Unsigned | app | no |
+| State_Text *(optional, enabled)* | BACnetARRAY[N] of CharacterString | app | no |
+| Property_List | BACnetARRAY[N] of BACnetPropertyIdentifier | stack | no |
+
+### Analog Output 1 "Chartreuse" - REAL setpoint, default 20.0 C; commandable via a 16-slot Priority_Array (WriteProperty, DS-WP-B) - Present_Value/Priority_Array/Current_Command_Priority are resolved by the stack from the slots the app serves through GetPropertyReal/GetPropertyBool
+
+| Property | Datatype | Served by | Writable |
+|---|---|---|:---:|
+| Object_Identifier | BACnetObjectIdentifier | stack | no |
+| Object_Name | CharacterString | app | no |
+| Object_Type | BACnetObjectType | stack | no |
+| Present_Value | Real | stack | yes |
+| Status_Flags | BACnetStatusFlags | stack | no |
+| Event_State | BACnetEventState | stack default, accepted (Generic Enumerated default: `0`) | no |
+| Out_Of_Service | Boolean | app | no |
+| Units | BACnetEngineeringUnits | app | no |
+| Priority_Array | BACnetARRAY[16] of BACnetOptionalReal | stack | no |
+| Relinquish_Default | Real | app | no |
+| Property_List | BACnetARRAY[N] of BACnetPropertyIdentifier | stack | no |
+| Current_Command_Priority | BACnetOptionalUnsigned | stack | no |
+
+### Binary Output 1 "Fuchsia" - active/inactive, default inactive; commandable via a 16-slot Priority_Array (WriteProperty, DS-WP-B) - Present_Value/Priority_Array/Current_Command_Priority are resolved by the stack from the slots the app serves through GetPropertyEnumerated/GetPropertyBool
+
+| Property | Datatype | Served by | Writable |
+|---|---|---|:---:|
+| Object_Identifier | BACnetObjectIdentifier | stack | no |
+| Object_Name | CharacterString | app | no |
+| Object_Type | BACnetObjectType | stack | no |
+| Present_Value | BACnetBinaryPV | stack | yes |
+| Status_Flags | BACnetStatusFlags | stack | no |
+| Event_State | BACnetEventState | stack default, accepted (Generic Enumerated default: `0`) | no |
+| Out_Of_Service | Boolean | app | no |
+| Polarity | BACnetPolarity | app | no |
+| Priority_Array | BACnetARRAY[16] of BACnetOptionalBinaryPV | stack | no |
+| Relinquish_Default | BACnetBinaryPV | app | no |
+| Property_List | BACnetARRAY[N] of BACnetPropertyIdentifier | stack | no |
+| Current_Command_Priority | BACnetOptionalUnsigned | stack | no |
+
+### Multi-state Output 1 "Indigo" - state 1 of 3, default state 1; commandable via a 16-slot Priority_Array (WriteProperty, DS-WP-B) - Present_Value/Priority_Array/Current_Command_Priority are resolved by the stack from the slots the app serves through GetPropertyUnsignedInteger/GetPropertyBool
+
+| Property | Datatype | Served by | Writable |
+|---|---|---|:---:|
+| Object_Identifier | BACnetObjectIdentifier | stack | no |
+| Object_Name | CharacterString | app | no |
+| Object_Type | BACnetObjectType | stack | no |
+| Present_Value | Unsigned | stack | yes |
+| Status_Flags | BACnetStatusFlags | stack | no |
+| Event_State | BACnetEventState | stack default, accepted (Generic Enumerated default: `0`) | no |
+| Out_Of_Service | Boolean | app | no |
+| Number_Of_States | Unsigned | app | no |
+| Priority_Array | BACnetARRAY[16] of BACnetOptionalUnsigned | stack | no |
+| Relinquish_Default | Unsigned | app | no |
+| Property_List | BACnetARRAY[N] of BACnetPropertyIdentifier | stack | no |
+| Current_Command_Priority | BACnetOptionalUnsigned | stack | no |
+
+### Network Port 1 "Vermilion" - BACnet/IP, network 1, configured (F-MULTIPORT/F-ROUTER port A). Network_Type/Protocol_Level are set from BACnetStack_AddNetworkPortObject()'s arguments at start-up (as is Network_Number = 1 / Network_Number_Quality = configured, verified readable on the wire; not listed as a separate row here because the stack's own property-profile-reference.md does not carry those two properties for the type-agnostic 'Network Port' heading this generator reads). Changes_Pending is likewise computed and answered natively. Reliability has no fault condition this example detects, so it is accepted at the generic default (normal). Routing_Table is deliberately NOT enabled - verified on the wire that the pinned stack cannot serve it outside a test-tool build (TODO.md, stack issue #2038).
+
+| Property | Datatype | Served by | Writable |
+|---|---|---|:---:|
+| Object_Identifier | BACnetObjectIdentifier | stack | no |
+| Object_Name | CharacterString | app | no |
+| Object_Type | BACnetObjectType | stack | no |
+| Status_Flags | BACnetStatusFlags | stack | no |
+| Reliability | BACnetReliability | stack default, accepted (Generic Enumerated default: `0`) | no |
+| Out_Of_Service | Boolean | app | no |
+| Network_Type | BACnetNetworkType | app | no |
+| Protocol_Level | BACnetProtocolLevel | app | no |
+| Changes_Pending | Boolean | app | no |
+| Property_List | BACnetARRAY[N] of BACnetPropertyIdentifier | stack | no |
+
+### Network Port 2 "Vermilion 2" - BACnet/IP, network 2, configured (F-MULTIPORT/F-ROUTER port B). Same notes as Network Port 1 (Vermilion) apply, with Network_Number = 2. Physically routes to network 1 via BACnetStack_AddRouterPort/SetRouterEnabled, but see TODO.md and stack issue #2037: this stack pin cannot forward NPDUs between two ports of the same network type, so cross-network traffic is not verified end-to-end even though both ports individually answer ReadProperty/WriteProperty correctly.
+
+| Property | Datatype | Served by | Writable |
+|---|---|---|:---:|
+| Object_Identifier | BACnetObjectIdentifier | stack | no |
+| Object_Name | CharacterString | app | no |
+| Object_Type | BACnetObjectType | stack | no |
+| Status_Flags | BACnetStatusFlags | stack | no |
+| Reliability | BACnetReliability | stack default, accepted (Generic Enumerated default: `0`) | no |
+| Out_Of_Service | Boolean | app | no |
+| Network_Type | BACnetNetworkType | app | no |
+| Protocol_Level | BACnetProtocolLevel | app | no |
+| Changes_Pending | Boolean | app | no |
+| Property_List | BACnetARRAY[N] of BACnetPropertyIdentifier | stack | no |
+
+
+
+## 12. References
+
+- ANSI/ASHRAE Standard 135-2024, Annex A (PICS template), Annex K (BIBBs),
+ Annex L (device profiles), Clause 6.6 (routers), Clause 12 (object types).
+- [README.md](../README.md) - what this example is and how to build it.
+- [TUTORIAL.md](../TUTORIAL.md) - how to extend it, and how to keep this
+ document honest when you do.
+- [TODO.md](../TODO.md) - the router-forwarding and DM-LM-B gaps in detail,
+ with the stack issues tracking them.
diff --git a/docs/objects.json b/docs/objects.json
index 9864982..aab79d4 100644
--- a/docs/objects.json
+++ b/docs/objects.json
@@ -8,12 +8,12 @@
"Firmware_Revision", "Application_Software_Version", "Description"],
"enabled": ["Description"],
"writable": [],
- "accepted": ["System_Status", "Protocol_Version", "Protocol_Revision",
- "Protocol_Services_Supported", "Protocol_Object_Types_Supported",
- "Object_List", "Max_APDU_Length_Accepted", "Segmentation_Supported",
- "APDU_Timeout", "Number_Of_APDU_Retries", "Device_Address_Binding",
- "Database_Revision"],
- "note": "vendor 389 (Chipkin Automation Systems); instance configurable with --deviceID. The properties in 'accepted' are not served from a GetProperty callback because the stack itself is the source of truth for them - Protocol_Revision/Protocol_Version are stack build constants, Protocol_Services_Supported/Protocol_Object_Types_Supported are computed from the BACnetStack_SetServiceEnabled/AddObject calls this example already makes, Object_List and Device_Address_Binding are live stack-maintained tables, System_Status/Database_Revision/Max_APDU_Length_Accepted/Segmentation_Supported/APDU_Timeout/Number_Of_APDU_Retries are the stack's own configuration defaults for a device this example does not override"
+ "accepted": ["System_Status", "Max_APDU_Length_Accepted", "Segmentation_Supported",
+ "APDU_Timeout", "Number_Of_APDU_Retries", "Database_Revision"],
+ "stack": ["Object_List", "Protocol_Version", "Protocol_Revision",
+ "Protocol_Services_Supported", "Protocol_Object_Types_Supported",
+ "Device_Address_Binding"],
+ "note": "vendor 389 (Chipkin Automation Systems); instance configurable with --deviceID. The stack rows are device-wide facts only the stack knows - the protocol version and revision it implements, the services and object types it was configured with, the live object list and address-binding table. The accepted rows are the stack's configured defaults for APDU limits, segmentation, system status and database revision; an application that answered them from its own constants could contradict the stack, so this example does not"
},
{
"type": "Analog Input",
diff --git a/main.cpp b/main.cpp
index 7d9f5c7..7cd63f1 100644
--- a/main.cpp
+++ b/main.cpp
@@ -105,18 +105,47 @@ static const char* APP_VERSION = "1.0.0";
// The device instance. BACnet requires this to be configurable, so it defaults
// to 389018 (docs/colour-table.md) and can be overridden with --deviceID.
+// CHANGE ALL OF THIS BEFORE YOU SHIP: like DEVICE_NAME above, this instance
+// number MUST BE UNIQUE ACROSS THE WHOLE BACNET INTERNETWORK. --deviceID
+// already makes it runtime-configurable; a real product still needs a process
+// (serial number, commissioning step, ...) for assigning a non-colliding value
+// per unit, since the default here is only a placeholder for this example.
static uint32_t g_deviceInstance = 389018;
// ---- Device identity: CHANGE ALL OF THIS BEFORE YOU SHIP --------------------
+// Every constant in this block is read by clients and shown to the operator in
+// EVERY discovery tool on the network. Left as-is, your product appears on a
+// real site announcing itself as a Chipkin demo. None of it is cosmetic.
+
+// Change to YOUR company's vendor ID, assigned free by ASHRAE:
+// https://bacnet.org/assigned-vendor-ids/ . Must match VENDOR_NAME below.
static const uint32_t VENDOR_IDENTIFIER = 389;
+
+// This is the device's Object_Name. It MUST BE UNIQUE ACROSS THE WHOLE BACNET
+// INTERNETWORK - two devices with the same Object_Name is a spec violation
+// that every scan tool renders as two perfectly good objects (see TUTORIAL.md's
+// "silent failure" warning for the same failure shape on a per-object level).
+// Here it is a COMPILE-TIME CONSTANT, which is fine for a tutorial but not for
+// a real product: a real device needs a name that is unique PER UNIT, not per
+// firmware build - derive it from a serial number, DIP switches, a config
+// file, or a --deviceName command-line argument, not a hardcoded string.
static const char* DEVICE_NAME = "Rainbow";
+
+// Change to a description of what YOUR device actually is.
static const char* DEVICE_DESCRIPTION =
"Chipkin CAS BACnet Stack example - B-RTR (Router) profile. Demonstrates "
"DS-RP-B + DS-WP-B + DM-DDB-A + DM-DOB-B + NM-RC-B: a stand-alone BACnet "
"router forwarding between two BACnet/IP networks, each with its own "
"Network Port object.";
+
+// Change to YOUR company name - must match VENDOR_IDENTIFIER above.
static const char* VENDOR_NAME = "Chipkin Automation Systems";
+
+// Change to your product's own model designation.
static const char* MODEL_NAME = "CAS BACnet Stack Example - B-RTR";
+
+// Change both to your product's REAL firmware / application software
+// versions - these are not the example's own version (APP_VERSION above).
static const char* FIRMWARE_REVISION = "1.0.0";
static const char* APPLICATION_SOFTWARE_VERSION = "1.0.0";
@@ -144,6 +173,11 @@ static const uint32_t MAX_APDU_LENGTH = 1476; // BACnet/IP APDU length
// The BACnet network NUMBERS the two ports directly connect (ASHRAE 135 cl.
// 6.2.2 numbers a network, distinct from the Network Port OBJECT instance).
// Configured (not learned), so Network_Number_Quality reports CONFIGURED.
+// CHANGE ALL OF THIS BEFORE YOU SHIP: these two placeholder numbers (1, 2)
+// must become YOUR site's actual network numbers - they must be unique across
+// the whole BACnet internetwork, exactly like the device instance above. A
+// wrong or colliding network number breaks routing for every device on it,
+// not just this one.
static const uint16_t NETWORK_NUMBER_PORT_1 = 1;
static const uint16_t NETWORK_NUMBER_PORT_2 = 2;