Testing and interop
The OT Fabric BACnet projects are tested against independent open-source BACnet stacks running in containers, and their requests are checked against Wireshark’s dissector. This is strong evidence of protocol correctness. It is not evidence about any vendor’s controller.
bacnet-interop: the reference stacks
Section titled “bacnet-interop: the reference stacks”bacnet-interop provides Dockerized BACnet/IP devices built from independent implementations, deterministic device fixtures, BBMD and routed-network scenarios and codec test vectors. It is maintained independently of go-bacnet and can be used by any BACnet client. The adapters never patch an upstream stack: they load a fixture, start the stack’s own server, and supply an object or service in adapter code only where the stack has none, which the capability matrix marks.
Latest images: v0.10.0.
| Image | Upstream | Role |
|---|---|---|
bacnet-interop-bacnet-stack |
bacnet-stack 1.6.1 (C) | Device oracle (+ BBMD) |
bacnet-interop-bacpypes3 |
BACpypes3 0.0.110 (Python) | Device oracle (+ BBMD) |
bacnet-interop-bacnet4j |
BACnet4J 6.1.1 (Java) | Device oracle (+ BBMD) |
bacnet-interop-worldiety |
worldiety/bacnet 3cb2aa80 (Go) |
Native transport and APDU layer; the adapter supplies the object model |
bacnet-interop-sysiobacnet |
System.IO.BACnet, NuGet BACnet 4.0.0 (C#) |
Device oracle; the stack behind YABE |
bacnet-interop-bacnet-stack-router |
bacnet-stack 1.6.1 apps/router (C) |
Native BACnet/IP to BACnet/IP router |
bacnet-interop-bip-router |
none (Python, written for the tests) | Topology aid for routed scenarios; not evidence about any stack |
Fixtures
Section titled “Fixtures”Device models live under fixtures/device/ and are selected with DEVICE_FIXTURE_FILE and FIXTURE. Every device image serves device-baseline-v2 by default.
| Fixture | Adds | Served by |
|---|---|---|
| v1 | Device, Analog Value, Binary Value (frozen) | All device peers |
| v2 | Trend Log (default) | All device peers (BACpypes3 skips the Trend Log) |
| v3 | Notification Class, Event Enrollment, alarms | bacnet-stack, BACnet4J; System.IO.BACnet with an adapter alarm model |
| v4 | File objects | bacnet-stack, BACnet4J, System.IO.BACnet |
| v5 | Object lifecycle and list mutation | bacnet-stack, BACnet4J |
| v6 | Messaging and time synchronization | bacnet-stack, BACpypes3, BACnet4J |
| v7 | Identity (Who-Am-I / You-Are) | bacnet-stack |
| v8 | Life safety | bacnet-stack, BACnet4J |
| v9 | 40 more objects; an Object_List that does not fit a 50-octet APDU | bacnet-stack only |
fixtures/codec/ holds wire vectors for go-bacnet’s codec tests; fixtures/topology/ and fixtures/bbmd/ hold the routed and BBMD scenarios.
Consumer contract
Section titled “Consumer contract”Every image writes JSON Lines to stdout. The first line is the ready event, written once the BACnet/IP socket is bound:
{"event":"ready","adapter":"bacnet-stack","version":"0.10.0","fixture":"device-baseline-v2","address":"0.0.0.0:47808","peer_version":"bacnet-stack-1.6.1"}Some adapters then write operation events for requests that have no observable reply (for example time synchronization). Diagnostics go to stderr. A consumer pins images by digest from the release’s manifest.json:
# by tag, for a quick local runBACNET_STACK_IMAGE=ghcr.io/otfabric/bacnet-interop-bacnet-stack:v0.10.0# by digest, for CIBACNET_STACK_IMAGE=ghcr.io/otfabric/bacnet-interop-bacnet-stack@sha256:<digest>Building and checking the images
Section titled “Building and checking the images”make validate-fixtures # manifest, schemas, fixture metadatamake dissect-fixtures # codec vectors against Wireshark's dissector (needs tshark)make build # all adapter images as bacnet-interop-<name>:localmake smoke # ready event (and a Who-Is probe) per imagemake ci # validate-fixtures + build + smokemake build-<name> builds one image. Published images are linux/amd64 and linux/arm64.
Running the go-bacnet interop tests
Section titled “Running the go-bacnet interop tests”go-bacnet owns the assertions (interop/, build tag interop); bacnet-interop owns the images and fixtures. CI pulls the release pinned in interop/bacnet-interop-pin.json by digest, and a second lane builds the adapters from bacnet-interop main.
-
Install Docker and jq, and check out
go-bacnetandbacnet-interopside by side (or setBACNET_INTEROP_ROOT). -
From the go-bacnet checkout, run the target you need:
Terminal window make interop-pinned # pull the pinned images, run everything, fail on anything missingmake interop # run against locally built :local images, skip what is missingmake dissector # check the client's requests against Wireshark (needs tshark, no Docker) -
Select tests with
INTEROP_RUN, for examplemake interop-pinned INTEROP_RUN='TestReadRange'.interop/doc.golists the environment variables.
Each test starts its own peer container. On Linux the tests talk to the containers directly; on Docker Desktop each test re-executes inside a Go container on the peer’s network, and a full run takes eight to nine minutes. A peer that behaves differently is not skipped: the difference is asserted, so a change in the peer is noticed.
The last full run against the v0.10.0 images: 206 scenarios pass, 11 skip (things a stack does not have), none fail.
What is validated
Section titled “What is validated”go-bacnet grades its evidence: ✅ the peer stack executes the service and the result is asserted; A the adapter supplies the semantics; D codec evidence only (a stack’s decoder or encoder agrees, nothing executes); W only Wireshark checks the request layout.
| Area | Evidence |
|---|---|
Who-Is / I-Am, broadcast Discover, Who-Has |
Multi-peer, all five stacks |
| ReadProperty, ReadPropertyMultiple (with per-property errors), WriteProperty | Multi-peer |
| WritePriority / RelinquishPriority | BACpypes3, BACnet4J, System.IO.BACnet |
ReadRange by position, sequence number and time; ReadRangeAll |
bacnet-stack, BACnet4J (others through adapters) |
| Atomic file read and write, CreateObject / DeleteObject, list element services | bacnet-stack, BACnet4J |
| SubscribeCOV with renewal and cancel | bacnet-stack, BACpypes3, BACnet4J |
| GetAlarmSummary, GetEventInformation, AcknowledgeAlarm | bacnet-stack, BACnet4J |
| Segmented ComplexACK receive | BACpypes3, BACnet4J, System.IO.BACnet |
| Segmented request send | BACpypes3, System.IO.BACnet (BACnet4J rejects segmented requests) |
| Routed device through bacnet-stack-router and bip-router | Multi-peer |
| Foreign-device registration, Read-BDT, Read-FDT, Delete-FDT entry | bacnet-stack, BACpypes3, BACnet4J |
| SubscribeCOVPropertyMultiple, COVNotificationMultiple, audit, VT | Codec evidence only |
| AuthRequest | No stack implements it; each peer’s refusal is asserted |
The make dissector oracle captures 45 requests and has Wireshark’s tshark dissect them, verified with Wireshark 4.2, 4.4 and 4.6. It checks requests only, and agreement with Wireshark is not acceptance by a device.
Peer behaviour worth knowing
Section titled “Peer behaviour worth knowing”The tests pin down differences between stacks that also matter on real networks:
- Do not rely on the Device wildcard (instance 4194303) to identify a device: BACpypes3 and the Worldiety adapter answer
unknown-object. - Do not treat a clear MORE_ITEMS flag as the end of a trend log; page until LAST_ITEM.
- Expect a refusal as an Error or an Abort.
- Do not assume a failed WritePropertyMultiple names the element that failed, and assume the writes before it were applied.
- bacnet-stack cannot send segmented replies; BACnet4J rejects segmented requests.
- The peers differ in which broadcast address they receive (subnet-directed or 255.255.255.255), so go-bacnet sends both.
The peer tests found real defects in go-bacnet, all fixed in v0.4.0: broadcasts only to 255.255.255.255, unreadable empty lists, ReadRangeAll stopping after one page, audit codec mismatches, dropped Errors with a foreign error-choice, and swapped Router-Busy and Router-Available codes.
bacnetctl testing
Section titled “bacnetctl testing”bacnetctl runs every command automatically against an in-process simulated device (internal/bactest) over loopback, in all three output formats. That proves commands, output and exit codes, but client and simulator share go-bacnet’s codecs, so it does not prove interoperability. Before 0.1.0 the commands were also run by hand against the five stacks of bacnet-interop v0.10.0 plus bacnet-stack’s router. events was tested against the simulated device only.
Field validation
Section titled “Field validation”go-bacnet’s docs/FIELD_VALIDATION.md is the checklist for recording physical-device evidence. For each device it records make and model, firmware, protocol revision, topology (local, routed, BBMD or foreign device), library version, interop pin and date, then which services were exercised (Who-Is, ReadProperty, ReadPropertyMultiple, WriteProperty only where safe, and optional COV, events, files and routed paths) and notes on segmentation, timeouts and Error, Reject and Abort responses. Completed runs go into the hardware table of docs/COMPATIBILITY.md, which is empty today.