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 @@ -152,8 +152,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 @@ -88,7 +88,7 @@ up/down nudge Analog Input 1.

## 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
36 changes: 36 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,42 @@ 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] - unreleased

### Fixed

- `Application_Software_Version` (12) and `Firmware_Revision` (44) were
hardcoded to the literal string `"1.0.0"` in `main.py` and never updated as
the example's real version advanced - the same class of bug found by
someone testing [BACnetProfileExample-B-SCHUB-CPP](https://github.com/chipkin/BACnetProfileExample-B-SCHUB-CPP)
against a real BACnet client (CAS BACnet Explorer), where the reported
version didn't match the running build. `Firmware_Revision` was never
meant to be this example's own version at all - it names the underlying
platform. Fixed: `Application_Software_Version` now reads `APP_VERSION`
directly (one source of truth, can't drift from `--version`'s own banner
again). `Firmware_Revision` is now built once at start-up, right after
`bacnet.bind(library)` succeeds in `main()`, from the CAS BACnet Stack's
own `BACnetStack_GetAPIMajorVersion()`/`GetAPIMinorVersion()`/
`GetAPIPatchVersion()`/`GetAPIBuildVersion()` - the same 4 calls
`cas_example_helper.print_version()` already uses for the startup banner -
and stored in `g_firmware_revision` for every later ReadProperty. Verified
with a real ReadProperty (via `bacpypes3`) against the running device:
`Application_Software_Version` read back `"1.0.1"` (matching `APP_VERSION`,
this release) and `Firmware_Revision` read back `"6.0.21.0"` (matching the
stack's own `--version` banner).

## [1.0.0] - unreleased

### Added
Expand Down
10 changes: 5 additions & 5 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -50,7 +50,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 @@ -83,7 +83,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 @@ -210,7 +210,7 @@ Expected output:
BACnet B-SS (Smart Sensor) Example - Python 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. Press 'h' for help.
FYI: Device 389001 ("Chipkin Example B-SS") ready. Vendor ID 389. Press 'h' for help.
```

The device listens on UDP **47808** (BACnet/IP) by default. Allow that port
Expand Down Expand Up @@ -251,10 +251,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.py`. 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
41 changes: 29 additions & 12 deletions main.py
Original file line number Diff line number Diff line change
Expand Up @@ -45,7 +45,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 @@
# 1. Example + device configuration
# -----------------------------------------------------------------------------
APP_NAME = "BACnet B-SS (Smart Sensor) Example - Python"
APP_VERSION = "1.0.0"
APP_VERSION = "1.0.2"

# The device instance. BACnet requires this to be configurable, so it defaults
# to 389001 and can be overridden on the command line with --deviceID. Keep it
Expand Down Expand Up @@ -128,11 +128,11 @@
# whole BACnet internetwork, and here it is a COMPILE-TIME (well, module-load-
# 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
# still have BOTH 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.
DEVICE_NAME = "Rainbow"
DEVICE_NAME = "Chipkin Example B-SS"

# The Device object's Description. Change it to what YOUR device actually is;
# this string describes this tutorial.
Expand All @@ -145,13 +145,19 @@
# VENDOR_NAME - your company name; it must match VENDOR_IDENTIFIER above.
# MODEL_NAME - your model designation. This is what a building operator
# reads to identify your device in a discovery tool.
# FIRMWARE_REVISION / APPLICATION_SOFTWARE_VERSION - your real versions.
# Wire them to your build rather than hard-coding a number
# that will go stale.
# Application_Software_Version (12) / Firmware_Revision (44) - read by
# clients via the get_property_character_string callback below.
# Application_Software_Version is this example's OWN version: it reads
# APP_VERSION directly, so it can never drift from what --version prints.
# Firmware_Revision names the PLATFORM underneath this app, not the app
# itself - it is built at runtime from the CAS BACnet Stack's own
# BACnetStack_GetAPI{Major,Minor,Patch,Build}Version() calls (the same 4
# calls cas_example_helper.print_version() already uses for the startup
# banner) into g_firmware_revision, once, right after the stack loads
# successfully in main() - see "Load the CAS BACnet Stack" below.
VENDOR_NAME = "Chipkin Automation Systems"
MODEL_NAME = "CAS BACnet Stack Example - B-SS"
FIRMWARE_REVISION = "1.0.0"
APPLICATION_SOFTWARE_VERSION = "1.0.0"
g_firmware_revision = None

# The sensor objects (all instance 1) and their colour names.
ANALOG_INPUT_INSTANCE = 1 # "Bronze"
Expand Down Expand Up @@ -457,10 +463,10 @@ def get_property_character_string(device_instance, object_type, object_instance,
return _return_character_string(MODEL_NAME, value, value_element_count,
max_element_count, encoding_type)
if property_identifier == c.PROPERTY_IDENTIFIER_FIRMWARE_REVISION:
return _return_character_string(FIRMWARE_REVISION, value, value_element_count,
return _return_character_string(g_firmware_revision, value, value_element_count,
max_element_count, encoding_type)
if property_identifier == c.PROPERTY_IDENTIFIER_APPLICATION_SOFTWARE_VERSION:
return _return_character_string(APPLICATION_SOFTWARE_VERSION, value, value_element_count,
return _return_character_string(APP_VERSION, value, value_element_count,
max_element_count, encoding_type)

return False
Expand Down Expand Up @@ -563,7 +569,7 @@ def _print_help():
# 4. main()
# -----------------------------------------------------------------------------
def main():
global g_device_instance, g_bacnet_ip_udp_port, g_analog_input_1_value
global g_device_instance, g_bacnet_ip_udp_port, g_analog_input_1_value, g_firmware_revision

parser = ArgumentParser(prog="main.py", description=APP_NAME)
parser.add_argument("--port", type=cas_example_helper.parse_port, default=47808,
Expand Down Expand Up @@ -594,6 +600,17 @@ def main():
return 1
bacnet.bind(library)

# Firmware_Revision (44) names the CAS BACnet Stack itself, not this
# example app - build it once from the stack's own version API, right
# after the stack is bound, so every later ReadProperty of (Device,
# Firmware_Revision) just returns the string computed here.
g_firmware_revision = "{}.{}.{}.{}".format(
bacnet.BACnetStack_GetAPIMajorVersion(),
bacnet.BACnetStack_GetAPIMinorVersion(),
bacnet.BACnetStack_GetAPIPatchVersion(),
bacnet.BACnetStack_GetAPIBuildVersion(),
)

if args.version:
cas_example_helper.print_version(APP_NAME, APP_VERSION)
return 0
Expand Down
Loading