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 @@ -174,8 +174,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/*
41 changes: 41 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,47 @@
All notable changes to this example. The format is based on
[Keep a Changelog](https://keepachangelog.com/en/1.1.0/).

## [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.
appVersion bumped 1.0.1 -> 1.0.2.

## [1.0.1] - unreleased

### Fixed

- **`Application_Software_Version` (12) and `Firmware_Revision` (44) were
hardcoded to `"1.0.0"` and never updated as the example's real version
advanced** - the same issue found and fixed in
[BACnetProfileExample-B-SCHUB-CPP](https://github.com/chipkin/BACnetProfileExample-B-SCHUB-CPP)
v1.1.13 via a real device read with CAS BACnet Explorer. Both were plain
`const string`s in `common/constants.go`, consumed directly in
`cas_bacnet_stack/property_dispatch.go`.

Go's `const` cannot hold a value only known at runtime (the stack's own
version) or reference a value from a package that imports it back
(`common` cannot import `main`, which imports `common`), so both were
changed from `const` to package-level `var` in `common/constants.go`,
given placeholder start-up defaults, and populated once in `main.go`'s
`run()` - right after `printVersion()` confirms the cgo-linked native
library works, before the socket is bound or any callback is registered:
`common.ApplicationSoftwareVersion` is set directly from `main.go`'s own
`appVersion`; `common.FirmwareRevision` is built from the CAS BACnet
Stack's own `bacnet.GetAPIMajorVersion()`/`GetAPIMinorVersion()`/
`GetAPIPatchVersion()`/`GetAPIBuildVersion()` (the same 4 calls
`printVersion()` already uses for the start-up banner) - it names the
underlying platform, not this app. Verified with `go build ./...` (clean)
and a real ReadProperty (`bacpypes3`) against the running device:
`Application_Software_Version = "1.0.1"`,
`Firmware_Revision = "6.0.21.0"`.

## [1.0.0] - 2026-09-16

First Go implementation of the BACnet B-SS (Smart Sensor) profile in this
Expand Down
4 changes: 2 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -26,7 +26,7 @@ in this series.

| Object | Instance | Name | Notes |
|---|:---:|---|---|
| Device | 389001 | Rainbow | vendor ID 389 (Chipkin), `--deviceID` configurable |
| Device | 389001 | Chipkin Example B-SS | vendor ID 389 (Chipkin), `--deviceID` configurable |
| Analog Input | 1 | Bronze | REAL, degrees Celsius, starts 21.5, nudged +/-1.1 via interactive commands |
| Binary Input | 1 | Emerald | starts inactive |
| Multi-State Input | 1 | Hot Pink | state 1 of 3 ("On"/"Off"/"Auto"), `State_Text` enabled |
Expand Down Expand Up @@ -217,7 +217,7 @@ Ctrl+C also stops the device cleanly.

From another machine (or the same one) on the same subnet, use a BACnet
explorer (e.g. YABE, VTS, or Chipkin's own BACnet Explorer) to send a
Who-Is and confirm Device 389001 ("Rainbow") answers with an I-Am, then
Who-Is and confirm Device 389001 ("Chipkin Example B-SS") answers with an I-Am, then
browse its four objects and read their properties.

## How the Go binding differs from the C++/C#/Python/Rust editions
Expand Down
75 changes: 44 additions & 31 deletions common/constants.go
Original file line number Diff line number Diff line change
Expand Up @@ -22,25 +22,25 @@ const (
const (
PropertyIdentifierObjectName uint32 = 77
PropertyIdentifierObjectType uint32 = 79
PropertyIdentifierPresentValue uint32 = 85
PropertyIdentifierPresentValue uint32 = 85
PropertyIdentifierDescription uint32 = 28
PropertyIdentifierVendorName uint32 = 121
PropertyIdentifierVendorIdentifier uint32 = 120
PropertyIdentifierModelName uint32 = 70
PropertyIdentifierFirmwareRevision uint32 = 44
PropertyIdentifierApplicationSoftwareVersion uint32 = 12
PropertyIdentifierOutOfService uint32 = 81
PropertyIdentifierUnits uint32 = 117
PropertyIdentifierPolarity uint32 = 84
PropertyIdentifierNumberOfStates uint32 = 74
PropertyIdentifierStateText uint32 = 110
PropertyIdentifierApduLength uint32 = 399
PropertyIdentifierReferencePort uint32 = 483
PropertyIdentifierBacnetIPUDPPort uint32 = 412
PropertyIdentifierBacnetIPMode uint32 = 408
PropertyIdentifierIPAddress uint32 = 400
PropertyIdentifierIPSubnetMask uint32 = 411
PropertyIdentifierIPDefaultGateway uint32 = 401
PropertyIdentifierVendorName uint32 = 121
PropertyIdentifierVendorIdentifier uint32 = 120
PropertyIdentifierModelName uint32 = 70
PropertyIdentifierFirmwareRevision uint32 = 44
PropertyIdentifierApplicationSoftwareVersion uint32 = 12
PropertyIdentifierOutOfService uint32 = 81
PropertyIdentifierUnits uint32 = 117
PropertyIdentifierPolarity uint32 = 84
PropertyIdentifierNumberOfStates uint32 = 74
PropertyIdentifierStateText uint32 = 110
PropertyIdentifierApduLength uint32 = 399
PropertyIdentifierReferencePort uint32 = 483
PropertyIdentifierBacnetIPUDPPort uint32 = 412
PropertyIdentifierBacnetIPMode uint32 = 408
PropertyIdentifierIPAddress uint32 = 400
PropertyIdentifierIPSubnetMask uint32 = 411
PropertyIdentifierIPDefaultGateway uint32 = 401
)

// -- BACnet engineering units (Engineering_Units enumeration) ---------------
Expand Down Expand Up @@ -106,25 +106,38 @@ const BACnetNetworkPortDefault uint32 = 4194303
// "CHANGE ALL OF THIS BEFORE YOU SHIP" if you turn this example into your
// own device.
const (
DeviceName = "Rainbow"
DeviceName = "Chipkin Example B-SS"
DeviceDescription = "Chipkin CAS BACnet Stack example - B-SS (Smart Sensor) profile. " +
"Demonstrates DS-RP-B: ReadProperty plus Who-Is/I-Am with read-only sensor objects."
VendorName = "Chipkin Automation Systems"
VendorName = "Chipkin Automation Systems"
VendorIdentifier uint32 = 389
ModelName = "CAS BACnet Stack Example - B-SS"
FirmwareRevision = "1.0.0"
ApplicationSoftwareVersion = "1.0.0"

AnalogInputInstance uint32 = 1 // "Bronze"
BinaryInputInstance uint32 = 1 // "Emerald"
MultiStateInputInstance uint32 = 1 // "Hot Pink"
MultiStateInputNumberOfStates uint32 = 3
BinaryInput1Value uint32 = 0 // inactive - the series-wide starting value
MultiStateInput1Value uint32 = 1 // state 1 ("On")
MaxApduLength uint32 = 1476 // BACnet/IP APDU length
ModelName = "CAS BACnet Stack Example - B-SS"

AnalogInputInstance uint32 = 1 // "Bronze"
BinaryInputInstance uint32 = 1 // "Emerald"
MultiStateInputInstance uint32 = 1 // "Hot Pink"
MultiStateInputNumberOfStates uint32 = 3
BinaryInput1Value uint32 = 0 // inactive - the series-wide starting value
MultiStateInput1Value uint32 = 1 // state 1 ("On")
MaxApduLength uint32 = 1476 // BACnet/IP APDU length
)

// MultiStateInputStateText: the three named states of Multi-State Input 1
// ("Hot Pink"), 1-indexed per BACnet's State_Text array (index 0 = state 1).
var MultiStateInputStateText = [3]string{"On", "Off", "Auto"}

// ApplicationSoftwareVersion / FirmwareRevision: the Device object's
// Application_Software_Version (12) and Firmware_Revision (44). These
// cannot be plain compile-time constants: Application_Software_Version must
// track this example's own real version (main.go's appVersion, which
// common cannot import without creating an import cycle), and
// Firmware_Revision must reflect the underlying CAS BACnet Stack's REAL
// version - it names the platform underneath this app, not the app itself -
// which is only known at runtime via the stack's own version-getter API.
// main.go's run() sets both, once, at start-up (see printVersion() and the
// assignment right after it) before the BACnet/IP socket is bound or any
// callback is registered, so there is no concurrent-write hazard.
var (
ApplicationSoftwareVersion = "0.0.0"
FirmwareRevision = "0.0.0.0"
)
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 `goCallbackGetProperty*` callback in `cas_bacnet_stack/cas_bacnet_stack_adapter.go`. 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
16 changes: 15 additions & 1 deletion main.go
Original file line number Diff line number Diff line change
Expand Up @@ -33,7 +33,7 @@ import (
)

const appName = "BACnet B-SS (Smart Sensor) Example - Go"
const appVersion = "1.0.0"
const appVersion = "1.0.2"

func printVersion() {
fmt.Printf("%s v%s (common v%s)\n", appName, appVersion, common.CommonVersion)
Expand Down Expand Up @@ -92,6 +92,20 @@ func run() int {
// for where to copy it.
printVersion()

// The Device object's Application_Software_Version (12) and
// Firmware_Revision (44) can't be plain constants: Application_Software_Version
// must track this example's own real version, and Firmware_Revision must
// reflect the underlying CAS BACnet Stack's REAL version, read from the
// stack itself - it names the platform underneath this app, not the app
// itself. Computed once here (the native library is already known to be
// linked and working - printVersion() above just called it), using the
// same 4 getter calls printVersion() uses for the banner, before the
// socket is bound or any callback is registered.
common.ApplicationSoftwareVersion = appVersion
common.FirmwareRevision = fmt.Sprintf("%d.%d.%d.%d",
bacnet.GetAPIMajorVersion(), bacnet.GetAPIMinorVersion(),
bacnet.GetAPIPatchVersion(), bacnet.GetAPIBuildVersion())

// --- Bind the BACnet/IP socket -------------------------------------------
if err := bacnet.SetupUDP(portValue); err != nil {
return fail("could not bind UDP port %d: %v (is another BACnet device already running on this machine? Try --port.)", portValue, err)
Expand Down
Loading