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;