Skip to content

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 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

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.

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:

Terminal window
# by tag, for a quick local run
BACNET_STACK_IMAGE=ghcr.io/otfabric/bacnet-interop-bacnet-stack:v0.10.0
# by digest, for CI
BACNET_STACK_IMAGE=ghcr.io/otfabric/bacnet-interop-bacnet-stack@sha256:<digest>
Terminal window
make validate-fixtures # manifest, schemas, fixture metadata
make dissect-fixtures # codec vectors against Wireshark's dissector (needs tshark)
make build # all adapter images as bacnet-interop-<name>:local
make smoke # ready event (and a Who-Is probe) per image
make ci # validate-fixtures + build + smoke

make build-<name> builds one image. Published images are linux/amd64 and linux/arm64.

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.

  1. Install Docker and jq, and check out go-bacnet and bacnet-interop side by side (or set BACNET_INTEROP_ROOT).

  2. From the go-bacnet checkout, run the target you need:

    Terminal window
    make interop-pinned # pull the pinned images, run everything, fail on anything missing
    make interop # run against locally built :local images, skip what is missing
    make dissector # check the client's requests against Wireshark (needs tshark, no Docker)
  3. Select tests with INTEROP_RUN, for example make interop-pinned INTEROP_RUN='TestReadRange'. interop/doc.go lists 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.

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.

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 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.

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.