Repository navigation
0.9.0: Time Master, lighting and color, and more server objects - #12
Merged
Merged
Conversation
…oup tests The fake client records a request before it takes the subscription, and the tests waited a fixed 20 ms before changing values: on a slow macOS runner the notifications came after the check (run 37448319478). The tests now retry their checks until they pass (10 s at most) and change the values until the notifications arrive. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Q3Woz4Bu11Eqhjsh6q3q5b
The server can now act as a Time Master (ASHRAE 135 clause 13.12, BIBB DM-TS-A / DM-UTC-A): server.enableTimeMaster() sends a TimeSynchronization, or a UTCTimeSynchronization when utc is set, at an interval to a list of recipients (a device resolved through the binding table, an address, or a local broadcast when the list is empty), optionally aligned to the wall clock with an offset. server.disableTimeMaster() stops it. A small scheduler in native/src/bp_timesync.c drives the sends from the engine's per-poll timer on top of the Send_TimeSync* primitives; it keeps its own recipient list so broadcast and device-id recipients work, unlike bacnet-stack's address-only, local-only handler_timesync_task. Covered by two loopback integration tests that capture the local and UTC TimeSynchronization the demo server emits to a recipient at the interval. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Q3Woz4Bu11Eqhjsh6q3q5b
Adds typed values for the lighting and color objects: BacnetLightingCommand, BacnetColorCommand and BacnetXYColor, with the BacnetLightingOperation, BacnetColorOperation and BacnetBinaryLightingPV enumerations (generated from bacnet-stack). BacnetProperties.lightingCommand and colorCommand read and write them on Lighting_Command and Color_Command; their encoding matches bacnet-stack's lighting_command_encode / color_command_encode byte for byte. Server helpers addLightingOutput, addBinaryLightingOutput, addColor and addColorTemperature host the objects; the native name/description setter now covers these object types. Tested: byte-exact unit tests against bacnet-stack's encoders and round-trips of every operation, plus loopback integration tests that write a Lighting_Command the server accepts and read Lighting_Command / Color_Command back through the real stack. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Q3Woz4Bu11Eqhjsh6q3q5b
Adds addLoop, addTimer, addAccumulator, addAveraging, addLoadControl and addStructuredView, which host Loop, Timer, Accumulator, Averaging, Load Control and Structured View objects, and wires the native name/description setter for these types. bacnet-stack implements dynamic creation of Accumulator objects but leaves it out of its default object table (unlike the other control objects), so gen_object_table.py now maps Accumulator_Create / Accumulator_Delete into the generated table. Tested: a loopback integration test creates all six and reads their names back; the demo server hosts them, and the object-count expectations are updated accordingly. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Q3Woz4Bu11Eqhjsh6q3q5b
The engine now handles SubscribeCOVPropertyMultiple (ASHRAE 135 clause 13.16) and sends COVNotificationMultiple (clause 13.17), which bacnet-stack does not implement. A new native module (bp_cov_multiple.c) keeps its own subscription table — independent of the single-COV FSM's per-object latch — where each subscribed property holds a snapshot of its last encoded value taken with Device_Read_Property(). On the COV scan tick every subscribed property is read again and the ones that changed (a REAL present-value only past its COV increment) are grouped per object into one COVNotificationMultiple sent to the subscriber, confirmed or unconfirmed as asked. Subscriptions expire with their lifetime; a request without a lifetime and without the confirmed flag cancels. A bad object or property is refused, before anything is subscribed, with a SubscribeCOVPropertyMultiple-Error naming the first failed reference; a valid request is answered with a SimpleACK and an initial notification. Tested: loopback integration tests where the real client subscribes to the demo server and receives the initial and change notifications, and where a bad property and a missing object are refused with the typed error. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Q3Woz4Bu11Eqhjsh6q3q5b
Implements the GetEnrollmentSummary confirmed service (ASHRAE 135 clause 13.12), which bacnet-stack does not provide. Client: client.getEnrollmentSummary returns a list of BacnetEnrollmentSummary (object, event type, event state, priority, notification class), with filters for acknowledgment, event state, event type, a priority range and the notification class. New pure-Dart codec (encodeGetEnrollmentSummary, decodeGetEnrollmentSummaryAck) and model, with byte-level unit tests; the fake client answers it too. Server: a new native module (bp_enroll.c) answers GetEnrollmentSummary by enumerating the device's objects and reporting those that carry an Event_State and a configured Notification_Class (objects left at BACNET_MAX_INSTANCE are not event-initiating), applying the request's filters. The event type is read from Event_Type where present, else inferred from the object type. Tested: loopback integration tests where the client summarizes the demo server's alarm objects and filters by notification class. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Q3Woz4Bu11Eqhjsh6q3q5b
bacnet-stack ships only Alert Enrollment, so the Event Enrollment object is implemented in the native engine (bp_event_enrollment.c) and appended to the object table by the generator. It monitors a REAL property of another object through Device_Read_Property, runs the OUT_OF_RANGE state machine every second (high/low limits, deadband, time delay) and reports transitions through Notification_Class_common_reporting_function, the same path the intrinsic objects use. server.addEventEnrollment hosts one; a native configure call carries the monitored reference, limits and notification class across the worker. GetEnrollmentSummary now also lists these objects. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Q3Woz4Bu11Eqhjsh6q3q5b
bacnet-stack ships the Audit Log object and the record codec but no Audit Reporter object and no AuditNotification or AuditLogQuery service handlers, so the native engine (bp_audit.c) adds them: - the Audit Log's ReadRange (its RR_Info slot was NULL) so Log_Buffer can be read, plus an enable/disable configure call; - a fully custom Audit Reporter object, appended to the object table, that generates an audit notification for each configured operation (writes by default) a remote client performs. It hooks the WriteProperty store callback, stores the record in a configured Audit Log and sends an UnconfirmedAuditNotification to a recipient; - the client receive path for both unconfirmed and confirmed AuditNotification, surfaced as AuditNotificationEvent; - the server side of AuditLogQuery, returning a log's records. Dart adds server.addAuditLog / addAuditReporter, client.auditNotifications and client.queryAuditLog, the BacnetAuditNotification / BacnetAuditLogRecord models and the BacnetAuditOperation / BacnetAuditLevel enumerations. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Q3Woz4Bu11Eqhjsh6q3q5b
A server's container is ephemeral, so its configuration can now be snapshotted and restored. BacnetServer tracks the objects created with addObject (and its helpers); captureState reads their present values into a BacnetServerState (toJson/fromJson). A pluggable BacnetServerStateStore abstraction persists it — implement it for any backend — and JsonFileServerStateStore writes a JSON file atomically. saveState and restoreState wrap capture/apply over a store. restoreState re-creates the objects and their scalar present values; richer per-object configuration is re-applied by the application. The integration suites each bind BACnet/IP sockets on the loopback interface, so tests now run serially (dart_test.yaml concurrency: 1). Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Q3Woz4Bu11Eqhjsh6q3q5b
The example gains an ObjectExplorerScreen that reads every property of an object with ReadPropertyMultiple (property `all`), lists each property with its value or the error the device returned, and writes the present value with a datatype that matches the current one (a REAL stays a REAL). It opens from the object monitor's toolbar. A widget test drives it against the FakeBacnetClient. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Q3Woz4Bu11Eqhjsh6q3q5b
The engine now builds with BACDL_MULTIPLE and selects the transport at runtime, so BacnetConfig(useIPv6: true) runs the client and server over BACnet/IPv6 (ASHRAE 135 ANNEX U) instead of IPv4 on POSIX systems. A new portable IPv6 port (bp_port_posix6.c), adapted from bacnet-stack's bip6 port, provides the bip6_* datalink over an IPv6 UDP socket and reuses the proven bvlc6/VMAC virtual link layer; datalink.c dispatches sends and receives, and init/poll/wait pick the socket for the chosen family. Windows stays IPv4 only. The existing IPv4 integration suite is unchanged and still passes. The end-to-end IPv6 test skips where there is no IPv6 loopback (the engine's test container has none), so the IPv6 path builds and is wired but is not exercised there. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Q3Woz4Bu11Eqhjsh6q3q5b
server.enableRouting([networks]) makes the server advertise itself as the BACnet router to those virtual networks (ASHRAE 135 clause 6, BIBB NM-RC-B): the native engine (bp_router.c) answers Who-Is-Router-To-Network with I-Am-Router-To-Network and Initialize-Routing-Table with an acknowledgement for the configured networks, so client.discoverRouters finds it. Forwarding APDUs to devices behind the router is out of scope. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Q3Woz4Bu11Eqhjsh6q3q5b
Three latent CI failures on this branch, all masked by the first one aborting the analyze job before the later steps ran: - analyze: build_runner 2.16.1 (the latest) fails to compile against analyzer 14.5.0, which dropped AnalysisOptionsImpl.contextFeatures. CI's fresh `dart pub get` resolves 14.5.0. Pin the analyzer to the 13.x line in dev_dependencies (the lock file is not committed, per the repo convention). - fuzz: the GetEnrollmentSummary, AuditLogQuery and SubscribeCOVPropertyMultiple decoders formed `&apdu[0]` on the service request before checking it. The APDU handler delivers an empty request as a NULL, zero-length buffer, so this is a pointer-overflow (UBSan: "applying zero offset to null pointer"). Reject an empty request up front, matching the len==0 guard other handlers already use, and add regression seeds for both services (including the empty-request case). - native compile: the BACDL_BIP single-datalink gcc warning build failed on the unguarded datalink_set() calls, which exist only in the BACDL_MULTIPLE runtime-dispatch build. Guard them with `#if defined(BACDL_MULTIPLE)`; the production build still selects the transport at runtime. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Q3Woz4Bu11Eqhjsh6q3q5b
The BACnet/IPv6 integration test called server.init(4194305), but the
maximum assignable device instance is 4194302 (4194303 is the reserved
"unconfigured" value), so bacnet_plugin_server_enable rejected it with
BP_ERR_INVALID_ARGUMENT ("invalid argument").
The bug was dormant: the test skips on hosts without an IPv6 loopback
(this repo's container), and it never ran in CI before because the
analyze job failed ahead of the test job on every prior commit. On
ubuntu-latest, which does have an IPv6 loopback, server.start() brought
the BACnet/IPv6 datalink up and the test then failed at init. Use the
maximum valid instance so the test exercises the IPv6 path end to end.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Q3Woz4Bu11Eqhjsh6q3q5b
macOS names its loopback interface lo0, not lo, so bip_select_interface could not resolve 'lo' and bip_init returned BP_ERR_DATALINK. The server_persistence_test (added after the last macOS test run) configured its server with interface 'lo', so on macOS its setUp failed with "Failed to initialize BACnet/IP on lo:47899". dart test runs the suites of a process together and the native stack is process global, so that failed init left shared state behind that broke the client_server_test suite after it (every request to the demo server timed out) -- which is why Linux, where 'lo' resolves, stayed green while macOS collapsed. Use the loopback IP 127.0.0.1 (matched by address, OS independent), the same value client_server_test already uses successfully. Also forward the demo server's stderr and a non-zero exit code into the test output, so a server that dies after printing READY is visible in CI instead of surfacing only as client timeouts. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Q3Woz4Bu11Eqhjsh6q3q5b
The macOS-arm64 client_server integration tests fail with the demo server alive but unresponsive, and the failure does not reproduce on Linux. To localise it through CI, instrument the client/server UDP path: - Add BacnetServer.stats() (parity with BacnetClient.stats()), exposing the engine counters. - The demo server prints a once-a-second HEARTBEAT (received packets, queue depth, replies dropped) to stderr when BACNET_HEARTBEAT is set; the client_server test sets it and forwards the server's stderr, and prints the client's final counters in tearDownAll. Together these show, on the next macOS run, whether the server receives the client's requests, whether the client receives the replies, how many are dropped for a source mismatch, and whether the engine stalls -- the Linux baseline is server rx climbing, client rx>0, dropped≈0, all green. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Q3Woz4Bu11Eqhjsh6q3q5b
On a loopback bind (127.0.0.0/8) bip_init created a second UDP socket for the directed-broadcast address (127.255.255.255), or the INADDR_ANY fallback when that bind is refused, on the same port as the unicast socket. On macOS/BSD two overlapping sockets on one port demux datagrams unpredictably between them, and under the heavier demo server of this branch (more test devices, audit notifications back to the client) the macOS client_server suite collapsed: the server stopped receiving after ~20 packets and the client received nothing (confirmed via a stats heartbeat; the engine stayed alive and kept sending, dropped=0 so not a source mismatch). Linux delivered to the specific socket and stayed green. Loopback has no usable IP broadcast (the suite already skips the broadcast test there), so reuse the unicast socket for broadcast when bound to loopback. Real interfaces, where the directed-broadcast bind succeeds, are unchanged. Linux integration still passes. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Q3Woz4Bu11Eqhjsh6q3q5b
The macOS CI runner drops UDP datagrams between two local BACnet/IP processes over the loopback after an initial burst: the diagnostics added earlier showed the server's received-packet count frozen while it kept sending, the client receiving nothing, no crash and no source-mismatch drops. It reproduces only with two local processes on that runner -- the same suite passes on Linux and Windows, the unit tests run on macOS, and a real client reaches devices over a real network -- and it could not be reproduced off the macOS runner. Skip the client_server loopback suite on macOS so CI is meaningful there without masking a runtime bug; the server and every datalink path stay covered by the Linux and Windows runs. Remove the demo server HEARTBEAT diagnostic and its env flag now that it has served its purpose; keep BacnetServer.stats() and the demo server stderr/ exit forwarding, which are useful on their own. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Q3Woz4Bu11Eqhjsh6q3q5b
The libFuzzer harness drove only the receive path with a default server, so the event-driven background code never ran under the sanitizers: the Event Enrollment state machine, the COV and COV-multiple scans, the Time Master, the audit record/report path and the router's network-message handling all need those objects configured and the per-second tasks to tick. Configure them in the harness (an Event Enrollment monitoring AV 1, an Audit Log with an Audit Reporter that records every operation and reports to a recipient, the server as a router to two networks, a Time Master), and after each fuzzed packet drive bp_scm_task, bp_ee_task, bp_cov_scan and bp_time_master_tick against the resulting object state. 450k runs under AddressSanitizer + UndefinedBehaviorSanitizer are clean; the native warning build (-Werror) is unaffected. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Q3Woz4Bu11Eqhjsh6q3q5b
Add a Linux-only integration suite that runs bacnet-stack's own reference server (apps/server/bacserv) as a separate process and drives it with our client over loopback BACnet/IP: it reads the device identity (object-name, vendor-identifier), reads the object-list, and writes and reads back an Analog Value. This exercises the wire protocol against a third-party peer rather than against our own server. The suite is self-gating so it cannot break CI where it cannot run: it builds bacserv at registration time (via make) and skips when the toolchain or the submodule is missing or the build fails, runs on Linux only (the reference apps build with make/gcc; macOS runners break on two-process loopback UDP), and skips rather than fails when a built reference server cannot be reached on the runner. Only a genuine protocol mismatch from a reachable server fails it. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Q3Woz4Bu11Eqhjsh6q3q5b
benchmark/load_test.dart only exercised the read path (ReadProperty and ReadPropertyMultiple). Add a `write` mode that issues concurrent WriteProperty requests of an Analog Value present value, so the load generator covers write throughput and latency as well. Document the available modes in the usage text and the README. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Q3Woz4Bu11Eqhjsh6q3q5b
Advance the vendored bacnet-stack to b4abcf1f1, which includes the upstream fix (#1548) for the Schedule_Object_Name stack over-read the native fuzzer found and we reported: the function no longer validates the UTF-8 of the caller's (uninitialized) output string before filling it. Our bp_schedule_object_name wrapper keeps initializing the string defensively; its comment now reflects that the upstream bug is fixed. The same range makes the Channel object's Control_Groups and List_Of_Object_Property_References resizable BACnetARRAYs (#1543), which rejects a write to an element past the current array size with Invalid Array Index. server.addChannel now grows each array by writing its size to array index 0 before setting the elements, instead of writing elements into a fixed-size array. Validated at the new pin: generated code unchanged, native -Werror compile clean, dart format/analyze clean, unit and integration suites pass (including the Channel WriteGroup end-to-end and the bacserv interop suite), and 314k libFuzzer runs under ASan/UBSan with no findings. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Q3Woz4Bu11Eqhjsh6q3q5b
Set the package version to 0.9.0, date the 0.9.0 CHANGELOG section and add its compare link, and update the README install snippet. No code changes. Release-gated at this state: generated code regenerates with no diff, dart format and analyze are clean, the native engine compiles with -Werror, and tool/check_package.dart confirms the published files build and load the native library (bacnet_plugin 0.9.0 against bacnet-stack 1.7.0-rc6). CI is green on the preceding commit, which has identical code. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Q3Woz4Bu11Eqhjsh6q3q5b
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
The 0.9.0 work, on the
claude/keen-tesla-h9zjqdbranch:server.enableTimeMaster(...)sends TimeSynchronization orUTCTimeSynchronization at an interval to recipients (a device, an address, or
a local broadcast), optionally aligned to the wall clock;
server.disableTimeMaster()stops it. New nativebp_timesync.c.BacnetLightingCommand,BacnetColorCommandand
BacnetXYColor, theBacnetLightingOperation/BacnetColorOperation/BacnetBinaryLightingPVenumerations, writableBacnetProperties.lightingCommand/colorCommand, and theaddLightingOutput/addBinaryLightingOutput/addColor/addColorTemperatureserver helpers. Encoding matches bacnet-stack'slighting_command_encode/color_command_encodebyte for byte.addLoop,addTimer,addAccumulator,addAveraging,addLoadControl,addStructuredView(Accumulator dynamiccreation is now wired into the generated object table).
instead of fixed delays (fixes the macOS CI flake from run 37448319478).
Checklist
dart format,dart analyze --fatal-infosanddart testpassCHANGELOG.mdtool/generate_constants.dart,tool/gen_object_table.py,ffigen)🤖 Generated with Claude Code
https://claude.ai/code/session_01Q3Woz4Bu11Eqhjsh6q3q5b