From 8058d5e41a9917fdfc377d724fe7cfd6d19d385d Mon Sep 17 00:00:00 2001 From: SWS-Chipkin Date: Tue, 22 Sep 2026 01:40:07 -0700 Subject: [PATCH 1/3] fix: wire Application_Software_Version and Firmware_Revision to real values Both properties were hardcoded const strings ("1.0.0") in common/constants.go and never updated as the example's real version advanced (same bug found and fixed in the sibling BACnetProfileExample-B-SCHUB-CPP v1.1.13 via a real device read with CAS BACnet Explorer). Go's const cannot hold a runtime-only value (the stack's own version) or reference a value from a package that imports it back (common cannot import main, which imports common), so ApplicationSoftwareVersion and FirmwareRevision moved from const to package-level var in common/constants.go, with start-up placeholder defaults. main.go's run() now sets both once, right after printVersion() confirms the cgo-linked native library works, before the socket is bound or any callback is registered: ApplicationSoftwareVersion <- appVersion; FirmwareRevision <- built from bacnet.GetAPI{Major,Minor,Patch,Build}Version() (the same 4 calls printVersion() already uses for the banner) - it names the underlying platform, not this app. Bumped appVersion to 1.0.1. Verified with `go build -o bacnet-b-ss-go.exe .` (the README's documented build command - clean, no errors) and a real ReadProperty via bacpypes3 against the running device: Application_Software_Version = "1.0.1", Firmware_Revision = "6.0.21.0". Co-Authored-By: Claude Sonnet 5 --- CHANGELOG.md | 29 +++++++++++++++++++++++++++++ common/constants.go | 18 ++++++++++++++++-- main.go | 16 +++++++++++++++- 3 files changed, 60 insertions(+), 3 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index ddf07c2..77a6d56 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -3,6 +3,35 @@ All notable changes to this example. The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/). +## [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 diff --git a/common/constants.go b/common/constants.go index 9191d05..64e1a48 100644 --- a/common/constants.go +++ b/common/constants.go @@ -112,8 +112,6 @@ const ( 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" @@ -128,3 +126,19 @@ const ( // ("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" +) + diff --git a/main.go b/main.go index b50eb2f..db470cb 100644 --- a/main.go +++ b/main.go @@ -33,7 +33,7 @@ import ( ) const appName = "BACnet B-SS (Smart Sensor) Example - Go" -const appVersion = "1.0.0" +const appVersion = "1.0.1" func printVersion() { fmt.Printf("%s v%s (common v%s)\n", appName, appVersion, common.CommonVersion) @@ -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) From d130024d1e9adde7b88267575f16add360b15ebf Mon Sep 17 00:00:00 2001 From: SWS-Chipkin Date: Tue, 22 Sep 2026 21:48:57 -0700 Subject: [PATCH 2/3] chore: rename Device from "Rainbow" to "Chipkin Example B-SS" Every example in the series announced the identical Object_Name "Rainbow" for its Device object, so two examples running on the same subnet were indistinguishable by name. Renamed to "Chipkin Example B-SS" (the series' BACnet profile code) so devices are identifiable on the network; sub-object names are unchanged. docs/colour-table.md (series root, not in this repo) updated to match. appVersion bumped 1.0.1 -> 1.0.2. gofmt -w re-aligned common/constants.go's const block after the longer value changed column widths. `go build ./...` still fails on the pre-existing, unrelated vendored-adapter header issue (submodules/cas-bacnet-stack/adapters/golang/CASBACnetStackAdapter.go: CASBACnetStackDLL.h not found) noted in an earlier session - not introduced by this change. Verified instead with `gofmt -l` (clean) and `go vet ./common/...` (clean). Co-Authored-By: Claude Sonnet 5 --- CHANGELOG.md | 12 +++++++++ README.md | 4 +-- common/constants.go | 59 ++++++++++++++++++++++----------------------- docs/PICS.md | 4 +-- docs/objects.json | 2 +- main.go | 2 +- 6 files changed, 47 insertions(+), 36 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 77a6d56..c2576ec 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -3,6 +3,18 @@ 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 diff --git a/README.md b/README.md index 5c72207..257f20d 100644 --- a/README.md +++ b/README.md @@ -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 | @@ -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 diff --git a/common/constants.go b/common/constants.go index 64e1a48..0f33a43 100644 --- a/common/constants.go +++ b/common/constants.go @@ -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) --------------- @@ -106,20 +106,20 @@ 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" - - 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 @@ -141,4 +141,3 @@ var ( ApplicationSoftwareVersion = "0.0.0" FirmwareRevision = "0.0.0.0" ) - diff --git a/docs/PICS.md b/docs/PICS.md index 58b6a58..7cde0ad 100644 --- a/docs/PICS.md +++ b/docs/PICS.md @@ -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 | @@ -119,7 +119,7 @@ handle data in all character sets. 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 | |---|---|---|:---:| diff --git a/docs/objects.json b/docs/objects.json index 75259ab..784a33e 100644 --- a/docs/objects.json +++ b/docs/objects.json @@ -3,7 +3,7 @@ { "type": "Device", "instance": 389001, - "name": "Rainbow", + "name": "Chipkin Example B-SS", "app": [ "Object_Name", "Description", diff --git a/main.go b/main.go index db470cb..654579a 100644 --- a/main.go +++ b/main.go @@ -33,7 +33,7 @@ import ( ) const appName = "BACnet B-SS (Smart Sensor) Example - Go" -const appVersion = "1.0.1" +const appVersion = "1.0.2" func printVersion() { fmt.Printf("%s v%s (common v%s)\n", appName, appVersion, common.CommonVersion) From 58c1ac9f28f54ddde2549e868f17738b22b48a7f Mon Sep 17 00:00:00 2001 From: SWS-Chipkin Date: Thu, 24 Sep 2026 16:10:02 -0700 Subject: [PATCH 3/3] ci: upload one copy of each release asset (fix duplicate-name publish failure) Every platform artifact carries the same docs (README.md, LICENSE, TUTORIAL.md, ...), and the publish job handed all of them to softprops/action-gh-release with `files: artifacts/**/*`. Uploading two assets with the same name makes the action fail part-way ("Not Found" updating asset metadata) and leaves a draft release behind - BACnetProfileExample-B-SCHUB-CPP v1.1.18 hit exactly this. The publish job now flattens the downloaded artifacts into release/, keeping one copy of each file name, and fails loudly if two files share a name but differ. Same asset set as before, each name once. Applied series-wide (the release job is a shared template, checked by tools/check-series.sh check 11). Co-Authored-By: Claude Opus 5.5 --- .github/workflows/release.yml | 20 +++++++++++++++++++- 1 file changed, 19 insertions(+), 1 deletion(-) diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 2a8ef59..5641fc5 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -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/*