Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
20 changes: 19 additions & 1 deletion .github/workflows/release.yml
Original file line number Diff line number Diff line change
Expand Up @@ -160,8 +160,26 @@ jobs:
- uses: actions/download-artifact@v4
with:
path: artifacts
# Every platform artifact carries the same docs (README.md, LICENSE, ...).
# Uploading two assets with one name makes the release action fail part-way
# and leaves a draft release behind (a real tagged release hit this). Flatten the
# artifacts into one folder with a single copy of each file name, and stop
# if two files share a name but differ.
- name: Collect release assets (one copy of each file name)
shell: bash
run: |
mkdir -p release
find artifacts -type f | sort | while read -r f; do
name=$(basename "$f")
if [ -e "release/$name" ]; then
cmp -s "$f" "release/$name" || { echo "FAIL: two different files are both named $name ($f)"; exit 1; }
else
cp "$f" "release/$name"
fi
done
ls -l release
- uses: softprops/action-gh-release@v2
with:
name: ${{ github.ref_name }}
generate_release_notes: true
files: artifacts/**/*
files: release/*
2 changes: 1 addition & 1 deletion AGENTS.md
Original file line number Diff line number Diff line change
Expand Up @@ -101,7 +101,7 @@ TUTORIAL.md "Interactive commands are a simplification"): `h` help, `q` quit,

## Conventions

- Device is named "Rainbow"; objects use the series' colour names; vendor id
- Device is named "Chipkin Example B-SS"; objects use the series' colour names; vendor id
389.
- Implement **only** the services and objects the B-SS profile requires -
but expose **every required property** of each object.
Expand Down
34 changes: 34 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,40 @@ 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).

## [1.0.2] - unreleased

### Changed

- **Device renamed from the series' colour placeholder "Rainbow" to "Chipkin
Example B-SS"** so devices from different examples in the series are
distinguishable from each other on the same BACnet network - every example
previously announced the identical Object_Name "Rainbow", which made two
examples on one subnet indistinguishable by name. Sub-object names are
unchanged. `docs/colour-table.md` (series root) updated to match.
APP_VERSION bumped 1.0.1 -> 1.0.2.

## [1.0.1] - 2026-09-22

### Fixed

- `Application_Software_Version` (12) and `Firmware_Revision` (44) were
hardcoded `"1.0.0"` constants that never tracked reality - the same class
of bug just found and fixed in the sibling C++ example
(BACnetProfileExample-B-SCHUB-CPP) by someone reading a real device with
CAS BACnet Explorer and noticing the reported version didn't match the
running build. `Application_Software_Version` now reads `APP_VERSION`
directly (the same constant `--version`'s own banner uses - one source of
truth, can't drift again). `Firmware_Revision` is now built once at
start-up, in `main()` right after the native library loads, from the CAS
BACnet Stack's own `BACnetStack_GetAPIMajorVersion`/`MinorVersion`/
`PatchVersion`/`BuildVersion` - the same four calls
`cas_example_helper::print_version` already uses for the start-up banner -
and stored in `STATE.firmware_revision` (the existing
`once_cell`-backed `Mutex<DeviceState>` pattern already used for
start-up-resolved fields like the Network Port's IP addressing).
`Firmware_Revision` was never meant to be this example's own version; it
names the platform underneath the app.

## [1.0.0] - 2026-09-16

### Added
Expand Down
2 changes: 1 addition & 1 deletion Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 1 addition & 1 deletion Cargo.toml
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
[package]
name = "bacnet-profile-example-b-ss-rust"
version = "1.0.0"
version = "1.0.2"
edition = "2021"
description = "BACnet B-SS (Smart Sensor) profile example - Rust, built on the CAS BACnet Stack"
license = "CC0-1.0"
Expand Down
10 changes: 5 additions & 5 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -55,7 +55,7 @@ and who answers it.
## The device this example creates

```
Device 389001 "Rainbow" (Vendor 389 - Chipkin Automation Systems)
Device 389001 "Chipkin Example B-SS" (Vendor 389 - Chipkin Automation Systems)
│
├── Analog Input 1 "Bronze" Present_Value 21.5 (REAL, degrees Celsius; read-only)
├── Binary Input 1 "Emerald" Present_Value inactive (read-only)
Expand Down Expand Up @@ -88,7 +88,7 @@ which is the point of a profile example.

| Object type | Instance | Name |
|-------------|:--------:|------|
| Device | 389001 | Rainbow |
| Device | 389001 | Chipkin Example B-SS |
| Analog Input | 1 | Bronze |
| Binary Input | 1 | Emerald |
| Multi-state Input | 1 | Hot Pink |
Expand Down Expand Up @@ -232,7 +232,7 @@ Expected output:
BACnet B-SS (Smart Sensor) Example - Rust v1.0.0 (common v1.0.0)
CAS BACnet Stack v6.0.21.0
FYI: listening on <your-ip>:47808 (broadcast <your-subnet-broadcast>)
FYI: Device 389001 ("Rainbow") ready. Vendor ID 389. Type 'h' + Enter for help.
FYI: Device 389001 ("Chipkin Example B-SS") ready. Vendor ID 389. Type 'h' + Enter for help.
```

The device listens on UDP **47808** (BACnet/IP) by default. Allow that port
Expand Down Expand Up @@ -276,10 +276,10 @@ Use a BACnet client such as the
instance **389001** (vendor **389**). It also broadcasts an I-Am at
start-up.
2. **Browse the object model** - the device shows five objects: the Device
(`Rainbow`), the three sensors, and the Network Port (`Vermilion`). Reading
(`Chipkin Example B-SS`), the three sensors, and the Network Port (`Vermilion`). Reading
the Device's `Object_List` returns all five.
3. **Read the Device** - ReadProperty `389001` -> `Object_Name` returns
`"Rainbow"`; `Protocol_Revision` returns `24`; `Description` returns the
`"Chipkin Example B-SS"`; `Protocol_Revision` returns `24`; `Description` returns the
profile description string.
4. **Read a sensor** - ReadProperty Analog Input `1` -> `Present_Value`
returns `21.5`; `Units` returns `degrees-Celsius`; `Out_Of_Service` returns
Expand Down
4 changes: 2 additions & 2 deletions docs/PICS.md
Original file line number Diff line number Diff line change
Expand Up @@ -82,7 +82,7 @@ is writable.

| Object type | Instance | Object_Name | Optional properties supported |
|---|:---:|---|---|
| Device | 389001 | Rainbow | Description |
| Device | 389001 | Chipkin Example B-SS | Description |
| Analog Input | 1 | Bronze | - |
| Binary Input | 1 | Emerald | - |
| Multi-State Input | 1 | Hot Pink | State_Text |
Expand Down Expand Up @@ -119,7 +119,7 @@ handle data in all character sets.
<!-- OBJECTS-PROPERTIES:BEGIN (generated by tools/gen-objects-properties.py from docs/objects.json - do not edit here) -->
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 `get_property_*` callback in `main.rs`. 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 389001 "Rainbow" - the device itself; the instance is 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
### Device 389001 "Chipkin Example B-SS" - the device itself; the instance is 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 |
|---|---|---|:---:|
Expand Down
2 changes: 1 addition & 1 deletion docs/objects.json
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@
{
"type": "Device",
"instance": 389001,
"name": "Rainbow",
"name": "Chipkin Example B-SS",
"app": [
"Object_Name",
"Description",
Expand Down
14 changes: 13 additions & 1 deletion src/common/device_state.rs
Original file line number Diff line number Diff line change
Expand Up @@ -53,17 +53,29 @@ pub struct DeviceState {
pub ip_address: [u8; 4],
pub ip_subnet_mask: [u8; 4],
pub ip_default_gateway: [u8; 4],

/// The Device object's Firmware_Revision (property 44) - the underlying
/// CAS BACnet Stack's own version, NOT this example's version. Built
/// once at start-up (in `main()`, right after the native library loads
/// successfully) from `BACnetStack_GetAPIMajorVersion`/`MinorVersion`/
/// `PatchVersion`/`BuildVersion` - the same four calls
/// `cas_example_helper::print_version` already uses for the start-up
/// banner. Empty until then; a `Get*Property` callback should never
/// observe it empty in practice because `main()` sets it before
/// registering any callback or adding the Device object.
pub firmware_revision: String,
}

impl DeviceState {
const fn new() -> Self {
fn new() -> Self {
DeviceState {
device_instance: 389001,
bacnet_ip_udp_port: 47808,
analog_input_1_value: 21.5,
ip_address: [0, 0, 0, 0],
ip_subnet_mask: [0, 0, 0, 0],
ip_default_gateway: [0, 0, 0, 0],
firmware_revision: String::new(),
}
}
}
Expand Down
41 changes: 33 additions & 8 deletions src/main.rs
Original file line number Diff line number Diff line change
Expand Up @@ -54,7 +54,7 @@
// object that every BACnet device must have. Each object is given a colour name
// (a convention shared across this example series):
//
// Device 389001 "Rainbow" (instance configurable with --deviceID)
// Device 389001 "Chipkin Example B-SS" (instance configurable with --deviceID)
// Analog Input 1 "Bronze" (REAL, degrees Celsius; starts at 21.5)
// Binary Input 1 "Emerald" (active / inactive)
// Multi-State Input 1 "Hot Pink" (an enumerated state, 1..3)
Expand Down Expand Up @@ -98,7 +98,7 @@ use common::device_state::STATE;
// 1. Example + device configuration
// -----------------------------------------------------------------------------
const APP_NAME: &str = "BACnet B-SS (Smart Sensor) Example - Rust";
const APP_VERSION: &str = "1.0.0";
const APP_VERSION: &str = "1.0.2";

// ---- Device identity: CHANGE ALL OF THIS BEFORE YOU SHIP --------------------
// Everything in this block is read by clients and shown to the operator in
Expand All @@ -120,11 +120,11 @@ const VENDOR_IDENTIFIER: u32 = 389;
// whole BACnet internetwork, and here it is a COMPILE-TIME constant. The
// device instance is runtime-configurable via --deviceID, so it is easy to
// ship two units, configure their instances correctly, and still have BOTH
// announce Object_Name "Rainbow" - a spec violation, and a hard BTL failure.
// announce Object_Name "Chipkin Example B-SS" - a spec violation, and a hard BTL failure.
// In a real product Object_Name must be per-unit configurable too: derive it
// from a serial number, DIP switches, a config file, or add a --deviceName
// argument.
const DEVICE_NAME: &str = "Rainbow";
const DEVICE_NAME: &str = "Chipkin Example B-SS";

// The Device object's Description. Change it to what YOUR device actually is;
// this string describes this tutorial.
Expand All @@ -134,8 +134,16 @@ const DEVICE_DESCRIPTION: &str = "Chipkin CAS BACnet Stack example - B-SS (Smart
// Device identity strings (read by clients, and used to populate I-Am).
const VENDOR_NAME: &str = "Chipkin Automation Systems";
const MODEL_NAME: &str = "CAS BACnet Stack Example - B-SS";
const FIRMWARE_REVISION: &str = "1.0.0";
const APPLICATION_SOFTWARE_VERSION: &str = "1.0.0";
// Firmware_Revision (44) and Application_Software_Version (12) are NOT
// consts: they must track reality, not a value someone typed once and
// forgot. Application_Software_Version is this example's own APP_VERSION
// (declared above) - the one source of truth also used by --version's
// banner. Firmware_Revision names the platform underneath this app, not
// the app itself: it is the CAS BACnet Stack's own version, computed once
// at start-up into STATE.firmware_revision (see device_state.rs) from the
// stack's BACnetStack_GetAPIMajorVersion/Minor/Patch/BuildVersion calls -
// the same four calls cas_example_helper::print_version already makes for
// the start-up banner.

// The sensor objects (all instance 1) and their colour names.
const ANALOG_INPUT_INSTANCE: u32 = 1; // "Bronze"
Expand Down Expand Up @@ -532,9 +540,9 @@ extern "C" fn get_property_character_string(
} else if property_identifier == c::PROPERTY_IDENTIFIER_MODEL_NAME {
Some(MODEL_NAME)
} else if property_identifier == c::PROPERTY_IDENTIFIER_FIRMWARE_REVISION {
Some(FIRMWARE_REVISION)
Some(state.firmware_revision.as_str())
} else if property_identifier == c::PROPERTY_IDENTIFIER_APPLICATION_SOFTWARE_VERSION {
Some(APPLICATION_SOFTWARE_VERSION)
Some(APP_VERSION)
} else {
None
};
Expand Down Expand Up @@ -708,6 +716,23 @@ fn main() -> std::process::ExitCode {
return std::process::ExitCode::SUCCESS;
}

// Firmware_Revision (44) is the CAS BACnet Stack's own version, not this
// example's - see the comment above FIRMWARE_REVISION's old const
// (removed) near APP_VERSION. Compute it once here, now that the native
// library has definitely loaded (the print_version() call above forced
// that), using the same four API calls print_version() uses for the
// start-up banner.
{
let mut state = STATE.lock().unwrap_or_else(|e| e.into_inner());
state.firmware_revision = format!(
"{}.{}.{}.{}",
bacnet::BACnetStack_GetAPIMajorVersion().unwrap_or(0),
bacnet::BACnetStack_GetAPIMinorVersion().unwrap_or(0),
bacnet::BACnetStack_GetAPIPatchVersion().unwrap_or(0),
bacnet::BACnetStack_GetAPIBuildVersion().unwrap_or(0),
);
}

// --- Bind the BACnet/IP socket -------------------------------------------
if let Err(e) = cas_example_helper::setup_udp(port) {
eprintln!("Error: could not bind UDP port {port}: {e}");
Expand Down
Loading