Skip to content
Version 6.1.0
Contact Us

Changelog

The CAS BACnet Stack is updated regularly. This page lists every change in version 6, newest first, across 17 releases. The same changelog ships in every release package as CHANGELOG.md. Moving from version 5? Start with the version 5 to 6 migration guide.

  • rb bs-20261028-4 (#4637): the BACnet/IPv6 data link no longer lets a hostile node on the link steer it. A VMAC learned only from a Forwarded-NPDU’s original source (the relaying node’s claim, which any node can make to a NORMAL node) is kept in the VMAC table and in Virtual_MAC_Address_Table but is no longer used to address this node’s own messages, nor releases a message held for that address: a message to that address waits for the address’s own answer to a Virtual-Address-Resolution, as one to an unknown peer does, and the VMAC heard from an address itself replaces a relayed one at once. Before, one Forwarded-NPDU naming a peer’s address with a made-up VMAC sent this node’s messages for that peer to a VMAC the peer drops. A Virtual-Address-Resolution, Register-Foreign-Device, Delete-Foreign-Device-Table-Entry or Distribute-Broadcast-To-Network that arrived on a multicast group (the host passes the destination address) is dropped unanswered, so one spoofed datagram no longer draws a reply from every node on the link. No BVLC-Result, ACK or other reply is sent to a multicast or unspecified source address, and no VMAC is learned at one.
  • rb dv-20261019-36 (#2522 CR2-F06-018, #3281): an NPDU whose SNET is 0 or 65535, or whose DNET is 0, is now discarded when it is decoded, on every node (clause 6.2.2.1 allows DNET 1-65535 and SNET 1-65534; the stack never sends those values itself). Before, such a frame decoded as valid, so any answer that copied a spoofed SNET 65535 into its DNET became an internetwork-wide broadcast, and the router relayed the frame onward unchanged. The frame gets no reject (clause 6.4.4 reason 6 covers address lengths, not network numbers); a BACnet/SC hub answers it with the BVLC-Result NAK it gives any NPDU that fails to decode.
  • rb bs-20261015-1 (#3088 CR2-F01-031): an error code a host callback returns is sent with its clause-18 Error Class. BUSY is now OBJECT / BUSY (cl. 18.2, and cl. 12.53.5 for a Channel refusing a write while Write_Status is IN_PROGRESS); it was PROPERTY / BUSY. The same applies to every code clause 18 lists under exactly one of DEVICE, OBJECT, RESOURCES, SECURITY or SERVICES - for example CONFIGURATION_IN_PROGRESS is now DEVICE, as cl. 12.56 requires for a refused Network Port Command. A code listed under more than one class (OTHER, READ_ACCESS_DENIED, WRITE_ACCESS_DENIED, VALUE_OUT_OF_RANGE, …) keeps PROPERTY.
  • rb bs-20261015-1 (#3931): the stack’s own I-Am-Router-To-Network and Network-Number-Is replies are a local broadcast whatever the request’s addressing (cl. 6.4.2: “It shall always be transmitted with a broadcast MAC address”; cl. 6.4.20), on BACnet/IP and MS/TP. A Who-Is-Router-To-Network received as a unicast was answered with a unicast. A host’s own BACnetStack_SendIAmRouterToNetwork is unchanged. A reply to a request that arrived through another router (it carries DNET/DADR) is now broadcast on the local link too, so a second router on that link that reaches the same network may also forward it. BACnet/SC still answers on the requester’s connection (#3961).
  • rb bs-20261016-1 (#3930): a WriteProperty to a Network Port’s BBMD_Broadcast_Distribution_Table no longer replaces the BBMD’s broadcast distribution table immediately. As cl. 12.56.38 requires, the written table is held pending and Changes_Pending reads TRUE. ReadProperty returns the written table (12.56.38 (c)), and Read-Broadcast-Distribution-Table keeps returning the table in use (d). The written table takes effect on ReinitializeDevice ACTIVATE_CHANGES or WARMSTART, and Network_Port Command DISCARD_CHANGES drops it. The written list is still validated when it is written (#2549: duplicates removed, size cap, this BBMD’s own entry located), and a refused write leaves nothing pending.
  • Behaviour change: a client that wrote the table and expected it to take effect at once must now send ReinitializeDevice ACTIVATE_CHANGES. A host that registers no ReinitializeDevice callback now gets the write refused (Abort OTHER), as other pending Network Port writes are (#3257), because the table could never take effect. BACnetStack_AddBDTEntry and the other host-side BDT exports are unchanged and still edit the table in use directly; a written table replaces such edits whole when it is activated. This BBMD’s own entry in a written table is located when it is written, from the port’s address at that time. A build without STACK_OPTION_REINITIALIZE_DEVICE refuses every BDT write.
  • rb bs-20261024-10 (#4478): clearing the failover hub URI while the hub connector waits in TryFailover no longer makes it dial an empty URI (a failed attempt latched against an unconfigured failover hub, or an accepted one holding the connector in AWAITING_WEBSOCKET); with no failover hub configured the connector returns to the primary (AB.5.4).
  • rb bs-20261024-10 (#4479): changing or clearing a hub URI while the hub connection is CONNECTED (SetBACnetSCHubConnector, clearing the URI, and the other callers of that close) now sends the hub a Disconnect-Request before the socket is closed in the same call, as SuspendCommunication has since #3524 (AB.6.2.2); it dropped the connection unannounced.
  • rb bs-20261024-10 (#4484): the same for an initiated direct connection: Initiate_Enable going false, a Hello capabilities change, SuspendCommunication and the other local teardowns of a CONNECTED initiated connection send the peer a Disconnect-Request before the close, the initiating twin of #4336 (AB.6.2.2).
  • rb bs-20261024-11 (#4481): a reply the hub host node produces to a request addressed to its own VMAC (the Address-Resolution-ACK, the Advertisement answering a solicitation, and the BVLC-Result NAK of either) now carries the node’s VMAC as its Originating Virtual Address (AB.3.1.2). Without one it read as the hub function’s, and a CAS-stack hub connector dropped the ACK and ignored the NAK, so it never resolved the hub host node for a direct connection. A request with no Destination Virtual Address keeps the OVA-less reply.
  • rb bs-20261024-11 (#4482): the direct-connect URI binding table is bounded at 64 entries (the least recently recorded or resolved binding makes room), and the bindings to a URI are forgotten when an initiated connection to it fails, so the next unicast resolves the VMAC again instead of redialling a dead or reassigned URI for as long as the hub connection lasts.
  • rb bs-20261024-11 (#4485): BACnetStack_SetBACnetSCCertificateFileObjects refuses (returns false, changing nothing) one File bound as both the operational certificate File and the certificate signing request File, as #3670 did for the issuer slots: GENERATE_CSR_FILE would have written the request over the live certificate and raised Changes_Pending.
  • rb bs-20261024-12 (#4477): the MS/TP send queue is first in, first out. A new frame took the lowest free slot and the frame to send was the lowest occupied slot, so under steady traffic the older frames in higher slots were not sent until the producer paused, and the segments of a segmented message went out of order. Each queued frame now carries a sequence number and the oldest is sent first.
  • rb bs-20261024-12 (#4483): the direct-connect initiator keeps its reconnect timeout and its growing back-off per peer URI (AB.4, AB.6.1). Switching the single initiator between two peers reset the attempt count and left one timer for one URI, so a peer that did not answer was redialled as soon as the application sent to the other peer and back. The state is held for up to 16 peer URIs (the entry whose timeout ends first makes room, never the URI being switched to); a full reset of the initiator (a configuration change) clears it for every peer.
  • rb bs-20261024-13 (#4487, items CR4-F05-015, -019, -021, -022, -006): a refused BACnetSegmentACKPDU::SetSegmentAck leaves the object invalid (it kept the previous values under IsValid()); a BACnetErrorPDU::Decode refused after its error was allocated (unknown choice, undecodable body, trailing octets) leaves no half-decoded error behind and the error choice reset; BACnetMessageClass::Decode rejects an invalid choice tag as INVALID_TAG, not OTHER; BACnetUnconfirmedRequestPDU::Clone() keeps the discarded-as-unsupported-service flag as operator= does; a bare token after #endif in the responding state machine became a comment.
  • rb bs-20261024-14 (#4488, items CR4-F08-013, -006, -004, -010): the MS/TP send-failure log says MS/TP, not BACnetIP; GetBits and the sprintf debug buffer in MSTP.c are file-local, no longer unprefixed global symbols in customer firmware; the 31 #elif MSTP_DEBUG directives in MSTP.c are #elif defined(MSTP_DEBUG), so the file passes a compiler syntax check when MSTP_DEBUG is defined empty as MSTP.h says to define it; releasing an MS/TP data link’s Network Port claim clears the discarded-frame count (#3980) of the port it served.
  • rb bs-20261024-15 (#4489, items CR4-F09-003, -005, -013, -022): BACnetSCHeaderOption::SetHeaderMarker and SetHeaderAsProprietary refuse a non-zero length with no data behind it (the first dropped the data and reported success, the second read the missing data); a BVLC-Result ACK followed by extra octets is refused by BACnetSCBVLCResult::Decode (COMMUNICATION / UNEXPECTED_DATA) instead of decoding as an ACK; BACnetSCHubConnector::Reset() clears the router VMACs learned on the old hub; the failed-connection log’s header comment says a repeat failure is replaced and moved to the newest position.
  • rb bs-20261024-16 (#4487, items CR4-F05-014, -028, -016): BACnetConfirmedRequestPDU::SetServiceRequestAndTakeControl handed the request the PDU already holds freed it and stored the dangling pointer (a use-after-free); it now changes nothing and returns true, as the ComplexACK twin has since CR2-F05-040. BACnetErrorBase::Encode into a buffer too small for the error class keeps the enumeration’s SEGMENTATION_NOT_SUPPORTED instead of overwriting it with Abort OTHER (the #4318 fix had reached only BACnetError::Encode). BACnetComplexACKPDU::SetComplexAckAsSegment refuses a service-ack choice octet past the last confirmed service instead of converting it to the enum.
  • rb bs-20261024-17 (#4489, items CR4-F09-001, -004, -017, -023): the hub connector’s two BVLC-Result NAK builders omit the Destination Virtual Address when the request’s Originating Virtual Address is the Local Broadcast VMAC, as the validator’s builder does since #3659 (a NAK addressed to X’FFFFFFFFFFFF’ would have been a broadcast); BACnetSCAddressResolutionACK::Decode drops a WebSocket-URI that is not well-formed UTF-8 or holds a control octet instead of recording it, (a tab or DEL counts as a control octet) so it is no longer recorded as a direct-connect binding; the BVLC-Result ACK/NAK log lines name the function, message ID and (NAK) error class and code in place of a “ToDo” marker, and two other “ToDo” comments are gone from that file; the hub function’s connect wait timer clock-step-back restart is now tested.
  • rb bs-20261024-19 (#4507): a BBMD that is also a router (or virtual router) keeps its BBMD role and BDT across BACnetStack_Reset. AddRouterPort and AddVirtualNetwork set the Router / VirtualRouter device type over an enabled BBMD, and Reset’s own SetDeviceType call then cleared the role Reset had just saved for the port (#3679); the device stopped being a BBMD after the host re-added the port. The saved role is now dropped only where the host chooses its BBMD or none again (the Bbmd and Device device types), and a refused SetDeviceType(Bbmd, badPort) keeps it.
  • rb bs-20261024-19 (#4505): a 6-octet MAC addressed to network 0, the local network, on a device whose BACnet/SC Network Port has a configured Network_Number is typed BACnet/SC; it was typed BACnet/IP (the resolver matched only a port whose number was literally 0). On a device with both an SC and an IPv4 port, an SC port left at the unconfigured default number 0 still wins (as before), and two configured ports leave network 0 ambiguous, keeping the IPv4 assumption.
  • rb bs-20261024-20 (#4504): an address-form destination on one of the device’s own networks (an event-notification or audit recipient given as a network number and MAC) now leaves by that network’s Network Port (a BACnet/IP or MS/TP port; a BACnet/SC port is unchanged, its send path does not read the egress port). The #2526 change dropped the DNET for an own network but never recorded which port the network belongs to, so on a device with two ports a destination on the second port’s network was sent by data link instance 0 onto the first port’s network, with no DNET for a router to carry it on. A zero-length-MAC broadcast on an own network names that port too, and takes its link type. Network 0 still names no port.
  • rb bs-20261024-21 (#4501): on a gateway that is both a virtual router and a physical router, a Reject-Message-To-Network passing through toward a remote originator (a DNET that is neither a virtual network nor one of the router’s own port networks) is no longer answered with a reason-1 Reject-Message-To-Network and dropped. It goes to ProcessRejectMessageToNetwork, which updates the routing table and relays it to the originating node by the ordinary cl. 6.5 procedures (cl. 6.6.3.5), as on a physical-only router. The same one-condition change is made on the BACnet/SC ingress path (ProcessNPDUOnly); that path is not covered by a test. A reject is never answered with a reject.
  • rb bs-20261024-22 (#4508, items CR4-F06-009, -021, -017): a Who-Is-Router-To-Network naming network 0 (the local network) or X’FFFF’ (the global broadcast) is silently ignored by a router - neither answered nor searched onward; before, the router broadcast its own Who-Is-Router-To-Network for that number out every other port. A reused BACnetNetworkParameters no longer keeps the previous recipient’s DADR into a broadcast on one of the device’s own networks (the case the test covers; the fallback branches get the same clear). AddVirtualNetwork’s error text names the broadcast network as 65535, not 65565.
  • rb bs-20261024-23 (#4509, items CR4-F07-006, -003, -012, -017): BACnetBVLL::SetReadBroadcastDistributionTableAck(NULL) leaves the object invalid, as its Foreign-Device-Table twin does (it returned before the reset and left the previous message valid); BACnetBDTEntry’s five-argument constructor leaves a refused argument invalid (a NULL mask used to leave a valid entry with no mask); a BBMD role saved across BACnetStack_Reset follows its device when the device instance is re-keyed (ReplaceNetworkPortDeviceInstance), where it used to stay under the old instance and be lost; Number_Of_APDU_Retries above 255 saturates at 255 retries instead of wrapping (256 became 0, 300 became 44).
  • rb bs-20261024-24 (#4487, items CR4-F05-018, -013): an Unconfirmed-Request with an unknown service choice (15 to 255) is discarded like one for an unsupported service (#2470) through both decode entry points, instead of being reported as a decode failure; an allocation failure while creating the service request of an unsegmented Confirmed-Request for a supported service is answered as an Abort (OUT_OF_RESOURCES), not as a Reject (UNRECOGNIZED_SERVICE); the reassembly path has not rejected an allocation failure since #4311 either.
  • rb bs-20261024-25 (#4489, items CR4-F09-008, -007): the seven BVLC-SC message decoders reset before they check their arguments, so a NULL or empty buffer leaves no previous message’s fields behind; the Connect-Request and Connect-Accept VMAC setters (and so their compound setters) refuse the reserved VMACs X’000000000000’ and X’FFFFFFFFFFFF’, matching what Decode already refuses (#3665).
  • rb bs-20261024-27 (#4489, item CR4-F09-037): the SC certificate AddIssuer and RemoveIssuer procedures also read the target’s Operational_Certificate_File and fail at the reference step (InvalidResponse) when an issuer slot names it, before any file is read or written; previously a target of another make could have its operational certificate overwritten (AddIssuer) or truncated to zero (RemoveIssuer).
  • rb bs-20261024-28 (#4489, items CR4-F09-033, -020): a stack reset now also resets the BACnet/SC direct-connect manager, so per-peer reconnect timeouts, the connect-wait and heartbeat timers and kept connection-end errors do not survive it; the hub function refuses (NODE_DUPLICATE_VMAC) a Connect-Request that carries the node’s Device UUID with a VMAC other than the node’s current one, so only the node’s own hub connector becomes “the node’s connector peer”.
  • rb bs-20261024-29 (#4489, item CR4-F09-028): when the direct-connect initiator returns to Idle, no dial can start from any callback that teardown makes. A host that reported the close from the Idle state-change callback used to redial at once, before the reconnect timeout was armed, and the close callback that followed closed the new socket.
  • rb bs-20261024-30 (#4489, item CR4-F09-029): a node that accepts direct connections no longer ends its existing connection to a known Device UUID (or its initiated one) before it has checked the new Connect-Request; a request that is NAKed (a VMAC collision) or fails to build now leaves the existing connection in place.
  • rb bs-20261024-31 (#4489, item CR4-F09-032): a Hello-capabilities change and SuspendCommunication forget the accepted direct connection before they ask the host to close its socket, as the other accepted-connection teardowns do; a host that reports the close back synchronously no longer finds the connection still recorded and flips it to DISCONNECTED_WITH_ERRORS.
  • rb bs-20261024-32 (#4489, item CR4-F09-031): SuspendCommunication and disabling the direct-connect initiator now reset the initiator before they ask the host to close its socket, as Disconnect does; a host that reports the close back synchronously no longer runs the initiator state machine mid-teardown or sees an abandoned attempt recorded as FAILED_TO_CONNECT.
  • rb bs-20261024-33 (#4489, item CR4-F09-036): an SC certificate procedure whose write step cannot be sent after an earlier write took effect now sends DISCARD_CHANGES (and reports SendFailed once the target answers it), as a write the target refused does; before, it reported SendFailed at once and left the target with Changes_Pending and a partly written credential.
  • rb bs-20261024-36 (#4508, items CR4-F06-011, -015): an I-Am-Router-To-Network refresh of a route on the same port no longer ends an active Router-Busy-To-Network congestion indication before its 30 s timer or a Router-Available-To-Network does (an operator re-registration or a move to another port still clears it); an operator route added after start-up to a network not reached before re-arms the router’s I-Am-Router-To-Network announcement, as adding a port does. A busy indication set by the operator (SetRouteBusy, which has no timer and “persists until cleared”) now also survives such a refresh.
  • rb bs-20261024-37 (#4508, item CR4-F06-008): a request routed to a virtual network is now answered only by a device on that network. A DADR naming a device on another virtual network, or the router’s own main device, used to be executed by that device and answered from an address the requester did not ask; it is now ignored.
  • rb bs-20261024-38 (#4508, item CR4-F06-014): an I-Am-Router-To-Network refresh that moves a learned route to another port now takes a slot of the destination port’s learned-route quota (evicting that port’s least recently refreshed route when it is full), as a new route does; before, one port could hold more than its quota and its flood could then evict routes another port’s peers had taught.
  • rb bs-20261024-39 (#4508, item CR4-F06-001): on a gateway that is also an enabled physical router, a virtual network added after the first Tick is now announced with an I-Am-Router-To-Network that lists it; before, the virtual router deferred to a physical announcement that had already gone and nothing announced it. The first Tick still sends one announcement.
  • rb bs-20261024-41 (#4508, item CR4-F06-026): the NPDU decoder stores a destination or source address length only after the length checks pass, so a refused frame (DLEN above 7, SLEN above 6) no longer leaves an out-of-range length in the NPDU for a later copy to read. The network number is still stored as read, because the reason-6 Reject-Message-To-Network names it.
  • rb bs-20261024-42 (#4508, item CR4-F06-023): the BACnet/IP BVLL message classes (Forwarded-NPDU, Original-Unicast-NPDU, Original-Broadcast-NPDU, Distribute-Broadcast-To-Network, Read-BDT-Ack, Read-FDT-Ack, Write-BDT, Register-Foreign-Device, Delete-Foreign-Device-Table-Entry) keep their own BVLC function code across Reset(); before, Reset() set X’00’ (BVLC-Result), so a message refused after its reset claimed to be a BVLC-Result and the BACnetBVLL wrapper cast it as one.
  • rb bs-20261024-43 (#4508, item CR4-F06-013): a device that has learned a router for a remote network now forgets that router when it receives a Reject-Message-To-Network with reason 1 (not directly connected to the network) from it, so the next send falls back to the cl. 6.5.3 local broadcast; a rejection from any other node, or with another reason, leaves the entry alone.
  • rb bs-20261024-44 (#4508, item CR4-F06-024): a Reject or Error that the data link makes while decoding a request routed to a virtual device (a DNET/DADR naming it) is now sent as that virtual device, so it carries Hop Count X’FE’ and the canonical SADR like its ordinary answers; before, it went out with Hop Count X’FF’. Being sent as the virtual device, it is also subject to that device’s gates (an unconfigured virtual device, cl. 19.7, sends no Reject, as the router’s main device already did since #4338).
  • rb bs-20261024-45 (#4509, items CR4-F07-004, -008, -016, -023): a device that is not a BBMD now also drops, without a NAK, a BBMD-only BVLL request sent to an IPv4 multicast group (224.0.0.0 to 239.255.255.255, the B/IP-M group among them), as it does one sent to a broadcast; BACnetFDTEntry gains GetIpAddressLength() (6 for a BACnet/IP address, 18 for BACnet/IPv6, 0 when absent); a comment on GetNetworkPortInstanceForSend is corrected and a verbatim 135-2024 sentence in a source comment is replaced by a paraphrase.
  • rb bs-20261024-46 (#4509, item CR4-F07-020): a write to a Network Port’s Command of a value the standard has not defined (11 to 127, or above 255) is refused with PROPERTY / VALUE_OUT_OF_RANGE instead of OPTIONAL_FUNCTIONALITY_NOT_SUPPORTED; a defined command the stack does not support, and the vendor values 128 to 255, still draw OPTIONAL_FUNCTIONALITY_NOT_SUPPORTED.
  • rb bs-20261024-48 (#4509, item CR4-F07-010, in part): a BBMD refuses a Register-Foreign-Device (Register-Foreign-Device NAK, X’0030’) whose UDP source is not a usable unicast address - an all-zero or limited-broadcast IP, a multicast group, a zero UDP port, the subnet’s directed broadcast - or is the BBMD’s own address, instead of entering it in the Foreign Device Table. The Forwarded-NPDU originating-address screen of #1096/#1884 is now the same function, IsUsableUnicastPeerAddress.
  • rb bs-20261024-51 (#4488, item CR4-F08-009): a received MS/TP frame whose receive callback supplies no source connection string now reaches the MS/TP data link, which counts it in its discarded-frame count (#3980) and drops it, instead of being dropped uncounted by the entry points above it. Other transports still refuse such a frame at the data link layer.
  • rb bs-20261024-52 (#4488, item CR4-F08-005): the MS/TP engine’s FrameHeaderCallBack is now called in every build, not only under MSTP_DEBUG, for each valid frame header received (any destination), with the eight header octets in wire order (X’55’, X’FF’, type, destination, source, length high and low, header CRC) instead of the padded host-endian struct. The Zephyr adapter’s frame counters therefore count. Transmitted frames are still reported only under MSTP_DEBUG.
  • rb bs-20261024-53 (#4509 CR4-F07-005, #4489 CR4-F09-006): Distribute-Broadcast-To-Network, Register-Foreign-Device, Delete-Foreign-Device-Table-Entry and BVLC-Result Decode now reset the object first and again on every refusal, so a reused object no longer keeps the previous frame’s content (the Read/Write table classes reset before their short-buffer check too). Five BACnet/SC message headers, two validator comments and three test comments no longer quote Annex AB table text; they say the same in our own words.
  • rb bs-20261024-54 (#4489, item CR4-F09-027): the BACnet/SC direct-connect initiator now times a WebSocket dial the host never reports on, using the WebSocket connect timeout the hub connector already uses (3 to 60 s). On expiry the attempt counts as failed (tcpConnectTimeout), the socket is closed and the reconnect timeout is armed, so one lost status report no longer leaves the single initiator in AWAITING_WEBSOCKET, and every other peer refused, for ever.
  • rb bs-20261024-55 (#4489, item CR4-F09-021): with the node’s own hub connector connected to its hub function, a unicast Advertisement addressed to the node (relayed to that connector peer since #4235) again updates the sending peer’s Maximum BVLC and NPDU Length in the hub function’s table, as it does when the node is reached in-process (#3358), so relays to that peer are held to its current maximums in both configurations.
  • rb bs-20261024-56 (#4487 CR4-F05-025, CR4-F05-026): BACnetStackUnknownAPDU::Decode no longer reads an application-tagged Boolean’s value (the tag’s length/value/type field, cl. 20.2.3) as a content length, so TRUE no longer swallows the next octet, and Encode writes it back as the one octet it came from. A failed decode now sets the error container on every failure exit, the unreachable negative-length check and the commented-out context-tag check are gone, and the #1092 comment is accurate.
  • rb bs-20261024-57 (#4487 CR4-F05-004): in SEGMENTED_RESPONSE a Confirmed-Request for the entry (cl. 5.4.5.4 UnexpectedPDU_Received) is now answered with the server Abort and the entry returns to IDLE even when SegmentTimer has already expired; before, the Timeout transition ran first and re-sent the window (or dropped the entry with no Abort) and the request was never answered. Same ordering as the client Abort fix of #3304.
  • rb bs-20261024-58 (#4487 CR4-F05-002): when a peer already holds its per-peer maximum of transactions (or the table is full), a stray client SegmentACK or a stray segment with a non-zero sequence number now draws its Abort(OUT_OF_RESOURCES) from the same per-source rate limit as the IDLE path (#3677 CR3-F05-032) instead of one Abort per frame. A new request that needs a table entry is still refused every time.
  • rb bs-20261024-6 (#4328 item 3, narrowed): a confirmed request delivered between devices this stack hosts on a virtual router (the router device to a hosted device, and by the same code a hosted device to another, which no test drives) now says its sender accepts no segmented answer. Before, a hosted device whose answer did not fit in one APDU (a ReadProperty of a large Object_List, say) sent the first segment out of the router and the requester’s transaction stayed open until it timed out; now the answering device sends Abort SEGMENTATION_NOT_SUPPORTED, which is delivered internally and ends the transaction at once (your HookAbort is called). A request to a device on the wire is unchanged. Delivering such a segmented exchange internally, so the answer arrives, stays open on #4328.
  • rb bs-20261024-60 (#4509 CR4-F07-009): a foreign device’s registration deadline is the BBMD’s acceptance plus the Time-to-Live, set when the Register-Foreign-Device is answered with a success, not each time a request is sent. A re-registration that is never answered no longer keeps a dead registration alive, so the Network Port’s BACnet_IP_Mode and the Distribute-Broadcast-To-Network broadcasts to the dead BBMD lapse at the Time-to-Live.
  • rb bs-20261024-61 (#4487 CR4-F05-009): an audited confirmed request whose segmented transaction ends in a TSM timeout (receiving the response in segments, or sending the request in segments) now records the COMMUNICATION / TIMEOUT Result before it is removed, as the AWAIT_CONFIRMATION timeout always did; the operation-source audit record is no longer dropped.
  • rb bs-20261024-61 (#4487 CR4-F05-012): when a router’s relayed answer re-keys a request that was sent as a local broadcast, the retained segment(s) are rewritten from BVLC Original-Broadcast-NPDU to Original-Unicast-NPDU, so a segment retry to the router is no longer sent as a broadcast frame (a router that is also a BBMD would have distributed it).
  • rb bs-20261024-62 (#4562): the BACnet/SC direct-connect acceptor now runs the AB.6.2.3 connect wait timer for a socket that has not had a Connect-Request accepted. Its first frame (a frame other than a Connect-Request is discarded, as before) starts the wait; after SC_Connect_Wait_Timeout without an accepted Connect-Request the socket is closed through CallbackDisconnectWebsocket with no BVLC-SC message. A socket the host reports closed, or one whose Connect-Request is accepted, ends its wait; switching accept off closes the waiting sockets. Bounded at 8 sockets.
  • rb bs-20261024-62 (#4480): changing the node’s VMAC (a different host-supplied one, or the default re-derived) now restarts the open hub connections, so the hub registers the node under the new VMAC (AB.2.10.1, AB.5.3.2, AB.3.1.2); before, the old connection stayed up registered under the old VMAC. Setting the VMAC it already has changes nothing.
  • rb bs-20261024-63 (#4500): on an enabled physical router, a frame whose DNET is another of the router’s own port networks and whose DADR is this device’s own MAC on that port is now delivered to this device’s application (cl. 6.1, 6.6.1) instead of being transmitted back out of that port; a remote broadcast for such a network is forwarded onto it and also delivered to this device (cl. 6.1 “and/or”; a confirmed request in it is still not answered). A DADR naming another device is forwarded as before.
  • rb bs-20261024-63 (#4502): the router now re-broadcasts, out every enabled port except the one it arrived on, an I-Am-Router-To-Network (cl. 6.6.3.3, listing every network number the received one listed), a Router-Busy-To-Network (cl. 6.6.3.6) and a Router-Available-To-Network (cl. 6.6.3.7), as 135.1-2023 10.2.2.1, 10.2.2.4.x and 10.2.2.5.x require. Each re-broadcast takes a token from the router’s search-broadcast bucket; with none left the route is still learned but nothing is re-broadcast.
  • rb bs-20261024-65 (#4476): a SEGMENTED_REQUEST(client) transaction no longer treats a SegmentACK that acknowledges only part of the final window as FinalACK_Received. The segments after the acknowledged sequence-number are sent again (cl. 5.4.4.2 NewACK_Received: “at least one segment remaining to send”), instead of being stranded until the request timed out. An acknowledgment of the final segment itself still ends the send.
  • rb bs-20261024-65 (#3801 item 2): a segmented getAlarmSummary request is no longer answered with Abort(OTHER) after reassembly. It goes to the application like the unsegmented request (cl. 5.4.5.2), and trailing octets that are not zero padding are Reject(INVALID_TAG), as the unsegmented request gets.
  • rb bs-20261024-65 (#3778 item 2): a failover hub URI equal to the primary hub URI is accepted but no failover hub is used (AB.5.4); the connector keeps retrying the primary instead of “failing over” to the same hub and crediting the failover socket’s frames to the primary. The URI still reads back as configured.
  • rb bs-20261024-66 (#4563): the CHANGE_OF_BITSTRING event algorithm now holds pListOfAlarmValues as a list (up to 64 patterns) instead of one pattern: the object goes OFFNORMAL when the masked value equals any of them (cl. 13.3.1 (a)), Alarm_Values serves one element per pattern, and Event_Parameters lists every pattern. The arming exports are unchanged and arm a one-element list; a list-taking arming call exists inside the stack for a later export.
  • rb bs-20261024-67 (#4503): on an enabled physical router, a message the stack’s own device originates to an address-form recipient on a remote network (an event or audit notification to a Notification Class recipient such as network 7, MAC 05) is now addressed by the routing table (cl. 6.5.3): it leaves by the egress port the route names, to the next-hop router’s MAC when one is known (a broadcast on that port when not), carrying the DNET and DADR. It used to go as a broadcast on the first port. BACnet/SC egress is unchanged.
  • rb bs-20261024-67 (#4508 CR4-F06-019): an unconfirmed event notification to a recipient that is the global broadcast (network 65535, empty MAC) is now sent out of every Network Port, not just the first, as the audit path already did.
  • rb bs-20261024-78 (#4509): when a client aborts a segmented request that is still sending segments (an out-of-place reply arrived), the request’s operation-source audit notifications now carry the Result COMMUNICATION / ABORT_INVALID_APDU_IN_THIS_STATE, as a reassembly abort already did; before, they went out with no Result.
  • rb bs-20261024-78 (#4509): a Network Port added at Protocol_Level BACNET_APPLICATION with no reference port no longer lists Reference_Port, as cl. 12.56.1.2 requires of a non-hierarchical port; PHYSICAL and PROTOCOL level ports, and a port given a reference port by the host, still carry it.
  • rb bs-20261024-80 (#4488): MS/TP. A Token, Poll For Master, Reply To Poll For Master or Reply Postponed that carries data octets no longer fails the header check, so another station’s malformed frame of those types no longer ends this node’s wait for a reply; one addressed to this node or broadcast is discarded in IDLE. A receive error the host reports with the new MSTP_ReportReceiveError() now takes the receive state machine’s Error transition (setting ReceivedInvalidFrame in HEADER, DATA, SKIP_DATA and RECEIVE_ENCODED_FIELDS). A data-expecting-reply request whose NPDU carries a destination network (to be routed on) is answered with a Reply Postponed at once (cl. 9.7.1).
  • rb bs-20261024-80 (#4488): documentation only: the send-callback documentation says where an MS/TP host takes the data-expecting-reply frame type from (bit 2 of the NPDU’s control octet, cl. 9.7.2).
  • rb bs-20261024-83 (#4565): internal. The test-tool callback type FPCallbackAuditNotificationReceivedForDevice is declared inside the BACNET_STACK_TESTTOOL guard like the other test-tool callback types, so the API Reference generator no longer takes it for a customer callback type. No customer-visible change.
  • rb bs-20261024-84: internal. The recorded sizes of two logo files in docs/_manual/data/logo-provenance.json (cpp.svg, java.svg) are the sizes as the repository stores them (LF line endings), so the site-claims check R20 passes on Linux. No customer-visible change.
  • rb bs-20261024-9 (#4473): a segmented confirmed request is no longer processed in the packet the server transaction owns. A service whose processor sends its own SimpleACK (SubscribeCOV, ReinitializeDevice, AcknowledgeAlarm, SubscribeCOVProperty) reached the transaction through the nested send, which answered and reset it and freed the reassembled request the processor, and in a test-tool build the audit code behind it, was still reading (a heap-use-after-free; AddressSanitizer reports it at the pre-fix tree). The reassembling function now owns the packet for the call and deletes it after, and the transaction keeps only a clone without the request, as an unsegmented request’s has since #4091 (so the whole reassembled request is also no longer held for the whole of AWAIT_RESPONSE and SEGMENTED_RESPONSE) — revert-red: an AddressSanitizer run of the new test fails on the pre-fix tree.
  • rb bs-20261024-9 (#4474): BACnetStack_RemoveDevice now forgets the device’s open server transactions (a segmented request being received, a request awaiting the application’s response, a segmented response waiting for a Segment-ACK). They kept a raw pointer to the device, and the next BACnetStack_Tick() drove them through it — a use-after-free that a peer could hold open for a retry period.
  • rb bs-20261024-9 (#4475): HookError for a SubscribeCOVPropertyMultiple-Error carrying first-failed-subscription now reports the error class and code inside it and the monitored object and property (useObjectProperty TRUE), as it does for a WritePropertyMultiple-Error; it reported DEVICE / OTHER (0 / 0) and no object or property.
  • rb bs-20261027-1 (#3861): the Event_Parameters read-back of the AccessEvent, ChangeOfLifeSafety, CommandFailure and FloatingLimit algorithms keeps the reference’s optional property array index and device identifier (it rebuilt the reference from type, instance and property only). No client can store those fields for these algorithms yet, so this is a defensive fix, tested at the algorithm level — revert-red: the new read-back test fails on the pre-fix algorithms.
  • rb bs-20261027-12 (found testing the Zephyr B-BC example on an ESP32-S3 against a BACnet MS/TP router, #769 follow-up): a build with a Network Port object and no router (STACK_OPTION_TARGET_EMBEDDED plus a profile, the Zephyr example’s configuration) did not compile: BACnetNetworkLayer.cpp used the device-address-proxy state, IsRejectMessageToNetworkNpdu, BACnetRejectMessageToNetwork and the router’s port table outside the STACK_OPTION_ROUTER / STACK_OPTION_ROUTER_PHYSICAL guards that declare them. Every unit-test build is STACK_OPTION_TARGET_FULL, so nothing had built it; the new _ci_scripts/check-embedded-profile-compile.sh does (a syntax check of every source/*.cpp with the example’s options header).
  • rb bs-20261027-12: adapters/zephyr: cbz::mstp::Start() was documented and defined but never declared in cas_bacnet_zephyr.h, so an application could not call it. cbz::SetReceiveObserver stopped seeing MS/TP traffic when the stack took over the port (#769 slice 3/4): the stack now takes NPDUs out of the octet stream itself, so the transport callback that called the observer never ran, and the B-BC example could not read the answers to its own ReadProperty requests over MS/TP. ReceiveBytes() now also runs the octet stream through a decoder (adapters/zephyr/src/cbz_mstp_decoder.h, header only, unit-tested on the PC with literal frames: preamble, header CRC, data CRC, a preamble inside a failed header is found again, an impossible length resynchronises at once, a COBS Extended Data frame of any length is skipped whole), queues each valid BACnet Data frame for this station or broadcast (four slots; Counters::observerDropped counts overflows), and a service on the stack thread passes the NPDUs to the observer, after the tick that took the octets (so it may run before or after the stack handles the same NPDU). The two COBS Extended Data frame types (over 501 octets) are not shown to the observer. Hardware result (ESP32-S3, 38400 baud, router with network 5, MAC 10): Profile Function 15/15, Failure Mode 7/7, discovery 20/23 (the three misses are BACnet/IP-only properties the test expects on every datalink), direct probe 6/6, 1,377 routed reads in 3 minutes with none lost (median 91 ms), 0 frames dropped by the stack, heap flat.
  • rb bs-20261027-7 (#4604): a router’s BACnet/IP port drops an Original-Broadcast-NPDU whose source is that port’s own bound B/IP address — its own broadcast, looped back by the host. It was processed as another node’s: a global broadcast (a Who-Is with DNET X’FFFF’) was relayed back onto the network it came from (cl. 6.3.2), the router’s own device answered every copy, and its own I-Am-Router-To-Network was learned as another router’s, so one request became a storm. Applies only to a port serving a router port with a recorded local address (a BBMD router port included, its own broadcast having been distributed when sent); a device with no routed port still processes its own broadcast locally, and a NAT’d BBMD’s loop-back from its public address is not recognised — revert-red: removing the drop, or dropping from any source, fails its own test.
  • rb bs-20261028-3 (#4635): on a BACnet/IPv6 port, a client send the caller marks broadcast = true (BACnetStack_SendIAm, BACnetStack_SendWhoIs and the other unconfirmed sends with a broadcast argument: Who-Has, TimeSynchronization, UTCTimeSynchronization, UnconfirmedTextMessage, unsubscribed COV notifications, WriteGroup, Who-Am-I, You-Are, and the network-layer BACnetStack_SendIAmRouterToNetwork, BACnetStack_SendWhoIsRouterToNetwork and BACnetStack_SendNetworkNumberIs) now goes out as an Original-Broadcast-NPDU: the host is handed it with the broadcast flag set and no connection string, and multicasts it (Annex U.4). Before, the connection string the caller had to pass was kept and the message went out as an Original-Unicast-NPDU to that address, so an application on BACnet/IPv6 could not announce itself or send a global Who-Is. The connection string is still required (an empty one is still refused); with broadcast = false the send is unicast to it as before, and a BACnet/IP broadcast keeps the address the caller gives.
  • rb bs-20261028-4 (#4638): a BACnet/IPv6 foreign device that registers again with the BBMD it is registered with (a re-activation of the same configuration) stays registered while the BBMD answers, so its own broadcasts keep going to the BBMD rather than to the local multicast group meanwhile; the registration lapses by the Time-to-Live the BBMD accepted, not one a renewal is still asking for. A foreign device refuses to send a broadcast its BBMD could not forward (an Original-Broadcast-NPDU over 1511 octets, which as a Forwarded-NPDU would exceed the 1529-octet frame maximum) instead of sending it and re-registering on the NAK. A retry of a confirmed request held for VMAC resolution asks the peer again, and so does a new message for the peer once the last question is a second old, so a lost Virtual-Address-Resolution no longer fails every message to that peer. A foreign device with no device to register or renew as tries again only after its retry wait, not on every Loop.
  • rb bs-20261028-4 (#4639): an activation (ReinitializeDevice ACTIVATE_CHANGES or WARMSTART) that changes only a BACnet/IPv6 port’s IPv6_Address or BACnet_IPv6_UDP_Port now gives the data link its new address: a BBMD forwards its own broadcasts with it, and a foreign device sends its old BBMD a Delete-Foreign-Device-Table-Entry for the old address and registers again from the new one. An activation at which the host does not serve the port’s IPv6_Address leaves the port as it is. A BDT entry the data link refuses at an activation that applies written values (the BBMD’s own new address, written with it) is removed from BBMD_Broadcast_Distribution_Table, so the property never lists a peer the BBMD does not forward to; when the host alone moves the BBMD onto an entry’s address, the entry is kept and forwarded to again once the BBMD moves off it. BBMD_Accept_FD_Registrations reads FALSE on an IPv6 port that is not a BBMD, as on a BACnet/IP port. A BACnet_IPv6_Multicast_Address write that is not a multicast address (first octet X’FF’) or whose UDP port is 0 is refused with VALUE_OUT_OF_RANGE. BACnetStack_AddNetworkPortObject with network type IPV6 is refused in a build without STACK_OPTION_DATA_LINK_LAYER_IPV6, which before made a port whose BBMD and foreign device writes stayed pending for ever.
  • rb bs-20261029-1 (#4646): reading a Network Port’s Command_Validation_Result (a BACnetHealth the stack serves itself) no longer logs “Unsupported datatype for callbacks” at error level on every read; the read was already answered correctly.
  • rb bs-20261030-3 (#4509, CR4-F07-015): a segmented ComplexACK is accepted or aborted by the Segmentation_Supported, Max_Segments_Accepted and APDU_Segment_Timeout of the device the request was sent as (a virtual device, #3411), not always the main device’s. The main device is used when no source device was recorded.
  • rb bs-20261030-3 (#4509, CR4-F07-014): the client transaction state machine’s own answers - the Abort for a stray segment or a stray server Segment-ACK, the Abort for a segmented ComplexACK a device does not accept, and the Segment-ACKs and Aborts of a reassembly - are not sent while the main device is unconfigured (cl. 19.7). Their reply NPDU is unchanged.
  • rb dv-20261017-10 (#3824 CR3-F01-004, -018, -023, -024, -036; #3713 CR3-F11-006; #3715 CR3-F13-001; #3679 CR3-F07-025; #3681 CR3-F09-047): CASBACnetStackDLL.h / BACnetCallbackInterface.h documentation only (IFC-127). The docs now match the code:
    • Lighting_Command clause citations.
    • The UpdateDeviceInstance and AddVirtualNetwork refusals, all checked before anything changes.
    • The NetworkPortCommand error classes.
    • The Get ObjectPropertyReference out-of-range answer.
    • The WriteGroup and text-message callbacks.
    • The Network Port held-pending writes and Routing_Table.
    • When the BACnet/SC state-change callback fires, and with which values. Maintainer comments were removed from the header. The same corrections are in api.ts and the API reference and manual pages.
  • rb dv-20261017-12 (#3763 CR3-F23-024): the memory estimate (GetMinimumMemoryCount) of a BACnetAuthenticationPeer and a BACnetAuthorizationPolicy now equals their encoded length.
  • rb dv-20261017-12 (#3763 CR3-F23-008): the Audit_Priority_Filter class’s name and bit-name table are defined once, in a new BACnetAuditPriorityFilter.cpp, instead of in every translation unit that includes the header.
  • rb dv-20261017-12 (#3763 CR3-F23-001, -002, -006, -007, -011, -013, -019, -020, -025): audit and authorization source comments and clause citations describe the code, and AuditLogQuery gains tests for its source, target, array-index and failures-only filters. The test-tool AuthRequest callback doc gives the errorCode packing as bits 16-23 and cites the service clause as 17.6 (IFC-129).
  • rb dv-20261017-13 (#3677 CR3-F05-019): BACnetSegmentAckProcessor reports a SEGMENTED_REQUEST(client) transaction it abandons to HookAbort with the reported source (#3281 item 1), so a peer claiming SNET 0xFFFF reaches the application as network 0 with no address; the Abort, Reject, Error and SimpleACK processors’ same conversion is now pinned by a test each.
  • rb dv-20261017-14 (#2844 CR2-F22-007, #2850 CR2-F28-021): a Trend Log or Trend Log Multiple with a Start_Time now logs from exactly Start_Time (cl. 12.25.5 / 12.30.8: on or after Start_Time); it used to stay disabled for the Start_Time second, so a polled log whose Start_Time sat on an interval boundary missed its first sample, and a write of Enable TRUE at exactly Start_Time kept LOG_DISABLED set. The Event Log already did this (#3620). Stop_Time is unchanged: logging stops at exactly Stop_Time. Internally BACnetDateTime::OnOrAfterTime, which has always computed “strictly after”, is renamed AfterTime; the Trend Log Multiple BTL 7.3.2.24.2 harness now checks the clock before each step 8 VERIFY, as the procedure says. Revert-red: the Trend Log and Trend Log Multiple at-Start_Time tests fail on the strict comparison (controls one second earlier pass); the Stop_Time and equality tests fail if AfterTime is made inclusive.
  • rb dv-20261017-15 (#3712 CR3-F10-005, #3089 CR2-F02-063): ReadRange of an Audit Log’s Log_Buffer is served (test-tool builds, which hold the audit log). No Range, By Position, By Sequence Number and By Time read the stack-held records, numbered as AuditLogQuery numbers them. The property is now profiled a BACnetLIST of BACnetAuditLogRecord (it was an ARRAY of Any, so every ReadRange was refused PROPERTY_IS_NOT_A_LIST), and a ReadRange-ACK of it decodes its items as audit log records. In builds without the test tool, and for an Audit Log the stack holds no buffer for, ReadRange of Log_Buffer now answers PROPERTY / READ_ACCESS_DENIED (was SERVICES / PROPERTY_IS_NOT_A_LIST). ReadProperty and ReadPropertyMultiple of Log_Buffer with an array index now answer PROPERTY / PROPERTY_IS_NOT_AN_ARRAY (was READ_ACCESS_DENIED), since the property is no longer an array; without an index they still answer READ_ACCESS_DENIED. Sequence numbers above 2^32-1 are refused (DEVICE / OPERATIONAL_PROBLEM) until #4076.
  • rb dv-20261017-15 (#3712 CR3-F10-017): the ReadPropertyMultiple processor’s header comment summarises cl. 15.7 instead of transcribing it; the INVALID_ARRAY_INDEX row and the Network Port wildcard are now right. Comment only.
  • rb dv-20261017-16 (#4077): a Load Control shed ends at exactly Start_Time + Shed_Duration, and a pending request is cancelled at that moment, one second earlier than before. A request with a Shed_Duration of 0 therefore ends at its Start_Time.
  • rb dv-20261017-17 (#4052): a device re-key (BACnetStack_UpdateDeviceInstance or a You-Are) now also moves the remaining references that name the device’s own old instance. These are the Event Enrollment event and fault algorithm property references (mode, setpoint, update-time, access-event-time, feedback, status-flags and fault-list), the Channel List_Of_Object_Property_References, and the Access Point Zone_To / Zone_From. Before, a FLOATING_LIMIT band stopped following its setpoint, a CHANGE_OF_LIFE_SAFETY or FAULT_LIFE_SAFETY enrollment stopped reacting to Mode changes, a CHANGE_OF_TIMER enrollment stopped re-alarming on a new Update_Time, and a FAULT_LISTED or FAULT_STATUS_FLAGS enrollment reported UNRELIABLE_OTHER. An Access Point accepted a Zone_From naming the same zone as its Zone_To when the two named it under the old and the new instance. A Channel member naming the device was not written locally: WriteGroup dropped it (no address binding resolved the old instance) or, once a later list edit had acquired a binding for the old instance, sent it to that instance. The algorithms keep the state they latched from the referent (the Mode and Update_Time baselines). A Channel member naming a remote device whose instance the device takes is now local, so its address binding is released.
  • rb dv-20261017-2 (#3681 CR3-F09-017): a BACnet/SC connection state (BACnetSCConnectionState, cl. 21) above failed-to-connect (3) is refused by its setter and on decode, as for other closed enumerations; an SC_Hub_Function_Connection_Status entry with connection-state 4 or more used to be accepted.
  • rb dv-20261017-4 (#4030): a BACnet/SC hub connector state (BACnetSCHubConnectorState, cl. 21) above connected-to-failover (2) is now refused, as for other closed enumerations. That covers its setter, an sc-hub-connector-state property state, and a Network Port SC_Hub_Connector_State decoded from another device. Such values used to be accepted.
  • rb dv-20261017-6 (#2774, #2775): a Trend Log or Trend Log Multiple record logged from a non-primitive value (any-value) now has a string form and round-trips by value.
    • The form is the lower-case hex of the encoded content. An empty value is "x".
    • Before, the Trend Log read failed, and the Trend Log Multiple read wrote "", which its Insert refused.
    • An Event Log insert of type 10 is now accepted too, since it shares the parser.
  • rb dv-20261017-6 (#3085): the CASBACnetStackDLL.h docs of the Trend Log read exports state the NULL out-parameter refusals #3447 made. The Trend Log Multiple Null example is corrected to type 6.
  • rb dv-20261017-7 (#1765, #2650): CASBACnetStackDLL.h / BACnetCallbackInterface.h documentation only.
    • SendSubscribeCOVProperty’s lifetime must be non-zero.
    • The Set ObjectIdentifier callback also fires on a You-Are assignment, and returning false refuses it.
  • rb dv-20261017-8 (#4025): an Event Enrollment that monitors an object on another device is no longer evaluated when the local object with the same identifier changes.
  • rb dv-20261017-8 (#4047): with alarms and events enabled, a Reliability fault on a Channel, Credential Data Input or Program now shows as Event_State FAULT. These are the Table 13-5 rows. Feedback_Value in the Binary Output / Binary Lighting Output CHANGE_OF_RELIABILITY notification is now pinned by tests.
  • rb dv-20261017-9 (#4050): a device re-key (BACnetStack_UpdateDeviceInstance or a You-Are) now moves the references that name the device’s own old instance to the new instance. That covers the Event Enrollment Object_Property_Reference, the Trend Log and Trend Log Multiple Log_Device_Object_Property, the Averaging Object_Property_Reference and the Life Safety Zone members. Since #4025 such an enrollment had silently stopped being evaluated.
  • rb dv-20261017-9 (#3088 CR2-F01-014, -060, -061, -096, -098, -100): CASBACnetStackDLL.h / BACnetCallbackInterface.h documentation only (IFC-126). Clause citations are corrected, callback parameters named, and the I-Am-Router unicast, Trend Log Multiple, CreateObject and atomic-read count notes now match the code.
  • rb dv-20261018-19 (#4088): a device re-key (BACnetStack_UpdateDeviceInstance or a You-Are) now also moves Schedule and Timer List_Of_Object_Property_References members that name the device’s own old instance. Before, such a member was no longer written to the local object when the Schedule or Timer fired: the write was skipped, or sent to whichever device had been bound under the old instance. A member naming a remote device whose instance the device takes is now local, so its address binding is released at the re-key. Deleting a Schedule or Timer after a re-key no longer releases another consumer’s binding for the old instance, and no longer leaks the binding for the new one. Reading a Schedule’s or Timer’s List_Of_Object_Property_References after the re-key returns the new instance for those members.
  • rb dv-20261018-19 (#4089): a Channel List_Of_Object_Property_References member whose device identifier is not of object type Device names no device. WriteGroup no longer writes it to the local object (when its instance matched this device’s) or to a remote device, and no address binding is acquired for it.
  • rb dv-20261018-19 (#4095): a device re-key now reconciles the address bindings a Notification Class holds for its Recipient_List device recipients. A recipient naming a remote device whose instance the device takes is now local, so its binding is released at the re-key, not at the next Recipient_List edit. A recipient naming the device’s own old instance now follows the re-key to the new instance, so a notification to it is still handed to this device’s Notification Forwarders (builds with Notification Forwarder support). Reading Recipient_List after the re-key returns the new device instance for those recipients (Device-typed recipients only).
  • rb dv-20261018-20 (#3188): BACnetStack_RemoveDevice doc says “the data links bound to the removed device’s Network Port objects are released and reset” (BACnet/SC connections closed through the stop-listening and disconnect callbacks before the call returns). The BACnetStack_UpdateDeviceInstance “NOT ATOMIC” note was already corrected by an earlier batch; the code was re-checked and nothing fails after the rename starts. Comment only.
  • rb dv-20261018-20 (#2959): FPCallbackProcessAuthRequest doc says device / other cannot be returned, because its packed value is 0, the “use the default” value. The bit layout was already corrected by an earlier batch. Comment only.
  • rb dv-20261018-21 (#4101): a device re-key (BACnetStack_UpdateDeviceInstance or a You-Are) now also moves Notification Forwarder Recipient_List device recipients that name the device’s own old instance (Device-typed recipients only). Before, such a recipient kept the old instance and the forwarder acquired an address binding for it on its next tick, so forwarded notifications went to whichever device answered as the old instance. A recipient naming a remote device whose instance the device takes is now local, so its binding is released at the re-key. Reading the forwarder’s Recipient_List after the re-key returns the new device instance for those recipients. Subscribed_Recipients entries are not rewritten.
  • rb dv-20261018-21 (#4101): a Notification Forwarder Recipient_List or Subscribed_Recipients entry whose device identifier is not of object type Device names no device, so no address binding is acquired for it.
  • rb dv-20261018-24 (#3678 CR3-F06-003): a relayed network layer message that does not fit Table 6-1’s routed NPDU length for the egress link (an unknown type or a vendor proprietary message with a large body, from a BACnet/SC port toward a non-SC port) is answered with Reject-Message-To-Network reason 4. It was measured on its NPCI alone, then discarded unanswered or sent over the limit.
  • rb dv-20261018-24 (#3678 CR3-F06-014): the JSON/XML decode of a vendor proprietary network layer message shows its vendorId and dataLength, and data when the message has a payload; it rendered as a bare name.
  • rb dv-20261019-108 (#4353): setting an Elevator Group’s Landing_Call_Control to a malformed BACnetLandingCallStatus through the API (SetProperty) now fails with the clause’s error for a malformed value (cl. 15.9.1.3.1) instead of WRITE_ACCESS_DENIED: VALUE_OUT_OF_RANGE for a status with neither a direction nor a destination. On the wire, the decoder already Rejects such a status. A defensive INVALID_DATATYPE covers a missing or invalid status object.
  • rb dv-20261019-109 (#4318): a BACnetError, or a CharacterString of 253 octets or more whose length tag does not fit, encoded into a buffer that is too small now reports Abort(SEGMENTATION_NOT_SUPPORTED) instead of Abort(OTHER). A non-segmenting device answering a ReadPropertyMultiple whose results are Errors now stops reading at the reference that overflows the APDU, as it already did for property values (#3687), instead of reading every reference before refusing the request.
  • rb dv-20261019-111 (#4343): a Lighting Output’s WARN_OFF and WARN_RELINQUISH now honour Transition (cl. 12.54.19) under FADE or RAMP. This holds for an immediate write, for an egress that ends on its timer, and for an egress halted by a higher-priority command. The output fades or ramps from where it was toward the new winner; for a relinquish, the transition belongs to the new winner’s priority. They used to snap whatever the Transition said. Transition NONE still snaps.
  • rb dv-20261019-112 (#4376): when a STEP_*, FADE_TO or RAMP_TO command halts a pending WARN_OFF or WARN_RELINQUISH egress on a Lighting Output with Transition NONE, the command now starts from the level the egress left. The halt writes 0.0% or relinquishes at once (cl. 12.54.6.1), so a STEP_UP from 0.0% does nothing and a STEP_ON stores 1.0%. These commands used to start from the level before the egress resolved. Under Transition FADE or RAMP they still start from the output where it stood.
  • rb dv-20261019-113 (#4322): a Channel whose member is the Present_Value of another local Channel, or of a stack-held Schedule or Timer, now writes it as a Present_Value write (cl. 12.53.5) and no longer hands it to the host’s CallbackSetProperty. The inner Channel passes the value on to its own members at the same priority. An out-of-service Schedule’s engine takes the value, and a Timer starts with the written timeout. A Channel that references itself, directly or through a cycle, writes its members once and stops.
  • rb dv-20261019-114 (#4378): a Lighting Command Channel whose member is another local Lighting Command Channel now passes the lighting command on, and that Channel passes it to its own members at the same priority. A Channel’s Present_Value is a BACnetChannelValue, which can hold a lighting command (cl. 12.53.5); the value used to be skipped as if the member’s datatype were unknown (Table 12-63). A Channel of another type still refuses the command. Color command and xy color Channels are tracked in #4380.
  • rb dv-20261019-115 (#4334, slice 1): a virtual router (a gateway) now sends its start-up I-Am-Router-To-Network on MS/TP ports as well as BACnet/IP ones (cl. 6.6.2). A Network-Number-Is follows on a port whose network number is configured. The announcement now waits until a port can carry it, so a Network Port object added after the first Tick still gets it; before, the gateway never announced. BACnet/SC ports are not yet covered.
  • rb dv-20261019-117 (#4388): _ci_scripts/resolver-board.sh no longer closes the successor board it has just created. GitHub’s issue listing lags behind gh issue create, so the race check saw the old board as the highest and treated the new one as a losing duplicate. The script now waits for the listing to catch up, and closes the new board only when an open board with a higher number exists. A stubbed-gh self-test is added: _ci_scripts/resolver-board-selftest.sh.
  • rb dv-20261019-120 (#4381): a BACnetLightingCommand rendered as JSON or XML (DecodeAsJSON / DecodeAsXML) now includes TargetLevel, RampRate, StepIncrement, FadeTime and Priority only when they are present. These fields are optional; absent ones used to render as zeros, including a Priority of 0, which is outside 1..16. Encoding is unchanged.
  • rb dv-20261019-121 (#4395): the JSON / XML rendering (DecodeAsJSON / DecodeAsXML) now includes optional fields only when they are present, for a BACnetColorCommand (TargetColor, TargetColorTemperature, FadeTime, RampRate, StepIncrement), a BACnetCOVSubscription (COVIncrement), a BACnetBDTEntry (broadcastMask), and the ConfirmedPrivateTransfer request and ACK and UnconfirmedPrivateTransfer request (their parameter block). An absent field used to render as a zero, or as an empty element. This follows #4381; encoding is unchanged.
  • rb dv-20261019-123 (#3435): a Network Port’s IP_DNS_Server that the application reports with no entries now reads as one X’00000000’ entry (not configured), as it already did when the application declines. Cl. 12.56.31 requires at least one entry.
  • rb dv-20261019-124 (#4401): a Trend Log’s Log_DeviceObjectProperty can now be written where the application makes it writable. A write that changes the reference empties the buffer, logs a BUFFER_PURGED record as the first entry and resets Records_Since_Notification (cl. 12.25.8); a write of the same reference changes nothing, and a reference to another device is refused with OPTIONAL_FUNCTIONALITY_NOT_SUPPORTED, as logging another device’s property is not supported. Adding or removing the optional device identifier counts as a change, as the encoded value changes. Before, the stack did not handle the write: it fell through to the application’s set-property callback. Now the stack handles it and the callback is not called for this property. Trend Log Multiple is unchanged: cl. 12.30 lets it mark the changed element’s earlier values LOGGED_VALUE_PURGED instead of purging, which it already does.
  • rb dv-20261019-125 (#4328 items 1 and 2): on a virtual router, the router device’s own message to a device the stack hosts now carries the SNET/SADR a router adds (cl. 6.5.4) when the router device’s port is a numbered MS/TP port (its MAC_Address, from the application) or a numbered BACnet/SC port (the node’s VMAC); before, only a BACnet/IP port gave one. One rule picks the port (the first numbered one with a known MAC, in the order BACnet/IP, MS/TP, BACnet/SC) for both the router device’s messages and a hosted device’s requests to it, so they still match. The router device’s requests to hosted devices no longer draw an invoke id that one of its other outstanding requests to the same device holds, which made the second one fail to send.
  • rb dv-20261019-126 (#4410): on a virtual router, a confirmed request the router device sends to a device the stack hosts, through an MS/TP or BACnet/SC Network Port, is now answered. Its transaction was kept under that port’s network type, while the answer is delivered internally as BACnet/IP, so the answer never matched and the request was retried until it timed out.
  • rb dv-20261019-127 (#4412): on a virtual router, the router device’s audited confirmed request to a device the stack hosts now carries its operation-source audit records (Audit_Source_Reporter), and in test-tool builds a forced duplicate invoke id (#84) to such a device is sent untracked as on any other link instead of refused. Both looked the request up under the link the caller named, while it is kept under the internal delivery source.
  • rb dv-20261019-128 (#4334 slice 2): a virtual router (no physical router enabled) now sends its start-up I-Am-Router-To-Network, and a Network-Number-Is for a configured port number, on each BACnet/SC port as well: once that port’s hub connection is up, through the hub, to the Local Broadcast VMAC (cl. 6.6.2, AB.4.2.1). Each BACnet/SC port announces when its own hub connects, including a port added later.
  • rb dv-20261019-130 (#4313): a BACnet/SC confirmed request addressed to a peer by VMAC (“uri;VMAC”) is now matched by its answer. Every answer carries the peer’s VMAC as its source address, and the request was kept with no address, so it was retried until it timed out. HookTimeout and HookAbort now report that VMAC as the request’s destination address.
  • rb dv-20261019-130 (#4313): such a request that the node switch sent over a direct connection (AB.4.2.1) is retried through the hub, addressed to the peer’s VMAC, once that direct connection has gone and the hub connection is up. The retries went to the closed connection. The request is then kept under the hub’s connection, where its answer arrives.
  • rb dv-20261019-131 (#4419): a BACnet/SC confirmed request to a peer, named by “uri;VMAC” or by the peer’s VMAC as the destination address, no longer draws an invoke id another request to that peer has outstanding - under the hub connection or a direct connection - which got the second one refused. A segmented “uri;VMAC” request now keeps its segmentation state, so the peer’s SegmentACKs move it on; they were ignored and the retry resent the first segment unsegmented.
  • rb dv-20261019-132 (#2690 CR2-F12-012): RemoveListElement on a Group’s List_Of_Group_Members now removes a member only when the complete element matches - the object and its whole list of property references (cl. 15.2.2) - so a request naming a member with other properties is refused with LIST_ELEMENT_NOT_FOUND instead of removing it. A request naming only the object still removes a member that holds just the default Present_Value reference, when no member is that exact element.
  • rb dv-20261019-133: a build without Network Port objects (an EMBEDDED build) compiles again - BACnetNetworkParameters.cpp read Network Port objects unguarded (#2526) - and a build without the Schedule and Calendar objects compiles again - BACnetDBDevice.h declared NetworkPortInUseRead (#4404) inside their block.
  • rb dv-20261019-18 (#2842, CR2-F29-030): a Program object that the stack moves to HALTED itself, after a Program_Change write of HALT with no CallbackProgramChangeRequested (or one that returns false), now reports Reason_For_Halt PROGRAM (3), the value cl. 12.22.6 gives a process halted by a Program_Change request. It used to report NORMAL (0), which says the process is not halted. Every other change the stack completes still reports NORMAL. The callback’s documentation says PROGRAM (comment-only, IFC-158). Revert-red: without the change, BACnetStackProgramTest.Issue2842_HaltThroughProgramChangeReportsProgram read 0, expected 3.
  • rb dv-20261019-2 (#4149, #3681 CR3-F09-025): the BACnet/SC state-change callback (BACnetStack_RegisterCallbackBACnetSCStateChange) now fires for every initiating-peer transition, as its documentation says. The direct-connect initiator never reported AWAITING_ACCEPT or DISCONNECTING, and returned to IDLE without a report when its communication was suspended or initiation was turned off. A hub connection that ended through a websocket reset (a refused or timed-out Connect-Request, a Disconnect-Request from the hub, a websocket error, a failed Connect-Request send, a local disconnection) also returned to IDLE silently. Each report now fires once the state it reports is fully set up, so a host that calls back into the stack from the callback finds it consistent. A host now sees each connection come up and go down in order, for example 0->1->2->3 and then 3->4->0.
  • rb dv-20261019-20 (#4193, split from #3753, CR3-F26-004): a Lift’s Car_Position, Car_Moving_Direction, Car_Assigned_Direction, Car_Door_Zone, Car_Load, Next_Stopping_Floor, Passenger_Alarm, Energy_Meter, Car_Drive_Status, Car_Mode, Making_Car_Call, Car_Door_Status and Car_Door_Command, and an Escalator’s Power_Mode, Operation_Direction, Energy_Meter, Passenger_Alarm and Escalator_Mode, can now be written while the object’s Out_Of_Service is TRUE, as cl. 12.59.30 and 12.60.15 require; the write reaches the host’s Set callback for the property’s datatype. While Out_Of_Service is FALSE the write is still refused with WRITE_ACCESS_DENIED. The stack does not range-check the written value; the host’s callback does, as for other out-of-service writes. Assigned_Landing_Calls, Registered_Car_Call and Landing_Door_Status (constructed values) and Fault_Signals (a list) need a host callback for complex values and are still read-only (#3753). Revert-red: without the change, Issue4193LiftEscalator.TheClausesStatusAndControlPropertiesAreWritableWhileOutOfService and the write tests failed.
  • rb dv-20261019-21 (#4192, cl. 19.1.3.4): a restore that ends successfully (ReinitializeDevice ENDRESTORE with the host’s CallbackCompleteRestore returning true) now sets the stack-held Database_Revision back to its value when STARTRESTORE was accepted, then increments it. Before, it kept whatever the restore had moved it to, so a client caching on Database_Revision could miss the replaced configuration. A restore the host fails to apply, or one ended from RESTORE_FAILURE, leaves it alone. A host that serves Database_Revision through CallbackGetPropertyUInt still owns its own value (#3695).
  • rb dv-20261019-22 (#4200, cl. 19.1.2.5, 19.1.3.2): a File that device A creates through CreateObject while a restore is being performed no longer changes Database_Revision, and neither does deleting a configuration File during a backup. Both used to raise it by one. Creating a File at any other time, deleting a File that is not a configuration file, and deleting a configuration File outside a backup still count. (Adding a configuration File with BACnetStack_AddFileObject already did not count.)
  • rb dv-20261019-23 (#4194, from #2150; cl. 12.31.30): Access_Event_Credential of an Access Point the application serves (added with BACnetStack_AddObject, its value not served by the host) answered ReadProperty with Abort(other). It now reads as the “no credential” reference: Access Credential instance 4194303 with no device identifier, the value the stack-held Access Point serves before its first access event. A test-tool build whose host registers the typed DeviceObjectReference Get callback still answers from that callback.
  • rb dv-20261019-24 (#4206; cl. 12.31.31, 12.32.17, 12.32.19): three optional access-control properties answered ReadProperty with Abort(other) when an application enabled them on an object it serves (added with BACnetStack_AddObject) but did not serve a value. They now read as their “nothing yet” values:
    • Access Point Access_Event_Authentication_Factor: format type UNDEFINED, format class 0, empty value.
    • Access Zone Last_Credential_Added and Last_Credential_Removed: Access Credential instance 4194303, the stack-held Access Zone’s value before any credential is added or removed.
  • rb dv-20261019-25 (#4210; cl. 12.31.35-36, 12.35.17, 12.35.19): four optional access-control properties answered ReadProperty with Abort(other) when an application enabled them on an object it serves (added with BACnetStack_AddObject) but did not serve a value. They now read as their “references nothing” values, the same as the stack-held objects serve:
    • Access Point Zone_To and Zone_From: Access Zone instance 4194303.
    • Access Credential Belongs_To: Access User instance 4194303.
    • Access Credential Last_Access_Point: Access Point instance 4194303. Access Rights Accompaniment is unchanged: its clause defines no “none” value.
  • rb dv-20261019-28 (#2852): the native Windows test build (windows-tests preset) compiles again, and BACnetUTCTimeSynchronizationRange.Year1969NeverCrashesOrWraps passes on MSVC. The test expected Windows to refuse 1969-12-31 23:59 UTC, but MSVC’s runtime converts it correctly (time_t -60; its range reaches 12 hours before the epoch). A new test covers 1969-12-30 23:59, which MSVC refuses and glibc converts. TestBACnetChannelValue.cpp built NaN and infinity by dividing by a constant zero, which MSVC rejects at compile time (C2124); it now uses std::numeric_limits. No library behaviour change (two source comments now give MSVC’s real pre-1970 limit). README and AGENTS.md list the Windows-only failures the native run now shows (#4217).
  • rb dv-20261019-3 (#4158): the BACnet/SC state-change callback now also reports a websocket status going back to Unknown when the stack resets a hub or direct-connect websocket at the end of an attempt or a connection. Before, the last status a host saw for a closed connection could be Connected. Each report starts from the status the socket actually had.
  • rb dv-20261019-30 (#3712 CR3-F10-007, owner ruling on #4163 item 7; cl. 15.8.1.1.4.3.1): a ReadRange By Time request whose ‘Reference Time’ is not a specific datetime is answered Reject(PARAMETER_OUT_OF_RANGE), as a Count of 0 already is. That covers an unspecified or pattern year, month or day, and an unspecified hour, minute or second. Before, it was compared as a real date and answered with the wrong items. An unspecified day of week is still accepted, and an unspecified hundredths (a to-the-second time) is accepted and read as .00, the start of that second.
  • rb dv-20261019-32 (#4203, CR3-F24-012; owner ruling on #4163 item 9; cl. 12.36.11): a Credential Data Input’s Update_Time now advances when a change to Supported_Formats or Supported_Format_Classes alters what Present_Value serves (Format-Type UNDEFINED, or another Format-Class), and the change goes through the value-update pipeline, so COV subscribers, who key on Update_Time, are notified. A change that leaves Present_Value as it was advances nothing, and neither does any change while Out_Of_Service is TRUE.
  • rb dv-20261019-34 (#3733): a CHANGE_OF_CHARACTERSTRING alarm value is matched against Present_Value character by character, not octet by octet (clause 13.3.16). UTF-8, UCS-2, UCS-4 and ISO 8859-1 strings are decoded and match across those character sets; a DBCS or JIS X 0208 string matches only a Present_Value in the same set. Before, the UTF-8 alarm value “FIRE” matched the UCS-2 octets 46 49 52 45 (two unrelated characters), and the same characters in two different sets did not match. Malformed UTF-8 (including Latin-1 octets in a string declared UTF-8), or a UCS-2/UCS-4 value that is not a whole number of characters, matches nothing - where the octet search used to match an ASCII alarm value inside it. The BACnetStack_SetIntrinsicChangeOfCharacterstringAlgorithm documentation says so (comment-only, IFC-162).
  • rb dv-20261019-35 (#2746 CR2-F17-016): the OUT_OF_RANGE, UNSIGNED_RANGE, DOUBLE_OUT_OF_RANGE, SIGNED_OUT_OF_RANGE and UNSIGNED_OUT_OF_RANGE event algorithms now default pLimitEnable to both flags TRUE. Clauses 13.3.6, 13.3.9 and 13.3.13-13.3.15 require that when the parameter is not provided; the default was FALSE. No current configuration path changes behaviour, because every intrinsic and Event Enrollment path passes the flags explicitly. The algorithm’s own default now conforms for any path that leaves them unset.
  • rb dv-20261019-37 (#2526 CR2-F06-022, slice 1): a notification to an address-form recipient on a REMOTE network is now sent the way clause 6.5.3 describes. The NPDU carries DNET and DADR (the recipient’s MAC) and goes to the router for that network, which the stack learns from the SNET of received frames (a node that is not a router keeps up to 32 networks, and the newest frame wins). While that router is not known, it goes as a local broadcast. Before, the remote MAC was used as the link destination and DADR was never set, so the frame named a broadcast on the remote network and went to the wrong link station. A zero-length MAC is now accepted as a broadcast (local, remote or global); it used to be refused. A network number equal to one of the device’s own Network Ports is sent with no DNET. Not yet in this slice: a CONFIRMED request to a remote device whose router is not known keeps the old addressing (its answer comes from the router and is not yet matched); a device with no Network Port object keeps the old addressing for remote unicasts; learning from I-Am-Router-To-Network in non-router builds; BACnet/SC; the learned router’s Network Port.
  • rb dv-20261019-40 (#3138, slice 1): devices hosted on this stack’s virtual router can now send unconfirmed event and audit notifications to each other. A unicast from one hosted virtual device to another (DNET/DADR naming a co-hosted device) never reached the wire’s far side, so it is now delivered internally on the next Tick. A recipient named by device instance, which a co-hosted device never was in the DAB, falls back to the virtual-router table. One named by address is also recognised: its virtual network and 3-octet MAC, or network 0 from the same virtual network. Broadcasts, confirmed requests, the router device’s own frames and every other destination are unchanged, and a device is never delivered its own notification. Internal deliveries come from the source 0.0.0.0:0, which is never learned as a device’s address. Confirmed exchanges between co-hosted devices are the issue’s second slice.
  • rb dv-20261019-41 (#2694 CR2-F16-032): an AcknowledgeAlarm whose Acknowledgment Source uses a reserved character set is processed instead of Rejected, as clause 13.5.2 requires. The source is read as an empty UTF-8 string. Only that field is lenient; the shared CharacterString decoder still refuses a reserved character set.
  • rb dv-20261019-42 (#3678 CR3-F06-022): a router port is now disabled while its Network Port object’s Out_Of_Service is TRUE, or while a BACnet/SC port has a primary hub configured and no hub connection (and no hub function or direct connection to send through). It is enabled again when neither holds. Its routes then read DISCONNECTED in Routing_Table, the networks behind it leave Initialize-Routing-Table-Ack and I-Am-Router-To-Network, what is held for it is dropped, and the start-up announcement goes out when it comes back. Before, only a direct call (tests) could disable a router port. Out_Of_Service comes from the stored value when the stack holds it, otherwise from CallbackGetPropertyBool. That callback is now asked for each router port’s Network Port Out_Of_Service on every Tick while the router is enabled. A message for a network behind a disabled port takes the unknown-network path (a Who-Is-Router-To-Network on the other ports), where it used to be handed to a data link that could not send it. IPv4 and MS/TP ports have no link-down signal.
  • rb dv-20261019-43 (#4243): an object with a fault algorithm and no event algorithm now tracks its own Event_State. Its Event_State reads FAULT while it is in fault (it always read NORMAL), and its TO_FAULT notifications carry From State, the Event_State before the transition, as Table 13-3 gives every event state transition. Before, they carried none. Consequences that follow the spec: such an object is now listed by GetAlarmSummary, GetEventInformation and GetEnrollmentSummary while in fault; its Status_Flags IN_ALARM reads TRUE while in fault, and the Event_State change is reported to COV subscribers. Inhibiting reliability-evaluation while it is in fault now sends a TO_NORMAL. Disabling event detection returns it to NORMAL.
  • rb dv-20261019-44 (#4250): attaching an event algorithm to an object that is in FAULT through its fault algorithm alone (for example an Analog Input given a fault check before its intrinsic reporting) now keeps the object in FAULT. Before, the new algorithm started in NORMAL: Event_State dropped from FAULT to NORMAL with no transition, and the next evaluation sent a second TO_FAULT from NORMAL.
  • rb dv-20261019-45 (#4228): the FAULT_CHARACTERSTRING fault algorithm matches its fault values character by character, in and across the UTF-8, UCS-2, UCS-4 and ISO 8859-1 character sets, as the CHANGE_OF_CHARACTERSTRING event algorithm has since #3733. Each fault value keeps its character set, including one written through Fault_Parameters and read back from it. The check that no alarm value is also a fault value compares characters too, so the same text in two character sets is now reported as a configuration error, and the same octets in two character sets no longer are. As on the event side, a value that is not well-formed in its character set (malformed UTF-8, a partial UCS-2 or UCS-4 character) now matches nothing and overlaps nothing.
  • rb dv-20261019-46 (#4239): on a Notification Forwarder whose properties the host serves (added with the generic object functions), a test-tool build now hands a whole-array WriteProperty or WritePropertyMultiple of Port_Filter to BACnetStack_RegisterCallbackSetPropertyConstructed’s callback. It arrives as one value: no array index, and the octets of every element in order. The host can therefore apply the write all or nothing. Before, only an indexed write reached the host, and a whole-array write was refused WRITE_ACCESS_DENIED. An empty whole-array write is still refused without asking the host. IFC-171.
  • rb dv-20261019-47 (#4253): CHANGE_OF_CHARACTERSTRING alarm values and FAULT_CHARACTERSTRING fault values in the IBM/Microsoft DBCS character set now match by code page first. A value’s characters then match anywhere after its two code-page octets in a monitored value from the same code page. Before, a DBCS value was found only at the start of another value. A DBCS value made of only its code page is now the empty string: it matches an empty monitored value, and the alarm/fault overlap check treats it as the same value as any other empty string. Matches in DBCS and JIS X 0208 are still not held to a character boundary, because the stack has no tables for their character widths.
  • rb dv-20261019-48 (#4223): a Credential Data Input’s Out_Of_Service is read through the host’s callback, so the stack sees no event when it changes. Its Present_Value Format-Type and Format-Class are now checked once a Loop second against what they last served. A return to service, or a move out of service, that changes what Present_Value serves therefore advances Update_Time, and COV subscribers are notified. An example is a return to service that makes Present_Value read UNDEFINED because Supported_Formats changed while the object was out of service. Pushes, format changes and Present_Value writes report any such pending change first and record their own afterwards, so no change is reported twice or lost. A device with a Credential Data Input now reads each one’s Out_Of_Service from the host once a second.
  • rb dv-20261019-50 (#3718 CR3-F16-016, owner ruling on #3883 item 2): a WriteProperty of an empty list to a stack-held Notification Forwarder’s Subscribed_Recipients, or to its Recipient_List once that is made writable, now clears the list and fires the matching persistence hand-off. The recipients’ address-binding references are released on the next Loop. Before, the write was refused PROPERTY / INVALID_DATATYPE, because an empty list has no element datatype. A forwarder the host serves still refuses it. CR3-F16-010 (ruling item 4) needed no behaviour change: GetEnrollmentSummary’s Event Type Filter still matches each object’s Event_Type, and a comment now records why.
  • rb dv-20261019-51 (#3757 CR3-F17-012, owner ruling on #3883 item 5): a NaN or infinite Deadband left OUT_OF_RANGE, DOUBLE_OUT_OF_RANGE and FLOATING_LIMIT unable to return to NORMAL. That value is now refused in four places:
  • rb dv-20261019-53 (#4265, part 1): disabling backup and restore while a procedure is running now ends it the same way the Backup_Failure_Timeout does. The host is told through CallbackCompleteBackup or CallbackCompleteRestore (as aborted), and System_Status leaves BACKUP_IN_PROGRESS or DOWNLOAD_IN_PROGRESS. Before, it stayed there for good, although cl. 19.1.3.4 requires System_Status to leave DOWNLOAD_IN_PROGRESS once a restore ends. A restore the host could not recover from gives DOWNLOAD_REQUIRED; otherwise the status is OPERATIONAL. A backup returns to the status it started from. The completion callback is called from inside the BACnetStack_SetBackupAndRestoreEnabled(..., false) call. The procedure has already ended by then, so calling the function again from the callback does nothing more. IFC-174 (doc lines only).
  • rb dv-20261019-54 (#4235, the #2601 ruling): while a node’s own BACnet/SC hub connector is connected to the node’s hub function (the accepted peer with the node’s UUID), the hub function now reaches the node through that connection, as AB.5.3 has it reach every node:
    • A unicast for the node’s VMAC is relayed to the connector, and so are an Address-Resolution and an Advertisement-Solicitation addressed to it. Before, they were handled in-process or answered by the hub function.
    • A broadcast now reaches the node once, through the connector. Before, the node also took an in-process copy, including of its own broadcasts.
    • The node’s own broadcast through its hub function is no longer sent back to it.
    • The connector peer is matched by the node’s UUID, so after the node’s VMAC changes, frames for either VMAC still reach it once.
    • With no connector peer, the node is reached in-process as before.
  • rb dv-20261019-57 (#3679 CR3-F07-023): after BACnetStack_Reset, the BBMD role returns to the Network Port the host made the BBMD, together with its mode, BDT and FDT, when the host re-adds that port. That port’s BACnet_IP_Mode reads BBMD again. Before, Reset re-enabled BBMD mode on IPv4 instance 0, so whichever port was re-added first became a BBMD. The table also stayed with the old data link index rather than with the port.
  • rb dv-20261019-58 (#4112): a wall clock that moves past the moment a router entry was stamped and then freezes no longer leaves the entry stuck. This covers a held message, a busy (congestion) indication and a learned next-hop MAC. Each now expires on the Tick count once the clock has not moved for the stall bound, counted from the clock’s last change, as an entry stamped on a stalled clock does (owner ruling, option C). Before, such an entry lasted until the clock moved again.
  • rb dv-20261019-59 (#3939): a list read served by CallbackGetListOfEnumerations no longer repeats an element. Before, the previous element was added again when a call returned true without writing one. Each element now starts unset (UINT32_MAX), so such a call adds nothing. more still keeps the value the previous call left in it. The callback’s doc in CASBACnetStackDLL.h is updated (IFC-175).
  • rb dv-20261019-59 (#3939): the host list callbacks that are called again while they set more now stop at 200,000 elements and refuse the read (outDeclined). This covers CallbackGetListOfEnumerations, the three lift sequence callbacks (assigned landing calls, registered car calls, landing door status) and the elevator group’s landing-call list. Before, a callback that always set more looped for ever.
  • rb dv-20261019-60 (#3939): the API Reference’s enumeration generator (_ci_scripts/generate-api-reference-enums.js, run by generate_doxygen.sh when a release builds the docs) failed on the pre-2024 engineering-unit names #2851 kept as aliases (voltsPerDegreeKelvin = voltsPerKelvin). An enumerator defined as an earlier one of the same enumeration now takes its value. pre-push.sh now runs the generator (--list), so a header it cannot read fails before a push rather than at release.
  • rb dv-20261019-62 (#2289): Node package, test-tool SendAlert: a vendorId above 65,535 now throws a range error. Before, the wrapper passed a 32-bit value to the uint16_t C parameter, which silently wrapped (65,536 became 0).
  • rb dv-20261019-62 (#2289): Node package, SendWriteGroup: changeListCount is read at the C export’s own width (uint32_t). The wrapper used to refuse any count above 255. The buffer-length guard (#447) still holds the count to what the buffer carries.
  • rb dv-20261019-62 (#2289): Node package, SetIntrinsicBufferReadyAlgorithm, SetIntrinsicDoubleOutOfRangeAlgorithm, SetIntrinsicFloatingLimitAlgorithm, SetIntrinsicSignedOutOfRangeAlgorithm, SetIntrinsicUnsignedOutOfRangeAlgorithm and SetIntrinsicUnsignedRangeAlgorithm: an objectType above 65,535 now throws a range error. Before, the wrapper passed the 32-bit read straight to the uint16_t C parameter, so 65,536 became 0 (Analog Input).
  • rb dv-20261019-64 (#2526): a device that is not a router now learns the router to a remote network from an I-Am-Router-To-Network (cl. 6.4.2), not only from the SNET of a routed frame. A later send to a device on one of the listed networks goes to that router instead of a local broadcast. The message is now decoded in any build with Network Port objects; before, it was decoded only in router builds and otherwise dropped. A network this device is on itself is never learned. One message teaches at most 16 networks (half the cache), so a single broadcast cannot replace every router learned before. BACnet/SC already learned router VMACs this way in its hub connector, and that path now works without the router build too.
  • rb dv-20261019-65 (#3138): a virtual device no longer sends a Who-Is, on every heartbeat interval for ever, for a device-form recipient that the same virtual router hosts. That device’s I-Am never comes back from the wire, so its address-binding entry never resolved, and unconfirmed messages to it already go internally from the virtual-router table. The heartbeat leaves such an entry out without counting it as a miss. External devices in the same table are still sought.
  • rb dv-20261019-66 (#2526): a message this stack sends on its own to a device on a remote network (an event notification to an address-form recipient, for one) now leaves by the Network Port the router for that network was learned on. It used to leave by the first BACnet/IP port, which on a multi-port device may not reach that router at all. A send that names its port, and a send to a network whose router is not known yet, are unchanged.
  • rb dv-20261019-67 (#2526): the event notification sent after an AcknowledgeAlarm (ACK_NOTIFICATION) to a device on a remote network now leaves by the Network Port that network’s router was learned on. It used to leave by the port the acknowledgement arrived on, which on a multi-port device may not reach that router.
  • rb dv-20261019-69 (#2526): a message this stack sends on its own (an event or audit notification, for one) to a device on a remote network, from a BACnet/SC port, now goes on the connected hub with the destination network and address. The data link addresses it to the router it learned for that network, or broadcasts it while none is known. It used to use the destination’s own address as the connection string, which names no connection, so nothing reached the hub. With two BACnet/SC ports, the hub whose connection learned the router is chosen.
  • rb dv-20261019-72 (#2526): a device with no Network Port object now sends to a remote network it has learned a router for (from a routed frame or an I-Am-Router-To-Network) through that router, carrying the destination network and address. It used to send straight to the destination’s MAC, which is not on its link. Other networks are unchanged.
  • rb dv-20261019-72 (#2526): a device no longer learns a “router” to one of its own Network Ports’ networks from a received frame’s source network; such a frame would have redirected its traffic for its own network to the sender.
  • rb dv-20261019-73 (#3138): an unconfirmed message from one virtual device to another that the same stack hosts, sent through an MS/TP or BACnet/SC Network Port (for example SendIAm naming that port), is now delivered internally like one sent through BACnet/IP. It used to go out on the wire, where nothing brought it back.
  • rb dv-20261019-74 (#3138): a confirmed request (a confirmed event or audit notification, for one) from one virtual device to another that the same stack hosts is now delivered internally, and its answer comes back the same way. It used to be skipped. A retry while no answer has come is delivered internally too, and a device that rejects the request ends the exchange with its Reject. Segmented exchanges between such devices are not covered.
  • rb dv-20261019-84 (#3687): a device that cannot transmit segments no longer aborts a ReadPropertyMultiple request on the response-size estimate. The estimate prices a whole-array read at 1000 elements and each string at the maximum string size, so requests whose real response fits one APDU were answered with Abort SEGMENTATION_NOT_SUPPORTED. Among them were 135.1-2023 9.20.1.12’s 162 × Object_Identifier from a 1476-octet client, a whole Object_List read, and a few Object_Names. Such a device now measures the encoded response after each property reference and aborts as soon as the response, with its 3-octet Complex-ACK header, can no longer fit the client’s maximum APDU. It therefore never holds more than about one APDU of results, which keeps the memory bound the estimate used to give (#667). Cl. 15.7.2 has processing continue until every specified property has been attempted. Segmented-transmit devices keep the estimate as their transmit-budget bound. Revert-red: the 162-reference and Object_List tests aborted with reason 4 on the pre-fix code.
  • rb dv-20261019-85 (#2907 item 1): tests now pin a Life Safety Zone’s Silenced across roll-up transitions (cl. 12.16.23: Silenced belongs to the most recent transition of the Zone’s own Present_Value). The behaviour is unchanged. A roll-up that moves the Zone between two non-NORMAL values (GENERAL_ALARM → ALARM → GENERAL_ALARM) unsilences it each time. A second member reaching the value the Zone already reports is not a transition, and the Zone stays silenced. Item 2, a host setter for local-process silencing, is #4316 (session D).
  • rb dv-20261019-86 (#3998): a ReadProperty of a lighting-command Channel’s Present_Value now answers in the BACnetChannelValue lighting-command [0] arm (clause 21). It used to send the bare BACnetLightingCommand without the [0] tag, which a client decoding the CHOICE misreads. The WriteProperty side already carried the tag. A wire-level ReadProperty test pins the octets.
  • rb dv-20261019-87 (#2649, slice 1): a You-Are whose Device Identifier instance is 4194303 now makes the device unconfigured, as cl. 16.11.4 requires (cl. 19.7). Before, the request was refused.
    • Model: 4194303 is the stack’s unknown-device sentinel, so the device keeps its stored instance and is flagged (BACnetDBDevice::IsUnconfigured).
    • While unconfigured: it sends no I-Am. It answers a Who-Is covering 4194303, or one with no range, with a Who-Am-I carrying its vendor identifier, model name and serial number. It does not answer a Who-Is for its stored instance.
    • Leaving the state: a later You-Are with a valid instance, including the stored one, configures it again and is answered with an I-Am.
    • Host consent: entering the state is offered to the host first through CallbackSetPropertyObjectIdentifier (the #2650 pattern), and a refusal keeps the device configured.
    • Not yet: executing nothing else while unconfigured is #4324, initiating nothing else is #4325, and the host exports are #4326 (session D).
    • Revert-red: the entry, Who-Am-I, re-configure and host-consent tests failed on the pre-fix code.
  • rb dv-20261019-88 (#3138, slice 2 item 2): the router device now reaches the devices hosted on its own virtual router internally, and their answers come back to it. That covers an event notification to a device-form or address-form recipient, and a client request such as ReadProperty named by DNET/DADR. Before, the router device’s frames carried no SNET, so they went to the wire and nothing brought them back.
    • Addressing (cl. 6.5.4, decided 2026-10-22): the message carries the SNET/SADR a router adds for a directly connected network: the port’s network number and the router device’s address on it.
    • Fallback: a port with no network number, or a data link whose own address is not known (only BACnet/IP reports one), sends it with no SNET.
    • The answer: a hosted device’s answer, addressed to the router device, is handed back with DNET/DADR removed, as a router does for a directly connected destination.
    • Transactions: the router device’s transaction is keyed under the internal source, so its answer ends it and a retry is re-queued.
    • Customer-visible: hooks report the answer’s source link address as the internal source, 0.0.0.0:0, not the link address the caller named.
    • Revert-red: the notification, confirmed-notification, ReadProperty and SNET/SADR tests failed on the pre-fix code.
  • rb dv-20261019-89 (#3304): a responder in SEGMENTED_RESPONSE that receives the client’s Abort (server = FALSE) now ends the transaction even after its SegmentTimer has expired (cl. 5.4.5.4 AbortPDU_Received). Before, the Timeout transition ran first and re-sent the window the client had just cancelled. AWAIT_RESPONSE already ended on a client Abort (CR2-F05-001). The ABORT.indication to the application is a new hook, #4310. Revert-red: the expired-timer Abort test re-sent the window on the pre-fix code.
  • rb dv-20261019-89 (#4311): a segmented Confirmed-Request for a service this build does not support is now answered with Reject UNRECOGNIZED_SERVICE, as the unsegmented request is (ruling 5, owner ruling on #3801). It used to get Abort(OTHER), which remains the answer for an allocation failure. The factory now says which of the two a NULL means.
  • rb dv-20261019-89 (#4311): when a reassembled segmented request fails (Reject, Error or Abort), the final SegmentACK now goes out before that answer (cl. 5.4.5.2 LastSegmentOfMessageReceived), as it does on the success path. Revert-red: both #4311 tests failed on the pre-fix code.
  • rb dv-20261019-90 (#3498): STACK_OPTION_BVLL_BBMD now turns on the Network Port object (STACK_OPTION_OBJECT_TYPE_NETWORK_PORT), however the BBMD option was set. A BACnet/IP build with the BBMD but without Network Port failed to compile BACnetBDTEntry.cpp. The STACK_OPTION_NM_BBMDC path stopped with an #error instead. Cl. 12.56 requires a Network Port object for each physical port, and every build is revision 24 or later, so this is the conformant choice (decided 2026-10-22, option (b)). Revert-red: the issue’s -fsyntax-only probe failed with 36 errors on the base; all 644 source/*.cpp now compile in that configuration.
  • rb dv-20261019-90 (#4331): BACNET_MAX_CALLBACK_LIST_ELEMENTS (#3939) was defined inside the STACK_OPTION_ROUTER_PHYSICAL block of BACnetStackConstants.h, so every build without the physical router failed to compile BACnetBusinessLogic.cpp. It is now defined outside that block. Found by the same probe; every gate builds TARGET_FULL, which has the router.
  • rb dv-20261019-91 (#4307): a virtual router (a gateway with virtual networks and no physical router) now broadcasts an I-Am-Router-To-Network listing its virtual networks on each BACnet/IP port, once, on the first Tick after a virtual network is added (cl. 6.6.2). It used to announce nothing, because the start-up announcement ran only for a physical router. AddVirtualNetwork itself still sends nothing. A physical router keeps its own announcement, which already lists the virtual networks (#3648).
  • rb dv-20261019-91 (#3382): the same start-up broadcast includes a Network-Number-Is for a port whose network number is configured (cl. 6.4.20, applied to a gateway by Annex H.1.1.2). A port with an unknown or learned number sends none. The BACnetStack_AddRouterPort documentation now says that port ID 0 is rejected (IFC-180).
  • rb dv-20261019-91 (#3649): a vendor-proprietary network layer message (X’80’-X’FF’) sent to a router (physical or virtual) with no DNET is now answered with Reject-Message-To-Network reason 3, unknown message type (cl. 6.5.2.2, 6.4.4), as the reserved types are, on every data link (BACnet/SC included) and within the per-source reject limit. A device that is not a router still ignores it. The stack interprets no vendor-proprietary message. It used to drop it with nothing sent back. A vendor message that carries a DNET is handled as before (a router relays it), and a broadcast one draws no reject.
  • rb dv-20261019-92 (#3663): the hub function now sends a Disconnect-Request to each CONNECTED peer before it closes the connection itself (AB.6.2.3, the #3524 owner ruling). This covers disabling the hub function, clearing its accept URIs, and replacing a peer that reconnects with the same Device UUID. The socket is still closed in the same call. The direct-connect accepting side is #4336.
  • rb dv-20261019-92 (#3670): SetBACnetSCCertificateFileObjects refuses a set in which an Issuer_Certificate_Files element names the operational certificate File or the CSR File (cl. 12.56.100, 12.56.101; ruling 4, as #2559), and changes nothing. With such a set, an issuer rollover overwrote the port’s own certificate.
  • rb dv-20261019-93 (#3696): a ReinitializeDevice WARMSTART or ACTIVATE_CHANGES received while a backup or restore is in progress (any Backup_And_Restore_State but IDLE) is refused with DEVICE / CONFIGURATION_IN_PROGRESS before the host’s callback (cl. 16.4.1.1.1, 16.4.1.3.1). Until now the request was admitted: Network Port changes could be applied mid-restore, and the host could be asked to reboot. COLDSTART is unchanged.
  • rb dv-20261019-93 (#4324): a device in the unconfigured state (cl. 19.7, entered by a You-Are with Device Identifier 4194303) now executes only Who-Is and You-Are. Every other request addressed to it is ignored, and a confirmed request gets no answer, because an unconfigured device has no valid identifier to be addressed by. A gateway’s other devices are unaffected.
  • rb dv-20261019-93 (#4325): an unconfigured device initiates nothing but Who-Am-I. COV and event notifications, the start-up and host-requested I-Am, Schedule, Timer and Channel writes to remote devices, and client requests are all held at BACnetDataLink::ProcessOutgoingPacket, the send funnel every data link uses, and at the BACnet/IP foreign-device I-Am, which bypasses that funnel. Confirmed-request retries and the device chosen for host sends are #4338. Network layer messages are not affected.
  • rb dv-20261019-94 (#3821): a BACnet/IP Network Port’s BACnet_IP_NAT_Traversal and BACnet_IP_Multicast_Address are writable when present, as cl. 12.56.36 and 12.56.30 require. They were profiled read-only and refused with WRITE_ACCESS_DENIED.
    • A write sets Changes_Pending, and a read returns the written value while it is pending.
    • ReinitializeDevice ACTIVATE_CHANGES or WARMSTART hands the value to the host, which serves both properties, through CallbackSetPropertyBool / CallbackSetPropertyOctetString. This is part of the existing all-or-nothing activation, so a refused activation hands back what the host had already taken. DISCARD_CHANGES drops the pending value.
    • The multicast address must be 4 octets. A write is refused on a port that is not BACnet/IP, and when the host has no Set callback or no ReinitializeDevice callback to activate the value.
  • rb dv-20261019-95 (#3699): when CallbackReadFile returns more octets than an AtomicReadFile stream request asked for, the stack still clamps the data to ‘Requested Octet Count’, and now decides End Of File for the clamped data from File_Size (cl. 14.1.3.1: TRUE exactly when the answer holds the last octet). It used to pass on the callback’s TRUE, so a client that reads until End Of File stopped with octets unread.
  • rb dv-20261019-95 (#3870): an AtomicReadFile record read of zero records at a start record equal to Record_Count is now answered with an empty ACK (cl. 14.1.5: that start does not exceed the file), as a zero-octet stream read at File_Size is (#2658). It used to get SERVICES / INVALID_FILE_START_POSITION. A read of one or more records from Record_Count is still refused.
  • rb dv-20261019-96 (#3755): a Lighting Output’s RESTORE_ON, DEFAULT_ON, TOGGLE_RESTORE and TOGGLE_DEFAULT now honour its Transition. Under FADE or RAMP the change fades or ramps from the output before the command; under NONE it still snaps. This applies to both Lighting_Command and the Present_Value special values -4.0 to -7.0, and to a command that halts a fade without changing the target. Cl. 12.54.19 has a transition run whenever Present_Value is directly commanded and exempts only FADE_TO, RAMP_TO and STEP_*. The WARN family is #4343.
  • rb dv-20261030-1 (#4661): a BACnet/SC node treats a malformed APDU the way BACnet/IP does. An Encapsulated-NPDU whose BVLC and NPDU are well formed but whose APDU cannot be decoded used to get a BVLC-Result NAK with MESSAGE_INCOMPLETE, which left the requester’s application layer to time out. It now goes through the BACnet/IP path’s error handling: usually a Reject-PDU, sometimes an Error-PDU or Abort-PDU, and no answer for an unconfirmed PDU, a broadcast, a request too short to report on, or a request for no device here. The Reject reason can still differ from BACnet/IP’s for a service the device has disabled. A payload with no identifiable PDU is discarded instead of NAKed (AB.2.4). This applies on the hub connector, on both directions of a direct connection and at the hub function.
  • rb dv-20261030-2 (#4663): a build without the Network Port object type compiles again. It stopped compiling with #4560, because the device address proxy held a type gated on that option. _ci_scripts/check-embedded-compile.sh now checks the configuration.
  • rb dv-20261030-2 (#4664): on a physical router’s hub function, an Encapsulated-NPDU holding an NPCI and nothing else no longer gives the router BVLC header octets as the APDU.
  • rb dv-20261030-3 (#4489 CR4-F09-012): a NODE_DUPLICATE_VMAC NAK on the primary probe restarts the failover connection with the new VMAC, instead of leaving it registered under the old one.
  • rb dv-20261030-3 (#4489 CR4-F09-015): a hub connector forwards to the local BVLL entity only what the established hub connection received (AB.5.2). A websocket awaiting its Connect-Accept, one closing after a fail-back and the primary probe no longer deliver NPDUs.
  • rb dv-20261030-3 (#4489 CR4-F09-030): on an initiated direct connection, only the Heartbeat-ACK with its Message ID answers a Heartbeat-Request (AB.6.3), as on the hub connector. Other traffic used to stand in for it.
  • rb dv-20261030-5 (#4666): a BACnet/SC hub connector no longer redials a hub that accepts and drops at once on the next pass. AB.6.1 sets a minimum wait between initiations, so the reconnect timeout now runs from the last initiation when an established connection is lost. A connection older than that wait is redialled at once, as before, and the attempt count is unchanged.
  • rb dv-20261031-1 (#4640): a Network Port’s Reliability reports RENEW_FD_REGISTRATION_FAILURE after a foreign device renewal fails (cl. 12.56.16), on BACnet/IP and BACnet/IPv6 ports. A renewal fails when the BBMD refuses a registration in force, when one lapses, and, on BACnet/IPv6, when RENEW_FD_REGISTRATION is refused or cannot be sent. The next accepted registration ends it, as does a BACnet/IPv6 port leaving foreign mode or a BACnet/IP port becoming a BBMD. A first registration that is refused renewed nothing, so it is not reported. While the port is Out_Of_Service the host’s Reliability is served.
  • rb dv-20261031-13 (#4603): the external WriteProperty a Channel, Schedule or Timer sends leaked its request when the send failed before the transaction took it (no invoke id, for one).
  • rb dv-20261031-3 (#4670): a Network Port with intrinsic reporting goes to Event_State FAULT and sends TO_FAULT when its Reliability reports a fault, and returns to NORMAL when the fault clears (cl. 12.56). It used to stay NORMAL. This includes the stack’s own RENEW_FD_REGISTRATION_FAILURE.
  • rb dv-20261031-4 (#4676): a Binary Lighting Output with no engine reads Priority_Array, Present_Value, Relinquish_Default and Feedback_Value and takes commands through the Enumerated callbacks, refusing a Present_Value outside the type’s list and a Relinquish_Default other than ON or OFF; those paths answered an error.
  • rb dv-20261031-4 (#4677): a Schedule or Timer command of a local host-managed object records that Schedule or Timer as the value source (cl. 19.5.1.3).
  • rb dv-20261031-4 (#4678): the customer header and the BACnet/SC port no longer say the stack does not proxy; a device whose physical router is enabled does.
  • rb dv-20261031-6 (#4315): tools/coverage/run.sh writes its HTML report with lcov 2.0; genhtml stopped on the same mismatched end lines the capture ignores.
  • rb dv-20261101-1 (#4695): a lighting Channel with a member whose device has no address yet holds IN_PROGRESS as a numeric one does, and sends the command when the device binds or is FAILED at APDU_Timeout x (Number_Of_APDU_Retries + 1) (135.1-2023 7.3.2.34.4 step 9). The known limit in dv-20261031-13 no longer applies. The pending-write table keeps its bound: an entry is one fixed size.
  • rb dv-20261101-1 (#4692): tests-lean and dev-build.sh --lean now build what they say. BACNET_STACK_TESTTOOL forces TARGET_FULL, so the preset also defines the two data link opt-outs, and the tests that need MS/TP or BACnet/SC are gated on them: the lean binary holds neither data link and runs 12,637 tests against the full 14,501.
  • rb dv-20261101-10 (#4732): a ConfirmedPrivateTransfer-Error whose [3] error-parameters pair holds nothing between its tags now decodes (cl. 20.2.1.6 allows zero or more elements, and the Request and ACK already accepted it); before, the Error refused it, so the client’s request never met its Result(-).
  • rb dv-20261101-10 (#4733): ENDBACKUP from BACKUP_FAILURE, and ENDRESTORE or ABORTRESTORE from RESTORE_FAILURE, no longer set System_Status to OPERATIONAL. From a failure state nothing was in progress, so System_Status stays as it was; before, a device reporting DOWNLOAD_REQUIRED reported OPERATIONAL after a procedure that did nothing, for instance when the retry of a failed restore failed in preparation and device A aborted it.
  • rb dv-20261101-10 (#4736, items CR4-F13-003, -004, -005; -005 is a test of the Backup_And_Restore state enumeration with no behaviour change): a DeviceCommunicationControl or ConfirmedPrivateTransfer request with no service parameters at all is now Rejected with MISSING_REQUIRED_PARAMETER (it was OTHER; a NULL buffer stays OTHER), and a refused DeviceCommunicationControl buffer leaves the request invalid rather than keeping the one an earlier decode produced. Items -006 to -009 of that roll-up are not done.
  • rb dv-20261101-100 (#4884, CR4-F23-001): an UnconfirmedAuditNotification that arrives over the wire reaches the ForDevice callback (#4565) with the receiving device’s instance; the unconfirmed dispatcher did not pass the device, so the callback was told device 0 for every one — revert-red: the new test saw device 0 through the dispatcher on the pre-fix code.
  • rb dv-20261101-100 (#4887, CR4-F24-001): an Access Door whose PULSE_UNLOCK is written at the priority an EXTENDED_PULSE_UNLOCK holds (or the reverse) starts the new value’s own countdown (Door_Pulse_Time or Door_Extended_Pulse_Time); the countdown was keyed on the priority alone, so a PULSE_UNLOCK stayed in force for what was left of the extended pulse (19 s with the defaults) — revert-red: the new test saw the PULSE_UNLOCK still in force past its 5.0 s on the pre-fix code.
  • rb dv-20261101-100 (#4888, CR4-F23-017): an enabled Audit Reporter whose Monitored_Objects overlap another enabled reporter’s (the same object, the same type, or an object and its own type) reports Reliability CONFIGURATION_ERROR; the first of the three conditions of its Reliability was not detected, so the misconfiguration (the lower instance silently wins) showed no fault — revert-red: the new test saw NO_FAULT_DETECTED for each overlapping pair on the pre-fix code.
  • rb dv-20261101-101 (#4898, CR4-F26-001): the Lift Car Door Command (none, open, close) and Lift Group Mode (unknown to up-peak) enumerations refuse a value outside their defined domain, as the door and lock enumerations do: a peer’s value past the domain decodes as a Reject UNDEFINED_ENUMERATION, and a WriteProperty of Car_Door_Command above 2 takes the base decode’s refusal (Error PROPERTY / VALUE_OUT_OF_RANGE by the WriteProperty mapping, relied on and not run end to end); both accepted any 32-bit value — revert-red: the new tests saw 3 and 7 accepted on the pre-fix code.
  • rb dv-20261101-101 (#4894, CR4-F25-004): a stack-held Life Safety Point or Zone has Alarm_Values and Life_Safety_Alarm_Values enabled only once it supports intrinsic reporting (when it is armed for CHANGE_OF_LIFE_SAFETY), as the object type’s footnotes require; they were enabled on every Point and Zone from Add — revert-red: the new test saw both properties present on an unarmed Point on the pre-fix code.
  • rb dv-20261101-102 (#4903, twin of #4787 CR4-F18-004): a CHANGE_OF_CHARACTERSTRING alarm value as long as the CharacterString cap (or longer) reads back in Alarm_Values and Event_Parameters; the element was built with the length-capped constructor, so the read failed with a misleading out-of-memory error for an alarm value the test-tool setup had accepted — revert-red: the new test saw the failed read of a 300-octet value on the pre-fix code.
  • rb dv-20261101-102 (#4876, follow-up to #4874): a Boolean CHANGE_OF_STATE arming on an Event Enrollment that references a Binary Output stores the enumerated binary state, as for a Binary Input or Binary Value, so ACTIVE matches; the Boolean alarm value it stored never matched the enumerated Present_Value — revert-red: the new test saw ACTIVE not match on the pre-fix code.
  • rb dv-20261101-102 (#4877, follow-up to #4833): a written Time_Delay carries to pTimeDelayNormal in every event algorithm that was armed with no Time_Delay_Normal of its own (Access Event, Change Of Bitstring, Characterstring, Discrete Value, Life Safety, State, Status Flags, Value, Command Failure, the Out Of Range family, Floating Limit and Unsigned Range), as CHANGE_OF_TIMER did since #4833; pTimeDelayNormal stayed at the armed value after a Time_Delay write. A Time_Delay_Normal the object has of its own is kept — revert-red: the new tests, for CHANGE_OF_STATE, CHANGE_OF_STATUS_FLAGS and OUT_OF_RANGE (three of the fifteen classes; the rest share the generated edit), saw pTimeDelayNormal stay at 0 on the pre-fix code.
  • rb dv-20261101-103 (#4808, the live sites among its grep-derived list; the other listed classes are commented-out copy code or already safe): copying or cloning a BACnetVTCloseError (a Close-Error from a VT Close) or a BACnetFileRecordAccessData (the record data of an AtomicReadFile / AtomicWriteFile record access) whose list copy fails for want of memory no longer yields an object that reports valid with its content gone; the copy is invalid and Clone() answers NULL, as BACnetReadRangeACK does since #4805 — revert-red: the new allocation-failure sweeps saw clones returned with their content lost on the pre-fix code.
  • rb dv-20261101-103 (#4815, item 5): BACnetCovNotification::SetMonitoredObjectIdentifier refuses an out-of-range identifier (an instance above 4194303 or a type above 1023) without touching the identifier it holds; the refusal erased it and made the whole notification invalid — revert-red: the new test saw the held identifier erased on the pre-fix code.
  • rb dv-20261101-104 (#4786, CR4-F18-011): a written Fault_Low_Limit or Fault_High_Limit is judged against the unchanged monitored value by a full evaluation at the next Tick (its own queue, since the first-stage pass the inhibit writes use never runs the fault algorithm); with no change of value nothing else re-judged the steady monitored value, so a value now outside the limits stayed NO_FAULT_DETECTED (and, with the limits crossed, read CONFIGURATION_ERROR with the Event_State still NORMAL) until the host next reported a changed value (cl. 13.2.2.1.1) — revert-red: the new test saw the Event_State stay NORMAL and no TO_FAULT notification at the Tick on the pre-fix code.
  • rb dv-20261101-105 (#4719, CR4-F03-041): a build with the Event Enrollment object and without the Timer object now links; the BACnetSequenceOf<BACnetTimerState> instantiation was gated on both objects while its user (the CHANGE_OF_TIMER event parameter) is gated on Event Enrollment alone, so that configuration compiled and then failed with undefined references — verified on the compiled object, not by a unit test (the unit tests build every object type): with STACK_OPTION_TARGET_EMBEDDED, Event Enrollment and IPv4 only, the pre-fix BACnetSequenceOf.o defines no BACnetSequenceOf<BACnetTimerState> member and the fixed one does.
  • rb dv-20261101-106 (#4908, CR4-F29-015): a Load Control whose clock is set back (a time change, cl. 12.28) re-evaluates as if Start_Time were written: an active shed (SHED_COMPLIANT or SHED_NON_COMPLIANT) whose Start_Time is now ahead returns to SHED_REQUEST_PENDING instead of staying compliant and shedding until the old window ends; Tick keeps the time of the previous Tick to see the clock go backwards. A clock set forward needs nothing (the window ends as it does between far-apart Ticks); the host is not asked to shed on a set-back tick that is before Start_Time — revert-red: the new test fails on the pre-fix code (first failure: the set-back tick still asked the host to shed).
  • rb dv-20261101-107 (#4794, CR4-F20-020): the Unsigned fields of the unsigned-range, unsigned-out-of-range and signed-out-of-range (deadband) notification parameters hold 64 bits, so an event notification from a peer on an Unsigned property above 4,294,967,295 (a 5 to 8 octet field) is decoded instead of refused with the whole notification; the getters of those arms return uint64_t and the setters still take uint32_t. The INTEGER fields of the signed-out-of-range arm stay 32-bit (the stack has no wider signed primitive) — revert-red: the three new tests failed on the pre-fix code.
  • rb dv-20261101-108 (#4941, CR4-F02-001): Network Port 4194303 (and What-Is-Network-Number / Network-Number-Is) resolves to the BACnet/IPv6 Network Port for a request that arrived over BACnet/IPv6 on a device with several Network Ports, as it does for BACnet/IP, MS/TP and BACnet/SC; the first-of-type scan had no IPv6 case and answered “no port” — revert-red: the new test saw the IPv6 request unresolved on the pre-fix code.
  • rb dv-20261101-108 (#4942, CR4-F02-006, follow-up to #4602): BACnetStack_SetPropertyByObjectTypeSubscribable for Present_Value on Analog Input, Analog Output, Analog Value, Integer Value, Positive Integer Value, Large Analog Value, Loop, Lighting Output, Pulse Converter or Staging also enables COV_Increment for the type, as the per-instance export does, so an object of the type that reports COV also serves COV_Increment; before, ReadProperty of it answered UNKNOWN_PROPERTY. A host that does not want it disables it at the same tier afterwards — revert-red: the new test saw COV_Increment refused for an existing and for a later Analog Input on the pre-fix code. The matching sentence in the doc comment of the export in CASBACnetStackDLL.h is not added (frozen header).
  • rb dv-20261101-108 (#4943, CR4-F02-007): an Averaging object that references a Double property (a Large Analog Value’s Present_Value) records its samples as Reals (cl. 12.5.13); every sample used to count as missed. A Double beyond the Real range, or not finite, is still a missed sample — revert-red: the new test saw Valid_Samples stay 0 on the pre-fix code.
  • rb dv-20261101-109 (#4945, CR4-F02-011, the #4874 shape): BACnetStack_SetFaultOutOfRangeAlgorithmReal on an Event Enrollment is judged against the referenced object’s type, as the same call on the object itself is (#993): an enrollment whose Object_Property_Reference names an Integer Value, Positive Integer Value, Large Analog Value or Accumulator is refused (its Present_Value is not REAL) instead of being armed with REAL limits that the monitored value could never be compared with, which left it NO_FAULT_DETECTED however far out of range the value went — revert-red: the new test saw the Integer Value and Large Analog Value enrollments armed on the pre-fix code.
  • rb dv-20261101-11 (#4753): a LifeSafetyOperation whose Requesting Source is in a character set above ISO 8859-1 (a reserved one) is now processed, the source read as an empty UTF-8 string, instead of being Rejected MISSING_REQUIRED_PARAMETER (cl. 13.13.2: an unsupported character set there is no reason to refuse the request); the operation that silences or resets the appliance is carried out. The helper that does this for AcknowledgeAlarm (#2694) is now shared, BACnetPrimitiveCharSTRING::MeasureReservedCharacterSetContextString. A truncated string or one of another class is still refused.
  • rb dv-20261101-11 (#4750): the guard that keeps an event notification with an out-of-range Date or Time from the Event Log and the Notification Forwarder (#3468) now also sees a flagged Date in the primitive arm of a change-of-discrete-value new value (the Time arm goes through the same hook but has no test of its own); before, only the date-time arm was checked, so such a notification was offered to the local objects, which cannot encode it. BACnetPrimitiveBase::IsOutOfRange() (false by default; true for a flagged Date or Time) is the hook. The change-of-reliability and extended arms are not changed.
  • rb dv-20261101-11 (#4751): a GetEventInformation reply is sized to what the encoder can write (65,535 octets, 1,074 summaries), not to the segmented budget alone; before, a device with more than 1,074 active objects and a client accepting many large segments built a page it could not encode, and the client got an Abort and could not page past it. The page now ends there with More Events TRUE and the next request carries the rest.
  • rb dv-20261101-11 (#4755, items CR4-F16-004 and -005): a GetEnrollmentSummary entry whose notification class is present but malformed is left invalid by the refusal (it stayed valid), and BACnetAcknowledgeAlarmRequest::SetTimestamp refuses an invalid timestamp without overwriting the one it holds. Items -002, -006, -007, -008 of that roll-up are not done.
  • rb dv-20261101-110 (#4948, CR4-F02-015): a write of the Network Port Command property while the port’s Out_Of_Service is TRUE is refused with PROPERTY / VALUE_OUT_OF_RANGE unless the command is RESTART_PORT, DISCONNECT or DISCARD_CHANGES (cl. 12.56.8), whether or not the stack supports the command; VALIDATE_CHANGES, RESTART_DEVICE_DISCOVERY and the other commands used to run on a port taken out of service. The host serves Out_Of_Service; a host that does not leaves the port in service — revert-red: the new test saw the out-of-service writes answered as before on the pre-fix code.
  • rb dv-20261101-110 (#4949, CR4-F02-016): RESTART_DEVICE_DISCOVERY on a port that is not BACnet/SC is refused with PROPERTY / INVALID_VALUE_IN_THIS_STATE while a change is pending (Changes_Pending TRUE, cl. 12.56.16), as the BACnet/SC, subordinate-proxy and RESTART_PORT arms refuse it; it used to forget the port’s Device_Address_Proxy_Table entries and answer Result(+) — revert-red: the new test saw the restart executed with a change pending on the pre-fix code.
  • rb dv-20261101-111 (#4952, CR4-F02-022, the #3736 shape): a WriteProperty of an alarm-and-event property (Event_Enable, Notification_Class, Notify_Type, Event_Detection_Enable and the others the alarm-and-event record serves) on a Trend Log, Trend Log Multiple, Event Log or Network Port that has such a record reaches the record, as the read already does; the object’s own routine answered “not mine” (false, no error) and the write ended there, so the written value was not the one read back or used, and the Notification_Class existence check was skipped. An object with no record keeps its answer — revert-red: the three new tests (Trend Log, Trend Log Multiple, Event Log) failed on the pre-fix code. The Network Port arm has the same change but no test of its own.
  • rb dv-20261101-112 (#4955, CR4-F02-030): a build with STACK_OPTION_ALARMS_AND_EVENTS (directly, or through the B-AAC, B-BC and B-LSC device profiles) or with the test-tool Audit Reporter, and without the Network Port object type, compiles again; BACnetDBDevice::GetNetworkPortInstances was declared and defined only with the Network Port object type while the message generator calls it in both of those builds, so BACnetStackMessageGenerator.cpp stopped at “no member named GetNetworkPortInstances”. It returns an empty list without Network Port support — verified by compiling every source/*.cpp with the embedded target, IPv4 and STACK_OPTION_ALARMS_AND_EVENTS: the pre-fix tree fails at BACnetStackMessageGenerator.cpp:1254, the fixed one compiles all 653 files. _ci_scripts/check-embedded-compile.sh now makes that second pass after its first, so the next break of this kind is found by the gate.
  • rb dv-20261101-113 (#4957 F02 roll-up, CR4-F02-020): a write of BACnet_IP_Multicast_Address on a Network Port is refused with PROPERTY / VALUE_OUT_OF_RANGE unless it is X’00000000’ (multicast off) or an IPv4 multicast group (first octet 224 to 239), as the IPv6 port already refuses a non-multicast address; any four octets used to be accepted — revert-red: the new test saw 10.0.0.1 accepted on the pre-fix code.
  • rb dv-20261101-113 (CR4-F02-023): a BACnet/IPv6 BBMD_Broadcast_Distribution_Table write with more entries than the data link holds is answered RESOURCES / NO_SPACE_TO_WRITE_PROPERTY, not the CreateObject code NO_SPACE_FOR_OBJECT — revert-red: the changed IPv6 BBMD test saw NO_SPACE_FOR_OBJECT on the pre-fix code.
  • rb dv-20261101-113 (CR4-F02-033): BACnetStack_SetTrendLogStartStopTime checks the start and the stop time before it applies either, so a refused Stop_Time leaves the Start_Time as it was (it used to move Start_Time and then return false); the log line for a refused stop time names the stop time — revert-red: the new test saw Start_Time moved by the refused call on the pre-fix code.
  • rb dv-20261101-113 (CR4-F02-037): decoding an object specifier with a context tag that is neither of its two choices answers Reject INVALID_TAG, as the sibling CHOICE and the shared decode path do, not Reject OTHER — revert-red: the new test saw OTHER on the pre-fix code.
  • rb dv-20261101-114 (#4960 F04 roll-up, CR4-F04-003): decoding a Real or Double from text (BACnetAbstractSyntaxType::DecodeString, used by the text interface) reads the text the stack itself writes, nan, inf and -inf included; the Real and Double arms used the generic parser, which refuses those three. ChipkinConvert::TryParseDouble is new (TryParseReal over a double); the rules of the refused forms (leading whitespace, trailing text, hex-float text, a finite value too large for the type) are unchanged — revert-red: the new DecodeString test failed on the pre-fix code. TryParseDouble is new, so its own test has no pre-fix to fail on.
  • rb dv-20261101-115 (#4957 F02 roll-up, CR4-F02-012): AddEventEnrollmentObjectWithOutOfRangeAlgorithm refuses a NaN limit and a non-finite deadband before it touches the enrollment, as the intrinsic OUT_OF_RANGE setter does; a refused re-add of an existing enrollment used to have replaced its algorithm and its monitored reference first — revert-red: the new test saw the reference moved by the refused call on the pre-fix code.
  • rb dv-20261101-115 (CR4-F02-017): a Channel whose host refuses to serve Present_Value answers as the Status_Flags read does: ABORT_OTHER aborts, and an error code the stack can send goes out under its own error class (UNKNOWN_OBJECT under OBJECT) instead of every code under PROPERTY — revert-red: the two edited Channel read tests and the new abort case failed on the pre-fix code.
  • rb dv-20261101-115 (CR4-F02-024): a zero write to a Binary Lighting Output’s Elapsed_Active_Time or Strike_Count stamps Time_Of_Active_Time_Reset / Time_Of_Strike_Count_Reset with the host clock when one is registered; it stamped the previous Loop tick’s time (1970 before the first tick) — revert-red: the new test saw 21:00 for 21:30.
  • rb dv-20261101-115 (CR4-F02-039): BACnetStackStoredDBPropertyValues and BACnetStackStoredDBObjectPropertyValues, which own raw pointers, are no longer copyable and have move operations that hand the values over and empty the source; BACnetDBDevice’s defaulted move used to fall back to a shallow copy of those members and would have freed the values twice — the test is compile-time (static_asserts, red against the old header) plus a runtime check that a move empties the source.
  • rb dv-20261101-116 (#4939 F01 roll-up, CR4-F01-005): a WriteProperty of Object_Name whose UTF-8 text holds a control character (U+0000 to U+001F, U+007F, or U+0080 to U+009F) is refused with PROPERTY / VALUE_OUT_OF_RANGE, as the empty name is; cl. 12.1.1.2 allows printable characters only, and only the empty string was refused. Other character sets are not inspected, and CreateObject’s initial Object_Name does not go through this check — revert-red: the new test saw the five control-character names accepted on the pre-fix code.
  • rb dv-20261101-116 (CR4-F01-009): a host-named REFERENCED_PORT_IN_ERROR is answered with error class OBJECT, and COMMUNICATION_DISABLED with SERVICES (cl. 18.2, 18.6); both went out as PROPERTY. Two existing tests that pinned COMMUNICATION_DISABLED to PROPERTY now expect SERVICES — revert-red: the new class test failed on the pre-fix code.
  • rb dv-20261101-116 (CR4-F01-015): BACnetInterface::Tick reports work when its only message was a COV_Period notification (SendPeriodicNotifications returns whether it offered an object to the COV check) — the new test needs the new return value, so it cannot run on the pre-fix code.
  • rb dv-20261101-116 (CR4-F01-031): the unused BACnetInterface::HandleDecodeError is removed. CR4-F01-002, -020 (clause citations) and -028 (the contract of IsKnownStackHeldListProperty) are comment fixes.
  • rb dv-20261101-117 (#4939 F01 roll-up, CR4-F01-018, -022, -024, -025): comment, log-text and no-op changes only, no behaviour change. AddVirtualNetwork references virtualNetworkPortInstance in a virtual-router build without the Network Port object type (the parameter was unused there); the WhoHas senders log “WhoHasRequest” where they said “WhoIsRequest”; the BBMD entry length check says which lengths are wrong; the AtomicReadFile and confirmed-request comments name what they build; a stale ToDo and two line-number references are gone; GetValueUsingCallbacks’ outRelevantCallbackDeclined doc points at UseCallbackGetSpecificProperty’s; the #1837 C13 Tick-contract comment is above CheckForAlarmsAndEvents; GetTransmittableServiceAckLength’s doc names AuditLogQuery as a caller — verified by compiling, not by a test: nothing here changes what the stack does.
  • rb dv-20261101-118 (#4795 F20 roll-up, CR4-F20-001): when a notification’s event values are refused, the reason the arm itself set now reaches the peer (a reserved life-safety operation-expected, or an access event above 65535, is Reject PARAMETER_OUT_OF_RANGE); BACnetEventNotificationRequest::Decode used to rewrite every reason except the enumerated-domain one to INVALID_TAG. An Abort an arm sets (OUT_OF_RESOURCES, BUFFER_OVERFLOW, OTHER for an unimplemented complex event type) now reaches the peer of a confirmed notification as that Abort. Only a refusal that set no reason is INVALID_TAG — revert-red: the new request-level test saw INVALID_TAG on the pre-fix code.
  • rb dv-20261101-119 (#4872): the BTL SubscribeCOV test 9_10_1_9 no longer fails intermittently. Its time-synchronisation callback (and the same callback in 9_11_2_X12 and 9_11_2_X14) passed an uninitialised struct tm to mktime; tm_isdst was stack garbage, mktime returned -1 for a non-zero value, the test clock jumped by the whole epoch and the subscription expired, so the notification carried Time Remaining 0 instead of 100. The struct tm is zero-initialised with tm_isdst = 0. A test-only change, no change to the stack — verified by repetition, not by a failing test: alone, 0 of 40 runs fail with the fix and 18 of 40 fail with the test file as it was; the full suite then passed in 6 of 6 runs.
  • rb dv-20261101-12 (#4728): a File that device A creates with CreateObject while a restore is in progress is now listed in the device’s Configuration_Files, and the failure timer is reset by that CreateObject and by the AtomicWriteFile requests to the File that follow (cl. 19.1.3.3: those messages count when they access a configuration File). Before, the new File was not listed, so a restore of a device with no existing configuration files timed out at Backup_Failure_Timeout with the writes still in progress, and the host was told the restore was aborted. A File created during a backup is not listed.
  • rb dv-20261101-12 (#4729): a CreateObject whose List of Initial Values holds a Null for a property that is not commandable is now answered with CreateObject-Error, PROPERTY / INVALID_DATATYPE with the element number, and the object is rolled back (cl. 15.3.1.3.1); before, the entry was dropped (the shortcut for a Null write to a non-commandable property, cl. 19.2.1, applied to it), the property was not initialized and the client got Result(+). A Null written to an existing such property with WriteProperty still succeeds and changes nothing, as before.
  • rb dv-20261101-120 (#4706, CR4-F07-024): a BACnet/IPv6 peer named by a BACnetAddress is now sent to. A recipient written as the 3-octet VMAC (or the 18-octet B/IPv6 address) used to end at SetClientNetworkParameters’s “unsupported networkType and address” refusal, so an event notification to an address-form Notification Class recipient was never delivered, and no Address-Resolution was ever transmitted. SetClientNetworkParameters(const BACnetAddress&, ...) now types a 3- or 18-octet MAC as BACnet/IPv6, and a Network Port of type IPv6 names its link (so a zero-length MAC, a local broadcast, has a port on an IPv6-only device). The data link sends to a VMAC in its table at the address it holds (naming it by that address, so a confirmed request’s transaction is the one its answer finds); for one that is not, it holds the frame, multicasts an Address-Resolution (to the BBMD for a registered foreign device) and sends the frame when the Address-Resolution-ACK arrives (cl. U.5). A held frame now has a second key, the VMAC, beside the address; a retry of a VMAC-addressed frame comes back to the data link through the layer’s retry route. Not changed: a confirmed request to a VMAC not yet in the table is still keyed by the VMAC, and a BBMD’s own Address-Resolution is not forwarded to its peers (#4969). Verified by revert-red: 14 of the 16 new tests fail without the source change (the other two are controls), and the confirmed-request test alone fails with the VMAC-to-address naming switched off.
  • rb dv-20261101-121 (#4939 CR4-F01-052, -053, -054, -048, -044): the COV notification senders no longer leak a request or send a PDU with nothing in it. SendCOVNotificationUsingDataLink freed neither the confirmed nor the unconfirmed request when PrepareCOVNotificationPropertyValues failed, and all four COV builders (single and multiple, confirmed and unconfirmed) ignored SetServiceRequestAndTakeControl’s false, so an invalid request was leaked and the PDU went out with no service request. They now free the request and send nothing; for the COV-multiple builders the values the fill had already taken from the queue are dropped with it (a request invalid for its own fields is invalid on every pass), and the call returns false. The event-notification data-link table in DeliverEventNotificationToRecipientUsingDataLink gains its BACnet/IPv6 row, so a notification keeps the caller’s IPv6 port as it keeps an IPv4, MS/TP or SC one. The test-tool SendConfirmedCOVNotificationMultiple and SendUnconfirmedCOVNotificationMultiple refuse a zero per-object value count at the entry header, with their own message, instead of failing at encode. A build whose STACK_OPTION_ROUTER_TICKS_PER_SECOND times the learned-next-hop TTL or the busy timeout no longer fits 32 bits fails with a negative-size array in BACnetStackConstants.h instead of wrapping to a shorter timeout.
  • rb dv-20261101-122 (#4720 CR4-F03-017, -047): a property-array-index or priority sent as a context opening tag of its own number is now refused with Reject(INVALID_TAG). BACnetObjectPropertyValue (array index [2], priority [4]), BACnetObjectPropertyReference (array index [2]) and BACnetPropertyValue (array index [1], priority [3], both decode paths) read such a tag as an absent optional field, reported the element valid and left its octets unconsumed, which desynchronised a SEQUENCE OF around them. BACnetPrimitiveBase::DecodeWithContext is unchanged: its optional path is shared with CHOICE probing, which has to go on to a constructed arm of the same number.
  • rb dv-20261101-123 (#4909 CR4-F29-010, -011, -012, -013): BACnetVTClass refuses a value above 65535, the largest Table 23-1 allows for this extensible enumeration, on SetValue and on Decode (UNDEFINED_ENUMERATION, through SetValue, as for the other closed enumerations); before, any 32-bit value was accepted and marked valid. BACnetVTCloseError::Encode logs “Invalid BACnetVTCloseError object” (it named a BACnetPropertyReference) and its comment says “Encode”. BACnetValueSource and BACnetVTSession use descriptive names instead of n, o, a and max. BACnetValueSource gains literal-byte tests of its three CHOICE arms and of the refusal of an unknown choice tag.
  • rb dv-20261101-124 (#4909 CR4-F29-002, -003, -007, -016, -017): BACnetHealth::GetMinimumMemoryCount counts the opening and closing tags of its constructed timestamp [0] and result [1] and counts the OPTIONAL property [2] and details [3] only when they are present (it counted absent optionals and no tags); BACnetChannelValue::GetMinimumMemoryCount no longer counts an opening and closing tag for the application arm, which encodes the bare primitive. While a Load Control object is SHED_INACTIVE, a Requested_Shed_Level written with another choice now re-derives Expected_Shed_Level and Actual_Shed_Level from that choice’s default (cl. 12.28.17); they kept the old choice’s default. Comment-only: the Load Control snap and EnterPending comments, FPCallbackLoadControlShed’s Duty_Window clause (12.28.13, not 12.28.15), the BACnetHealth default-timestamp comment in the SC port, and a quoted paraphrase in the Prescale test.
  • rb dv-20261101-125 (#4909 CR4-F29-008, #4906 CR4-F28-001): BACnetAction, BACnetPolarity, BACnetProgramRequest, BACnetProgramState, BACnetTimerState and BACnetTimerTransition are closed ENUMERATEDs (no extension marker), and now refuse a value above their last enumerator on SetValue; Decode stores through SetValue, so a value outside the type from the wire is refused too with UNDEFINED_ENUMERATION. Before, any 32-bit value was accepted and the object marked valid, so a change-of-timer Event_Parameters decode or insert, a Program object value or a Loop action carried it unchecked. BACnetBinaryPV (#4720 CR4-F03-018) is not in this change: existing fixtures use binary-value states 2 and 3 as stand-ins. A malformed lastStateChange in a change-of-timer notification now answers UNDEFINED_ENUMERATION where it answered INVALID_TAG, and a new-state or last-state-change above the enumeration, which the arm answered PARAMETER_OUT_OF_RANGE, answers UNDEFINED_ENUMERATION; the arm’s own range checks stay as backstops. A host that hands BACnetChangeOfTimerEventAlgorithm::SetChangeOfTimerParameters an alarm value above 2 now gets an Event_Parameters read that fails where it used to serve the bad value.
  • rb dv-20261101-126 (#4909 CR4-F29-001, -004, -014, -018, -020): comment-only fixes and one test. BACnetEngineeringUnits.h’s ASN.1 comment names the seven units that 135-2024 renamed (volts-per-kelvin, volt-ampere-hours-reactive, kilovolt-ampere-hours-reactive, megavolt-ampere-hours-reactive, watts-per-meter-per-kelvin, watts-per-square-meter-per-kelvin, minutes-per-kelvin), as the enumerators already do. The Group member docs (BACnetStackGroup.h, BACnetStack_AddGroupMember and BACnetStack_RemoveGroupMember in CASBACnetStackDLL.h) say that RemoveListElement compares the complete element while the host API BACnetStack_RemoveGroupMember matches by object identifier on purpose, and that a reference to the Present_Value, ALL or REQUIRED of a Group or Global Group member is refused (BACnetStack_AddGroupMember returns false). The Program settle docs name BACnetStackTestTool_SetProgramState (the callback doc named an export that does not exist) and add its Description_Of_Halt refusal to \return. The BACnetStackTestTool_AddAveragingObject doc says a remote reference’s scheduled samples are recorded as missed samples. A new test pins BACnetActionCommand::Decode’s refusal of a device-identifier that is not a Device object.
  • rb dv-20261101-127 (#4897 CR4-F24-003, -006, -007, -011, -015): BACnetCredentialAuthenticationFactor::SetAuthenticationFactor refuses an invalid factor before it replaces the stored one, leaving the stored factor and the object’s validity as they were (it overwrote first). An Access Zone’s Adjust_Value with a positive value saturates Occupancy_Count at 4294967295, as an entry does; it wrapped modulo 2^32. A Credential Data Input whose Format-Type is UNDEFINED serves Format-Class 0 once back in service, whatever class the value carried while Out_Of_Service was TRUE (12.36.4). The Credential Data Input’s Update_Time sequence number steps from 65535 to 1, so it never returns to 0, the value for “never updated” (12.36.11). The Access Point’s disabled-factor mapping comment says it was checked against Table 12-42 (it matches).
  • rb dv-20261101-128 (#4897 CR4-F24-004, -014): BACnetStackAccessUser::SetProperty declines a property it does not hold without writing an error (the caller then falls through to the generic path, as the header says); it parsed the value first and answered a value of the wrong type with an error. The reserved-range sentence of clause 21 in the comments of BACnetAccessCredentialDisable, BACnetAccessCredentialDisableReason, BACnetAccessUserType and BACnetDoorAlarmState is paraphrased, as in their sibling headers.
  • rb dv-20261101-129 (#4889 CR4-F23-007): BACnetAuthenticationClient (authenticated, device), BACnetAuthenticationPeer (device, auth-aware, router, hub) and BACnetAuthorizationConstraint (origin, authentication) refuse a context tag where cl. 21.6 gives an application-tagged field of an untagged SEQUENCE, with Reject(INVALID_TAG). The primitives’ Decode checks the tag number of an application tag only and took a context tag of any number, so 09 01 21 05 decoded as an AuthenticationClient {authenticated TRUE, device 5} and 19 02 29 01 as a constraint {origin any-network, authentication secure-path}. A shared BACnetPrimitiveBase::RefuseContextTaggedField does the check. BACnetAuthorizationPolicy’s clients list and BACnetAccessToken’s audience, which go through the shared BACnetSequenceOf, are not in this change.
  • rb dv-20261101-13 (#4725): a confirmed WriteProperty whose value starts with a tag that does not decode (an extended tag number with its octet missing, say) is now Rejected with INVALID_TAG; before, the request decode returned Error(PROPERTY, DATATYPE_NOT_SUPPORTED), which the confirmed-request decode does not carry, so the requester got no reply at all. (#3800 had fixed only a request that ends at the opening tag.) The same wording remains in the WritePropertyMultiple element decode (BACnetPropertyValue), which is not changed here.
  • rb dv-20261101-13 (#4726): a write from a device on this device’s own BACnet/IPv6 link is now recorded in Value_Source and Value_Source_Array as network 0 and the writer’s 3-octet VMAC (cl. U.1.3); before, every such write left them none and the writer could not later write its own Value_Source slot, because the fallback that records a local-link writer by its connection string accepted at most 6 octets and a B/IPv6 one is 18. A writer with no known VMAC still records none. The audit-source fallback in BACnetDBDevice has the same 6-octet limit and is not changed here.
  • rb dv-20261101-13 (#4727, items CR4-F11-002, -008, -010): a WriteProperty or WritePropertyMultiple request with no service parameters at all is now Rejected MISSING_REQUIRED_PARAMETER (it was OTHER; a NULL buffer stays OTHER), and a refused BACnetPriorityFilter buffer leaves the filter invalid rather than keeping the one an earlier decode produced. The other items of that roll-up are not done.
  • rb dv-20261101-130 (#4889 CR4-F23-002, -003, -005, -013, -014, -019): BACnetAuthorizationStatus::operator= guards against self-assignment (its list copy empties the destination first, so status = status emptied all four event lists). BACnetAuditLogRecord::SetAuditNotification refuses an invalid notification and leaves the record as it was (it marked the record valid with the notification invalid), and SetTimestamp re-derives validity from the notification as well. BACnetConfirmedAuditNotificationRequest::Clone and BACnetUnconfirmedAuditNotificationRequest::Clone return NULL when the notification list copy fails (they returned a valid request that had lost its notifications). Comment-only: the clause 21 reserved-range sentence is paraphrased in BACnetAuditOperation.h (its duplicate in-class copy dropped) and BACnetAuthorizationExemption.h; BACnetAuditNotification.h says the optional-field flags gate encoding, rendering and comparing and that Reset() clears the members and the flags.
  • rb dv-20261101-131 (#4906 CR4-F28-002, -003, -004, -007): the test-tool BACnetTimeValue::CreateFromCompactString refuses a time part above 255 ("257:00:00.00" scheduled at 01:00 through an unchecked uint8_t cast), a value or datatype field that is not wholly a number, and an Unsigned, Enumerated or Signed value outside its 32-bit range; its locals now have descriptive names. A Schedule_Default write holding one value that is not a primitive is refused with PROPERTY / INVALID_DATATYPE and leaves the held value alone, as the Present_Value arm does; it was refused with no error, so the write fell through to the host’s write path. A Timer’s Priority_For_Writing starts at 16, the lowest priority and the Schedule’s default (it started at 1, Manual-Life Safety), and BACnetStackTestTool_AddTimerObject’s doc says so. Tests now reach BACnetTimeValue::Decode’s refusal of a context-tagged or constructed value and BACnetTimerStateChangeValue::Decode’s refusal of an unknown context tag.
  • rb dv-20261101-132 (#4906 CR4-F28-005, -006, -008): a Schedule’s Effective_Period, List_Of_Object_Property_References and Priority_For_Writing, which the engine serves on read, now refuse a write with PROPERTY / WRITE_ACCESS_DENIED even when the host marks them writable (the write routing sent them to the host’s callbacks), as the Timer does since #2827. BACnetStackSchedule::GetProperty answers an out-of-memory exit on any arm (Weekly_Schedule size, a day or the whole array, Present_Value, Schedule_Default, Effective_Period, Priority_For_Writing) with Abort(other) through a shared InsertOrAbort, as the Timer does; it returned a bare false, which reads as “this engine does not hold the property” and let the read fall through to the host callbacks after Weekly_Schedule was partly filled. The unused BACnetTimeRangeSpecifier class and its test are deleted (no source file used it, it is not a 135-2024 production; BACnetAccessRule keeps its own enumeration).
  • rb dv-20261101-133 (#4902 CR4-F27-003, -004; #4891 CR4-F25-003, -005; #4899 CR4-F26-003, -004): BACnetStackLightingOutput::SetRelinquishDefault re-evaluates Last_On_Value and Tracking_Value as well as storing the value (with every slot Null the Relinquish_Default is the Present_Value, and neither followed it; a fade or ramp in progress keeps Tracking_Value), as the Binary Lighting Output sibling does. The Life Safety Point/Zone Silenced reset (#2808) fires when no Alarm_Values, Life_Safety_Alarm_Values or Fault_Values are configured, treating any change to a value other than quiet as a new indication; with no lists nothing was classified as non-NORMAL, so a SILENCE applied once silenced every later indication. Comment-only: the quoted clause fragments in BACnetStackBinaryLightingOutput.h, BACnetLightingCommand.h, BACnetStackLifeSafetyObject.h and BACnetStackLifeSafetyZone.h are paraphrased; InsertLandingCall and InsertLandingDoor say that ownership stays with the caller on a refusal. The commented-out copy constructor and operator= blocks of BACnetAssignedLandingCalls, BACnetLandingDoorStatus and BACnetLiftCarCallList, and the two commented-out AssignmentConstructor tests, are deleted.
  • rb dv-20261101-134 (#4797 CR4-F21-006, -010, -012): an Alert Enrollment whose Present_Value names this device follows a re-key of the Device object (You-Are, BACnetStack_UpdateDeviceInstance, a WriteProperty of Object_Identifier) to the new instance, as every other device reference does since #4050/#4052/#4095; it kept naming the old one. BACnetDestination::Reset() seeds the Recipient with the zero-length local broadcast of cl. 21, not a 6-octet IPv4 MAC (FF FF FF FF BA C0). Comment-only: BACnetStackNotificationClass’s RemoveProperty cites cl. 15.2.2 for atomicity (it cited 15.2.1.3.1, the Error type clause) and says the clause does not address an empty request list, which returns Result(+).
  • rb dv-20261101-135 (#4781 CR4-F17-009, -017): BACnetChangeOfCharacterstringEventAlgorithm::BuildEventParameters leaves a Null alarm value out of the Event_Parameters it reports for an Event Enrollment; it was read back as an empty string, which is a different alarm value (an empty string is a substring of every monitored string). BACnetChangeOfStatusFlagsEventAlgorithm no longer tracks or validates a pStatusFlags that cl. 13.3.11 does not have (m_statusFlags and SetStatusFlags removed; a malformed Status_Flags passed alongside the monitored value used to fail the run).
  • rb dv-20261101-136 (#4795 CR4-F20-007): test-only. The out-of-range date and time gate (HasOutOfRangeDateTime) is now pinned for the Time primitive and date-time arms of a change-of-discrete-value new value and for an access-event-time, each through the request decode and the Event Log offer; only the Date primitive arm was pinned before.
  • rb dv-20261101-137 (#4795 CR4-F20-022): test-only. The six range and limit notification arms (floating limit, out of range, unsigned range, double, signed and unsigned out of range) now have a field-level test: changing any one field makes IsEqual false, Clone() equals the original at another address, GetType() is pinned to the choice tag and GetDatatype() differs between arms.
  • rb dv-20261101-138 (#4781 CR4-F17-008): CHANGE_OF_CHARACTERSTRING condition (c) no longer depends on the order of the alarm list. While OFFNORMAL the alarm value behind the committed OFFNORMAL is tried first, so a monitored string that matches two alarm values re-notifies only when the committed one stops matching; before, the first match in list order was taken and a list order that put the other value first re-notified for it.
  • rb dv-20261101-139 (#4781 CR4-F17-018): test-only. COMMAND_FAILURE’s pTimeDelayNormal path, the deadline of both delays (the transition happens at the deadline, not a second later) and an abandoned delay (a flap starts a fresh delay) now have tests.
  • rb dv-20261101-14 (#4722, security): with MAX_RPM_PROPERTIES_IN_REQUEST set, a ReadPropertyMultiple request could stay under the cap by listing expensive references first: the byte estimate saturated at 65,535 after about ten whole-array reads and the walk stopped there, so the count compared with the cap was cut short; since #3687 a device that does not segment, or has no cap on its segments, no longer aborts on the estimate before that check. The walk now goes on after the estimate saturates, when the count is asked for, so the count is exact. A caller that does not ask for the count still stops at once.
  • rb dv-20261101-14 (#4740, item CR4-F14-006): an Encode of the file-access CHOICE into a buffer that is too short now aborts (tested for stream access; the record arm has the same change) with SEGMENTATION_NOT_SUPPORTED, the reason the shared encoder sets, instead of Abort OTHER. Items -003, -004, -005, -007 and -008 of that roll-up are not done.
  • rb dv-20261101-140 (#4781 CR4-F17-015): an EXTENDED event-algorithm indication made while Event_Algorithm_Inhibit is TRUE, Event_Detection_Enable is FALSE, or a first-stage reliability fault is held is now spent, as one made while the object is in FAULT already was (#2712). It was kept and fired as a transition from NORMAL once the condition ended. EXTENDED arming is test-tool-only.
  • rb dv-20261101-141 (#4781 CR4-F17-024): a fault that the fault algorithm held silently while Event_Detection_Enable was FALSE, and that clears on the first healthy sample after detection is re-enabled, no longer sends a TO_NORMAL “from state FAULT” out of an object whose Event_State is NORMAL. The clearing is not a transition out of FAULT when Event_State was never FAULT, so nothing is reported.
  • rb dv-20261101-142 (#4747 CR4-F15-008, -011, -017): BACnetCovValue::Clone no longer fails on the out-of-range Time of Change that a client decode keeps (#3454); it copies the Time of Change by assignment, octets and presence as they are. A queued COV-multiple change goes out at its Max Notification Delay, not one second after it (<= for <). The rendered name of BACnetClientCOV is “BACnetClientCOV”, not “BACnetCov”.
  • rb dv-20261101-143 (#4736 CR4-F13-010, -018): a You-Are that reaches a BACnet/IPv6 port ignores its Device MAC Address (cl. 16.11.3.1.5: a VMAC-addressed device ignores it), so the Device Identifier it carries is applied instead of the whole request being refused for a MAC that “does not suit the port”; a MAC-only request has nothing left to act on. An undefined ReinitializeDevice state (decode and processor) and an undefined text-message priority are rejected UNDEFINED_ENUMERATION, not PARAMETER_OUT_OF_RANGE.
  • rb dv-20261101-144 (#4736 CR4-F13-011, and the You-Are half of -012): the Who-Am-I an unconfigured device sends carries Model_Name and Serial_Number in the character set the device holds them in, not labelled UTF-8 whatever the set (BACnetYouAreProcessor::GetModelName and GetSerialNumber gained an optional encoding out-parameter). The You-Are identity match (Model_Name, Serial_Number) compares characters in the set each side declares, not octets, so the same name held in UCS-2 and sent in UTF-8 matches; the same octets in the same set still match. The Who-Has Object_Name half of -012 is not changed.
  • rb dv-20261101-145 (#4747 CR4-F15-018): a SubscribeCOVPropertyMultiple renewal that shortens the Max Notification Delay shortens the wait of the timestamped changes already queued: the armed deadline moves to the earliest queued Time of Change plus the new delay. It stayed at the old, longer delay. A renewal that lengthens the delay leaves it as it was.
  • rb dv-20261101-146 (#4747 CR4-F15-020): cancelling one reference of a SubscribeCOVPropertyMultiple subscription now drops the changes of that property that are still queued for its timestamped notification; they used to go out at the deadline for a property the client had unsubscribed. The other references’ queued changes are untouched and keep their order.
  • rb dv-20261101-147 (#4736 CR4-F13-026): a clock step (TimeSynchronization or UTCTimeSynchronization) moves the backup / restore failure timer’s deadline with the clock, as it already moved the COV expiry; the deadline is an absolute clock time and kept its old value, so a forward step expired a procedure early and a backward step held it for the step plus the timeout. A timer that is not running (0) is left alone, and a step back larger than the time left leaves it armed (1) rather than turning it into “not running”.
  • rb dv-20261101-148 (#4755 CR4-F16-019, -022): BACnetStackMessageGenerator::DeliverEventNotificationToRecipient reports a notification it never sent as dropped, not sent: a missing device, data link layer, recipient or parameters, and a recipient that names no Device (the drain counted them as delivered). A device with a Notification Forwarder no longer reads the UTC offset through the host on every Loop tick: the local-hop hand-off reads it, through the tick’s lazy reader, only for a hop that exists. The capacity-drop half of -019 is not changed.
  • rb dv-20261101-15 (#4716): a segment of a segmented ComplexACK is now sent with data_expecting_reply TRUE (cl. 5.4.5.3 and 5.4.5.4: the next segment waits on the Segment-ACK); before, the bit was clear because only a confirmed request set it. The bit on the wire is tested (BACnet/IPv6 data link, with an unsegmented ComplexACK as the control). What follows on MS/TP comes from the engine’s existing handling of that bit and is not exercised by a test here: a request answered with a segmented ComplexACK is answered with a Reply Postponed (cl. 9.8) and the segments go out when the node holds the token, where segment 0 used to go out at once as a Data Not Expecting Reply frame without it. A router delivering a segment onto MS/TP copies the value it now carries (cl. 9.7.2).
  • rb dv-20261101-16 (#4761): the Source Device of an audit notification for an operation requested from this device’s own BACnet/IPv6 link is now the requester’s 3-octet VMAC (network 0), or its device identifier when a local I-Am bound it (the binding holds the VMAC since #4707), and the Invoke Id is included; before, the 18-octet connection string was over the 6 octets the local-link fallback accepted, so the requester was reported as this device. A request with no known VMAC is still taken for this device’s own, as before. Same cause as #4726.
  • rb dv-20261101-16 (#4762): a WritePropertyMultiple value that starts with a tag that does not decode (and the other services that decode values through BACnetPropertyValue) is now Rejected with INVALID_TAG; before, it produced Error(PROPERTY, DATATYPE_NOT_SUPPORTED), which the confirmed-request decode does not carry, so the requester got no reply. Same fix as #4725 for WriteProperty.
  • rb dv-20261101-16 (#4739, partly): the per-source rate limiter behind the Reject-Message-To-Network now takes the 18-octet key of a BACnet/IPv6 sender (the address and port) and counts the 16-octet address as the sender’s identity, so one host varying its UDP port shares one burst of 4, as on BACnet/IP; before, it refused every such key, so a reject that is keyed by the connection string (the network layer’s reserved-type and vendor-message rejects, the data link’s, a virtual router or gateway’s) was never sent to such a sender. A key longer than 18 octets is still refused. Not changed: the physical router’s own rejects take their peer from ResolveIngressPeerAddress, which still keeps at most 6 octets for the held-message copy; the physical router has no BACnet/IPv6 port, so this does not arise there today. The virtual-router and gateway bound is widened too and has no test of its own.
  • rb dv-20261101-17 (#4752): a Notification Forwarder no longer forwards a notification it received as a local broadcast to a recipient named by Device identifier whose DAB binding is on the network the broadcast arrived on (the binding’s network number equals the ingress port’s, or the binding is directly attached through that same port); loop-prevention rule (d) judged only address-form recipients, so such a device was sent a duplicate of a notification it had already heard. A device bound on another network or another port, or not yet bound, is unaffected. Like the address-form rule, it applies only when the ingress port has a network number (not 0 or 65535), and a device hosted on this stack’s own virtual router is not in the DAB and is not covered.
  • rb dv-20261101-18 (#4743): an object whose COV_Period property is enabled and non-zero (a Pulse Converter, for one) now sends each SubscribeCOV / SubscribeCOVProperty subscriber a COV notification every COV_Period seconds even when nothing changed (cl. 13.1, 12.23.19); before, no code read the property, so a quiet object looked dead to a client relying on the heartbeat. The stack looks at most once per clock second, reads COV_Period through the host’s unsigned-integer property callback (so the host must serve it for the object), and starts the period at the first look after the subscription or a renewal; it restarts at every notification, whatever caused it, and when the clock is set back. A zero period sends none. A due object’s live values are read then, so a change the host did not report is noticed at that point. Only Present_Value subscriptions are periodic; SubscribeCOVPropertyMultiple subscriptions are not yet.
  • rb dv-20261101-19 (#4724, items CR4-F10-005, -010, -011, -014): decode failures that reached the caller with no Reject or Abort set now set one. A ReadPropertyMultiple List of Results with a wrong opening tag, a Property Access Result with no access-result CHOICE, and a Read Result cut off before its value (or Property Access Error) now give Reject INVALID_TAG, INVALID_TAG and MISSING_REQUIRED_PARAMETER (INVALID_TAG for a malformed tag), and an allocation failure in those decoders or in the ReadProperty ACK’s gives Abort OUT_OF_RESOURCES. A ReadRange request with no service parameters, or cut off inside its Range, is now Rejected MISSING_REQUIRED_PARAMETER rather than OTHER. Reusing a ReadRange request or ACK without a Property Array Index no longer keeps the index of its previous use.
  • rb dv-20261101-19 (#4724, items CR4-F10-008, -009, -013, -016, -017; no behaviour change): standard text removed from two source comments, five wrong comments about Reset() and validity dropped, the Audit Log named in the ReadRange processor’s comment, and BACnetReadRangeACK’s sequence enum renamed BACnetReadRangeACKSequenceEnum and made public like its siblings. Items -003, -004, -006, -012 and -015 of that roll-up are not done.
  • rb dv-20261101-20 (#4724, items CR4-F10-003, -004, -006):
    • A ReadPropertyMultiple whole-array read of (Device, 4194303) or (Network Port, 4194303) is now priced for the segmented-transmit bound as the object it is substituted with: this device, with its existence and property checks, for the wildcard Device, and as an existing port for the wildcard Network Port (the estimate cannot know which port Process() will use, so it can overcount, never undercount). It was priced as a missing object, so a request the bound refuses at the real instance passed at the wildcard.
    • ReadPropertyMultiple now answers with the abort or reject a property read raised (Abort OUT_OF_RESOURCES for a failed copy of a stored value, as ReadProperty does) instead of always Abort OTHER, and the out-of-memory branch for the empty ReadAccessResult answers OUT_OF_RESOURCES too.
    • Copying a Property Access Result (Global Group Present_Value elements) whose value could not be copied no longer reports valid, and Clone() returns NULL for it.
    • Items -012 and -015 of the roll-up are not done.
  • rb dv-20261101-21 (#4730, items CR4-F12-003, -004): a CreateObject request cut off right after the opening [1] of its List of Initial Values is now Rejected INVALID_TAG (it was OTHER, as the list decoder refuses an empty buffer), like every other cut-off list. When the CreateObject-ACK’s own copy of the service ACK fails (out of memory) the PDU is not valid; it is now treated like a PDU that could not be allocated, so the object is rolled back and the request is answered Abort OUT_OF_RESOURCES, instead of an invalid PDU being returned with the object left created.
  • rb dv-20261101-21 (#4730, items CR4-F12-006, -007; no behaviour change): the CreateObject processor header’s two spec phrases are paraphrased, and four copied comments and log strings (the object-instance error class, the error-PDU service name, add-versus-remove, and a property name in a log line) say what the code does. Items -005 and -008 of that roll-up are not done.
  • rb dv-20261101-22 (#4727, items CR4-F11-009, -012, -013, -004):
    • A Priority Filter (GetEnrollmentSummary) whose closing tag is wrong is now Rejected INVALID_TAG and holds no values; it returned 0 with no error and a filter still reporting valid, which the request-level decoder read as an absent field and went on with an unset Priority Filter.
    • A WriteGroup with an empty Change List is now Rejected MISSING_REQUIRED_PARAMETER (the check written for it never ran; the empty list returned 0 with no reason).
    • A Group Channel Value cut off after its channel, or failing to allocate, now sets its error (Reject INVALID_TAG, Abort OUT_OF_RESOURCES) as the function’s contract says.
    • A WriteProperty request whose value copy failed (out of memory) is no longer valid, its Clone() returns NULL, and SetPropertyValue returns false, instead of reporting a valid request holding an empty value.
    • Not done within these items: a WriteProperty request given an empty (never valid) value still reports valid (only the failed copy of a valid value is fixed), and an optional Priority Filter whose opening tag cannot be decoded is still read as absent. Items -005, -006, -007, -011, -014, -015 and -016 of that roll-up are not done.
  • rb dv-20261101-23 (#4755, items CR4-F16-008, -012, -018):
    • A Notification Forwarder now forwards a present zero-length Message Text as it arrived; the parameters carried no presence flag, so a notification with an empty [7] was forwarded with the parameter removed. The rebuilt parameters gain a useMessageText flag (set by the forwarder; a locally raised notification with no text still omits it).
    • A GetEnrollmentSummary request with no service parameters is now Rejected MISSING_REQUIRED_PARAMETER (the Acknowledgment Filter is its one mandatory argument) instead of OTHER, and a request object reused for it no longer keeps its previous contents.
    • A disabled Port_Filter entry for Port_ID 0 now applies on a device that is not a router. A non-router stamps no ingress Port ID, and its Port_ID is 0; the filter ignored the entry. A missing entry still lets the notification through. It applies only to a notification received without an ingress Port ID (a router never has a port-0 entry of its own).
    • Item -002 (a wire Time Remaining of 0, which needs an owner ruling) and the other items of that roll-up are not done.
  • rb dv-20261101-24 (#4805): copying a valid ReadRange ACK whose item data could not be copied (out of memory) no longer reports the copy valid with the items gone, and Clone() returns NULL for it; the copy was reported valid and encoded without the items it was meant to carry.
  • rb dv-20261101-25 (#4747, items CR4-F15-002, -003, -007, -010):
    • Encoding a COV reference (Active_COV_Multiple_Subscriptions, SubscribeCOVPropertyMultiple) into a buffer too small for a field now keeps the field’s out-of-space abort (the one that drives the grow-and-retry) instead of replacing it with Abort OTHER, so a value that would fit after one grow no longer fails outright.
    • A COV Subscription decoded into a reused object, given an empty or NULL buffer, no longer keeps the previous decode’s fields and validity.
    • Copying a COV-multiple subscription whose list of specifications could not be copied (out of memory) no longer reports the copy valid with the specifications gone, and its Clone() returns NULL for it.
    • A client COV increment whose tag cannot be decoded now fails with Reject INVALID_TAG set, like its other failures.
    • Items -004, -005, -006, -008, -009, -011, -012, -017 to -024 and -026 to -028 of that roll-up are not done.
  • rb dv-20261101-26 (#4810): copying a valid SubscribeCOVPropertyMultiple request whose list of specifications could not be copied (out of memory) no longer reports the copy valid with the specifications gone, and Clone() returns NULL for it. (The other half of that issue, the COV notification Decode reset order, was not a defect: the base classes are never instantiated directly and are reached only through subclasses that already reset first.)
  • rb dv-20261101-26 (correction to #4747 item CR4-F15-007, same change as #4810): the copy of a COV-multiple subscription or SubscribeCOVPropertyMultiple request is judged invalid only when the source’s list was valid and the copy’s came back invalid. The first form of the #4747 fix judged the copy by its list alone, which refused to clone a valid subscription holding a specification with no references (that specification reports invalid and folds into the list), returning NULL with nothing failing.
  • rb dv-20261101-27 (#4720, items CR4-F03-007, -015, -026, -035):
    • A refused device instance (above 4194303) in a Device Object Reference, a Device Object Property Reference or a Device Object Property Value no longer erases the device identifier the reference already named; it was reset, so the reference read as the local device while staying valid. The instance is checked before the field is touched.
    • Copying an Object Property Value whose property value could not be copied (a child clone failed, out of memory) no longer reports the copy valid with an empty value, and Clone() returns NULL for it. The copy is judged invalid only when the source’s value was valid and the copy’s is not.
    • A CharacterString or OctetString whose decode was refused (a reserved character set, a truncated value, the wrong tag) now refuses to encode, as every sibling primitive does; it wrote the tag’s declared length (or its tag octet) with no payload and returned a size.
    • Items -001 to -006, -008 to -014, -016 to -025 and -027 onward of that roll-up are not done.
  • rb dv-20261101-28 (#4813): a refused device instance (above 4194303) no longer erases the device identifier of an Action Command or an Address Binding; the identifier was reset, so the object read as the local device while staying valid. The instance is checked before the field is touched, as for the Device Object references in #4720. BACnetPrimitiveObjectIdentifier::SetValue itself still resets on a refusal, and other callers are not changed.
  • rb dv-20261101-29 (#4815, items 1 and 2): a refused identifier (an instance above 4194303, or an object type above 1023) no longer erases the identifier a request already held: the optional device identifier of a Property Access Result, the initiating device and monitored object identifiers of a COV notification, the initiating device identifier of a COV-multiple notification, and the monitored object identifier of SubscribeCOV and SubscribeCOVProperty requests. Each reset the field on the refusal while the object stayed valid. The check is made before the field is touched, as for the Device Object references in #4720 and the Action Command and Address Binding in #4813. Items 3 to 6 of #4815 (event notification, acknowledge alarm, I-Am, I-Have, COV notification value, audit log query setters) are not done.
  • rb dv-20261101-3 (#4699): Access_Doors, Negative_Access_Rules, Positive_Access_Rules, Authentication_Factors and Assigned_Access_Rights of a host-managed object read as empty arrays when the host does not serve them, not UNKNOWN_PROPERTY: index 0 is 0 and index 1 is INVALID_ARRAY_INDEX. A host that answers the size is still served. An empty answer can misreport a host with real doors or rules until the typed callbacks arrive (#2150).
  • rb dv-20261101-3 (#4509 CR4-F07-021): BACnet_IP_Global_Address (407) can be read and written (cl. 12.56.37). It is opt-in, through BACnetStack_SetPropertyEnabled, and uses the FD_BBMD_Address callbacks with its own property identifier, reading ‘none’ with port 0 when the host does not serve it. A write sets Changes_Pending and reaches the host on ACTIVATE_CHANGES or WARMSTART, all or nothing with the other pending port values. A host that works the address out itself can make it read-only with BACnetStack_SetPropertyWritable.
  • rb dv-20261101-30 (#4815, items 3 and 6): a refused identifier (an instance above 4194303, or an object type above 1023) no longer erases the identifier a request already held in the event notification (initiating device, event object), acknowledge alarm (event object) and audit log query (request and ACK: audit log) setters; each reset the field on the refusal. The check is made before the field is touched, as in #4720, #4813 and the first half of #4815. Item 4 of #4815 (the I-Am and I-Have device identifier setters) is not a defect: they are private and reached only through SetIAmRequest and SetIHaveRequest, which mark the object invalid before the first field is written (#848), so a refusal there already leaves an invalid object. Item 5 (BACnetCovNotification, which needs a decision) and BACnetFirstFailedSubscription are not done.
  • rb dv-20261101-31 (#4740, item CR4-F14-007): an AtomicWriteFile whose write the application has already committed now resets the restore failure timeout (cl. 19.1.3.4) and flags the change of a bound certificate file (Changes_Pending) before its ACK is built, so an ACK that cannot be built (out of memory, or refused by the PDU) no longer loses them while the file content has changed; the restore could time out and abort in the middle of a write that had succeeded. The Changes_Pending half of the move (the bound certificate file) is not covered by a test, because no fixture here binds a file to an SC port; the restore keepalive half is. Items -003, -004, -005, -006, -008 and -009 of that roll-up are not done (-004 needs an owner ruling; -005 and -008 touch the documentation of frozen-header exports).
  • rb dv-20261101-32 (#4736, items CR4-F13-006, -007, -008, -009; no behaviour change a client can see):
    • The I-Am request and the ConfirmedPrivateTransfer-Error encode paths log texts that name the field and say “encode” (they said “decode”, and the I-Am request named a property reference).
    • The Confirmed and Unconfirmed PrivateTransfer processors refuse an invalid request as they do a missing one, as the I-Am processor does; no caller hands them an invalid request today, so there is no test that can fail without it.
    • The DeviceCommunicationControl processor drops a protocol-revision test that is always true (every build is at revision 24 or later) and the arm for the deprecated Disable that can no longer be reached after it is refused; the unreachable invalid-value branch stays as a logged defensive return. Its step-4 comment now names the services cl. 16.1.2 exempts from disabled initiation (I-Am and I-Have as replies, the audit notifications).
    • The DeviceCommunicationControl grammar comment gains the password bound, and the text message request’s constructor comment describes its one value constructor.
    • Items -003, -004 and -005 of that roll-up were #4760.
  • rb dv-20261101-33 (#4736, items CR4-F13-014, -019, -028):
    • A Who-Is or Who-Has request whose device-instance range limit is refused (above 4194303) is now left invalid, so a send path that checks validity cannot send it as an unbounded request to every device; the refusal invalidated only the limit, and the request stayed valid and encoded without limits.
    • A ReinitializeDevice request with the service enabled and no host callback registered is now answered with an Error (SERVICES / SERVICE_REQUEST_DENIED), as the Text Message processor does, instead of Abort(OTHER) (owner ruling 5: an Abort is for resource or state failures, not for a request the device cannot carry out).
    • Decoding a ReinitializeDevice request or a text message into a reused object, given a NULL or empty buffer, no longer leaves the previous decode’s state, password or message text in place.
    • Items -010 to -013, -015, -016, -018, -022 to -027 and -029 to -032 of that roll-up are not done.
  • rb dv-20261101-34 (#4788, #4792):
    • Decoding a change-of-reliability notification-parameters value, or an extended notification-parameters value, no longer accepts an unbounded number of elements. A list longer than STACK_OPTION_MAX_SEQUENCE_OF_ELEMENTS is now refused with Abort(buffer-overflow), as the event-parameter and fault-parameter twins already did, so a peer can no longer make one notification allocate one element per few wire octets
  • rb dv-20261101-35 (#4800):
    • A COV-type Trend Log of a REAL property no longer goes silent after a NaN, and no longer records a NaN on every Loop tick. A value going to or from NaN is now a change; NaN held is not (the comparison of a NaN is false against everything, so the increment test never fired once a NaN was the stored value, and the no-increment test fired on every call)
  • rb dv-20261101-36 (#4799):
    • A COV-type Trend Log of a property that is not REAL, BOOLEAN, ENUMERATED, Unsigned or INTEGER (a Double, a BIT STRING such as Status_Flags, a CharacterString, a Date or Time, an Object Identifier, a constructed value) now records every change. It used to record its first value and never another, because the change test had no arm for those datatypes and answered “not changed”; any difference from the last logged value is now a change
    • A value the log holds only as encoded bytes also lands in that arm: it records any byte difference and ignores a COV increment, where before it recorded nothing after the first value
  • rb dv-20261101-37 (#4785):
    • Writing an Event Enrollment’s Fault_Parameters as a fault-state with an empty list-of-fault-values (a legal value) no longer makes every later read of the property answer Error(PROPERTY, VALUE_NOT_INITIALIZED). The fault-state algorithm required a BACnetPropertyStates type that an empty list cannot supply; it now needs one only when it holds a value, so the empty list reads back as an empty fault-state. Fault values armed with no type are still refused
  • rb dv-20261101-38 (#4801):
    • A Trend Log or Trend Log Multiple with Align_Intervals TRUE and polled logging no longer takes its first data record at whatever moment logging starts. The first record waited for no boundary, so it was stamped off the Log_Interval grid (for example 10:17:23 for an hour-aligned log); it now waits for the next aligned boundary. This covers Enable written through WriteProperty (even when Align_Intervals is written after it) as well as a Start_Time window that opens later. A free-running polled log still takes its first sample at once, and a log enabled exactly on a boundary takes its first sample at the next one
  • rb dv-20261101-39 (#4790):
    • A received event notification whose Extended, Change Of Reliability, Command Failure or Change Of Status Flags values carry a Date or Time octet outside its range (a month of 0, say) is no longer offered to the local Event Log and Notification Forwarder. The tolerant decode keeps such a value, and encoding refuses it, so the offered record could not be read or forwarded; the out-of-range check now covers these arms (and a Date, Time or Date-Time held in any abstract value, nested ones included), as it already did for Access Event, Change Of Discrete Value and Change Of Timer
  • rb dv-20261101-40 (#4771):
    • Reading Event_Parameters of a CHANGE_OF_DISCRETE_VALUE Event Enrollment before it has observed its first monitored value now answers Error PROPERTY / VALUE_NOT_INITIALIZED, as the algorithms still waiting for a property reference already did (#3902). It answered UNKNOWN_PROPERTY, which says the property does not exist, against the object’s Property_List, and then began to succeed
  • rb dv-20261101-41 (#4773):
    • A Change Of State, Change Of Bitstring or Change Of Characterstring Event Enrollment whose monitored property the stack serves as encoded bytes only (a lighting output’s Blink_Warn_Enable, a device’s Max_Segments_Accepted) no longer ends every evaluation in a run error. The value is now decoded first, as the Change Of Discrete Value algorithm already did (#2725), and a value that is not a single primitive of the expected kind is still refused. The other algorithms that read their monitored value the same way (Life Safety, Timer, Command Failure, the range algorithms, Change Of Value, …) are unchanged and are tracked in a follow-up issue
  • rb dv-20261101-42 (#4830):
    • Event Enrollments and intrinsic reporting that use the Change Of Life Safety, Change Of Timer, Unsigned Out Of Range, Out Of Range, Floating Limit or Change Of Value algorithm no longer end every evaluation in a run error when the monitored property is one the stack serves as encoded bytes only (a device’s System_Status or Max_Segments_Accepted, a lighting output’s Tracking_Value, Transition or In_Progress, and others). The value is decoded first, as Change Of State, Change Of Bitstring, Change Of Characterstring (#4773) and Change Of Discrete Value (#2725) already did, and a value of the wrong datatype is still refused. Command Failure needed no change: it keeps a copy of the value and compares it by value, which does not read the datatype
  • rb dv-20261101-43 (#4777):
    • Writing a Change Of Timer Event Enrollment’s Event_Parameters with a new time-delay now also moves the time delay used for the return to normal when the enrollment has no Time_Delay_Normal of its own (the delay then follows the time-delay). It stayed at the previous value, so the return to normal ignored the written delay and no property showed why. An enrollment that has its own Time_Delay_Normal keeps it, as before
  • rb dv-20261101-44 (#4776):
    • An event notification built for an Event Enrollment (Change Of Bitstring, Change Of Characterstring, Change Of Life Safety, Change Of State, Change Of Timer, Command Failure, Double, Signed and Unsigned Out Of Range, Out Of Range, Floating Limit and Unsigned Range) now conveys the monitored object’s own IN_ALARM status flag. The algorithm rewrote IN_ALARM from the enrollment’s new state before the notification was built, so an enrollment on an object that was not in alarm notified IN_ALARM TRUE, and one on an object that was in alarm notified FALSE when the enrollment returned to normal. Intrinsic reporting, where the two are the same object, is unchanged. Change Of Discrete Value, Change Of Value, Access Event and Change Of Status Flags were not changed: the first three never rewrote it, and the last does not convey the flags
  • rb dv-20261101-45 (#4775):
    • A Command Failure Event Enrollment now reads the property its Feedback_Property_Reference names as the feedback value, before each evaluation, and a report of that property re-evaluates the enrollment. The reference was stored only so Event_Parameters could be read back, so the feedback value stayed unset, the pair was treated as agreeing, and the enrollment never left NORMAL. A referent that cannot be read keeps the last feedback value, and a reference naming an array element is not fed; a reference to another device is not fed. A referent property the host enabled but does not serve (other than Present_Value) is read as the stack’s default value, as the other references of this kind are, so such a reference can report a disagreement
  • rb dv-20261101-46 (#4774):
    • A report of a Binary Output’s or Multi-state Output’s Feedback_Value alone now evaluates its intrinsic Command Failure alarm. Only a Present_Value report did, so an actuator that dropped out with the command unchanged never raised the alarm, and a feedback that caught up never cleared it until the next Present_Value report. Present_Value is judged against the new feedback, as a Loop’s new Setpoint is for Floating Limit
  • rb dv-20261101-47 (#4802, items CR4-F22-001, -002, -003, -005, -010):
    • An Event Log record no longer reports a log datum as set, and stays valid, when the copy of that datum came out invalid (a failed clone of its notification)
    • An Event Log datum that switches between its log-status, notification and time-change arms no longer returns the previous arm’s value for the new arm
    • A Trend Log Multiple record built from log data that is not valid is now invalid and the call says so, for both the date-time and the time_t form; the record still frees the log data it was given
    • A log datum’s log-status and bitstring-value setters refuse an invalid bit string instead of reporting success
    • A log record’s Status_Flags setter refuses a bit string that is not exactly four bits, instead of storing it for the wire or dropping it at encode
    • Items -004, -006, -009, -011, -012 and -014 to -016 of that roll-up are not done
  • rb dv-20261101-48 (#4787, items CR4-F18-003, -012):
    • Decoding a fault parameter (the CHOICE and its eight alternatives other than none) into a reused object, given a null or empty buffer, no longer leaves the previous decode valid with its old contents; the object is reset first, as the event-parameter family already did
    • The test-tool header’s notes for the Fault_Parameters and Event_Parameters arming functions now say a ReadProperty of a parameter not yet configured answers VALUE_NOT_INITIALIZED, not UNKNOWN_PROPERTY
    • The other items of that roll-up are not done
  • rb dv-20261101-49 (#4789, items CR4-F19-001, -005, -010, plus the comment items -006, -013, -014, -015):
    • Decoding the Change Of Value event parameter’s criteria no longer takes an opening or closing tag numbered 0 or 1 for a bit string or a REAL; it is refused as an invalid tag, as the notification twin already did
    • Two Change Of Value criteria compare equal only when they hold the same CHOICE arm, so an empty bitmask no longer equals an increment of +0.0
    • The element cap on the extended event-parameter and extended fault-parameter lists is now gated by a test at the cap and one element above it
    • Comments only: the buffer-ready note names the log’s total record count, not a property that does not exist; the extended element class says eleven of its fourteen alternatives match the notification twin; the extended event-parameter header paraphrases clause 12.12.7 instead of quoting it; the cap comment no longer says the loop has no cap
    • The other items of that roll-up are not done
  • rb dv-20261101-50 (#4797, items CR4-F21-001, -002, -007, -009):
    • Decoding a time stamp resets the object before it checks its buffer, so a rejected buffer no longer leaves the previous stamp valid; an empty (not NULL) buffer is reported as a missing required parameter rather than Reject(OTHER)
    • A time stamp built from a Time that is not valid is now invalid, instead of becoming a valid stamp (a default-constructed BACnetPrimitiveTIME is not valid, so a stamp built from one is no longer a valid 00:00:00.00)
    • A notification destination whose From Time or To Time failed to set is no longer reported valid
    • An Event Enrollment refuses a write of Event_State, Acked_Transitions, Event_Time_Stamps and Status_Flags with WRITE_ACCESS_DENIED, as it already did for Event_Type and Object_Property_Reference; they are the stack’s own state
    • The other items of that roll-up are not done
  • rb dv-20261101-51 (#4795, items CR4-F20-006, -009, -015):
    • Decoding a Change Of Life Safety notification whose operation-expected value is refused (reserved 10 to 63, or above 65535) no longer leaves the object holding that value; it is reset, as for a refused new-state or new-mode
    • Two Change Of Timer notification parameters compare equal only when the same optional fields (last state change, initial timeout, expiration time) are present, so a present zero value no longer equals an absent one; two Change Of Value new-value choices compare equal only when they hold the same alternative, so an empty changed-bits no longer equals a changed-value of +0.0
    • Two comments now cite the clause that defines event-values (13.8.1.1.13) instead of the Get Alarm Summary clause
    • The other items of that roll-up are not done
  • rb dv-20261101-52 (#4781, items CR4-F17-025, -007):
    • A write of NaN to an Out Of Range algorithm’s High_Limit or Low_Limit is now refused (PROPERTY / VALUE_OUT_OF_RANGE) before the host is asked, as a non-finite Deadband already was; a NaN limit made every comparison false and could latch the alarm
    • Comments only: the access-event header says a changed tag or value fires when the time is unspecified, not “within one hundredth”; the buffer-ready note names pPreviousCount
    • The other items of that roll-up are not done; the “from the review finding, unverified” provenance notes were left in place because the clauses they cite were not re-checked
  • rb dv-20261101-53 (#4843):
    • A write of NaN to a Double Out Of Range algorithm’s High_Limit or Low_Limit is now refused (PROPERTY / VALUE_OUT_OF_RANGE) before the host is asked, as it already is for Out Of Range (#4781); a NaN limit made every comparison false and could latch the alarm. An infinite limit is still accepted, as a way to disable that limit
  • rb dv-20261101-54 (#4845):
    • Arming an intrinsic Out Of Range or Double Out Of Range algorithm with a NaN High_Limit or Low_Limit now fails and changes nothing, as a non-finite Deadband already did and as a write of such a limit now does (#4781, #4843). A NaN limit made every comparison false and could latch the alarm. An infinite limit is still accepted: it disables that limit
  • rb dv-20261101-55 (#4847):
    • Adding an Event Enrollment with the Out Of Range algorithm and a NaN High_Limit or Low_Limit now fails and adds nothing, as arming an intrinsic Out Of Range algorithm (#4845) and writing such a limit (#4781, #4843) already did; infinity is still accepted
    • Arming an intrinsic Floating Limit algorithm with a NaN Low_Diff_Limit or High_Diff_Limit now fails and changes nothing (a NaN band edge made every comparison false). A NaN setpoint is still accepted: the algorithm skips an evaluation while the setpoint is NaN
  • rb dv-20261101-56 (#4747, items CR4-F15-004, -005):
    • A COV-multiple subscription is valid when its four mandatory fields are set, whichever order the setters and the insert were called in; a field set after the list was filled used to leave it invalid (an empty list stays valid, as the four-argument constructor already built it)
    • A COV subscription’s monitored property reference setter refuses an invalid reference instead of storing it, and copying a subscription whose reference is refused resets the member rather than keeping a stale one
    • Items -006 (refuse an invalid element in the two list owners) was not done: a subscription may legitimately hold a specification with no references, which reports invalid, so the recommended refusal would reject valid content (see #4812); the other items (-002, -003 and -007 were merged earlier; -008, -009, -011, -012, -017 to -024, -026 to -028 and -006 are not done)
  • rb dv-20261101-57 (#4727, item CR4-F11-011):
    • A priority array built through its setter now equals its own decoded copy: a never-written slot (no value) and a Null slot compare as the same content in the array’s comparison, because the array encodes the first as the second. Two arrays that differ in a written value or in the priority of a value still compare unequal
    • Comment only: a “ToDo” with no issue was removed from the write-property decode
    • Items -002, -008, -010, -012 and -013 of that roll-up were merged earlier (#4763, #4806), and -004 and -009 in part (the never-valid-value half of -004 and the undecodable-opening-tag half of -009 are not done); the others are not done
  • rb dv-20261101-58 (#4727, items CR4-F11-005 and -015):
    • Comment only, no behaviour change: six comments in the write services that described what the code no longer does now describe it (the out-of-range priority answer for every property, the copy of a write access specification carrying the original’s validity, the write-group channel value’s typed child and the cl. 21 range for the overriding priority, the first-element failure answer of WritePropertyMultiple, and the datatypes the asynchronous write accepts)
  • rb dv-20261101-59 (#4740, item CR4-F14-009):
    • Diagnostics and comments only, no behaviour change: the file-access services’ log lines now name the field that failed (the file identifier, the file stream or record access data, the requested count, the file access parameters), and two comments and one clause citation in the stream write branch and the read and write processors were corrected
  • rb dv-20261101-60 (#4720, items CR4-F03-031, -032, -033, -034, -039, -040):
    • An OctetString decode no longer consumes an unextended context opening or closing tag (length/value/type 6 or 7) as a 6- or 7-octet value: it answers Reject INVALID_TAG, as the Unsigned64 decode already did. Before, an opening or closing tag where an OctetString belongs (a MAC address, file data) was taken as data with the next octets and the request was processed
    • A zero-length CharacterString is Reject INVALID_TAG whatever octet follows it; before, the following field’s octet was read as the character set and, for a value above 5, the answer was PARAMETER_OUT_OF_RANGE
    • Diagnostics and comments only: the bit string, double, real and unsigned encoders log their own type instead of “enum”, one citation and one comment name the right clause and class, and a dead empty branch with an unreferenced “ToDo” was removed from the CharacterString decode
  • rb dv-20261101-61 (#4720, items CR4-F03-036 and -037):
    • A signed, unsigned, 64-bit unsigned, object-identifier or octet-string decode that was refused after its tag no longer keeps the wire’s tag: a value set on the object afterwards encodes under the type’s own application tag, not under the refused frame’s (an application Date, or a context tag). The same reset the real, double, date and time primitives already had
    • An octet string decode answers a missing value as Reject OTHER instead of Abort OTHER, and a value shorter than its declared length as Reject INVALID_TAG instead of Reject OTHER, as every other primitive does. A request cut off after the field before an octet string (a file write, a device address) is now Rejected instead of Aborted
  • rb dv-20261101-62 (#4720, items CR4-F03-027 and -029):
    • A boolean, bit string, character string or enumerated decode that was refused after its tag no longer keeps the wire’s tag, and an absent optional field decoded with a context tag no longer keeps the next field’s tag: a value set on the object afterwards encodes under the type’s own application tag. The same reset the other primitives got in the previous batches
    • An optional unsigned whose CHOICE tag does not decode now fails with Reject INVALID_TAG instead of no reason at all, as the optional integer already did
  • rb dv-20261101-63 (#4720, items CR4-F03-030 and -038):
    • A property state whose CHOICE tag number has no arm in the standard (26, 29, 35, 61 and 62) is answered Reject INVALID_TAG, before anything is allocated, instead of Reject OTHER with a log line saying a feature is “not yet implemented”
    • A context-tagged Boolean whose content octet is neither 0 nor 1 is answered Reject INVALID_DATA_ENCODING instead of INVALID_PARAMETER_DATA_TYPE
  • rb dv-20261101-64 (#4720, items CR4-F03-016 and -044):
    • The sixteen optional-value codecs (binary lighting and binary present value, bit string, character string, date, date pattern, door value, double, integer, octet string, priority filter, real, time pattern, date-time, date-time pattern and the any-value) answer a tag that does not decode (a truncated extended tag, the reserved tag number 255, an application tag whose length/value/type is 6 or 7) with Reject INVALID_TAG instead of Reject OTHER, as the primitive codecs already do
    • Copying a tag that a decode refused on its reserved tag number 255 no longer leaves the copy’s tag number indeterminate: the copy starts from the reset state
  • rb dv-20261101-65 (#4720, items CR4-F03-042 and -043):
    • A list decode (the generic list and the property-value list) that fails on an undecodable tag, on an allocation failure, or on a bad parameter no longer leaves the elements it decoded before the failure, or those of an earlier decode, in the list: the list is empty as well as invalid, as the element-failure exit already left it
    • A list cut off right after its opening tag (a non-NULL buffer with nothing left) is answered Reject INVALID_TAG instead of Reject OTHER, as the same list cut off one element later already was; a NULL buffer stays Reject OTHER
  • rb dv-20261101-66 (#4720, items CR4-F03-013, -021, -024):
    • Comments and one local variable name only, no behaviour change: stale comments in the abstract-syntax type (a deleted document path, a line reference, and “Ungated” above two gated cases) and in the 32-bit object identifier header (the checked constructor is no longer “the only way” and the raw-word constructor is no longer wire-only) now say what the code does, and the throwaway variable o in the object property value comparison is named otherValue
  • rb dv-20261101-67 (#4720, item CR4-F03-049):
    • Comparing two lists of audit notifications (the notifications of a confirmed or unconfirmed audit notification request) compares the notifications, not just the length: two lists of the same length whose notifications differ are no longer equal. The catch-all comparison that answered “equal” for any element type that was not an object base was removed, so a new element type of that kind fails to build instead of comparing equal
  • rb dv-20261101-68 (#4720, item CR4-F03-050):
    • Comments, commented-out code and two dead branches only, no behaviour change: the property-value encode comments say “encode”, an unreferenced “ToDo” and three commented-out blocks (a copy constructor, an assignment operator and an accessor, in the source, the header and a test) were removed, the list validity comment says when an explicit invalid mark ends, the list encode logs its own type, two priority-range citations in a test cite the right clause, and an unreachable capacity check and an unreachable else were removed from the tag encode
  • rb dv-20261101-69 (#4720, items CR4-F03-011 and -012):
    • No behaviour change: two members of the abstract-syntax value that nothing ever set or read (a list flag and an array flag) were removed, and the comment that presented copying them as a fix now names the one member that matters; the string encoder of that class, which only the unit tests call, is marked as such in its header
  • rb dv-20261101-70 (#4720, items CR4-F03-020 and -023):
    • Storing a value in one of twelve optional-value objects (binary lighting and binary present value, bit string, date, date pattern, door value, double, integer, octet string, priority filter, real, time pattern) now stores it application-tagged, whatever tag the value arrived with. Before, a value that carried a context tag (decoded from a context-tagged field, or given one with SetContextTagNumber) kept it, so the object reported valid and encoded a context-tagged arm of a choice whose arms are application-tagged: a frame its own decode refuses and that is wrong on the wire. The primitive base gets SetApplicationTag(), the inverse of SetContextTagNumber()
    • Comment only: the any-value setter’s “a refusal leaves the previous value” is scoped to a refused value; an out-of-memory failure of its deep copy still drops the previous value (the result is still false and invalid)
  • rb dv-20261101-71 (#4720, items CR4-F03-005 and -006):
    • Copying a value that holds only encoded bytes (what the stack’s own property reads build) when the byte copy fails for want of memory no longer leaves a copy that reports valid and empty: the copy is invalid, as it already was when a decoded child could not be copied
    • Reading one object out of a value that holds two or more encoded objects no longer leaks the objects it had already decoded when the allocation for the next one fails
  • rb dv-20261101-72 (#4720, items CR4-F03-001 and -008):
    • Encoding a device object reference or a device object property reference into a buffer that is too short inside one of its fields answers Abort SEGMENTATION_NOT_SUPPORTED, the reason the grow-and-retry encoders act on, instead of overwriting it with Abort OTHER (six field sites in the two classes, as the object property reference already did)
    • Parsing a character string longer than the cap from its text or raw form into an abstract-syntax value now fails at the parse step, as the date, time, date-time and date-range arms already did, instead of returning success for an invalid value that failed later at encode
  • rb dv-20261101-73 (#4720, items CR4-F03-002 and -003):
    • A property value of the any type, or an unknown-typed value, that fails to decode now says why: a reserved application tag inside it (13 to 15) is Reject INVALID_TAG instead of Abort OTHER (which told the client the server had failed), and a missing or wrong opening tag, a closing tag that is not the value’s own, and a tag number no primitive uses, are Reject INVALID_TAG instead of failing with no reason. A reply-side decoder that has no backstop of its own (a ReadProperty or ReadPropertyMultiple acknowledgement) now records the reason. An allocation failure for a nested value is Abort OUT_OF_RESOURCES
  • rb dv-20261101-74 (#4720, item CR4-F03-045):
    • Tests only, no product change: two lines of the property value that no test could fail on now have one. Replacing a property value’s value twice leaves nothing allocated behind (the replaced value is freed) and the stored value is the second one; a cloned property value keeps its “value out of range” flag, so a cloned element is still answered with a property error and not a service-parameter reject
  • rb dv-20261101-75 (#4720, item CR4-F03-014; test-tool interface builds only):
    • Capturing the raw bytes of a programmatically built list element no longer refuses an element larger than one APDU (1476 octets): a segmented request is reassembled up to the unknown-value cap, and the capture made while decoding such an element already succeeded, so the two capture paths disagreed. The capture encodes through a buffer that grows, as every encoded property value does
  • rb dv-20261101-76 (#4755, items CR4-F16-009, -014, -024, -025):
    • Comments and log text only, no behaviour change: the notification forwarder, life-safety, event summary, enrollment summary and acknowledge-alarm sources no longer quote the standard’s wording (they cite the clause and paraphrase), no longer describe a retry question that was ruled or a “storage only” sprint state, cite the right clause (15.2.2) for the remove-list-element atomicity rule, name the right class or field in their log lines (event summary, enrollment summary, unconfirmed event notification, event priorities), and drop a dead commented-out block and an “unverified” citation note
  • rb dv-20261101-77 (#4778, #4782): an object whose fault algorithm is replaced while it holds a fault (an Event Enrollment’s Fault_Parameters written, or re-armed with another Fault_Type) makes the Fault state’s ToNormal at the next evaluation when the new algorithm finds no fault; it used to stay in Event_State FAULT with Reliability NO_FAULT_DETECTED and never run its event algorithm again — revert-red: the new test saw noChange and Event_State FAULT on the pre-fix code.
  • rb dv-20261101-77 (#4779): writing Event_Detection_Enable FALSE discards the first-stage fault (CONFIGURATION_ERROR or the host’s own) the object had reported, so a fault still held after the property is TRUE again is reported by the next evaluation instead of never — revert-red: the new test saw noChange and Event_State NORMAL on the pre-fix code.
  • rb dv-20261101-77 (#4780): a WriteProperty of Reliability_Evaluation_Inhibit or Event_Detection_Enable is evaluated at the next Tick, as an Event_Algorithm_Inhibit write already was, so the Fault state’s ToNormal (inhibit TRUE) or a held first-stage fault’s ToFault (detection enabled again) no longer waits for the monitored value to change — revert-red: the Tick-driven tests saw Event_State unchanged and no notification.
  • rb dv-20261101-78 (#4768): BUFFER_READY armed on an Event Enrollment now names the log the enrollment references as the notification’s Buffer_Property (it named the enrollment itself) and seeds the notification baseline from that log’s Total_Record_Count (it started at 0, so the first evaluation notified for every record already logged); the log-only properties are no longer enabled on the enrollment — revert-red: the new test saw a previous count of 0 and Buffer_Property naming the enrollment on the pre-fix code.
  • rb dv-20261101-78 (#4769): a read of Records_Since_Notification on a log with a BUFFER_READY algorithm counts the records logged since the algorithm last ran, so it moves while Event_Algorithm_Inhibit is TRUE, the object is in FAULT or Event_Detection_Enable is FALSE — revert-red: the new test read 0 after four records on the pre-fix code.
  • rb dv-20261101-78 (#4772): an enumerated CHANGE_OF_STATE algorithm can be armed on an Event Enrollment that references an Access Door, Access Zone, Load Control or Binary object, the alarm value taking the referenced object’s BACnetPropertyStates type; it was refused for every enrollment — revert-red: the arming returned false on the pre-fix code.
  • rb dv-20261101-79 (#4833): a written Time_Delay on a CHANGE_OF_TIMER algorithm that has no Time_Delay_Normal of its own also sets pTimeDelayNormal, as the Event_Parameters write does (#4777) — revert-red: Time_Delay_Normal stayed at the armed 0 after a Time_Delay write of 60.
  • rb dv-20261101-79 (#4874): the Boolean and Unsigned CHANGE_OF_STATE arming of an Event Enrollment is judged against the referenced object’s type: a Boolean alarm value on an enrollment that references a Binary Input or Binary Value is stored as the enumerated binary state, so ACTIVE now matches the monitored value (it never did), and Alarm_Values is no longer enabled on the enrollment — revert-red: the new test saw ACTIVE not match on the pre-fix code.
  • rb dv-20261101-8 (#4708): a router reached over BACnet/IPv6 is now learned from an I-Am-Router-To-Network or a received source network, with its whole 18-octet address and port (H.7.1 makes the VMAC function as a MAC); the learned-router cache refused every address over 7 octets, so each message for a remote network went out as a local broadcast. BACnetRemoteNetworkRouterCache::Learn now accepts up to 18 octets.
  • rb dv-20261101-8 (#4709): a stray segmented ComplexACK or server Segment-ACK from a BACnet/IPv6 peer now draws the rate-limited Abort (cl. 5.4.4.1); the per-source limiter refused every 18-octet key, so none was sent. The sender is keyed by its 16-octet address, so one host varying its UDP port shares one burst of 4, as on BACnet/IP. The limiter’s widest key is now 18 octets.
  • rb dv-20261101-8 (#4710): a clock set back no longer freezes the BACnet/IPv6 VMAC table or its held messages. Both are aged from the moment of the step (as the foreign device table already was), so a held message is dropped three seconds after it, a device that moved is followed one minute after it, and a silent entry is forgotten an hour after it, rather than after the clock has regained the step.
  • rb dv-20261101-8 (#4707): Device_Address_Binding lists a BACnet/IPv6 peer bound by a local I-Am, under its 3-octet VMAC (135-2024 H.7.1); the 18-octet connection string was over the 6 octets an NPDU MAC holds, so the read left the peer out and the audit path that names a requester by address could not match it. Sends still use the connection string.
  • rb dv-20261101-8 (#4711): a RENEW_FD_REGISTRATION command that the BBMD never answers now sets Reliability to RENEW_FD_REGISTRATION_FAILURE once the retry wait of 10 seconds has passed (cl. 12.56.16); before, a node whose BBMD stayed silent reported nothing, however long it retried, and a registered one only at the lapse of its Time-to-Live. An accepted registration ends the failure, as before. An unanswered first registration, which renews nothing, still reports nothing.
  • rb dv-20261101-8 (#4712): releasing a BACnet/IPv6 Network Port (removing its device, or a new port taking over a claim whose object is gone) now returns the data link to NORMAL mode and clears its BDT, FDT, local address, accept-registrations setting and foreign-device registration, ending the registration with its BBMD when a device can still send as this node (a new port taking over a stale claim; a removed device cannot, so its BBMD entry lapses with its Time-to-Live); as the BACnet/IP link has done since #3655. Before, the next port to claim the link read NORMAL while the link went on running as a BBMD, or renewing with the old BBMD. The VMAC table and held messages stay.
  • rb dv-20261101-80 (#4781, CR4-F17-016): a CHANGE_OF_TIMER algorithm now monitors only Timer_State, and a CHANGE_OF_STATUS_FLAGS algorithm only Status_Flags (or a Global Group’s Member_Status_Flags), as their clauses type the monitored value. Any other Enumerated or BitString property used to be accepted and then produced a notification with no event values or a runError on every evaluation — revert-red: Binary Input’s Event_State and Limit_Enable were accepted on the pre-fix code.
  • rb dv-20261101-81 (#4789, CR4-F19-004): a malformed choice tag (one that does not decode, such as FE FF) in an Event_Parameters, Fault_Parameters or change-of-value criteria value is answered with Reject INVALID_TAG; the event and fault parameter decoders answered OTHER and the criteria decoder set no reason — revert-red: the new tests saw reason 0 (OTHER) and no error on the pre-fix code.
  • rb dv-20261101-82 (#4789 CR4-F19-011, #4795 CR4-F20-014): a tag that does not decode in a BACnetEventParameter or BACnetNotificationParameters Extended value, its parameter element, a change-of-reliability value’s property-values list, a change-of-status-flags value, a change-of-value new-value or a fault-extended Fault_Parameters value is answered with Reject INVALID_TAG; these decoders left the reason unset, which a peer saw as OTHER — revert-red: the eight new tests saw no error set on the pre-fix code.
  • rb dv-20261101-83 (#4795, CR4-F20-010): handing a notification-parameters wrapper the arm it already holds no longer deletes it and reads the freed pointer (a use-after-free and a double free); the arm is kept — revert-red: the new test crashed (segmentation fault) on the pre-fix code.
  • rb dv-20261101-83 (#4795, CR4-F20-005): a change-of-discrete-value new-value that is a context-tagged primitive is refused, since the CHOICE arms are application-tagged and Decode refuses a context tag (the notification would have been undecodable by a peer) — revert-red: the context-tagged value was accepted on the pre-fix code.
  • rb dv-20261101-83 (#4795, CR4-F20-012, -013): a refused new-state on a change-of-state notification leaves the held new-state and validity as they were, a copy whose new-state failed is not valid, and SetNotificationParameters reports it — revert-red: see the PR (the tests for these two items).
  • rb dv-20261101-84 (#4795, CR4-F20-004): a change-of-access-event notification’s access event above 65535 is refused by SetAccessEvent (and the constructor) and by the decoder, which answers Reject PARAMETER_OUT_OF_RANGE and keeps no value; the enumeration is extensible only to 65535, as the life-safety states are — revert-red: 65536 was accepted and decoded as valid on the pre-fix code.
  • rb dv-20261101-84 (#4795, CR4-F20-017): test-only: the change-of-timer notification’s context tags are pinned against literal octets.
  • rb dv-20261101-85 (#4797, CR4-F21-003): BACnetTimeStamp::SetDatetime(date, time) given the stamp’s own date and time (references into the stamp) no longer empties them before reading them; it used to fail and leave the stamp invalid with its value lost — revert-red: the new test saw false and an invalid stamp on the pre-fix code.
  • rb dv-20261101-85 (#4797, CR4-F21-004): copying an unset BACnetTimeStamp no longer logs an “invalid rhs” error (the copy still clears the target) — revert-red: the new test counted the message on the pre-fix code.
  • rb dv-20261101-85 (#4797, CR4-F21-011): comments only: the Event Enrollment header and source describe the VALUE_NOT_INITIALIZED answer for a parameter not yet configured.
  • rb dv-20261101-87 (#4802, CR4-F22-009, -014): a Trigger written TRUE on a triggered Trend Log or Trend Log Multiple that cannot log (Enable FALSE, outside Start_Time/Stop_Time, or full with Stop_When_Full) is reset to FALSE when the logging gate refuses it, since the acquisition cannot happen; it used to stay TRUE and log a record, stamped with the later time, when logging resumed. A call with no clock keeps the Trigger — revert-red: the Trigger stayed TRUE after the refused gate and fired at Enable on the pre-fix code.
  • rb dv-20261101-88 (#4797, CR4-F21-008): replacing the fault algorithm’s property reference of an Event Enrollment (BACnetStackTestTool_SetEventEnrollmentFaultAlgorithmPropertyReference) clears the “fault referent unreadable” state of the old referent, so Reliability no longer reads UNRELIABLE_OTHER for a referent nothing has judged; it is set again by the next evaluation if the new referent cannot be read — revert-red: UNRELIABLE_OTHER stayed on the pre-fix code.
  • rb dv-20261101-89 (#4787, CR4-F18-004): a fault-characterstring fault value as long as the CharacterString local cap or longer (set up through BACnetStackTestTool_SetFaultCharacterstringAlgorithm, whose documentation says no upper limit is checked) no longer makes the Event Enrollment’s Fault_Parameters unreadable (UNKNOWN_PROPERTY); the element is built without the local length cap — revert-red: BuildFaultParameters returned false for a 300-octet value on the pre-fix code.
  • rb dv-20261101-9 (#4745): a COV subscription whose last reported Present_Value was NaN (a failed sensor), or whose value goes to NaN, now reports the step between NaN and a number, with a COV_Increment in effect; before, |NaN - x| >= increment was false for every x, so a subscription taken while the value was NaN never reported the recovered value and never re-baselined, and a number going to NaN was not reported. REAL and DOUBLE; NaN to NaN is still no change. The shared comparison serves SubscribeCOV, SubscribeCOVProperty and COV-multiple.
  • rb dv-20261101-9 (#4744): the List of Values of a SubscribeCOV notification now follows the order of Table 13-1 (cl. 13.6.1.1.5): Status_Flags comes straight after the first member (Present_Value; Access_Event for an Access Point), not last. Access Door, Access Point, Credential Data Input, Load Control, Loop, Pulse Converter and Staging were out of order; Analog, Binary and the other two-member objects were right. What a client sees: a client that reads the list by position now finds Status_Flags second on those seven object types. A SubscribeCOVProperty notification is unchanged.
  • rb dv-20261101-9 (#4741): each of the several UnconfirmedCOVNotificationMultiple / ConfirmedCOVNotificationMultiple requests a large queue is split into carries the Timestamp of the latest change that request conveys (cl. 13.17.1.1.4, 13.18.1.1.4), and none when none of its values has a Time of Change; before, every request carried the newest Time of Change of the whole queue.
  • rb dv-20261101-9 (#4742): a Load Control object now sends a COV notification when any of Duty_Window, Shed_Duration, Start_Time or Requested_Shed_Level is given a different value, as well as for Present_Value and Status_Flags (Table 13-1, cl. 13.1); before, a rescheduled Start_Time or a changed Duty_Window with Present_Value still SHED_INACTIVE told a subscriber nothing. Applies to a SubscribeCOV subscription; COV-multiple is unchanged.
  • rb dv-20261101-93 (#4781, CR4-F17-014): a COMMAND_FAILURE Event Enrollment whose monitored property is not Boolean, Enumerated, Unsigned or Signed (an Analog Output Present_Value is REAL) is now armed and runs; the gate that asks whether the property can be monitored refused it silently, so the enrollment never evaluated. The algorithm accepts any datatype, as its clause types the monitored and feedback values — revert-red: the new gate test failed on the pre-fix code.
  • rb dv-20261101-94 (#4787, CR4-F18-002): a FAULT_EXTENDED fault that is held and then re-indicated with a different fault reliability is now indicated again (TO_FAULT with the new reliability, so the Reliability property and the notification follow); it was ignored, and the only way through was to clear the fault, which sent a false TO_NORMAL — revert-red: the new test saw noChange and the first reliability on the pre-fix code.
  • rb dv-20261101-94 (#4787, CR4-F18-010): a FAULT_STATE enrollment whose fault list was written under a BACnetPropertyStates choice no longer faults on a monitored value of another datatype that happens to carry the same number (boolean-value is a Boolean, unsigned-value an Unsigned, every other choice an Enumerated); an Unsigned 1 faulted under binary-value { 1 }. The intrinsic path, with no choice named, keeps the numeric compare — revert-red: the new test saw ToFault for each mismatched datatype on the pre-fix code.
  • rb dv-20261101-95 (#4789, CR4-F19-012): the Extended event-parameter element and the Extended notification-parameter element decode an Unsigned of five to eight octets (as a BACnetPrimitiveUINT64) and re-encode it to the same octets; the element’s Decode refused it with Reject INVALID_TAG although an application could store the value and Encode wrote it, and an Unsigned has no 32-bit bound — revert-red: the new tests saw the refusal for the event-side and the notification-side element on the pre-fix code.
  • rb dv-20261101-95 (#4789, CR4-F19-002): encoding a none [20] Event_Parameters into a buffer with one octet left answers Abort segmentationNotSupported instead of returning 0 with no error, so a caller that grows its buffer on that Abort can retry — revert-red: the new test saw no error set on the pre-fix code.
  • rb dv-20261101-96 (#4917, CR4-F01-010): a Timer’s Status_Flags is built from the Event_State, Reliability and Out_Of_Service the Timer actually has, and a Global Group’s from its Reliability only when it has one; an absent optional component leaves its flag FALSE. A Timer or Global Group with only its required properties answered every read of its Required Status_Flags with VALUE_NOT_INITIALIZED, and no COV subscription to it was ever notified — revert-red: the new tests saw the failed read on the pre-fix code.
  • rb dv-20261101-96 (#4912, CR4-F01-003): while Out_Of_Service is TRUE the Reliability property of an Analog Output, Binary Output, Multi-state Output, Program, Access Door, Access Point, Global Group, Notification Forwarder, Channel, Lighting Output, Binary Lighting Output, Network Port, Accumulator and Staging is writable (as the Out_Of_Service clause of each of those objects provides), where the write answered WRITE_ACCESS_DENIED — revert-red: the new test saw false for all fourteen on the pre-fix code.
  • rb dv-20261101-96 (#4910, CR4-F01-001): a CharacterString the stack cannot hold, a Lighting_Command with a field out of range and an Authentication_Factor_Format with a field out of range, returned by the host, answer Error(PROPERTY, VALUE_OUT_OF_RANGE), as the Date, Time, DateTime and ObjectIdentifier getters already did (#3807); the CharacterString one fell through to an empty string the host never served, the other two to a bare failure — revert-red: the new tests saw the empty string and the missing error on the pre-fix code.
  • rb dv-20261101-97 (#4914, CR4-F01-006): a Channel whose local member takes a CharacterString, OctetString, BitString, Date, Time or ObjectIdentifier value now writes the member through the host’s Set callback for that datatype, as a WriteProperty does; the fan-out had no arm for those datatypes, so the member was never written and the Channel’s Write_Status ended FAILED — revert-red: the new test, for a CharacterString member (the same code shape serves the other four datatypes; an ObjectIdentifier member is not handled, because a WriteProperty has no arm for it either), saw the host callback never called and the Channel FAILED on the pre-fix code.
  • rb dv-20261101-97 (#4913, CR4-F01-004): a Channel member that is any property of an object the stack holds (a Lighting Output’s Default_Fade_Time, a Trend Log’s Enable, a Load Control, Life Safety, Access Zone, Access Point, Averaging or Event Log property), not only its Present_Value, is written through the stack’s engine as a WriteProperty of it would be; it went to the host’s callback, so the engine, which ReadProperty answers from, never changed while Write_Status read SUCCESSFUL, or the Channel failed when the host declined; Access User, Access Credential, Credential Data Input, Notification Class, Network Port, Trend Log Multiple and Audit Log members are not yet routed (#4913 stays open for them) — revert-red: the new test saw the value go to the host callback and the Lighting Output’s Default_Fade_Time unchanged on the pre-fix code.
  • rb dv-20261101-98 (#4915, CR4-F01-007): a WriteProperty of an empty list to an Access User’s Members, Member_Of or Credentials clears it; it answered INVALID_DATATYPE for a well-typed empty list, so a client could only revoke a user’s last credential or membership with a RemoveListElement per entry — revert-red: the new tests saw the refusal on the pre-fix code.
  • rb dv-20261101-98 (#4916, CR4-F01-008): a host-managed Lighting Output (one with no stack engine, added with AddObject) answers VALUE_OUT_OF_RANGE, with the host callback not called, for a Present_Value (other than the special values -1.0 to -7.0, which it accepts as the engine does) or Relinquish_Default that is NaN or outside 0.0..100.0, a Default_Fade_Time outside 100..86400000 ms, a Default_Ramp_Rate or Default_Step_Increment outside 0.1..100.0 and a Lighting_Command_Default_Priority of 0, 6 or above 16, as the Binary Lighting Output already did (#4676) — revert-red: the new test saw each of them accepted on the pre-fix code.
  • rb dv-20261101-98 (#4918, CR4-F01-011): an Event Enrollment whose Object_Property_Reference names a local object’s Status_Flags is re-evaluated by any report on that object, so a report of Out_Of_Service or Present_Value, or the stack’s own Event_State change, raises (or clears) an OUT_OF_SERVICE or IN_ALARM alarm on it; only a report of Status_Flags or Reliability re-evaluated it, so the alarm was missed or late — revert-red: the new test saw the enrollment stay NORMAL after the Out_Of_Service report on the pre-fix code.
  • rb dv-20261101-99 (#4921, CR4-F01-014): an object with a non-zero COV_Period notifies a SubscribeCOVPropertyMultiple subscriber of its Present_Value every period though nothing changed, as it did only SubscribeCOV and SubscribeCOVProperty subscribers (#4743); the multiple subscriber heard nothing after the initial notification. The period of each reference starts at its first look and restarts at each notification, and a change of value restarts it — revert-red: the new test saw no periodic COV notification on the pre-fix code.
  • rb dv-20261101-99 (#4923, CR4-F01-019): a SubscribeCOV or SubscribeCOVProperty this device sends, and its cancellation, is reported as a SUBSCRIPTION operation by an enabled Audit_Source_Reporter, with the monitored object (and for SubscribeCOVProperty the property) as the target and a Boolean Target Value (TRUE for a subscription, FALSE for a cancellation); they were the only confirmed services of Table 19-5 with no operation-source record — revert-red: the new tests saw no audit notification on the pre-fix code.
  • testtool-20261023 (chipkin/chipkin-bacnet-testing-tool#432): an intrinsic BUFFER_READY notification on a Trend Log, Event Log or Trend Log Multiple names the log’s Log_Buffer as its Buffer_Property (cl. 13.3.7, pLogBuffer); it named Total_Record_Count. Revert-red: the three Issue432_* tests failed on the pre-fix code.
  • rb v-20261016-1 (#3976): a build without a DateTime Value or DateTime Pattern Value object type serves DateTime-valued properties from the Date and Time callbacks again, including the File object’s required Modification_Date (cl. 12.12.7). In such a build a ReadProperty of Modification_Date answered value-not-initialized. Note for a minimal build: those properties used to fall to the constructed-value callbacks there; they now use the Date and Time callbacks, as a full build always has.
  • rb v-20261016-1 (#3977): the B-SS and B-ASC presets (STACK_OPTION_TARGET_EMBEDDED with STACK_OPTION_PROFILE_B_SS or _B_ASC, on BACnet/IP or MS/TP) compile. BACnetDBDevice.cpp used BACnetTimeStamp outside its option gate, and BACnetInterface.cpp relied on the ReinitializeDevice headers for BACnetReinitializedStateOfDevice.
  • rb v-20261016-1 (#3978): BACnet/SC compiles for MCU/RTOS toolchains. Under STACK_OPTION_TARGET_EMBEDDED the VMAC generator uses no thread_local, std::random_device or std::chrono; it is seeded from its own address and the device UUID, so give each unit its own UUID. The try around std::random_device in other builds is guarded like the stack’s other try blocks, so a -fno-exceptions build compiles.
  • rb v-20261016-1 (#3979): MSTP.c no longer hangs when the host’s timer does not advance. MSTP_SendFrame’s Tturnaround wait is bounded (with a Sleep callback, one turnaround of requested sleep while the timer is stalled; without one, 1,000,000 stalled polls). The silence-timer reset now reads the timer first, so a host timer that advances its reference by what TimerDifference() reported does not count a long transmission as silence. MSTP.h states the TimerReset contract.
  • rb v-20261017-1 (#3985): the CMake build uses mold only through a route the compiler supports. CMAKE_LINKER_TYPE MOLD is set where CMake supports it for both languages; otherwise -fuse-ld=mold is passed when the compiler accepts it; otherwise the default linker is used. Before, configure failed on GCC older than 12.1 whenever mold was installed (Ubuntu 22.04). Revert-red: cmake --preset tests reported “LINKER_TYPE ‘MOLD’ is unknown or not supported by this toolchain”.
  • rb v-20261017-1 (#3969): five Node addon callback wrappers (ReinitializeDevice, DeviceCommunicationControl, LifeSafetyOperation, AcknowledgeAlarm and the TextMessage hook) seed their errorCode/errorClass out-buffers before calling JavaScript. Before, a JavaScript callback that declined without writing a code sent the stack uninitialised bytes, which became the error on the wire. New gate _ci_scripts/check-napi-seeded-out-buffers.js; revert-red: it reports the six unseeded buffers on the pre-fix file. The four errorCode Buffers JavaScript receives are now 4 bytes long, as api.ts documents (writeUInt32LE(code, 0)); they were 16. A handler that wrote past offset 0 now gets a RangeError.
  • rb v-20261017-1 (#3968): _ci_scripts/adapter-signature-gate.js also compares each Go trampoline’s return type with the header’s function pointer. With the (*[0]byte) casts back, cgo no longer checks it, and the #2111 mismatch was a return type. Mutation: extern time_t fpCallbackGetSystemTime(); now fails the gate; it passed before.
  • rb v-20261017-1 (#3970): the Rust adapter returns a library that cannot be opened as Err from every binding instead of panicking. New load() lets an application check for the library at start-up. Revert-red: tests/missing_library.rs panicked in the old unwrap().
  • rb v-20261017-1 (#3968): the Go adapter compiles again. Its callback registrations take the (*[0]byte)(unsafe.Pointer(...)) cast cgo requires, and #cgo CFLAGS finds the header in place. Revert-red: go vet reported “cannot use … (value of type unsafe.Pointer) as *[0]byte value”. Making it an importable package is #3995.
  • rb v-20261017-10 (#3998): a WriteProperty or WritePropertyMultiple of a BACnetLightingCommand to a LightingCommand Channel’s Present_Value (BACnetStack_AddChannelObject datatype 97) reaches CallbackSetPropertyLightingCommand with Present_Value and the write priority. It used to fail (“Unsupported datatype for callbacks”). Test tool: with both that callback and the constructed-value callback registered, the typed one now gets the write. Reading the Channel’s Present_Value is not supported yet.
  • rb v-20261017-10 (#4012): a BACnet/IP Network Port added before the host registered its property callbacks now learns its own address when CallbackGetPropertyOctetString or CallbackGetPropertyUnsignedInteger is registered; before, the data link had no own address, so own-address checks, frame attribution and an address-form self recipient (#4009) missed. A BACnet_IP_UDP_Port above 65535 is refused instead of truncated. To pick up an address that changes later, call BACnetStack_AddNetworkPortObject again (periodic re-reading is owner question #4016).
  • rb v-20261017-10 (#3933, #3894): documentation (IFC-117) - what the send callback’s broadcast flag and connection string mean in every combination the stack passes, and one parameter line per argument of FPCallbackAuditNotificationReceived. Changing the BBMD re-broadcast’s connection string is owner question #4014.
  • rb v-20261017-11 (#4000): the ToFault re-sent when event detection is re-enabled on an object whose fault is still held carries the fault algorithm’s Event Values (Table 13-5), for example Fault_Signals on a Lift or Escalator, not the event-monitored property. BUFFER_READY armed after Total_Record_Count wrapped counts the records across the wrap instead of starting from 0.
  • rb v-20261017-12 (#4021): a host callback error code above 65535, or an abort- or reject-type code (COMMUNICATION class, cl. 18.7), is answered with that site’s no-code answer instead of being truncated to 16 bits (0x10054 used to go out as SUCCESS) - GetProperty, SetProperty, the list services, AtomicReadFile/AtomicWriteFile, AcknowledgeAlarm, LifeSafetyOperation, a declined array element and the Network Port write decline.
  • rb v-20261017-12 (#3486): a build with STACK_OPTION_OBJECT_TYPE_ACCESS_POINT and without STACK_OPTION_ALARMS_AND_EVENTS compiles (found in review before merge).
  • rb v-20261017-2 (#3955, #3428, #3433, #3390, #3293, #3958): customer-header documentation now describes what the code does (IFC-112; comment-only, the headers are unchanged with comments stripped):
    • Channel Present_Value writes reach the Set callbacks as Present_Value with the command priority, not as PriorityArray.
    • A lighting-command Channel (datatype 97) is supported for WriteGroup. Reads, WriteProperty and WritePropertyMultiple of one are not yet (#3998).
    • FPCallbackLoadControlShed can also fire inside a WriteProperty or WritePropertyMultiple.
    • LEARNED_CONFIGURED is sent as “learned” in Network-Number-Is.
    • The missing false cases of UpdateVirtualNetworkNumber (another port’s Network_Number, a physical-router network), AddScheduleObjectPropertyReference and the test-tool AddTimerObjectPropertyReference (a refused Device Address Binding, an out-of-range reference) are listed.
    • An unnamed object’s Object_Name is “Object ,”.
    • Schedule and Calendar objects need the application to enable and serve Local_Date (and Local_Time for a Schedule).
  • rb v-20261017-3 (#2733): FAULT_OUT_OF_RANGE and FAULT_LISTED CHANGE_OF_RELIABILITY notifications carry IN_ALARM and FAULT for the state being entered: TRUE on the transition to FAULT, FALSE on the transition to NORMAL, as 135.1-2023 8.5.17.12 and 8.4.17.13 expect. Before, they carried the flags read before the transition, which were the opposite. 14 assertions in tests/btl that pinned the old values were changed (owner ruling 2).
  • rb v-20261017-3 (#3731): Records_Since_Notification on an Event Log, Trend Log or Trend Log Multiple that already holds records when BUFFER_READY is armed now counts the records logged since logging began, or since the Event Log’s last purge, until the first notification (cl. 12.27.17). Before, it dropped to 0 at arming. A Trend Log purged before arming still counts from its creation.
  • rb v-20261017-4 (#3961): on BACnet/SC, an I-Am-Router-To-Network or Network-Number-Is this device sends in reply to a request is a local broadcast (cl. 6.4.2, 6.4.20), sent through the hub or distributed by this device’s own hub function, instead of a unicast to the requester. A host’s own SendIAmRouterToNetwork / SendNetworkNumberIs keeps the addressing it chose. With no hub connection such a reply is not sent: a broadcast goes only to the hub (AB.1.6). Any broadcast addressed through one of this device’s own hub function peers now goes to its hub function, which distributes it to every peer (AB.5.3.3), rather than to a hub connector connected elsewhere.
  • rb v-20261017-5 (#3959): a WriteProperty of an empty list to a writable Notification Class Recipient_List clears the list and releases its recipients’ address bindings (cl. 12.21.8 allows an empty list). It was answered Error(PROPERTY, INVALID_DATATYPE).
  • rb v-20261017-6 (#3238, #3285, #3217, #3450, #3549, #3869): customer-header documentation now describes what the code does (IFC-114; comment-only, the headers are unchanged with comments stripped):
  • rb v-20261017-7 (#3425, #3376, #3375, #3960): customer and test-tool header documentation now describes what the code does (IFC-115; comment-only, the headers are unchanged with comments stripped):
    • BACnetStack_UpdateValue runs the alarm and event algorithms before COV and trend logging; the old “returns before alarm evaluation” trap is gone.
    • A declined BACnetStack_RegisterCallbackDeleteObject callback answers Error(OBJECT, OBJECT_DELETION_NOT_PERMITTED); a second BACnetStack_SetProprietaryProperty for the same property returns false.
    • BACnetStack_AddDevice states the APDU_Timeout (10000 ms) and Number_Of_APDU_Retries (0: never retransmitted) defaults; the BACnet/SC manual and the generated property reference say the same. Whether to change the default is owner question #4006.
    • The test-tool Event Enrollment ChangeOfState alarm value rules (Binary Output, refused non-Boolean properties).
  • rb v-20261017-8 (#3967): a Lift or Escalator whose Passenger_Alarm is TRUE when a fault clears now goes to OFFNORMAL and sends the CHANGE_OF_STATE notification, after the ToNormal one. The event algorithm used to be handed Fault_Signals and fail (logged as “invalid datatype=[255]”), leaving Event_State NORMAL.
  • rb v-20261017-8 (#3964): a pending Network Port BDT or BBMD_Accept_FD_Registrations is activated on the BACnet/IP data link its write was validated against. With several unclaimed IPv4 data links, or on a gateway where two devices have a Network Port of the same instance, it could be committed to another data link. A virtual device’s pending BBMD values now activate at all (they stayed pending); with no claimed data link they go to the data link the write was validated on.
  • rb v-20261017-8 (#3957): a device’s own event notification that the host loops back (a broadcast delivered to the sending socket) is no longer offered to its own Event Log and Notification Forwarders a second time as received. CallbackEventNotificationReceived still fires for it.
  • rb v-20261017-9 (#4009): a Notification Class recipient given as this device’s own BACnet/IP address (network 0, any of its BACnet/IP ports) now hands a locally originated notification to the local Notification Forwarders, as a device-identifier recipient does (cl. 12.51); since #3957 it was lost. A broadcast recipient does not count. Two recipients naming this device with the same Process Identifier hand it off once.
  • rb v-20261017-9 (#4010): the data link recorded for a pending BBMD value (#3964) is forgotten when the changes are discarded, the Network Port is deleted or renamed.
  • rb v-20261017-9 (#3996): the Node addon starts the ReceiveMessage connection-string length bytes and every bool callback out-value at 0/false, so a JS callback that does not write one no longer passes an uninitialised length or bool to the stack.
  • rb v-20261018-10 (#4109): a recipient whose device identifier is not of object type Device no longer matches a Device with the same instance number, in two more places:
    • the Enrollment Filter of GetEnrollmentSummary, on either side (the filter or the Notification Class’s Recipient_List entry);
    • the test tool’s Audit_Notification_Recipient: such a recipient takes no address binding, and its audit notifications are dropped instead of being sent to the Device with that instance.
  • rb v-20261018-11 (#3676 CR3-F03-030): in a build without alarms and events, a BACnetTimeStamp value decodes as a time stamp again. Such values include a Device’s Last_Restore_Time, a Last_Command_Time, an Access Point’s Access_Event_Time and a Credential Data Input’s Update_Time. Likewise, in a build without Credential Data Input, an Access Point’s Access_Event_Authentication_Factor decodes as an authentication factor. Before, both fell through to unknown data. Builds with STACK_OPTION_TARGET_FULL were not affected.
  • rb v-20261018-11 (#3676 CR3-F03-027): an unset BACnetOptionalDateTime, BACnetOptionalDateTimePattern, BACnetOptionalAny or BACnetOptionalUnsigned no longer compares equal to another unset one, and renders nothing in DecodeAsJSON/XML. This matches the other optional-value types. Before, two unset objects compared equal and rendered a Null (or Unsigned) element.
  • rb v-20261018-12 (#4128): in a build without alarms and events, a TimeStamp-typed property served by the host is read through the Time callback again; this covers properties such as an Access Point’s Access_Event_Time. Likewise, in a build without Credential Data Input, an Access Point’s Access_Event_Authentication_Factor is read through the OctetString callback. Before, the callback wrappers existed only with those options, so such a property read as no value. Builds with STACK_OPTION_TARGET_FULL were not affected.
  • rb v-20261018-13 (#3768 CR3-F28-005): a WriteProperty of Exception_Schedule whose special event has an event-priority outside 1..16, or a calendar-reference to an object that is not a Calendar, now answers Error(PROPERTY, VALUE_OUT_OF_RANGE), as cl. 15.9.1.3.1 names. Before, it answered Reject(OTHER) (owner ruling on #3874).
  • rb v-20261018-15 (#2691 CR2-F13-030): the ReinitializeDevice callback docs now say to validate and return, then restart after the call. The stack sends the SimpleACK after the callback returns, so a restart inside it loses the reply and the client times out.
  • rb v-20261018-15 (#2691 CR2-F13-009, #2694 CR2-F16-005): doc corrections.
    • For a numeric Message Class, the text-message hook’s messageClassString is an empty string of length 0, never NULL; the doc said NULL.
    • The UTCTimeSynchronization processor comment now cites cl. 16.8 (it cited 16.7).
    • The LifeSafetyOperation callback now cites cl. 13.13 (it cited 13.8).
  • rb v-20261018-16 (#2611 CR2-F07-041): a second BACnetStack_SetBBMD keeps the host BDT entry it has when that entry carries the port’s address. Before, it bound the first entry with that address, which could be another entry with a different mask. BACnetStack_DeleteBDTEntry decides every refusal before it erases, so a call that returns false removes nothing.
  • rb v-20261018-17 (#3088 CR2-F01-056): the internal BACnetStackMemoryBuffer::AdoptAndPrepend refuses to adopt from itself; it used to return true with an empty buffer, silently losing the content.
  • rb v-20261018-17 (#3088 CR2-F01-010): the API wrappers in BACnetInterface.cpp mark the parameters a compiled-out feature does not use, so a reduced build (STACK_OPTION_TARGET_EMBEDDED with IPv4, MS/TP or IPv4 + BACnet/SC) has no unused-parameter warnings from that file under -Wall -Wextra.
  • rb v-20261018-17 (#2611 CR2-F07-023): the BACnetStack_AddNetworkPortObject doc says a Network Port instance number must be used on only one device; the data-link callbacks and per-port exports name a port by its instance alone (IFC-142; refusing a repeat is #4071).
  • rb v-20261018-18 (#2688 CR2-F10-025): the internal ReadProperty request and ACK setters refuse an object type above 1023 or an instance above 4194303 instead of masking it into another object’s identifier. The BACnetStack_SendReadProperty path was already range-checked (#3812), so no export changes (the client builder’s new IsValid check is defensive).
  • rb v-20261018-18 (#3088 CR2-F01-009): BACnetInterface.h doc comments match the code - 22 network parameters renamed destinationNetwork, three broadcast parameters and a bdtEntries parameter that no longer exist removed, the unsubscribed COV notification’s parameters documented, and SendCreateObject’s value described as raw binary, not text.
  • rb v-20261018-18 (#3088 CR2-F01-052): the SendAuthRequest refusal tests now reach the check each one names, and a positive send decodes the captured request.
  • rb v-20261018-19 (#4053, first slice): source/CASBACnetStackTestToolDLL.h and the AtomicWriteFile processor’s comments no longer quote the BACnet standard: each sentence an 8-word-run match against 135-2024 found (about 120) is now a clause citation with a paraphrase. ASN.1, property and enumeration names, and 135.1 test names stay as structural data. Comment-only; the rest of source/ and the manual are later slices of #4053.
  • rb v-20261018-19 (#3936): the v5-to-v6 migration guide says which BBMD table functions kept their version 5 parameter lists (SetBBMD, DeleteBDTEntry, DeleteFDTEntry, ResetBroadcastDistributionTable, ResetForeignDeviceTable) and which port each acts on. The message-callback and networkPortInstance changes the issue asked for were already in the guide.
  • rb v-20261018-20 (#4053, slice 2): the doc comments in CASBACnetStackDLL.h and BACnetCallbackInterface.h paraphrase the BACnet standard, with clause numbers, instead of quoting it (about 80 sentences, found by a word-run match against the extracted text). Comment-only (IFC-144).
  • rb v-20261018-20 (#3089 CR2-F02-052): one test now checks, for every object type, that the required-property list and the property profiles agree in both directions; each earlier fix was pinned one property at a time.
  • rb v-20261018-21 (#4053, slice 3): the comments in the 34 service processors (source/*Processor.cpp) paraphrase the BACnet standard, with clause numbers, instead of pasting its service procedures and error tables (about 200 detector hits, roughly 350 sentences). Comment-only, except two AcknowledgeAlarm debug-log messages that also copied the error table’s wording and are reworded. Table 13-1 in the SubscribeCOV processor (object types, properties and change criteria) stays as structural data.
  • rb v-20261018-22 (#4053, slice 4): the comments in BACnetDBDevice.cpp, BACnetBusinessLogic.cpp, BACnetNetworkLayer.cpp and BACnetRespondingStateMachine.cpp paraphrase the BACnet standard, with clause numbers, instead of quoting it (about 380 sentences, including the pasted cl. 5.4.5 segmentation transitions and the cl. 6.4.19 What-Is-Network-Number text). Comment-only. Several citations the old comments carried were corrected on the way (12.52.5 to 12.52, 19.2 to 19.2.1, 6.6.3.3 to 6.6.3.2, the File truncation rule to 12.13.11).
  • rb v-20261018-23 (#4053, slice 5): the comments in MSTP.c/.h, the Lighting Output and alarm-and-event objects, the notification-parameter and event-parameter headers, the BACnet/SC hub, initiating-peer and direct-connect files, and eight protocol-core files paraphrase the BACnet standard, with clause numbers, instead of quoting it (about 500 sentences, including the pasted Clause 9 MS/TP and Annex AB state machines). Comment-only. Citations corrected on the way include the MS/TP SendFrame procedure (now cl. 9.5.5.1’s numbered steps, from an older revision’s lettered ones), Max_Info_Frames/Max_Master writability (cl. 12.56.55/56), and the Lighting Output WARN conditions (Table 12-67).
  • rb v-20261018-24 (#4053, slice 6, the long tail): the comments in about 255 more source/ files paraphrase the BACnet standard, with clause numbers, instead of quoting it (about 1,100 sentences: encoding notes, event and fault algorithm conditions, BVLL and BACnet/SC field descriptions, network-message descriptions, object clauses). Comment-only. Enumeration lists, ASN.1, bit layouts and field tables stay as structural data. About a dozen citations the old comments carried were corrected (for example 12.1.1.1 to 12.1.1, 13.1 to 13.1.2, 12.25.12 to 12.25.13, 13.3 to 13.3.3).
  • rb v-20261018-3 (#3676 CR3-F03-002): a tag number 0-14 encoded in the extended form (B’1111’ followed by an extension octet) is refused as a decode fault, as 255 already was; a confirmed request carrying one is answered with Reject INVALID_TAG. It used to decode as the tag it named. The stack never emits that form.
  • rb v-20261018-3 (#3676 CR3-F03-032): two values that encode to the same octets compare equal when a composite element differs only in a context tag its encoding does not carry. A primitive’s or a constructed element’s context tag is still compared.
  • rb v-20261018-4 (#3825 CR3-F02-021): three log messages named the wrong cause: a missing error container in BuildObjectList, an Active_COV_Subscriptions build failure, and a host-name FD_BBMD_Address the application declined.
  • rb v-20261018-6 (#3762): a log record kept in object form whose copy ran out of memory part-way is no longer stored as an unreadable record; the add (or rewrite) fails instead.
  • rb v-20261019-25 (#4053, the manual): docs/_manual/content/docs/data-links/bacnet-sc.md paraphrases the five passages it quoted from the BACnet standard, and cites cl. 12.1.1 (not 12.1.1.1) for the 4194303 “not in use” instance.
  • rb v-20261019-25 (#3088 CR2-F01-103, in part): a test drives the 18 BACnetStack_Send* exports that take an (objectType, objectInstance) pair through the flat C API; each sends with a valid pair and refuses, sending nothing, the same pair with 1024 added to the type or 0x400000 added to the instance (values that a missing range check would mask back onto the valid pair).
  • rb v-20261019-26 (#3939): API Reference doc fixes in CASBACnetStackDLL.h: CASBACnetTime is documented; BACnetStack_AddRouterPort and BACnetStack_DecodeAs list their network-type values (0 BACnet/IP, 1 MS/TP, 2 BACnet/SC); the routing parameters of SendWhoIs and 14 sibling exports say what each destination value does; the BBMD and foreign-device exports cite their Annex J subclauses; the GetInfo example shows protocol revision 30. Comment-only (IFC-145).
  • rb v-20261019-26 (#3939): doc-lint.sh also refuses a two-digit #NN, “issue NNNN” and “Owner-ruled” in the customer header’s doc text, and checks every name in a \param a, b list whose commas are spaced.
  • rb v-20261019-27 (#3939): _ci_scripts/adapter-model.js drops a \see that wraps over several lines whole (its continuation lines used to reach the binding’s description) and reads a bare \par as a paragraph break rather than literal text. No header has either shape today, so the generated bindings do not change; the self-test pins both.
  • rb v-20261019-27 (#3939): doc-lint.sh --self-test builds its \ingroup fixtures with awk instead of GNU-only sed, so it also runs on BSD/macOS.
  • rb v-20261019-28 (#3088 CR2-F01-103): two more tests drive the 44 non-Send exports that take an (objectType, objectInstance) pair (object, property-flag, trend log, lift, channel, group, schedule and alarm-algorithm setters) through the flat C API. Each succeeds with a valid pair and refuses, changing nothing, the same pair with 1024 added to the type or 0x400000 to the instance. With the 18 Send* exports from v-20261019-25, every pair-taking export is now covered.
  • rb v-20261019-29 (#3983): the BACnetStack_RegisterCallbackReceiveMessageForPort documentation now states what an MS/TP port requires. Both connection strings must be 1 octet: the source is the sender’s MAC address (never 255), and the destination is this station’s MAC address or 255 for a broadcast. The stack discards a frame without them (or with source 255), and reports nothing unless API debug logging is built in (IFC-146). The Zephyr adapter (adapters/zephyr/src/cbz_mstp.cpp) and the manual’s MS/TP example already pass the destination. No other example or adapter has an MS/TP receive path.
  • rb v-20261019-30 (#3939): the adapter generators’ doc-block reader (_ci_scripts/adapter-model.js) now ends a wrapped \see at a section command (or, as before, a blank line), not at any command. A continuation line that starts with an inline command such as \ref or \c used to end it, and that line’s text leaked into every binding’s description. A \see directly followed by \ingroup now also ends there, and \sa (Doxygen’s alias) is dropped like \see. No header uses any of these cases today, so the generated adapters are unchanged (drift checks clean). A new self-test case gates each change: each one, reverted, fails it. (Corrected by v-20261019-31: a \result after a \see was always read, and the list no longer names \internal, \todo, \bug and similar commands, whose raw text it would have passed into the bindings.)
  • rb v-20261019-31 (#3939; follow-ups to #3681, #3088 and #4053 - the post-hoc second reviews of v-20261019-25, -26, -28, -29 and round 2 of -30). Changes:
    • _ci_scripts/doc-lint.sh accepts a spaced multi-name \param a, b whose names are all real. Before, the every-parameter check flagged the second name.
    • doc-lint’s maintainer-metadata rule now also catches Issue#NN / IssueNN, PRNN, issues NN, PR NN, a #NN after a word (gh#NN), and an owner-ruling note in any letter case.
    • doc-lint self-test cases cover both changes; reverting either one fails them.
    • _ci_scripts/adapter-model.js no longer ends a dropped \see at \internal, \author and similar commands. It has no handling for them, so their raw text would have reached every binding. A self-test line pins this.
    • The CASBACnetTime and BACnet/SC state-change doc comments are corrected (IFC-147).
    • The pair-refusal tests are tidied:
      • an <ostream> include;
      • comment fixes;
      • Setters A records the pair the device holds, so a regressed guard leaves nothing behind.
    • A manual footnote is reworded further from the standard’s text.
  • rb v-20261019-33 (#1595): five customer-header doc comments now name the BACnet type they describe instead of C shorthand. AddRecipientToNotificationClass’s validDays is a BACnetDaysOfWeek BIT STRING. SetIntrinsicChangeOfStatusFlagsAlgorithm’s selectedFlags is the BACnetStatusFlags pSelectedFlags. SetIntrinsicChangeOfTimerAlgorithm’s values are BACnetTimerState and its pAlarmValues. SetAlarmsAndEventsForObjectEnabled’s notifyType is a BACnetNotifyType, and ReadTrendLogRecord’s statusFlags is the record’s BACnetStatusFlags. Each cites its clause (IFC-152). Comment-only; the generated bindings carry the new text.
  • rb v-20261019-34 (#1595): the API conventions page now says the objectType/objectInstance pair is one BACnet Object Identifier (a 10-bit type and a 22-bit instance) carried as two parameters. It also says an export refuses a pair that does not fit those widths instead of letting it wrap onto another object. In the alarm section, 24 exports’ separate objectType and objectInstance parameter docs are now one entry that says the same, and RemoveObject’s return note now names an out-of-range pair (IFC-153). Comment-only; the generated bindings carry the new text.
  • rb v-20261019-35 (#1595): every object identifier pair in CASBACnetStackDLL.h (except SendCreateObject’s optional one) now says its type and instance are one BACnet Object Identifier carried as two parameters (IFC-154). Beyond the 24 alarm-section exports done by IFC-153, that covers objectType/objectInstance in 22 exports and the prefixed pairs (monitoredObject*, eventObject*, refObject*, memberObject*, logObject*, lastReceivedObject*, energyMeterRefObject*) in 16 more, and per-export constraints are kept. With IFC-153, the export header follows the API conventions page; the callback header is not yet done. Comment-only; the generated bindings carry the new text.
  • rb v-20261019-36 (#1595): every object identifier pair in the callback header (BACnetCallbackInterface.h, 60 of them, with any prefix) now says its type and instance are one BACnet Object Identifier carried as two parameters (IFC-155). With IFC-153 and IFC-154, both customer headers now describe the pair the way the API conventions page does. Comment-only; the generated bindings carry the new text.
  • rb v-20261019-37 (#1595): a section-by-section read of CASBACnetStackDLL.h found 48 doc comments that described a BACnet constructed or named type only in C terms (a list of fields, “the date and time”, “an array of values”, a bare number). Each now names the type and its clause (IFC-156). Examples: Trend Log start and stop times as a BACnetDateTime, the property references of Channels, Schedules and Trend Logs as BACnetDeviceObjectPropertyReference, AcknowledgeAlarm’s acknowledged state as a BACnetEventState with its values, and the arms of the ReadRange range CHOICE. The base SendReadRange no longer says only “by-position” is omitted: it sends no range at all. Comment-only; the generated bindings carry the new text.
  • rb v-20261020-1 (#4217): an idle BACnetStack_Tick() no longer makes a heap allocation on every tick in an MSVC build. Tick() declared an empty routing-table std::map on every tick, and MSVC’s std::map allocates its head node when constructed (libstdc++’s does not). The map is now built only when the routing tables are pushed. The idle-tick allocation tests now also pass under MSVC’s debug standard library, where they allow only its per-container bookkeeping allocations.
  • rb v-20261020-2 (#4224): an idle BACnetStack_Tick() of a device whose Notification Forwarder has a device-form recipient no longer allocates on every tick, on any platform. The device address binding check built the recipient list and a sorted copy of it as new vectors on every tick; both now live in the forwarder and keep their capacity. The forwarder’s delivery step also returns at once when nothing is queued, so it no longer builds and swaps in an empty retry list.
  • rb v-20261020-3 (#3809): a Channel created with the BACnetLightingCommand or BACnetObjectIdentifier datatype now reads its Present_Value, the last value written to it (cl. 12.53.5), through CallbackGetPropertyLightingCommand or CallbackGetPropertyObjectIdentifier without an array index. It reads as NULL while the host serves nothing, answers the host’s error when it names one, and answers VALUE_OUT_OF_RANGE for a value the stack cannot build. Before, every read answered Error(PROPERTY, VALUE_NOT_INITIALIZED). Every other Channel datatype still reads from the priority slots. The BACnetStack_AddChannelObject doc also drops two stale statements: that a lighting-command Channel’s Present_Value cannot be written (wrong since #3998), and that an all-NULL read answers UNKNOWN_PROPERTY (wrong since #3808) (IFC-166).
  • rb v-20261021-1 (#3524): the hub connector sends a Disconnect-Request on each established hub connection before it closes it (cl. AB.6.2.2), for example on a Hello-capabilities change or when the port goes out of service; teardown is still synchronous. Before, the socket was closed with no Disconnect-Request (owner ruling).
  • rb v-20261021-1 (#3681): a received header option of type 0 is still treated as unknown (ignored, or HEADER_NOT_UNDERSTOOD with Must Understand; a data option is relayed), now commented and pinned by tests (CR3-F09-008). The failed-connection log comment now says its VMAC+UUID key resists only an identical repeat (CR3-F09-020). Owner rulings, #3883 items 9 and 11.
  • rb v-20261022-review-fixes (#4346): the manual’s BACnet/SC example comment says the host application generates the CSR file on GENERATE_CSR_FILE; it called the file stack-generated.
  • rb v-20261022-review-fixes (#4347): the migration chapters say version 6 renamed the version 5 ENABLE_* / CASBACNETSTACK_ENABLE_* build flags to STACK_OPTION_* (new checklist item 10); they had said the flags “have always been” STACK_OPTION_*.
  • rb v-20261022-review-fixes (#4348): bacnetstack.com calls the stack a C/C++ library, and the VeriBACnet card no longer claims the harness runs on every change.
  • rb v-20261022-review-fixes (#4349): the Zephyr adapter’s CMake comment says the stack needs neither RTTI nor exceptions, with or without BACnet/SC.
  • rb v-20261023-1 (#2612, CR2-F08-013): the MS/TP engine’s header check applies cl. 9.5.8’s own tests, and no longer refuses a reserved Frame Type (8-31, 34-127). Before, another station’s frame of such a type set ReceivedInvalidFrame, which ended this node’s WAIT_FOR_REPLY and gave up on the reply it was waiting for. A reserved-type frame addressed to this node is now discarded by the master node (cl. 9.5.6.2). A COBS-encoded frame’s Length is checked against 5 to 2043 (cl. 9.5.8), rather than 501.
  • rb v-20261106-1 (#5003): a build with the virtual router but without the physical router compiles again. That is the embedded target with STACK_OPTION_ENABLE_ALL_BIBBS (which turns on STACK_OPTION_GW_VN_B), and the full target with STACK_OPTION_DISABLE_ROUTER_PHYSICAL; every source/*.cpp was syntax-checked in both. Since #4501 the virtual router’s reject path called BACnetNetworkLayer::IsRejectMessageToNetworkNpdu, which was declared and defined only with STACK_OPTION_ROUTER_PHYSICAL, so BACnetNetworkLayer.cpp stopped at “‘IsRejectMessageToNetworkNpdu’ is not a member of ‘CASBACnetStack::BACnetNetworkLayer’” (lines 3751 and 3957). The helper is now compiled for every router build. _ci_scripts/check-embedded-compile.sh now checks both configurations as well, after its embedded and alarms-and-events passes, and carries on past a configuration that does not compile, so one run reports every one that fails.
  • rb v-20261109-1 (#5007): the profiles page’s frozen profile codes now stay at the top of the table after the Lato font swaps in; the section row above them was measured once, in the fallback font, so on a machine with a wider fallback the header sat off the region’s top — revert-red: the new Playwright test read 179.75px on the pre-fix script.
  • #3942 review: the manual, the README files and tools/coverage/README.md no longer point at deleted build files or at internal files a release package does not carry; the coverage README describes the CMake cache options a coverage build uses. A comment in CASBACnetStackDLL.h names the CMake DLL build instead of the deleted Visual Studio project (IFC-111). No declaration changed.
  • rb bs-20261024-1 (#4405, slice 2 of #3165): a write of Link_Speed or Link_Speed_Autonegotiate on a Network Port (each read-only unless the host makes it writable) is now held pending by the stack, as the standard requires (cl. 12.56.21, 12.56.23): Changes_Pending reads TRUE, a client’s read returns the written value, and the value is handed to the host’s Set callback (CallbackSetPropertyReal for Link_Speed, CallbackSetPropertyBool for Link_Speed_Autonegotiate) at ReinitializeDevice ACTIVATE_CHANGES or WARMSTART (or BACnetStack_ActivateNetworkPortPendingChanges()), with the value in use handed back if the host refuses one. Your Set callback is no longer called at the write, and the write is refused with Abort(other) when CallbackReinitializeDevice or the Set callback is not registered. A negative, infinite or NaN Link_Speed is refused with VALUE_OUT_OF_RANGE, and Link_Speed is refused with WRITE_ACCESS_DENIED while Link_Speed_Autonegotiate is TRUE. Check that your Set callbacks expect to be called at activation.
  • rb bs-20261024-18 (#4488, items CR4-F08-007, -008): documentation only. A decision record (_spec/decisions/2026-10-24-mstp-encoded-data-length.md) states why MSTP.c checks a COBS-encoded frame as DataLength + 2 against the input buffer rather than the literal cl. 9.5.4.4 comparison; and #769 was updated with the news that the shipped Zephyr adapter is now a caller of MSTP.c.
  • rb bs-20261024-26 (#4489, item CR4-F09-018): the hub-connector state machine and helper sources, and the SC packet-processing and network-port sources, are indented with tabs like the rest of source/; whitespace only.
  • rb bs-20261024-3 (#4464, follow-up to #4405): when a Network Port has Link_Speeds enabled, a write of Link_Speed whose rate is not in the array your application serves for Link_Speeds (the element count through CallbackGetPropertyUnsignedInteger at array index 0, then CallbackGetPropertyReal per element) is refused with PROPERTY / VALUE_OUT_OF_RANGE (cl. 12.56.21); so is a write when Link_Speeds is enabled but your callbacks do not serve it. A port with no Link_Speeds enabled is checked as before. Check that a host that enables Link_Speeds serves it.
  • rb bs-20261024-35 (#4508, items CR4-F06-003, -004, -006, -022, -025): stale comments on the network layer’s source-network and broadcast-flag checks, on the unreachable-DNET paths, on the client network-parameter setters and on the unimplemented network layer message types are corrected; the never-set m_isRouter member and a duplicate #include <vector> are removed (IsRouter() answers the same). No behaviour change.
  • rb bs-20261024-47 (#4509, item CR4-F07-007): stale comments in the BVLL base decoder, the BDT/FDT entry classes and the FDT timer are corrected (the BVLC length is not a fixed X’000A’; the #741 clamp has landed; the entry layouts are the Annex J octet layouts, not ASN.1; the FDT grace period is the 30 seconds J.5.2.3 states); three unused constants are removed from BACnetBVLL.h and the wrapper decode uses BACnetBVLLBase::BVLCTYPE_BACNETIP instead of the literal X’81’. No behaviour change.
  • rb bs-20261024-49 (#4487, items CR4-F05-027, -029, -030, -031, -032): the debug cl. 18 error-pairing diagnostic now knows OBJECT / VALUE_OUT_OF_RANGE (cl. 13.13.1.3.1) and COMMUNICATION / PARAMETER_OUT_OF_RANGE (cl. AB.3.1.5) and treats cl. 18.5’s UNKNOWN_KEY as UNKNOWN_CERTIFICATE_KEY (204), also in the callback error-code to class mapping; the table is compiled in the test builds too and has a test seam. Comments: the clause 23 range sentence in four enumeration headers is paraphrased, IsRawServiceRequest’s doc is corrected, and shipped comments no longer cite the machine-local spec extraction by path. No change to what the stack sends. A host callback that reports error code 204 (UNKNOWN_CERTIFICATE_KEY) is now classed SECURITY (it used to fall to PROPERTY), and 107 (the removed UNKNOWN_KEY) is no longer classed SECURITY.
  • rb bs-20261024-5 (#4462, slice 2b of #3165): a write of IP_DNS_Server on a BACnet/IP Network Port (read-only unless the host makes it writable) is now held pending by the stack, as the standard requires (cl. 12.56.31): Changes_Pending reads TRUE, a client’s read returns the host array with the written elements laid over it, and each written element is handed to the host’s CallbackSetPropertyOctetString at ReinitializeDevice ACTIVATE_CHANGES or WARMSTART (or BACnetStack_ActivateNetworkPortPendingChanges()), with its array index (useArrayIndex true, index from 1), the element in use handed back if the host refuses one. Your Set callback is no longer called at the write, and the write is refused with Abort(other) when CallbackReinitializeDevice or the Set callback is not registered. A write is the whole array at the length your callbacks serve it, or one element by index; an address of other than 4 octets is refused with VALUE_OUT_OF_RANGE, a wrong array length or an index past the end with INVALID_ARRAY_INDEX, index 0 (the length) with WRITE_ACCESS_DENIED, and any write while IP_DHCP_Enable reads TRUE with WRITE_ACCESS_DENIED. Check that your Set callback expects to be called at activation, once per element, with an array index.
  • rb bs-20261024-50 (#4487, items CR4-F05-005, -011): removed BACnetDataLink::RunRespondingStateMachine (no callers) and the BACnet/SC fallback loop in BACnetStackSentConfirmedRequests::FindRequest, which compared the same key fields the map lookup had just used and so could never match. Stale comments and two mislabelled abort log strings in the responding state machine are corrected. No change to what the stack sends.
  • rb bs-20261024-60 (#4509 CR4-F07-002, 013, 019): removed the BBMD’s always-true m_receivedMessage guard and its setter, corrected four stale comments in BACnetBBMD.cpp, updated the BACnetDataLinkLayer.h docs for the egress-port fallback and the takeover rebinds, and removed BACnetPacket::DecodeAPDU (no caller; the receive path uses DecodeAPDUUsingSupportedServices). No change to what the stack sends.
  • rb bs-20261024-61 (#4487 CR4-F05-020): stale line citations in the PDU-family comments now name the reserved field instead, and four inaccurate comments and one log message are corrected. No change to what the stack sends.
  • rb bs-20261024-62 (#4486): SC_Hub_Function_Enable, SC_Direct_Connect_Initiate_Enable and SC_Direct_Connect_Accept_Enable (and the accept URIs, binding and connection status that go with them) now stay present on the Network Port once the host has configured the role, and read FALSE when it is off. They used to disappear when the role was switched off, so a client that wrote FALSE could never write TRUE again. Property_List therefore keeps them after a disable.
  • rb bs-20261024-64 (#4506): a BBMD no longer distributes an Original-Broadcast-NPDU whose UDP source is outside the BBMD’s own IP subnet (J.5.1: such a sender is a foreign device and has Distribute-Broadcast-To-Network); the frame is still delivered to the device’s own network layer. The subnet comes from the Network Port’s IP address and subnet mask; while either is not known the frame is distributed as before. A multinetted host whose senders sit on another of its subnets therefore needs the Network Port’s mask set to cover them.
  • rb bs-20261024-68 (#4558, #4559 slice 1): internal only. Eleven pair-form (object type, object instance) forwarders that only the test-tool interface used are deleted, and BACnetObjectIdentifier32::FromTrustedPair replaces the FromRaw(GenerateObjectIdentifier(…)) idiom at 91 internal sites. No customer-visible behaviour changes.
  • rb bs-20261024-69 (#4569): a BBMD now accepts a BBMD_Broadcast_Distribution_Table (Network Port) write whose entries use a host NAME instead of an IP address (cl. 12.56.38), as long as each has the 4-octet broadcast mask. Before, the whole write was refused with VALUE_OUT_OF_RANGE. A dotted-decimal name (“10.0.0.2”) is parsed and treated as that address (Annex J.1.2.1); any other name is stored as written and stays unresolved, since this stack does no name lookup. The property reads each entry back as written (a name stays a name, with its port); the Read-Broadcast-Distribution-Table-Ack lists a resolved name’s address and six zero octets for an unresolved one, in the order written (cl. 12.56.38 (e)); and an unresolved entry is never forwarded to and never matches an address. An internal hook (BACnetBBMD::SetBroadcastDistributionTableEntryResolvedAddress) lets a resolver supply the address later. A name entry without a broadcast mask, a none host and a 16-octet address are still refused.
  • rb bs-20261024-7 (#4195 slice 3): the C++ test-tool class BACnetTestToolInterface’s seven parallel-array setters (SetLifeSafetyZoneMembers, SetAccessDoorMembers, SetAccessZoneEntryPoints, SetAccessZoneExitPoints, SetAccessPointAccessDoors, SetAccessRightsNegativeAccessRules, SetAccessRightsPositiveAccessRules) take an array of BACnetObjectIdentifier32 where they took an object-type array and an object-instance array (IFC-188). The BACnetStackTestTool_* exports keep their parameters and accept and refuse the same input as before; only C++ code that called the class directly changes. BACnetTestToolInterface.h now declares no (object type, object instance) pair.
  • rb bs-20261024-70 (#4568): internal only, no behaviour change by default. An enabled physical router can now apply an Initialize-Routing-Table message that carries a table (cl. 6.6.3.8, 6.4.7) and answer it with the data-less Ack, behind an internal opt-in that is off, so the update is still ignored and not acknowledged until #4111 turns it on. With the opt-in on, a Port ID of zero purges every route to that DNET and a non-zero Port ID replaces or appends it (never aged or evicted); an entry naming a DNET of 0 or 65535, or a Port ID that is not a configured port, refuses the whole update.
  • rb bs-20261024-72 (#4559 slice 2): internal only. BACnetObjectIdentifier32::FromTrustedPair replaces the FromRaw(GenerateObjectIdentifier(…)) idiom at 185 more internal sites in 52 files (everything except BACnetDBDevice.cpp). Same packing; no behaviour change.
  • rb bs-20261024-73 (#4559 slice 3): internal only. BACnetObjectIdentifier32::FromTrustedPair replaces the FromRaw(GenerateObjectIdentifier(…)) idiom at 349 sites in BACnetDBDevice.cpp. Same packing; no behaviour change.
  • rb bs-20261024-74 (#3695): documentation only. The docs of CallbackPrepareRestore, CallbackCompleteRestore, BACnetStack_RegisterCallbackCompleteRestore and CallbackGetPropertyUInt now say how an application that serves Database_Revision itself meets cl. 19.1.3.4: it keeps its revision when it accepts the restore and sets it to that value plus one when CallbackCompleteRestore returns true for a normal end of restore. No behaviour change; the stack already does this for the value it stores.
  • rb bs-20261024-76 (#4559): internal only. 51 more uses of BACnetPrimitiveObjectIdentifier::GenerateObjectIdentifier in source/ now use BACnetObjectIdentifier32::FromTrustedPair(…).Raw(), which packs identically. No behaviour change.
  • rb bs-20261024-77 (#4559): internal only. 49 uses of BACnetPrimitiveObjectIdentifier::GetObjectTypeFromObjectIdentifier and GetObjectInstanceFromObjectIdentifier in source/ now read the type and instance through BACnetObjectIdentifier32 (same shift and mask). No behaviour change.
  • rb bs-20261024-79 (#4487): documentation only. The BACnetStack_RegisterHookTimeout documentation and the Acting as a client page no longer say HookTimeout fires only after every retry is exhausted: it also fires when a retry cannot be sent (the sending device may no longer initiate, or local send failures reached their cap), and a segmented request that times out while sending its segments reports through HookAbort with TSM_TIMEOUT instead. No behaviour change.
  • rb bs-20261024-8 (#4442): the documentation of BACnetStack_AddObject and BACnetStack_SetObjectTypeCreatable in CASBACnetStackDLL.h (and the bindings generated from it) now says what the calls have done since #4434 and #4441: a standard object type this build does not model is refused (false), a proprietary type (128 to 1023) is accepted by SetObjectTypeCreatable, and an out-of-range object type is refused by AddObject. The BACnetStack_SetLiftOrEscalatorEnergyMeterRef device parameter text is reworded. Documentation only (IFC-189): no behaviour changes.
  • rb bs-20261024-89 (#4559): internal only. The 3,693 test uses of BACnetPrimitiveObjectIdentifier::GenerateObjectIdentifier now pack through BACnetObjectIdentifier32::FromTrustedPair(...).Raw() (the same packing, including the 22-bit mask), and the helper is removed together with the codec’s GetObjectTypeFromObjectIdentifier / GetObjectInstanceFromObjectIdentifier. The objectIdentifier32 typedef stays until the #1876 tail lands. No customer or test-tool surface changes.
  • rb bs-20261027-1 (#4355): a NaN Low_Diff_Limit or High_Diff_Limit of a FLOATING_LIMIT algorithm (a Loop’s Error_Limit / Low_Diff_Limit, or an Event Enrollment’s FLOATING_LIMIT parameters) now makes the object’s Reliability CONFIGURATION_ERROR, and a WriteProperty of NaN to a Loop’s Error_Limit or Low_Diff_Limit is refused with PROPERTY / VALUE_OUT_OF_RANGE before the host’s Set callback is called, so the host never receives the NaN (the direct write, an armed Loop’s own algorithm write, and a Channel member write). An Event Enrollment’s FLOATING_LIMIT Event_Parameters cannot be written by a client today (the algorithm has no Event_Parameters write support), so that half of the ruling has nothing to guard yet. Check a host that stored NaN there as “disabled” — revert-red: the new tests fail on the pre-fix code.
  • rb bs-20261027-11 (#769): the Zephyr adapter’s MS/TP datalink (adapters/zephyr) now uses the stack-owned engine. It is not built or hardware-tested: it was checked only by a syntax-only compile against stub Zephyr headers, and the ESP32-S3 / BACnetProfileExample-B-BC-Zephyr token-ring checks listed in the migration notes have not been run. cbz_mstp.cpp registers the byte callbacks (all waiting octets in one ReceiveBytes, the same SendBytes body with the #4038 sleep-then-poll, a wrapping microsecond clock, the driver-enable callback), drops its frame queues and timer workaround, and runs BACnetStack_LoopMSTP() on the MS/TP thread; mstp::GetCounters() reads BACnetStack_GetMSTPCounters(). New mstp::GetOctetStringProperty, GetUnsignedProperty and GetRealProperty let the application’s property callbacks serve MAC_Address, Max_Master, Max_Info_Frames and Link_Speed from the Settings. CONFIG_CAS_BACNET_STACK_MSTP_QUEUE_DEPTH is a deprecated no-op.
  • rb bs-20261027-11 (#769): the MS/TP chapter of the manual describes byte mode and packet mode only (the engine functions, timer and engine-callback sections are gone), and the spec-coverage records mark the MS/TP wiring gap resolved.
  • rb bs-20261027-11 (#769, review): the Zephyr adapter’s cbz::mstp::Open() no longer starts the MS/TP thread; call the new cbz::mstp::Start() after the Network Port object is added (not built or hardware-tested). Open() rejects baudRate 0, the microsecond clock is exact at any counter frequency, and the transmit pointer shared with the UART interrupt is atomic.
  • rb bs-20261027-14 (#769 follow-up, found making the Zephyr example’s RS-485 port a Dashboard setting): adapters/zephyr: the RS-485 port MS/TP runs on is chosen when the datalink is opened, not at build time. cbz::mstp::Settings gains a trailing port member; the chipkin,cas-bacnet-mstp devicetree binding gains an optional integer port property; a board with several RS-485 ports declares one node per port (each on its own UART, so no pin muxing at run time) and cbz::mstp::Open() uses the node whose port equals Settings::port, and returns false (logging the number) when none does. A board with one node and no port property is unchanged: Settings::port 0 matches it. Hardware (ESP32-S3, port 3 on UART1 and port 4 on UART2): the port was switched from 3 to 4 by a stored setting and a reboot, the board joined the token ring on port 4, and a PC on that bus read it 6/6 (mstp_probe.py). The port 3 UART was opened without error but not tested on a live bus.
  • rb bs-20261027-2 (#3463): a build that defines STACK_OPTION_PROFILE_B_AS (the Authorization Server, AA-AS-B) without BACNET_STACK_TESTTOOL now stops with an #error. AuthRequest is served by the test-tool build only; such a build used to compile and then answer every AuthRequest with Reject UNRECOGNIZED_SERVICE. A STACK_OPTION_TARGET_FULL customer build is unaffected — revert-red: the new gate check-b-as-testtool-only.sh reports the profile “compiled” when the #error is removed.
  • rb bs-20261027-4 (#4053): _ci_scripts/changelog-fold.py refuses to fold a CHANGELOG fragment that quotes the standard: it checks every fragment line for a 12-word run that also appears in the extracted 135-2024 / 135.1-2023 text, prints file:line and the run, and writes and deletes nothing. A structural list is allowed with a trailing <!-- structural -->; with no local extract the check is skipped with a warning. Existing CHANGELOG.md text is unchanged — revert-red: with the check disabled the guard’s self-test fails because the quoting fragment is no longer refused.
  • rb bs-20261027-4 (#4031): _ci_scripts/review-run.py gains the promote subcommand for the code-review gate (numbered blocks, clock-dated verification, reject rows), brought onto the integration branch from the review session’s commit f5301ecb1 — revert-red: none (resolver tooling; its own self-test covers the renumbered heading, the replaced field and the found-by stamp).
  • rb bs-20261027-5 (#3468): a Confirmed or Unconfirmed EventNotification or AuditNotification whose time stamp has an out-of-range Date or Time octet (a month of 0, say) now decodes, as the COV notification decodes already did: the value keeps its raw octets, flagged, and IsValid() stays true. The confirmed form is answered with Result(+) and the host hook fires. An event notification with such a time stamp, or such a date or time inside its event values (Change-Of-Timer, Access Event, Change-Of-Discrete-Value), is not offered to the local Event Log or Notification Forwarder (they store and re-send the value, and encoding refuses it); the stack keeps no Audit Log record of a received audit notification, so there is nothing to gate there. DecodeAs also decodes requests tolerantly (it is a display path); real request processing stays strict — revert-red: removing the tolerant scope from a decode, from DecodeAs, or the local-offer gate fails its own test.
  • rb bs-20261027-6 (#4080): BACnetStack_SetSchedulePriorityForWriting now refuses a priority of 0 or 17..255 (returns false, keeps the stored Priority_For_Writing); it used to store 16 and report success. Pass 1..16. Comment-only edit of the export’s doc block (IFC-196), adapters regenerated — revert-red: removing the range check fails the new export test.
  • rb bs-20261027-6 (#3853): the Notification Forwarder re-sends a notification that does not fit the destination once without its optional Message Text (counted delivered if it then fits, logged); a refusal that can clear (no SC connection yet, port out of service, Device Communication Control disabling initiation, a short BACnet/IP host send, signalled apart from the others so the audit delivery still drops it at once) or a Device recipient not yet resolved in the DAB stays pending for 3 further Drain passes (MAXIMUM_RETRY_DRAIN_PASSES) before it counts undeliverable; an unresolved recipient is never counted as sent; for a global-broadcast recipient a port is retried only when no other port took the notification. The strip also applies to a locally originated notification that does not fit. DeliverEventNotificationToRecipient reports sent / dropped / retry — revert-red: disabling the strip, the DCC transient mark, the unresolved retry, the give-up bound or changing the bound fails its own tests.
  • rb bs-20261028-2 (#4406, slice 3 of #3165): on a BACnet/IPv6 Network Port, a write of IPv6_Address, IPv6_Prefix_Length, IPv6_Default_Gateway, IPv6_DNS_Server, IPv6_Auto_Addressing_Enable, BACnet_IPv6_UDP_Port or BACnet_IPv6_Multicast_Address is held pending until ReinitializeDevice ACTIVATE_CHANGES or WARMSTART, as #4404 does for the BACnet/IP properties: Changes_Pending reads TRUE, a read returns the written value, and the host’s Set callback is called at activation. An address that is not 16 octets, a multicast address that is not 18 (the address and the UDP port, cl. 12.56.48), a prefix length outside 1 to 128 or a UDP port over 65535 is refused with VALUE_OUT_OF_RANGE; IPv6_Address, IPv6_Prefix_Length, IPv6_Default_Gateway and IPv6_DNS_Server are refused with WRITE_ACCESS_DENIED while IPv6_Auto_Addressing_Enable reads TRUE (Table 12-71.4 footnote 4); an IPv6 property written to a port that is not BACnet/IPv6, or an IPv4 one to a BACnet/IPv6 port, is refused with WRITE_ACCESS_DENIED. An IPv6_DNS_Server the host does not serve reads as one entry of 16 zero octets, which cl. 12.56.49 defines as not configured. IPv6_Zone_Index is not held pending (it is a CharacterString, which the pending store does not hold yet), and BACnet_IPv6_Mode stays read-only, as BACnet_IP_Mode is.
  • rb bs-20261028-3 (#4612): RENEW_FD_REGISTRATION on a BACnet/IPv6 Network Port whose mode in use is FOREIGN now sends the registration again (INVALID_VALUE_IN_THIS_STATE while a change is pending, NOT_ENABLED when FOREIGN with no BBMD to register with; a send that fails is retried by the data link); in NORMAL or BBMD it is still refused with VALUE_OUT_OF_RANGE. Before, it was refused on every IPv6 port.
  • rb bs-20261028-7 (#4643): the manual’s BACnet MS/TP chapter has its two diagrams again, redrawn for the stack-owned engine: the bus and token ring with the byte callbacks and the main loop’s BACnetStack_LoopMSTP() and BACnetStack_Tick(), and a ReadProperty sequence with the reply deadline (an answer at once within 125 ms, otherwise Reply Postponed). The diagrams that drew the removed MSTP.h engine are deleted.
  • rb bs-20261029-1 (#4602): the BACnetStack_SetPropertySubscribable documentation now says it does not enable COV_Increment, which Analog Input, Analog Output, Analog Value, Integer Value, Positive Integer Value, Large Analog Value, Loop, Lighting Output, Pulse Converter and Staging objects need when they report COV (cl. 12.2.16); enable it with BACnetStack_SetPropertyEnabled and serve its value. Documentation only, no behaviour change.
  • rb bs-20261029-2 (#4487, CR4-F05-008): a stray client Segment-ACK or a stray segment of a Confirmed-Request, received while no transaction is open, is now answered with Abort INVALID_APDU_IN_THIS_STATE (was SEGMENTATION_NOT_SUPPORTED), the reason the other server states use for the same out-of-place PDU (cl. 5.4.5.1, owner ruling on #4570). A device that does not receive segments still answers the first segment of a segmented request with SEGMENTATION_NOT_SUPPORTED. Hosts that read the abort reason in the peer’s logs will see the new value.
  • rb bs-20261030-1 (#4602): BACnetStack_SetPropertySubscribable on Present_Value now also enables COV_Increment for the object types that require it when they report COV (Analog Input/Output/Value, Integer Value, Positive Integer Value, Large Analog Value, Loop, Lighting Output, Pulse Converter, Staging), as the owner ruled. A COV_Increment the host does not serve reads 1.0 (was a fabricated 0.0). A host that does not want the property calls BACnetStack_SetPropertyEnabled(..., COV_Increment, false) afterwards.
  • rb bs-20261030-10 (#3895 items 2, 3, 4; owner ruling of 2026-10-26): a LifeSafetyOperation request without an Object Identifier is now reported to the audit reporter as ONE LIFE_SAFETY record whose Target Object is the device’s own Device object (the Requesting Process Identifier and Request as the Target Value, the Requesting Source as the Source Comment); it used to be reported to nobody. The record is queued only by a reporter that monitors the Device object. SubscribeCOVPropertyMultiple stays unaudited, and a ReinitializeDevice that ends a communication disable stays DEVICE_RESET only, both as Table 19-5 maps them (now recorded in comments at the sites). The NOTIFICATION operation (item 1) is not done yet.
  • rb bs-20261030-2 (#4014, owner ruling 2026-10-26 option A): on BACnet/IP, a local broadcast the stack chooses itself now reaches CallbackSendMessageForPort as broadcast = true with a NULL connection string of length 0, where it used to carry this BBMD’s own address, the requester’s address, or the last I-Am sender’s address: a BBMD’s local re-broadcast of a Forwarded-NPDU (two-hop) and its execution of a Distribute-Broadcast-To-Network (one-hop and two-hop), a broadcast answer to a request (I-Am, I-Have, I-Am-Router-To-Network, Network-Number-Is), and the periodic device-address-binding Who-Is. A host that follows the documented rule (a broadcast with no connection string goes to the port’s B/IP broadcast address) needs no change; a host that sends to the string, or dereferences it, must handle NULL. A string the host passed to its own BACnetStack_Send* call, and every unicast, are unchanged. BACnetStack_SendWhoIs and SendWhoIsWithLimits accept a NULL / 0 connection string for a BACnet/IP broadcast.
  • rb bs-20261030-5 (#4608, slice 2): in a STACK_OPTION_THREAD_SAFE build, a host callback that calls BACnetStack_RemoveDevice or BACnetStack_RemoveObject is refused (they return false and the device or object stays), and a callback that calls BACnetStack_Reset is ignored with a logged error; the stack frees nothing the outer call may still be walking. The Send* exports are not yet on the list of exports a callback may call. A build without the option is unchanged (the single-threaded contract is still the ban). In every build, the random first invoke id of a new peer now comes from the stack’s own generator instead of the C library’s rand() (the host’s rand() stream is no longer consumed), and BACnetDateTime::DateTimeFromTimeT converts with gmtime_r / gmtime_s into storage of its own instead of the shared gmtime() buffer. Still to come for #4608: the threading-contract documentation and the TSAN preset with its stress test.
  • rb bs-20261030-6 (#4608, slice 3): documentation of the STACK_OPTION_THREAD_SAFE build’s threading contract in the README (“Threading model”), the API conventions page and the Node section: any thread may call any export, calls are serialised, callbacks run with the lock held and must not block or wait on a thread that is waiting to call the stack, a callback may not re-enter the stack (the removals are refused, a reset is ignored; only the two BACnet/SC certificate exports and BACnetStack_SetBACnetSCWebSocketStatus may be called from a callback), BACnetStack_LoopMSTP runs outside the lock, and an RTOS port supplies the lock with STACK_OPTION_THREAD_SAFE_CUSTOM. The Node addon is not built thread-safe and worker_threads share one stack. The two Network Port callback notes in CASBACnetStackDLL.h now point to the conventions page instead of a block that no longer exists (comment lines only; IFC-202). Still to come for #4608: the TSAN preset and stress test.
  • rb bs-20261030-8 (#4508 CR4-F06-016, owner ruling on #4571): the router’s search-broadcast bucket and its per-source Reject-Message-To-Network limiter now refill from the wall clock alone whenever a system-time callback is registered, so neither can run at a multiple of its configured rate when Loop() is called fast; the Tick count refills them only when no clock is registered. A device with a registered but frozen clock gets the initial burst of route searches and rejects and then no more until the clock advances.
  • rb bs-20261030-9 (#4606, test-tool only): the AuthRequest server (AA-AS-B) no longer issues unsigned access tokens. Tokens issued from an Authorization Policy now name signing key 1 and carry an ES256 signature that the application produces through the new BACnetStackTestTool_RegisterCallbackSignAccessToken callback (C++: BACnetTestToolInterface::RegisterCallbackSignAccessToken); the stack has no cryptography library and passes the callback the encoded token octets that precede the signature. With no callback registered the request is refused (SERVICES / OPTIONAL_FUNCTIONALITY_NOT_SUPPORTED), and a callback that fails or returns no usable signature gets SERVICES / SERVICE_REQUEST_DENIED. A host that used the policy path must register a signer.
  • rb bs-20261104-gitlab-removal: comments in source/ and tests/ and one README.md line no longer link to or name GitLab, which Chipkin is retiring; each comment keeps its explanation in words, and the historical issue numbers that test names still carry are described as the pre-GitHub tracker’s. The cas-common provenance line in the four ChipkinConvert/ChipkinUtilities files keeps the project name and drops the URL. Comments only, no behaviour change. Historical changelog entries are left as written.
  • docs (#3947): the manual’s chapters link the first mention of each BACnetStack_* export in a section to its API Reference page; the links come from a name-to-URL map the API Reference generator writes, so a renamed topic cannot leave a dead link.
  • rb dv-20261017-1 (#2578): a BACnet/SC hub connector waits out the reconnect timeout (and counts the attempt toward the increasing timeouts) after every unsuccessful connection attempt, as cl. AB.6.1 requires: a Connect-Request NAK, a NODE_DUPLICATE_VMAC NAK, the connect wait timeout, or the WebSocket closing before the Connect-Accept. It used to re-dial the hub on the next Tick(), so a hub that NAKs every Connect-Request received a new TLS handshake every two ticks from every node. The loss of an established connection still re-dials at once.
  • rb dv-20261017-1 (#4024): an ACCESS_EVENT notification fires only when the access event time changes (cl. 13.3.12). A new Access_Event_Tag, or a different Access_Event, at the time last notified no longer re-notifies (IFC-119). With an unspecified time (a device with no valid clock) the tag and Access_Event still tell events apart.
  • rb dv-20261017-11 (#3088 CR2-F01-006): BACnetStack_UpdateValue bumps Database_Revision only after its checks pass; a refused call leaves the revision unchanged (IFC-128).
  • rb dv-20261017-11 (#3088 CR2-F01-015): the DeviceCommunicationControl and ReinitializeDevice password limit (1..20) is counted in characters, not octets. A NULL password with a non-zero length is refused, and nothing is sent.
  • rb dv-20261017-11 (#3089 CR2-F02-019): BACnetStack_SetAlarmsAndEventsForObjectEnabled refuses a notifyType other than ALARM or EVENT when enabling.
  • rb dv-20261017-11 (#2850 CR2-F28-013): a Schedule Effective_Period with an endpoint that is neither an open bound nor a real date in 1900..2154 is refused, and nothing is stored.
  • rb dv-20261017-11 (#3715 CR3-F13-009): a ReinitializeDevice STARTBACKUP/STARTRESTORE the stack cannot serve answers Error SERVICES / OPTIONAL_FUNCTIONALITY_NOT_SUPPORTED instead of Abort(other).
  • rb dv-20261017-11 (#3718 CR3-F16-003): a LifeSafetyOperation request with a reserved request value answers Error OBJECT / VALUE_OUT_OF_RANGE instead of being Rejected.
  • rb dv-20261017-13 (#3677 CR3-F05-031): a client Abort reaching the responding state machine’s IDLE state is no longer dispatched to the business logic twice (IDLE only enters IDLE; the data link’s dispatch is the single delivery). No hook fired for it either way, so applications see no difference.
  • rb dv-20261017-13 (#3677 CR3-F05-003, -022, -028): tests pin every BACnetErrorCode (211 current, 18 former), every BACnetErrorClass and BACnetAbortReason::inconsistentAttributes to their cl. 21.4 values; cover BACnetRawUnconfirmedServiceRequest’s payload cap, Decode and assignment; and check that cloning an Unconfirmed-Request PDU keeps its typed or raw service request byte for byte. Comment and citation corrections (CR3-F05-004, -010, -014, -016, -023, -036).
  • rb dv-20261017-16 (#4064, owner ruling 5): a CallbackReinitializeDevice refusal with an error code outside the six the stack maps by name is answered with a Result(-), not Abort(other). A code that can be sent as an Error Code gets its clause-18 class from the shared callback-code mapping. No code, OTHER, a value above 65535, or an abort-type or reject-type code is answered with SERVICES / SERVICE_REQUEST_DENIED. ABORT_OTHER still asks for an Abort(other).
  • rb dv-20261017-16 (#4065): a DeviceCommunicationControl or ReinitializeDevice password that is not well-formed in its declared character set (malformed UTF-8) is refused. The senders (BACnetStack_SendDeviceCommunicationControl, BACnetStack_SendReinitializeDevice) return false and send nothing, and a received request carrying one is rejected, as an out-of-range password already is. Before, “A” followed by 79 continuation octets counted as one character and passed.
  • rb dv-20261017-16 (#4065): BACnetStack_SCCertificateAddIssuer, BACnetStack_SCCertificateReplaceOperational and BACnetStack_SCCertificateRemoveIssuer return false for a NULL password with a non-zero length, and a password the final ReinitializeDevice step would refuse, before sending anything. Before, the NULL password became “no password”, and an over-long password failed only at the last step, after the certificate writes had gone out.
  • rb dv-20261017-16 (#4064, #4065): the FPCallbackReinitializeDevice documentation in BACnetCallbackInterface.h, and the password and refusal documentation in CASBACnetStackDLL.h, now describe the behaviour above; the C#, Java, Python and Rust bindings that copy those doc comments were regenerated, and the Node api.ts SC certificate comment lists the new refusals.
  • rb dv-20261017-2 (#3681 CR3-F09-040): the hub connector’s seven HandleHubConnector* test seams are compiled only into test-tool and unit-test builds; customer builds no longer contain them (no customer interface named them).
  • rb dv-20261017-2 (#3681 CR3-F09-004, -039; #4029): source comments only - BACnet/SC clause citations name each message’s own format clause, and five hub-connector comments now describe what the code does.
  • rb dv-20261017-3 (#3757 CR3-F17-011, -018; #2746 CR2-F17-011): CASBACnetStackDLL.h documentation only (IFC-121). The intrinsic-algorithm exports now say what the code does. The UNSIGNED_RANGE deadband and the FLOATING_LIMIT limit enables are ignored. A CHANGE_OF_STATE Bool call replaces its alarm value. An Enum call replaces it on Binary objects and adds to the list on Access Door, Load Control and Access Zone. An Unsigned call adds. CHANGE_OF_CHARACTERSTRING matching follows cl. 13.3.16 (substring, empty and Null cases). Behaviour is unchanged.
  • rb dv-20261017-4 (#3995): the Go adapter in adapters/golang/ can now be imported.
    • It is the package casbacnetstack. An application sets its Callback* function variables, calls RegisterBACnetStackCallbacks(), and builds with only CGO_LDFLAGS naming the library.
    • The example device is now its own main program in adapters/golang/example/.
    • The default link line now adds -lstdc++ -lm, which a static link needs. Building with -tags casbacnetstack_nodefaultlink leaves the default out, so another library name can be supplied.
    • adapter-signature-gate.js now also checks the //export stubs and the C trampolines.
  • rb dv-20261017-5 (#2413, #3057, #2923, #2928, #3175): CASBACnetStackDLL.h documentation only (IFC-122). The Network Port docs now say:
    • Network_Number is held pending until activation and needs the Set and Reinitialize callbacks. A host Get callback for Network_Number or Changes_Pending is still served first; this text describes today’s code and changes when #2413 item 2 (stack-owned Network_Number and Changes_Pending) is implemented.
    • BBMD_Accept_FD_Registrations starts TRUE and is writable only on a BBMD. A write is held pending until activation, and while the value in use is FALSE every Register-Foreign-Device is NAK’d.
    • Two paths renumber a virtual network. The other corrections:
    • SetMaxActiveCOVSubscriptions and SetCOVSettings set both COV tables, so call SetCOVMultipleSettings after them.
    • A Schedule’s Exception_Schedule is served and writable, and AddScheduleExceptionEvent* fails at 255 events.
    • Log_Buffer reads answer Error(PROPERTY, READ_ACCESS_DENIED).
    • A ReadRange count of 0 is refused.
    • The full-table I-Am-Router-To-Network lists the networks reachable other than through the sending port. Behaviour is unchanged. The same corrections are in api.ts and two manual pages.
  • rb dv-20261017-6 (#2392): BACnetStack_ReadTrendLogRecord and BACnetStack_ReadTrendLogMultipleRecord now return timestamps as a UTC epoch. The Device’s current UTC_Offset (and Daylight_Savings_Status) is applied on export, matching the Insert exports, so a backup restores exactly (IFC-123). Records stored under the other daylight-saving state export one DST shift off. Behaviour change for a host that serves a non-zero UTC_Offset: Read used to return local time.
  • rb dv-20261017-6 (#2773): a Null Trend Log datum reads out as "null", not "", so a successful read is never 0. Insert still accepts "" as Null. A Null element inside a Trend Log Multiple record is still "". An empty bitstring reads out as "b" in both.
  • rb dv-20261017-7 (#2726): a Lighting Output with intrinsic reporting applies the NONE event algorithm, as cl. 12.54 requires, and OctetString Value no longer has intrinsic reporting (cl. 12.41 gives it none) (IFC-124).
    • Behaviour change: BACnetStack_SetIntrinsicChangeOfValueAlgorithm (Lighting Output) and BACnetStack_SetIntrinsicChangeOfDiscreteValueAlgorithm (OctetString Value) now return false. Both are documented as deprecated for intrinsic use.
    • Both algorithms remain available to Event Enrollment.
  • rb dv-20261017-7 (#3037): BACnetStack_SendYouAre sends a Device MAC Address from any port, not only from an MS/TP one. The clause leaves that choice to the receiver, so a BACnet/IP workstation can now assign an MS/TP MAC behind a router.
  • rb dv-20261017-7 (#4046): BACnetStack_SetAlarmsAndEventsForObjectEnabled attaches the NONE event algorithm to an object whose intrinsic algorithm is NONE, so a Lighting Output’s or Binary Lighting Output’s Event_State shows FAULT while a Reliability fault is held. Before, it read NORMAL while TO_FAULT went out. A re-enable keeps an armed algorithm. Behaviour change: enabling an OctetString Value now returns false.
  • rb dv-20261017-8 (#2413): a Network Port’s Network_Number and Changes_Pending are served by the stack only, per the 2026-09-25 owner ruling (IFC-125). Behaviour change: a host Get callback for either property is now ignored. After BACnetStack_UpdateVirtualNetworkNumber, no host-side path updates the port’s Network_Number any more; only a BACnet client’s write of it does. That consequence is raised with the owner on a follow-up issue.
  • rb dv-20261017-8 (#4023): an Access Point’s own ACCESS_EVENT notification carries the fired event’s Access_Event_Credential (cl. 12.31.30), not the credential the algorithm was armed with.
  • rb dv-20261018-18 (#4081): a WriteProperty of an Event Enrollment’s Fault_Parameters with a FAULT_STATE list that uses the integer-value [41] BACnetPropertyStates choice is still refused, but now answers PROPERTY / OPTIONAL_FUNCTIONALITY_NOT_SUPPORTED (was VALUE_OUT_OF_RANGE): the stack does not support that option for FAULT_STATE. The other refused choices (for example extended-value [63]) still answer VALUE_OUT_OF_RANGE. A client that matched on VALUE_OUT_OF_RANGE for this write should check for the new code. Revert-red: Issue4081_WritingAnIntegerValueFaultStateListIsRefusedAsOptionalFunctionalityNotSupported got 37 where 45 was expected on the pre-fix code. A list that mixes integer-value with another choice, or an oversized list containing one, also gets OPTIONAL_FUNCTIONALITY_NOT_SUPPORTED now (the integer-value check runs first).
  • rb dv-20261018-18 (#4082): the change-of-state, change-of-bitstring, change-of-characterstring, change-of-life-safety (both lists) and access-event event-parameter setters (Insert*AndTakeControl) refuse an element that is not valid, free it and return false, as change-of-timer’s already did; before, it was stored and the whole alternative silently became invalid. No production caller passes an invalid element, so nothing changes on the wire. Revert-red: EveryListAlternativeAcceptsEmptyAndRefusesABadElement failed on the pre-fix code at all six setters (each accepted the invalid element).
  • rb dv-20261018-19 (#4089): a Channel List_Of_Object_Property_References element whose device identifier is not of object type Device is now refused with PROPERTY / VALUE_OUT_OF_RANGE on every write path, not only WriteProperty, and the list is left unchanged.
  • rb dv-20261018-20 (#4079): an object whose own Audit_Level is AUDIT_ALL or AUDIT_CONFIG is now audited while its Audit Reporter is at NONE, as 135.1-2023 7.3.1.29.1 steps 6-9 expect (135-2024 cl. 19.6.3 reads the other way; the test standard is followed); before, a reporter at NONE silenced every object it reported for, whatever the object’s own level. A reporter at NONE still silences objects that take its level (no Audit_Level, or DEFAULT) and writes to the reporter itself. A host that set a reporter to NONE to silence objects with their own level should set those objects to NONE. Revert-red: on the pre-fix BACnetDBDevice.cpp, Issue4079_ObjectAuditLevelDecidesUnderAReporterAtNone got 0 notifications where 1 was expected (step 6), and ObjectAuditLevel_OverridesTheReporterUnlessDefault got false for the object’s AUDIT_ALL under a reporter at NONE.
  • rb dv-20261018-20 (#4075): a value the stack decodes but does not model (BACnetStackUnknownAPDU, for example a context-tagged primitive inside an ABSTRACT-SYNTAX.&Type such as a command-failure command-value) now re-encodes to the octets it was decoded from, so it can be stored, relayed and read back; before, its Encode always refused, so the stack accepted values it could not send. Responses that now carry the decoded context-tagged or unknown value instead of failing: ReadProperty and ReadPropertyMultiple of a stored proprietary or unknown-datatype value, COV notification values, and an audit notification’s Target Value. An object with no decoded tag is still refused with an abort reason. Revert-red: on the pre-fix BACnetStackUnknownAPDU.cpp, Issue4075_ContextTaggedPrimitiveInACommandValueReencodesToItself re-encoded 0 of 19 octets, Issue4075_DecodedValueEncodesToTheSameOctets and Issue4075_ContextTaggedValueReencodes were refused, and the #4058 command-failure sweep reported “accepted a value it cannot encode”; with only the extended-length flag fix reverted, the 6-octet context sample encoded 7 octets (as an opening tag) instead of 8.
  • rb dv-20261018-21 (#4101): a Notification Class Recipient_List entry hands a notification this device generated to its local Notification Forwarders only when it names this device with a Device identifier. Before, any device identifier carrying this device’s instance did, whatever its object type.
  • rb dv-20261018-21 (#4015): on MS/TP, the Who-Is the stack broadcasts to find a device it has not yet heard from (the device address binding heartbeat) now reaches the send callback with connection string 0xFF (the MS/TP broadcast MAC) and broadcast false, the form a host uses to broadcast on MS/TP through the API. The stack’s own internal MS/TP broadcasts (routed fan-out, cl. 6.4.2 replies) still reach the send callback as an empty connection string with broadcast true, so an MS/TP send callback must handle both. Before, it carried the MAC of the last station whose I-Am the stack received, so only that station received the Who-Is. BACnet/IP is unchanged: the frame is an Original-Broadcast-NPDU passed with broadcast true and the last I-Am sender’s address, which the host sends to the port’s broadcast address (see BACnetStack_RegisterCallbackSendMessageForPort). BACnet/SC is unchanged: the last I-Am sender’s connection string with broadcast true and DNET 0xFFFF, which the BACnet/SC data link sends to the hub.
  • rb dv-20261018-22 (#4076): ReadRange reference indexes, reference sequence numbers and First Sequence Number are Unsigned64 (cl. 15.8.1.1.4.1.1, 15.8.1.1.4.2.1, 15.8.1.2.7 ask this of a device with an Audit Log, whose Total_Record_Count is Unsigned64). A ReadRange request whose By Position or By Sequence Number reference is 5 to 8 octets is decoded (it was refused Reject INVALID_TAG), and a ReadRange-ACK whose First Sequence Number is 5 to 8 octets is decoded (it was refused). A reference or First Sequence Number longer than 8 octets is still refused. ReadRange of an Audit Log’s Log_Buffer (test-tool builds) now answers By Sequence Number and By Time reads whose records are numbered above 2^32-1, reporting First Sequence Number at the length its value needs; they were refused DEVICE / OPERATIONAL_PROBLEM. A By Position or By Sequence Number reference above 2^32-1 to a Trend Log, Trend Log Multiple or Event Log names no record, so the answer is an empty Result(+). Before, such a request could not be decoded. BACnetStack_SendReadRangeWithSequenceNumber still takes a 32-bit sequence number. In the C++ classes, BACnetReadRangeRequest adds GetReference64, SetRangeByPosition64 and SetRangeBySequenceNumber64, and BACnetReadRangeACK adds GetFirstSequenceNumber64 and SetFirstSequenceNumber64. The existing 32-bit accessors return the low 32 bits.
  • rb dv-20261018-23 (#2604): SC_Direct_Connect_Connection_Status and SC_Hub_Function_Connection_Status now keep a connection’s entry after it ends (owner ruling 2026-10-18; cl. 12.56.93 and 12.56.98 leave how long to the device). The entry keeps its final state - NOT_CONNECTED after a successful disconnection by either side, DISCONNECTED_WITH_ERRORS when it ended with an error (for example after a websocket error, a heartbeat timeout, a disconnect-ack timeout or a refused Disconnect), FAILED_TO_CONNECT for an initiated attempt that failed - with its Disconnect Timestamp (the Connect Timestamp for a failed attempt) and, for an error, its Error and Error Details (a Connect-Request or Disconnect-Request NAK reports the NAK’s own). A peer that connects again replaces its entry (same Peer UUID, or the same URI for an initiated attempt). Each list keeps at most 8 finished entries (BACNET_SC_MAX_FINISHED_CONNECTION_STATUSES), dropping the oldest first; live connections are never dropped. Before, an entry disappeared when its connection ended, so the Disconnect Timestamp and the error were never readable. A port restart, a configuration change or taking the port out of service ends its connections as NOT_CONNECTED entries. The live initiated direct connection is listed as CONNECTED from its Connect-Accept until it has finished disconnecting: a websocket that had closed cleanly is no longer shown as DISCONNECTED_WITH_ERRORS. An initiating attempt that is not yet connected is still not listed (#4113).
  • rb dv-20261018-23 (#4104): an event notification is no longer sent to a Notification Class or Notification Forwarder recipient whose device identifier is not of object type Device; such an identifier names no device. Before, it was resolved by its instance number alone, so (analog-input, 2005) was sent to Device 2005 whenever another part of the stack held an address binding for that device. A Notification Forwarder’s Subscribed_Recipients entry now keeps its object type when the forwarder delivers to it, instead of becoming a Device identifier. Such recipients are still accepted when the Recipient_List is written, as the BACnetRecipient datatype allows any object identifier.
  • rb dv-20261018-24 (#3678 CR3-F06-007, owner ruling #3877 item 1): a router answers an NPDU that fails to decode on its DLEN or SLEN with a Reject-Message-To-Network reason 6 only when the frame was received as a link unicast and names a DNET other than X’FFFF’ (cl. 6.6.3.5). A malformed frame with no DNET, with the global broadcast DNET, or received as a link broadcast is now dropped silently; every enabled router on the segment used to answer it.
  • rb dv-20261018-24 (#3678 CR3-F06-008, owner ruling #3877 item 2): no device answers an Initialize-Routing-Table that carries a routing table (Number of Ports above 0), because none applies it. A virtual router (gateway), including one that is also a physical router, used to return a data-less Initialize-Routing-Table-Ack; a physical-only router was already silent. The query form (Number of Ports 0) is answered as before.
  • rb dv-20261018-24 (#3678 CR3-F06-023, owner ruling #3877 item 3): with CallbackGetSystemTime registered and advancing, the router’s congestion indication (cl. 6.6.3.6’s 30 s), its held messages (TOmin) and its learned next-hop MACs expire on the wall clock. The Tick count (STACK_OPTION_ROUTER_TICKS_PER_SECOND) expires one only when the clock has stalled: it still reads the second the entry was stamped at and the router has not seen it change for the larger of the Tick bound and twice the bound at the BACnetStack_Loop() rate it measured from the clock’s own steps (no clock registered counts as stalled). Before, a BACnetStack_Loop() faster than that rate cut those windows short; at 50 kHz the 30 s indication cleared after 3 s, and a held message was released 0.2 s into TOmin. The search-broadcast refill is unchanged.
  • rb dv-20261018-24 (#3678 CR3-F06-028): the seven test-tool event-notification senders (BACnetStackTestTool_SendConfirmedEventNotification, ...WithTimestampForm, ...WithEventValues, ...WithOutOfRangeValues, and the three unconfirmed ones) set the NPDU network priority from the event priority (cl. 13.2.5.4 Table 13-6), as the stack’s own notifications do. They sent every one at Normal.
  • rb dv-20261018-25 (#3679, CR3-F07-017): a BACnet/IP port that is not a BBMD no longer answers a BBMD-only BVLL request (Write-Broadcast-Distribution-Table, Read-Broadcast-Distribution-Table, Register-Foreign-Device, Read-Foreign-Device-Table, Delete-Foreign-Device-Table-Entry, Distribute-Broadcast-To-Network) that was sent to a broadcast address; it drops it (owner ruling on #3877). A broadcast address is 255.255.255.255 or the port’s own directed broadcast, taken from the destination the host reports with the received frame (CallbackReceiveMessageForPort). Annex J.2.1 requires the NAK only for a request sent by unicast, and answering a broadcast one let a single spoofed frame draw a NAK from every such device on the subnet. A unicast request, and a request for which the host reports no destination, is still answered with the NAK. A BBMD answers as before.
  • rb dv-20261018-25 (#3679, CR3-F07-024): the internal class-level Network Port SetNetworkPortProperty writes no property any more (it had no caller in the stack). It wrote Network_Type, Protocol_Level, Reference_Port and BACnet_IP_Mode at once and narrowed a written enumerated value above 255 before its range check. Property writes go through the device, which holds them pending where the standard requires it. No public API changes.
  • rb dv-20261018-p4099 (#4099): a property read looks the property’s profile up once instead of up to three times. GetProperty hands the profile it already has to the generated-value path, which used to look it up again to ask whether the property is an array and a third time to build the array. Looking up an object’s per-instance property settings is also one map lookup instead of two: it no longer asks whether the object exists first, because those settings are removed with the object (a new test pins every path that adds, moves or removes them). B-BC reference device, instructions per request: ReadPropertyMultiple of 20 properties 302,404 to 278,176 (-8.0%), ReadProperty 54,244 to 47,795 (-11.9%), WriteProperty 53,050 to 47,885 (-9.7%), Who-Is 46,782 to 40,851 (-12.7%), ReadRange 183,995 to 177,032 (-3.8%), idle Tick 22,419 to 18,250 (-18.6%). No behaviour change.
  • rb dv-20261019-1 (#4060, part): an idle BACnetStack_Tick() no longer reads the Device’s UTC offset for Trend Log and Trend Log Multiple objects that are not due and need no local time to decide. A log asks for the offset only when its decision needs it: a Start_Time or Stop_Time to compare, Align_Intervals, or a record to stamp. A Schedule whose evaluation lands on the value Present_Value already holds keeps it, instead of replacing it with a copy every tick, and the Schedule engine no longer copies Present_Value every tick to detect a change. Hosts whose UTC_Offset or Daylight_Savings_Status callbacks have side effects see fewer calls. Nothing logged, stamped or written changes.
  • rb dv-20261019-10 (#1839, owner ruling 2026-09-23): a You-Are that matches this device’s identity and carries a valid MS/TP MAC address now assigns that MAC to the receiving MS/TP Network Port, as cl. 16.11.4 allows when the MAC is changeable. The MAC becomes the port’s pending MAC_Address, as a written Network Port value does: Changes_Pending reads TRUE, a read of MAC_Address serves the assigned value, and DISCARD_CHANGES drops it. At ReinitializeDevice ACTIVATE_CHANGES or WARMSTART, or at BACnetStack_ActivateNetworkPortPendingChanges(), the stack first reads the current MAC through CallbackGetPropertyOctetString, if you registered it. It then calls CallbackSetPropertyOctetString(deviceInstance, 56 (Network Port), portInstance, 423 (MAC_Address), value, 1, false, 0, 0, &errorCode), a new kind of call for your host. Return false (setting errorCode) to refuse it: the activation then fails, the MAC stays pending, and any MAC already accepted in that pass is handed back to you with its previous value (when that value could be read). Keeping the MAC across restarts is your host’s job. A host that has not registered the octet-string Set callback or the ReinitializeDevice callback sees no change, and the MAC stays as it is. BACnet/IP ports (whose MAC is IP_Address and UDP_Port) and BACnet/SC ports are unchanged.
  • rb dv-20261019-100 (#4049): BACnetStack_UpdateVirtualNetworkNumber now also sets the Network_Number of every virtual Network Port object on the renumbered network, as its active value: the main device’s (from AddVirtualNetwork) and each virtual device’s (from AddDeviceToVirtualNetwork). It used to leave the old number there (#973), so the device advertised a network it no longer routed (cl. 12.56.13). A pending Network_Number a client wrote is kept. Doc-only header change: IFC-183; the virtual-devices guide is updated.
  • rb dv-20261019-101 (#4358): a client’s Network_Number write on a virtual network’s Network Port, once activated, now also sets the Network_Number of each virtual device’s VIRTUAL Network Port on that network (cl. 12.56.13), as BACnetStack_UpdateVirtualNetworkNumber does since #4049. They used to keep the old number.
  • rb dv-20261019-101 (#4309): the fifteen test-tool typed constructors (BACnetStackTestTool_Add*Object for Lighting Output, Binary Lighting Output, Timer, Averaging, Load Control, Life Safety Point and Zone, Access Door, Credential, Rights, Zone and Point, Credential Data Input, Alert Enrollment and Program) now add their BACnet object when it does not exist. They used to require BACnetStack_AddObject first (owner ruling on #893). A refused call removes the object it added; the two-step order keeps working. IFC-184.
  • rb dv-20261019-102 (#3411 item 1, slice 1): a virtual device’s Schedule, Timer and Channel writes to an object in a remote device are now sent. They go through the interface that owns the virtual device, and they go out as the virtual device: SNET is its virtual network and SADR its MAC (cl. 6.5.4), as a device behind the router would send them. They used to be skipped and logged, because a virtual device had no send path. Co-hosted devices now share learned address bindings: an I-Am the main device receives also binds the sender in each hosted device that waits for it, and that device’s kept writes go out, as itself. A device that may not initiate (unconfigured, or DeviceCommunicationControl DISABLE_INITIATION) keeps its writes. Writes between two virtual devices of the same router are a later slice.
  • rb dv-20261019-103 (#3411 item 1, slice 2): a Schedule, Timer or Channel write to a device hosted on the same virtual router is now delivered internally. Such a write comes from one of its virtual devices, or from the router device. It is addressed from the virtual-router table (DNET the target’s virtual network, DADR its instance) and tracked under the internal source; the target executes the WriteProperty and its answer ends the transaction. Nothing goes to the wire. A hosted device is never learned into the address binding, so such a write used to wait, kept, for a binding that never came. Routers whose only port is MS/TP or BACnet/SC, and writes to the router device itself, are #4364.
  • rb dv-20261019-104 (#4364): two follow-ups to #3411’s co-hosted reference writes.
    • A virtual device’s Schedule, Timer or Channel write to the router device is now delivered internally, as are its other messages to the router device (a device-form event notification, for one). The router device is never learned into a virtual device’s address binding, so the write used to be kept for good. It is named by the router’s BACnet/IP port network and MAC, the SNET/SADR the router device’s answer carries, so the answer ends the transaction.
    • On a router with no BACnet/IP port claimed (only MS/TP or BACnet/SC), a confirmed request to a co-hosted device names no port and is sent as BACnet/IP, as a co-hosted event notification is. It used to be refused as an ambiguous port, and the write was lost.
  • rb dv-20261019-105 (#4336): the BACnet/SC direct-connect accepting side now sends a Disconnect-Request to a CONNECTED accepted direct connection before it closes the connection itself (AB.6.2.3, as the hub function does since #3663; owner ruling on #3524: send it, then close in the same call). The closes covered are a Hello capabilities change, disabling direct-connect accept, clearing the accept URIs, the listener shutdown in Loop, and SuspendCommunication. The peer used to see an unannounced WebSocket close. A connection that never reached CONNECTED gets none.
  • rb dv-20261019-106 (#3261): a Notification Forwarder target that names the owning device (a Device identifier with this device’s instance) now hands the notification to the device’s other Notification Forwarders. It used to be dropped silently, because the device’s own instance never resolves in its address binding. Cl. 12.51 lets forwarders be chained in one device. The receiving forwarders apply their Process_Identifier_Filter, their other acceptance rules and the loop-prevention rules, judged on the parameters the notification arrived with. It is never handed back to the sender, and it makes at most one in-device hop (a modelling decision).
  • rb dv-20261019-106 (#4261): documented that a forwarder’s targets are fixed when it processes (accepts) the notification (cl. 12.51.8); no behaviour change.
  • rb dv-20261019-107 (#4338): a device that may not initiate (unconfigured, cl. 19.7, or with DeviceCommunicationControl DISABLE_INITIATION, cl. 16.1.2) no longer resends its confirmed requests or their segments.
    • Retries and segment retransmissions go straight to the send callback, past the data link’s gates. A transaction now records the device it was sent as, and while that device may not initiate the transaction ends at its next expiry as a timeout (HookTimeout, or HookAbort for a segmented request).
    • The Reject or Error a BACnet/IP or MS/TP data link makes while decoding a malformed request addressed to this device is now sent as that device, so the same gates hold it. An unconfigured device no longer answers a malformed request.
  • rb dv-20261019-116 (#3902): a ReadProperty of an Event Enrollment’s Event_Parameters now answers PROPERTY / VALUE_NOT_INITIALIZED (cl. 18.3) instead of UNKNOWN_PROPERTY while the event algorithm’s required property reference is not yet set. This applies to FLOATING_LIMIT, ACCESS_EVENT, CHANGE_OF_LIFE_SAFETY, CHANGE_OF_TIMER and COMMAND_FAILURE. Fault_Parameters does the same for FAULT_LISTED, FAULT_STATUS_FLAGS and FAULT_LIFE_SAFETY with no reference yet. UNKNOWN_PROPERTY said the property did not exist, although it is in Property_List.
  • rb dv-20261019-118 (#4385): an Event Enrollment’s Fault_Parameters now answers PROPERTY / VALUE_NOT_INITIALIZED (cl. 18.3) instead of UNKNOWN_PROPERTY in two more cases: a FAULT_STATE algorithm whose BACnetPropertyStates choice is not yet set, and a FAULT_EXTENDED algorithm whose vendor-id and extended-fault-type are not yet supplied. This is the same answer #3902 gives for an unset property reference.
  • rb dv-20261019-122 (#4312, slice 2): four more test files (BBMD, alarm-and-event services, ComplexACK wire level, Lighting Output) no longer copy text from 135-2024 or 135.1-2023 in their comments; they paraphrase it and keep the clause number. Comments only.
  • rb dv-20261019-123 (#4019): after a log’s alarms are disabled and BUFFER_READY is armed again, Records_Since_Notification counts from the re-arm, and Last_Notify_Record is the Total_Record_Count at that moment (cl. 13.3.7). Records logged while alarms were off are not counted. This is a modelling decision: cl. 12.25.18 counts from the last notification. A Trend Log or Trend Log Multiple armed after a purge now also counts from the purge, as an Event Log already did.
  • rb dv-20261019-135 (#4312, slice 3): the Access Point, alarm-and-event algorithm, network-message relay and Forwarded-NPDU tests, and three ASN.1 field notes in BACnetBusinessLogic.cpp, no longer copy text from 135-2024 in their comments; they paraphrase it and keep the clause number. Comments only.
  • rb dv-20261019-136 (#4312, slice 4): the BBMD reply-distribution, BACnetInterface, DBDevice and responding-state-machine segmentation tests no longer copy text from 135-2024 in their comments; they paraphrase it and keep the clause number. Comments only.
  • rb dv-20261019-137 (#4312, slice 5): the BACnet/SC BVLC-Result, Encapsulated-NPDU and header-option tests (their Annex AB example decodes) and the COV-increment coverage tests no longer copy text from 135-2024 in their comments; they paraphrase it and keep the clause number. Comments only.
  • rb dv-20261019-138 (#4312, slice 6): the CreateObject, Event Log, audit target-generation, responding state machine and alarm-and-event interface tests no longer copy text from 135-2024 or 135.1-2023 in their comments; they paraphrase it and keep the clause number. Comments only.
  • rb dv-20261019-139 (#4312, slice 7): the Notification Forwarder, Schedule, physical-and-virtual router, Event Enrollment, pending Network_Number and fault/time-delay event-algorithm tests no longer copy text from 135-2024 or 135.1-2023 in their comments; they paraphrase it and keep the clause number. Comments only.
  • rb dv-20261019-14 (#4060, part): an idle BACnetStack_Tick() no longer allocates memory to walk the device list. Every tick used to copy the device list into a new vector at each place it walks the devices: one heap allocation per copy, so up to six per tick with every option on (five in the tick itself, one more in the BACnet/SC data link once BACnet/SC is set up), and at least one in every build. An allocation costs more on an MCU whose heap is in PSRAM. The tick now reuses those lists and checks the BACnet/SC Network Ports in place; behaviour is unchanged. Other per-tick work can still allocate, for example the routing-table rebuild while a physical router is enabled. Revert-red: without the change, Issue4060IdleTick.AnIdleTickAllocatesNothing counted 5,000 allocations over 1,000 idle ticks (expected 0), and with only the BACnet/SC change reverted AnIdleTickWithBACnetSCAllocatesNothing counted 1,000.
  • rb dv-20261019-140 (#4312, slice 8): the BBMD, BACnet/SC hub forwarding and resolution, host first-stage reliability, Network Port VALIDATE_CHANGES, Date primitive and router-port network-number activation tests no longer copy text from 135-2024 or 135.1-2023 in their comments; they paraphrase it and keep the clause number. Comments only.
  • rb dv-20261019-141 (#4312, slice 9): the access-control events and occupancy, fault-algorithm event values, ConfirmedRequest PDU, BACnet/SC initiating peer connection, segmented-request client state and alarm-and-event reporting tests no longer copy text from 135-2024 in their comments; they paraphrase it and keep the clause number. Comments only.
  • rb dv-20261019-142 (#4312, slice 10): the access-control object behaviour, MS/TP frame boundary, CharacterString primitive, source-routed reply, virtual devices and Audit Reporter object-model tests no longer copy text from 135-2024 in their comments; they paraphrase it and keep the clause number. Comments only.
  • rb dv-20261019-143 (#4312, slice 11): ten more test files (Channel, Life Safety, Trend Log, Access Door, Network Port device functions, audit delivery, AuthRequest, backup/restore keep-alive, ReinitializeDevice and ReadPropertyMultiple) no longer copy text from 135-2024 in their comments; they paraphrase it and keep the clause number. Comments only.
  • rb dv-20261019-144: three comments cited 135-2024 clauses that do not exist. Network_Number_Quality is cl. 12.56.14 (was 12.9.24), the What-Is-Network-Number discard rule is cl. 6.4.19 (was 24.1.2), and the audit notification’s user id and role now say that Table 19-4 points at clause 24.14, which 135-2024 deleted. Comments only.
  • rb dv-20261019-145 (#4434): BACnetStack_AddObject now refuses a standard object type that this build does not model, and returns false. That covers a type whose STACK_OPTION_OBJECT_TYPE_* option is off, a type the standard removed such as Network Security (38), and an unassigned standard number (65-127). Before, the object was put in Object_List and every read of it then failed: Property_List answered unknown-property, and ReadPropertyMultiple ALL answered an Abort. A host that added a type it never enabled now has the call fail; turn on that type’s STACK_OPTION_OBJECT_TYPE_* option. Proprietary types (128-1023) are still accepted.
  • rb dv-20261019-145 (#4433): the generated objects-and-properties reference now gives the Device’s Max_Segments_Accepted (16) and APDU_Segment_Timeout (5000 ms) defaults. It also marks both as present by default in a build with segmentation. Both values are read from BACnetStackConstants.h, so they can no longer drift.
  • rb dv-20261019-146 (#4441): BACnetStack_SetObjectTypeCreatable (and the internal SetObjectTypeSupported) refuse to turn on a standard object type this build does not model, following #4434’s BACnetStack_AddObject rule. Before, such a type could be advertised in Protocol_Object_Types_Supported. A CreateObject of it also reached the host’s create callback, only for the object to be refused afterwards. Clearing either flag is still allowed.
  • rb dv-20261019-147 (#4312, slice 12): twelve more test files no longer copy text from 135-2024 in their comments; they paraphrase it and keep the clause number. The files cover floating-limit, BACnet/SC hub connection and hub-function connection, the Double, Real and Time primitives, the virtual gateway, Alert Enrollment, Calendar, Credential Data Input, subscribed-recipient matching, and AddListElement atomicity. Comments only.
  • rb dv-20261019-148 (#4312, slice 13): twelve more test files no longer copy text from 135-2024 in their comments. They now paraphrase it and keep the clause number. The files are ChangeList error reporting, WriteGroup members, the EXTENDED and OUT_OF_RANGE algorithms, BVLC length bounds, the SC BVLL validator, direct-connect sockets, the hub connector and its failover tests, DateTime, event-notification transmit and Network-Number-Is. Comments only.
  • rb dv-20261019-149 (#4312, slice 14): the comments in eleven more test files no longer copy text from 135-2024. They paraphrase it and keep the clause number. The files cover Group Present_Value, Program, Trend Log ReadRange by time, access-control property validity, Access Credential, the Network Port command callback, audit-log query, COV subscriber isolation, active COV subscriptions, the packet layer and sending confirmed requests. Comments only.
  • rb dv-20261019-15 (#3088, CR2-F01-059): in the test-tool build, an audit notification the data link cannot send yet is now retried instead of dropped. The data link now reports whether a refused send can succeed later: the sent-confirmed-request table has no room for the request or its invoke ID is still outstanding, a BACnet/SC connection it needs (the hub connection for a broadcast, a direct connection being set up) is not ready, or the BACnet/SC Network Port is out of service. The Audit Reporter keeps such a notification, with the rest of its queue behind it, sets Reliability to COMMUNICATION_FAILURE, and tries again at most once a second. Each notification is retried for up to 60 seconds and then dropped, and the next one in the queue gets its own 60 seconds, so a refusal that never clears cannot hold the queue for ever. Every other refusal is still dropped at once. Revert-red: without the change, IssueCR2F01059_AFullRequestTableIsRetriedNotDropped and IssueCR2F01059_ARefusalThatDoesNotClearIsDroppedAfter60Seconds failed (the notification was dropped on the first refusal: queue 0, expected 1).
  • rb dv-20261019-150 (#4312, slice 15): source/BACnetReadRangeRequest.cpp and thirteen more test files no longer copy text from 135-2024 in their comments. They paraphrase it and keep the clause number. The test files cover Averaging, status flags, unserved time defaults, ReadRange, SC Connect-Request, routing-table sync, I-Am-Router-To-Network, Initialize-Routing-Table, Life Safety Point, Access Zone, COV-multiple context, SubscribeCOVPropertyMultiple, and value-source COV reporting. Comments only.
  • rb dv-20261019-151 (#4312, slice 16): the comments in sixteen more test files no longer copy text from 135-2024. They paraphrase it and keep the clause number. Comments only.
  • rb dv-20261019-152 (#4312, slice 17): the comments in sixteen more test files no longer copy text from 135-2024. They paraphrase it instead and keep the clause number. Comments only.
  • rb dv-20261019-153 (#4312, slice 18): the comments in twenty more test files no longer copy text from 135-2024. They now paraphrase it and keep the clause number. Comments only.
  • rb dv-20261019-154 (#4312, slice 19): 25 more files (11 in source/) no longer copy text from 135-2024 in their comments. They paraphrase it and keep the clause number. Comments only.
  • rb dv-20261019-155 (#4312, slice 20): the last files with copied 135-2024 text in quotation marks in their comments now paraphrase it instead, keeping the clause number. Comments only.
  • rb dv-20261019-156 (#4312, slice 21): copied 135-2024 prose that sat outside quotation marks is now paraphrased in 41 files, keeping the clause number. Most of it was the notes inside ASN.1 productions copied into enum and PDU headers. Comments only.
  • rb dv-20261019-157 (#4456, slice 1): eight files no longer copy prose from 135.1-2023 in their comments. Each copied test Purpose, Test Concept and note is paraphrased with its clause number kept. The files are the virtual-gateway, virtual-device, physical-router and Load Control tests, two source headers, and the two largest tests/btl/ files (AcknowledgeAlarm execution and Trend Log Multiple). Step lines that only name messages, addresses and field values are kept. Comments only.
  • rb dv-20261019-158 (#4456, slice 2): the remaining tests/btl/ files no longer copy 135.1-2023 prose in their comments. Test Purpose, Test Concept, Configuration Requirements, Notes to Tester and step notes are paraphrased, and each keeps its clause number. Step lines are kept. Comments only.
  • rb dv-20261019-159 (#4195, IFC-187): four BACnetTestToolInterface methods take a packed object identifier where they took a type and an instance. These are the two audit-notification sends, the flat SendAuditLogQuery, and SetIntrinsicAccessEventAlgorithm’s credential. The flat exports are unchanged: they check the pair at the boundary, and for the three flag-gated audit pairs only when the flag is set. No input they accept or refuse changes.
  • rb dv-20261019-16 (#3939): API Reference wording and structure. Eight cautions in the API docs that were written as “TRAP:” with words in capitals now read “Note:” or “Caution:” in normal case, with the same meaning (removing a device, Channel datatypes, disabling alarms and events, the BUFFER_READY threshold and time delay, the 32-bit bitstring algorithm, the WhoHas range). BACnetTagApplicationDatatypes, the datatype codes BACnetStack_SetProprietaryProperty and BACnetStack_AddChannelObject take, is now documented in its real scope (BACnetTag::BACnetTagApplicationDatatypes) on a page of its own; the two references used to link to the Enumerations index. The Enumerations index now also lists the enumerations the API header declares itself (BACnetDecodeType, BACnetStackSetting, the BACnetSCCertificateProcedure enumerations and the header’s BACnetPropertyIdentifier), which are on the Constants page. Header changes are comment-only (IFC-157).
  • rb dv-20261019-17 (#4060, part): with the physical router enabled, BACnetStack_Tick() no longer rebuilds every Network Port object’s Routing_Table on every tick. It records what the tables are built from (the router’s ports and routes, each Network Port’s Network_Number) and rebuilds them only when that changed since the last rebuild; the comparison is exact and allocates nothing. Before, an idle tick of a router with two ports, two Network Port objects and one remote route made 9 heap allocations. Routing_Table still changes on the tick after a route, a port, the router’s enabled state or a Network_Number changes, or a Network Port is added or re-created. A change to the Routing_Table property’s enabled state made through BACnetStack_SetPropertyEnabled() now holds until one of those changes (except on a BACnet/SC port, whose data link sets it every tick); before, the next tick undid it. Revert-red: without the change, RoutingTableSyncTest.Issue4060_AnIdleRouterTickAllocatesNothing counted 1,800 allocations over 200 idle ticks (expected 0); with routes and Network_Number left out of the recorded inputs, the two change tests failed.
  • rb dv-20261019-19 (#3034): the API docs now say which BACnet/IP data link the BDT and FDT delete and reset functions act on. BACnetStack_DeleteBDTEntry and BACnetStack_DeleteFDTEntry take no Network Port and act only on the BACnet/IP data link of the first BACnet/IP Network Port added, unlike BACnetStack_AddBDTEntry, BACnetStack_GetBDTEntry and BACnetStack_GetFDTEntry, which take one; BACnetStack_ResetForeignDeviceTable and BACnetStack_ResetBroadcastDistributionTable clear the table of every BACnet/IP data link, and resetting the BDT ends BBMD operation on all of them. Documentation only (IFC-159); per-port variants are tracked in #3992.
  • rb dv-20261019-26 (#4195, from #1876; IFC-160): six BACnetTestToolInterface access-control methods take the referenced object as one BACnetObjectIdentifier32 instead of an object type and instance: SetAccessCredentialBelongsTo, UseAccessCredential, EvaluateAccessRights, AccessZoneEnter, AccessZoneExit and IsAccessZonePassbackViolation. The BACnetStackTestTool_* exports are unchanged and accept and refuse the same inputs. Test-tool C++ surface only.
  • rb dv-20261019-29 (#2746 CR2-F17-015, owner ruling on #4163 item 4): in OUT_OF_RANGE, DOUBLE_OUT_OF_RANGE, SIGNED_OUT_OF_RANGE, UNSIGNED_OUT_OF_RANGE, UNSIGNED_RANGE and FLOATING_LIMIT, two transitions can be pending at once from HIGH_LIMIT or LOW_LIMIT: the cross transition to the other limit (Time_Delay) and the return to NORMAL (Time_Delay_Normal). They now each keep their own timer; the first to expire wins, and the cross transition wins a tie. Before, the cross condition won whenever both held and restarted the NORMAL timer, so with a Time_Delay_Normal shorter than Time_Delay an object went straight to the other limit. A host may now see TO_NORMAL at Time_Delay_Normal, followed later by the transition to the other limit.
  • rb dv-20261019-38 (#3820 CR3-F02-026): a Notification Forwarder’s Port_Filter is now writable by default, as clause 12.51.11 requires whenever it is present. A stack-held forwarder accepts a write that changes only Enabled flags and refuses one that changes the array size or a Port_ID. In a test-tool build, a host-held forwarder (one added with the generic object functions) now receives an indexed Port_Filter write through BACnetStack_RegisterCallbackSetPropertyConstructed, where it used to be refused with WRITE_ACCESS_DENIED; a whole-array write of a host-held forwarder’s Port_Filter is still refused. Only hosts that enabled Port_Filter are affected.
  • rb dv-20261019-39 (#3558): AcknowledgeAlarm follows clause 13.5.2’s procedure. CallbackAcknowledgeAlarm is now called only for an acknowledgment that has passed the stack’s own checks: the object exists and generates events, the Time Stamp matches the transition’s most recent time, and the Event State Acknowledged matches its To State. Before, it ran first. A host therefore no longer sees, or logs, acknowledgments the device refuses, and its refusal can no longer replace the error the clause requires (SERVICES / INVALID_TIMESTAMP, which 135.1 9.1.2.1 checks). A host refusal is now sent with the class the clause 13.5.1.3.1 table pairs with its code: OBJECT for UNKNOWN_OBJECT and NO_ALARM_CONFIGURED, SERVICES otherwise. It was always SERVICES. The callback’s documentation says so (comment-only, IFC-170).
  • rb dv-20261019-4 (#3088 CR2-F01-102, #2691 CR2-F13-006): documentation only. BACnetStack_SendWriteProperty’s doc now says its value bytes are text, with the text form for each datatype (an ObjectIdentifier is “type,instance”; a CharacterString needs at least one character), which differs from the raw binary that BACnetStack_SendCreateObject and BACnetStack_SendWriteGroup take. The HookTextMessage doc says the message and message class arrive as the sender’s octets in its own character set, unconverted, and that the character set is not passed. No code changes.
  • rb dv-20261019-41 (#2689 CR2-F11-010): WriteGroup now sends each change-list entry (BACnetGroupChannelValue) in the clause 21 form: channel [0], then value [1] wrapping the channel value, then the optional overriding priority [2]. It used to send the form shown in the informative Annex F.3.11 example, which puts the priority as [1] before an unwrapped value. Both forms are accepted on receipt. Every WriteGroup the stack sends is two octets longer per entry on the wire. The decode-as XML/JSON output now numbers value and overriding priority 1 and 2, matching the wire.
  • rb dv-20261019-49 (#3681 CR3-F09-049, owner ruling on #3883 item 14): a BACnet/SC Network Port’s SC_Hub_Function_Binding and SC_Direct_Connect_Binding now report every configured accept URI, comma-separated, whenever their role is enabled, before and after the listener starts (cl. 12.56.92, 12.56.97). Before, the hub binding read empty until the listener was up and then showed only the first URI. The direct-connect binding showed the URI of a peer this node had connected to. SC_Direct_Connect_Binding is now present only when direct-connect accept is enabled (Table 12-71). An initiate-only port no longer lists it. A binding longer than the CharacterString construction cap is read in full.
  • rb dv-20261019-5 (#2608 CR2-F03-049): source/BACnetOptionalAny.h and .cpp now carry the full standard license notice. Before, they had a truncated copy. Comment-only.
  • rb dv-20261019-52 (#3715 CR3-F13-010, owner ruling on #3883 item 7a): when CallbackCompleteRestore returns false for a restore that was in progress, System_Status is now left DOWNLOAD_REQUIRED instead of OPERATIONAL (cl. 19.1.3.4). That applies to ENDRESTORE, ABORTRESTORE, or a restore abandoned at the Backup_Failure_Timeout. A false return now means the application could neither apply the restore nor recover its previous configuration. A host that returns false after recovering should return true. A restore ended from RESTORE_FAILURE, where nothing was written, still leaves OPERATIONAL. A backup taken while DOWNLOAD_REQUIRED puts that status back when it ends. A later successful restore clears it. IFC-173 (doc lines only).
  • rb dv-20261019-52 (#3715 CR3-F13-014, owner ruling on #3883 item 8): a You-Are received on a BACnet/SC port now checks the Device MAC Address as on BACnet/IP: 6 octets, and not X’000000000000’ or X’FFFFFFFFFFFF’ (H.7.3). An invalid one refuses the whole request, including the Device Identifier, which used to be assigned. A valid MAC is accepted but not changeable, and is answered with an I-Am. A MAC-only request on SC used to get no answer.
  • rb dv-20261019-55 (#2851 CR2-F29-004, owner ruling #4163): seven BACnetEngineeringUnits members now use their 135-2024 names (cl. 21.6): voltsPerKelvin (176), voltAmpereHoursReactive (242), kilovoltAmpereHoursReactive (243), megavoltAmpereHoursReactive (244), wattsPerMeterPerKelvin (189), wattsPerSquareMeterPerKelvin (141) and minutesPerKelvin (236). The DecodeAsJSON/XML strings use the new names. The old enumerator names still compile as aliases.
  • rb dv-20261019-55, documented (no behaviour change, owner rulings #4163):
    • #3089 CR2-F02-020: an object without its own Audit_Priority_Filter uses its Audit Reporter’s, now recorded in a comment.
    • #2851 CR2-F29-038: a HALT while a Program is LOADING follows Figure 12-3, now recorded in a comment.
  • rb dv-20261019-55, no change: #2851 CR2-F29-005 (noted). #2608 CR2-F03-007: the SecurityKeySet/NetworkSecurityPolicy profile entries sit under revision < 22, which no build reaches since #3824.
  • rb dv-20261019-56 (#3763 CR3-F23-009): a ConfirmedAuditNotification sent to a device that is not an audit logger is now refused with SERVICES / SERVICE_REQUEST_DENIED (cl. 13.20.1.3.1). A device counts as an audit logger when it holds an Audit Log object or the host has registered CallbackAuditNotificationReceived. Such requests used to get a SimpleACK. The unconfirmed form still gets no answer.
  • rb dv-20261019-56 (#3763 CR3-F23-005): when an Audit Reporter’s 256-entry queue is full, it now evicts its oldest ordinary notification to make room for the new one. Notifications of audit-configuration changes are protected and are dropped last (cl. 19.6.6). Those changes are Audit_Level, Auditable_Operations, Audit_Priority_Filter, and any write to an Audit Reporter. Before, the newest notification was always the one dropped. Every eviction still counts toward the AUDITING_FAILURE report.
  • rb dv-20261019-57 (#3679 CR3-F07-027): while a Network_Number write is pending, a read of Network_Number_Quality now follows the pending value: UNKNOWN for 0, CONFIGURED otherwise (cl. 12.56.13, 12.56.15). Routing still uses the active quality until the change is activated. Before, a pending 0 read as (0, CONFIGURED).
  • rb dv-20261019-57 (#3679 CR3-F07-031): a client that receives a retransmission of the final segment of a segmented ComplexACK it has already completed now acknowledges it again with a Segment-ACK (cl. 5.3.5.3 (c)). Before, it answered with the #3314 Abort, which told the server that a successful transaction had failed. The client keeps a short-lived record of each completed response for its own segment timeout (Tseg times four). Any other stray segment still draws the Abort, and both answers share the per-source rate limit.
  • rb dv-20261019-6 (#3712 CR3-F10-019): a ReadPropertyMultiple error result for an object that does not exist (UNKNOWN_OBJECT), or for a property the object does not have, no longer echoes the Property Array Index the request carried. Cl. 15.7.3.2.2.2 carries the index only for a property of datatype array. A non-array property read with an index still echoes it with PROPERTY_IS_NOT_AN_ARRAY, as 135.1-2023 9.20.2.3 expects. If your client reads the Property Array Index back from such an error result, it is now absent; match those results on object and property alone. A property the object has keeps the index, whatever the error.
  • rb dv-20261019-60 (#3939): which callback types the API Reference lists as customer types is now read from the customer exports’ registrar parameter lists (adapter-model.js customerRegisteredCallbackTypes). Before, any mention of the name anywhere in CASBACnetStackDLL.h, comments included, counted. Both the enumeration generator and the manual’s API-reference generator use the new rule. The resulting set is unchanged (72 types). The BACnetStack_DecodeAs doc no longer names the two removed exports (IFC-176).
  • rb dv-20261019-61 (#2288, owner ruling #4163 item 16): in the Node package’s typed API (api.ts), the callback parameter of 30 Register* functions is now named callback, matching the other 41, instead of the C typedef name (FPHookIAm, FPCallbackCreateObject, …). The name is not part of a call, so callers are unaffected. RegisterHookIAmRouterToNetwork and RegisterHookICouldBeRouterToNetwork now declare the callback optional, as their wrappers already accept. RegisterHookInitializeRoutingTableAck, RegisterHookReadBroadcastDistributionTableAck and RegisterHookReadForeignDeviceTableAck no longer declare | null, because their wrappers throw a TypeError on null. TypeScript now rejects that call instead of letting it fail at run time.
  • rb dv-20261019-63 (#910): customer header documentation (comment-only, IFC-177), per the owner’s 2026-09-22 ruling to document these side effects.
  • rb dv-20261019-7 (#4006, owner answer): Number_Of_APDU_Retries now defaults to 3, the value cl. 12.11.29 suggests; it was 0. A host that does not serve the property now has confirmed requests and confirmed event notifications retransmitted up to three times, and a request to a peer that never answers fails after four APDU_Timeouts (40 s at the 10 s default) instead of one. Peers now see the retransmissions. To keep the old behaviour, serve Number_Of_APDU_Retries as 0 from the callback you register with BACnetStack_RegisterCallbackGetPropertyUnsignedInteger().
  • rb dv-20261019-8 (#3235, owner ruling 2026-10-17): a WriteProperty of an Event Enrollment’s Event_Parameters whose choice names a different event algorithm (one that would change Event_Type) is now answered PROPERTY / OPTIONAL_FUNCTIONALITY_NOT_SUPPORTED, following cl. 12.12.7. It used to be WRITE_ACCESS_DENIED. An Event Enrollment with no event algorithm gives the same error. A value that is not a BACnetEventParameter is refused as before. A write naming the Enrollment’s own algorithm, where that algorithm takes no writes (every algorithm except CHANGE_OF_TIMER), is still WRITE_ACCESS_DENIED. Check any client that matched on the old error code.
  • rb dv-20261019-82 (#2526): a confirmed request to a device on a remote network whose router is not known is now sent the cl. 6.5.3 way: as a local broadcast carrying DNET/DADR, as unconfirmed messages already were. It used to be a unicast to the remote MAC on the local link.
    • How the answer is matched: until the router answers, the request’s transaction is tracked under an internal placeholder. The first answer the router relays (with SNET/SADR naming that device) moves it under the router’s address. Retries go out as the same broadcast.
    • HookTimeout and HookAbort: they report such a request with no connection string, plus its network and MAC.
    • Who is affected: confirmed event and audit notifications to an address-form recipient on a remote network.
    • Known limits: a foreign device’s confirmed broadcast stays on its own subnet (#385). On a BBMD, only the first attempt is distributed to the BDT and FDT. Any node on the local link can answer first.
  • rb dv-20261019-83 (#3364): the BACnet/SC hub connector now reads CallbackInitiateWebsocket’s return. false means the WebSocket could not be initiated (cl. AB.6.2.2), so the connection goes back to idle at once. The attempt is counted and the next one waits for the reconnect timeout. Before, the connection waited in AWAITING_WEBSOCKET until the connect timeout, and a later Connected report started a Connect-Request on a socket the host said it never opened.
    • Who is affected: a host that returns false while it does open the socket. Return true whenever the connection will be attempted.
    • Node addon: an explicit false refuses, and a callback that throws fails. An async callback’s Promise, or a callback that returns nothing, is read as true.
    • Docs: the BACnetStack_RegisterCallbackInitiateWebsocket documentation is corrected (IFC-178).
  • rb dv-20261019-86 (#3956): a Channel now passes every write of its Present_Value on to the members of List_Of_Object_Property_References at the write’s priority (cl. 12.53.5, 19.2.1.6). That covers WriteProperty, WritePropertyMultiple, WriteGroup, and a Schedule or Timer in the device whose reference names the Channel. Before, only a WriteGroup reached the members; a WriteProperty stored the Channel’s value and stopped. The member rules are unchanged: Out_Of_Service, empty references, Table 12-63 coercion, stack-held members, and remote members through a WriteProperty. Revert-red: the WriteProperty, relinquish, Schedule and lighting-command member tests failed on the pre-fix code.
    • Who is affected: a host that passed a Channel’s WriteProperty writes on to the members itself, as the B-LS profile example’s workaround does. Each member is now written twice. Remove the host-side propagation.
    • Schedule/Timer to a Channel: the write now passes the same writability check as a WriteProperty. A host that made the Channel’s Present_Value read-only now has those writes refused and logged.
    • Docs: the BACnetStack_AddChannelObject documentation is corrected (IFC-179).
  • rb dv-20261019-9 (#4168): an Event_Parameters write to an EXTENDED Event Enrollment whose vendor-id or extended-event-type differs from the Enrollment’s now fails with PROPERTY / OPTIONAL_FUNCTIONALITY_NOT_SUPPORTED, the same error a write for a different event type gets since #3235. Before, it failed with WRITE_ACCESS_DENIED. A write with the Enrollment’s own vendor-id and extended-event-type still fails with WRITE_ACCESS_DENIED, because an EXTENDED algorithm’s parameters cannot be written.
  • rb dv-20261019-92 (#3668): a BACnet/SC Network Port that is not in error now reports Current_Health and Command_Validation_Result with Result = Error Class OBJECT, Error Code SUCCESS, as cl. 12.56.17 requires (ruling 5). It was SECURITY / SUCCESS from #2025. A client or test that checked for the SECURITY class must check for OBJECT.
  • rb dv-20261019-95 (#4021 items 2 and 3, ruling 5): AtomicReadFile / AtomicWriteFile no longer answer configuration problems or callback refusals with Abort(other). They answer with a Result(-):
    • A stream request with no CallbackReadFile / CallbackWriteFile registered gets SERVICES / OPTIONAL_FUNCTIONALITY_NOT_SUPPORTED.
    • So does a File whose File_Size or Read_Only the host cannot supply.
    • A file callback that refuses with abortOther gets SERVICES / FILE_ACCESS_DENIED, like any other code that cannot be sent as an Error.
    • Abort remains for allocation and internal failures. The BACnetStack_RegisterCallbackReadFile / WriteFile documentation says so (IFC-182), and the BACnet/SC guide’s note on File objects added without AddFileObject is corrected.
  • rb dv-20261019-96 (#3900): no behaviour change. A Binary Lighting Output STOP written at a higher priority than a pending WARN_OFF / WARN_RELINQUISH does not halt it. The STOP value’s definition makes a STOP with no warn executing at its own priority an ignored value, and an ignored value is not the higher-priority write cl. 12.55.4.1 halts on. This is now documented at the site and pinned by a test.
  • rb dv-20261019-97 (#3737): an Event Enrollment whose monitored object or property cannot be read now reports Reliability UNRELIABLE_OTHER and enters FAULT for that reason. It used to report MONITORED_OBJECT_FAULT. This is step (1) of cl. 12.12.21: the Event Enrollment cannot perform its monitoring function, the same answer #3240 gives for an unreadable fault-algorithm referent. MONITORED_OBJECT_FAULT stays for a readable monitored object whose own Reliability is not NO_FAULT_DETECTED. A client that keyed on 14 for a deleted or unreadable monitored object now sees 7.
  • rb dv-20261019-98 (#3749): an Access Point that denies a credential because one of its access rights’ negative rules matches now records the Access_Event cl. 12.34.9.2 names. A rule whose Location is an Access Zone gives DENIED_ZONE_NO_ACCESS_RIGHTS (133). Any other negative rule gives DENIED_POINT_NO_ACCESS_RIGHTS (134). It used to record DENIED_NO_ACCESS_RIGHTS (135), which stays the event when no positive rule grants access. DENIED_OUT_OF_TIME_RANGE is still not distinguished. The test that pinned 135 for a negative-rule veto now expects 134 (ruling 2).
  • rb dv-20261030-3 (#4489 CR4-F09-038): BACNET_SC_HEARTBEAT_TIMEOUT_DEFAULT is 300 seconds, the value cl. 12.56.86 recommends. It was 30. A host that sets its own timeout sees no change; one relying on the default sends a tenth of the idle Heartbeat-Requests and detects a dead hub connection later.
  • rb dv-20261030-3 (#4489 CR4-F09-002): a BVLC with an Originating or Destination Virtual Address, or Data Options, on a function that may not carry them is NAKed PARAMETER_OUT_OF_RANGE on every BACnet/SC path (AB.3.1.5). It was HEADER_ENCODING_ERROR or UNEXPECTED_DATA.
  • rb dv-20261030-4 (#957): the release zip is built from an allowlist, _ci_scripts/release-zip-paths.txt, so a new top-level folder no longer ships by default. Its contents are unchanged apart from the new notices file. check-release-paths.js, in pre-push.sh --fast, fails when a tracked top-level path is not classified.
  • rb dv-20261030-4 (#3995): go vet runs over the Go adapter in tests.yml’s fast gates and in pre-push.sh --fast, which skips it with a notice where Go is not installed.
  • rb dv-20261031-10 (#4555), in every build, not only under the option: a proxied I-Am waiting for a router port that is disabled or gone is dropped when its turn comes, and uses none of Max_Proxied_I_Ams_Per_Second. BACnetStackDeviceAddressProxy::DisableNetwork keeps MS/TP subordinate entries, and LearnIAm treats them as the option describes.
  • rb dv-20261031-11 (#4688), in every build: BACnetMSTPEngine and BACnetMSTPBytePort carry a source address with a queued frame. With no proxied frame queued, nothing on the wire changes.
  • rb dv-20261031-12 (#4690): a packet-mode MS/TP port is unchanged: the four properties answer UNKNOWN_PROPERTY, RESTART_SUBORDINATE_DISCOVERY answers OPTIONAL_FUNCTIONALITY_NOT_SUPPORTED, and it sends nothing new.
  • rb dv-20261031-12 (#4690): a proxied I-Am due out of a byte-mode MS/TP port waits up to 5 seconds for room in the port’s outbound queue, using no budget until then, instead of being lost to a full queue. Entries for other ports go past it. This also applies to device address proxying (#4560).
  • rb dv-20261031-13 (#4603, IFC-207): the stack keeps a Channel’s Write_Status (cl. 12.53.7) for a Channel added with BACnetStack_AddChannelObject, and CallbackGetPropertyEnumerated is no longer asked for it. It reads IDLE until a write has members to reach, IN_PROGRESS from that write, and SUCCESSFUL or FAILED once every member has been attempted and every remote member’s transaction has ended. FAILED covers a coercion failure, a local member the host refused, a remote member the stack could not send to (DeviceCommunicationControl DISABLE_INITIATION, no room for the transaction), and an Error, Reject, Abort or timeout. A numeric remote member whose device has no address yet stays IN_PROGRESS: the stack seeks it with a Who-Is once it has heard an I-Am from any device, sends the write if it binds within APDU_Timeout x (Number_Of_APDU_Retries + 1), and is FAILED otherwise. Known limit: a lighting command is not held, so a lighting Channel with a never-bound member reads FAILED where 135.1-2023 7.3.2.34.4 step 9 expects IN_PROGRESS. A Null write refused as INVALID_DATATYPE or INVALID_PARAMETER_DATA_TYPE, or refused in any way by a local member that is not commandable, is not a failure. The confirmed-request hooks fire as before, and a SubscribeCOVProperty or Trend Log on Write_Status is told of each change.
  • rb dv-20261031-13 (#4603): during IN_PROGRESS the stack answers a WriteProperty or WritePropertyMultiple of a Channel’s Present_Value with OBJECT / BUSY (cl. 12.53.5), without calling the host. A WriteGroup, Schedule, Timer or chained Channel write is applied and starts a new fan-out, whose result is the one reported.
  • rb dv-20261031-13 (#4603): promotion warning: a host that refuses Channel writes from its own Write_Status tracking, by returning BUSY from its Set callback, keeps doing so: that answer still goes out as OBJECT / BUSY, so a tracker left stale refuses writes the stack would accept. Such a host must remove its tracking on this version.
  • rb dv-20261031-5 (#4608): the fast gates run check-export-locks.js, and sanitizers.yml runs the tests-thread-safe suite on version tags and manual dispatch.
  • rb dv-20261031-5 (#4673): the npm runtime floor check covers the linux-arm64 prebuild as well as linux-x64.
  • rb dv-20261031-5 (#3939): the release.yml comment no longer says the Set Build Flags step is for the Doxygen pass.
  • rb dv-20261101-1 (#4693, IFC-208): from a Channel write that ends FAILED until one ends SUCCESSFUL, with its Reliability enabled, the stack serves Reliability with the cause (cl. 12.53.9): COMMUNICATION_FAILURE, CONFIGURATION_ERROR or PROCESS_ERROR (the table is in IFC-208), and CONFIGURATION_ERROR over COMMUNICATION_FAILURE over PROCESS_ERROR when members failed differently. Status_Flags FAULT and, with intrinsic reporting, Event_State and TO_FAULT follow. Otherwise, and while Out_Of_Service or reliability evaluation is inhibited, the host’s Reliability is served.
  • rb dv-20261101-2 (#4559): the objectIdentifier32 typedef is gone (172 lines in source/ and tests/). A value that names an object is now a BACnetObjectIdentifier32, including GetObjectByName, which returns a bool and fills an out-parameter; a container keyed on the packed number says uint32_t. No behaviour changes. check-object-identifier-boundary.sh has a third check, with no allowlist, that keeps the typedef and the three retired packing helpers out of code; its pair-declaration allowlist still drains with the #1876 tail.
  • rb dv-20261101-2 (#4559): no packed pair needed TryMake. Pairs from a request come from a decoded identifier, and an out-of-range machineRoomId was already refused further down, now pinned by a test.
  • rb dv-20261101-79 (#4874): BACnetStack_SetIntrinsicChangeOfStateAlgorithmBool and ...Unsigned on an Event Enrollment now return false when the referenced object’s Present_Value is of the other datatype (Boolean on a multistate, Access Door, Access Zone or Load Control referent; Unsigned on a binary, Lift, Escalator, Access Door, Access Zone or Load Control referent). Those armings were accepted before, but could never fire; check the setters’ return values.
  • rb dv-20261101-80 (#4781, CR4-F17-016): arming CHANGE_OF_TIMER or CHANGE_OF_STATUS_FLAGS on an Event Enrollment that references another property is now refused where it was accepted before and could not work; check the setters’ return values.
  • rb dv-20261101-86 (#4802, CR4-F22-006, -012, -016): comment-only fixes in the Event Log, Trend Log, Trend Log Multiple and Annex AA codec sources and headers (stale line-number cross-references replaced by function names, the Trend Log clause cited in Event Log and Trend Log Multiple code replaced by their own clauses, the Annex AA codec contract stated as the code has it); BACnetStackTrendLog::InsertClonedRecordOrFail (private, no longer clones) is renamed InsertRecordOrFail. No behaviour change.
  • rb dv-20261101-88 (#4797, CR4-F21-013, -014): six source comments in the Notification Class Destination and the Event/Alert Enrollment objects paraphrase 135-2024 instead of quoting it; the From/To Time wildcard comment records that accepting a wildcard octet is deliberate. No behaviour change.
  • rb dv-20261101-89 (#4787, CR4-F18-005, -007, -014): test-only pins for the fault-parameter CHOICE decode (four malformed frames are Reject INVALID_TAG) and the status-flags fault algorithm’s value guards; a parameter and three loop variables renamed to descriptive names. No behaviour change.
  • rb dv-20261101-91 (#4727, CR4-F11-014, -016): BACnetWriteGroupProcessor.cpp drops the includes of the code #3956 moved out (including BACnetInterface.h, which was compiled even in a build without WriteGroup); the WriteGroup ‘Inhibit Delay’ relay to CallbackWriteGroupInhibitDelay (once per Channel of a member device, the effective flag TRUE only when the parameter was present and TRUE) is pinned by two processor tests. No behaviour change.
  • rb dv-20261101-92 (#4736, CR4-F13-023): the Last I Am Time of each Device_Address_Proxy_Table entry is served in the device’s local time (the Device’s UTC_Offset and Daylight_Savings_Status applied, as for every other time the stack generates); it was UTC, so on a device with a non-zero UTC_Offset it read hours away from Local_Date / Local_Time — revert-red: the new test saw the UTC value for an offset of 480 minutes on the pre-fix code.
  • rb dv-20261101-92 (#4736, CR4-F13-015, -016): comments and two log texts only: the Who-Is and Who-Has clause citations for the 22-bit limit, the You-Are, Who-Is processor and text-message header comments, and the “Invalid BACnetWhoIsRequest / BACnetWhoHasRequest object” log lines. No behaviour change.
  • rb dv-20261101-93 (#4781, CR4-F17-019, -026): the NORMAL branch of HandleTimeDelay in the Out Of Range, Double Out Of Range, Signed Out Of Range, Unsigned Out Of Range, Unsigned Range and Floating Limit algorithms is removed. Nothing has called it since the return to NORMAL moved to HandleCrossOrNormalTimeDelays (#4219). No behaviour change.
  • rb dv-20261101-95 (#4789, CR4-F19-007): the ChangeOfTimer event parameter renders its alarm values as one list element (“AlarmValues”), as every other list arm does, where it rendered one “AlarmValue” per item and nothing for an empty list. Display only; no wire effect — revert-red: the new rendering test saw the per-item elements on the pre-fix code.
  • rb perf-20261018-1 (#4087): every request costs fewer instructions. Resetting a tag or primitive, which a request does hundreds of times, is now a few direct assignments instead of calls through the validating setters. B-BC reference device, instructions per request: ReadPropertyMultiple of 20 properties 639,748 to 354,322 (-45%), ReadRange of 10 records 453,037 to 261,589 (-42%), WriteProperty 89,489 to 61,316, Who-Is 72,389 to 51,535, ReadProperty 80,620 to 61,242, idle Tick 31,408 to 25,762. The stack’s flash in the x64 size build shrinks by 9 KiB. No behaviour change: the state after a reset is the same.
  • rb perf-20261018-2 (#4091): a confirmed request is no longer deep-copied when its transaction enters AWAIT_RESPONSE. The copy was kept only to carry the timeout Abort, which replaces the request in it, so the packet is now kept without its APDU. The Abort is unchanged. B-BC reference device, instructions per request: ReadPropertyMultiple of 20 properties 354,322 to 315,834 (-11%), ReadRange 261,589 to 256,231, ReadProperty 61,242 to 58,829, WriteProperty 61,316 to 57,188; one fewer heap copy of each confirmed request.
  • rb perf-20261018-3 (#4093): reading a property no longer walks every object type’s settings to find its own. Ten lookups in BACnetDBDevice (whether a property is enabled, its profile, whether it is writable or subscribable, the required and optional property lists, and others) use the map’s find(). B-BC reference device, instructions per request: idle Tick 25,762 to 22,419 (-13%), Who-Is 51,535 to 46,782, ReadProperty 58,829 to 54,244, WriteProperty 57,188 to 53,050, ReadPropertyMultiple 315,834 to 302,404, ReadRange 256,231 to 250,339. No behaviour change.
  • rb perf-20261018-4 (#4094): a ReadRange reply is no longer copied record by record into the ComplexACK. The ReadRange-ACK is built on the heap and handed over, as the other services already do, so the reply holds one copy of the records while it is built instead of two. B-BC reference device, ReadRange of 10 Trend Log records: 256,231 to 189,692 instructions per request (-26%). The reply is unchanged.
  • rb planner-20261017-1 (#4026), internal resolver tooling, not shipped (_ci_scripts/ is outside the release export): a new _ci_scripts/tick-findings.py ticks a checklist issue’s items without erasing another session’s ticks. It re-reads the body just before writing, checks it twice afterwards (the second time after --settle seconds), and retries from the newest copy. It replaces hand-editing the body, which had silently reverted 32 finished review items when two sessions worked one roll-up. The runbook now requires it. Mutation: 21 external mutations of the helper, all caught by its 62-check self-test.
  • (#3990): tools/coverage/run.sh also records branch coverage (without the branches GCC adds for exception paths), and its HTML report shows it.
  • (#4057): BACnetStack_Tick() reads the Device’s UTC offset (UTC_Offset and Daylight_Savings_Status) and its Local_Date from the host at most once per tick, and only when an object needs them. A tick used to read the UTC offset once for every Trend Log, Trend Log Multiple and other time-based object, and Local_Date once for the Calendars and again inside every Schedule; on a B-BC device those reads were most of an idle tick. An idle tick of the B-BC reference device drops from 46,936 to 31,591 instructions (x86-64). A tick’s Calendars and Schedules now always see the same date; a change an object’s own write makes to the Device’s UTC_Offset, Daylight_Savings_Status or Local_Date during a tick takes effect from the next tick.
  • (#4055): Trend Log and Trend Log Multiple objects hold each log record as its BACnet encoding instead of as a record object, and create a buffer slot only when a record arrives. A REAL Trend Log record costs about 56 bytes of heap instead of 376 (64-bit), a three-point Trend Log Multiple record about 56 instead of 816, and a log costs nothing for records it has not logged yet (a 200-record Trend Log used to take 67 KB the moment it was created). ReadRange, BACnetStack_ReadTrendLogRecord / BACnetStack_ReadTrendLogMultipleRecord and backup return the same octets as before: a record is kept encoded only when its encoding decodes and re-encodes identically, and a record with an any-value datum keeps its object form. Because nothing is allocated up front, BACnetStack_AddTrendLogObject and BACnetStack_AddTrendLogMultipleObject now accept a buffer size of 4294967295 (cl. 12.25.13 / 12.30.18: constrained only by available resources) instead of refusing it (#2772); such a log grows until memory runs out, and a record that cannot be stored is refused, not thrown. The Event Log still refuses that size.
  • rb v-20261016-1 (#3981): MSTP.c implements ANSWER_DATA_REQUEST (cl. 9.5.6.9). A reply to a BACnet Data Expecting Reply that the host queues within Treply_delay / 2 (125 ms) is sent at once as BACnet Data Not Expecting Reply; otherwise Reply Postponed is sent and the reply goes on the next token, as before; after Treply_delay nothing is sent until the token. Every request used to get Reply Postponed at once. Treply_delay is now set (250 ms, cl. 9.5.3). Frames on the wire change; check on an RS-485 rig before release.
  • rb v-20261017-11 (#3700): a refusing AtomicReadFile or AtomicWriteFile callback (stream or record) is answered with a Result(-) carrying the code it named and that code’s clause-18 error class; the codes the cl. 14.1.4.1 / 14.2.4.1 tables name get the table’s class (for example SERVICES / INVALID_TAG). It used to be Abort(other) for every code except INVALID_FILE_START_POSITION and FILE_FULL. A refusal with no usable code answers SERVICES / FILE_ACCESS_DENIED; ABORT_OTHER still aborts. A write to a read-only File answers SERVICES / FILE_ACCESS_DENIED instead of PROPERTY / WRITE_ACCESS_DENIED (IFC-118; owner ruling 5).
  • rb v-20261017-11 (#3642): an enumerated value outside its closed domain (Access Door Door_Value, Lock_Status, Door_Secured_Status, Access_Passback_Mode) written as a property value answers Error(PROPERTY, VALUE_OUT_OF_RANGE) - WriteProperty, WritePropertyMultiple (naming the element) and a CreateObject initial value (the first failing element, after the object-level checks). As a service parameter of a confirmed request it answers Reject(UNDEFINED_ENUMERATION); an unconfirmed notification is discarded. Reject(OTHER) is no longer sent (owner ruling on #3799).
  • rb v-20261017-12 (#4020): a WritePropertyMultiple or CreateObject carrying an out-of-range Color, xyColor, LightingCommand or elevator value answers the per-element Error(PROPERTY, VALUE_OUT_OF_RANGE) naming it (CreateObject: the element number), instead of rejecting the whole request with Reject(PARAMETER_OUT_OF_RANGE); WritePropertyMultiple applies the earlier elements first.
  • rb v-20261017-12 (#3486): test tool - a stack-held Access Point’s fired access events also feed every ACCESS_EVENT Event Enrollment in this device that monitors its Access_Event: the Access Point’s tag and credential, and its time when the enrollment’s time reference is unset or names Access_Event_Time (IFC-120). The enrollment’s armed credential is replaced on each event.
  • rb v-20261017-2 (#3958): a Schedule that cannot read the Device object’s Local_Date or Local_Time is logged once, as an error naming the Schedule, each time that starts. It used to be skipped with nothing logged. Revert-red: Issue3958_UnreadableLocalTimeIsLoggedOncePerOnset failed with the change reverted (verify-fails-without.sh) and passes with it.
  • rb v-20261017-3 (#3727): with Event_Detection_Enable FALSE, reliability evaluation keeps running. Reliability (and Status_Flags.FAULT) now reports a fault the monitored value is in; before, it was forced to NO_FAULT_DETECTED. Event_State still stays NORMAL and nothing is notified. When detection is enabled again, a fault still held is reported with a ToFault.
  • rb v-20261017-4 (#2572): under STACK_OPTION_TARGET_FULL the BACnet/SC hub connector advertises a Maximum BVLC Length of 6144 by default, the value the Network Port starts with, so a hub relays the frames AB.5.1 requires without a BACnetStack_SetBACnetSCSettings call. It was 1600. Embedded builds keep 1600. 135.1-2023 12.5.1.1.15 requires only the configured value, so its repository test was re-pinned (owner ruling 2).
  • rb v-20261017-4 (#3726): BACnetStack_SetBACnetSCSettings returns false, and changes nothing, when the NPDU length plus the 16-octet relayed-broadcast header exceeds the BVLC length. A Network Port WriteProperty of Max_BVLC_Length_Accepted or Max_NPDU_Length_Accepted that would break the same rule answers Error(PROPERTY, VALUE_OUT_OF_RANGE) (IFC-113). Check the result: pass a maximumNPDULength no larger than maximumBVLCLength - 16.
  • rb v-20261017-5 (#3954): STACK_OPTION_TARGET_FULL builds without the test tool (the shipped static and DLL libraries) now compile WriteGroup and LifeSafetyOperation, as the preset’s “all BIBBs” promise says; BACnetStack_SetServiceEnabled stopped refusing them. The B-LSC and B-ALSC profile presets now compile LifeSafetyOperation, which their AE-LS-B BIBB executes. A new pre-push gate (_ci_scripts/check-full-target-options.sh) keeps TARGET_FULL in step with the test-tool build.
  • rb v-20261017-9 (#4008): BACnetStack_AddNetworkPortObject returns false, and changes and claims nothing, when the instance is already a Network Port object of a different network type (IFC-116). It used to return true, keep the old type, and still bind a data link from the networkType argument - for example an IPv4 data link on a virtual device’s virtual-network Port 1. Check the result; to change a port’s type, remove the object and add it again. AddDeviceToVirtualNetwork on a device that already has an IPv4 Network Port 1 now fails the same way.
  • rb v-20261018-1 (#900): every BACnetStack_SetIntrinsic*Algorithm* and BACnetStack_SetFault*Algorithm* export now documents its real precondition. Alarms and events must be enabled for the object with BACnetStack_SetAlarmsAndEventsForObjectEnabled(..., enabled=true), or the object must be an Event Enrollment; otherwise the setter returns false. The header used to say only that the object must exist (IFC-133; documentation only, no behaviour change).
  • rb v-20261018-1 (#4053): the customer headers CASBACnetStackDLL.h and BACnetCallbackInterface.h no longer quote 135-2024 or 135.1-2023 text; each quotation is now a clause citation and a paraphrase. The generated C#, Java, Python and Rust bindings carry the same text (IFC-133; documentation only).
  • rb v-20261018-10 (#4113): SC_Direct_Connect_Connection_Status now lists a direct connection this port is initiating from the start of the attempt (cl. 12.56.98), not only once it is connected. While the WebSocket opens and the Connect-Request waits for its Connect-Accept, the entry reads NOT_CONNECTED with the URI being used, unspecified Connect and Disconnect Timestamps and no Peer VMAC or UUID. The same entry becomes CONNECTED when the peer accepts or, if the attempt fails, is recorded as FAILED_TO_CONNECT; an attempt the port abandons itself is not recorded. While a new attempt to a peer is in progress, that peer’s earlier finished entry is not listed beside it.
  • rb v-20261018-12 (#3769 CR3-F29-012): an Averaging object now samples on a cumulative schedule, sample k at start + k x Window_Interval / Window_Samples, so its window spans Window_Interval to within a second (cl. 12.5.14; owner ruling on #3874). Before, each sample was one truncated period after the last, so Window_Interval 100 over 3 samples sampled every 33 seconds and the window covered 99 seconds. The default 900 / 15 still samples every 60 seconds. A sample taken late, or after the clock is set forward, moves the schedule to the next instant without a burst of catch-up samples.
  • rb v-20261018-13 (#3768 CR3-F28-004): a Schedule exception defined by an every-year DateRange whose start falls after its end in the year now runs over New Year. “Every year” means year 0 from the host, or year X’FF’ over the wire, at both endpoints; Dec 24 .. Jan 2 is an example. Before, such a range never matched: the exception was stored but never ran, and the Schedule took the weekly schedule or Schedule_Default over the holiday period. A year-X’FF’ range written to Exception_Schedule is still accepted, so writing back what the stack serves keeps working. 135-2024 cl. 12.1.7 defines no encoding for “every year” in a DateRange endpoint, so another device may read such a range differently (owner ruling on #3874; IFC-139).
  • rb v-20261018-14 (#3764 CR3-F24-013): an Access Point request that identifies an Access Credential and then denies it at authentication now records the use on the credential. That covers a disabled authentication factor and a credential that is not active (expired, inactive, disabled and so on). Last_Use_Time, Last_Access_Point and Last_Access_Event show the denial, as they already did for a grant or an authorization denial (cl. 12.35.19 to 12.35.21). An unknown factor still records nothing. The Absentee_Limit inactivity check (cl. 12.35.16) now counts from the credential’s last granted use, not from Last_Use_Time. Granted uses include an Authorization_Mode NONE request; authorization denials do not count. As a result a denied presentation never re-enables a DISABLED_INACTIVITY credential. This is a deliberate deviation from the clause’s wording (owner ruling on #3874; _spec/decisions/2026-10-18-absentee-limit-last-granted-use.md).
  • rb v-20261018-14 (#3764 CR3-F24-014): a grant with a SOFT_PASSBACK violation now fires PASSBACK_DETECTED and then GRANTED, so GRANTED is the final event of the transaction (cl. 12.31.27.1; 135.1-2023 7.3.2.42.4 step 9). Before, PASSBACK_DETECTED replaced GRANTED, so the grant had no GRANTED event (owner ruling on #3874).
  • rb v-20261018-14 (#3764 CR3-F24-016): a factor that is read but matches no credential now fires AUTHENTICATION_FACTOR_READ and then DENIED_UNKNOWN_CREDENTIAL, which is still the final event. A factor read with an error still fires DENIED_AUTHENTICATION_FACTOR_ERROR alone (owner ruling on #3874).
  • rb v-20261018-14 (#3764 CR3-F24-004): BACnetCredentialAuthenticationFactor::SetAuthenticationFactor now returns whether the factor was stored, as its sibling setters do. It no longer reports whether the whole object is complete. Setting the factor before the disable now returns true; the object stays invalid until the disable is set (owner ruling on #3874).
  • rb v-20261018-15 (#3089 CR2-F02-016): BACnetStack_AddGroupObject now returns false, and creates no Group engine, when the device has no Group object with that instance. Its doc always asked for an existing Group object. Before, the call built a Group engine that served nothing. Call BACnetStack_AddObject(deviceInstance, 11, instance) first (owner ruling 4, 2026-10-17; IFC-140).
  • rb v-20261018-16 (#2611 CR2-F07-043): BACnetStack_AddNetworkPortObject now returns false, and adds nothing, for a referencePort above 4194303, which is not a Network Port instance number. The value used to be stored and read back as Reference_Port. The port’s internal SetReferencePort refuses it too (owner ruling 4, 2026-10-17; IFC-141).
  • rb v-20261018-16 (#2689 CR2-F11-004): a WriteProperty or WritePropertyMultiple whose Priority is outside 1..16 is answered with Error SERVICES / PARAMETER_OUT_OF_RANGE, the error the service’s table names, whatever the property (the clause would let a non-commandable property ignore it; refusing keeps a priority of 17..255 away from the host’s set-property callbacks). It used to be refused with Reject PARAMETER_OUT_OF_RANGE (for WritePropertyMultiple, when the first element carried it) (owner ruling 5).
  • rb v-20261018-16 (#2691 CR2-F13-031): a DeviceCommunicationControl with the deprecated value DISABLE and a wrong password is answered SECURITY / PASSWORD_FAILURE; with the right password it is still refused with SERVICES / SERVICE_REQUEST_DENIED. The password check now comes first, as cl. 16.1.2 orders it. As a result the host’s DCC password callback is now called for a DISABLE request that the stack then refuses (#653 had moved the refusal ahead of the callback); a callback should only validate. Both callback docs say so (IFC-141).
  • rb v-20261018-17 (#3088 CR2-F01-071): a build that defines STACK_OPTION_PROFILE_B_SCHUB without STACK_OPTION_DATA_LINK_LAYER_SC now stops with an #error. Before, it compiled with no BACnet/SC hub function. Add the SC data link (it also needs IPv4) or drop the profile. The B-BBMD profile’s IPv4 check was added earlier.
  • rb v-20261018-18 (#3089 CR2-F02-015): BACnetStack_AddObjectPropertyReferenceToChannel now returns false, and appends nothing, when refUseDeviceIdentifier is true and refDeviceInstance is above 4194303. That value is not a device instance, so the reference could never resolve; it used to be appended. Pass 0..4194302, or 4194303 for an empty reference (owner ruling 4, 2026-10-17; IFC-143).
  • rb v-20261018-2 (#3824 CR3-F01-025): every build now requires BACNET_PROTOCOL_REVISION 24 or later. CASBACnetStackOptions.h stops a build below 24 with an #error; such a build already failed to compile further in, because Revision 24 property identifiers are named unconditionally. The test-tool build used to be stopped only below 20. Leave the revision at its default (30).
  • rb v-20261018-2 (#3824 CR3-F01-031): the stray-segment Abort limiter (a burst of 4, then 1 a second per source) refills from Tick counts only when no CallbackGetSystemTime is registered. With a clock registered the rate is exactly 1 a second, and a clock that stops advancing stops the Aborts after the burst. The router’s Reject-Message-To-Network limiter is unchanged.
  • rb v-20261018-2 (#3824 CR3-F01-038, test tool): BACnetStackTestTool_SetAccessPointLockout returns false, and changes nothing, when its time is not an actual date and time, as BACnetStackTestTool_AccessPointRequestAccess does (IFC-134).
  • rb v-20261018-2 (#3824 CR3-F01-041, test tool): BACnetStackTestTool_SetTimerPriorityForWriting returns false for priority 0 or above 16 and keeps the stored value; it used to clamp to 16 and return true (IFC-134).
  • rb v-20261018-3 (#3676 CR3-F03-042): a received BACnetDeviceObjectPropertyValue (an extended notification parameter) whose device-identifier is not a Device object is decoded rather than rejected, as the other device-identifier references are (cl. 21.6). The stack’s own setter still refuses one.
  • rb v-20261018-4 (#3825 CR3-F02-004): a Trend Log no longer keeps unread default copies of Stop_Time, Log_Interval, Client_COV_Increment and Trigger in the device’s stored-value store; its engine serves them, as before. One visible effect: Client_COV_Increment is read-only by default, and the Trend Log engine has no write for it. A host that made it writable used to see a WriteProperty of it succeed while the value read back unchanged; that write is now refused with writeAccessDenied.
  • rb v-20261018-5 (#3677 CR3-F05-034): the per-source rate limiter behind Reject-Message-To-Network and the stray-segment Abort no longer gives every new source a fresh burst once its 8-entry table is full. A new source then takes over the bucket of the least-recently-used source it evicts, so a flood rotating through many addresses draws a bounded number of replies. While a slot is free a new source still starts with a full burst.
  • rb v-20261018-5 (#3677 CR3-F05-021, test tool): BACnetStackTestTool_SendConfirmedRequestRaw accepts up to 65529 octets of service data (it was 1491) and sends a payload too large for one APDU as a segmented request (IFC-135).
  • rb v-20261018-6 (#3762 CR3-F22-006): BACnetStack_AddLoggedObjectToTrendLogMultiple called on a Trend Log Multiple that already holds records keeps them and gives each record holding logged values an error (class PROPERTY, code LOGGED_VALUE_PURGED) for the new Log_Device_Object_Property entry (135-2024 cl. 12.30.11). Before, old records had one value fewer than new ones, so a reader of Log_Buffer could not tell which value belonged to which entry. If the records cannot be updated (out of memory), the call returns false and the entry is not added (IFC-136).
  • rb v-20261018-7 (#2836 part b): a WriteGroup now converts the value it propagates to each Channel member into the member property’s datatype, by 135-2024 Table 12-63 and the coercion rules of cl. 12.53.5. A member in this device takes the datatype from its property profile; a member in another device takes the standard’s datatype for that property. A value the table refuses, or one outside a rule’s range, is not written to that member. Examples:
    • a Real 1.0 to a Binary Output’s Present_Value is written as ENUMERATED 1;
    • a Boolean TRUE to a Binary Lighting Output commands it ON;
    • an Unsigned 3 to a remote Analog Value’s Present_Value is sent as REAL 3.0;
    • a Real 3.0E9 to an Unsigned property is not written. A Real or Double converted to an integer is truncated toward zero. Rules 5 and 6 take the Integer range -2147483000 to 2147483000; the printed upper limit 214783000 is read as a typo (owner ruling on #3886).
  • rb v-20261018-8 (#2836 part d): the Channel object no longer presents Execution_Delay or Allow_Group_Delay_Inhibit. Both are optional properties, and the stack applies no execution delay: every member is written when the Channel is. Reading or writing either property now answers UNKNOWN_PROPERTY, and Property_List no longer lists them. Before, Execution_Delay was listed, readable and writable while having no effect. BACnetStack_AddObjectPropertyReferenceToChannel still accepts its executionDelay argument; pass 0. An EPICS that lists either property should drop it (owner ruling on #3886).
  • rb v-20261018-8 (#2851): a Load Control’s Requested_Shed_Level now reads back the LEVEL that was written. The object still acts on that level snapped down to the nearest lower Shed_Levels entry (cl. 12.28.18): the shed callback is asked for the snapped level, and Expected_Shed_Level and Actual_Shed_Level follow it. For example, with Shed_Levels {2, 5}, writing LEVEL 3 reads back 3, the callback receives 2, and Expected_Shed_Level reads 2. Before, Requested_Shed_Level read back 2. If Shed_Levels is later changed so that no entry is at or below the written LEVEL, the object acts on the lowest entry. A non-zero LEVEL below every Shed_Levels entry is still refused with VALUE_OUT_OF_RANGE (owner ruling on #3886).
  • rb v-20261018-9 (#3088), test-tool builds only: CASBACnetStackTestToolDLL.h now declares its PrivateTransfer, AuditLogQuery and Device Authorization exports in every build; before, it declared them only when the matching STACK_OPTION_* macro was defined. The header includes no options header, so before this a program that included it before CASBACnetStackOptions.h silently lost those thirteen declarations. The exports are now defined in every test-tool build as well: with the option compiled out, each logs an error and returns false. In a test-tool build only PrivateTransfer can be compiled out (STACK_OPTION_DISABLE_PRIVATE_TRANSFER); before this change a program that called the two PrivateTransfer sends failed to link in that build (owner ruling on #3886).
  • rb v-20261019-29 (#3681 CR3-F09-047): the BACnet/SC state-change callback now reports the tracked Network Port’s device and object instances for the transitions the stack drives on its own direct-connect socket (websocket status and initiating-peer state), as it already did for a websocket status the host reports. They used to report 0 and 0. The transitions the hub connector drives itself still report 0 and 0. The BACnetStack_RegisterCallbackBACnetSCStateChange documentation says which transitions carry the instances (IFC-146). Revert-red: the new test Issue3681_StackDrivenDirectConnectTransitionsCarryTheTrackedNetworkPort fails with the old emitter.
  • rb v-20261019-38 (#888): no library change. Research into an opt-in thread-safe build found that it needs no change to any exported function or callback signature, so it can come in a later minor release. The stack’s single-threaded contract is unchanged.
  • rb v-20261019-39 (#892): no library change. Research into running the stack on a big-endian host found the BACnet wire codecs already convert byte order correctly. A port would mainly need explicit little-endian reads of the public packed buffers, plus a big-endian test run. Big-endian builds stay refused by the existing compile-time guard, and the packed buffers stay little-endian by design.
  • rb v-20261020-2 (#4209): no behaviour change. Following the owner’s ruling, SubscribeCOVPropertyMultiple keeps skipping a COV subscription specification whose list of COV references is empty, and still answers Result(+); an empty first subscription creates no context. The decision is now commented where it happens and pinned by two tests.
  • rb v-20261020-3 (#3470): the four test-tool COV-notification sends (BACnetStackTestTool_Send{Confirmed,Unconfirmed}COVNotification and their Multiple forms) refuse a propertyValuesCount or notificationCount of 0 and return false without sending. Before, they sent an empty ‘List of Values’ or ‘List of COV Notifications’, which cl. 13.6.1.1.5, 13.7.1.1.5, 13.17.1.1.5 and 13.18.1.1.5 do not allow. A deliberately malformed request still goes through the Send*RequestRaw exports (IFC-163; owner ruling).
  • rb v-20261020-3 (#3805): an Unsigned written to array index 0 of a host-held resizable array now reaches CallbackSetPropertyUInt at index 0 with the new length, the same call a whole-array write of another length already made. The arrays are Channel Control_Groups (with WriteGroup support), Multi-state State_Text, Global Group Group_Members and Group_Member_Names, Command Action_Text, and Structured View Subordinate_List and Subordinate_Annotations. Before, such a write to an array whose elements are not Unsigned answered Error(PROPERTY, WRITE_ACCESS_DENIED) without asking the host. Arrays the standard sizes are unchanged, and a host with no CallbackSetPropertyUInt keeps the old routing (cl. 12.1.5.1; IFC-165; owner ruling).
  • rb v-20261020-3 (#3472): no behaviour change. Following the owner’s ruling, an Access Zone keeps matching a credential in Credentials_In_Zone and the passback record by object type and instance only; the single-site decision (cl. 12.32.16) is now commented in BACnetStackAccessZone.cpp and in the BACnetStackTestTool_AccessZoneEnter doc (IFC-164).
  • rb v-20261020-4 (#2601): each BACnet/SC port now has one VMAC, used on its hub connection, by its hub function (the VMAC in its Connect-Accept, cl. AB.5.3.1), on direct connections (Connect-Request, Connect-Accept, and the addressed-to-me and duplicate checks) and as the Network Port’s MAC_Address. With none configured (no BACnetStack_SetBACnetSCHubConnector call, or a NULL or zero-length VMAC) it is the UUID’s last six octets in Random-48 form (cl. H.7.3), so it stays the same across restarts. Before, the hub connection drew a random VMAC while the hub function and direct connections presented a second, UUID-derived one; a configured VMAC is now presented on those paths too. The hub function accepts a Connect-Request carrying the port’s VMAC with the device’s own UUID (its own hub connector, cl. AB.5.3), and still refuses that VMAC with any other UUID as NODE_DUPLICATE_VMAC. Two BACnet/SC ports that configure no VMAC now share the same default, since the UUID is device-wide; VMACs need be unique only within one BACnet/SC network. Delivering the node’s own traffic through that connector, rather than in-process, is #4235 (IFC-167; owner ruling).
  • rb v-20261020-4 (#2595): after a direct connection’s Connect-Request is refused with NODE_DUPLICATE_VMAC, the port chooses a new random Random-48 VMAC before it retries (cl. AB.6.2.2), as it already did on the hub connection, and closes a live hub connection so that it reconnects with the new VMAC; the UUID does not change. Before, the retry carried the same VMAC. The new VMAC lasts until a restart or reconfiguration (IFC-167; owner ruling).
  • rb v-20261021-1 (#2613): the BACnet/SC state-change callback now carries the configured Network Port’s device and object instances for the transitions the hub connector drives itself (its state, the hub-connection status, and its websockets’ status and initiating-peer state), as host-reported and direct-connect transitions already did. Before, these reported 0 and 0. A host that filtered on device 0 / port 0 now receives them with the real instances (IFC-169; owner ruling, CR2-F09-041).
  • rb v-20261021-1 (#3681): when a certificate procedure (AddIssuer, ReplaceOperational, RemoveIssuer) fails after a file write has taken effect on the target (a later AtomicWriteFile block, or the File_Size write after the certificate is written), or a write step’s request times out or is aborted, the stack first writes Command := DISCARD_CHANGES to the target’s Network Port, then reports the original failure for the step that failed. Before, the target could be left with Changes_Pending TRUE and a partly written certificate that the next WARMSTART or ACTIVATE_CHANGES would put into use. The discard also drops anything else staged on that port (cl. 12.56.100, 12.56.101; IFC-169; owner ruling, CR3-F09-031).
  • rb v-20261021-1 (#3681): the hub function distributes a broadcast BVLC-Result, Advertisement or Advertisement-Solicitation to every other hub connection (cl. AB.5.3.3), as it already did Proprietary-Message and Address-Resolution; a broadcast Advertisement still updates the sender’s maximums. A node’s hub connector no longer answers a broadcast Advertisement-Solicitation (cl. AB.2.9 makes it unicast), so a distributed one draws no Advertisement from every node (owner ruling, CR3-F09-016).
  • rb v-20261023-2 (#4404, slice 1 of #3165): on a BACnet/IP Network Port, a write of IP_Address, IP_Subnet_Mask, IP_Default_Gateway, IP_DHCP_Enable or BACnet_IP_UDP_Port (each read-only unless the host makes it writable) is now held pending by the stack, as the standard requires (cl. 12.56.26 to 12.56.29, 12.56.32): Changes_Pending reads TRUE, a client’s read returns the written value, and the value is handed to the host’s Set callback at ReinitializeDevice ACTIVATE_CHANGES or WARMSTART (or BACnetStack_ActivateNetworkPortPendingChanges()), with the value in use handed back if the host refuses another pending change in the same activation. DISCARD_CHANGES drops the pending values. Before, the write went straight to the host’s Set callback and Changes_Pending stayed FALSE, because the stack, which alone computes Changes_Pending (#2413), held nothing pending. What a host sees now: its Set callback for these properties is called at activation, not at the write; a write is refused with Abort(other) unless CallbackReinitializeDevice and the Set callback of the property’s type are registered; an address that is not 4 octets, or a UDP port over 65535, is refused with VALUE_OUT_OF_RANGE; IP_Address, IP_Subnet_Mask and IP_Default_Gateway are refused with WRITE_ACCESS_DENIED while IP_DHCP_Enable reads TRUE (cl. 12.56.26, 12.56.28, 12.56.29); a write on a port that is not BACnet/IP is refused with WRITE_ACCESS_DENIED. While a change is pending, the stack’s own uses of the port’s address - MAC_Address, the BACnet/IP data link’s local address, a BBMD’s own BDT entry - keep the address in use; MAC_Address follows at activation, the other two when the host adds the port again (as for any address change the host makes). Pending addresses and a pending IP_DHCP_Enable TRUE written together are handed to the host in one activation, IP_DHCP_Enable first; a host that refuses the addresses once DHCP is on makes every activation fail, and keeps everything pending, until DISCARD_CHANGES.
  • rb v-20261103-site-review: bacnetstack.com, from colleague review. The site is light only, with no theme control. Compare moved from the landing page’s section bar into the header. The section bar sits flush against the header and marks the section being read. The Profiles table keeps its profile codes and sideways scrollbar on screen, and its BIBB families collapse. The manual uses the same header as the rest of the site, and the landing page links the datasheet.
  • rb v-20261104-release-prep: preparation for the first customer release of version 6, which will be 6.1.0. The npm package version now matches the stack version, and was reset from 6.1.2 to 6.0.23 until that release (owner, 2026-11-04). The datasheet is at revision 12 and describes 6.1. The migration chapter no longer says version 6 is unreleased, drops its “Still being finalized” section, lists BACnetStack_SetMaxOutstandingConfirmedRequests() and BACnetStack_SetAllowIndefiniteCOVLifetime() as removed, and says the two discussed rename groups are not in version 6. Every GitHub Actions workflow moves from the Node 20 action releases to the Node 24 ones, with setup-node’s automatic caching turned off where a step had no cache before.
  • rb v-20261105-docs-restructure: the manual’s BACnet/SC chapter is now one sidebar group of seven pages under Data links: an overview, the integration guide, certificates, transport callbacks, operations and troubleshooting, moving from BACnet/IP, and the reference. It replaces the “Why BACnet/SC”, “BACnet/SC implementation manual” and “BACnet/SC reference” pages, whose old addresses now forward to the new pages. BACnet/IP (overview, sending and receiving, the Network Port object, practical notes) and BACnet MS/TP (overview, byte and packet mode, the Network Port and settings, practical notes, moving from MSTP.c) are page groups the same way, at their old addresses. Their example programs are no longer copied into the manual: each overview links its example on GitHub instead (BACnetProfileExample-B-SS-CPP for BACnet/IP, BACnetProfileExample-B-BC-CPP for MS/TP), which is kept up to date with the stack, and the old example addresses forward there. The ReinitializeDevice guide’s 166-line example program is replaced the same way, by a link to BACnetProfileExample-B-BC-CPP and a description of how it handles the service. The data links overview’s list of example projects names the same two in place of the older BACnetServerExampleCPP and BACnetServerMSTPExampleCPP. The feature-by-feature changes from version 5 are now section 7 of the upgrade guide instead of a page of their own, and that address forwards too. The “Audience” lines at the top of ten manual pages are gone. A review of the hand-written chapters against the source corrected 69 points, among them: the client examples no longer send on BACNET_NETWORK_PORT_DEFAULT, which builds with more than one data link refuse; the Quick Start’s debug callback no longer adds a second newline, and its send callback follows the current broadcast rules; the BACnet/SC pages now say to call BACnetStack_ActivateNetworkPortPendingChanges() after binding the certificate files, and give the real defaults and limits of BACnetStack_SetBACnetSCSettings(), the hub-connector state mapping and the failback behaviour; the upgrade guide’s CreateObject element layout, datatype tags and DeviceCommunicationControl DISABLE handling are corrected; the threading rule names its two exceptions; the BACnet/SC glossary is merged into the manual’s glossary. The landing page says thousands of people a day use the CAS BACnet Explorer (owner, 2026-11-05).
  • rb v-20261106-design-guides: the design-guides submodule moves to chipkin-design-guides main (c5f7f9b), which adds the CAS BACnet Stack version 6 facts and the bacnetstack.com links to standards/branding/facts.md, names MS/TP “Multidrop Serial Bus/Token Passing” in voice-and-tone.md, and adds the open-items drift audit under reviews/. Nothing the manual build reads changed.
  • rb v-20261106-design-guides: the site’s origin is now https://www.bacnetstack.com (owner, 2026-11-06; design-guides Q-203), so every canonical URL, og:url, sitemap entry and the robots.txt sitemap line carry the www form, as do the README and the Quick Start program’s link. check-site-claims.js pins that origin, and its R16 now also requires robots.txt to name exactly the sitemap on it; self-test cases cover a bare-host canonical, a bare-host robots.txt and a missing one (105/105).
  • rb v-20261106-docs-review2: four more reviews of the manual’s hand-written chapters against the source corrected about 70 further points, some of them introduced by the previous round’s corrections. Among them: the DeviceCommunicationControl guide now says the host’s callback runs first and a DISABLE is refused after it; AtomicReadFile and AtomicWriteFile without a file callback get an Error (services, optional-functionality-not-supported), not an Abort; the BACnet/SC transport callbacks carry whole BVLC-SC messages, broadcast is always false on BACnet/SC, and the examples keep the 6144-octet default instead of lowering it; the stack enables the SC hub connection status properties itself, the SC Network Port’s Out_Of_Service is writable, and validate-changes works once its callback is registered; CallbackDisconnectWebsocket is required in every role; the certificate File example serves File_Size, without which every stream read is refused, and the CSR File is read-only; the node-only BACnet/SC build example selects the embedded target; JSON decoding needs XML decoding; the XML/JSON page describes the BACnet/SC decode output; BACnet/IPv6 is listed with the other data links; the presets page lists every CMake option, STACK_OPTION_THREAD_SAFE included; the upgrade guide names BACnetClientCOV in the decode output, says Notification Class Recipient_List is read-only by default, and no longer lists three functions that never shipped in version 5.
  • rb v-20261109-1 (#5007): _spec/interface/IFC-210.md records the comment-only edits that four batches (#4839, #4974, #4976, #4981) made to CASBACnetStackDLL.h, BACnetCallbackInterface.h and CASBACnetStackTestToolDLL.h without an entry.
  • rb bs-20261024-4 (#4314): CMake presets for the build without BACnet/SC (STACK_OPTION_DISABLE_DATA_LINK_SC=ON): linux-x64-release-nosc, linux-x64-release-shared-nosc, windows-x64-nosc and windows-x64-dll-nosc (with windows-x64-nosc-release and windows-x64-dll-nosc-release build presets), so the NoSC library no longer needs a hand-typed -DSTACK_OPTION_DISABLE_DATA_LINK_SC=ON. They leave the BACnet/SC sources in the tree, where they compile to nothing; the NoSC release package also strips them with _ci_scripts/no_sc.sh. They install to the same file names as the SC builds.
  • rb bs-20261024-71 (#4565): test-tool surface only (BACnet Testing Tool builds). BACnetStackTestTool_AddAuditReporter(deviceInstance, objectInstance) creates the stack-side state of an Audit Reporter object that has been added, so a test-tool host can configure one; and BACnetStackTestTool_RegisterCallbackAuditNotificationReceivedForDevice registers a receive callback with the receiving local device’s instance as its first argument (used in preference to BACnetStack_RegisterCallbackAuditNotificationReceived when both are registered). The customer header is unchanged.
  • rb bs-20261024-75 (#4557): the stack now remembers who commanded each priority slot of a commandable object the host manages (one the stack holds no priority array for), and answers Value_Source, Value_Source_Array, Last_Command_Time and Command_Time_Array for it when the application enables them with SetPropertyEnabled. A Present_Value write or relinquish the host accepts is recorded with its source (the writing device’s address, or the Channel object for a Channel write) and time; a write the host refuses records nothing, a relinquish clears who may write Value_Source back, and the record follows an object rename. Objects that did not enable these properties are unchanged.
  • rb bs-20261024-81 (#4556): MS/TP. The engine can now run more than one instance in a process. struct MSTPPort (MSTP.h) holds one engine’s state; MSTP_PortAttach binds it to caller-owned MSTPRunningVars, MSTPSettings and MSTPCallbackFunctions, and MSTP_PortInit, MSTP_PortLoop, MSTP_PortIsConnected, MSTP_PortSetBaudRate, MSTP_PortSetMaxMaster, MSTP_PortSetRS485EnableTXDPin, MSTP_PortInsertFrameToSend and MSTP_PortReportReceiveError drive an attached port. MSTP_Init, MSTP_Loop and the rest still drive the default port, bound to the global mstp_runningVariables, mstp_settings and mstp_callbackFunctions, so existing hosts are unchanged. Ports are driven one at a time, as before.
  • rb bs-20261024-82 (#4560): internal, not yet reachable by a host. BACnetStackDeviceAddressProxy, the data of device address proxying (cl. 19.9.1): the Device_Address_Proxy_Table per proxied network (bounded, at least 128 entries, never evicting), learning from I-Ams, revalidation after 24 hours or a restart with two unicast Who-Is 10 seconds apart, offline detection within Device_Address_Proxy_Timeout (at most 300 seconds), and the Max_Proxied_I_Ams_Per_Second budget. It is not yet used by the network layer, and the Network Port properties stay unsupported.
  • rb bs-20261024-85 (#4560): internal, off by default and not reachable by a host yet. Device_Address_Proxy_Enable and Device_Address_Proxy_Timeout are held by every Network Port type (they were the BACnet/SC port’s alone), writable by WriteProperty on a port other than BACnet/SC with the timeout held to 1 to 300 seconds (cl. 12.56.64); Device_Address_Proxy_Table on the Device is served from the stack’s proxy table; RESTART_DEVICE_DISCOVERY forgets a non-SC port’s proxied devices (cl. 12.56.16). All of it exists only once BACnetDBDevice::SetDeviceAddressProxySupported(true) is called, which nothing does yet: the proxying behaviour (answering Who-Is, learning from I-Ams) is the next slice.
  • rb bs-20261024-86 (#4560): internal, off by default and not reachable by a host yet. Device address proxying on a physical router (cl. 19.9.1), once BACnetDBDevice::SetDeviceAddressProxySupported(true) is called: an I-Am from a device on a proxied network is learned and not forwarded; a Who-Is is answered with unicast I-Ams for the proxied devices in range, each carrying the device’s network and MAC as SNET/SADR, within Max_Proxied_I_Ams_Per_Second (default 10, queued); a Who-Is is not forwarded into a proxied network; a device unheard for 24 hours or for Device_Address_Proxy_Timeout is asked with a unicast Who-Is and removed if it does not answer. Not done: Who-Is requests to other routers on a proxied network (cl. 19.9.1.1), and turning the function on for a router.
  • rb bs-20261024-87 (#4560, owner-approved 2026-10-26): a device whose physical router is enabled (two or more router ports) now performs device address proxying (cl. 19.9.1). Visible to a client: the Device object gains Device_Address_Proxy_Table and Max_Proxied_I_Ams_Per_Second (writable, greater than zero, default 10) and each Network Port gains Device_Address_Proxy_Enable (default FALSE) and Device_Address_Proxy_Timeout (default 300), all in Property_List. A port proxies nothing until its Device_Address_Proxy_Enable is written TRUE, so a router that nobody configures behaves as before. A host that registered CallbackGetPropertyUInt for Max_Proxied_I_Ams_Per_Second is bypassed while the device proxies (the stack holds it). Not done: Who-Is requests to other routers on a proxied network (cl. 19.9.1.1), retention of the table over a restart, and any 135.1 test run.
  • rb bs-20261024-88 (#4560): a device that proxies a network (cl. 19.9.1.1) now also sends a Who-Is that its proxy table cannot answer in full - one with no device range, or a range naming more devices than the table holds - to each router learned on a proxied network, as a unicast carrying the global broadcast DNET, so devices behind that router can answer. Once per router per request; nothing is sent when no network is proxied.
  • rb bs-20261027-10 (#769, slice 3 of 4): the stack can now run the MS/TP engine itself. Register BACnetStack_RegisterCallbackMSTPReceiveBytes(), ...MSTPSendBytes() and ...MSTPMicroseconds() (and, optionally, ...MSTPTransmitEnable() and ...MSTPFrameHeader()), and call BACnetStack_LoopMSTP(networkPortInstance) from your main loop or from a thread of its own: the stack then frames, checks and passes MS/TP frames, answers Poll For Master, handles token loss, receives COBS-encoded Extended Data frames, and sends them for NPDUs over 501 octets that a router or the host supplies (the stack itself still limits APDUs on MS/TP to 480 octets), with no MSTP.c in your build. MSTP.c and MSTP.h are still there for packet mode: removing them and moving the Zephyr adapter to this interface is slice 4 of #769 and is not done. BACnetStack_ReportMSTPReceiveError() takes a UART error (safe from an interrupt) and BACnetStack_GetMSTPCounters() reads frames heard / for this station / sent / dropped, octets lost, receive errors, whether the node is in the ring, the last frame’s header, and the frames the MS/TP data link discarded for a bad connection string, using the new BACnetMSTPCounter enumeration. A port is in this mode only while the three required callbacks are registered; with none registered it behaves exactly as before (packet mode), and registering NULL for one of them stops the engine and returns the port to packet mode. The engine takes MAC_Address, Max_Master (maxManager in BACnetPropertyIdentifier), Max_Info_Frames and Link_Speed from the port’s Network Port object through the host’s property callbacks, at start-up and after each activation of pending changes. BACnetStack_LoopMSTP() takes no lock and may run on its own thread; it shares with BACnetStack_Tick() only a 4-NPDU outbound queue, an 8-NPDU inbound queue, a configuration snapshot and the counters, all lock-free. Call it at least every 20 ms. The functions exist in every build and return false or do nothing without MS/TP. The Node addon has no twin of them: Node keeps packet mode. See the MS/TP chapter of the manual and IFC-197.
  • rb bs-20261027-3 (#3703): STACK_OPTION_COV_REFUSE_INDEFINITE_LIFETIME (off by default). When defined, a SubscribeCOV whose Lifetime is 0 or absent is refused with SERVICES / VALUE_OUT_OF_RANGE, so a peer that fills an object’s subscription slots holds them only while it keeps renewing. A cancellation is unchanged, and SubscribeCOVProperty already refused a Lifetime of 0. Bound the total with BACnetStack_SetMaxActiveCOVSubscriptions — revert-red: the new refusal tests fail with the check removed.
  • rb bs-20261027-3 (#3652): STACK_OPTION_MAX_FOREIGN_DEVICE_TABLE_ENTRIES_PER_IP, defaulting to STACK_OPTION_MAX_FOREIGN_DEVICE_TABLE_ENTRIES (so behaviour is unchanged). Lower it to stop one host that varies its UDP source port from filling the foreign-device table; a new registration past one IP’s share gets the Register-Foreign-Device NAK, and a re-registration of an existing entry is never refused — revert-red: the new per-IP tests fail with either AddForeignDevice check removed.
  • rb bs-20261027-8 (#769, slice 2 of 4; #769 stays open): STACK_OPTION_DISABLE_DATA_LINK_MSTP (CMake option, default OFF) leaves the MS/TP data link out of a STACK_OPTION_TARGET_FULL / STACK_OPTION_ENABLE_ALL_DATA_LINK build, as STACK_OPTION_DISABLE_DATA_LINK_SC does for BACnet/SC; BACnetStack_GetInfo then reports dataLinkMSTP as disabled, and defining it together with STACK_OPTION_DATA_LINK_LAYER_MSTP is a preprocessor #error. New gate _ci_scripts/check-mstp-optout.sh (pre-push) — revert-red: removing the opt-out guard, or the contradiction #error, fails it.
  • rb bs-20261027-9 (#769, slice 1 of 4; #769 stays open): BACnetMSTPEngine (source/BACnetMSTPEngine.{h,cpp}), a per-port C++ MS/TP engine that reproduces MSTP.c’s receive and master-node state machines (cl. 9.5, COBS Extended Data frames, CRC-32K, echo suppression, receive-error report) with no global state, no per-frame allocation, one wrapping microsecond clock instead of the timer pair, and no blocking wait. Not wired into the stack yet and MSTP.c is unchanged; the 104 MSTP.c tests are ported to it plus 12 added (mapping in _agents/handoffs/769-slice1-test-mapping.md, 12 adapted with reasons) — revert-red: one-change mutations for every fix group, each failing at least one new test (769-slice1-red-green.md). A host must advance the clock: with a frozen clock the engine never sends (no blocking wait).
  • rb bs-20261028-1 (#4609, BACnet/IPv6 slice 1 of 4, #255; owner ruling 2026-10-26): the BACnet/IPv6 data link (Annex U), in a build with STACK_OPTION_DATA_LINK_LAYER_IPV6 (on in STACK_OPTION_TARGET_FULL, and usable on its own for a stack that speaks BACnet/IPv6 only). BACnetBVLLIPv6 encodes and decodes the BVLL-IPv6 header of every message in Table U-1 (the BBMD ones are decoded and dropped until slice 4); BACnetIPv6Packet and BACnetDataLinkIPv6 send an NPDU for one peer as an Original-Unicast-NPDU and one for everyone as an Original-Broadcast-NPDU, answer an Address-Resolution for the device with an Address-Resolution-ACK and a Virtual-Address-Resolution with a Virtual-Address-Resolution-ACK, discard a unicast for another VMAC (Annex U.3) and the device’s own multicast heard again, and keep a 64-entry VMAC table (Annex U.5) learned from each incoming message, the peer heard from longest ago forgotten first. The VMAC address is the device instance in three octets (H.7.2). BACnetPacket::NetworkType_IPv6 (3) and BACnetDataLink::DataLinkType_ipv6 (3) are the fourth values; GetMinimumRoutedNpduLengthForNetworkType answers Table 6-1’s 1497 octets for it (its default, 228, would have under-permitted it). The host’s callbacks carry an 18-octet connection string (the 16-octet IPv6 address and the 2-octet UDP port): documented in comment-only edits to CASBACnetStackDLL.h (the adapters regenerated), and a broadcast is sent with no connection string and the broadcast flag set, for the host to send to the FF0X::BAC0 multicast group. In a build with BACnet/IPv6 as the only transport the default Network Port resolves to it; in a build with several transports the IPv6 Network Port object (#4611) is what will name it, so the host’s send and receive callbacks cannot name it yet. BACnetInterface.cpp includes ChipkinConvert.h itself: an IPv6-only build includes no IPv4 header that brought it in. Known limits of this slice, for #4610 to close: a unicast reply is addressed by the peer’s IPv6 address, so a host or router carrying several devices behind one address and port can have a reply sent with another device’s VMAC (the Source-Virtual-Address of the request will be carried); the VMAC table is learned before a message is validated and holds 64 entries, so a flood of spoofed sources can evict real peers; a Forwarded-Address-Resolution is dropped. Not here yet: sending to a peer that has not spoken (#4610), the Network Port object (#4611), BBMD and foreign devices (#4612).
  • rb bs-20261028-2 (#4611, BACnet/IPv6 slice 3 of 4, #255): the Network Port object of a BACnet/IPv6 port (Table 12-71.4). BACnetStack_AddNetworkPortObject with network type IPV6 (9) adds one at the BACNET_APPLICATION protocol level, with Network_Number, Network_Number_Quality, APDU_Length, MAC_Address (the device’s VMAC, read only), Virtual_MAC_Address_Table (following the data link’s VMAC table), BACnet_IPv6_Mode (normal), BACnet_IPv6_UDP_Port, BACnet_IPv6_Multicast_Address (the 18 octets of [FF02::BAC0]:47808 until the host serves another or a client’s write is activated), IPv6_Address, IPv6_Prefix_Length, IPv6_Default_Gateway and IPv6_DNS_Server. The object claims the BACnet/IPv6 data link, so the host’s per-port callbacks name it by its instance and the sends carry it; a second IPv6 Network Port object is refused (one BACnet/IPv6 data link per stack). The BACnet/IP foreign device and BBMD settings no longer consider a port of type IPV6 (they would have used it as a BACnet/IP port), and RENEW_FD_REGISTRATION on an IPv6 port is refused with VALUE_OUT_OF_RANGE, its mode being NORMAL. Before, an IPv6 Network Port object was refused as an unsupported network type.
  • rb bs-20261028-2 (#4610, BACnet/IPv6 slice 2 of 4, #255): VMAC resolution (Annex U.5). A message to a peer whose VMAC is not known is held (at most 4, each for at most 3 s) while a Virtual-Address-Resolution asks the peer’s address for it, and goes out when the ACK arrives, even when the table is too full of recent peers to remember it; a confirmed request’s retries are given the VMAC too, and a retry of a request still held replaces it rather than being sent twice. Address-Resolution is not sent: the stack always knows a peer’s address and only its VMAC can be unknown, which U.5 resolves with Virtual-Address-Resolution. Without CallbackGetSystemTime nothing can be held: the peer is asked and the message refused. A reply goes to the Source-Virtual-Address of the request it answers, so several devices behind one address and port each get their own. A Forwarded-Address-Resolution for a device here is answered with an Address-Resolution-ACK at the asker’s address, unless that address is multicast or unspecified. The VMAC table learns a sender only once the network layer has taken its NPDU, forgets a peer not heard from for an hour, refuses a new peer while it is full of peers heard in the last minute (a flood of made-up VMACs no longer pushes out real peers), and refuses a VMAC heard from a second address while the first is still speaking (two devices with one instance, or one copying another’s VMAC); after a minute of silence the table follows a device that moved.
  • rb bs-20261028-3 (#4612, BACnet/IPv6 slice 4 of 4, #255): BACnet/IPv6 BBMDs and foreign devices (Annex U.4). The BACnet/IPv6 data link runs as NORMAL, FOREIGN or BBMD. Every node now answers a Register-Foreign-Device, Delete-Foreign-Device-Table-Entry or Distribute-Broadcast-To-Network it receives while not a BBMD with the matching BVLC-Result NAK (X’0090’, X’00A0’, X’00C0’), by unicast to the sender, and delivers a Forwarded-NPDU as a broadcast from its original source, learning that VMAC at the original address when it is not already known (a Forwarded-NPDU moves a known VMAC only once its address has been silent for 60 seconds, and never without CallbackGetSystemTime); a malformed message is still answered with nothing. A BBMD keeps a Broadcast Distribution Table of up to 16 peer BBMDs (never itself) and a Foreign Device Table bounded by STACK_OPTION_MAX_FOREIGN_DEVICE_TABLE_ENTRIES (128) overall and STACK_OPTION_MAX_FOREIGN_DEVICE_TABLE_ENTRIES_PER_IP (128) per IPv6 address, each entry living its Time-to-Live plus 30 seconds. It forwards a broadcast from its link to its peers and foreign devices as a Forwarded-NPDU, its own broadcasts included when the node’s own address is known; multicasts a Forwarded-NPDU from a peer and sends it to its foreign devices, and discards one from any other sender; distributes a Distribute-Broadcast-To-Network from a registered foreign device everywhere but back to it, delivering it locally too (a NAK for anyone else, or when no copy could be sent anywhere); relays an Address-Resolution whose target VMAC is not this node’s as a Forwarded-Address-Resolution, and answers a Forwarded-Address-Resolution for itself from any sender, as every node does; accepts, renews, refuses and deletes registrations. A frame whose forwarded form would exceed the stack’s frame maximum (1529 octets) is delivered but not forwarded. A BACnet/IPv6 host that runs a BBMD must pass each datagram’s destination address to the receive call (BACnetStack_RegisterCallbackReceiveMessageForPort‘s destination connection string): the multicast group it arrived on, or this node’s unicast address it was sent to, not the socket’s bound address. A BBMD passes a Forwarded-NPDU or Forwarded-Address-Resolution from a peer on to its link and foreign devices only when it arrived by unicast, so no forwarding loop can form whatever the host passes; it forwards an Original-Broadcast-NPDU or Address-Resolution to its peers and foreign devices unless it arrived by unicast, and answers a unicast Address-Resolution from a node that is not a registered foreign device with an Address-Resolution NAK (X’0030’). Without a destination the BBMD still forwards its own link’s broadcasts to its peers and foreign devices, but does not pass its peers’ broadcasts on (logged once); a host that passes its socket’s bound unicast address for every datagram makes every arrival look unicast, and the BBMD forwards nothing from its own link. A foreign device registers with its BBMD, registers again at half its Time-to-Live (unlike the BACnet/IP foreign device, which the host re-registers), asks again every 10 seconds while unanswered, and after a refusal waits 20 seconds, doubling with each further refusal up to 320; it registers again at once when its BBMD answers a Distribute-Broadcast-To-Network with a NAK. It sends its broadcasts to the BBMD as Distribute-Broadcast-To-Network once it is accepted, and takes Forwarded-NPDUs only from that BBMD. Leaving FOREIGN, changing BBMD, or a new own address at activation sends the old BBMD a Delete-Foreign-Device-Table-Entry for the address it registered, whether that registration was accepted or still awaiting its answer (best effort; it needs the port’s own address). Everything timed needs CallbackGetSystemTime: without it a BBMD refuses registrations and a foreign device registers once.
  • rb bs-20261028-3 (#4612): a BACnet/IPv6 Network Port object is configured as a BBMD or a foreign device through its properties. BACnet_IPv6_Mode, FD_BBMD_Address and FD_Subscription_Lifetime are present and writable on every IPv6 port; BBMD_Broadcast_Distribution_Table, BBMD_Accept_FD_Registrations and BBMD_Foreign_Device_Table (read only, the data link’s table, 18-octet entries) are present only in a build with STACK_OPTION_BVLL_BBMD (which needs BACnet/IP), and in a build without it BACnet_IPv6_Mode refuses BBMD with VALUE_OUT_OF_RANGE. A write is held pending (Changes_Pending TRUE, read back as written) and handed to the data link, with the port’s own address from IPv6_Address and BACnet_IPv6_UDP_Port, at ReinitializeDevice ACTIVATE_CHANGES or WARMSTART; DISCARD_CHANGES drops it. When no BACnet/IPv6 data link serves the port (none claimed it), the configuration stays pending (Changes_Pending TRUE) rather than reading as in use. Activating FOREIGN without an FD_BBMD_Address, or with an FD_Subscription_Lifetime of 0, ends a registration in place. The host’s callbacks are never asked for these values. The BBMD properties are writable only while the mode (in use or written) is BBMD and the FD properties only while it is FOREIGN, otherwise WRITE_ACCESS_DENIED. A BDT entry is a 16-octet IPv6 address and a port with no broadcast-mask; a value holding anything that is not a BACnetBDTEntry (a REAL, an Unsigned) is refused with INVALID_DATA_TYPE, and an empty list is a BDT with no peers; a host name, a mask, the none host, a multicast or unspecified address, or this port’s own address is refused with VALUE_OUT_OF_RANGE, and a 17th peer with NO_SPACE_FOR_OBJECT. FD_BBMD_Address is a 16-octet IPv6 address or the none host; a host name is refused with VALUE_OUT_OF_RANGE. There is no BACnetStack_* function for any of this yet.
  • rb bs-20261030-11 (#3895 item 1, first half; owner ruling of 2026-10-26): a device that generates an event notification or a single-object COV notification now reports it to its audit reporter as a NOTIFICATION record (Table 19-5), once per notification before delivery. An event notification carries Event_State as the Target Property and the new Event_State as the Target Value; a COV notification carries neither. NOTIFICATION is off in a reporter’s default Auditable_Operations (cl. 12.63.10), so nothing changes until a host turns it on. Acknowledgement notifications are not reported. Not yet done: the COV-multiple notifications and the source-side record for a received notification.
  • rb bs-20261030-4 (#4608, slice 1): the opt-in build option STACK_OPTION_THREAD_SAFE (off by default, a CMake option() and a build-configuration macro). When it is on, every export of CASBACnetStackDLL.cpp and CASBACnetStackTestToolDLL.cpp opens with one recursive lock (BACNET_STACK_LOCK(), source/BACnetStackLock.h), so calls from different threads are serialised (the threading contract is documented in a later slice; BACnetStack_LoopMSTP is the exception below). STACK_OPTION_THREAD_SAFE_CUSTOM (a macro, set through CASBACNET_EXTRA_OPTIONS or a define) replaces std::recursive_mutex with BACnetStack_PortLock / BACnetStack_PortUnlock supplied by an RTOS port; the combination with STACK_OPTION_TARGET_EMBEDDED without it is a compile error. Callbacks run with the lock held and still may not re-enter the stack. With the option off nothing changes. BACnetStack_LoopMSTP does not take the lock (it runs on its own thread). Not yet in this slice: the re-entry refusals, the documentation of the contract, the small rand() / gmtime fixes, and the stress test.
  • rb bs-20261030-7 (#4608, slice 4): a tests-tsan CMake preset (and the CASBACNET_TSAN option) that builds the unit tests with ThreadSanitizer and STACK_OPTION_THREAD_SAFE on (a release-time check; it cannot be combined with the AddressSanitizer option), and a stress test that runs only in a thread-safe build: one thread ticks while others add and remove objects, enable properties and send COV subscribe and cancel requests, and the test checks that every invoke id handed to a peer is unique and exactly the expected objects remain. The test binary’s allocation tracker is now thread-safe. This completes the planned work of #4608.
  • rb cov-20261017-1 (#4058): unit tests send every truncation and two single-octet corruptions of a valid encoding at the alarm-and-event decoders: every notification-parameters arm, ConfirmedEventNotification-Request, BACnetEventSummary, GetEventInformation-ACK, AcknowledgeAlarm-Request, every event-parameter arm and the fault-parameter arms. Each frame must be refused, or decode to a value that re-encodes and decodes back to itself, and leave no allocation behind. Test-only; no stack change. Alarms-and-events branch coverage 45.1% to 46.7% (5,426 to 5,630 of 12,044 branches). The one asymmetry found (a context-tagged primitive inside an any-value decodes, but BACnetStackUnknownAPDU::Encode refuses it) is #4075.
  • rb dv-20261019-11 (#4173): in the test-tool build, with an Audit Reporter object, a SubscribeCOV or SubscribeCOVProperty subscription that times out is now reported as an unsubscription, as Table 19-5 asks for the SUBSCRIPTION operation. The stack removes an expired subscription on its periodic sweep of the subscription table, when it next checks that object for a change of value, or when Active_COV_Subscriptions is read, and it reports the timeout then; the notification’s Target Timestamp is that removal time. The notification looks like a cancellation: Target Value FALSE, and Target Property the monitored property for SubscribeCOVProperty only. No request ends the subscription, so the Source Device is this device and there is no Invoke Id. The same Audit_Level and Auditable_Operations rules apply as for a subscription. SubscribeCOVPropertyMultiple is not audited. Revert-red: with ReportAuditSubscriptionTimeout returning early, Issue4173_SubscriptionTimeoutIsReportedAsAnUnsubscription failed at the first expiry (2 notifications, expected 3).
  • rb dv-20261019-12 (#2616): in the test-tool build, with an Audit Reporter object whose Auditable_Operations includes READ (it is off by default), a successful ReadProperty, ReadPropertyMultiple, ReadRange or AtomicReadFile is now reported as a READ operation, as Table 19-5 asks. ReadPropertyMultiple reports each property it read in its own notification, once the whole response is built; a property that answered with an access error is not reported. A request the stack answers with an Error or Abort reports nothing, except an Abort put in place when the response turns out too large to encode, which comes after the report. In ReadProperty and ReadPropertyMultiple, a Device or Network Port wildcard instance (4194303) is reported as the object it stands for. A read carries no Target Value, and a file read has no Target Property. Expect many notifications when READ is switched on: every read is reported, including a ReadRange of a monitored Audit Log’s own Log_Buffer. Revert-red: with the four source files reverted to the base, all five Issue2616 READ tests (ReadProperty, ReadPropertyMultiple, the Device wildcard, ReadRange, AtomicReadFile) failed at their notification-count assertion.
  • rb dv-20261019-13 (#2616): in the test-tool build, with an Audit Reporter object whose Auditable_Operations includes READ (off by default), a successful AuditLogQuery is now reported as a READ of the Audit Log it names, completing Table 19-5’s READ row. The service names no property, so the notification has no Target Property, and a read has no Target Value. A query that fails is not reported. Revert-red: without the change, Issue2616_AuditLogQueryIsReportedAsAReadOfTheLog failed at its notification-count assertion (0, expected 1).
  • rb dv-20261019-134 (#4423, IFC-186): two test-tool Event Enrollment constructors, BACnetStackTestTool_AddEventEnrollmentObjectWithChangeOfValueIncrementAlgorithm (CHANGE_OF_VALUE with the referenced-property-increment criteria, cl. 13.3.3, monitoring a REAL) and BACnetStackTestTool_AddEventEnrollmentObjectWithChangeOfDiscreteValueAlgorithm (cl. 13.3.18), with their Node bindings. Neither form could be created before. A monitored property the stack knows to be the wrong datatype, or a negative or non-finite increment, is refused before anything changes.
  • rb dv-20261019-27 (#4191, #3393 item 3; IFC-161): BACnetStackTestTool_RegisterCallbackProcessAuthRequest, a flat-C form of the test-tool AuthRequest hook, and its Node binding RegisterCallbackProcessAuthRequest. A host behind the flat exports can now answer each received AuthRequest itself: it gets the request’s encoded service parameters and grants with an encoded AuthRequest-ACK, or denies with an error class and code. Before, it could answer only from a stored Authorization Policy. Test-tool only.
  • rb dv-20261019-33 (#3075, CR2-F02-051; owner ruling on #4163 item 13; cl. 12.11.66): the Device’s Audit_Notification_Recipient is now served.
    • It is present while the device has an Audit Reporter object.
    • It reads as the Device with instance 4194303 (uninitialized) until set.
    • It is writable, which is how it is configured; writing the Device with instance 4194303 clears it.
    • A change generates an audit notification sent as a global broadcast (UnconfirmedAuditNotification, DNET 0xFFFF) out of each of the device’s Network Ports. This happens even when the Audit Reporter’s Audit_Level, Auditable_Operations or priority filter would not report the write, and with no recipient configured.
    • A device whose Network Ports resolve to no data link uses the default one. In a build with more than one data link type that cannot be resolved, so such a device (a virtual-router child, say) cannot broadcast: the notification is dropped and the Audit Reporter’s Reliability becomes COMMUNICATION_FAILURE.
    • Test-tool builds (where the audit subsystem is compiled).
  • rb dv-20261019-62 (#2289): _ci_scripts/adapter-signature-gate.js now checks the 54 hand-written (“bespoke”) Node napi wrappers against the header model. It reports an argument that names another parameter of the export (a same-typed swap), a scalar local or info[n] read whose type differs from the parameter’s (a silent conversion), a result assigned to another type than the export returns, and arity. It runs as --napi, on its own pre-push.sh line so it can be deferred alone, and found the eight defects above.
  • rb dv-20261019-75 (#2457, test tool): a WriteProperty this device sends as a client is now audited as an operation source (cl. 19.6.1). When the write is answered, the Audit Reporter marked Audit_Source_Reporter sends an audit notification. It names this device as the source and carries the write’s invoke id, its target device, object, property, priority and value, and, when the write failed with an Error, that Error as the Result. The reporter’s Audit_Level and Auditable_Operations filter these notifications. A Reject, an Abort, a timeout and the other auditable operations are not reported yet.
  • rb dv-20261019-76 (#2457, test tool): a WriteProperty this device sends that ends in a Reject, an Abort or a timeout is now audited as a failed operation. The Result is error class COMMUNICATION with code REJECT_, ABORT_ or TIMEOUT. A reason with no code of its own gives the *_OTHER code, and a proprietary reason gives *_PROPRIETARY.
  • rb dv-20261019-77 (#2457, test tool): a WritePropertyMultiple this device sends is audited as an operation source with one audit notification per object and property it writes (cl. 19.6.5). When the server answers with a WritePropertyMultiple-Error, the targets before the first failed write are reported as successes and the failed one carries the Error. Later targets were not attempted and are not reported.
  • rb dv-20261019-78 (#2457, test tool): DeleteObject, DeviceCommunicationControl and ReinitializeDevice requests this device sends are now audited as an operation source, mapped as in Table 19-5:
    Request Audit operation
    DeleteObject DELETE: the deleted object, with no property
    DeviceCommunicationControl DEVICE_DISABLE_COMM or DEVICE_ENABLE_COMM
    ReinitializeDevice DEVICE_RESET, DEVICE_BACKUP or DEVICE_RESTORE, by state
    ACTIVATE_CHANGES has no audit operation and is not reported.
  • rb dv-20261019-79 (#2457, test tool): AddListElement, RemoveListElement and AtomicWriteFile requests this device sends are now audited as an operation source (Table 19-5 WRITE).
    • List services: the audit names the list property, and its Target Value is the elements added or removed.
    • AtomicWriteFile: the audit names the File object only.
    • Confirmed requests the stack sends on its own are now audited too. Every audited confirmed request goes through one path, so a Schedule’s or Channel’s write to another device is now reported, as cl. 19.6.1 requires of an operation source. So is each AtomicWriteFile of a BACnet/SC certificate procedure.
  • rb dv-20261019-80 (#2457, test tool): ReadProperty, ReadPropertyMultiple, ReadRange, AtomicReadFile and AuditLogQuery requests this device sends are now audited as an operation source (Table 19-5 READ), when the Audit_Source_Reporter’s Auditable_Operations selects READ. The default leaves READ out (cl. 12.63.10).
    • ReadProperty and ReadRange: the audit names the object and property read. A read has no Target Value.
    • ReadPropertyMultiple: one audit per object and property read. A result answered with a property access error is that target’s failure, and carries its Result.
    • AtomicReadFile: the audit names the File object only.
    • AuditLogQuery: the audit names the Audit Log object only.
    • Decided when sent: whether an operation is audited is decided when it is requested. A request this device sends while its operation is filtered out is not tracked for auditing.
  • rb dv-20261019-81 (#2457, test tool): CreateObject, AcknowledgeAlarm and LifeSafetyOperation requests this device sends are now audited as an operation source (Table 19-5 CREATE, ACKNOWLEDGE_ALARM and LIFE_SAFETY).
    • CreateObject: the audit names the created object, as the CreateObject-ACK identifies it, with no initial values. A failed request that named only the object type reports that type with instance 4194303.
    • AcknowledgeAlarm: the Target Value is a BACnetAcknowledgeAlarmInfo, and the Acknowledgement Source is the Source Comment.
    • LifeSafetyOperation: the Target Value is a BACnetLifeSafetyOperationInfo, and the Requesting Source is the Source Comment. A request that names no object is reported without a Target Object.
  • rb dv-20261019-94 (#4256): test-tool export BACnetStackTestTool_RegisterCallbackNotificationForwarderPortFilterChanged. It registers a persistence hand-off that fires after a successful Port_Filter write (whole or indexed) on a stack-held Notification Forwarder, like the Recipient_List and Subscribed_Recipients hand-offs (IFC-181).
  • rb dv-20261019-99 (#2457): an Audit Reporter with Audit_Source_Reporter TRUE now reports a WriteGroup this device sends, as a WRITE. There is one notification per change-list entry, made when the request is sent. It has no Invoke Id, since WriteGroup is unconfirmed. It also has no Target Object, Property or Priority, because the source knows the target only as a channel in a group. The entry’s value is the Target Value. A system-generated Source Comment reads “WriteGroup group G channel C priority P”, where P is the entry’s overriding priority or the request’s write priority. The Target Device is the destination address. For a broadcast it is the network sent to (0 local, 65535 global) with an empty MAC address. Table 19-4 does not cover a target the source cannot name, so this is a modelling decision (option A on the issue). Current Value (slice 4) is complete: a client does not know the value before the write.
  • rb dv-20261030-4 (#957): THIRD_PARTY_NOTICES.md lists the third-party code and content in the release zip and the npm package. Nine items are marked as needing licence review, and the built manual’s bundle was not scanned.
  • rb dv-20261031-10 (#4555): behind STACK_OPTION_DM_SP_B_SUBORDINATE_PROXY, a router answers a Who-Is for the bound subordinates of its other MS/TP ports (cl. 19.9.2.1): one I-Am each, out of the ingress port, with the subordinate’s network and MAC as SNET and SADR, within Max_Proxied_I_Ams_Per_Second. BACnetStackSubordinateProxy reads Max_APDU_Length_Accepted, Segmentation_Supported and Vendor_Identifier once per binding, and no I-Am goes out until they are known. Bound subordinates are mirrored into Device_Address_Proxy_Table and leave it with their binding; one that reports it executes Who-Is is rediscovered on a proxied network (cl. 19.9.2.2). A bound device that stops giving a usable answer, or whose MAC address sends an I-Am naming another device on a proxied network, is read by instance 4194303, and the binding is dropped only if that read finds another device (cl. 19.9.1.2.2). The local-segment form stays on #494, so the option stays out of STACK_OPTION_TARGET_FULL.
  • rb dv-20261031-11 (#4688): behind STACK_OPTION_DM_SP_B_SUBORDINATE_PROXY, a Who-Is on an MS/TP port in byte mode, at a router or at a device that does not route, draws an I-Am for each matching bound subordinate of that port, in a BACnet Data Not Expecting Reply frame with the subordinate’s MAC as its source (cl. 19.9.2.1). They leave one at a time, with the port in the token ring and nothing queued for it, within Max_Proxied_I_Ams_Per_Second, and one still waiting after 5 seconds is dropped and counted. A frame’s echo is not taken for the subordinate. A Who-Is from a routed source is answered through the router that relayed it. On a packet-mode port the send callback has no source address, so the answer is refused, logged and counted: DM-SP-B needs byte mode.
  • rb dv-20261031-12 (#4690): MS/TP subordinate proxying (DM-SP-B) is part of STACK_OPTION_TARGET_FULL. A Network Port in stack-owned byte mode shows four properties (Subordinate_Address_Binding, Manual_Subordinate_Address_Binding, Auto_Subordinate_Discovery and Subordinate_Proxy_Enable) from its first Tick in byte mode. Leaving byte mode takes them away, ends proxying and empties the bound list; the manual list stays.
  • rb dv-20261031-2 (#2331): the npm package carries prebuilt binaries for linux-arm64 (glibc) and darwin-arm64, so those platforms install with no C++ toolchain. An Intel Mac and musl Linux still compile from source. projects/npm/binding.gyp gains the xcode_settings a macOS build needs.
  • rb dv-20261031-6 (#3895): NOTIFICATION audit records for a COV-multiple send, one per object (cl. 19.6.5), and source-side records for a received event or COV notification, whose Target Device is the notifying device (owner ruling 2026-10-31). A subscription’s initial notification is reported too, and a device’s own notification looped back is not reported twice. Test-tool builds only, as the audit feature is.
  • rb dv-20261031-7 (#4679): the test-tool exports BACnetStackTestTool_SendConfirmedEventNotificationWithBufferReadyValues and BACnetStackTestTool_SendUnconfirmedEventNotificationWithBufferReadyValues, and their Node twins, send a buffer-ready event notification.
  • rb dv-20261031-8 (#4553): a build with the new STACK_OPTION_DM_SP_B_SUBORDINATE_PROXY holds four properties (Subordinate_Address_Binding, Manual_Subordinate_Address_Binding, Auto_Subordinate_Discovery and Subordinate_Proxy_Enable) on an MS/TP Network Port, with up to 32 entries per list. WriteProperty, AddListElement and RemoveListElement apply to Manual_Subordinate_Address_Binding, Subordinate_Address_Binding is read-only, and ReadRange by position serves both lists. The option is off by default and is not part of STACK_OPTION_TARGET_FULL: nothing proxies yet (#494).
  • rb dv-20261031-9 (#4554): behind STACK_OPTION_DM_SP_B_SUBORDINATE_PROXY, an MS/TP port reads its subordinates (cl. 19.9.2.2): BACnetStackSubordinateProxy confirms bound and manual entries, and discovers devices by MAC address when Auto_Subordinate_Discovery is TRUE. One read is outstanding per port. RESTART_SUBORDINATE_DISCOVERY now works on such a port, and answers NOT_ENABLED while Subordinate_Proxy_Enable is FALSE. RESTART_DEVICE_DISCOVERY restarts the same detection when Subordinate_Proxy_Enable is TRUE on a device that supports device address proxying, and removes the port’s Device_Address_Proxy_Table entries, whatever Device_Address_Proxy_Enable holds (cl. 12.56.16). The option stays out of STACK_OPTION_TARGET_FULL until the proxied I-Am (#4555).
  • (#3990): a release report. tools/reports/run.sh builds a B-BC reference device against the stack and measures its footprint (flash and static RAM from the linker map, for the B-BC profile and for STACK_OPTION_TARGET_FULL, 64- and 32-bit, and with tools/reports/run-mcu.sh on an nRF52840 and an ESP32-S3 built with Zephyr and adapters/zephyr), its heap (after setup, with the trend logs full, at the peak of a request workload, and per extra object), the latency and instruction count of ReadProperty, ReadPropertyMultiple, WriteProperty, Who-Is and ReadRange, and, with --with-coverage, line, function and branch coverage of source/. Each release publishes the result as the manual’s “Performance, footprint and coverage” page, reports.json and an engineering report, with the change from the previous release. tools/reports/README.md has the details.
  • rb v-20261016-1 (#3980): BACnetDataLinkMSTP counts received frames discarded for their connection strings (a missing or wrong-length destination or source, or source 0xFF). On MS/TP the destination connection string stays required. Exposing the count to the host is #3983.
  • rb v-20261017-8 (#3972): test tool only - the string WriteProperty path builds a BACnetDateRange, "yyyy-mm-dd (w);yyyy-mm-dd (w)", with special values passed through (BTL 9.23.2.22).
  • rb v-20261019-32 (#625): a C# BACnetStack_DecodeAs(JSON) sample (adapters/csharp/decodeasjsonsample/) and smoke test (adapters/csharp/decodeasjsonselftest/). The smoke test decodes the shared I-Am fixture through the C# binding and asserts the result byte-for-byte against adapters/README.md’s fixture JSON. It loads bin/CASBACnetStack_x64_Release.dll, or the library CAS_BACNET_STACK_LIBRARY names, and fails rather than skips if the library is missing. With this, every adapter language has its sample and smoke test.
  • rb v-20261022-review-fixes (#4350): the manual’s configuration page states the year-2038 limit of builds whose platform has a 32-bit time_t.
  • rb v-20261022-v6-page: bacnetstack.com has a version 6 launch page at /v6/ (what is new, moving from version 5, contact), the page the v6 launch posts, emails and press release link to. check-site-claims.js R1 allows the owner-approved BTL minimum-revision sentence on that page only (owner, 2026-10-22).
  • rb v-20261023-1 (#4308): the MS/TP engine (MSTP.c) sends and receives COBS-encoded BACnet Extended Data frames (Frame Types 32 and 33, cl. 9.3.9, 9.3.10, 9.10). MSTP_InsertFrameToSend now accepts an NPDU of up to 1497 octets (MSTP_MAX_NPDU_OCTETS), and sends one longer than 501 as Extended Data Expecting or Not Expecting Reply (cl. 9.1.1.4). A received Extended Data frame is checked against its CRC-32K and decoded, and its NPDU, up to 1497 octets, is passed to APDUCallBack. A host whose callback copies the NPDU into a 501-octet buffer must size that buffer by MSTP_MAX_NPDU_OCTETS, or drop what does not fit. The buffers grow by about 5.5 KB; a node that does not route can define MSTP_MAX_NPDU_OCTETS as 501 on the compiler command line to save about 5 KB of that. The stack still sizes MS/TP APDUs at 480 octets, because a larger NPDU needs every router on the path to support COBS-encoded frames (cl. 9.7.1).
  • rb bs-20261027-11 (#769, slice 4 of 4): source/MSTP.c and source/MSTP.h, the C MS/TP engine, and the whole MSTP_* free-function API and mstp_* callback types with them (MSTP_Init, MSTP_Loop, MSTP_IsConnected, MSTP_SetBaudRate, MSTP_SetMaxMaster, MSTP_SetRS485EnableTXDPin, MSTP_InsertFrameToSend, MSTP_ReportReceiveError, the global mstp_settings and mstp_runningVariables). Breaking for anything that compiled MSTP.c itself: the Zephyr adapter (migrated here), BACnetServerMSTPExampleCPP, BACnetProfileExample-B-BC-Zephyr, and any customer application. Register the byte callbacks (BACnetStack_RegisterCallbackMSTPReceiveBytes(), ...MSTPSendBytes(), ...MSTPMicroseconds()) and call BACnetStack_LoopMSTP(), or stay in packet mode with your own framing. MSTP_MAX_PACKET_OCTETS (501), MSTP_MAX_NPDU_OCTETS (1497) and MSTP_INPUT_BUFFER_SIZE (1508) are now CASBACnetStack::BACnetMSTPEngine::MAX_PACKET_OCTETS, MAX_NPDU_OCTETS and INPUT_BUFFER_SIZE in source/BACnetMSTPEngine.h; the -DMSTP_MAX_NPDU_OCTETS=501 memory option has no equivalent. The migration table is in the MS/TP chapter (“Moving from MSTP.c”) and the v5-to-v6 feature changes. The library is now all C++ and the CMake project no longer enables the C language.
  • rb bs-20261027-11 (#769): the 104 tests of the old C engine (tests/datalinks/mstp/TestMSTPFrameBoundary.cpp, 99, and TestMSTPPortContext.cpp, 5), whose ported counterparts in TestBACnetMSTPEngineFrameBoundary.cpp and TestBACnetMSTPEnginePortContext.cpp stay (104 of 104 carried over, plus 12 new).
  • rb dv-20261018-25 (#3679, CR3-F07-012, CR3-F07-029): internal dead code with no caller: BACnetBVLL::GetSecurityWrapper (Secure-BVLL is a deleted clause, J.2.13; SetSecureBVLL now leaves the BVLL reset), and the packet-level DecodeUsingSupportedServices of BACnetPacket, BACnetIPPacket and BACnetMSTPPacket (the receive path decodes the APDU through DecodeAPDUUsingSupportedServices). No public API changes.
  • rb v-20261017-1 (#3971): the unused adm-zip and @types/adm-zip devDependencies of the npm package. Their advisories reached every Node example’s lockfile through npm workspaces.
  • rb dv-20261019-107 (#4265, part 2): the CallbackCompleteRestore doc now says download-required stays until a later restore session ends with the callback returning true. No function sets System_Status (IFC-185).
  • rb dv-20261019-110 (#4312, slice 1): comments in source/ and tests/ that copied 135-2024 text now paraphrase it with the clause number (cl. 6.2.2.1 network-number limits, cl. 12.56.13, and the tag-encoding, router and BACnet/SC test comments). The changes are to comments only.
  • rb bs-20261024-34 (#4522): a regression test pins that a whole-array ReadProperty of a Global Group’s Present_Value, whose host serves only one element, is answered with an error and never dereferences a missing value (the crash itself was fixed by #3089 CR2-F02-042).
  • rb bs-20261024-40 (#4508, item CR4-F06-005): a test pins that an I-Am-Router-To-Network naming one of this device’s own virtual networks is never learned against a real port, while an ordinary network in the same announcement is.
  • rb bs-20261024-59 (#4487 CR4-F05-007): BACnetStackPerSourceRateLimiter gets ungated tests (the existing ones need STACK_OPTION_ROUTER_PHYSICAL) for the contracts nothing covered directly: a NULL, empty or over-long address and an identity prefix past the address are refused without taking a token or a slot, and a wall clock that goes backwards grants exactly one period of refill. No source change.
  • rb dv-20261019-68 (#2526): end-to-end tests send a confirmed and an unconfirmed event or audit notification to a device on a remote network through each of the three address-form call sites, and check the frame. A call site that loses its recipient’s confirmed flag now fails a test.
  • rb dv-20261019-70 (#2526): an end-to-end test sends a confirmed request on the BACnet/SC hub to a device on a remote network and delivers the router’s relayed answer. The answer from that device ends the transaction, and one from another device on the same network does not.

The source tree and the release package are reorganised. What changes for you:

  • The documentation is one manual. The release package carries an offline HTML manual at docs/manual/index.html: the Quick Start, the 5 → 6 migration guides (with a worked “seed a read-only File object” example), the BACnet/IP, BACnet MS/TP and BACnet/SC data-link chapters (with “Why BACnet/SC?”), the guides (BBMD and foreign devices, debug logging, Device Communication Control, the Network Port object and ReinitializeDevice, virtual devices and gateways), the decoded XML/JSON output reference, the object and property tables, and the API Reference generated from the header. The PDFs and the separate Doxygen HTML are gone.
  • CMake is the build. CMakeLists.txt and CMakePresets.json build the library for Windows x64 (static library and DLL, Visual Studio 2026 or 2022) and for Linux x64, x86, ARMv7 and ARM64, and build the unit tests; the library output names are unchanged. The makefiles and the Visual Studio solution are removed. The Linux libraries now also contain source/MSTP.c, as the Windows library always did; the stack still never calls it.
  • Build settings. source/CIBuildSettings.h is optional: a release package ships the one its libraries were built with, a checkout without it uses source/CIBuildSettingsDefaults.h, and a CMake build takes the options as cache options (-D STACK_OPTION_TARGET_FULL=OFF ...). A compiler without __has_include uses the defaults unless CAS_BACNET_STACK_USE_CIBUILDSETTINGS is defined.
  • npm package. The addon’s build files moved to projects/npm/ (binding.gyp, Directory.Build.props); the package’s API and behaviour are unchanged.
  • Coverage. tools/coverage/run.sh builds the unit tests with coverage and writes an lcov/genhtml report on Linux (CMake 3.25 or later, Ninja, lcov).
  • The release package. It is the source tree plus the built libraries and the manual, without internal material: no underscore-prefixed folder, no AGENTS.md, no .pdb file. The vendored coverage binaries, the old reports and the old Examples/ folder are gone; the examples are separate repositories tagged cas-bacnet-stack-example in the chipkin GitHub organisation.
  • Header documentation now matches the code. Comments in CASBACnetStackDLL.h and BACnetCallbackInterface.h now describe the current behaviour of DeviceCommunicationControl without a callback, the BACnetStack_AddBDTEntry and BACnetStack_SendRegisterForeignDevice length checks, the Network Port properties, BACnetStack_RemoveDevice, answering Who-Has, BACnetStack_AddFileObject’s Read_Only, and the ReinitializeDevice error codes. No declaration changed.

For contributors: the unit tests are in subject folders under tests/ (see tests/README.md), the CI and helper scripts are in _ci_scripts/, process material and records are in _agents/ and _spec/, and the C++ code style guide is in the design-guides/ submodule. The full record is _spec/decisions/2026-10-15-repository-reorganisation.md and issues #3943 to #3953 and #3974.

  • rb bs-20260924-15 (#2703): COV subscriptions can no longer be taken over through a lookalike address. A COV context is keyed by the requester’s address (135-2024 cl. 13.14.2 “the same BACnet address contained in the PDU”), and that address decides who may renew or cancel a subscription. A renewal also stores the requester’s network parameters, which redirects the notifications. Two gaps are closed:
    • Same MAC on another port. Two directly connected devices with the same MAC on different Network Ports (for example MS/TP MAC 5 on two trunks, both local network 0) were one context. They are now different whenever both frames name the port they arrived on.
    • Spoofed routed source. A routed source’s SNET/SADR were believed from any sender. A host on the local network could claim a routed subscriber’s SNET/SADR, with or without a forged Forwarded-NPDU, and renew, cancel or redirect its subscription. For a routed source (SNET other than 0; cl. 6.2.2.1 allows SNET “from 1 to 65534”), the claim is now believed only from the same directly connected sender as the subscription. That sender is determined as follows. On BACnet/SC it is the Originating-Virtual-Address the hub vouches for (#344), so a routed subscriber behind an SC router is keyed by the router’s VMAC, not the hub’s shared VMAC; it is therefore unaffected by hub failover and cannot be claimed by another hub node. Otherwise it is the link peer the datalink vouched for (the real UDP sender of a forwarded NPDU), or else the link-level sender, such as the router. A directly attached BACnet/SC node (SNET 0) is not held to this rule, because the hub already checks its originating address.
    • Forged Forwarded-NPDU against a direct subscriber. A Forwarded-NPDU has its connection string rewritten to the claimed originator (Annex J.2.5). When either frame came with a vouched link peer, the link peers must now match too. Behaviour change: a routed subscriber whose path changes to a different router makes a new context rather than renewing the old one. The old context stays until its lifetime ends. An indefinite subscription (Lifetime 0, allowed for SubscribeCOV) stays until it is cancelled through the original router or the device restarts. It also counts toward the active-subscription limit meanwhile.
  • rb bs-20260924-16 (#2757): SubscribeCOVPropertyMultiple contexts now get the same address hardening as SubscribeCOV contexts (#1147, #2665, #2703). 135-2024 cl. 13.16.2 finds a COV-multiple context that “matches the source BACnet address, the recipient process and the form of notification”. The table used to match on the destination network and the connection string only. Contexts are now matched with the single-property table’s address comparison, which closes four gaps:
    • Two routed clients behind one router that used the same process identifier shared one context, so either could cancel or renew the other’s.
    • Two devices with the same MAC on different Network Ports shared one context.
    • A local host could claim a routed client’s SNET/SADR and cancel it, renew it, or redirect its notifications.
    • A forged Forwarded-NPDU naming a directly connected client could cancel that client’s context.
    • An empty-list re-subscription (#2671) renewed, and redirected, whichever context shared the requester’s recorded recipient. A routed client’s empty-list renewal did nothing, and the value in its initial notification was not recorded as the baseline for its next change (#2448). A BACnet/SC client’s frames carry its hub’s URI as the connection string. After hub failover, the client’s renewal or cancellation now still finds its context. Previously it created a second context. Behaviour change: in Active_COV_Multiple_Subscriptions, a routed recipient’s network is now its source network, as Active_COV_Subscriptions reports it. It was previously the request’s destination network, which is 0 for a request addressed to this device. A routed client whose path changes to a different router now gets a new context, and the old one stays until its lifetime ends. The same applies to SubscribeCOV (#2703).
  • rb bs-20260928-40: public API, interface and business logic (F01).
    • An ACK_NOTIFICATION to an MS/TP- or SC-typed recipient no longer corrupts the heap through the IPv4 data link (#3045).
      • The acknowledgment notification is built for the RECIPIENT’s network type: a 1-octet MAC is MS/TP, and a DAB binding carries the type its I-Am arrived on.
      • It was sent through the data link the AcknowledgeAlarm arrived on. On BACnet/IP, the IPv4 data link read an MS/TP packet as a BACnetIPPacket and wrote a BVLL past the end of the object; an SC packet’s own BVLL was overlaid.
      • The arrival data link is still used when it carries the notification’s network type, so a multi-port device answers on the port the request arrived on. Otherwise the notification is routed by its own network type through the data link layer, as every other event notification is.
      • The IPv4 data link now refuses any packet that is not a BACnetIPPacket. That matters for a virtual device, which has no data link layer to route through here: its MS/TP-typed acknowledgment notification is dropped with an error rather than corrupting memory (delivery is follow-up #3199).
  • rb bs-20260928-84: a Notification Forwarder can no longer be made to re-broadcast notifications (#2680).
    • Any network-0xFFFF target is now treated as a global broadcast and never sent to, whatever its MAC. This is loop-prevention rule (c), cl. 12.51 (135-2024): “The Notification Forwarder object shall not send any forwarded notification using a global broadcast.”
    • A network-0 target with a broadcast MAC is now treated as a local broadcast. That covers an IPv4 broadcast (FF FF FF FF ..) and the MS/TP broadcast (FF). Rule (a) skips such a target when the notification’s ingress port is known: “shall not be forwarded as a local broadcast to the BACnet network directly attached to that port”.
    • A broadcast-shaped Subscribed_Recipients entry is now always skipped. A peer adds entries to that list with AddListElement, and no subscriber is a broadcast address. The ingress port is stamped only on router ports, so on a single-port device the port-gated rule (a) check never applied. A Recipient_List entry, which the integrator configures, is still skipped under rule (a) only when the ingress port is known.
    • Not covered yet (#3277): a subnet-directed IPv4 broadcast such as 192.168.1.255, which every broadcast check in the stack treats as unicast.
    • Before, only an empty-MAC shape was treated as a broadcast, and the send path never sends that shape. An unauthenticated peer could subscribe {network 65535, <any MAC>} or {network 0, FF FF FF FF ..}, and every accepted notification was then re-sent as a global broadcast, or as a local broadcast back onto the network it arrived on.
  • rb bs-20260928-85: two more ways to reach a global broadcast through a notification recipient are closed (#3277, items 2 and 3).
    • A confirmed notification to a recipient on network 0xFFFF is now refused. Such a recipient is sent with DNET 0xFFFF, a global broadcast, whatever its MAC. The #385 confirmed-to-broadcast refusal looked only at the MAC, so a Notification Class recipient {65535, <unicast MAC>} with confirmed notifications built a confirmed request to every network. A confirmed request needs exactly one respondent (cl. 5.4). Unconfirmed notifications to such a recipient are sent as before.
    • An I-Am with source network 0xFFFF no longer binds a device in the Device Address Binding; the entry keeps what it had. Cl. 6.2.2.1 (135-2024): “Allowable network number values for DNET shall be from 1 to 65535 and for SNET from 1 to 65534.” Before, the address was stored as sent, so a spoofed I-Am with SNET 0xFFFF bound a device-identifier recipient to network 0xFFFF. Every notification or external write to it then went out as a global broadcast.
    • Ignoring the I-Am fails closed on every data link. Rebinding it as local would misaddress a BACnet/SC device behind a hub, or a device behind the router that relayed it.
    • Still open: #3277 item 1, and #3279 (a confirmed request with SNET 0xFFFF is answered with DNET 0xFFFF).
  • rb bs-20260928-86: a request that claims source network 0xFFFF can no longer make the device answer with a global broadcast (#3279).
    • Replies are no longer addressed to DNET 0xFFFF. Both reply builders used to copy the request’s SNET into the reply’s DNET: BACnetNPDU::SetFromNetworkParameters for responses, and SetFromOnlyNetworkParameters for error replies. A request with SNET 0xFFFF was therefore answered with DNET 0xFFFF, a global broadcast. One spoofed frame could make the device broadcast its reply across the whole internetwork.
      • Cl. 6.2.2.1 (135-2024): “Allowable network number values for DNET shall be from 1 to 65535 and for SNET from 1 to 65534.”
      • Such a request is now answered unrouted to the link peer, as the physical router’s reject path already does for SNET 65535.
    • HookIAm now reports such a sender as reached through its link: network 0, with no source address.
  • rb bs-20260928-87: every application hook that reports a received frame’s sender now reports a source network of 0xFFFF as a sender on the local link (#3281). HookAbort and HookTimeout for a request this device sent keep the stored destination, which #3277 already stops from being network 0xFFFF.
    • HookIHave, HookTextMessage, HookSimpleAck, HookError, HookReject, HookAbort, the ReadPropertyMultiple-ACK error hook, and the router hooks (HookInitializeRoutingTableAck, HookIAmRouterToNetwork, HookICouldBeRouterToNetwork) now report such a sender as network 0 with no address, as HookIAm has since #3279.
    • Cl. 6.2.2.1 (135-2024): “Allowable network number values for DNET shall be from 1 to 65535 and for SNET from 1 to 65534.”
    • Before, an application that addressed an unconfirmed follow-up from one of these hooks could send a global broadcast.
    • New accessors: BACnetNetworkParameters::GetReportedSourceNetwork / GetReportedSourceAddress / GetReportedSourceAddressLength. Transaction lookups still use the raw source, because it has to match what was stored when the request was sent.
    • Not changed: the physical router still forwards a frame with SNET 0xFFFF and answers it with an unrouted reject. That is the practice choice a live-path test pins.
  • rb bs-20260928-89: a DeviceCommunicationControl password longer than 80 octets is now refused however many characters it counts as (#2653).
    • Cl. 16.1.1.1.3 (135-2024): “This optional parameter shall be a CharacterString of up to 20 characters.”
    • The decoder counts UTF-8 characters by lead octets and does not validate UTF-8, so orphan continuation octets counted as nothing. “secret” followed by 256 octets of 0x80 counted as 6 characters, passed the cap, and the uint8_t passwordLength handed to the host wrapped (262 became 6).
    • 20 characters are at most 80 octets in every charset, so a spec-legal password is never refused.
  • rb bs-20260928-90: a ReinitializeDevice WARMSTART or COLDSTART now lifts DeviceCommunicationControl DISABLE / DISABLE_INITIATION only after CallbackReinitializeDevice accepts the request (#2652).
    • Cl. 16.1.2 (135-2024): “Communication shall be disabled in this manner until either the ‘Time Duration’ has expired or a valid DeviceCommunicationControl (with ‘Enable/Disable’ = ENABLE) or, if supported, a valid ReinitializeDevice (with ‘Reinitialized State of Device’ = WARMSTART or COLDSTART) message is received.”
    • The host checks the password in that callback. Before, communication was re-enabled ahead of the callback, so any peer could lift an operator’s password-protected DISABLE_INITIATION with a wrong password. The peer got PASSWORD_FAILURE back, but the device went back to initiating COV and event notifications.
    • Communication is re-enabled, and any Time_Duration cleared, only if the pending Network Port changes also apply.
    • Behaviour change for hosts: messages the host initiates from inside CallbackReinitializeDevice (an I-Am, startup notifications) are still under the DeviceCommunicationControl in force, so under DISABLE_INITIATION they are refused. Before, they went out because communication had already been re-enabled. Replies such as the SimpleACK are never held back by DeviceCommunicationControl.
    • Not changed: a ReinitializeDevice with any other state is still refused with COMMUNICATION_DISABLED before the callback.
  • rb bs-20260929-96: an initiated BACnet/SC direct connection honours a Connect-Accept only while it waits for one (AWAITING_ACCEPT) and only when the Connect-Accept answers its own pending Connect-Request’s message ID (#2613, CR2-F09-071). Cl. AB.6.2.2, state AWAITING_ACCEPT: “On receipt of a Connect-Accept message enter the CONNECTED state.” Before, a connected peer could send another Connect-Accept at any time and rewrite the VMAC and UUID the node switch had learned in the handshake.
  • rb e-20260924-151 (#2532): a WriteProperty of a Network Port’s BBMD_Broadcast_Distribution_Table containing an entry whose BBMD host was none or a host name made the stack memcpy from a NULL address, a remote crash on a BBMD that exposes the table. The write is now refused as a whole with Error(property, value-out-of-range), leaving the live table unchanged, unless every entry has a 4-octet ip-address host and a 4-octet broadcast mask. The same refusal applies to 16-octet (B/IPv6) hosts and to a missing mask; both were previously accepted, truncated or given a default. 135-2024 cl. 12.56.38: “The ‘none’ choice of the BACnetHostAddress portion shall not be used for list entries.” The stack does not resolve host names for this table. BACnetBDTEntry::operator= now copies the host choice and mask faithfully instead of normalising to 4 octets.
  • rb e-20260925-154 (#2475, security): BACnetAbstractSyntaxType::Decode now refuses a value nested more than 32 opening tags deep (BACNET_MAX_ABSTRACT_SYNTAX_NESTING_DEPTH), with Reject reason other, before it recurses. It recursed once per wire opening tag with no bound, so one 1-octet X’0E’ per C++ stack frame let a single UnconfirmedPrivateTransfer, AuditNotification or proprietary ReadProperty-ACK value set the stack depth; a Windows x64 Debug test process died at 1000 levels on the pre-fix code. 135-2024 sets no limit (cl. 20.2.1.6(d): “The tagged stream between opening and closing tags may be parsed according to these same four rules via a process of recursive descent until only primitive tags are encountered or until no tags are encountered.”), so 32 is this stack’s own bound: the deepest standard production found nests about six levels. The value’s own enclosing context tag is not counted.
  • rb e-20260925-154 (#2483, security): Encode() on BACnetPrimitiveENUM, INT, UINT, UINT64 and ObjectIdentifier no longer writes one octet past maxBufferLength when the tag is longer than one octet. That happened with a context tag number of 15 or more (cl. 20.2.1.2: “Tag numbers ranging from 15 to 254 (inclusive) shall be encoded by setting the Tag Number field of the initial octet to B’1111’ and following the initial tag octet by an octet containing the tag number”), and with any UINT64 above 0xFFFFFFFF, whose 5 or more content octets take the extended-length form. The capacity check and GetMinimumMemoryCount() now use the tag’s real encoded size, from the new BACnetTag::GetEncodedLength().
  • rb e-20260925-155 (#2903, security): a value decoded by datatype that contains another value of its own kind now counts toward the same 32-level nesting bound as #2475 (BACNET_MAX_ABSTRACT_SYNTAX_NESTING_DEPTH), and is refused with Reject reason other before it recurses past it. Group Present_Value is a BACnetLIST of ReadAccessResult (135-2024 cl. 12.14.6), and each ReadResult value is decoded by datatype again, so a WriteProperty to a Group’s Present_Value or a ReadPropertyMultiple-ACK could nest ReadAccessResults about 11 octets per level. Each level restarted #2475’s count at 0; 200 levels (2,225 octets) decoded in full on the pre-fix code. The depth is now carried in the decode’s CErrorContainer: every DecodeByDatatype entry holds one level, and an abstract-syntax value decoded inside a typed value starts its opening-tag count from there.
  • rb e-20260925-155 (#2905, security): Encode() on BACnetPrimitiveREAL, DOUBLE, DATE and TIME no longer writes one octet past maxBufferLength with a context tag number of 15 or more (cl. 20.2.1.2: “Tag numbers ranging from 15 to 254 (inclusive) shall be encoded by setting the Tag Number field of the initial octet to B’1111’ and following the initial tag octet by an octet containing the tag number”). GetMinimumMemoryCount(), which the capacity check uses, returned a fixed 5 (10 for DOUBLE); it now returns the tag’s real encoded size plus the content octets, as #2483 did for the integer types.
  • rb e-20260925-156 (#2580, security): the BACnet/SC hub connector binds an Address-Resolution-ACK’s WebSocket URI to a VMAC only when the ACK’s Originating Virtual Address is the VMAC the Address-Resolution was sent to. It matched by message ID alone, and IDs are sequential, so another node on the SC network could answer with the right ID, bind its own URI to the victim’s VMAC and receive the direct connection meant for it. The hub adds that address (135-2024 AB.5.3.2: “When forwarding a unicast BVLC message, the ‘Originating Virtual Address’ parameter shall be added, indicating the VMAC address of the connection peer node of the hub connection from which the message was received”); an ACK with another OVA, or none, is ignored.
  • rb e-20260929-172 (#2535): a BBMD now distributes a received Forwarded-NPDU (the local re-broadcast and the copies to foreign devices) only when it comes from a peer BBMD in its BDT. 135-2024 J.4.5: “Upon receipt of a BVLL Forwarded-NPDU message, a BBMD shall process it according to whether it was received from a peer BBMD as the result of a directed broadcast or a unicast transmission.” The UDP source’s IP address must equal a non-host BDT entry’s IP address exactly, for one-hop and two-hop entries alike: a peer sends from the B/IP address its entry holds (J.4.3.2). The UDP port is not compared, because a NAT router (J.7) may rewrite it. Any host could previously unicast one Forwarded-NPDU and have it broadcast on the BBMD’s subnet and sent to every registered foreign device, with an Originating Device of its choosing that every recipient then answers. The frame is still processed by the BBMD’s own device.
  • rb e-20260929-172 (#2536): the #1884 screen that stops a Forwarded-NPDU’s Originating Device being this subnet’s directed-broadcast address (for example 192.168.1.255) now works in production. When a BACnet/IP Network Port object is added, the subnet mask read from its ipSubnetMask property is now recorded on the data link with its address; it was read and discarded, so replies (and #427 quota bucketing) could still be aimed at the subnet broadcast address. A /31 or /32 mask is not screened, since neither has a directed-broadcast address (under /32 it would be the device’s own IP, under /31 the link’s other host). As before, this needs a BBMD-enabled build and the host’s ipAddress, bacnetIpUdpPort and ipSubnetMask property callbacks to answer when the object is added.
  • rb e-20260930-186 (#2627): the two hand-written list decoders behind every ReadPropertyMultiple request and ACK (BACnetListOfReadAccessSpecifications, BACnetListOfReadAccessResults) now apply the STACK_OPTION_MAX_SEQUENCE_OF_ELEMENTS element cap that BACnetSequenceOf<T> has applied since #617 (4096 on the FULL target, 1024 by default, 128 on EMBEDDED). The element after the cap is refused with Abort(BUFFER_OVERFLOW) and the partly built list is discarded. Before, a segmented request of short Read Access Specifications could build thousands of heap objects before any later request-size check ran.
  • rb e-20261008-1 (#3720): a BACnet/SC direct-connect peer could send a frame under another node’s VMAC. Direct-connect ingress copied the frame’s Originating-Virtual-Address into the source address without checking it against the peer VMAC learned in the connect handshake, so the #3701 COV-subscriber takeover worked over a direct connection. 135-2024 AB.3.1.2: “In every BVLC message, the ‘Originating Virtual Address’, if present, shall always be the VMAC of the node that originally initiated the BVLC message.” AB.4.2.1 has a direct-connection peer omit it, so the peer is always the originator. A frame whose Originating-Virtual-Address is present and differs from the handshake peer VMAC is now discarded silently. The hub function applies the equivalent check to Encapsulated-NPDUs (#344); this applies to every function. So is a frame with an Originating-Virtual-Address that arrives before the handshake. One that equals the peer VMAC is still accepted.
  • rb e-20261008-1 (#3672, CR3-F09-033): the hub connector matched a BVLC-Result to its pending request by message ID alone. Any node on the SC network could relay a Result through the hub and have it taken as the hub’s answer to this node’s Connect-Request, Disconnect-Request, Heartbeat-Request or Address-Resolution. Worst case: a failed connection request was recorded against the failover hub, and fail-back to the primary was blocked. 135-2024 AB.3.1.2: “Note that a BVLC-Result received without an ‘Originating Virtual Address’ was produced by the connection peer, which is the hub function for hub connections, or is the destination node for direct connections.” A Result is now ignored, and the request stays tracked, in two cases: its Originating-Virtual-Address presence differs from the request’s Destination-Virtual-Address presence, or both are present and differ. This mirrors the OVA/DVA comparison #2580 added for Address-Resolution-ACK, but keeps the request tracked rather than untracking it. A relayed NAK from the node an Address-Resolution was sent to still matches. Side effect: a NAK from the hub itself (no OVA) for a request that carried a DVA, such as an Address-Resolution the hub could not deliver, is now ignored too; that request stays tracked until it is answered or the hub connection ends.
  • rb e-20261008-2 (#3665, CR3-F09-015): the BACnet/SC hub function, and the direct-connect acceptor, accepted a Connect-Request whose VMAC was X’FFFFFFFFFFFF’ (the Local Broadcast VMAC) or X’000000000000’, and registered the peer under it. Registered as X’FFFFFFFFFFFF’, the peer’s unicasts were relayed with the broadcast VMAC as Originating-Virtual-Address, so each reply to it became a broadcast. 135-2024 H.7.3: “The values X’000000000000’ and X’FFFFFFFFFFFF’ are not valid as VMAC addresses for a device and can have other uses defined by the data link.” Connect-Request decoding now refuses both values as an out-of-range payload parameter. The request gets a BVLC-Result NAK with Error Class COMMUNICATION and Error Code PARAMETER_OUT_OF_RANGE (AB.3.1.5), it is recorded in SC_Failed_Connection_Requests, and no peer is registered. Connect-Accept decoding refuses them too, so an initiating node never records a reserved VMAC for its peer. Such a Connect-Accept is discarded unanswered, because it is a response message, and the connection attempt then times out.
  • rb g-20260928-2 (#2596, security): in the BACnet/SC direct-connect accepting role, a BVLC other than Connect-Request is now processed only when it arrives on the socket whose Connect-Request was accepted (135-2024 cl. AB.4.2, “All direct connections are required to be established as BACnet/SC connections”). From any other socket it is discarded (fail-closed). Before, an Encapsulated-NPDU from a socket that never sent a Connect-Request reached the application layer under the accepted peer’s VMAC, and its Disconnect-Request ended that peer’s connection.
    • When the host reports the accepted socket Disconnected or Error through SetBACnetSCWebSocketStatus, the accepted connection is now forgotten, on any BACnet/SC port. Before, it outlived its socket, so a later socket that the host gave the same connection string would pass the check under the departed peer’s VMAC.
    • The socket is now closed after the NODE_DUPLICATE_VMAC NAK and after the Disconnect-ACK (cl. AB.6.2.3).
    • A new peer that replaces the accepted connection now sends the replaced peer a Disconnect-Request and closes its socket, instead of leaving it open and untracked.
    • Still open: this device accepts one direct connection at a time. A peer with a valid operational certificate can therefore evict the accepted peer, and two such peers can evict each other in turn.
  • rb g-20261001-5 (#2654): deleting a Schedule or Timer now releases the Device Address Binding entries its List_Of_Object_Property_References acquired. Before, each delete left one reference held per external device, so create/configure/delete cycles could fill the 4096-entry table (#1142) until BACnetStack_Reset, after which no new notification recipient or external write target resolved. The deleted object’s own writes still waiting for an unbound device are dropped too, so they are no longer sent when another object keeps that device bound and the device’s I-Am arrives (#3009); a queued write another Schedule or Timer could own (the same target at that object’s current priority) is kept.
    • Still not done for #2654: that check uses the other object’s current Priority_For_Writing, so if Schedule B queued a write at 8 and was then moved to 10, deleting Schedule A (same target) can drop B’s stale write at 8. B’s next write goes out at 10. The Notification Class and Channel legs were fixed by #3255 and #2837.
    • Host-visible change: BACnetStack_AddScheduleObjectPropertyReference and BACnetStackTestTool_AddTimerObjectPropertyReference now return false, and add nothing, for a reference to another device when the address binding table is full. Before, the reference was added without an entry and its device never resolved.
  • rb g-20261001-5 (#2896): a router build without the physical router (a GW-VN-B gateway, or STACK_OPTION_TARGET_FULL with STACK_OPTION_DISABLE_ROUTER_PHYSICAL) now limits Reject-Message-To-Network per source when CallbackGetSystemTime is registered: a burst of 4, then 1 a second, matched on the sender’s IP address on BACnet/IP, as the physical router has since #471. Before, every frame that drew a reject got one. With no system clock callback (CallbackGetSystemTime) registered, such a build - a GW-VN-B or NM-RC-B integrator’s included - gets no reject limiting at all (fail open), since nothing would refill the bucket. The key is the link peer, so every sender behind one BBMD, router or BACnet/SC hub shares a bucket.
  • rb g-20261005-5 (#3088, CR2-F01-027): a CallbackGetPropertyConstructed that returns true but reports more bytes than the maxBufferSize it was given (1497) is refused: the read gets no value from the callback, logged, the same as a zero-length return. Before, the stack copied the reported length out of its 1497-byte stack buffer (an out-of-bounds read) into the value, and those bytes went onto the wire in the ReadProperty answer. One level out, GetProperty handles it exactly as a zero-length return: it falls through to its stored-value and later fallback steps, except for a LIST-datatype property with the constructed callback registered, which is answered READ_ACCESS_DENIED (#3042). Exactly maxBufferSize bytes is still accepted. Test-tool builds only (the constructed callback is BACNET_STACK_TESTTOOL).
    • New gtests BACnetCallbackGetPropertyConstructedTest.CallbackReportsLengthAboveMaxBufferSize_Refused (revert-red: fails with the source reverted) and ...CallbackReportsLengthEqualToMaxBufferSize_Accepted (the 1497-byte control; fails under a > to >= mutation of the bound).
  • rb g-20261008-1 (#3701, CR3-F15-001): on a BACnet/SC device, any node on the same hub could cancel, renew or redirect another SC subscriber’s COV and COV-multiple subscriptions by sending an NPDU source specifier that claimed SNET 0 and the subscriber’s VMAC as SADR: the claim replaced the hub-vouched Originating-Virtual-Address and matched as “the same BACnet address” (cl. 13.14.2). With SNET 0, a request now matches a stored subscription only when both carry an Originating-Virtual-Address and the two are equal. Hub failover (the same subscriber’s VMAC through another hub) still renews. Wire effect: a forged cancellation is now a no-op that still answers success, the cl. 13.14.2 behaviour of succeeding as if a context had existed, and the victim’s context is untouched. A forged subscription or renewal creates a separate context for the forger instead of replacing the victim’s.
  • rb g-20261008-1 (#3706, CR3-F15-021): a COV-multiple context queued timestamped changes without bound until Max Notification Delay (up to 3600 s), one heap entry per change, then drained them into one notification too large for an APDU. New option STACK_OPTION_MAX_COV_MULTIPLE_QUEUED_NOTIFICATIONS, default 16 on every target: when the queue reaches it, the context’s notification is sent at once (cl. 13.1 makes Max Notification Delay a maximum). 16 timestamped REAL entries encode in about 264 octets, inside the 480-octet MS/TP Max_APDU a FULL build can face; an unconfirmed notification cannot be segmented and the queue is drained before the encode, so a notification that did not fit would lose every change in it. Wire effect: a timestamped subscriber receives a COV-multiple notification every 16 queued changes as well as at the delay. Not covered: a count does not bound the encoded size of large values (CharacterString, BitString), so 16 of those can still exceed an APDU; splitting the drain across several notifications (cl. 13.1, “Multiple notification messages may be required to convey all changes”) is a follow-up.
  • rb g-20261008-12 (#3780): a reply to a request that arrived as an Original-Broadcast-NPDU (or a Distribute-Broadcast-To-Network a BBMD executes locally) is now sent as an Original-Unicast-NPDU (81 0A) to the sender’s B/IP address instead of an Original-Broadcast-NPDU (81 0B) link broadcast. Wire-visible: any SimpleACK, ComplexACK, SegmentACK, Error, Reject or Abort built in answer to a broadcast frame - in practice the client Abort to an unsolicited segmented ComplexACK - is unicast, so one broadcast datagram no longer draws one link broadcast per device (and, on a BBMD, a distribution of each to the BDT and FDT). A BBMD answering a registered foreign device’s Distribute-Broadcast-To-Network now unicasts the reply directly to that foreign device; it used to go out as a broadcast on the BBMD’s own subnet, which never reached the foreign device. I-Am, I-Have and every other unconfirmed request are unchanged; the network-layer (no-APDU) path is unchanged. 135-2024 cl. 6.3, paraphrased from the review finding, unverified.
  • rb g-20261008-2 (#3685, CR3-F10-011): a ReadPropertyMultiple request’s property references are now budgeted across the whole request, not only per object and per object count. The two caps multiplied: on EMBEDDED one 16384-octet segmented request could make the decode allocate about 7,900 property references. The budget is BACnetListOfReadAccessSpecifications::MAX_PROPERTY_REFERENCES_PER_LIST, equal to STACK_OPTION_MAX_SEQUENCE_OF_ELEMENTS (4096 FULL, 128 EMBEDDED), as #3417 did for WritePropertyMultiple. Wire effect: a request carrying more references than that is answered Abort BUFFER_OVERFLOW instead of being decoded.
  • rb g-20261008-2 (#3684, CR3-F10-010): the List of Results inside one ReadAccessResult (a ReadPropertyMultiple-ACK this device receives, or a Group Present_Value) now has the STACK_OPTION_MAX_SEQUENCE_OF_ELEMENTS element cap the other ReadPropertyMultiple lists got in #2627. A responding peer could otherwise make the decode allocate one result per 8 octets of the APDU. Wire effect: such an ACK is refused with BUFFER_OVERFLOW instead of being decoded.
  • rb g-20261008-3 (#3751, CR3-F24-010): an Access Point’s Failed_Attempts is no longer reset by an authentication that the authorization stage then denies (DENIED_DENY_ALL, DENIED_NO_ACCESS_RIGHTS, threat level, passback, occupancy). 135-2024 cl. 12.31.18: “This property shall be set to zero when a successful access attempt occurs or when the property Lockout becomes FALSE.” Closes a lockout bypass: presenting any valid but unauthorised credential between guesses kept Max_Failed_Attempts from ever locking the point out. The reset now happens on a grant, and when authentication is the request’s final outcome (Authorization_Mode NONE, or AccessPointAuthenticateFactor). Wire and integrator effect: Failed_Attempts read over BACnet keeps counting through authorization denials, and LOCKOUT_MAX_ATTEMPTS fires where it did not before.
  • rb g-20261008-4 (#3650, CR3-F06-024): a reply the stack builds with no device context (the client Abort to a segmented ComplexACK it does not accept or did not ask for, the Abort to an unsolicited server SegmentACK, and any other SetFromOnlyNetworkParameters reply) is no longer addressed to DNET X’FFFF’ when the request arrived with DNET X’FFFF’. 135-2024 cl. 6.3: “only the BACnet-Unconfirmed-Request-PDU may be transmitted using a multicast or broadcast network layer address”. Closes a global amplifier: one Original-Broadcast-NPDU with DNET X’FFFF’ and no SNET drew an Abort from every device on the subnet running the stack, each addressed to DNET X’FFFF’ and so relayed by every router to every network (cl. 6.3.2). Wire effect: the reply goes to the request’s SNET/SADR when one is present, and otherwise unrouted to the link peer, with no DNET and no SNET. (Answering a request received as a link broadcast at all is a separate, BACnetDataLink-side question, not changed here.)
  • rb g-20261008-5 (#3743, CR3-F22-010): BACnetStack_InsertTrendLogRecord / BACnetStack_InsertEventLogRecord with a NULL datumAsString and a non-zero datumLength now return false instead of constructing std::string from NULL (on Linux, std::logic_error thrown out of the C export and the host process terminated). NULL with length 0 is still accepted as the Null datum’s documented empty text. The same guard is applied to BACnetSingleLogData::SetSingleLogDataFromString, reached only through the Trend Log Multiple record parser.
  • rb g-20261009-10 (#3783): BACnetHostAddress::SetHostAddressName refuses a NULL name with a non-zero length (was std::string(NULL, n), undefined behaviour), the #3743 guard. Defence in depth: tracing the named sites found no export that reaches it with NULL (FD_BBMD_Address host name comes from a stack buffer, the SC hub authority from a std::string). The other named sites are already guarded: the text-message sends refuse a NULL message, BACnetSCWebsocket::SetWebsocketUri refuses NULL, and InsertTrendLogRecord ignores the length when statusFlags is NULL. No host- or wire-visible change.
  • rb g-20261009-2 (#3775): a typed value of ReadAccessSpecifications holds at most BACnetListOfReadAccessSpecifications::MAX_PROPERTY_REFERENCES_PER_LIST property references across all its specifications (#3685’s budget), and at most STACK_OPTION_MAX_SEQUENCE_OF_ELEMENTS specifications. Both limits are 4096 on FULL, 128 on EMBEDDED and 1024 otherwise. The second limit covers specifications with no references, which add nothing to the first. This applies to Group List_Of_Group_Members in a WriteProperty, the list of elements of an AddListElement or RemoveListElement on it (and likely a remote Group’s List_Of_Group_Members in a ReadProperty-ACK, if that decode reaches the same path - not traced). Before, the value was bounded only by the APDU; now a larger value is Aborted BUFFER_OVERFLOW instead of decoded. The budget is per value, not per frame: a frame-level budget and a generic DecodeByDatatype element cap are follow-up #3831.
  • rb bs-20260924-10 (#2669, #2670, #2671, #2672): four SubscribeCOVPropertyMultiple defects (135-2024 cl. 13.16).
    • #2669: a Lifetime of 0 is refused with SERVICES / VALUE_OUT_OF_RANGE. Cl. 13.16.1.1.3: “A value of zero shall not be allowed.” It used to be stored as a subscription that never expired and held a capacity slot until restart. This is the #1149 fix for this service.
    • #2670: a request with exactly one of Lifetime and Max Notification Delay is rejected with missing-required-parameter. Both “shall be present if the request is a subscription, or re-subscription, and absent if the request is a cancellation”. An absent Lifetime used to become an indefinite subscription. This is the #1784 ruling for this service.
    • #2671: a re-subscription with an empty list now renews the context’s lifetime and Max Notification Delay. Cl. 13.16.1.1.5 allows “an empty list for re-subscriptions that only update the lifetime of the COV-multiple subscription”. It used to answer Result(+) and change nothing, so the subscription still expired at its original time. With no existing context, nothing is created.
    • #2672: issue-confirmed-notifications [1] is mandatory (cl. 21, Table 13-20). A request without it is rejected. Before, it decoded with the flag read as FALSE, which could file the subscription under, or cancel, the wrong context. The request object is valid for encoding only once the flag is set. The ASN.1 comments in three headers are corrected. Behaviour change for clients: a request built with BACnetSubscribeCOVPropertyMultipleRequest(subscriberProcessIdentifier) must also call SetIssueConfirmedNotifications before it can be encoded. BACnetStack send paths already set it.
  • rb bs-20260924-11 (#2666, #2667, #2698): COV subscription parameter checks (135-2024 cl. 13.14, 13.16).
    • #2666: a SubscribeCOV cancellation now always succeeds, including for an object that does not support COV. Cl. 13.14.2: “Cancellations that are issued for which no matching COV context can be found shall succeed as if a context had existed”. The not-supported Result(-) applies only “If the ‘Issue Confirmed Notifications’ parameter is present”. It used to answer OBJECT / OPTIONAL_FUNCTIONALITY_NOT_SUPPORTED for, for example, the Device object.
    • #2667: a SubscribeCOV with a Lifetime but no Issue Confirmed Notifications is rejected with missing-required-parameter. Cl. 13.14.1.1.3: “If the ‘Lifetime’ parameter is present then the ‘Issue Confirmed Notifications’ parameter shall be present.” It used to become an unconfirmed subscription. This is the #1784 ruling for this service. Issue Confirmed Notifications without a Lifetime is still an indefinite subscription.
    • #2698: SubscribeCOVPropertyMultiple refuses a Max Notification Delay equal to the Lifetime. Cl. 13.16.1.1.4: “shall be less than the value of the Lifetime parameter”. The COV-multiple table’s comments no longer describe a Lifetime of 0 as legal for that service (#2669).
  • rb bs-20260924-12 (#2665): COV subscriptions from two clients that use the same Subscriber Process Identifier are now two contexts.
    • Cl. 13.14.2 identifies a context by “the BACnet address from the PDU that carries the SubscribeCOV request and the same ‘Subscriber Process Identifier’ and ‘Monitored Object Identifier’”. The table held one entry per process identifier for each object and property.
    • #1147 had replaced a silent overwrite with a refusal. So once one workstation subscribed with process identifier 1, every other client using 1 got RESOURCES / NO_SPACE_TO_ADD_LIST_ELEMENT for that object. Any peer could also block popular identifiers on purpose.
    • Within an object and property, a context is now matched by process identifier, address, service (SubscribeCOV or SubscribeCOVProperty) and array index. The service and array index are this stack’s context identity from #760 (the clauses name address, process identifier and monitored object/property). GetSubscription takes them too; its defaults select the SubscribeCOV context.
      • A SubscribeCOV followed by a SubscribeCOVProperty on the same property used to be aborted with Abort(other). It is now a second context.
      • SubscribeCOVProperty contexts for two array indexes used to overwrite each other. They are now separate.
    • Re-subscription and cancellation still match only the requester’s own context (#1147). The active-subscription limits are unchanged.
  • rb bs-20260924-13 (#2673, #2668): the Device’s Active_COV_Subscriptions entries.
    • #2673: an entry now carries the subscription’s property array index and COV Increment. Cl. 12.11.39: “If the subscribed-to property represents a numeric quantity, the COV Increment in use for the COV subscription shall be included in the Active_COV_Subscriptions entry”. Before, a SubscribeCOVProperty on Priority_Array[8] was listed as the whole array, and no entry carried an increment. The increment listed is the one a SubscribeCOVProperty request supplied. When none was supplied, the increment in use is the object’s own COV_Increment, for a SubscribeCOV or a SubscribeCOVProperty without one. That value is not yet read into the entry, so such an entry still has no increment; tracked in #2707.
    • #2668: an Access Point SubscribeCOV context is listed with Access_Event, not Access_Event_Time. Table 13-1 footnote 1: “the Monitored Property Reference shall contain the Access_Event property identifier.” Access_Event_Time is still what the subscription watches. A SubscribeCOVProperty context lists the property it named.
  • rb bs-20260924-14 (#2707, the rest of #2673): an Active_COV_Subscriptions entry for a numeric property whose subscription supplied no COV Increment now lists the increment in use (135-2024 cl. 12.11.39). This covers a SubscribeCOV, or a SubscribeCOVProperty without one. For Present_Value the increment in use is the object’s COV_Increment when it has one (cl. 13.15.1.1.6). Otherwise it is “a local matter”, and this stack reports every change, so 0 is listed. A property that is not numeric (a Binary Input’s Present_Value, for example) is listed without an increment. The object’s COV_Increment is read once per object and property, not once per subscriber.
  • rb bs-20260924-14 (#2707): assigning a BACnetCOVSubscription without an increment onto one that has one no longer keeps the old increment.
  • rb bs-20260924-17 (#2762): Active_COV_Subscriptions and Active_COV_Multiple_Subscriptions now report the subscriber’s own BACnet address as each entry’s recipient, taken from the subscription’s stored network parameters:
    • Routed subscriber: the property reported its source network together with the router’s MAC. That address names a different device, or no device. It is now the subscriber’s SNET/SADR.
    • BACnet/SC subscriber: the property reported a hub or peer URI, which is not a BACnet address. The COV-multiple table also kept the old hub’s URI after a hub failover. It is now the subscriber’s VMAC. That is the Originating-Virtual-Address when present. When it is absent, as from a direct-connect peer, it is the sender’s VMAC (135-2024 AB.2.1: “the receiver shall assume the ‘Originating Virtual Address’ to be the VMAC of the sender”).
    • Invalid source network: a source address is reported only for a source network from 1 to 65534 (cl. 6.2.2.1). A frame claiming SNET 0 or 65535 is reported by its link address.
    • Directly connected subscriber on BACnet/IP or MS/TP: unchanged.
  • rb bs-20260924-18 (#2693, slice 1): COV request and value decoding hygiene.
    • Failed decode leaves no stale state (CR2-F15-015, CR2-F15-028). SubscribeCOV, SubscribeCOVProperty, the four COV notification requests (Confirmed/Unconfirmed, single and Multiple) and BACnetClientCOV now reset the object before checking the buffer. Before, a Decode of a NULL or empty buffer on a reused object returned 0 but left the previous decode’s fields in place, still marked valid.
    • Malformed cov-increment rejected where it is decoded (CR2-F15-021). A present but malformed cov-increment in a BACnetCOVReference is now rejected at the cov-increment itself. The request was already rejected, but only because the next field failed on the same bytes.
    • Client_COV_Increment tag class (CR2-F15-036). A BACnetClientCOV is a CHOICE of an application-tagged REAL or NULL. A context tag with the same tag number is now rejected with invalidTag; before, it was decoded as a real or default increment.
    • Citations (CR2-F15-016). The #767 comments in the SubscribeCOV and SubscribeCOVProperty decoders now cite the right run-1 findings.
  • rb bs-20260924-19 (#2693, slice 2): more COV request hygiene.
    • SetCOVNotificationMultipleRequest (CR2-F15-031). The setter now validates the initiating device identifier and the timestamp before changing anything. Before, a failure could leave the object half-updated. Calling it without a timestamp now also clears a previous Timestamp, which used to go on being encoded.
    • Failed decode leaves no stale state. SubscribeCOVPropertyMultiple requests, COV references and COV subscription specifications now reset the object before checking the buffer, as slice 1 did for the other COV decoders.
    • Diagnostics (CR2-F15-032, CR2-F15-039). Encode failures in COVSubscription and COVMultipleSubscription no longer log “Failed to decode”, and CovValue’s time-of-change encode failure no longer logs “priority”. Log messages that named the wrong class or field are corrected, and swapped #endif labels are fixed in four headers. The rendered name “BACnetCov” of BACnetClientCOV is unchanged, because it appears in JSON/XML output that customers may parse.
  • rb bs-20260924-20 (#2693, slice 3): COV copies, clones and allocation failures.
    • Single-object COV notification Clone (CR2-F15-026). A failed child clone used to be dropped while the clone still reported itself valid. Under memory pressure, a notification could then go out missing Status_Flags. Clone now returns NULL instead, as the Multiple siblings already did (#669).
    • COVNotificationMultiple Clone timestamp (CR2-F15-027). The Timestamp is copied exactly. It used to be round-tripped through time_t, which zeroed the hundredths, recomputed the weekday, and turned an unspecified field into 1970-01-01.
    • Copying a stored COV subscription (CR2-F15-006). A copy now keeps the SubscribeCOVProperty fields: the service, array index and COV increment. Copying from a subscription with no value now clears the value and still copies the network parameters.
    • Status_Flags disabled after subscribing (CR2-F15-007). A value update for an object whose Status_Flags is no longer enabled no longer dereferences a NULL status-flags pointer. The value is still compared.
    • COV-multiple reference allocation (CR2-F15-004, partial). A reference that cannot be allocated is no longer stored as NULL, which every lookup dereferenced. The table reports the failure and keeps no empty context. The SubscribeCOVPropertyMultiple response under that failure is unchanged: it is still Result(+).
  • rb bs-20260924-21 (#2471): Node adapter: SendWhatIsNetworkNumber now works under Electron. The C export refuses any non-NULL destinationAddress, even with length 0 (#371). Under Electron, a zero-length Buffer’s data pointer is not NULL, so every call from the CAS BACnet Explorer failed. The generated wrapper now passes NULL when the destinationAddress Buffer is empty. A non-empty Buffer is still passed, and still refused. This is controlled by a new opt-in nullWhenEmpty flag on the input-buffer role in ci_scripts/adapter-roles.json. The C-side rule is unchanged.
  • rb bs-20260924-25 (#2859): DecodeAs (JSON and XML) of a Read-Broadcast-Distribution-Table-Ack, Read-Foreign-Device-Table-Ack or Write-Broadcast-Distribution-Table now includes the table entries. BDT entries carry the B/IP address and broadcast distribution mask; FDT entries carry the B/IP address, Time-to-Live and remaining time. Before, the output named the BVLC function and nothing more, and the CAS BACnet Explorer had to parse the entries itself (chipkin/chipkin-bacnet-explorer#233). Output shape: in JSON, a table with one entry renders its key (BDTEntry / FDTEntry) as an object, and a table with two or more entries renders it as an array; this follows the renderer’s existing rule for repeated elements. A large table now makes the output much longer (roughly 100 bytes of JSON per entry), so a caller whose output buffer fitted only the function name may now get 0 until it is enlarged.
  • rb bs-20260924-26 (#2693, slice 4): COV subscription and value hygiene.
    • COV Increment on a non-numeric property (CR2-F15-014). SubscribeCOVProperty on a property that is not numeric, such as an Access Door’s or Binary Value’s Present_Value, now ignores the COV Increment, as 135-2024 cl. 13.15.1.1.6 requires: “This parameter is ignored if the datatype of the monitored property is not numeric.” Before, a NaN or negative increment made the request fail with PROPERTY / VALUE_OUT_OF_RANGE. “Numeric” is judged per object type by GetNumericMonitoredDatatype (the Table 13-1 rows), the same judgement the notification path and #2707 already use. That judgement is broader than the property’s datatype, so a Multi-state Value’s Present_Value (an Unsigned) is treated as not numeric. A NaN increment there now succeeds, and a valid increment is dropped and no longer listed in Active_COV_Subscriptions; notifications do not change, because they already ignored it. A proprietary REAL property the stack does not know is treated the same way. The #688 check still refuses such an increment for a numeric property. SubscribeCOVPropertyMultiple (cl. 13.16.3.1.2.2) still refuses it and is tracked on #2693.
    • Equality with optional fields (CR2-F15-035). IsEqual on BACnetClientCOV, BACnetCOVSubscription and BACnetCovValue now compares which optional field or CHOICE arm is present before comparing values. A default increment no longer equals a real increment of 0.0, and an absent COV increment no longer equals 0.0. CovValue equality is also symmetric.
    • SetCovValue (CR2-F15-038). It no longer keeps an earlier array index or time of change that the caller did not ask for. A time it refuses (for example hour 24) now leaves the object unchanged. A value it refuses is freed on every failure path, as its ownership contract says.
    • Citation (CR2-F15-024). The array-index range check cites cl. 13.16.3.2.2.3 and quotes it.
  • rb bs-20260924-27 (#2693, slice 5): COV-multiple and COV notification hygiene.
    • Initiating Device Identifier (CR2-F15-029). ConfirmedCOVNotification and ConfirmedCOVNotificationMultiple requests whose Initiating Device Identifier is not a Device object are now rejected with PARAMETER_OUT_OF_RANGE. The unconfirmed forms are dropped, and DecodeAs no longer renders such a frame (the same as I-Am and I-Have with a non-Device identifier). 135-2024 cl. 13.6.1.1.2 says the parameter “shall convey the Device Object_Identifier of the device that initiated the ConfirmedCOVNotification service request”, and cl. 13.7.1.1.2, 13.17.1.1.2 and 13.18.1.1.2 say the same of their services. Before, the object type was kept, the accessor returned only the instance, and a clone relabelled the identifier as a device.
    • Repeated reference in one SubscribeCOVPropertyMultiple (CR2-F15-023). A reference that one request names twice now counts once against the subscription limit and appears once in the initial COVNotificationMultiple. The later occurrence’s increment and timestamped flag are kept, as they were when both occurrences were committed. Before, with one slot free, the second occurrence failed with First-Failed RESOURCES / NO_SPACE_TO_ADD_LIST_ELEMENT.
    • COV-multiple storage cannot be copied (CR2-F15-005). The four classes that own raw pointers (the context value, its COV references, the notification queue and its queued notifications) now delete their copy operations, so an accidental copy is a compile error rather than a double free. The context table is move-only; a context is built in place.
  • rb bs-20260924-28 (#2693, slice 6): SubscribeCOVPropertyMultiple and COV notification hygiene.
    • One unreadable property no longer suppresses the initial COVNotificationMultiple (CR2-F15-022). After Result(+), a subscribed property whose value cannot be read is skipped. The others are still sent, as 135-2024 cl. 13.16.2 requires a notification “for each COV subscription specification”. Before, one failed read cancelled the whole initial notification, and no baselines were stored. A skipped property keeps the baseline it had; a new one has none and is reported at its first evaluation. If nothing can be read, nothing is sent.
    • COV Increment on a non-numeric property (cl. 13.16.3.1.2.2). SubscribeCOVPropertyMultiple now ignores the increment, as the clause requires: it “shall be ignored if the value of the monitored property does not represent a numeric quantity or a BACnetARRAY of numeric quantities”. Before, a NaN or negative increment on, for example, Out_Of_Service failed the request with PROPERTY / VALUE_OUT_OF_RANGE. As in SubscribeCOVProperty (CR2-F15-014), “numeric” is GetNumericMonitoredDatatype’s per-object-type judgement. A whole array read without an index is judged by its elements: an array of numbers keeps the increment and its check, and any other array drops it. SubscribeCOVProperty has no such arm, because its clause (13.15.1.1.6) names only a numeric datatype.
    • COV notification validity (CR2-F15-037). A COV notification is valid when its monitored object is set and it holds at least one value (cl. 13.17.3.1.2: “a list of one or more property values”), whatever order they were set in. InsertCovValueAndTakeControl refuses a NULL or invalid value and frees an invalid one. A refused value no longer changes the notification’s validity: it no longer makes an empty notification valid, or a populated one invalid. When the multiple-notification generator’s value is refused, it drops that queued notification rather than re-queueing it forever. That branch needs a property identifier above 22 bits and has no test.
  • rb bs-20260924-29 (#2693, slice 7): SubscribeCOVPropertyMultiple reports a subscription it could not store (CR2-F15-004). The case is a COV reference that the device’s table refuses because it is full or out of memory. Before, the stack answered Result(+) for a subscription that did not exist. That entry is now the First Failed Subscription, with RESOURCES / NO_SPACE_TO_ADD_LIST_ELEMENT, the same error the capacity check already reports. Entries stored before it stay subscribed, following the cl. 13.16.2 First Failed model; entries after it are not stored. The failing entry gets no initial notification. BACnetDBDevice::AddOrUpdateCOVMultipleSubscription now returns whether the reference was stored. A reference that cannot be stored no longer renews an existing context’s lifetime, Max Notification Delay or address.
  • rb bs-20260924-30: Timer object fixes.
    • Timer no longer counts down while Out_Of_Service is TRUE (#2828). 135-2024 cl. 12.57.9 requires that, while Out_Of_Service is TRUE, “(a) the object shall behave as specified, except that Present_Value shall not automatically count down in the RUNNING state”. Before, the timer kept counting, expired, and fired its State_Change_Values writes and CHANGE_OF_TIMER. Writes to Present_Value, Timer_Running and Timer_State still take effect. A timer without the optional Out_Of_Service property is in service, and it is not read on every Loop. Known gap: Expiration_Time is fixed when the timer starts, and it does not move while the countdown is frozen, so it reads early after an out-of-service period.
    • Writes to a provisioned Timer’s other properties are no longer refused (#2827). Before, only the four properties the timer engine writes (Present_Value, Timer_Running, Timer_State, Default_Timeout) could be written; every other property got PROPERTY / WRITE_ACCESS_DENIED even when enabled and writable. That included properties the engine does not hold, such as Out_Of_Service, Reliability, Object_Name, Description and the intrinsic-reporting properties. Those now follow the generic write path. The engine’s other properties are still refused, even when the host marks them writable, because the engine would never see the write: Update_Time, Expiration_Time, Last_State_Change, Initial_Timeout, Min_Pres_Value, Max_Pres_Value, Resolution, Priority_For_Writing, State_Change_Values and List_Of_Object_Property_References. Out_Of_Service matters most, because cl. 12.57.9 uses it to simulate a timer.
    • Timer State_Change_Values elements refuse an invalid value (#2830, the rest of #1223). BACnetTimerStateChangeValue’s setters for a date-time, a lighting command, an application value or a constructed value now refuse an invalid value and leave the element as it was. Before, the element was marked valid and failed later, when State_Change_Values was encoded. The TakeControl setters free a refused value. SetNoValue, SetDateTime and SetLightingCommand build the new value before they free the old one. An out-of-memory failure now leaves the element as it was, instead of valid with no value (SetNoValue) or invalid with its value lost.
  • rb bs-20260924-31: Timer Expiration_Time and State_Change_Values.
    • Expiration_Time is computed when it is read while the timer is RUNNING (#2826, #2892). 135-2024 cl. 12.57 says of the RUNNING state: “While in this state, Expiration_Time shall be calculated at the time the property is accessed.” A read now returns the current date and time plus the remaining Present_Value. Before, it returned the target fixed when the timer started. That target is wrong after a change of local time (a DST change, or a Local_Time or UTC_Offset write), and wrong while Out_Of_Service freezes the countdown, when it drifts into the past. This closes the gap left by #2828. The CHANGE_OF_TIMER notification’s Expiration_Time is computed the same way, when the algorithm runs (cl. 13.3: “the expiration time that was calculated at the time of application of the algorithm”); that path has no test of its own.
    • A natural expiry sets Expiration_Time to the current date and time (#2826). The clause says, for a timer that expires, “set Expiration_Time to the current date and time; set Update_Time to the current date and time”. Before, a natural expiry kept the start-time target, so Expiration_Time differed from Update_Time whenever the expiry was noticed after the target instant (135.1 7.3.2.47.1.2 compares them). A forced expiry already did this.
    • Unset State_Change_Values elements are no-value (#2829). Cl. 12.57.20 says an element “may contain a value”, and gives no-value as the choice for “no value shall be written”. A timer’s seven elements now start as no-value. Before, an element the host never set was invalid, so a ReadProperty of State_Change_Values, or of an unset index, aborted.
  • rb bs-20260924-32: Schedule Time_Values.
    • A Time_Value with an out-of-range time is refused (#2819). BACnetTimeValue’s validity now includes its time. Before, a time such as hour 24 (a common “end of day” mistake in AddScheduleWeeklyTimeValue) became a valid entry at 00:00: it applied from midnight, and later reads of Weekly_Schedule failed to encode. A Weekly_Schedule day and an Exception_Schedule event now refuse an invalid Time_Value, and SetTime reports an invalid time.
    • A Time_Value must name a specific hour and minute (#2820). 135-2024 cl. 12.24.7 says: “The time portion of each BACnetTimeValue shall contain a specific time.” A Weekly_Schedule write whose hour or minute is unspecified (X’FF’) is refused with PROPERTY / VALUE_OUT_OF_RANGE. The host path and Exception_Schedule events refuse it too. Before, 08:FF:FF.FF was accepted and took effect only from 12:19:17.55.
    • An unspecified second or hundredths in a time reads as zero (#2820). This follows cl. 12.1.7 by analogy: for timestamps it recommends accepting “time values with trailing unspecified octets”. 08:00:FF.FF now takes effect at 08:00, and counts as the same time as 08:00:00.00 in the duplicate-time check. The device’s current time is read the same way. SetWeeklyScheduleDayAndTakeControl applies the duplicate and specific-time checks too. Consequence: the test tool can no longer write these values to a remote device, as with the #1037 duplicate refusal.
    • InsertExceptionTimeValue no longer frees a TimeValue it refuses for an invalid exception index. Its caller frees it, so before this it was freed twice.
  • rb bs-20260924-33: Calendar entries and Schedule exception periods.
    • A malformed BACnetWeekNDay is refused (#2821, the rest of #1225). 135-2024 cl. 21.6: “BACnetWeekNDay ::= OctetString (SIZE (3))”. The octets are month 1..14, week-of-month 1..9 and day-of-week 1..7, each of which may be X’FF’.
      • A Calendar Date_List write whose weekNDay has the wrong length or an out-of-range octet now gets PROPERTY / VALUE_OUT_OF_RANGE.
      • The raw BACnetCalendarEntry::SetWeekNDay(octets, length) overload and BACnetStackCalendar::AddEntry refuse one too.
      • A malformed weekNDay never matches a date. Before, a one-octet weekNDay was read as “any month, any week, any day”, so the Calendar was TRUE every day, and so was every Schedule exception that references it.
      • The decoder still accepts one, so DecodeAs can show what arrived.
    • Schedule exception events need a known period type and in-range fields (#2823). InsertExceptionEvent, behind the host export AddScheduleExceptionEventWithCalendarEntry, accepts only a Date (0), DateRange (1) or WeekNDay (2) period, with every field in its domain.
      • A Date or DateRange year must be 1900..2154, or 0. For a DateRange, 0 means the evaluated year, so Dec 24 to Dec 26 with year 0 recurs every year, as before.
      • A DateRange endpoint must be fully unspecified or name a specific month and day, following cl. 12.1.7: “an unspecified date or a specific date only”. A wildcard month or day in an endpoint is refused.
      • Before, periodType 3 bound the event to Calendar instance 0 without checking that it existed, and any other value was stored and never matched.
      • The check, IsValidCalendarMatchPeriod, lives in BACnetCalendarMatch beside the matcher.
  • rb bs-20260924-7 (#2455): the Audit Reporter property profile now matches Table 12-82.
    • Audit_Priority_Filter is a BIT STRING, not an Unsigned.
    • Monitored_Objects, Maximum_Send_Delay and Send_Now are listed as optional. Maximum_Send_Delay and Send_Now are writable (Table 12-82 footnote 1: “If present, this property shall be writable.”).
    • Audit_Notification_Recipient is no longer listed as a required Audit Reporter property. It is a Device property (cl. 12.11.66), so it drops out of an Audit Reporter’s Property_List. Still to come: writing these properties through WriteProperty (the stack has no write handler yet: a write goes to the host callback, but a stack-held reporter still answers ReadProperty with its own value, so a written value does not read back until the write path lands), Reliability (cl. 12.63.6), generating notifications from operations (#2456, #2457), and a Device profile entry for Audit_Notification_Recipient (O in the Device table) so the recipient can be read.
  • rb bs-20260925-1: elevator floor numbers above 255 are refused (#2809, #2810, the rest of #1217). 135-2024 cl. 21 declares these fields Unsigned8: floor-number and destination in BACnetLandingCallStatus, floor-number in the landing-calls entries of BACnetAssignedLandingCalls (BACnetLandingCall), and floor-numbers in BACnetLiftCarCallList. The generic Unsigned decoder accepted any length, and the accessors cast to uint8_t.
    • Effect before the fix: a peer’s Landing_Call_Control write with floor 257 reached the host as floor 1, and destination 258 as destination 2, so a car was sent to a floor the writer never named. A Registered_Car_Call value of 300 was kept, and re-encoded as a two-octet Unsigned.
    • Now: such a value fails to decode with Reject PARAMETER_OUT_OF_RANGE, and BACnetLiftCarCallList::InsertFloorNumber refuses it.
    • Client side: a response carrying such a floor now fails to decode as a whole. Every property in that ReadPropertyMultiple response is lost, and DecodeAs no longer renders the frame. This is the same trade-off #1217 made for BACnetLandingDoor.
    • Deviation from the issue: #2809 asks for Error PROPERTY / VALUE_OUT_OF_RANGE on WriteProperty. A property-value decode failure can only produce a Reject today, because the confirmed-request path carries reject reasons only. Reporting an Error there needs that path to carry one.
  • rb bs-20260925-10: authorization value classes and the Audit Reporter (F23, test-tool builds).
    • An incomplete Authorization Constraint or Authorization Scope Description is no longer reported valid (#2927, part). SetComplete marked either valid whatever was set. Both of their fields are mandatory (cl. 21), and each now composes validity from them, as the authentication siblings do since #949 and #2787. BACnetAccessToken::SetComplete, the third site #2927 names, is #2804.
    • The Audit Reporter’s Reliability and Status_Flags report delivery faults (#2784). Reliability is Required (Table 12-82), but the reporter held no reliability state and discarded the delivery results that decide it (cl. 12.63.6).
      • It is now CONFIGURATION_ERROR while the reporter is enabled and the Audit_Notification_Recipient device cannot be resolved, either because none is configured or because delivery is deferred for want of a binding.
      • It is COMMUNICATION_FAILURE when a notification could not be sent, and NO_FAULT_DETECTED once a pass sends everything it tried.
      • Status_Flags’ FAULT follows it (cl. 12.63.5). It is served from the reporter; before, it went to the generic path.
      • The condition is detected on a delivery attempt, so an enabled reporter with nothing queued keeps its last value. Disabling the reporter clears it. A delivery deferred because memory ran out is also reported as CONFIGURATION_ERROR; it cannot be told apart from an unresolved recipient.
      • Status_Flags for a stack-held Audit Reporter now comes from the reporter, so a host that served it through its BitString callback is overridden. The reporter does no event reporting, so IN_ALARM is FALSE (cl. 12.63.7 makes Event_State NORMAL).
      • Monitored_Objects that overlap another enabled reporter’s, also CONFIGURATION_ERROR in cl. 12.63.6, are not detected yet.
  • rb bs-20260925-11: AuditLogQuery and the Audit Log object (F23, test-tool builds).
    • AuditLogQuery sequence numbers start at 1 (#2779). cl. 12.64: “Each log record in the log buffer has an implied sequence number that is equal to the value of the Total_Record_Count property immediately after the record is added.” They were 0-based, one below the spec value, so paging with Start At Sequence Number was shifted by one.
    • A record without a priority matches any Target Priority (#2778). cl. 13.19.1.1.2.1: “Audit records without a ‘Priority’ will match any ‘Target Priority’.” A query with Target Priority dropped every non-commanded record.
    • A query with both a device identifier and a device address matches either (#2777). cl. 13.19.1.1.2.1: “When both ‘Target Device Identifier’ and ‘Target Device Address’ parameters are included, a record is a match if it matches either of the two parameters.” 13.19.1.1.2.2 says the same for the source.
      • Such a query matched nothing: the record’s device had to be in device form, and then in address form as well.
      • Records whose device is stored in address form could not be returned at all. They now match the address.
    • The Audit Log’s properties are read from its engine (#2781). Record_Count and Total_Record_Count (both Unsigned64, cl. 12.64.11–12) and Buffer_Size were never served from the stack-held log. A ReadProperty fell to the host callbacks and disagreed with what AuditLogQuery returned from the same log. Enable stays with the host until #2782 is ruled: the engine’s Enable starts FALSE and nothing sets it.
    • Total_Record_Count’s wrap to 1 after 2^64-1 (cl. 12.64.12) is not modelled.
  • rb bs-20260925-12: the AuthRequest server (F23, test-tool builds).
    • AuthRequest refusals carry cl. 17.6.4.1’s error codes, and too much scope is reduced, not refused (#2789). Every refusal used to be SERVICES / SERVICE_REQUEST_DENIED.
      • With no callback and no policy, or when the first policy (the one that decides) is disabled, it is now OPTIONAL_FUNCTIONALITY_NOT_SUPPORTED: “The device is not configured to grant access tokens.”
      • A client the policy has no entry for gets UNKNOWN_CLIENT.
      • A known client that asks for more than its entry allows gets a token for the part it is allowed. cl. 17.6.5.1: “if a client requests “too much scope”, the authorization server will return a reduced scope token rather than returning an error.”
      • Because a known client always gets a (possibly empty) reduced token, NO_POLICY is never returned. Policies carry no audience yet, so UNKNOWN_AUDIENCE is not returned either.
      • The comments’ citations of a non-existent cl. 18.7.1 now name cl. 17.6. A callback that refuses without naming a code still answers SERVICE_REQUEST_DENIED.
    • A policy-granted token can be encoded (#2788). The policy-path token left four mandatory fields of cl. 21’s BACnetAccessToken unset (issued, constraint, key-id, signature), so the ACK reported as a grant could not be sent.
      • It now carries issued (the device’s current time, unspecified without a clock callback), constraint (origin any-network, authentication certified: cl. 17.4.6, “The ‘authentication’ constraint for an access token shall default to “Certified”“), key-id 0 and an empty signature.
      • The token is unsigned: signing is not implemented. Key-id 0 is outside cl. 17.4.6’s 1 and 2, so a validating target selects no key and fails closed.
      • The ACK is trial-encoded, into one unsegmented APDU’s worth of buffer, before it is attached, on the callback path too. An ACK that cannot be encoded, or is larger than that, is answered with an Abort. BACnetAccessToken::SetComplete still does not check its fields; that is #2804.
  • rb bs-20260925-13: the Audit Reporter (test-tool builds) and the audit and authorization value classes (every build that compiles them) (F23).
    • Maximum_Send_Delay and Send_Now can be written (#2783). Table 12-82 footnote 1: “If present, this property shall be writable.”
      • The stack-held Audit Reporter served both, and the profile marked them writable, but a WriteProperty went to the host callbacks and never reached the reporter. It now reaches the reporter.
      • Maximum_Send_Delay supports 0..3600 (cl. 12.63.14); outside that the write gets Error PROPERTY / VALUE_OUT_OF_RANGE. The value is stored; the reporter does not delay notifications (cl. 12.63.14 does not require it), so Send_Now has nothing held back to force and resets once the queue is empty.
      • The other writable-looking reporter properties (Audit_Level, Auditable_Operations, Audit_Priority_Filter, Issue_Confirmed_Notifications) are R in Table 12-82 and are not routed.
    • A rejected re-decode no longer leaves an audit or authorization value valid (#2845: CR2-F23-005, -017, -041).
      • Fourteen Decode bodies rejected a NULL or empty buffer before Reset(), so an object that was valid kept its validity and every stale field. This is the fix #1199–#1202 made elsewhere.
      • The classes: AuditLogQueryParameters, AuditLogQueryRequest, AuditLogRecord, AuditLogRecordResult, AuditNotification, Confirmed- and UnconfirmedAuditNotificationRequest, and the seven Authorization composites: Constraint, Event, Policy, Scope, ScopeDescription, Server and Status.
      • AuditNotification computes its validity from its fields, so its reject clears them.
  • rb bs-20260925-14: the authorization token cache and value classes (#2845 part, F23).
    • The token cache fails closed on a time it cannot judge (CR2-F23-035).
      • When the current time could not be converted, a token with a not-after compared as not yet expired, against the Reset() date. It is now treated as not current.
      • The same applies to a not-before or not-after whose year, month or day is unspecified (X’FF’) or a pattern, or whose hour, minute or second is unspecified. BACnetPrimitiveDATE‘s comparison sorts X’FF’ after every real date, so such a token never expired.
      • The weekday and hundredths may be unspecified.
      • The cache has no production caller yet; it matters once the AuthRequest client uses it.
    • A token for audience -1 serves any device (CR2-F23-036). cl. 17.4.6: “Group number 1 (represented as -1) is reserved to mean “all devices”.” Both cache lookups now accept it for a device lookup; a group lookup still needs its own group number.
    • Closed enumerations are range-checked (CR2-F23-039, the #802 shape). BACnetAuthorizationConstraint’s origin and authentication (0..2 each) and BACnetAuthorizationEvent’s decision (0..13, ending “deny-other (13)”) were stored whatever the wire carried.
      • Out-of-range values now make Decode fail with Reject PARAMETER_OUT_OF_RANGE, and SetDecision refuses them.
      • SetValues checks both constraint values before applying either.
  • rb bs-20260925-15: audit and AuthRequest value checks (#2845 part, F23). The Audit Reporter, AuditLogQuery processor and AuthRequest processor are test-tool builds; the AuditLogQueryRequest validity change (CR2-F23-018) ships in every build with Audit Log or Audit Reporter.
    • Audit_Level is never DEFAULT (CR2-F23-015). cl. 12.63.8: “This property shall not have the value DEFAULT.” BACnetStackAuditReporter::SetAuditLevel now refuses DEFAULT (3) and the reserved 4..127. It keeps none, audit-all, audit-config and the proprietary 128..255.
    • AuditLogQuery answers only for an Audit Log object (CR2-F23-016). A request whose audit-log named another object type (e.g. Analog Value 1) was answered with the Audit Log of the same instance. It now gets Error OBJECT / UNKNOWN_OBJECT (cl. 13.19.1.3.1: “The specified Audit Log object does not exist”).
    • An AuditLogQuery request is valid only with every mandatory field (CR2-F23-018, the #798 shape). SetRequestedCount alone marked it valid. Validity is now derived from audit-log, query-parameters and requested-count whenever one of them is set, so a request missing one of them now fails to encode.
    • An AuthRequest with an empty audience is refused (CR2-F23-029). cl. 17.6.2.1: the audience “is required to contain at least one value”. The policy path now answers Error SERVICES / UNKNOWN_AUDIENCE instead of issuing a token valid nowhere. The codec still accepts the empty list.
  • rb bs-20260925-16: audit notification and factor-format value checks (#2845 part, F23). These codecs ship in every build with Audit Log, Audit Reporter or Credential Data Input.
    • AuditNotification fields are held to their declared ranges (CR2-F23-001). cl. 21: “invoke-id [7] Unsigned8 OPTIONAL, source-user-id [8] Unsigned16 OPTIONAL, source-user-role [9] Unsigned8 OPTIONAL”. BACnetAuditOperation is 0..63 (“Enumerated values 0-31 are reserved for definition by ASHRAE. Enumerated values 32-63 may be used by others”). The primitive decode accepted four octets and the accessors narrowed, so invoke-id 261 read back as 5. Decode now answers Reject PARAMETER_OUT_OF_RANGE, and SetOperation refuses a value above 63. So BACnetStackTestTool_SendConfirmedAuditNotification / ...SendUnconfirmedAuditNotification no longer send an operation above 63. The stale “0-63 ASHRAE, 64-65535 others” comment in BACnetAuditOperation.h is corrected.
    • Audit notification requests compare their notifications (CR2-F23-002). BACnetConfirmedAuditNotificationRequest::IsEqual and the unconfirmed twin compared only the number of notifications, so a read and a write compared equal. They now compare each notification.
    • An audit log record’s log-datum tag is checked (CR2-F23-019). The CHOICE tag must be a context tag, and audit-notification [1] must be an opening tag. Decode now answers Reject INVALID_TAG instead of taking a primitive [1] as the notification. A log-status that is not three bits (BACnetLogStatus: log-disabled, buffer-purged, log-interrupted) is refused with PARAMETER_OUT_OF_RANGE.
    • BACnetAuthenticationFactorFormat vendor fields are Unsigned16 (CR2-F23-031). cl. 21: “vendor-id [1] Unsigned16 OPTIONAL, vendor-format [2] Unsigned16 OPTIONAL”. SetVendorId and SetVendorFormat now refuse a value above 65535, and Decode rejects one with PARAMETER_OUT_OF_RANGE.
  • rb bs-20260925-17: audit notification rendering and optional fields, AuthRequest IsEqual (#2845 part, F23). The notification changes ship in every build with Audit Log or Audit Reporter; the AuthRequest change in builds with STACK_OPTION_AUTH_REQUEST.
    • A decoded audit notification renders its values and result (CR2-F23-003). BACnetAuditNotification::GetElements left out target-value [14], current-value [15] and result [16], so the XML/JSON output of a received notification did not show the written value, the prior value or why the operation failed.
    • An audit notification’s optional fields cannot be present and invalid (CR2-F23-004). A failed re-set of an optional field (e.g. SetSourceObject with an instance above 4194303) left the field marked present over a reset value, so AddNotification accepted the notification and Encode then failed partway. The field is now absent after a failed set. SetTargetProperty, SetTargetValue, SetCurrentValue, SetSourceTimestamp and SetTargetTimestamp refuse an invalid argument, and IsValid() requires every present optional field to be valid.
    • Audit enum comments quote cl. 21 (CR2-F23-006). BACnetAuditLevel.h now gives the ranges as “0-127 are reserved for definition by ASHRAE. Enumerated values 128-255 may be used by others”. BACnetAuditPriorityFilter.h names the production BACnetPriorityFilter and the bit critical-equipment-controls. The AuditOperation ranges were corrected in bs-20260925-16.
    • BACnetAuthRequest::IsEqual checks the type before casting (CR2-F23-028). Its datatype check could not tell it from BACnetRawConfirmedServiceRequest or BACnetRawUnconfirmedServiceRequest, which also report Invalid, so comparing against one of them static_cast an unrelated object (undefined behaviour). It now uses the same typeid guard as the raw request.
  • rb bs-20260925-18: authorization render, memory-count and documentation hygiene (#2845 part, F23). These classes ship in builds with STACK_OPTION_DEVICE_AUTHORIZATION (the AuthRequest-Error class with STACK_OPTION_AUTH_REQUEST).
    • Render hygiene (CR2-F23-033). BACnetAuthRequestError::GetElements inserted its elements and then returned false; it now returns true, as #1243 fixed for BACnetCreateObjectError. BACnetAuthenticationEvent::GetElements rendered decision-details even when absent; it now renders it only when present.
    • Minimum memory counts leave out absent optional fields (CR2-F23-042). BACnetAuthorizationServer counted signing-key-1 and signing-key-2 when neither was set. BACnetAuthorizationScope counted the extended list and its opening/closing wrap, which Encode writes only when the list has entries. The #1816 shape.
    • Authentication and authorization enum headers quote cl. 21.6 (CR2-F23-034, -040). BACnetAuthenticationDecision.h, BACnetAuthorizationDecision.h and BACnetAuthorizationPosture.h showed an extension marker and a 256-65535 vendor range; the 135-2024 productions have neither.
    • The authorization cache header matches the code (CR2-F23-038). It said the lookups do not consult the current time; they do, since #1198. It now says that expired tokens are never pruned, so they keep their FIFO slots.
    • Test hygiene (CR2-F23-021). The disabled AuditLogQuery-ACK truncation sweep passes and is re-enabled, with a control that the full encoding decodes. The Audit Reporter tests use the class’s own guard (BACNET_STACK_TESTTOOL && STACK_OPTION_OBJECT_TYPE_AUDIT_REPORTER).
  • rb bs-20260925-19: authorization decode, network key copy, and audit/authorization documentation (#2845 part, F23).
    • A constructed tag on a primitive OPTIONAL field is malformed, not absent (CR2-F23-043, the #890 residual). BACnetAuthorizationServer (signing-key-1 [1], signing-key-2 [2]), BACnetAuthorizationEvent (decision-details [5]) and BACnetAuthorizationStatus (error-details [3]) read an opening/closing tag with the field’s own number as “field absent”. They decoded valid and left the bytes unconsumed. They now answer Reject INVALID_TAG for an opening tag. A closing tag with the number is left to the enclosing wrapper, which may share it. Builds with STACK_OPTION_DEVICE_AUTHORIZATION.
    • BACnetStackDeviceNetworkKey copies exactly (CR2-F23-045). The copy constructor and operator= re-applied the key through SetNetworkKey, which stops at the first failing setter. An empty connection string copied nothing, so assignment kept the target’s old key. A network type the setter refuses copied the connection string without the network or address. The key is used in std::maps in the responding state machine. Both now copy the base memberwise, and operator= guards self-assignment. Every build.
    • Network-security leftovers are gated (CR2-F23-044). BACnetSecurityPolicy models a production of clause 24, which 135-2024 deletes (“24 DELETED CLAUSE This clause has been removed.”). Nothing referenced it and nothing gated it. It is now gated with its sibling BACnetSecurityLevel under STACK_OPTION_OBJECT_TYPE_NETWORK_SECURITY, which no shipped configuration enables.
    • Stale audit documentation and a dead hook are removed (CR2-F23-020). The AuditLogQuery processor header described the withdrawn 2020 time-window model and a Phase 6 stub; it now describes the 2024 request and ACK. BACnetStackAuditLog’s notification hook had no caller (the Audit Reporter delivers by its own route, #2454) and is removed. BACnetAuditLogRecord.h no longer calls BACnetLogStatus 4-bit. The sequence-number comment was already corrected by #2779.
    • The authorization policy header says what IsAllowed decides (CR2-F23-037). It documented an audience parameter that does not exist. It now says the decision ignores the audience, any cl. 17.4.1 constraint and any time range, and that the table is issuance policy, not the Device’s Authorization_Policy (cl. 12.11.72). Comment only; changing the policy model is left for a later issue.
  • rb bs-20260925-2: NaN is refused by the lighting and color range checks (#2811, #2818; siblings of #661 and #1219). Every ordered comparison with NaN is false, so checks written as value < min || value > max let NaN through everywhere they were used. They are now written as !(value >= min && value <= max).
    • Lighting_Command: a NaN target-level, ramp-rate or step-increment is refused by the setters and by Decode. ApplyLightingCommand also re-checks each one, as defence in depth. No current path reaches those re-checks, and they are untested. Before, a FADE_TO with a NaN target-level wrote NaN into the priority array, so Present_Value read NaN. A RAMP_TO with a NaN ramp-rate never completed. cl. 12.54.6 says a field “outside of the allowable range of values” gets Result(-) PROPERTY / VALUE_OUT_OF_RANGE.
    • Lighting Output Real properties: WriteProperty of Default_Ramp_Rate, Default_Step_Increment, Default_On_Value, Min_Actual_Value or Max_Actual_Value refuses NaN. So do SetLightingOutputActualRange and the default ramp rate and step increment of AddLightingOutputObject (0.1..100.0). A NaN Default_Ramp_Rate used to drive the same endless ramp.
    • BACnetxyColor: a NaN x or y coordinate is refused by the setters and by Decode. cl. 12.65.4: “The range of values for x and y shall be 0.0 to 1.0.”
  • rb bs-20260925-20: Trend Log, Trend Log Multiple and Event Log (F22).
    • A Record_Count purge resets Records_Since_Notification when BUFFER_READY is armed (#2765). cl. 12.27.14 (and in substance 12.25.15, 12.30.20): “A write of the value zero to this property shall cause all log records in the log buffer to be deleted and Records_Since_Notification to be reset to zero.” With a BUFFER_READY algorithm the property is served by the algorithm, which the purge never touched: it kept the pre-purge count. The property now counts from the purge until the next notification, so it reads 1 (the BUFFER_PURGED record), as the Event Log’s own member already did. Last_Notify_Record (pPreviousCount, cl. 13.3.7) and the notification test are unchanged.
    • The Buffer_Size write is bounded (#2767, security). A peer that could write Buffer_Size (not writable by default) reached one up-front allocation of that many records: 4294967295 ended the process, and 0 left a log that could not record anything, not even the BUFFER_PURGED record. The Trend Log and Trend Log Multiple now refuse 0, and anything above 65535 unless the log is already that large, with VALUE_OUT_OF_RANGE. The value 2^32-1 is reserved by cl. 12.25.13 to mean the size is unknown. The new bound MAX_WRITTEN_LOG_BUFFER_SIZE is pinned.
    • Event Log By Time ReadRange over 32767 records (#2769) was already fixed by #2631. A test now pins the issue’s reproduce on an Event Log: 40000 records, Count -1, returns sequence 40000.
  • rb bs-20260925-21: Stop_When_Full on the Trend Log, Trend Log Multiple and Event Log (#2766, #2770). cl. 12.25.12 (12.27.11, and 12.30.17): “When logging ceases because the addition of one more log record would cause the log buffer to be full, Enable shall be set to FALSE and the event recorded.”
    • The stop now happens however the log reaches its last free slots. The stop sequence (the record, a LOG_DISABLED record, Enable := FALSE) ran only for an insert at exactly Record_Count == Buffer_Size - 2. Two cases missed it, filled the log with Enable still TRUE and no LOG_DISABLED record, and then let later inserts overwrite the oldest record:
      • Stop_When_Full set while one slot was left.
      • Buffer_Size 1, where 1 - 2 wrapped.
    • A log whose own re-enable status record filled it no longer takes one more record. When that status record landed on the reserved slot, the stop sequence did run, but ReadyToLog / ShouldLog still reported “ready”, so the caller’s record went into the full buffer and pushed out the oldest one.
    • Now:
      • A full stop-when-full log refuses an insert.
      • With one slot left, only the LOG_DISABLED record is written (keeping BUFFER_PURGED when a purge put it there) and Enable is set FALSE. The insert then reports false: the offered record was not logged.
      • Writing Stop_When_Full TRUE at Buffer_Size - 1 stops the log at once.
  • rb bs-20260925-22: Event Log record timestamps and log-value text (F22).
    • A captured notification’s Event Log record carries the local insertion time (#2771). cl. 12.27.13: Timestamp is “The local date and time that the log record was placed into the log buffer.” The record used to carry the notification’s own event timestamp. For a received notification that is the remote device’s clock, possibly hours off. That duplicated the timestamp already inside the notification datum, lost the insertion time, and left the buffer out of time order for a By Time ReadRange. The event timestamp is still in the datum. The record timestamp now has second resolution (hundredths 0), like every other log record. The test tool’s InsertNotificationLogRecord, which takes an explicit timestamp, is unchanged.
    • REAL log values survive the record string round trip (#2776). The Trend Log Multiple record string (BACnetStack_ReadTrendLogMultipleRecord / InsertTrendLogMultipleRecord) and the Trend Log real-value and time-change strings used the default 6 significant digits: 1234.567 came back as 1234.57. “nan” / “inf” were written but could not be read back, so the whole record was refused. A REAL is now written as the shortest text that reads back to the same float (123.45 stays “123.45”; 1234.567 is “1234.567”). “nan”, “inf” and “-inf” are read back. A finite value too large for a float, and hex-float text, are still refused. The text is written and read in the classic (“C”) locale whatever locale the host sets. A REAL’s text can be up to 3 characters longer than before (e.g. “-3.4028235e+38”), so size a record-string buffer for the widest record, as the DLL documentation says.
  • rb bs-20260925-23: Recipient_List Valid Days and BACnetTimeStamp decoding (F21).
    • A Recipient_List entry whose Valid Days selects no day is accepted (#2742). cl. 12.21.8 lets a device restrict a writable Recipient_List only to all days / all times / all transitions: “A device shall not otherwise restrict the value of Recipient_List entries.”
      • Before, a WriteProperty or AddListElement carrying Valid Days B’0000000’ was answered with Reject(INVALID_DATA_ENCODING), although that is a valid encoding.
      • A ReadProperty-ACK containing such an entry, from a third-party device, failed to decode.
      • BACnetDestination::SetValidDays(0) also refused it (#1190), while an all-false Transitions was accepted.
      • The entry is now kept and re-emitted. It never matches a day, so it is never used.
    • BACnetTimeStamp decoding reports a bad tag as Reject INVALID_TAG (#2743). An unknown CHOICE tag, a tag that cannot be parsed, or a datetime [2] that is not constructed returned with no error set. An AcknowledgeAlarm, or any confirmed service carrying a timestamp, was therefore answered with Abort(other) instead of the Reject cl. 18.9 names: “INVALID_TAG - While parsing a message, an invalid tag was encountered.”
    • A timestamp sequence-number above 65535 is rejected (#2744). cl. 21: “sequence-number [1] Unsigned (0..65535)”. It decoded as valid, and 65536 reached application callbacks as 0 through the 16-bit accessor, while the stack’s own comparison kept the full value. It is now Reject(PARAMETER_OUT_OF_RANGE).
  • rb bs-20260925-24: Event Enrollment and intrinsic-reporting property writes; test tool Recipient_List (F21).
    • Notify_Type, Event_Enable and Notification_Class writes take effect (#2745).
      • What was wrong: an Event Enrollment’s writes, and an intrinsic-reporting object’s writes of these three, never reached the object once a host had made them writable. The dispatch answered “not handled”, so the write went to the host’s SetProperty callback and the generic store, which answered Result(+); reads and event notifications kept the old value.
      • Now: they change the object’s alarm-and-event state, the same three arms the Alert Enrollment already had. The host’s SetProperty callback is no longer called for these three (as for Event_Detection_Enable already).
      • Notify_Type must be ALARM or EVENT (cl. 12.12.6); anything else, including ACK_NOTIFICATION and 256, is VALUE_OUT_OF_RANGE. An Event_Enable that is not three bits is INVALID_DATA_TYPE.
      • A Notification_Class that names no Notification Class object is VALUE_OUT_OF_RANGE. Such a value made AcknowledgeAlarm fail, stopped event detection for the object, and failed GetEventInformation for the whole device.
    • Read-only Event Enrollment properties refuse a write (#2745). The read-only Event_Type (cl. 12.12.5, “This read-only property”) answers WRITE_ACCESS_DENIED. So do Event_Parameters and Object_Property_Reference, which have no write path yet. All three used to answer a silent Result(+) if a host flagged them writable.
    • The test tool sends a Recipient_List entry whose Valid Days selects no day (#2969). BACnetStackTestTool_AddListElementDestinations / RemoveListElementDestinations refused Valid Days 0x00. After #2742 that is a legal value (cl. 12.21.8), and a device under test can now be checked for accepting it. Bit 7 is still refused.
  • rb bs-20260925-25: Alert Enrollment writes and the BUFFER_READY event algorithm (F21, F17).
    • Alert Enrollment Notify_Type and Notification_Class (#2971). These get the same checks #2745 added for Event Enrollment and intrinsic objects:
      • Notify_Type must be ALARM or EVENT; ACK_NOTIFICATION is VALUE_OUT_OF_RANGE.
      • A Notification_Class that names no Notification Class object is VALUE_OUT_OF_RANGE.
    • A wrapped Total_Record_Count below Notification_Threshold no longer moves pPreviousCount (#2728). cl. 13.3.7: pPreviousCount “represents the value of pMonitoredValue at the time the most recent transition to NORMAL was indicated”, and it is updated “When a transition to NORMAL is indicated”. The #1168 fix still re-baselined on a sub-threshold wrap. That discarded the records counted before and across the wrap, so the next notification fired late and reported the wrong Previous_Notification.
    • BUFFER_READY armed on a log that already holds records no longer fires for them (#2729). cl. 13.3.7: “Upon initialization of the event algorithm, this parameter shall be set to the value of pMonitoredValue.”
      • Before, pPreviousCount started at 0. Arming on a Trend Log, Trend Log Multiple or Event Log with 5000 records sent a notification for all 5000 at the next evaluation, or (with a threshold above 5000) left Records_Since_Notification reading 5000.
      • Now the first arming seeds pPreviousCount from Total_Record_Count. Re-arming, for example with a new threshold, keeps the existing value.
  • rb bs-20260925-26: event and fault algorithm write errors (F17).
    • A host callback that declines a write without a code answers WRITE_ACCESS_DENIED (#2731). #1329 made a bare false from the SetProperty callback reject a Time_Delay / Time_Delay_Normal / limit / deadband write. The error code, preset to SUCCESS, was copied into the Error PDU, so a client received Error(PROPERTY, SUCCESS).
      • Where: every event and fault algorithm’s write path, 25 sites in 17 algorithm files including Out-of-Range, Floating Limit, Change-of-Value and Command Failure.
      • Now: a bare decline is WRITE_ACCESS_DENIED, the default BACnetBusinessLogic already uses. A code the callback does supply is kept.
  • rb bs-20260925-27: callback decline error codes outside the event algorithms (F17).
    • A host callback that declines a write or an operation without a code no longer answers Error(…, SUCCESS) (#2987). This is the #2731 follow-up: the same pattern existed outside the event and fault algorithms. The error code was preset to SUCCESS, and a bare false from the callback copied it into the Error PDU.
      • Network Port Max_Master, Max_Info_Frames and FD_Subscription_Lifetime writes: now Error(PROPERTY, WRITE_ACCESS_DENIED), the BACnetBusinessLogic default.
      • LifeSafetyOperation: now Error(OBJECT, OTHER). The cl. 13.13.1.3.1 table has no row for “declined, reason not given”, and OTHER is valid in any error class.
      • A code the callback does supply is kept in every case.
  • rb bs-20260925-28: callback decline error classes, LifeSafetyOperation bounds, false error logs.
    • A declining Network Port write callback is answered with the host’s code in its own class (#2993). This covers the Max_Master, Max_Info_Frames, FD_Subscription_Lifetime and FD_BBMD_Address writes, and matches how BACnetBusinessLogic answers a declining SetProperty callback.
      • ABORT_OTHER is now sent as an Abort PDU. It was sent as Error(PROPERTY, ABORT_OTHER).
      • NO_SPACE_TO_WRITE_PROPERTY and OUT_OF_MEMORY are now class RESOURCES.
      • FD_BBMD_Address no longer turns every decline into Abort(OTHER).
    • LifeSafetyOperation: SERVICE_REQUEST_DENIED from the host callback is class SERVICES (#2993). This follows the cl. 13.13.1.3.1 error table. Every other code stays class OBJECT.
    • BACnetLifeSafetyOperationInfo refuses a Request above 65535 (#2880). Both SetRequest and Decode now check the bound, which BACnetLifeSafetyOperationRequest has had since #2674.
    • A LifeSafetyOperation request whose Requesting Source is at or above STACK_OPTION_MAX_CHARACTER_STRING_SIZE (256 in the full build) copies exactly (#2880). The decoder accepted such a request, but the copy was invalid, so the service answered Abort(OTHER).
    • ReinitializeDevice COLDSTART, WARMSTART and ACTIVATE_CHANGES no longer log “Backup callbacks are missing” (#2977). Only the Backup and Restore states check those callbacks now.
    • AuditNotification decode no longer logs a false ERROR for each absent optional constructed field (#2957). The affected fields are [0], [1], [12], [14], [15] and [16].
  • rb bs-20260925-29: ChangeOfLifeSafety bound (F20).
    • ChangeOfLifeSafety notification parameters refuse an Operation_Expected above 65535 (#2997 item 2). Both SetOperationExpected and Decode check the bound, as the Request (#2674) and Info (#2880) siblings already do. BACnetLifeSafetyOperation is defined only up to 65535.
  • rb bs-20260925-3: out-of-range writes get the Error the object clauses require, and undefined Lighting_Command operations are refused.
    • A WriteProperty carrying an out-of-range value is answered with Error PROPERTY / VALUE_OUT_OF_RANGE (#2817, #2815). The request decodes structurally, but its value is outside the property’s range. The value decoders now record that Error. BACnetConfirmedRequestPDU carries it beside the reject reason, and the data link sends it as an Error-PDU (BACnetDataLink::HandleProcessingErrors), labelled with the request’s service choice and invoke id. The decoders that now do this are BACnetxyColor, BACnetLightingCommand and BACnetColorCommand.
      • Color Present_Value, cl. 12.65.4: “Writes to Present_Value outside of the valid range of values shall cause a Result(-) to be returned with an Error Class of PROPERTY and an Error Code of VALUE_OUT_OF_RANGE.”
      • The same wording covers Lighting_Command fields in cl. 12.54.6. Color_Command fields get the same Error by analogy; the clause for Color_Command, cl. 12.65.6, names VALUE_OUT_OF_RANGE only for an operation it does not define.
      • Before, these got Reject PARAMETER_OUT_OF_RANGE, or Reject OTHER for Color_Command.
      • Other services that decode property values keep the Reject PARAMETER_OUT_OF_RANGE they got before. That covers WritePropertyMultiple, CreateObject and the rest. Their Error form needs extra parameters, such as the first failed write attempt, that this decode cannot supply.
      • Any other decode Error is still not answered, as before.
    • Still open on #2815: whether a field that a given operation does not use should be ignored rather than refused needs an owner ruling.
    • Undefined Lighting_Command operations (#2816). Such an operation now gets PROPERTY / VALUE_OUT_OF_RANGE, and it no longer halts a fade in progress or reads back as the last Lighting_Command. That covers ASHRAE-reserved 15..255 and values above the enumeration’s 65535 ceiling. Before, it was acknowledged as a no-op.
      • Vendor values 256..65535 are still accepted (cl. 21.6: “Enumerated values 256-65535 may be used by others”). They still halt a fade at the same or a lower priority before doing nothing; that is for the owner to decide.
  • rb bs-20260925-30: life safety arming, Write-BDT (F25, F07).
    • BACnetStack_SetIntrinsicChangeOfLifeSafetyAlgorithm refuses an Operation_Expected that is not a BACnetLifeSafetyOperation (#3005). This covers the reserved range 10-63 and values above 65535.
      • Before, the call succeeded, and every ChangeOfLifeSafety notification build then refused the value, so each notification was dropped.
      • The call now returns false and arms nothing.
    • Test tool: a failed BACnetStackTestTool_SendWriteBroadcastDistributionTable discards the accumulated entries (#2954).
      • A refused BACnetStackTestTool_BuildWriteBroadcastDistributionTable entry does the same.
      • Before, the leftover entries went out in front of the next table, which replaces the BBMD’s whole table.
  • rb bs-20260925-4: Lighting Output.
    • Lighting_Command_Default_Priority refuses 6 (#2814). 135-2024 cl. 12.54.27: “The priority value 6 shall not be used for this property. If a value of 6 is written to this property, then a Result(-) shall be returned with an ‘Error Class’ of PROPERTY and an ‘Error Code’ of VALUE_OUT_OF_RANGE.” Before, 6 was acknowledged and stored.
      • AddLightingOutputObject now applies the same check to its default priority (1..16, not 6), and checks its default fade time (100..86400000 ms).
      • Before, an out-of-range default made every command that omitted the field fail.
    • A refused write no longer halts a pending egress or an operation in progress (#2812). cl. 12.54.6.1 halts an operation in progress for “(a) A valid command, other than STOP”, or a Present_Value write. Halting an egress applies its effect: WARN_OFF writes 0.0% and WARN_RELINQUISH relinquishes. So a Present_Value write out of range, or a Lighting_Command missing a required field or with one out of range, used to turn the lights off before being answered with Result(-). Such writes are now validated first, and a refused one changes nothing. Binary Lighting Output had the same ordering. BACnetDBDevice already refuses its invalid values on the wire, and the engine now also checks before halting.
    • A transition started by relinquishing the current command priority belongs to the new winner (#2813). cl. 12.54.19: “A transition comes into effect when the Present_Value is directly commanded or when the current command priority value been relinquished.”
      • Before, it was recorded under the relinquished (now empty) priority. STOP at that priority wrote Tracking_Value back into the empty slot, bringing a relinquished command back. STOP or other commands at the real current priority did not halt it.
      • It now runs at the new winning priority. With every slot empty, it runs at Relinquish_Default, which no command names: no STOP matches it, and any command halts it.
    • Not changed, noted for follow-up:
      • BACnetStackLightingOutput::SetLightingCommandDefaultPriority (host side) still takes any value. Its only callers today are the checked WriteProperty path and AddLightingOutputObject.
      • The test-tool export docs for AddLightingOutputObject do not yet mention the new refusals.
      • Relinquishing a priority that has a WARN_RELINQUISH egress pending starts no transition, because the egress already relinquished the slot. This is pre-existing.
  • rb bs-20260925-5: two refusals move from a Reject to the Error the spec asks for, now that #2914 carries a decode Error to the wire.
    • A Color_Command with an undefined operation is refused (#2915). 135-2024 cl. 12.65.6: “If Color_Command ‘operation’ is written with an enumeration value not shown in Table 12-85 a Result(-) shall be returned with an Error Class of PROPERTY and an Error Code of VALUE_OUT_OF_RANGE.” BACnetColorOperation is closed at stop (6).
      • An operation above 6 now fails to decode with that Error. A WriteProperty answers with that Error-PDU, and WritePropertyMultiple keeps a Reject PARAMETER_OUT_OF_RANGE. SetOperation refuses it too. Before, it was accepted.
      • Refusing an operation that the object written to does not support, such as a CCT operation on a Color object, depends on that object and is left to the host that handles the write. So is NONE (0): Tables 12-85 and 12-88 say it “shall not be written”, but Color_Command reads it back, so the decoder must accept it. An unconfirmed WriteGroup whose change list holds such a Color_Command is dropped as a whole, as it already was for an out-of-range fade-time or ramp-rate.
    • An elevator floor number above 255 is refused with Error PROPERTY / VALUE_OUT_OF_RANGE (#2809 follow-up). This covers the floor and destination of Landing_Call_Control, landing calls, and car-call lists. #2809 as merged refused it with Reject PARAMETER_OUT_OF_RANGE, because a value decode could not yet carry an Error. A WriteProperty now gets the Error that cl. 15.9.1.3.1 names for “The value provided is outside the range of values that the property can take on”.
  • rb bs-20260925-6: Group and Program objects (F29).
    • SECURITY: a Group can no longer be made to read itself without end (#2833). 135-2024 cl. 12.14.5: “Nesting of group objects is not permitted; that is, the List_Of_Group_Members shall not refer to the Present_Value property of a Group object or a Global Group object.”
      • Nothing enforced that rule. A member naming a Group’s Present_Value made the Present_Value builder read the group through itself, so a Group listing its own Present_Value crashed the device, with a stack overflow, on the first read. The member could be planted with BACnetStack_AddGroupMember, or over the network where the host had made List_Of_Group_Members writable.
      • Every path that stores a member now refuses a reference to a Group or Global Group’s Present_Value, ALL or REQUIRED. Refusing ALL and REQUIRED is this stack’s reading of the clause: both expand to include Present_Value. WriteProperty, for the whole list or one element, and AddListElement answer with Error PROPERTY / VALUE_OUT_OF_RANGE, and BACnetStack_AddGroupMember returns false. Other properties of a Group, and OPTIONAL, are still accepted.
      • The builder also reports such a reference as a per-property error instead of reading it.
    • A Group member reference with an array index is read at that index (#2839). Present_Value “contains the values of all the properties specified in the List_Of_Group_Members” (cl. 12.14.6). The builder dropped each reference’s array index, so a member configured as Priority_Array[8] reported the whole array with no index, and Priority_Array[8] and [9] collapsed into one entry. Each reference is now read at its index and reported with it, in the order the member lists them. A reference repeated exactly (same property, same index) is read once.
    • A new Program serves Description_Of_Halt and Program_Location (#2843). Both started as an invalid CharacterString, so reading either one aborted until something set it, and Program_Location had no exported setter to fix that. Both now start as the empty string. No other BACnetStack* object class keeps a CharacterString member, so none of them has the same fault.
  • rb bs-20260925-7:
    • Averaging timestamps can be read before the first sample (#2841). Minimum_Value_Timestamp and Maximum_Value_Timestamp (cl. 12.5.5, 12.5.9) were served invalid while there was no valid sample, so reading them failed. This covered a new object, a table-footnote-1 reset (a write of Attempted_Samples, Window_Interval or Window_Samples), and a run of missed samples. Both are now the fully unspecified BACnetDateTime (all X’FF’).
    • A malformed optional field fails a BACnetHealth decode (#2832). When property [2] or details [3] had its tag present but its content failed to decode, the field was treated as absent. The Health decoded as valid, and the malformed bytes were left for the enclosing decoder. That affected Current_Health and Command_Validation_Result in ReadProperty and COV replies and in DecodeAs output. The decode now fails, the fix #767 made elsewhere, which never reached this header-only class. A failure to decode any of its fields now also marks the value invalid. Reset() leaves it valid, since its default is a healthy value.
    • Status_Flags that cannot be worked out is answered with an Error (#2894). When the stack could not read an object’s Event_State, Reliability or Out_Of_Service, it served an empty BIT STRING, which went on the wire as X’80 00’. Peers rejected the reply, and the stack could not decode its own APDU. One way to reach this is a Network Port added with the generic AddObject.
      • The read now answers with Error PROPERTY / VALUE_NOT_INITIALIZED. A host that serves Status_Flags through its BitString callback still gets to answer, as before. If that callback declines, the read keeps the Error, or answers with the code the host named, rather than taking the default.
      • A COV subscription on such an object now skips that update, because its Status_Flags read fails. Before, it went ahead with empty flags.
      • For an object type the stack does not build the flags for, such as Staging, the default when no callback serves them is now four FALSE flags instead of an empty BIT STRING.
      • The invalid encoding of an empty BIT STRING itself is #2919. The issue also asks whether AddObject should accept object type 56 (Network Port) at all, and whether Object_List should then list it. Both are left for the owner.
  • rb bs-20260925-8: Schedule object (F28).
    • Exception_Schedule is served (#2824, #2911). cl. 12.24.8 makes it a BACnetARRAY of BACnetSpecialEvent. The engine evaluated the special events a host adds, but nothing served them, so a peer saw Present_Value depart from Weekly_Schedule with no way to read why.
      • It is now served as the whole array, with index 0 as its size and index n as the nth event. Every period kind is covered: a Date, a DateRange, a WeekNDay, or a Calendar reference.
      • BACnetStack_AddScheduleExceptionEventWithCalendarEntry and …WithCalendarReference now enable the property, so it is listed in Property_List. The frozen header’s documentation of Exception_Schedule and of these two exports is now out of date; that is for the owner (#2923).
    • Exception_Schedule can be written, once the host makes it writable (#2911).
      • A write may replace the whole array or one element. It is refused as a whole when any element is out of its domain, with Error PROPERTY / VALUE_OUT_OF_RANGE, or has a duplicate time, with DUPLICATE_ENTRY (cl. 12.24.8).
      • Writing index 0 resizes the array. New elements are events with an empty list of time values, as cl. 12.24.8 requires; their period is every date, so they never change Present_Value.
      • A Schedule holds at most 255 events, however they are added. A resize past that is refused with RESOURCES / NO_SPACE_TO_WRITE_PROPERTY, and the two host exports return false at the ceiling.
      • An empty whole-array write is refused (INVALID_DATATYPE) by the generic empty-write check, which exempts only Calendar Date_List. A client clears the array by writing 0 to index 0.
      • The engine’s “every year” DateRange (year 0 on an endpoint with a specific month and day) is served, and accepted, as an endpoint with an unspecified year and a specific month and day. That is not one of cl. 12.1.7’s two endpoint forms; it is the only way to carry the engine’s value.
    • Effective_Period is served when the host never set one (#2825). It is Required (Table 12-28). It is now the all-unspecified BACnetDateRange, which is the value the engine already applied: cl. 12.1.7, “The use of an unspecified date in both the startDate and the endDate means “any date” or “always.”” It used to fall through to the host callbacks, which have no DateRange shape.
  • rb bs-20260925-9: authentication value classes (F23).
    • An incomplete Authentication Event is no longer reported valid (#2787). BACnetAuthenticationEvent::SetComplete marked the event valid whatever was set, so an event missing a mandatory field (timestamp, peer, client or decision; cl. 21) was accepted, then failed with an Abort when encoded. It now composes validity from those four fields, as its siblings Client, Peer, Policy and PolicyElement have since #949.
    • A rejected re-decode no longer leaves a stale value valid (#2786). Decode of BACnetAuthenticationFactorFormat, BACnetAuthenticationPeer, BACnetAuthenticationPolicy and BACnetAuthenticationPolicyElement rejected a NULL or empty buffer before Reset(). A previously valid object therefore kept IsValid() true, and every field, after a failed decode. They now clear validity, as #1199–#1202 did for the other four classes.
    • Copying an AuthRequest Error, or any CharacterString, keeps its text whole (#2785).
      • BACnetAuthRequestError’s copy went through SetErrorDetails with the length cast to uint8_t, so details of 256 octets or more were truncated modulo 256 or dropped, leaving the target’s own previous details in place. It now copies the member.
      • BACnetPrimitiveCharSTRING’s copy went through SetValue, which refuses a string at or above STACK_OPTION_MAX_CHARACTER_STRING_SIZE (256 by default). Decode applies no such limit, so any string a frame carries arrives. A copy of a longer one kept the target’s own previous value while taking the source’s validity: a valid-looking copy with the wrong text. It now copies the value and encoding directly.
      • Both affect every stored or cloned copy of a received Error-PDU, in every build.
  • rb bs-20260928-1: alarm acknowledgement and event state (F16, F17).
    • A transition that Event_Enable disables can still be acknowledged (#2684, #3013). This covers TO_OFFNORMAL, TO_FAULT and TO_NORMAL. When its Ack_Required bit is set, such a transition clears its Acked_Transitions bit. Its parameters were not recorded, though, so an AcknowledgeAlarm of the real state and time stamp got SERVICES / INVALID_EVENT_STATE, and the transition could never be acknowledged.
      • The parameters are now recorded whatever Event_Enable holds; Event_Enable gates only the notification (cl. 13.2.5).
      • The acknowledgment is a Result(+). As before (#2685), no ACK_NOTIFICATION is distributed for a disabled transition.
    • A non-conformant unconfirmed ACK_NOTIFICATION is no longer offered to the Event Log or the Notification Forwarder (#3014). This is one that carries AckRequired, From State or Event Values.
      • It is still decoded as sent, and the test-tool receive hook still sees it (#2678).
    • Writing Event_Algorithm_Inhibit TRUE on an Event Enrollment returns it to NORMAL at the next Tick (#3017). Before, it waited for its monitored value to change. Intrinsic reporting objects already behaved this way (#2713).
      • This applies when the monitored object is on the same device. One on another device still waits for its value.
      • A write of FALSE is evaluated at the next Tick too, so a condition that still holds re-enters OFFNORMAL once its time delay runs out.
  • rb bs-20260928-10: Landing_Calls floor text, AddVirtualNetwork, BACnet/SC hub connector configuration (F01).
    • ElevatorGroup Landing_Calls and Landing_Call_Control floor text (#3040).
      • A floor-text length past the buffer, or one that fills it, is refused. Before, the constructor copied past the buffer (a stack over-read), and the element it could not hold was reported as a successful read.
      • useFloorText is honoured. It was fetched and never read, so text sent with “no floor text” still went out.
      • An element no longer inherits the previous element’s floor-text length.
    • AddVirtualNetwork checks every input before changing anything (#3029, #3028).
      • The checks cover the network number, whether that virtual network already exists, the Network Port instance range, and whether that Network Port object already exists.
      • Before, a refused Network Port step left the network registered and the device a virtual router, so the corrected retry was refused.
      • Naming an existing Network Port (for example the BACnet/IP port) is now refused. Before, the call reported success and overwrote that port’s Network_Number with the virtual network’s.
    • SetBACnetSCHubConnector (both overloads) checks every input before changing anything (#3035).
      • The checks cover the VMAC shape, the primary URI (present, wss, at most 255 octets) and the failover URI (NULL only with length 0, wss).
      • Before, a refused call had already replaced the VMAC and erased the primary hub URI.
    • SetBACnetSCHubConnector with no failover URI clears the one set by an earlier call (#3036).
      • This is what the header’s “may be empty if none” promises.
      • Before, the connector kept failing over to the old hub.
      • Behaviour change: a caller that re-applies the configuration without its failover now removes the failover.
  • rb bs-20260928-11: DateTime Priority_Array, special date values, object-identifier setters, virtual network numbers (F01).
    • A commandable DateTime Value’s Priority_Array reads (#3051).
      • The stack could write a DateTime Value at a priority but not read it back: GetPriorityArray had no DateTime arm, so Priority_Array (and Present_Value, which is resolved from it) answered value-not-initialized.
      • The read now uses the Bool, Date and Time callbacks, as the scalar DateTime read does. DateTime Pattern Value is served the same way.
      • Cl. 19.2.1.1 lists both among the objects whose “designated properties … may optionally be commandable”.
    • StringToDate accepts the BACnet special date values (#3046, owner ruling).
      • Accepted: month 13 and 14 (odd and even months) and day 32, 33 and 34 (last, odd and even days).
      • Cl. 20.2.12: “A value of 13 in the second octet shall indicate odd months”.
      • DateToString already wrote these values, so a date pattern did not round-trip and could not be sent as text.
    • SetTrendLogStartStopTime and SetLiftOrEscalatorEnergyMeterRef refuse an out-of-range object type or instance (#3030).
      • Before, the identifier was masked onto a different object and the call returned true.
      • Every sibling export already refuses these values. The check is a range check at the interface; converting the two exports to the packed identifier (as their siblings were, WP-6) is left to the boundary work.
    • AddVirtualNetwork refuses a network number one of the device’s own Network Ports already carries (#3136).
  • rb bs-20260928-12: property profiles for Access Point, Trend Log Multiple, Analog Input and Binary Lighting Output (F02).
    • Access Point Priority_For_Writing is required (#3077).
      • Table 12-36: “Priority_For_Writing Unsigned (1..16) R”.
      • It was profiled optional, so a new Access Point did not list it in Property_List and answered it UNKNOWN_PROPERTY.
      • After AddAccessPointObject, the stack’s Access Point engine serves it (default 8). An Access Point added with a plain AddObject is answered by the host’s UInt callback, as its other required properties are.
    • Trend Log Multiple Start_Time and Stop_Time are writable (#3076).
      • Cl. 12.30.9 says “This property shall be writable if present”, and cl. 12.30.10 the same.
      • AddTrendLogMultipleObject makes both present. They were profiled read-only.
    • Analog Input Event_Message_Texts and Event_Message_Texts_Config are arrays (#3073).
      • Cl. 12.2.27: “of type BACnetARRAY[3] of CharacterString”.
      • They were profiled as scalars, so every indexed read was refused with PROPERTY_IS_NOT_AN_ARRAY.
      • Analog Input was the only object type whose profile had them as scalars.
    • Binary Lighting Output enables Time_Of_Active_Time_Reset and Time_Of_Strike_Count_Reset alongside Elapsed_Active_Time and Strike_Count (#3066).
      • Table 12-69 footnotes 2 and 3 require each pair to be present together.
      • Open point: cl. 12.55.19/.21 make each reset time writable when its counter accepts non-zero writes, which these counters do. That is filed as #3143.
    • The Binary Lighting Output profile has Event_State (#3079).
      • Table 12-69 lists it as O1, required with intrinsic reporting.
      • Before, it could not be enabled.
  • rb bs-20260928-13: property profiles for Analog Output/Value, Lighting Output, Staging and Audit Reporter (F02).
    • Analog Output and Analog Value Profile_Location and Profile_Name are CharacterString, not arrays (#3142).
      • Tables 12-3 and 12-4 list both as CharacterString. They shared the Event_Message_Texts case, so they were profiled as arrays.
      • A host callback for them is now called without an array index, and an indexed read is refused.
    • Lighting Output profiles its intrinsic-reporting properties and High_End_Trim, Low_End_Trim and Trim_Fade_Time (#3078).
      • The intrinsic-reporting properties are Event_Detection_Enable, Notification_Class, Event_Enable, Acked_Transitions, Notify_Type, Event_Time_Stamps, Event_Message_Texts and Event_Message_Texts_Config.
      • Table 12-64 footnote 1: “These properties are required if the object supports intrinsic reporting.”
      • Before, none of them could be enabled. Arming alarms and events on a Lighting Output now adds the intrinsic-reporting properties to its Property_List, where the arming used to fail on them silently.
      • The trims are profile entries only: the stack does not yet clamp Tracking_Value to them or report TRIM_ACTIVE (cl. 12.54.49). That is #3145.
    • Staging profiles its intrinsic-reporting properties, Reliability_Evaluation_Inhibit and Value_Source (#3080).
      • Table 12-80 lists them. Staging profiled Event_State as required but none of these, so its intrinsic reporting could not be configured.
      • Value_Source is read-only, as on every host-managed object type. A cl. 19.5 write to it is refused.
    • Audit Reporter profiles its intrinsic-reporting properties and Reliability_Evaluation_Inhibit (#3081).
      • Table 12-82 lists them, as #1053 already did for Audit Log.
    • All the added properties are optional and read-only in the profile. Arming alarms and events enables the intrinsic-reporting set on these object types; the host enables Event_Message_Texts, Event_Message_Texts_Config and the trims.
  • rb bs-20260928-14: engine-less object writes, repeat Add calls, Trend Log Multiple leak, Configuration_Files rename (F02).
    • A Program created with the generic AddObject accepts WriteProperty (#3068). Such a Program has no engine. Before, every write answered UNKNOWN_OBJECT for an object that ReadProperty and Object_List both show. Now the write falls through to the host callbacks, as its reads do (#2187).
    • An Access Zone created with the generic AddObject accepts Adjust_Value and Occupancy_Count writes (#3069). Before, they answered UNKNOWN_OBJECT. Now they go to the host callbacks when the zone has no engine.
      • Schedule still answers UNKNOWN_OBJECT in that case (#3147).
    • A repeat AddElevatorGroupObject or AddLiftOrEscalatorObject returns true and changes nothing (#3064, owner ruling). This is the header’s repeat-call rule.
      • Before, a repeat emptied Group_Members, or appended the lift a second time.
      • A failed repeat’s rollback deleted the existing object.
      • Behaviour change: a host that created the object with BACnetStack_AddObject first, then called the typed Add, used to get it set up by that call. Now the typed call returns true and leaves it as created. The header already lists these types under “DO NOT USE” for AddObject.
      • The same rollback hazard in the Notification Class and Network Port Add functions is #3149.
    • RunTrendLogMultiple frees its log data on every failure return (#3062). Before, a failing Trend Log Multiple leaked it on every Loop tick.
      • A failing poll still retries every tick, because Time_Last_Polled is not advanced. That decision is left open as #3150.
    • UpdateObjectIdentifier moves a renamed configuration File’s entry in Configuration_Files to its new instance (#3065). Before, the entry stayed on the old instance.
      • Deleting a configuration File still leaves its entry (#3148).
  • rb bs-20260928-15: Schedule engine-less writes, deleted configuration Files, Add rollbacks, virtual network numbers (F02, F01).
    • Weekly_Schedule, Schedule_Default and Exception_Schedule writes to a Schedule created with AddObject, and not yet configured with AddScheduleObject, no longer answer UNKNOWN_OBJECT (#3147).
      • They fall through to the host or the generic write path, as its reads already do. Without a host callback that can take the value, that path answers with a property error such as WRITE_ACCESS_DENIED.
    • Deleting a configuration File removes it from Configuration_Files (#3148).
      • Before, it stayed listed.
      • A File re-created at the same instance inherited configuration-file status.
    • The Add functions’ failure rollbacks delete only an object the call created (#3149).
      • This covers Network Port, Trend Log, Trend Log Multiple, Event Log and Notification Class.
      • Before, a failed call deleted an object that already existed, because AddObject returns true for one.
      • Reachable examples: a repeat AddNetworkPortObject with an unimplemented network type (such as arcnet) deleted the host’s live port. A typed Add refused by validation (such as maxBufferSize 0) after a generic AddObject deleted the host’s Trend Log, Trend Log Multiple or Event Log.
    • A Network Port may not be given a network number the device already serves as a virtual network (#3140).
      • This applies both through AddNetworkPortObjectWithNetworkNumber and through a Network_Number write, which is answered with PROPERTY / VALUE_OUT_OF_RANGE.
      • AddVirtualNetwork also checks each port’s pending Network_Number, not only its active one.
      • These complete #3136, which guarded only one ordering.
  • rb bs-20260928-16: Lighting Output actual-value bounds, Access User RemoveListElement, Device conditional properties (F02).
    • Lighting Output Min_Actual_Value and Max_Actual_Value stay ordered (#3072).
      • Cl. 12.54.25: “Changing Min_Actual_Value to a value greater than Max_Actual_Value shall force Max_Actual_Value to become equal to Min_Actual_Value”. Cl. 12.54.26 gives the mirror rule for Max_Actual_Value.
      • Before, each write stored only its own bound, so Min above Max was accepted and served.
    • RemoveListElement on an Access User’s Members, Member_Of or Credentials is served by the stack (#3070).
      • WriteProperty and AddListElement already store those lists in the stack.
      • Removal is atomic: if an element named in the request is not present, nothing is removed, and the answer is SERVICES / LIST_ELEMENT_NOT_FOUND.
      • Before, the request answered DEVICE / OPERATIONAL_PROBLEM. In a test-tool build it could also rewrite the host’s copy while the stack’s stayed unchanged.
    • Device Active_COV_Subscriptions, Active_COV_Multiple_Subscriptions, Local_Date and Local_Time follow every service that requires them (#3061).
      • Active_COV_Subscriptions follows SubscribeCOV or SubscribeCOVProperty (Table 12-13 footnote 9). Local_Date and Local_Time follow either TimeSynchronization service (footnotes 3 and 4).
      • Active_COV_Multiple_Subscriptions follows SubscribeCOVPropertyMultiple (footnote 18). Before, enabling that service never listed it.
      • Before, each followed only one service. Enabling only SubscribeCOVProperty never listed Active_COV_Subscriptions, and disabling TimeSynchronization removed Local_Date while UTCTimeSynchronization was still enabled. That also stopped the Schedule and Calendar engines, which read Local_Date.
  • rb bs-20260928-17: Staging / Audit Log / Audit Reporter Status_Flags, Notification Forwarder Process_Identifier_Filter, Lighting Output actual-value range (F01, F02).
    • The stack builds Status_Flags for Staging, Audit Log and Audit Reporter (#3052).
      • Staging (cl. 12.62.8): IN_ALARM from Event_State, FAULT from Reliability, OUT_OF_SERVICE from Out_Of_Service.
      • Audit Log (cl. 12.64.5): IN_ALARM from Event_State, FAULT from Reliability when present; OVERRIDDEN and OUT_OF_SERVICE always FALSE.
      • Audit Reporter (cl. 12.63.5): IN_ALARM and FAULT from Event_State and Reliability when present; OVERRIDDEN and OUT_OF_SERVICE always FALSE. This is the host-held reporter; a test-tool stack-held Audit Reporter still builds its own (#2784).
      • Before, the required property came from the host’s BitString callback, or was an empty BIT STRING.
      • Behaviour change for hosts: serve Event_State, Reliability and (Staging) Out_Of_Service for these objects. The BitString callback is no longer asked for their Status_Flags in the normal case.
        • A component the host declines without an error code reads as its default (NORMAL, NO_FAULT_DETECTED, FALSE), so its flag is FALSE.
        • A component declined with an error code fails the read, unless the BitString callback then answers with exactly four flags (the #2894 fallback).
    • A Notification Forwarder whose Process_Identifier_Filter was never set reads it as NULL (#3139).
      • Cl. 12.51.10: “if the Process_Identifier_Filter property contains a Null, then the notification will be accepted”. Before, the read answered Abort(other) until the host called SetNotificationForwarderProcessIdentifierFilter. Forwarding was already unfiltered.
    • Lighting Output Min_Actual_Value / Max_Actual_Value (#3154).
      • Setting either bound, by WriteProperty or the host, also configures the other at its default (Min 1.0, Max 100.0). Table 12-64 footnote 2: “If either of these properties is present, they shall both be present.” Before, a lone Min_Actual_Value write left Max_Actual_Value reading UNKNOWN_PROPERTY.
      • BACnetStackTestTool_SetLightingOutputActualRange refuses min greater than max and changes nothing (IFC-065). Before, it returned true and stored a repaired or inverted pair.
  • rb bs-20260928-18: Trend Log failed polls, Binary Lighting Output counters, Lighting Output actual-value defaults (F02, F27).
    • A Trend Log or Trend Log Multiple poll whose read fails without an Error is recorded and waits for the next interval (#3150).
      • Cl. 12.25 and 12.30: “Errors that prevent the acquisition of the data … are also recorded.” A read that ends in an Abort or a Reject (for example a host that declines with abortOther) is now recorded as a failure with DEVICE / OTHER, and Time_Last_Polled advances. A Trend Log Multiple records it for that member and logs the others.
      • Before, the run returned without recording anything or advancing Time_Last_Polled, so it retried, and logged an error line, on every Loop tick. A Trend Log Multiple dropped the whole record.
      • A COV-type Trend Log is unchanged: it has no interval to wait for, so a record per failure would be a record per Loop tick (#3159).
    • Binary Lighting Output Elapsed_Active_Time and Strike_Count accept writes of zero only (#3143).
      • Cl. 12.55.18 / 12.55.20: “it shall be a local matter whether the property accepts writes of zero only”. Cl. 12.55.19 / 12.55.21 make the reset time writable if the counter “accepts writes of non-zero values”; the stack holds both reset times read-only.
      • Behaviour change: a WriteProperty, WritePropertyMultiple or CreateObject carrying a non-zero value now answers PROPERTY / VALUE_OUT_OF_RANGE. A write of zero resets the counter and stamps its reset time, as before. There is no host setter for these counters, so only the stack’s own accrual gives them a non-zero value.
    • Lighting Output Min_Actual_Value and Max_Actual_Value read 1.0 and 100.0 from AddLightingOutputObject on (#3158).
      • Both are listed in Property_List from AddLightingOutputObject, but read UNKNOWN_PROPERTY until a bound was set. A range the host already set is kept by a repeat call.
  • rb bs-20260928-19: Device writable-if-present properties, Trend Log failure records, Schedule Out_Of_Service (F02).
    • Six optional Device properties are writable when present (#3074).
      • Time_Synchronization_Recipients, UTC_Time_Synchronization_Recipients, Time_Synchronization_Interval, Align_Intervals, Interval_Offset and Deployed_Profile_Location. Cl. 12.11.31, 12.11.43, 12.11.44, 12.11.45 and 12.11.46: “If this property is present, it shall be writable.” Table 12-13 footnotes 5 and 19 say the same for the group and for Deployed_Profile_Location.
      • Behaviour change: a peer write to one of the four scalar properties the host has enabled now reaches the host’s typed Set callback. Before, it answered WRITE_ACCESS_DENIED. The two Recipient lists are now writable too, but have no typed Set callback; how their writes and list services are served is #3161.
    • A COV-type Trend Log records a persisting read failure once (#3159).
      • A COV log runs on every Loop tick, so a failure that persisted was recorded on every tick, flooding the buffer. It is now recorded once, and again only after a value is read or the failure changes. The value read after a failure is always recorded.
      • This also records, once, an Abort or Reject failure that #3150 had left unrecorded for COV logs.
      • Defensive: a polled Trend Log whose Log_DeviceObjectProperty names another device (not yet supported, and not reachable through AddTrendLogObject or WriteProperty today) waits for its next interval instead of retrying every tick.
    • A Schedule’s Present_Value written while Out_Of_Service is TRUE is held by the stack, and the schedule stops evaluating (#3071).
      • Cl. 12.24.4: Present_Value “shall be writable when Out_Of_Service is TRUE”. Cl. 12.24.14: while it is TRUE, “(a) the Present_Value property is decoupled from the internal calculations”, and “(b) other functions that depend on the state of the Present_Value, such as writing to the members of the List_Of_Object_Property_References, shall respond to changes made to that property”.
      • The write now lands in the Schedule the read is served from, and a change is written to the List_Of_Object_Property_References members. When Out_Of_Service goes back to FALSE, evaluation resumes and the recomputed value is written to the members. Before, the write did not reach the Schedule (in the test harness it answered WRITE_ACCESS_DENIED), and the Schedule kept evaluating while out of service.
      • Behaviour change for hosts: a stack-held Schedule’s Present_Value written while Out_Of_Service is TRUE no longer reaches the host’s Set callback. The stack now reads each Schedule’s Out_Of_Service on every Loop tick; a host that serves Schedules should answer it (unanswered, it counts as FALSE and a debug-logging build logs the failed read).
  • rb bs-20260928-2: Notification Class own reporting, doc corrections (F21, F24, F02).
    • A Notification Class object whose own fault reporting is armed with another class now reads that class as its Notification_Class (#3102). Before, it read its own instance (the #2912 default) while it reported through the other class.
      • Disarming puts the #2912 default back. The property returns to the state it had before the first arm: still present if the host enabled it, absent otherwise (Table 12-21 O1,2).
      • A Notification Class armed to report through itself keeps doing so after a rename.
    • Test tool: the BACnetStackTestTool_PushCredentialDataInputFactor doc now says the push is dropped while Out_Of_Service is TRUE (#3019, IFC-064). The Node api.ts doc says the same.
    • Docs: docs/property-profile-reference.md shows the Device segmentation defaults since #2992: segmentedBoth, Max_Segments_Accepted 16, APDU_Segment_Timeout 5000 (#3106). The generator fix is chipkin/chipkin-bacnet-testing-tool#158.
  • rb bs-20260928-20: declined list reads, portable UTC conversion (F01).
    • A host callback that declines a list or sequence read without naming an error code now answers PROPERTY / READ_ACCESS_DENIED (#3042).
      • This covers CallbackGetListOfEnumerations (which has no errorCode parameter, so every decline named none), a constructed-list decline that names no code, the Lift and Elevator Group sequence callbacks declining mid-list, and an Elevator Group Landing_Call_Control the host declines or answers with an invalid element. The Lift’s Assigned_Landing_Calls, Registered_Car_Call and Landing_Door_Status are arrays: a read of one element (one door’s list) did not reach the check before. A whole-array read already reported an error.
      • Before, the read returned false with no error: ReadProperty answered Abort(other), and ReadPropertyMultiple aborted the whole request. A decline that names a code is answered with that code, as before (#2096).
      • Behaviour change: a host that registers CallbackGetListOfEnumerations for some properties and declines the rest now gets READ_ACCESS_DENIED for the rest instead of an Abort.
    • UTC date/time conversion works on toolchains that are neither unix-like nor Windows (#3047).
      • A date that a 32-bit time_t cannot hold now converts to -1, as glibc’s timegm reports it, rather than to a wrapped date. The Linux tests cannot exercise the non-unix branch itself; they test the portable conversion directly.
      • swsutilities::timegm returned 0 there, so every UTC conversion (Date/Time to seconds, UTCTimeSynchronization, schedule, trend and event times) became 1970-01-01T00:00:00Z, with no build error. It now uses a portable conversion, which also replaces the compile error on a unix toolchain without the feature-test macros (#574).
  • rb bs-20260928-21: Network Port pending values (F02).
    • A written Max_Manager, Max_Info_Frames or FD_Subscription_Lifetime is held pending until ReinitializeDevice ACTIVATE_CHANGES or WARMSTART (#3067).
      • Cl. 12.56.55 (Max_Manager), and 12.56.56 (Max_Info_Frames) in the same words: “Writing to this property shall set the Changes_Pending property to TRUE. A value written to this property shall become effective when the device receives a ReinitializeDevice service request with a ‘Reinitialized State of Device’ of ACTIVATE_CHANGES or WARMSTART.” Cl. 12.56.42 (FD_Subscription_Lifetime) says the same of a writable property.
      • The written value sets Changes_Pending (as the stack serves it) and is what a read returns. The host’s CallbackSetPropertyUInt is called at activation, in the same all-or-nothing pass as a pending Network_Number (the #2326 ruling, option 1). DISCARD_CHANGES drops it, and COLDSTART leaves it pending.
      • If the host refuses one value at activation, the activation fails, every value it had already taken in that pass is handed back (the value read through CallbackGetPropertyUInt before activation), and all stay pending.
      • Before, the write went to the host at once and Changes_Pending stayed FALSE.
      • Behaviour change for hosts: CallbackSetPropertyUInt for these three properties is called at activation, not at the write, and a decline fails the ReinitializeDevice rather than the WriteProperty. A write now needs CallbackReinitializeDevice as well as CallbackSetPropertyUInt registered, as a Network_Number write already did; without them it answers Abort(other). A host that serves Changes_Pending itself through its Boolean callback overrides the stack’s value, as for Network_Number; one that raises its own flag in CallbackSetPropertyUInt now reports it backwards (FALSE after the write, TRUE after activation), so it should stop serving Changes_Pending (#3165).
      • FD_BBMD_Address is not included yet: #3164, with #2999.
  • rb bs-20260928-22: Network Port FD_BBMD_Address pending value (F02).
    • A written FD_BBMD_Address is held pending until ReinitializeDevice ACTIVATE_CHANGES or WARMSTART, and the host takes it whole (#3164, #2999).
      • Cl. 12.56.41: “If this property is writable, then a successful write to this property shall set the Changes_Pending property to TRUE. A value written to this property shall become effective when the device receives a ReinitializeDevice service request with a ‘Reinitialized State of Device’ of ACTIVATE_CHANGES or WARMSTART.”
      • As #3067 did for Max_Manager, Max_Info_Frames and FD_Subscription_Lifetime: Changes_Pending (as the stack serves it) is TRUE, a read serves the written address, DISCARD_CHANGES drops it, and COLDSTART leaves it pending. At activation the host is given the host part and then the port, in the same all-or-nothing pass.
      • #2999: the write used to reach the host at once, in two callbacks, and a decline of the port left the new host part applied while the write answered an error. Now, if the host refuses either part at activation, the address it held before activation is read back and handed back, with every other value it took in that pass, and everything stays pending.
      • Behaviour change for hosts: the FD_BBMD_Address Set callbacks (CallbackSetPropertyNull / CallbackSetPropertyOctetString / CallbackSetPropertyCharString for the host part, CallbackSetPropertyUInt for the port) are called at activation, not at the write; a decline fails the ReinitializeDevice. A write needs CallbackSetPropertyUInt, the host part’s callback for its type (Null, OctetString for an IP address, CharString for a name) and CallbackReinitializeDevice registered; without them it answers Abort(other). CallbackSetPropertyOctetString was required for every host type; it is now required only for an IP address. The written address is held as its encoding, so a host name keeps its full length and character set. The address is handed back only when the host serves FD_BBMD_Address (its Get callbacks) so the value in use can be read.
  • rb bs-20260928-23: intrinsic ChangeOfState Alarm_Values (F01, F02).
    • Alarm_Values of an object armed with the ChangeOfState algorithm reads as the armed alarm values (#3131).
      • After SetIntrinsicChangeOfStateAlgorithmUnsigned (a Multi-state Input or Value), or SetIntrinsicChangeOfStateAlgorithmEnum on an Access Door or Access Zone, Alarm_Values is enabled and served by the stack from the algorithm, each value an application-tagged Unsigned or Enumerated, as High_Limit and the other intrinsic parameters are served after SetIntrinsicOutOfRangeAlgorithm.
      • Before, it read UNKNOWN_PROPERTY until the host enabled it, and then an empty list: the read went to the host, which had no way to serve a list of Unsigned without a constructed callback. The object alarmed on those values all the same.
      • Behaviour change for hosts: once the algorithm is armed with alarm values, the stack answers Alarm_Values for the object; a host that served it through CallbackGetListOfEnumerations (Access Door / Zone) or CallbackGetPropertyConstructed is no longer asked.
      • Known limits, unchanged by this fix: Alarm_Values is not writable by default; a host that makes it writable (SetPropertyWritable) receives the write while reads keep serving the armed values. Disarming (SetAlarmsAndEventsForObjectEnabled(false)) leaves Alarm_Values enabled, as it leaves Time_Delay; it then reads as the host serves it. An armed alarm value is only ever added to the list, never removed.
  • rb bs-20260928-24: Lighting Output trims (F27).
    • Lighting Output High_End_Trim, Low_End_Trim and Trim_Fade_Time take effect (#3145).
      • Cl. 12.54.49: “If the Present_Value is written with a value above the High_End_Trim, the Tracking_Value shall be clamped to High_End_Trim and the property In_progress shall be set to Trim_Active.” Cl. 12.54.50 likewise below Low_End_Trim, for a value above 0.0%.
      • The stack holds the three properties (defaults 100.0, 1.0 and 0 ms; a trim clamps only once it has been set). Tracking_Value, as read and as given to the physical output, is clamped to the Operating Range, and In_Progress reads TRIM_ACTIVE while Present_Value is outside it (cl. 12.54.7) - except when Present_Value is commanded at priority 1 or 2, where cl. 12.54.49 / .50 say the trims “shall not be applied”. STEP_*, STOP, and the start of a fade or ramp use that clamped Tracking_Value (Table 12-67).
      • While High_End_Trim is below Low_End_Trim, Reliability reads CONFIGURATION_ERROR (cl. 12.54.49 / .50) and nothing is clamped.
      • A trim change that puts Present_Value outside the Operating Range fades the output there over Trim_Fade_Time (cl. 12.54.51); a new command ends the trim fade. While TRIM_ACTIVE, “the method for adjusting the calculation of fade-time shall be a local matter” (cl. 12.54.49 / .50): a fade or ramp runs as before and its output is clamped.
      • Known limit (#3169): CONFIGURATION_ERROR is served on Reliability but does not reach intrinsic fault reporting, and it takes precedence over a host-served Reliability while the trims are inconsistent.
      • A write of a trim outside 1.0-100.0, or of Trim_Fade_Time above 86400000 ms, answers PROPERTY / VALUE_OUT_OF_RANGE. The trims are optional and read-only in the stack’s profile; they are writable once the host makes them so (SetPropertyWritable).
      • Before, the properties could be listed (since #3078) but nothing stored or applied them.
      • Behaviour change for hosts: a Lighting Output with a stack-held engine now serves these three properties itself, and Reliability while the trims are inconsistent; a host that served them is no longer asked. As for any instant change of the output, a trim change with Trim_Fade_Time 0 does not fire CallbackLightingOutputTrackingValueChanged (only Tick()-driven changes do).
  • rb bs-20260928-25: DeviceCommunicationControl DISABLE_INITIATION (F13) and an intrinsic Time_Delay regression pin (F01).
    • DISABLE_INITIATION exempts an I-Am / I-Have only as the Who-Is / Who-Has reply (#3093).
      • Cl. 16.1.2: under DISABLE_INITIATION “the responding BACnet-user shall discontinue the initiation of messages except for I-Am requests issued in accordance with the Who-Is service procedure, I-Have requests issued in accordance with the Who-Has service procedure, and ConfirmedAuditNotification and UnconfirmedAuditNotification requests.”
      • Before, every I-Am and I-Have was let through. Now the stack marks the reply it builds to a Who-Is or Who-Has, and only a marked I-Am / I-Have passes; the audit notifications stay exempt.
      • Behaviour change for hosts: while DISABLE_INITIATION is in force, BACnetStack_SendIAm returns false and sends nothing, and the I-Am after an accepted You-Are is not sent. (The I-Am a foreign device sends after registering goes straight to the data link and never reached this check, before or after.) Who-Is and Who-Has are still answered.
    • An intrinsic alarm’s Time_Delay completes on Tick() with no further UpdateValue (#3135): not reproduced; test added. An Analog Value served by the callback, OUT_OF_RANGE 12..30 with Time_Delay 2 s, is changed to 35 with one UpdateValue. Ticking for 4 s gives HIGH_LIMIT and one TO_OFFNORMAL notification. Going back to 20 returns to NORMAL the same way. The cause at the issue’s pin (42a7daf0, on 6.x) is not determined: that tree already drained the main device’s time-delay queue on Tick().
  • rb bs-20260928-26: out-of-range object identifiers on two Send exports (F01), BACnet/SC Loop after Reset (F09), the foreign-device I-Am under DeviceCommunicationControl (F13).
    • BACnetStack_SendCreateObject and BACnetStack_SendLifeSafetyOperation refuse an out-of-range object identifier (#3059). Per the WP-6 ruling (“out-of-range rejected, not truncated”, docs/interface/IFC-033.md), an object type above 1023 or an instance above 4194303 now returns false and sends nothing. Before, it was masked into the identifier and the request named a different object. SendDeleteObject and the other identifier Send exports already did this. The instance is checked only when it is sent: for CreateObject, when the instance flag (bit 0) is set; for LifeSafetyOperation, when useObjectIdentifier is true. The guard is in BACnetInterface, so no export or signature changes.
    • BACnetDataLinkSC::Loop() after a direct Reset() returns false instead of crashing (#3132). Reset() drops the callbacks and database pointers. Loop() now returns at entry while either is NULL. Previously, once SC was configured again (a UUID set), it dereferenced the NULL callbacks. The public API always re-initialises after a reset, so this path is not known to be reachable from it.
    • The I-Am a foreign device sends after registering is withheld under DISABLE_INITIATION (#3172). It went straight to the data link and skipped the DeviceCommunicationControl check. Cl. 16.1.2 exempts only the I-Am “issued in accordance with the Who-Is service procedure” (#3093), which this is not. It now gets the same check as every other send, including the disable timeout. The registration itself still succeeds, and the I-Am is not sent later.
  • rb bs-20260928-27: I-Am-Router-To-Network full table (F01), Device conditional properties on SetServiceEnabled (F02).
    • BACnetStack_SendIAmRouterToNetwork with no network list announces what cl. 6.6.2 asks for (#3032). Cl. 6.6.2: “each router shall broadcast out each port an I-Am-Router-To-Network message containing the network numbers of each accessible network except the networks reachable via the network on which the broadcast is being made.”
      • On a physical router, the list is now every reachable network, whether directly connected or learned behind another port, except those behind the port it is sent on. This is the same list the stack’s own start-up announcement uses.
      • Before, it listed every enabled port’s own network, including the sending port’s, and omitted learned routes.
      • When nothing is left to announce, it returns false and sends nothing, because cl. 6.4.2 says the message is “followed by one or more 2-octet network numbers”. The same applies to a virtual router with no virtual network.
      • If more than one router port shares the sending data link, it returns false and asks for the list to be passed explicitly.
    • Disabling a time synchronization service no longer removes Local_Date, Local_Time, UTC_Offset or Daylight_Savings_Status (#3155). Table 12-13 footnotes 3 and 4 say “If the device supports the execution of the TimeSynchronization service, then these properties shall be present” (likewise UTCTimeSynchronization). That is “if”, not “only if”. Footnote 12 keeps them present “if the device is capable of tracking date and time”. Enabling still adds them. A host whose device has no clock removes them with SetPropertyEnabled. Removing them had also stopped the Schedule and Calendar engines, which read Local_Date.
      • Enabling You-Are now adds Serial_Number (footnote 11, “if”).
      • The VT services (footnote 2) are left to the host: the stack implements none and cannot serve Active_VT_Sessions.
      • The remaining Table 12-13 footnotes follow a capability, not a service (segmentation, MS/TP manager node, backup and restore, restart, intrinsic reporting), so SetServiceEnabled leaves them alone.
  • rb bs-20260928-28: I-Am-Router-To-Network on a device that is both a virtual and a physical router (F01), and the Lighting Output trim configuration error in reliability evaluation (F27).
    • BACnetStack_SendIAmRouterToNetwork with no network list, on a device that is both a virtual and an enabled physical router, lists both (#3176). Cl. 6.6.2 asks for “each accessible network”. The full table is now the virtual networks plus the physical reachable networks, with each network listed once. It used to list only the virtual networks. When the sending data link serves more than one router port, the physical networks are left out (it no longer refuses outright), so the virtual networks are still announced.
    • A Lighting Output trim CONFIGURATION_ERROR reaches intrinsic fault reporting (#3169). Cl. 12.54.49 / .50: “If the High_End_Trim value is less than the Low_End_Trim value, the Reliability property shall indicate CONFIGURATION_ERROR.”
      • With intrinsic reporting armed, the trim error is now a first-stage fault (cl. 13.2.2.2). The next Tick() after a trim write that enters or leaves the error moves Event_State to FAULT (or back) and sends the notification.
      • Reliability now reads through the alarm-and-event object, so the value read and the value evaluated agree. Before, Reliability read CONFIGURATION_ERROR while Event_State stayed NORMAL.
      • Like the stack’s other configuration checks, it wins a tie with the host’s own first-stage Reliability.
      • While Out_Of_Service is TRUE, a Reliability written to the object is served as the host holds it, whether or not intrinsic reporting is armed (in a build with STACK_OPTION_ALARMS_AND_EVENTS). With intrinsic reporting armed and reliability-evaluation inhibited, Reliability reads NO_FAULT_DETECTED (cl. 12.2.33), as for every other fault.
      • Known gap: arming intrinsic reporting on an object whose trims are already inconsistent does not evaluate it until the next trim, Present_Value or Reliability change (follow-up).
  • rb bs-20260928-29: BBMD Network Port properties per port (F01), evaluating an object armed with a configuration error (F01).
    • The BBMD_ Network Port properties answer for the Network Port the data link serves (#3055).* Annex J.4.4 says the table “shall be supplied by writing to the BBMD_Broadcast_Distribution_Table property of the Network Port Object which represents this B/IP port”.
      • A ReadProperty or WriteProperty of BBMD_Broadcast_Distribution_Table or BBMD_Foreign_Device_Table used to be served from the BACnet/IP data link the request arrived on, whichever Network Port it named. On a device with two BACnet/IP ports, a request naming port B that arrived on port A read, or replaced, port A’s table.
      • A data link claimed by a Network Port now answers only for that port, the same rule as FD_BBMD_Address (#2336). A data link claimed by no Network Port answers as before. A request naming another port now gets the ordinary answer (the default, an empty list, or the host’s value) rather than that port’s live table; routing it to the named port’s own data link is a follow-up.
      • BBMD_Accept_FD_Registrations now reads IsBBMD() (cl. 12.56.39: “indicates whether (TRUE) or not (FALSE) this device shall accept foreign device registrations”). It was TRUE on every BACnet/IP data link, including those that NAK every registration.
    • Arming intrinsic reporting on an object that already has a stack-detected configuration error evaluates it at the next Tick() (#3178). Example: a Lighting Output whose High_End_Trim is below Low_End_Trim, #3169. Before, the TO_FAULT waited for the next trim, Present_Value or Reliability change. A fault the host reports is still evaluated on its UpdateValue.
  • rb bs-20260928-3: one-value writes, 64 KB APDU estimates, a Clause 21 misprint (F03, F07, F02).
    • A write that must carry one value is refused with INVALID_DATATYPE when it carries more (#3026). #2477 did this for a property that is neither a list nor an array. It now also covers:
      • a Value_Source write-back, which used to return before the check;
      • a write to one array element (Property_Array_Index given). Before, a write of two values to one element reached the host with the first value only.
    • The APDU length estimate no longer wraps at 64 KB (#2962). EstimateApduLength() on the ComplexACK and Confirmed-Request PDUs is now 32-bit.
      • A service part of 65533 octets or more used to wrap the estimate to a small value, which was then treated as fitting the peer’s max APDU unsegmented.
      • The responder’s unsegmented-or-segmented decision is now made in 32 bits. This includes the case of a memory buffer filled to 65535 octets, whose 65538-octet APDU used to wrap to 2 and be sent unsegmented. CreateSegment also refuses a payload over 65535 octets, as a guard; its memory buffer cannot hold one today.
    • The property 497 comment in BACnetPropertyIdentifier.h now names audit-source-reporter (#2900). The old comment copied a misprint in 135-2024 Clause 21’s numeric list. The enum value is pinned by a test, as error codes 215-217 already were.
  • rb bs-20260928-30: rendering BACnetAccessToken and the Device Authorization_Cache profile (F23); Device_Address_Binding wire-level coverage (F01).
    • BACnetAccessToken renders Audience, and NotBefore / NotAfter when present (#3123 part B). They were encoded and decoded but left out of GetElements, so a JSON/XML decode of an access token showed less than the wire carried.
    • The Device Authorization_Cache property has a profile (#3123 part B): BACnetLIST of BACnetAccessToken, read-only, gated like the datatype’s own factory. A received ReadProperty-ACK now decodes it as access tokens rather than UnknownAPDU. Part C (named Origin / Authentication enumerations in BACnetAuthorizationConstraint) is not taken.
  • Tests only: Device_Address_Binding is now checked on the wire in ReadProperty, ReadPropertyMultiple (named and ALL) and ReadRange responses (#2956 part 1). Each response is matched against a routed peer’s and a local peer’s literal BACnetAddressBinding bytes.
  • rb bs-20260928-31: Reliability written while Out_Of_Service under the reliability-evaluation inhibit (F01), BACnet/SC activation independent of the string-size limit (F09).
    • With Reliability_Evaluation_Inhibit set and Out_Of_Service TRUE, a written Reliability drives Event_State (#2925 part 1).
      • Cl. 12.2.33: “When reliability-evaluation is disabled, the Reliability property shall have the value NO_FAULT_DETECTED unless Out_Of_Service is TRUE and an alternate value has been written to the Reliability property.”
      • Cl. 13.2.2.3: “While Out_Of_Service is TRUE, writing to the object’s Reliability property can be used to simulate internal faults.”
      • A Reliability read already served the written value (#2384), but the evaluation used NO_FAULT_DETECTED. Event_State now moves to FAULT with its notification at the next Tick(), and writing NO_FAULT_DETECTED returns it to NORMAL.
      • With the inhibit set and the object in service, the Reliability is still NO_FAULT_DETECTED and Event_State stays NORMAL. A change of Out_Of_Service while the inhibit is set (WriteProperty or UpdateValue) is evaluated at the next Tick(), so returning the object to service clears the written fault.
    • BACnet/SC activation no longer fails on SC_Current_Health Details that exceed STACK_OPTION_MAX_CHARACTER_STRING_SIZE (#2986).
      • An integrator can set the limit below the refresh’s 51- and 55-character texts, and nothing enforces a minimum. The refresh ran after CommitSecureConnectConfiguration, so activation stopped half-applied.
      • The text is now truncated to fit, or left out, since Details is optional. The error class, code and property are still reported.
  • rb bs-20260928-32: a virtual device’s re-key moves its Network Port data link (F13).
    • Re-keying a virtual device (BACnetStack_UpdateDeviceInstance) moves its Network Port’s data-link claim to the new instance (#2979).
      • #2702 made a Device re-key move the claimed data links’ owner. It went through the BACnetInterface back-pointer that only the main device has, so a virtual device’s port stayed claimed under the old instance: FindIpv4DataLinkByNetworkPort(new, port) found nothing.
      • The re-key now uses its own pointer to the BACnetInterface, set for every device it adds, virtual ones included.
      • A WriteProperty(Object_Identifier) of a virtual device now also takes UpdateDeviceInstance, as the main device’s does. Renaming it to another local device’s instance is refused with DUPLICATE_OBJECT_ID, and the virtual-router mapping follows the rename. Before, a virtual device’s rename skipped both.
      • The main device’s back-pointer is unchanged, so the Schedule/Channel engines and the UpdateValue notifications stay the main device’s.
  • rb bs-20260928-33: device re-key and removal on a multi-device (virtual router) stack (F13).
    • BACnetStack_UpdateDeviceInstance refuses a virtual-device rename the virtual router would refuse, before anything moves (#3185).
      • The router step used to run after the database had been re-keyed and the Network Port claims moved (#2979). When it failed, the call returned false with nothing rolled back.
      • Reachable case: a removed virtual device keeps its router mapping, and there is no API to remove it. Renaming another virtual device to that removed instance was refused only after the damage was done.
      • The check now runs first.
      • A refused rename by WriteProperty(Object_Identifier) now answers PROPERTY / DUPLICATE_OBJECT_ID instead of falling through to the host’s SetProperty callback.
    • BACnetStack_RemoveDevice releases the removed device’s Network Port data-link claims (#3184, device part).
      • The claims were never released. The removed device’s (device, port) key still found a data link, and the instance could not be reused.
      • The release clears the claim with the address, subnet mask or MS/TP MAC it configured. The data link’s own state is not reset: a router port binding, BBMD tables and a foreign-device registration pass to the next claim of that link.
      • Removing a Network Port object alone deliberately keeps its claim. The host still names that port in Process(..., networkPortInstance), and ingress is resolved through the claim; #311’s “no Network Port object configured” case depends on it. Whether a port delete should also release the claim is left open on #3184.
  • rb bs-20260928-34: BACnet/SC broadcast frames and the reject rate limiter (F06/F09); a removed virtual device’s router mapping (F13).
    • A frame to the BACnet/SC broadcast VMAC is a broadcast at the network layer (#2896 part 2).
      • The hub connector and hub function ingresses now mark it as B/IP marks a broadcast. A direct connection needs no change: AB.4.2.2 discards a broadcast destination there before the network layer.
      • Before, the virtual router spent a Reject-Message-To-Network rate-limiter token on such a frame (DLEN > 0 to an unknown DNET) before the caller dropped it. On a hub connector the bucket is keyed by the hub’s VMAC, so one node broadcasting such frames silenced the rejects owed to every node behind the hub.
      • Every other broadcast rule now applies to them as well. In particular, a confirmed request to the broadcast VMAC is not answered (cl. 5.4.5.1 ConfirmedBroadcastReceived).
      • Part 1 of #2896 (a limiter for gateway builds without STACK_OPTION_ROUTER_PHYSICAL) is still open.
    • BACnetStack_RemoveDevice removes a virtual device’s virtual-network mapping (#3187).
      • The mapping stayed, so the virtual router kept routing the removed instance to its virtual network.
      • Renaming the main device to that instance succeeded while its traffic was misrouted. Renaming a virtual device to it was refused only by #3185’s pre-check.
  • rb bs-20260928-35: Access Point and Access Zone property values (F24).
    • Access Point Number_Of_Authentication_Policies is never zero (#2794). Cl. 12.31.11: “This property shall always have a value greater than zero.”
      • With no Authentication_Policy_List set (it is optional), it read 0; it now reads 1, the one implicit policy, which Active_Authentication_Policy can select as 0.
      • Setting an empty policy list is refused, since the list’s size must equal the count.
      • Known gap: if the host enables Authentication_Policy_List without setting it, the read is an empty array next to a count of 1.
    • Access Zone Occupancy_Count and Adjust_Value read zero while Occupancy_Count_Enable is FALSE (#2796).
      • Cl. 12.32.11: “If the value of the Occupancy_Count_Enable property is FALSE, then this property shall have a value of zero.” Cl. 12.32.13 says the same of Adjust_Value.
      • Counting could leave both nonzero, and disabling it left them reading that value.
      • The stored values are kept and read again once counting is enabled.
      • Known gap (follow-up): the Access Point’s occupancy-limit checks still compare the stored count while counting is disabled.
      • Interpretation, flagged for the owner: while Out_Of_Service is TRUE, Occupancy_Count serves its written value even with counting disabled. Cl. 12.32.10(c) decouples it “from the processing of occupancy counting” and (e) makes it writable “to allow simulating specific conditions”.
  • rb bs-20260928-36: access control (F24).
    • An Access Point’s occupancy-limit checks skip a zone whose counting is disabled (#3191). Such a zone reads Occupancy_Count 0 and Occupancy_State DISABLED (#2796), and the stack’s rule is that it has no occupancy to enforce, in either direction. Before, its stale stored count could deny an entry with DENIED_UPPER_OCCUPANCY_LIMIT or DENIED_LOWER_OCCUPANCY_LIMIT. (A literal comparison with the reported 0 would instead deny every exit under a lower limit above 0.)
    • Access Credential Absentee_Limit counts whole unused days (#2797). Cl. 12.35.16: “An Absentee_Limit of 0 specifies that the credential this object represents must be used every day to avoid being disabled due to inactivity.”
      • The current day, which may still see a use, no longer counts.
      • Before, a limit of 0 disabled a credential used every day at the first midnight, before the day’s use, and it then stayed disabled.
      • Every limit now disables one day later than before: a limit of N disables after N+1 whole unused days.
    • Credential Data Input keeps Present_Value’s Format-Type within Supported_Formats (#2800).
      • Cl. 12.36.4: the Format-Type “shall take on the value UNDEFINED” when it is “not specified in the Supported_Formats property, such as after a modification of the Supported_Formats property or after the Out_Of_Service property changes from TRUE to FALSE”. Before, the dropped format kept reading.
      • While Out_Of_Service is TRUE, a simulated value reads as written.
      • The Format-Type is derived when Present_Value is read. The stored value, and so Update_Time, does not change.
      • Setting Supported_Formats sizes Supported_Format_Classes to it, new elements taking 0 (cl. 12.36.9.1: “shall be maintained so that both have the same size”). Setting the classes is unchanged; as its test-tool export documents, the caller keeps them that size.
  • rb bs-20260928-37: access control (F24).
    • Access Point Active_Authentication_Policy is a 1-based array index (#2793). Cl. 12.31.10: “The value of the Number_Of_Authentication_Policies property specifies the maximum value that can be written to Active_Authentication_Policy”, and the value “is an array index that corresponds to the authentication policy as specified in the Authentication_Policy_List property”.
      • It was 0-based. With two policies, a write of 2 (the last policy) was refused and a write of 0 was accepted as a policy.
      • 1..Number_Of_Authentication_Policies is now accepted. A write of 0 or above the maximum is refused with VALUE_OUT_OF_RANGE.
      • The default is 1, the first policy (or the one implicit policy, #2794).
      • The value drops to 0 (“no valid authentication policy”) only when the list shrinks below it.
      • The CONFIGURATION_ERROR Reliability for 0 is follow-up #3193.
    • Access Door unlock delay and Secured_Status (#2798).
      • Cl. 12.26.18: Door_Unlock_Delay_Time is the time “which the physical door lock will delay unlocking when the Present_Value changes to a value of PULSE_UNLOCK or EXTENDED_PULSE_UNLOCK”. A plain UNLOCK was delayed too; it now unlocks at once.
      • A switch between the two pulse values during the delay keeps the countdown running; it used to unlock at once. (An UNLOCK that supersedes a pending pulse delay still unlocks at once.)
      • The Present_Value change a Loop tick pushes to COV and trend logs now compares the served value. A delay expiry no longer logs an unchanged Present_Value, and a pulse relinquished during the delay is now logged.
      • Follow-up #3194: the pulse timer starts at the command, not when the delay ends.
      • Cl. 12.26.14: Secured_Status is SECURED “if, and only if” (among others) “(d) the Present_Value property has a value of LOCK”. It read the lock actuator, which stays locked during the delay. A client could read Present_Value PULSE_UNLOCK and Secured_Status SECURED at the same time. It now reads the commanded Present_Value.
  • rb bs-20260928-38: access control (F24).
    • Access Door PULSE_UNLOCK / EXTENDED_PULSE_UNLOCK counts its pulse from the end of the unlock delay (#3194).
      • Cl. 12.26.4: with an unlock delay “the door shall remain in the locked state for Door_Unlock_Delay_Time tenths of seconds before it is commanded to the unlocked state”, and PULSE_UNLOCK commands the door “to the unlocked state for a maximum of the time specified by Door_Pulse_Time”.
      • The pulse counted from the command, so the lock was open for the pulse time minus the delay. A pulse no longer than the delay was relinquished before the lock ever opened. (The clause lets a controller end a pulse early as a local matter; never opening the lock is not a useful reading of it.)
      • A Loop tick that spans the end of the delay carries the rest of its time into the pulse.
    • Access Point Reliability is CONFIGURATION_ERROR while Active_Authentication_Policy is 0 (#3193). Cl. 12.31.10: “If this property has the value of zero, then the Reliability property shall have the value CONFIGURATION_ERROR.”
      • The value becomes 0 when the policy list shrinks below the active index (#2793).
      • As with the Lighting Output trims (#3169), this is a first-stage fault for intrinsic fault reporting. Entering and leaving it (a shrinking list; selecting a policy again, by the setter or WriteProperty) is queued for evaluation at the next Tick, and so is arming reporting while it holds.
      • Without intrinsic reporting, the Reliability read reports it.
      • While Out_Of_Service is TRUE, the host’s Reliability is served.
  • rb bs-20260928-39: object/property database (F02).
    • Removing an object whose intrinsic algorithm is in a pending time delay no longer frees its alarm-and-event object twice (#3060).
      • The pending queue holds the same pointer the alarm-and-event map owns. DeleteObject deleted it through both, a heap double free.
      • This was reachable through RemoveObject, and through a remote DeleteObject of a deletable object with intrinsic reporting armed.
      • The pending entry is now only erased. Disarming an object’s alarms and events (SetAlarmsAndEventsForObjectEnabled, enabled FALSE) now drops its pending entry too; it was left pointing at the deleted object.
    • The object-type setters in BACnetDBDevice.cpp exist in a build without STACK_OPTION_ALARMS_AND_EVENTS (#3196).
      • An alarm-and-event #ifdef opened before the Channel block was not closed until after the Event Enrollment helpers. It swallowed every object-type block in between: Channel, Group, Schedule, Calendar, Timer, Averaging, Load Control, the access-control objects, Life Safety and Lighting Output.
      • BACnetDBDevice.h declares those members under their object-type options only. An à-la-carte build with, for example, the Access Point on and alarms and events off declared them but never defined them.
      • The block now closes before Channel and reopens after Binary Lighting Output.
      • The three Reliability-evaluation calls #3193 (bs-20260928-38) added to the Access Point setters are guarded by STACK_OPTION_ALARMS_AND_EVENTS; without it, BACnetDBDevice.cpp did not compile with the Access Point on.
      • Checked by compiling every source file with the access-control, Schedule, Calendar, Timer, Lighting Output and Load Control objects on and alarms and events off, then linking with -Wl,--no-undefined: 0 undefined references. With only the guard move reverted, 40 BACnetDBDevice members are undefined. The Channel, Group, Averaging, Life Safety and Binary Lighting Output blocks were checked by reading, not compiled.
    • #3063 (a double delete in AddScheduleExceptionTimeValue with an unknown exception index) was already fixed by #2899. It gets a regression test.
  • rb bs-20260928-4: CHANGE_OF_BITSTRING, BBMD on a second port (F17, F01).
    • A CHANGE_OF_BITSTRING notification’s Referenced_Bitstring is the monitored value at its own length (#3110). Cl. 13.3.1: it “conveys the value of pMonitoredValue”.
      • Before, it was cut to the armed length, so an 8-bit arming fed a 16-bit Present_Value reported 8 bits.
      • The armed length is still the span of the bitmask comparison.
    • BACnetStack_SetBBMD on a second BACnet/IP Network Port finds its host entry in that port’s own BDT (#3111). It looked in the first port’s table, so it either failed or set the first port’s host index. This changes what the frozen export does on a multi-port device; a single-port device behaves as before, because an unclaimed port was already refused.
  • rb bs-20260928-41: public API, interface and business logic (F01).
    • A virtual device’s ACK_NOTIFICATION reaches a recipient on another network type (#3199).
      • #3045 routed an acknowledgment notification by the recipient’s network type only for the main device, because only the main device has GetInterface().
      • A virtual device’s MS/TP- or SC-typed ACK_NOTIFICATION that arrived over BACnet/IP was refused by the IPv4 data link and dropped.
      • It now routes through the owning BACnetInterface that every device BACnetInterface adds records (BACnetDBDevice::GetOwningInterface). That is the data link layer the virtual device’s other event notifications already use.
  • rb bs-20260928-42: object/property database (F02).
    • Enabling a property per object type keeps its default Writable (#3082).
      • SetPropertyByObjectTypeEnabled stored the default profile through a constructor whose arguments had changed order, so Writable took the default’s Subscribable, which is false.
      • The worst case: SetPropertyByObjectTypeSubscribable(analogOutput, presentValue, true) enables the property first, so it made every Analog Output’s Present_Value refuse WriteProperty with write-access-denied.
      • The whole default profile is now stored. A property enabled per object type now takes its default Writable in both IsPropertyWritable and WriteProperty, so default-writable properties that this made read-only are writable again. Subscribable is unchanged (no default profile is subscribable).
    • The fix sits inside the per-object-type settings that #987’s ruling would remove; it corrects live behaviour until then.
  • rb bs-20260928-43: fault algorithms (F18), logging objects (F22).
    • FAULT_STATE monitors a Boolean value (#2736).
      • Cl. 13.4.5: “The monitored value may be of any discrete or enumerated data type, including Boolean.”
      • A Boolean monitored value was a run error, and a Boolean property could not be monitored at all, although a boolean-value fault value was accepted.
      • TRUE is now 1 and FALSE is 0, as the fault values store them.
    • A log buffer the host cannot allocate is refused instead of terminating the process (#2772).
      • Event Log cl. 12.27.12 (and likewise Trend Log 12.25.13, Trend Log Multiple 12.30.18) reserves 2^32-1 for a buffer “constrained solely by currently available resources”.
      • An Event Log, Trend Log, Trend Log Multiple or (test-tool) Audit Log added with a size that cannot be allocated threw std::bad_alloc / std::length_error out through the Add export.
      • The buffer resize now reports the failure, and the Add returns false with no stack-held log.
      • No size cap is added (audit ruling on #2772). A Buffer_Size write that cannot be allocated is refused too.
  • rb bs-20260928-44: audit (F23), date/time (F28).
    • The (test-tool) Audit Log honours its own Enable and records changes to it (#2782).
      • Cl. 12.64.8: “Logging occurs if and only if Enable is TRUE. Log_Buffer records of type log-status are recorded without regard to the value of the Enable property.” Cl. 12.64: “Audit Log enabling and disabling is recorded in the audit log buffer.”
      • Enable was never read and always reported FALSE.
      • Decision (groomed option 1, flagged):
        • An Audit Log now starts enabled.
        • A change of Enable appends a log-status record (LOG_DISABLED = !Enable), and a change that cannot be timestamped is refused.
        • Records other than log-status are refused while Enable is FALSE.
        • Enable is read from, and WriteProperty reaches, the engine (the #2932 leftover). A test-tool host’s CallbackGetPropertyBool / CallbackSetPropertyBool no longer sees Enable for a stack-held Audit Log.
    • A date-time whose only wildcard is the hundredths octet is a comparable time (#2831).
      • Cl. 3.2: “unspecified time: a time composed entirely of unspecified octets”. Such a value is strictly a time pattern (cl. 3.2 “specific time” admits no unspecified octets), but cl. 12.25.6 ignores Start_Time only for an “unspecified datetime”. Treating X’FF’ hundredths as the second it names is a local decision.
      • A to-the-second time with X’FF’ hundredths (a common wire form) was refused by ToTimeT and treated as “unspecified” by the Trend Log, Trend Log Multiple, Event Log and Load Control start/stop checks.
      • The effect: a future Start_Time was ignored, a Stop_Time never stopped logging, and an Access Credential’s Expiration_Time never expired.
      • Only the hundredths octet is now ignored; a wildcard date, hour, minute or second is handled as before.
  • rb bs-20260928-45: Timer (F28), Channel (F29).
    • Timer: X’FF’ hundredths in the current time no longer adds about 2.55 s to Expiration_Time (#3205).
      • AddMilliseconds added the octet as 255 hundredths.
      • It is now the start of that second, as ToTimeT reads it (#2831).
    • Channel List_Of_Object_Property_References honours the array index and keeps Execution_Delay the same size (#2835).
      • Cl. 12.53 Table 12-62 footnote 1: “These array properties shall be the same size.”
      • Cl. 12.53.11.2: “If the size of this array is increased by writing to array index zero, each new array element shall contain an empty reference. The size of Execution_Delay shall be automatically increased to be the same.”
      • A write to element N replaced the whole array with that one reference; it now replaces only element N.
      • Index 0 resizes both arrays: new references are empty (instance 4194303, cl. 12.53.11.1) and new delays 0. Growth past 256 elements is INVALID_ARRAY_SIZE. An index past the end is INVALID_ARRAY_INDEX.
      • A whole-array write now resizes Execution_Delay with it, keeping the existing delays by position (new ones 0).
      • Follow-ups: #3207 (Execution_Delay[0] per cl. 12.53.12.1) and #3208 (TimeSynchronization with X’FF’ hundredths). The WriteGroup fan-out to empty references is noted on #2836.
  • rb bs-20260928-46: lighting (F27), Channel (F29).
    • A Lighting_Command field the operation does not use is ignored instead of refused (#2815 part 1).
      • Cl. 12.54.6: “If a BACnetLightingCommand is sent that includes an optional field that is not explicitly described for that operation in Table 12-67, then the field value shall be ignored.”
      • The decode range-checked every field, so a STEP_UP carrying an out-of-range fade-time (which STEP_UP does not use) failed.
      • Target-level, ramp-rate, step-increment and fade-time are now stored as decoded. ApplyLightingCommand checks, for the operation’s own fields only, the range and NaN, answering PROPERTY / VALUE_OUT_OF_RANGE.
      • A host-held Lighting Output (CallbackSetPropertyLightingCommand) is protected the same way: a used field that is out of range or NaN is refused with VALUE_OUT_OF_RANGE before the host sees it, and unused fields pass through as before.
      • Color_Command is unchanged: cl. 12.65.6 has no such sentence, and there is no stack-side apply to move the check to.
    • Channel Execution_Delay[0] resizes both arrays (#3207).
      • Cl. 12.53.12.1: “If the size of this array is increased by writing to array index zero, each new array element shall contain zero. The size of List_Of_Object_Property_References shall be automatically increased to be the same.”
      • It was INVALID_ARRAY_SIZE (#1782), which treated the array as fixed-size; 135.1 9.23.2.13 tests fixed-size arrays only.
      • It now resizes like List_Of_Object_Property_References[0] (#2835): new references empty, new delays 0, growth capped at 256. A whole-array Execution_Delay write of another length is still INVALID_ARRAY_SIZE (the clause resizes through index 0 only).
      • The CAS BTL test tool may still select Execution_Delay as a fixed-size array for 135.1 9.23.2.13: follow-up #3210.
  • rb bs-20260928-47: notification parameters (F20).
    • A notification client decodes access-event and change-of-timer notifications without hosting the object (#2869).
      • The access-event [13] and change-of-timer [22] notification-parameter codecs, and their field types (BACnetAccessEvent, BACnetTimerTransition), were compiled only with the Access Point / Timer object types.
      • A build with alarms and events but without those objects (for example an EMBEDDED B_ALSC profile) refused such a notification from another device, and the EventNotification was answered with Reject(invalid-tag).
      • They are now compiled with STACK_OPTION_ALARMS_AND_EVENTS as well, and so are BACnetPropertyStates’ access-event / timer-state / timer-transition arms; the event algorithms stay gated on their objects. #2739’s “arm not compiled in” refusal is removed, since no such arm remains.
      • Footprint (ci_scripts/footprint.sh, -Os objects): EMBEDDED + B_AAC and EMBEDDED + B_BC grow by 15,173 B of flash (+1.5%) and 1,440 B of data. Core-only profiles and FULL are unchanged.
      • Follow-ups: #3213 (the other PropertyStates arms; a lean decode smoke test) and #3212 (a pre-existing Access-Point-only compile break, found by the sweep).
      • Cl. 13.8.2: “After verifying the validity of the request, the responding BACnet-user shall take whatever local actions have been assigned to the indicated event occurrence and issue a ‘Result(+)’ service primitive.”
    • Also fixed a compile break in builds with alarms and events but without Event Enrollment: BACnetStateFaultAlgorithm.cpp used BACnetPrimitiveBOOL (#2736, bs-20260928-43) with its include gated on Event Enrollment.
  • rb bs-20260928-48: life safety event lists (F25) and an Access-Point-only build (F24).
    • A Life Safety Point / Zone has one Alarm_Values, Life_Safety_Alarm_Values and Fault_Values list, not two (#2806).
      • Arming CHANGE_OF_LIFE_SAFETY or FAULT_LIFE_SAFETY stored its lists only in the algorithm. A stack-held object’s own lists stayed empty.
      • As a result, ReadProperty Alarm_Values returned an empty list, Present_Value never latched on an armed alarm value, and RESET_ALARM / RESET_FAULT never unlatched.
      • Arming now writes the armed lists into the object, and the test-tool list setters now update the armed algorithm.
      • Because the setter now reaches the armed algorithm, a Fault_Values list longer than it can hold (64) is refused and leaves both lists unchanged, rather than being silently clamped by the algorithm (#995).
      • Cl. 12.15.17: “This property, of type BACnetLIST of BACnetLifeSafetyState, is the pAlarmValues parameter for the object’s event algorithm.” Cl. 12.15.16 and 12.15.18 say the same for pLifeSafetyAlarmValues and pFaultValues.
    • A build with Access Point objects but without alarms and events compiles again (#3212).
      • BACnetStackAccessPoint.cpp serves Access_Event_Time as a BACnetTimeStamp (#2792), but BACnetTimeStamp was not compiled for that option.
      • The BACnetTimeStamp gate now includes STACK_OPTION_OBJECT_TYPE_ACCESS_POINT.
      • The same sweep found a Life-Safety-Zone-only build failing on BACnetLifeSafetyState in the Zone roll-up; BACnetDBDevice.cpp now includes it outside the alarms-and-events gate.
  • rb bs-20260928-49: access events (F24, F17).
    • Access_Event_Tag identifies the access transaction, not the event (#2795).
      • The Access Point advanced Access_Event_Tag on every access event, so one access request produced several tags (for example AUTHENTICATION_FACTOR_READ, then GRANTED).
      • The tag now advances once per transaction: an access request, a Lockout change, and an Out_Of_Service change. Every event of that transaction carries the one tag.
      • Lockout through Max_Failed_Attempts (LOCKOUT_MAX_ATTEMPTS) is its own transaction after the denial, so the denial stays the final event of the request’s transaction.
      • Cl. 12.31: “All access events that belong to the same access transaction have the same access event tag.” Cl. 12.31.27.1: “When a single access transaction causes multiple access events to be generated, then the granted event or the denied event shall be the final event generated.”
    • ACCESS_EVENT notifies on a change of Access_Event_Time (#2730).
      • The algorithm treated an unchanged Access_Event_Tag as “no new event”, so a second event of the same transaction, fed by a host through BACnetStackTestTool_SetAccessEventContext, was not notified.
      • It now fires when pAccessEventTime changes. A new tag at the same time (two transactions within one hundredth of a second) also counts.
      • The stack’s own Access Point still stamps one context per access request, so only a request’s final event is notified (#3218, unchanged by this batch).
      • Cl. 13.3.12 (a): “If pCurrentState is NORMAL, and pAccessEventTime changes, and pMonitoredValue is equal to any of the values contained in pAccessEvents, then indicate a transition to the NORMAL event state.”
  • rb bs-20260928-5: COV on virtual-router children, COV without an increment (F01).
    • Tick purges expired COV subscriptions and runs the COV-multiple loop on every device, not only the main device (#3116). Before, on a virtual-router child:
      • an expired subscription stayed in its table;
      • its SubscribeCOVPropertyMultiple lifetimes and notifications were never run.
    • A Present_Value COV subscription on an object with no COV_Increment no longer notifies on every update (#3117). Only a changed value notifies now. Cl. 13.15.1.1.6: an increment is “the minimum change in the monitored property that will cause a ConfirmedCOVNotification or UnconfirmedCOVNotification to be issued”. An increment of 0 means any change, not every evaluation. This lowers the notification rate subscribers see for such an object, through the unchanged customer exports.
    • CheckForChangeOfValue gives each of its reads its own error container (#3117). GetProperty returns early when the container it is given is already set, so a failed COV_Increment read stopped the later Door_Alarm_State and Present_Stage reads. This part has no dedicated test.
  • rb bs-20260928-50: CHANGE_OF_RELIABILITY property values (F18) and Life Safety list setters (F25).
    • An intrinsic CHANGE_OF_RELIABILITY notification carries the properties Table 13-5 lists for the object type, in the table’s order (#2763, part a).
      • The fault algorithms know only their own monitored sample, so the notification carried at most that one value. For example, Access Door sent Door_Alarm_State without Present_Value, Global Group sent nothing, and Life Safety Point / Zone sent Mode only.
      • The Property_Values are now rebuilt from the object’s Table 13-5 row, reading each property from the device. For example, Life Safety sends Present_Value, Mode and Operation_Expected; Binary / Multi-state Output send Present_Value and Feedback_Value; Access Point sends its four Access_Event properties.
      • A property the object does not have, or whose value the device cannot read, is left out, as the table’s footnotes 2 and 3 allow.
      • Object types with no Table 13-5 row are unchanged. The Event Enrollment row (its Object_Property_Reference first) is part b of #2763 and still open.
      • Cl. 13.2.5.3: “The property values required to be conveyed are specified in Table 13-5, and shall be included in the order shown in the table.”
    • A Life Safety list setter re-evaluates the object (#3216).
      • SetLifeSafetyAlarmValues, SetLifeSafetyLifeSafetyAlarmValues and SetLifeSafetyFaultValues now re-evaluate the object, as a WriteProperty would, instead of waiting for the next Tracking_Value input.
      • For example, a Fault_Values entry that is also an Alarm_Values entry (a cl. 12.15.10 CONFIGURATION_ERROR) now puts the object into FAULT at the set.
  • rb bs-20260928-51: CHANGE_OF_RELIABILITY property values from an Event Enrollment (F18).
    • An Event Enrollment’s CHANGE_OF_RELIABILITY notification carries Table 13-5’s Event Enrollment row (#2763, part b).
      • It used to carry only the monitored value, under the property identifier the algorithms assume.
      • It now carries, in order:
        • the Event Enrollment’s Object_Property_Reference;
        • the referenced property’s value, under that property’s own identifier;
        • the monitored object’s Reliability;
        • the monitored object’s Status_Flags.
      • A value the Event Enrollment did not read (a Reliability the monitored object does not have, or Status_Flags it could not read) is left out, as footnote 2 of the table requires.
      • Cl. 13.2.5.3: “In the case of the Event Enrollment object, the first property in the property-values parameter shall be the Event Enrollment object’s Object_Property_Reference property. The second property is the property that is referenced by the Event Enrollment object’s Object_Property_Reference property.”
  • rb bs-20260928-52: Event Enrollment reliability and Mode (F18).
    • An Event Enrollment whose monitored object is in fault enters FAULT (#3222).
      • Its Reliability read MONITORED_OBJECT_FAULT, but its Event_State followed the event algorithm (for example HIGH_LIMIT), because the state machine was always given NO_FAULT_DETECTED as the first stage.
      • MONITORED_OBJECT_FAULT is now the first-stage reliability. The Event Enrollment’s own CONFIGURATION_ERROR still takes precedence, and its fault algorithm still comes after, in cl. 12.12.21’s order.
      • A monitored property that cannot be read at all is also MONITORED_OBJECT_FAULT. That evaluation used to stop without a transition, and now runs the first stage.
      • Cl. 12.12.21: “(2) If the reliability is NO_FAULT_DETECTED, then if the reliability of the monitored object is not NO_FAULT_DETECTED, then the reliability shall be MONITORED_OBJECT_FAULT.”
    • An Event Enrollment’s FAULT_LIFE_SAFETY is fed pMode (#3223).
      • Its mode-property-reference was stored but never read, so a Mode change while in MULTI_STATE_FAULT never re-indicated (cl. 13.4.4 condition (c)).
      • The referenced Mode is now fed before each evaluation, and a change of that property (UpdateValue) re-evaluates the Event Enrollment.
      • The Event Enrollment’s monitored value, Status_Flags and Reliability are now read from its own Object_Property_Reference, not from whichever object triggered the evaluation. The two differ only for a Mode trigger on another object.
  • rb bs-20260928-53: Life Safety Mode reporting (F25) and Event Enrollment CHANGE_OF_LIFE_SAFETY pMode (F18).
    • An Event Enrollment’s CHANGE_OF_LIFE_SAFETY is fed pMode (#3226).
      • Its mode-property-reference was stored for Event_Parameters but never read, so an Event Enrollment never sent the Mode-change notification (cl. 13.3.8).
      • The referenced Mode is now fed before each evaluation, as for FAULT_LIFE_SAFETY (#3223), and a report of that property re-evaluates the Event Enrollment.
    • A Mode written to a stack-held Life Safety Point or Zone is reported as Mode (#3225).
      • A write of Mode was reported only as Present_Value, so anything watching Mode itself (an Event Enrollment’s pMode reference, a COV subscription to Mode) did not see it.
      • A write that changes Mode now also reports Mode. A write of the same Mode reports nothing.
  • rb bs-20260928-54: Event Enrollment Life Safety parameters (F18).
    • An Event Enrollment’s CHANGE_OF_LIFE_SAFETY notifications carry the monitored object’s Operation_Expected (#3229).
      • Table 12-15.1 names the monitored object’s Operation_Expected as pOperationExpected for CHANGE_OF_LIFE_SAFETY, and an Event Enrollment’s Event_Parameters carry none of their own.
      • The notifications used to carry the value the Event Enrollment was armed with. The monitored object’s value is now read before each evaluation.
      • A monitored object without an Operation_Expected leaves the value in place.
      • The operationExpected an Event Enrollment is armed with (BACnetStack_SetIntrinsicChangeOfLifeSafetyAlgorithm on an Event Enrollment) is therefore only a fallback. A stack-held Life Safety Point reads NONE until it is given an Operation_Expected, and that is now what the Event Enrollment reports.
    • Re-pointing a Life Safety mode-property-reference is not a Mode change (#3230).
      • Changing an Event Enrollment’s CHANGE_OF_LIFE_SAFETY or FAULT_LIFE_SAFETY mode-property-reference kept the Mode baseline recorded from the old property.
      • As a result, re-pointing it at an object in another Mode sent a Mode-change notification (cl. 13.3.8 (c), (g), (k)) that no Mode change caused.
      • A different reference now starts a new baseline, taken at the next evaluation. For FAULT_LIFE_SAFETY this happens while still in MULTI_STATE_FAULT, so a later Mode change re-indicates as cl. 13.4.4 (c) says.
      • A refused Fault_Parameters write (more than 64 fault values) no longer changes the mode-property-reference.
      • The cl. 13.3.8 (c) comment in the algorithm is now quoted verbatim.
  • rb bs-20260928-55: leaving FAULT onto an off-normal value (F18).
    • When a fault clears while the monitored value is still off-normal, the object returns to its off-normal state (#3227).
      • A transition out of FAULT enters NORMAL (cl. 13.2.2.1.3), and the event algorithm was not run again until the monitored value next changed.
      • As a result, an object whose value was still, for example, above its high limit read NORMAL indefinitely.
      • The event algorithm now judges the current value from NORMAL in the same evaluation. The object re-enters OFFNORMAL, or starts its Time_Delay, as cl. 13.2.2.1.1 Normal, ToOffNormal describes.
      • This applies to intrinsic reporting and to Event Enrollments. The TO_NORMAL of leaving FAULT is still sent first.
      • It applies whether the fault was a first-stage one (a host Reliability, CONFIGURATION_ERROR, MONITORED_OBJECT_FAULT) or the fault algorithm’s, and also when TO_NORMAL notifications are disabled by Event_Enable.
      • Cl. 13.2.2.1.3: “If reliability-evaluation indicates a value of NO_FAULT_DETECTED, then perform the corresponding transition actions and enter the Normal state.”
  • rb bs-20260928-56: Event Enrollment CHANGE_OF_TIMER inputs (F20).
    • An Event Enrollment’s CHANGE_OF_TIMER notifications carry the update-time and the Timer’s optional values (#2876, part 2).
      • Only intrinsic reporting fed CHANGE_OF_TIMER (#2741). An Event Enrollment watching a Timer sent the unspecified update-time and no last-state-change, initial-timeout or expiration-time, and its condition (c) re-alarm on a changed update-time could never fire.
      • pUpdateTime is now read from the Event Enrollment’s Update_Time_Reference referent (Table 12-15: “Referent’s value is pUpdateTime”). A stack-held Timer’s Update_Time is read from the timer engine, as the intrinsic path does; any other local referent is read as a property.
      • A monitored stack-held Timer’s Last_State_Change, Initial_Timeout and Expiration_Time are fed as for intrinsic reporting (Table 12-15.1).
      • A report of the Update_Time_Reference referent re-evaluates the Event Enrollment, so condition (c) re-alarms on a new update-time even when the referent is not the monitored Timer.
      • Part 1 of #2876, making an Event Enrollment’s Event_Parameters writable for CHANGE_OF_TIMER, is still open.
  • rb bs-20260928-59: Protocol_Services_Supported advertises only what the build executes (F01).
    • SetServiceEnabled refuses a service this build cannot execute (#3031, cl. 12.11.14).
      • Protocol_Services_Supported “indicates which standardized protocol services are executed by this device’s protocol implementation”. Before this fix, only createObject/deleteObject, subscribeCOVProperty and subscribeCOVPropertyMultiple were refused when compiled out. Every other compiled-out service, and the unimplemented virtual-terminal services, could be advertised.
      • The check is now one table (IsServiceExecutionCompiled) that carries the same STACK_OPTION_* guard as each service’s case in the confirmed and unconfirmed request processors. The virtual-terminal services (21-23) and the removed services (13, 24, 25) are always refused.
      • Disabling a service is always accepted.
      • Integrators: BACnetStack_SetServiceEnabled can now return false for an in-range service. In a customer (non-test-tool) build this covers:
        • GetAlarmSummary (3), GetEnrollmentSummary (4) and LifeSafetyOperation (37), whose processors are compiled for the test tool only (LifeSafetyOperation unless STACK_OPTION_DM_LSO_LIFE_SAFETY_OPERATION is defined).
        • the audit services (44-46) and AuthRequest (49).
        • any service whose STACK_OPTION_* is not defined, including TimeSynchronization/UTCTimeSynchronization without STACK_OPTION_TIME_SYNCHRONIZATION.
        • Before this fix, the call returned true and the device advertised a service it answered with nothing.
    • UpdateValue no longer returns false in a build without SubscribeCOV (#3031). The COV block is skipped. It used to return false before the alarm/event and COV trend-log evaluation, so those stopped working with only a log line.
  • rb bs-20260928-6: Device authorization property profiles (F02).
    • A ReadProperty-ACK for a Device’s revision 29 authorization properties now decodes to their typed values (#3101). This covers Authorization_Groups, _Policy, _Scope, _Server and _Status (135-2024 Table 12-13). Authorization_Cache waits for #3123, whose AccessToken renderer must first show the audience and validity fields the generic decode shows today.
      • They had no property profile, so they decoded as UnknownAPDU and lost every context-tagged field.
      • Each profile is present only in builds that compile its datatype.
      • A value the typed codecs reject now fails the decode, where before it fell back to UnknownAPDU.
      • None is writable by default.
  • rb bs-20260928-60: an Event Enrollment’s FAULT_STATUS_FLAGS and FAULT_LISTED judge their own referent (F18).
    • Table 12-15.3 is honoured for FAULT_STATUS_FLAGS and FAULT_LISTED (#2734, cl. 12.12.23). The table says “FAULT_STATUS_FLAGS Status_Flags_Property_Reference pMonitoredValue” and “FAULT_LISTED Fault_List_Reference Referent’s value is pMonitoredList”.
      • Before this fix, the reference was stored only so Fault_Parameters could be read back. The fault algorithm was offered the Object_Property_Reference’s value instead, which is usually not a BitString or a list, so the fault algorithm never ran.
      • The Event Enrollment now reads the referent and passes it to CheckAndRunAlgorithms as the fault algorithm’s input (BACnetFaultAlgorithmReferent).
      • A referent that is not local, or cannot be read, skips the fault algorithm for that run.
      • An algorithm armed through BACnetStack_SetFaultStatusFlagsAlgorithm / BACnetStack_SetFaultListedAlgorithm has no reference. It keeps judging the Object_Property_Reference, as before.
      • A report of the referent re-evaluates the Event Enrollment. For a referenced Status_Flags, any report on its object does, because the stack changes Status_Flags itself.
    • FAULT_LIFE_SAFETY’s pMode feed landed earlier (#3223, #3230). With this, all three of the issue’s alternatives are wired.
  • rb bs-20260928-61: GetEventInformation survives a deleted Notification Class (F16).
    • An active object whose Notification Class no longer exists is listed with Event Priorities 255/255/255, not a service failure (#2687).
      • The Notification Class may have been deleted or renumbered after the objects that reference it were configured. Before this fix, the first such object, intrinsic or Event Enrollment, failed the whole GetEventInformation with DEVICE/INTERNAL_ERROR. That hid every active event on the device and made every later page unreachable.
      • Cl. 13.12.1.2.1.7 takes the priorities from “the Priority property of the associated Notification Class object”, and there is none. The lowest priority is reported and the gap is logged.
      • The object is not skipped. Cl. 13.12.1.2.2’s More Events counts every object “that meet[s] the active event state criteria”, and a skipped object would still set More Events, sending the client after a page that never arrives.
      • GetEnrollmentSummary and AcknowledgeAlarm still handle a missing Notification Class their own way (they drop the object, or fail); aligning them is #3242.
  • rb bs-20260928-62: two event-state fixes (F17).
    • CHANGE_OF_VALUE ignores a NaN sample (#2720, cl. 13.3.3 (a)). Condition (a) is “the absolute value of pMonitoredValue changes by an amount equal to or greater than pIncrement”, and a NaN has no amount to change by.
      • Before this fix, a NaN first sample, such as a host reporting a failed sensor, became the baseline. Every later comparison was then false, and the algorithm stayed silent until Reset.
      • A NaN is now neither the baseline nor compared with it, and a pending time delay is dropped.
      • The other half of the issue, refusing a NaN or negative pIncrement, waits on the owner’s question about the frozen header.
    • Disabling event detection restores Acked_Transitions to (T,T,T) (#2715, cl. 12.2.24, 12.2.29).
      • Cl. 12.2.29 says Acked_Transitions shall be “equal to their respective initial conditions”, and cl. 12.2.24 gives each flag “the value TRUE if no event of that type has ever occurred for the object”.
      • SetEventDetectionEnable(false) set (F,F,F): three unacknowledged transitions that never happened, still owed an acknowledgment after re-enabling. It also now clears the notification parameters an AcknowledgeAlarm would re-report. That is defence in depth: with every timestamp reset to X’FF’, an acknowledgment is refused before those parameters are read.
  • rb bs-20260928-63: a Loop’s FLOATING_LIMIT follows its live parameters (F17).
    • The band moves with the Loop’s Setpoint, Error_Limit and Low_Diff_Limit (#2716).
      • Cl. 12.17.17: Setpoint “shall be the pSetpoint parameter for the object’s event algorithm”. Cl. 12.17.32 and 12.17.46: Error_Limit is pHighDiffLimit, and also pLowDiffLimit while Low_Diff_Limit is absent or Null.
      • Before this fix, all three were frozen at arm time. A Loop whose setpoint moved raised HIGH_LIMIT or LOW_LIMIT while it sat exactly on setpoint, and missed real excursions around the new one.
      • They are now read from the Loop before each evaluation: the host’s callback answer, or a value the stack stores for it. A value that is not enabled or not served keeps the last one. The stack’s 0.0 default for an unserved REAL is never used, so a host that does not serve Setpoint keeps the armed band.
      • Deadband needs no feed. The algorithm serves and accepts it itself.
      • A report of any of them re-evaluates the Loop.
      • The #2358 CONFIGURATION_ERROR check now sees a live negative Error_Limit or Low_Diff_Limit.
      • Still open: arming does not enable Error_Limit / Low_Diff_Limit on a Loop (#3246), and an Event Enrollment FLOATING_LIMIT’s setpoint-reference is not fed (#3247).
  • rb bs-20260928-64: an Event Enrollment’s FLOATING_LIMIT follows its setpoint-reference; a Loop-without-Event-Enrollment build compiles again (F17).
    • An Event Enrollment’s FLOATING_LIMIT band follows the value its setpoint-reference names (#3247).
      • Cl. 12.12.23 Table 12-15, FLOATING_LIMIT: Setpoint_Reference, “Referent’s value is pSetpoint”.
      • The referent is read before each evaluation.
        • First, a value the stack generates itself. For a commandable Present_Value, the usual setpoint, that is the value its Priority_Array resolves to.
        • Then the host’s callback, then a stored value. The stack’s 0.0 default is never used.
        • A report of the referent re-evaluates the Event Enrollment.
        • A referent that is remote, names an array element, or is not a REAL is not fed, and the band keeps its last setpoint.
      • Before this fix, the reference was stored only for the Event_Parameters read-back, and the band stayed at the armed setpoint.
    • A build with Loop and Alarms & Events but without Event Enrollment compiles again.
      • bs-20260928-63 (#2716) placed the Loop FLOATING_LIMIT helpers inside the STACK_OPTION_OBJECT_TYPE_EVENT_ENROLLMENT block of BACnetBusinessLogic.cpp. That configuration failed with IsLoopFloatingLimitParameter / FeedLoopFloatingLimitParameters “not declared”.
      • The TARGET_FULL CI build and the gating sweep’s rows all define both options, so neither saw it.
      • The shared REAL reader (ReadHostRealProperty) is now under LOOP || EVENT_ENROLLMENT, and the Loop feed is under LOOP.
  • rb bs-20260928-65: GetEnrollmentSummary survives a deleted Notification Class, as GetEventInformation does (F16).
    • GetEnrollmentSummary lists an object whose Notification Class no longer exists at Priority 255 (#3242).
      • This is the same fallback GetEventInformation uses since #2687. Before this fix, the object, intrinsic or Event Enrollment, was dropped from the summary without a log.
      • The Priority Filter judges 255. The Enrollment Filter still cannot match such an object, because it needs the Notification Class’s Recipient_List.
    • GetEnrollmentSummary’s Priority Filter checks the priority of the most recent transition (#3242, cl. 13.11.1.1.5).
      • The clause says “the Priority checked by the filter is the Priority associated with the most recent transition”.
      • That is now TO_NORMAL in NORMAL, TO_FAULT in FAULT, and TO_OFFNORMAL otherwise. Before this fix, it was TO_OFFNORMAL whatever the state.
      • The Priority reported in the summary is unchanged.
    • AcknowledgeAlarm accepts an object whose Notification Class no longer exists (#3242, cl. 13.5.2).
      • Cl. 13.5.2 allows a Result(-) only when the object does not exist or the Time Stamp does not match. The class only chooses the recipients of the acknowledgment notification.
      • The Acked_Transitions bit is now set and a Result(+) returned. Only the notification, which has nobody to go to, is skipped, as #2685 does for an Event_Enable FALSE transition.
      • This used to be DEVICE/INTERNAL_ERROR.
  • rb bs-20260928-66: a relinquish is recorded in the value-source properties (F29).
    • Relinquish records its source and time; Last_Command_Time reflects it (#2838, cl. 19.5.1, 19.5.1.4, 12.54.43).
      • Cl. 19.5.1: the value source mechanism identifies “which device or object has commanded, or relinquished, a value at each of the priority levels”.
      • Cl. 19.5.1.4: “when Present_Value changes due to a relinquish command, the Last_Command_Time indicates the time at which the relinquish command was received and not the earlier time at which the now current priority command was provided”.
      • A relinquish at priority p used to erase Value_Source_Array[p] and Command_Time_Array[p]. It now records the relinquisher and the relinquish time (BACnetStackValueSourceState::RecordRelinquish), on Lighting Output, Binary Lighting Output and Access Door.
      • Last_Command_Time is now the latest command or relinquish time at or above the current winner (the latest at any priority under Relinquish_Default), rather than the winner’s own command time.
      • A command whose source is not known (a local write, or a WriteProperty with no network parameters) is recorded as “none” with its time. It used to clear the slot, so a live winning value read Last_Command_Time as the zero default.
      • Still not attributed: an Access Door pulse’s own auto-relinquish (Tick() has no command source or wall-clock time); that slot is cleared as before.
      • Known imprecision: relinquishing a slot that was already empty, above the winner, advances Last_Command_Time although nothing changed.
  • rb bs-20260928-67: Load Control accepts the 135.1 write order (F29).
    • A shed request is not cancelled while its Requested_Shed_Level is still to be written (#2840, 135.1-2023 7.3.2.38.1, cl. 12.28).
      • The 135.1 test writes Shed_Duration, then Start_Time (“any valid start time including values in the past, present or future”), then Duty_Window, and only then Requested_Shed_Level.
      • Before this fix, the “Requested_Shed_Level at its choice default” cancel was checked on every tick. The first tick after the Start_Time write cancelled the request and reset Start_Time and Shed_Duration. The level write then landed on an INACTIVE object, so Present_Value never left SHED_INACTIVE.
      • Cl. 12.28’s CancelShed judges “the newly written shed parameters”. Writing Requested_Shed_Level with its default now cancels a pending or active request at once.
      • A pending request whose level is still the default waits for the level: the host is not consulted and nothing is shed. The end of the window still cancels it.
  • rb bs-20260928-68: Load Control’s Actual_Shed_Level while a shed is pending (F29).
    • Actual_Shed_Level reads the requested level, not the default, while a shed is pending (#3252).
      • 135.1-2023 7.3.2.38.1 step 21 requires Actual_Shed_Level to be “not equal to the default value”, and says “the above VERIFY statements apply all through the time that there is a pending or active shed event”.
      • Before this fix, every entry to SHED_REQUEST_PENDING reset it to the default. That included a shed with a future Start_Time, and every reconfiguration of an active shed.
      • It now reads the requested level, as Expected_Shed_Level does, until the host’s achieved value replaces it.
      • A reconfiguration of an active shed that keeps the choice (a new Duty_Window, for example) keeps the level the host last achieved until it answers again.
      • Cl. 12.28.17 requires the default only in SHED_INACTIVE.
      • Still open on #3252: consulting the host at the Requested_Shed_Level write, rather than at the next tick.
  • rb bs-20260928-69: a Channel’s references written over the wire reach the DAB (F29).
    • A client write of a Channel’s List_Of_Object_Property_References acquires each external device it names into the DAB, and releases each device the replaced list named (#2837). This matches what AddObjectPropertyReferenceToChannel does.
      • WriteGroup writes a remote member only once its device is resolved (SendExternalWriteProperty), and the DAB learns only devices that were acquired.
      • Before this fix, a list written over the wire, which is the normal way to configure a Channel, got Result(+). But its remote devices were never bound, so every WriteGroup to them was skipped.
      • Whole-list, element and resize writes are all covered.
      • Deleting a Channel now releases the entries its references acquired. This is the Channel leg of #2654, and it matters more now that a peer can fill the list.
      • Still open, in #3255: Acquire’s result is ignored, and a whole-list write has no size cap.
      • Schedule’s List_Of_Object_Property_References has no wire-write path, so it has no such gap.
  • rb bs-20260928-7: optional-field logging, Device_Address_Binding writes (F03, F01).
    • An absent optional constructed field no longer logs a false “Not an opening tag” ERROR (#2998).
      • This was the shared decode path (BACnetObjectBase::DecodeWithContext, BACnetPriorityFilter). Every absent optional field and every CHOICE alternative probed and not taken logged a false ERROR.
      • A missing required field is still logged, and so is an optional field whose content is cut short. An optional field whose tag has the right number but not the opening form is treated as absent, as before, and is now silent too.
    • Device_Address_Binding is refused as a WriteProperty, AddListElement or RemoveListElement target: WRITE_ACCESS_DENIED (#2956). The stack serves it from its own address binding table.
      • This holds even when the host marks the property writable.
      • Before, such a write was accepted into a stored value that is never read back.
  • rb bs-20260928-70: a build that turns on Alarms & Events late gets AcknowledgeAlarm and GetEventInformation (F01).
    • STACK_OPTION_ALARMS_AND_EVENTS reached through the Alert Enrollment or Notification Forwarder object type now brings STACK_OPTION_AE_ACKNOWLEDGE_ALARMS and STACK_OPTION_AE_INFORMATION with it (#3048).
      • Those late blocks in CASBACnetStackOptions.h define A&E after the main Alarm and Event cascade has run. An à-la-carte build, such as TARGET_EMBEDDED with an Analog Input and a Notification Forwarder, compiled intrinsic reporting but neither service. A peer’s AcknowledgeAlarm and GetEventInformation were rejected as unrecognized, although the spec requires them: cl. 13.2.3 “Every device that supports acknowledgeable transitions shall support execution of the AcknowledgeAlarm service”; cl. 13.2.4 “Notification-servers are required to support execution of the GetEventInformation service”.
      • The main cascade is now repeated after the late blocks.
      • An #error invariant at the end of the option-expansion section stops a future block from turning A&E on after that repeat. Builds that reach A&E only through these object types now also compile AcknowledgeAlarm and GetEventInformation execution.
  • rb bs-20260928-71: three option-gating build breaks the gating sweep found (F01).
    • Builds with the Network Port object, BACnet/SC or a BBMD but without ReinitializeDevice compile again (#3257).
      • The Network Port pending-value writes read CallbackReinitializeDevice, which exists only with STACK_OPTION_REINITIALIZE_DEVICE, since #3164. They now ask CanActivatePendingNetworkPortChanges, which answers FALSE without the option, so the write is refused as when the callback is not registered.
      • The activation code’s BACnetReinitializedStateOfDevice enum is now compiled for the Network Port object too.
      • The BACnet/SC hub connector’s I-Am-Router-To-Network learning is compiled only with STACK_OPTION_ROUTER, where that message class exists.
      • The gating sweep rows NETWORK_PORT, DATA_LINK_LAYER_SC, NM_BBMDC, NM_SCCM_A and PROFILE_B_BBMD failed, and now pass. TARGET_FULL CI defines everything, so it cannot see this class of break.
  • rb bs-20260928-72: an alarms-and-events build decodes every change-of-state value a peer can send (#3213).
    • A change-of-state notification about another device’s Program, Access Door, Load Control, Access Zone, Access Credential, Access Point, Loop, Lighting Output, Binary Lighting Output, Escalator, Lift or Lift Group object now decodes in a build that does not host that object type.
      • The 26 property-state types those notifications carry (for example BACnetDoorValue, BACnetShedState, BACnetLiftFault, BACnetBinaryLightingPV) were compiled only with their object type. The BACnetPropertyStates factory returned NULL for them in any other build, so the notification was rejected.
      • The types are now also compiled with STACK_OPTION_ALARMS_AND_EVENTS, and their factory arms are no longer gated.
      • The binary-lighting arm was gated on LIGHTING_OUTPUT while its type is gated on BINARY_LIGHTING_OUTPUT. That mismatch is gone.
  • rb bs-20260928-73: a Notification Forwarder delivers to recipients given as a device identifier (#2681).
    • A Subscribed_Recipients or Recipient_List entry in device-identifier form now receives forwarded notifications once that device answers Who-Is.
      • Cl. 12.51 (135-2024) says “The notification shall then be sent to the recipient indicated by the BACnetDestination.”
      • Delivery resolves a device recipient only through the Device Address Binding table, and the forwarder never registered its recipients there. The entry got a SimpleACK but never a notification, and the drop was silent.
      • Each tick, before the forwarder sends, the device now registers one DAB reference per device-form entry. This covers every way the lists change: the API, WriteProperty, Add/RemoveListElement, and Time_Remaining expiry.
      • The forwarder releases these references when an entry goes away, and when the forwarder is deleted.
      • Only references the DAB accepted are released later. When the table is full, a refused registration is not recorded, so the forwarder never drops a binding another consumer holds.
    • A target naming the owning device itself is still not delivered. That needs a design decision (#3261).
  • rb bs-20260928-74: a full Device Address Binding table no longer lets a Channel or Notification Class drop another consumer’s binding (#3255).
    • The Channel and the Notification Class now release only the DAB references they actually took.
      • When the table is full (#1142), an Acquire is refused. The Channel’s List_Of_Object_Property_References write and the Notification Class’s Recipient_List reconcile still released that device later. If another consumer had acquired it in the meantime, that consumer’s binding was dropped.
      • Each object now keeps the references it holds, through the same reconcile the Notification Forwarder uses (#2681). A refused Acquire is retried on the next change.
      • Deleting a Notification Class now releases its recipients’ references. Before, they stayed held until the device was reset.
      • An Execution_Delay[0] shrink, which truncates the references too, now releases the devices it cuts off.
    • A whole List_Of_Object_Property_References write longer than 256 elements is refused. The limit was already enforced for an index-0 resize. Cl. 15.9 (135-2024): “The supplied value would resize the array to a size that is not supported.” The answer is PROPERTY / INVALID_ARRAY_SIZE. Before, one write could name enough devices to fill the DAB.
  • rb bs-20260928-75: a Loop’s intrinsic FLOATING_LIMIT exposes its diff limits as the Loop’s Error_Limit and Low_Diff_Limit (#3246).
    • Arming a Loop’s FLOATING_LIMIT now enables Error_Limit, and Low_Diff_Limit when the band is asymmetric.
      • It used to enable Low_Limit and High_Limit. A Loop does not have those properties, so the enables failed silently.
      • Error_Limit was absent unless the integrator enabled it by hand.
    • The algorithm now serves and accepts both, as it does Deadband.
      • Cl. 12.17.32 (135-2024): Error_Limit “is both the pLowDiffLimit and pHighDiffLimit parameter for the object’s event algorithm if the property Low_Diff_Limit is not present or has a value of Null.” So with a symmetric band, a written Error_Limit moves both limits. With Low_Diff_Limit present, it moves only the high limit (cl. 12.17.46).
      • The band is held as the setpoint plus the two diff limits, so a written limit reads back exactly.
      • The #2716 host feed still overrides both when the host serves them.
    • A WriteProperty of Low_Diff_Limit is still refused: its profile datatype is BACnetOptionalREAL, which the write path cannot decode (#3264). The algorithm accepts the value, but nothing on the wire reaches it yet.
  • rb bs-20260928-76: a Loop’s Low_Diff_Limit can be written over the wire, as a REAL or as Null (#3264).
    • A WriteProperty of Low_Diff_Limit now reaches the Loop’s FLOATING_LIMIT. Before, every such write was refused with INVALID_DATATYPE.
      • The property’s profile datatype was the legacy OptionalREAL slot, which has no decoder. It is now OptionalReal, the BACnetOptionalReal type that decodes both REAL and Null.
      • Cl. 12.17.46 (135-2024): Low_Diff_Limit, “of type BACnetOptionalReal, is the pLowDiffLimit parameter for the object’s FLOATING_LIMIT event algorithm if it has a value other than Null. If this property has the value Null, then the value of the Error_Limit property is used as the pLowDiffLimit parameter”.
      • A REAL sets pLowDiffLimit. A Null makes Error_Limit both limits, and the property then reads Null. The host is told through CallbackSetPropertyReal or CallbackSetPropertyNull.
    • A Null written to an OptionalReal property is now treated as one of its values. Before, it took the path for relinquishing a non-commandable property: reported as a success, with no effect.
    • The #2716 host feed keeps a written Low_Diff_Limit value. After a Null, the low limit follows Error_Limit.
    • Whether Low_Diff_Limit is writable stays the integrator’s choice (SetPropertyWritable), as it is for Deadband.
    • A client decoding a peer Loop’s Low_Diff_Limit (ReadProperty, COV, DecodeAs JSON/XML) now gets a BACnetOptionalReal rather than a bare REAL or Null.
    • Not fixed: a Low_Diff_Limit that the host serves itself (a symmetric band with the property enabled by hand) still cannot be read or written through the generic callbacks, and neither can Interface_Value, which still uses the legacy slot (#3266).
  • rb bs-20260928-77: a BACnetOptionalREAL property that the host holds can now be read and written through the host’s callbacks (#3266).
    • An Analog Input’s or Analog Output’s Interface_Value now reads from the host.
      • Cl. 12.2.35 (135-2024): “This read-only property, of type BACnetOptionalReal, indicates the value, in engineering units, of the physical input.”
      • Its profile datatype was the legacy OptionalREAL slot, which has no decoder and no callback path, so every read failed with “Unsupported datatype for callbacks”. It is now OptionalReal.
    • The host callbacks now handle an OptionalReal property.
      • A read comes from CallbackGetPropertyReal.
      • A written value goes to CallbackSetPropertyReal, and a written Null goes to CallbackSetPropertyNull.
      • So a Loop’s Low_Diff_Limit that the host serves (a symmetric band, with the property enabled by hand) now reads and writes (#3264 follow-up).
    • The REAL getter cannot report Null. A host that does not know the physical input’s value cannot yet report the Null that cl. 12.2.35 calls for. That gap, and Interface_Value on Binary and Multi-state objects, are tracked in #3268.
  • rb bs-20260928-78: Interface_Value reads on every object type that has it (#3268).
    • A Binary Input or Output Interface_Value (BACnetOptionalBinaryPV) now reads from the host’s CallbackGetPropertyEnumerated.
    • A Multi-state Input or Output Interface_Value (BACnetOptionalUnsigned) now reads from the host’s CallbackGetPropertyUnsignedInteger. So does a Current_Command_Priority that the stack does not generate itself. The stack always generates it first, so the host is asked only when the Bool callback is missing or declines Priority_Array; before, that read failed with no error.
      • Neither datatype had a callback case, so these reads failed with “Unsupported datatype for callbacks”. #3266 fixed the Analog ones.
    • An Interface_Value the host does not serve now reads Null. Before, the read failed with no error set.
      • Cl. 12.2.35 (135-2024). 12.3.36 has the same wording; 12.6.34, 12.7.38, 12.18.29 and 12.19.30 state the same Null rule for the “logical state” of the physical input or output: “If the BACnet device is not capable of knowing the value of the physical input, then the value of this property shall be Null.”
  • rb bs-20260928-79: a device registered for address binding before any peer has answered is looked for as soon as one does (#2656).
    • The Device Address Binding heartbeat now marks an entry as tried only when a Who-Is was actually sent for it.
      • A device not yet heard from (a Notification Class recipient, or a Channel or Schedule reference) is looked for with a broadcast Who-Is. That broadcast needs a template learned from any peer’s I-Am.
      • Before any I-Am, the heartbeat sent nothing but still marked those entries as tried. So their first Who-Is waited a full heartbeat interval (an hour by default) after the first I-Am arrived. Notifications and external writes to those devices were skipped for that hour.
    • Entries still waiting for a template no longer make the heartbeat re-plan on every tick.
    • A resolved entry with no connection string, which only an I-Am without network parameters could create, is never sent a heartbeat Who-Is. It therefore no longer counts misses toward Stale. Stale is diagnostic only: lookups treat Resolved and Stale the same.
  • rb bs-20260928-8: host-length checks, object-name lookup, CreateObject object type, XML noncharacters (F01, F02, F04).
    • Security: the AuthenticationFactor read trusted the length the host reported (#3039).
      • Credential Data Input Present_Value is read through the OctetString callback. A length past the callback’s capacity was copied out of the stack buffer into the ReadProperty-ACK or COV notification.
      • The read is now refused, as #620 already did for the plain OctetString read.
    • The CharacterString, OctetString and BitString Priority_Array reads trusted the length the host reported (#3050).
      • An over-reported length is now refused. This covers both the single-slot read and the whole-array read.
      • A CharacterString slot now keeps the host’s character set. It was always sent as UTF-8.
      • A slot whose character set the stack cannot hold (a reserved set, or a UCS-2 value of odd length) now fails the read, as the scalar read has since #2487. Before, it was sent as UTF-8.
    • An object-name lookup no longer skips objects after one whose Object_Name read failed (#3049).
      • One error container was shared across the whole object list, so every later callback-served object was skipped.
      • This affected the duplicate-name check on an Object_Name write and Who-Has by name.
    • An object specifier with an object type past 1023 no longer reads as a different, valid object type (#3086).
      • It now reads as invalid, which the CreateObject processor’s existing object-type check answers with UNSUPPORTED_OBJECT_TYPE. The test covers the specifier; the processor step is by reading.
      • Before, the value was cut to its low 16 bits, so object type 65536 created an Analog Input.
      • Cl. 20.2.14 says: “A 10-bit object type”.
      • Copying or comparing an object specifier now uses the decoded value.
    • Security: the XML renderer replaces U+FFFE and U+FFFF with U+FFFD (#3087).
      • XML 1.0’s Char production excludes both characters, so one peer string made the whole document malformed.
  • rb bs-20260928-80: AtomicReadFile stream access follows the 135.1 boundary tests (#2658, #2659).
    • A read of one or more octets starting at File_Size or beyond is refused by the stack with SERVICES / INVALID_FILE_START_POSITION (#2658).
      • 135.1-2023 9.12.2.2.1 sends “‘File Start Position’ = (any number n: n >= the number of octets in the test file)” with a count above 0 and expects that error.
      • Before, a start exactly at File_Size was passed to the host’s read callback. A callback that answered with End Of File and zero octets, as the processor’s own comment suggested, failed the test. That comment cited 9.12.1.2.5, which reads zero octets from a position inside the file; it has been corrected.
      • A zero-octet read at File_Size and a read of an empty file from position 0 behave as before.
    • A ‘Requested Octet Count’ above the 2048-octet buffer is clamped instead of aborted (#2659). A file with fewer octets remaining now returns them with End Of File TRUE, as cl. 14.1.5 and 135.1-2023 9.12.1.2.6 require. Before, it answered Abort(out-of-resources) without reading.
      • With more than 2048 octets remaining, the answer holds 2048 octets and End Of File FALSE. This is the same declared limitation as the record-count clamp.
  • rb bs-20260928-81: two COV-multiple notification fixes (#2661, #2662).
    • A COV-multiple notification’s Timestamp now reflects only the changes it carries (#2661).
      • Cl. 13.17.1.1.4 (135-2024): the Timestamp “shall convey the date and time of the last change conveyed in the notification. This parameter shall be present if, and only if, any of the COV notifications conveyed in the ‘List of COV Notifications’ parameter contain the ‘Time of Change’ parameter.”
      • The queue remembered the last Time of Change it had ever seen, and that value outlived the drain. Once a subscription had queued one timestamped change, every later notification carried that old Timestamp, including one with no timestamped change in it. The Timestamp is now the latest Time of Change among the items being sent.
    • A COV-multiple subscription to a property with a non-primitive datatype now reports its changes (#2662). Examples are a Channel’s Present_Value, a DateTime, a TimeStamp, or a proprietary property.
      • Change detection compared only primitive datatypes and reported “no change” for anything else. Such a subscription got its initial notification and then never another. It now compares values the same way SubscribeCOVProperty does.
  • rb bs-20260928-82: a clock step no longer holds a COV-multiple subscriber’s queued timestamped changes (#2660).
    • A clock step now moves the notification queue’s Max_Notification_Delay deadline along with the subscription’s expiry. A step can come from TimeSynchronization or UTCTimeSynchronization.
    • Cl. 13.16.1.1.4 (135-2024): Max Notification Delay is “the maximum number of seconds that may elapse before a notification is issued if changes of properties occurred for which the ‘Timestamped’ parameter is TRUE and which were queued up for notification.”
    • The deadline is an absolute clock time, and #1151 compensated only the expiry. After a backward step, queued timestamped changes were held for the size of the step plus the delay, and the queue kept growing meanwhile.
  • rb bs-20260928-83: GetEventInformation from a client with a very small Max APDU no longer answers “nothing here, but more to follow” forever (#2686).
    • When no event summary fits the requester’s Max APDU but events are active, the answer is now Abort SEGMENTATION_NOT_SUPPORTED.
      • At 50 octets, the smallest a device may accept, no summary fits: each one is 61 octets.
      • The device used to answer an empty list with More Events TRUE. That contradicts cl. 13.12.1.2.1 (135-2024): “If the list is of length zero, then this shall be interpreted to mean that there are no event-initiating objects that have active event states in this device.”
      • A client paging on More Events had no last object to continue from, so it repeated the request forever.
      • The abort is the reason cl. 18.10 gives when “a BACnet-ComplexACK-PDU is large enough to require segmentation but it cannot be transmitted because either the transmitting device or the receiving device does not support segmentation”. ReadRange gives the same reason when its answer cannot be cut to fit.
      • GetEventInformation pages by the requester’s Max APDU alone and never segments. It therefore answers this way even to a client that accepts segmented responses; #3275 tracks that.
    • With nothing active, a 50-octet request still gets the empty list with More Events FALSE.
  • #2676 (LifeSafetyOperation error class) was closed as already fixed by #2994 and #3002.
  • rb bs-20260928-88: the CHANGE_OF_DISCRETE_VALUE event algorithm now monitors every discrete datatype that 135-2024 cl. 13.3.18 names (#2725). Before, it accepted only an OCTET STRING, so an Event Enrollment on any other discrete property returned a run error and never reported.
    • Cl. 13.3.18: “The CHANGE_OF_DISCRETE_VALUE event algorithm, for monitored discrete values of datatype Boolean, Unsigned, Integer, ENUMERATED, CharacterString, OctetString, Date, Time, BACnetObjectIdentifier and BACnetDateTime, detects changes to the monitored value.”
    • Changes are detected by comparing the decoded values. A value the stack serves as encoded bytes only (Device Database_Revision, Object_Identifier, …) is decoded first.
    • The notification’s new-value carries the monitored value in the CHOICE arm of its own datatype. A BACnetDateTime uses the datetime arm. The Event_Parameters new-value is built the same way.
    • CanObjectPropertyBeMonitoredByEvent now accepts any property whose profile datatype is one of the ten. An Event Enrollment on a Multi-state Value or CharacterString Value Present_Value is accepted; a REAL is still refused.
  • rb bs-20260928-89: BACnetStack_SendWhoHasWithObjectIdentifierAndLimits / BACnetStack_SendWhoHasWithObjectNameAndLimits with a reversed range, or a limit above 4194303, now return false and send nothing (#2651). Before, they returned true: the Who-Has constructors ignored the rejected range and the request went out with no limits, an unbounded Who-Has to every device. The header’s doc comment still describes the old behaviour; the header is frozen, so its update is #3285.
  • rb bs-20260928-89: a ConfirmedTextMessage received with no HookTextMessage registered is answered with Error (SERVICES, SERVICE_REQUEST_DENIED) instead of a SimpleACK claiming delivery (#2647). Cl. 16.5.2: “If the service request cannot be executed, a ‘Result(-)’ service primitive shall be issued indicating the encountered error.” An UnconfirmedTextMessage with no hook is still dropped silently.
  • rb bs-20260928-9: FD_BBMD_Address host lengths, BACnet/SC NULL URI, text-message class, PrivateTransfer parameters, Landing_Calls truncation (F02, F01).
    • The Network Port FD_BBMD_Address read trusted the lengths the host reported (#3128).
      • An IP address longer than the 4-octet buffer, or a host name longer than the character-string buffer, is now refused.
      • So is any host value the stack cannot hold as a BACnetHostNPort, such as an IP address that is not 4 octets, or a name that fills the whole buffer. Before, such a read returned true with an invalid address.
      • Before, the bytes past the buffer were encoded into the answer.
    • BACnetStack_SetBACnetSCWebSocketStatus refuses a NULL or empty URI (#3038).
      • A NULL URI with a length reached a memcmp against NULL in the hub-function peer lookup, which crashed.
    • The text-message senders refuse a message class they cannot set (#3033).
      • This is a CharacterString class whose character set is past ISO 8859-1.
      • Before, the message went out with no class and the call returned true.
      • The unconfirmed sender now checks the request’s own validity instead of forcing it valid. It also no longer leaks the PDU when a class is requested without parameters.
    • Test tool: the PrivateTransfer senders refuse service parameters that do not decode (#3044).
      • This covers a NULL buffer with a length, a length past 65535, and bytes left over after the decoded values.
      • A single closing tag after the values is still accepted, because Decode stops before it (#365) and existing callers pass one.
      • Before, the request went out with no Service Parameters and the call returned true.
    • An ElevatorGroup Landing_Calls read fails when the host returns an invalid command choice partway through the list (#3043).
      • Before, the elements already read were answered as a complete, valid list.
      • An invalid choice on the first element used to read as an empty, valid list. It now fails too.
  • rb bs-20260928-91: an unknown network layer message type received as a broadcast is no longer answered with Reject-Message-To-Network, reason 3 (#2890). A Forwarded-NPDU counts as a broadcast since #2751.
    • Cl. 6.6.3.5 (135-2024): “Reject-Message-To-Network is generated by a router when it receives a message that it is unable to relay to the DNET specified in the NPCI or if it receives an unknown network layer message directed specifically to that router.” A broadcast is not directed specifically to the router.
    • The same message received unicast is still rejected with reason 3, and it is still rate limited per source (#471).
    • A broadcast no longer spends the sender’s reject budget.
    • The #2512 spoofed-originator path, which varied the Forwarded-NPDU’s B/IP Address of Originating Device to get fresh reject budgets, now draws no reject at all.
  • rb bs-20260929-92: BACnet/SC Out_Of_Service on one Network Port no longer suspends the other BACnet/SC ports (#2602). Each SC port has its own data link instance (#304). An instance that serves a Network Port object now reads only that port’s Out_Of_Service; before, any SC port being out of service suspended every instance, including its hub connection and listeners. An instance with no tracked port keeps the device-wide check.
  • rb bs-20260929-92: a BACnet/SC hub-function or direct-connect accept URI whose listener fails to start is now retried on every Loop() until it starts, also while another accept URI of the same role is listening, and also for a URI added with AddBACnetSCAcceptUri while listening (#2605). Before, once any URI of a role was listening, a failed URI (for example a port busy at boot) stayed unbound for the life of the process.
  • rb bs-20260929-92: SC_Direct_Connect_Connection_Status reports an accepted direct connection with an empty URI (#2604, in part: the disconnect timestamp is still open). Cl. 12.56.98 (135-2024): “For accepted direct connections, this field shall be an empty string.” Before, it reported the host’s connection string for the socket, including its per-socket client token.
  • rb bs-20260929-93: an initiated BACnet/SC direct connection now detects a dead peer (#2594). Cl. AB.6.3 (135-2024): “if a Heartbeat-ACK message is not received from the accepting peer, the initiating peer shall initiate the ‘Local disconnection’ procedure.”
    • When nothing is received for a whole heartbeat timeout after a Heartbeat-Request, the stack sends a Disconnect-Request and waits out the disconnect wait timer.
    • Any BVLC message received over the connection restarts the heartbeat timer. Cl. AB.6.3: a Heartbeat-Request is sent “if the initiating peer has not received a BVLC message over the connection within the heartbeat timeout.”
    • Before, a Heartbeat-Request was sent every heartbeat timeout whatever was received, and a silent peer was never disconnected.
  • rb bs-20260929-93: the BACnet/SC start-up checks are re-run on every Loop() instead of latching (#2607). The checks are the UUID and the GetSystemTime, SendMessageForPort and SCStartListening callbacks.
    • Before, an application that configured part of BACnet/SC, ticked once, and then set the UUID or registered a callback got an SC port that never started. Only Reset() cleared the latch.
    • Each start-up error is logged once, not on every Loop(), and a different error that replaces it is logged too. HasStartUpErrorOccurred() is true only while a precondition is missing.
  • rb bs-20260929-94: every BACnet/SC timestamp is now local time (#2330, owner ruling 2026-09-23). Cl. 12.56.88 (135-2024), Connect Timestamp: “This field, of type BACnetDateTime, indicates the local date and time when the connection was established, or the connection attempt failed.”
    • Affected: SC_Primary_Hub_Connection_Status, SC_Failover_Hub_Connection_Status, SC_Hub_Function_Connection_Status, SC_Direct_Connect_Connection_Status and SC_Failed_Connection_Requests.
    • SC_Failed_Connection_Requests, and a hub-connection failure the hub connector latches, are recorded in UTC and converted as they are copied into the Network Port. They use the Device’s UTC_Offset in force at that copy, not at the failure.
    • These now apply the Device’s UTC_Offset and Daylight_Savings_Status through the same conversion SC_Current_Health uses (#2472). Before, they were UTC, so the two could disagree on the same port.
  • rb bs-20260929-94: a BACnet/SC accept URI whose listener fails to start is retried once per WebSocket reconnect timeout (default 10 s), not on every Loop() (#3351). Before, a listener that could never start was retried at the loop rate.
  • rb bs-20260929-95: the NM-SCCM AddIssuer and ReplaceOperational procedures no longer empty the target’s certificate file before writing the new certificate (#2560).
    • They now write the new certificate first (AtomicWriteFile), then set File_Size to its length, then send ReinitializeDevice. The progress hook reports steps 7, then 6, then 8.
    • Cl. 19.8.3.2 (135-2024): “Device A writes the File object that represents the operational certificate for this node.” There is no truncation before the write.
    • Before, the target file was emptied first: for ReplaceOperational, the live operational certificate. A write that was then refused left the target with no operational certificate (tested); a write that timed out or a target that restarted mid-write did the same.
    • Setting File_Size afterwards still removes the tail of a longer previous certificate. Cl. 12.13.6: “Writing to the File_Size property with a value less than the current size of the file shall truncate the file at the specified position.”
    • RemoveIssuer is unchanged and still writes File_Size := 0 (cl. 19.8.3.3).
    • The frozen header’s procedure docs still describe the old order; that doc change is #3355.
  • rb bs-20260929-96: BACnet/SC fixes from the #2613 roll-up:
    • The direct-connect disconnect wait uses the configured disconnect timeout, not the compile-time 10 s default (CR2-F09-075). The hub state machine already did.
    • Address-Resolution-ACK encoding reserves a separator only between URIs, so an ACK that exactly fits its buffer is no longer refused (CR2-F09-024). Cl. AB.2.7.1: the URIs are “separated by a single space character”.
    • InsertWebsocketURI refuses an empty URI or one containing a space, since neither would decode back (CR2-F09-024).
    • BACnetSCDirectConnection::Decode refuses a peer-vmac that is not 6 octets, a peer-uuid that is not 16, and an unknown connection-state (CR2-F09-072). Cl. 21 defines peer-vmac as OctetString (SIZE(6)) and peer-uuid as OctetString (SIZE(16)).
  • rb bs-20260929-97: a BACnet/SC direct connection now completes when the host reports the socket connected from inside CallbackInitiateWebsocket (#3359).
    • The status report runs the state machine re-entrantly, which sends the Connect-Request and waits in AWAITING_ACCEPT.
    • When the callback returned, the state machine used to be set back to AWAITING_WEBSOCKET, so the peer’s Connect-Accept was discarded and the connection never completed.
    • The initiator now enters AWAITING_WEBSOCKET before calling the host, and rolls back if the host refuses. An Error reported from inside the callback now fails the attempt the normal way: it is counted and the reconnect timeout is armed. Before, the initiator started a second websocket from inside the callback and was then left in AWAITING_WEBSOCKET.
  • rb bs-20260929-97: BACnet/SC fixes from the #2613 roll-up:
    • BACnetSCHubFunctionConnection enforces the peer VMAC and UUID sizes in its setters and in Decode (CR2-F09-057). The sizes are class constants, because the class is also built for the Network Port object with BACnet/SC compiled out. Cl. 21: “peer-vmac [4] OctetString (SIZE(6)), peer-uuid [5] OctetString (SIZE(16))”.
    • BACnetSCHubConnection::Decode refuses an unknown connection-state (CR2-F09-043).
    • Both decoders reset before refusing a NULL buffer, so a refused buffer no longer leaves an earlier value looking valid (CR2-F09-043, CR2-F09-057).
    • BACnetSCDirectConnectManager.h includes the options header its guard tests (CR2-F09-074). A duplicate <cstddef> include is removed (CR2-F09-059).
  • rb bs-20260929-98: BACnetSCFailedConnectionRequest (SC_Failed_Connection_Requests entries) decodes as cl. 21 defines it (#2613, CR2-F09-055): “peer-vmac [2] OctetString (SIZE(6)) OPTIONAL, peer-uuid [3] OctetString (SIZE(16)) OPTIONAL,” and “error-details [5] CharacterString OPTIONAL”.
    • An entry without its OPTIONAL fields now decodes with no error set. Before, it came back as a success with an error set. The stack’s own log records such entries while a peer’s VMAC and UUID are still zero.
    • A present but truncated error-details, and a peer VMAC or UUID of the wrong size, now fail the value instead of being dropped or accepted.
    • A refused buffer no longer leaves the object valid.
  • rb bs-20260929-98: BACnetSCFailedConnectionRequest::GetMinimumMemoryCount now counts what Encode writes (#2613, CR2-F09-056). It includes the opening and closing tags of timestamp, peer-address and error, which it undercounted by 6 octets, and leaves out absent OPTIONAL fields.
  • rb bs-20260929-98: the BACnet/SC manual (v6) agrees with CASBACnetStackDLL.h (#3094):
    • Callbacks are registered with RegisterCallbackSendMessageForPort / RegisterCallbackReceiveMessageForPort, and their samples carry networkPortInstance rather than a networkType byte.
    • BACnetStack_SendIAm takes the Network Port instance.
    • The SetBACnetSCHubFunctionConfig / SetBACnetSCDirectConnectConfig signatures have 4 arguments, and accept URIs are set with BACnetStack_AddBACnetSCAcceptUri.
    • GENERATE_CSR_FILE goes through the Network Port command callback. There is no certificate-validation callback.
    • The state-change sample copies a URI that can be NULL safely.
  • rb bs-20260929-99: the BACnet/SC hub connector’s initiating state machine fails a connection attempt as soon as the WebSocket reports an Error while it waits for the Connect-Accept (#2613, CR2-F09-069). Cl. AB.6.2.2: “In any state, before the events specific to the state are considered: WebSocket Failure On failure to establish, or unintended disconnection of the WebSocket connection, enter the IDLE state.”
    • A WebSocket Error in AWAITING_ACCEPT used to be ignored until the connect wait timer expired, and was then recorded as a timeout. It is now handled as an Error in AWAITING_WEBSOCKET is: the transport error is recorded, the state machine returns to Idle, and the reconnect timeout is armed.
    • The hub connector still ignores CallbackInitiateWebsocket’s return, as the public header documents. Changing that is an interface decision (#3364).
  • rb bs-20260929-99: the BACnet/SC Connect-Request, Connect-Accept and Advertisement messages are updated all or nothing (#2613, CR2-F09-025).
    • A compound setter that refuses one argument now leaves the message unchanged. Before, it could keep the new VMAC and UUID with the old lengths.
    • A refused Decode leaves no half-decoded fields behind.
    • A changed Hello capability now replaces the one already present instead of being ignored.
  • rb bs-20260930-100: a BACnet/SC hub connection attempt that the host reports as Disconnected is now recorded as a failed attempt (#2755). This covers a close in AWAITING_WEBSOCKET or AWAITING_ACCEPT: TCP refused, a TLS failure reported as a close, or the hub closing before its Connect-Accept.
    • SC_Primary/Failover_Hub_Connection_Status now reads FAILED_TO_CONNECT with WEBSOCKET_CLOSED_BY_PEER, and SC_Failed_Connection_Requests lists the attempt. Only an Error report was recorded before.
    • The close of an established connection is still a lost connection and is not recorded.
    • Not fixed here: a failed attempt still neither counts toward nor waits out the reconnect timeout, so with no failover hub the connector re-dials on the next tick (#2755’s second point). Arming the timeout conflicts with BTL 12.5.1.1.4/.9/.10 and is back with the owner on #2578.
  • rb bs-20260930-100: a hub websocket’s reconnect-attempt count resets when the connection reaches CONNECTED, not when the socket opens (#2578, part - arming the reconnect timeout on every failed attempt is still open there). Before, an attempt that failed after the socket opened restarted the increasing reconnect timeouts at the first step. Cl. AB.6.1: “Increasing reconnect timeouts should be applied between unsuccessful attempts to connect.”
  • rb bs-20260930-101: a BACnet/SC hub device’s own unicast to one of its hub function’s peers now omits the Destination Virtual Address (#2862). This covers its I-Am, ComplexACK and routed traffic. 135-2024 AB.3.1.2: “When a unicast BVLC message is sent to the connection peer, the ‘Destination Virtual Address’ parameter shall be absent.”
    • The frame goes out on that peer’s own connection, so the destination is already known. AB.5.3.2 removes the DVA the same way when the hub function forwards a unicast.
    • Before, the DVA was always carried, which a strict third-party node could treat as malformed.
    • Broadcasts keep the broadcast DVA.
    • Tests that identified the receiving SC peer by its DVA now identify it by the connection the frame was sent on.
  • rb bs-20260930-102: the BACnet/SC Advertisement that the hub function or a direct connection sends in answer to an Advertisement-Solicitation now gets its own message ID instead of the solicitation’s (#2706). 135-2024 AB.3.2: “Note that even though the Advertisement message is sent in response to the solicitation, it is not considered a “response message” and thus does not copy the message ID of the Address-Solicitation message.” The hub connector has done this since #2576. The issue’s other two points were already fixed at the tip: the configured lengths by #2588 and #2598, and the accept-direct-connections predicate, which is equivalent in all three roles because SetBACnetSCDirectConnectConfig refuses to enable accept with no accept URI.
  • rb bs-20260930-102: a peer’s Advertisement to the BACnet/SC hub function now updates the Maximum BVLC / NPDU Length stored for that peer (#3358). Relays are held to each peer’s maximums (#3347), and until now those maximums came from the peer’s Connect-Request only. AB.3.2: “On receipt of an Advertisement message, the node shall update its status information of the sending node as provided by the Advertisement message.”
  • rb bs-20260930-103: BACnet/SC fixes from the #2613 roll-up:
    • CR2-F09-037: the hub connector records Advertisement status and lengths only from the hub’s own Advertisement. One the hub relayed from another node carries that node’s Originating Virtual Address and no longer overwrites the hub’s Maximum BVLC/NPDU Length, which the hub connection’s egress checks use (#3347). AB.3.1.2: “If absent, the message originated at the connection peer node”.
    • CR2-F09-042: the failed-connection peer key and peer-address builders share one URI authority parser. The key now skips userinfo, so two hubs reached as svc:pw@... no longer overwrite each other’s SC_Failed_Connection_Requests entry. A port above 65535 is refused instead of being truncated to 16 bits.
    • CR2-F09-054: a repeat failure from a peer already in the failed-connection log moves that entry to the newest position, so the log evicts the actual oldest entry. Cl. 12.56.99: “the oldest entry shall be overwritten with the information from the new failed connection request”. The host text stands in for the clause’s “IP Address of the Peer Address”: the stack is given URIs, not resolved addresses, so keying on the resolved IP is not done (noted on #2613). A URI whose peer address does not parse, such as one with a malformed port, is recorded with the ‘none’ host and port 0.
    • CR2-F09-038 was re-verified as already fixed at the tip: every caller frees the message when SendMessageAndTakeOwnership fails.
  • rb bs-20260930-104: BACnet/SC hub connector fixes from the #2613 roll-up:
    • CR2-F09-040: a Connect-Accept to an earlier attempt’s Connect-Request is no longer taken as the current attempt’s. The unanswered Connect-Request is untracked before the next one is sent. A Connect-Accept is taken only while the websocket is in AWAITING_ACCEPT and waiting for one (AB.6.2.2, in AWAITING_ACCEPT: “On receipt of a Connect-Accept message enter the CONNECTED state.”). A Disconnect-Request is untracked when its disconnect wait expires.
    • Unanswered Connect-Requests and Disconnect-Requests no longer pile up until the 16-bit message ID wraps. Ageing out unanswered Address-Resolution entries, the finding’s third case, is left open on #2613.
    • CR2-F09-039: DisconnectWebsocket resets the websocket even when no CallbackDisconnectWebsocket is registered. Before, the connect-wait timeout re-fired and re-recorded a failure on every Loop, and NODE_DUPLICATE_VMAC drew a new VMAC on every Loop.
    • Adding both websocket callbacks to the start-up checks, the finding’s other suggestion, is not done here. It would refuse to start a port whose host registers them late, which is a behaviour change for a later decision.
    • CR2-F09-041 is left open: the public header documents that the hub connector’s state-change callbacks report device and port instance 0, so changing that is an interface decision.
  • rb bs-20261004-1: BACnet/SC fixes from the #2613 roll-up:
    • CR2-F09-086: a frame that encodes to zero octets is no longer handed to the send callback. A host that reported 0 bytes written made the send look successful, which defeated the #1569 exact-length check at 0.
    • CR2-F09-073: the websocket’s peer Maximum BVLC / NPDU Length setters (SetConnectionProperties, SetAdvertisedPeerStatus) now refuse values outside the received-value ranges the Connect-Accept and Advertisement decodes apply. A refused value leaves the stored one unchanged. The GetReconnectAttempts doc now says the count resets on CONNECTED. The finding’s other site, the setter returns in GetDirectConnectConnectionStatuses, no longer exists; that function no longer copies lengths through setters.
  • rb bs-20261004-1: test-only: TestBACnetPhysicalRouter passes MAX_IMPLEMENTED_BACNET_PROPERTY_IDENTIFIER to ASSERT_EQ by value. The by-reference ODR-use did not link in the -O0 dev build.
  • rb bs-20261004-10: MS/TP engine fixes from the #2612 roll-up:
    • CR2-F08-002: a queued MS/TP frame carries the full 501-octet data field. 135-2024 cl. 9.3.6: “The length of the data portion of a BACnet Data Expecting Reply frame may range from 0 to 501 octets.” Cl. 9.3.7 says the same for Not Expecting Reply. The queue buffer was sized so that the header and Data CRC fit inside 501, which capped a payload at 491. A maximum-APDU reply to a request routed from another network (a 480-octet APDU plus an NPCI of up to 12 octets) could not be queued. MSTPFrame::buffer is now header + 501 + Data CRC.
    • CR2-F08-003: sending a data frame no longer changes PollStation. Cl. 9.5.2: “PS “Poll Station,” the MAC address of the node to which This Station last sent a Poll For Manager.” A data send to a station between TS and NS set PollStation to it. With a subordinate addressed on every token hold, maintenance polling never reached the addresses around it, so a new manager there could never join the ring.
    • CR2-F08-005: DONE_WITH_TOKEN starts maintenance polling at TokenCount Npoll, as 135-2024 cl. 9.5.6.5 says (“TokenCount is less than Npoll” / “greater than or equal to Npoll”). It used Npoll - 1, one token early. A side effect: an integrator who sets Npoll to 0 now gets a Poll For Master on every token. Before, Npoll - 1 wrapped, so maintenance polling never happened. 135-2024 fixes Npoll at 50.
  • rb bs-20261004-11: MS/TP engine fixes from the #2612 roll-up:
    • CR2-F08-007: MSTP_SetMaxMaster never sets Max_Master below ThisStation. The ring arithmetic can reach this node only when TS <= Max_Master. With TS 100 and Max_Master 50, a node alone on the bus polled 0..50 forever and never used the token, so nothing it queued was sent. A lower value is raised to ThisStation; MSTP.h documents this.
    • CR2-F08-008: the recovery after about 3 s of bus silence restarts the state machines (cl. 9.5.6.1 INITIALIZE) and keeps the integrator’s settings and the queued frames. It used to re-run MSTP_Init, which silently reverted Max_Master to 127, Max_Info_Frames to 1 and the timers to their defaults, and discarded up to three queued frames.
    • CR2-F08-009: MSTP_Init always recomputes the baud-rate timers. A baud rate set before MSTP_Init computed Tturnaround against Tno_token 0 and clamped it to 1 ms, and MSTP_Init kept that. At 9600 baud the node enabled its driver about 4 ms early. MSTP.h now says to configure the engine after MSTP_Init, which sets every setting to its default. A Max_Master set before MSTP_Init is overwritten by that default.
  • rb bs-20261004-12: MS/TP engine fixes from the #2612 roll-up:
    • CR2-F08-001: a received Test_Request is answered at once with a Test_Response carrying the same data. 135-2024 cl. 9.1.3: “The response with Test_Response to a received Test_Request PDU is mandatory for all MS/TP nodes.” Cl. 9.3.5: “The data, if present, shall be that which was present in the initiating Test_Request.” The node used to answer Reply Postponed and never sent the response, so a peer’s loopback test always failed, and it handed the test pattern to APDUCallBack as if it were an NPDU. The reply is sent at once, cl. 9.5.6.9’s Reply transition, and not queued for the next token hold, which was the finding’s alternative. A received Test_Response, in IDLE or WAIT_FOR_REPLY, is no longer delivered to APDUCallBack either. The reply uses a dedicated 511-octet static buffer (RAM).
    • CR2-F08-010: a received frame whose SourceAddress is ThisStation, this node’s own transmission read back by a half-duplex transceiver, is treated as not for us. An echoed broadcast used to reach APDUCallBack. The new MSTP_SetRS485EnableTXDPin registers the transmit-enable callback, which MSTP_Init forced to NULL, so an integrator can disable the receiver while transmitting. Not done: echoed octets still count toward EventCount, so on hardware that echoes and has no TX-enable callback, PASS_TOKEN can still read the echo as the successor using the token.
  • rb bs-20261004-13: network-layer fixes from the #2610 roll-up:
    • CR2-F06-023: a copied network-layer NPDU encodes its Message Type octet. BACnetNPDU::operator= did not copy the network-layer-message flag, so the copy encoded the NPCI with bit 7 set and no Message Type (01 80 instead of 01 80 00). operator= and SetNetworkLayerMessageAndTakeControl now free the message the object already owned instead of leaking it, and operator= guards self-assignment. A test counts live message instances to show both.
    • CR2-F06-024: BACnetNPDU::SetNetworkPriority replaces the priority bits instead of ORing them. Setting Urgent over Critical Equipment gave Life Safety, and a value above 3 could reach the expecting-reply bit.
    • CR2-F06-030: BACnetNetworkNumberIs::operator= assigns the members directly, as #753 did for Who-Is-Router-To-Network. Assigning a default object left the old network number in place. Decode validates both fields before committing either, so a frame with an invalid flag no longer leaves the new network number written.
    • CR2-F06-034: Initialize-Routing-Table refuses trailing octets after its last port. CR-160 gave its fixed-size siblings the same exact-length rule. 00 12 34 was read as a Number of Ports 0 query and answered with the whole routing table.
    • CR2-F06-040: a vendor-proprietary network message whose payload does not fit fails the encode, as the Raw sibling does. It reported success with only the type and Vendor ID, and the frame went out without its data.
  • rb bs-20261004-14: network-layer and BVLL fixes from the #2610 roll-up:
    • CR2-F06-004: BACnetRoutingTableEntry::Decode decodes the DNET, port id and PortInfoLength into locals and commits only when every check passes. A failed decode marks the entry invalid. The fields were written before the PortInfoLength bound, so a failed decode left a reused entry valid with a corrupt length. operator= resets and copies the members directly, so assigning an invalid entry no longer keeps the old DNET. BACnetRouterEntry::Decode resets before refusing a NULL or empty buffer.
    • CR2-F06-027: BACnetVMACEntry refuses a virtual-mac-address longer than 6 octets, in SetVirtualMacAddress and in Decode. 135-2024: “virtual-mac-address [0] OctetString, – maximum size 6 octets”. This matches what #789 did for BACnetHostAddress.
    • CR2-F06-039: Original-Unicast-NPDU and Original-Broadcast-NPDU encode fails when the stored NPDU does not fit, as #1390 made Forwarded-NPDU do. Before, the encode returned the 4-octet header alone, whose BVLC Length declared the whole message. The empty-NPDU egress form still encodes.
    • CR2-F06-041: Forwarded-, Original-Unicast- and Original-Broadcast-NPDU render at most the NPDU they store, and reset after a failed decode. They rendered GetNPDULength() octets, computed from the BVLC Length, out of the stored vector. A failed Forwarded decode made that 65530 over an empty vector, and an egress message set by length alone did the same. Both read past the vector.
  • rb bs-20261004-15: network-layer fixes from the #2610 roll-up:
    • CR2-F06-026: BACnetNPDU, BACnetAddress, BACnetHostAddress, BACnetHostNPort and BACnetVMACEntry decodes reset before refusing a NULL or empty buffer, so a reused object no longer stays valid with its previous contents. A BACnetHostAddress CHOICE tag other than none [0], ip-address [1] or name [2] now sets a Reject (invalid-tag). It returned 0 with no error, which a caller read as “absent”.
    • CR2-F06-031: a Reject-Message-To-Network with reason 5 renders as removed in the JSON/XML decode output, not securityError. 135-2024 cl. 6.4.4: “5: This rejection reason value has been removed.” The enumerator keeps its name (#362).
  • rb bs-20261004-16: router fixes from the #2610 roll-up:
    • CR2-F06-013: a Reject-Message-To-Network that arrives with no SNET/SADR is relayed with SNET set to the network it arrived from and SADR set to the sender’s address. 135-2024 cl. 6.5.4: “The SADR field shall be set equal to the SA of the incoming NPDU.” It was relayed with no source, so its destination could not tell which router rejected. If the source cannot be set, the relay is dropped; if the sender’s address is unknown (an SC frame with no Originating Virtual Address), the reject is relayed without a source, as a data NPDU is.
    • CR2-F06-029: a router relays a Reject-Message-To-Network whatever its reason octet. cl. 6.6.3.5: “regardless of the contents of the rejection reason octet, the router shall relay the message in the normal manner”. A reason above 6 used to fail the decode, so the reject was dropped. The relayed copy, operator= and Clone() now keep the octet as received. SetRejectionReason still accepts only 0-6.
    • CR2-F06-033: Initialize-Routing-Table and Initialize-Routing-Table-Ack render their port list (DNET, Port ID, Port Info) in the JSON/XML decode output. GetElements was a stub, so the output showed only the number of ports.
  • rb bs-20261004-17: router and Network Port fixes from the #2610 roll-up:
    • CR2-F06-014: in a router build without STACK_OPTION_ROUTER_PHYSICAL, a received Router-Available-To-Network is ignored and logged at Info, as Router-Busy-To-Network is. Every such broadcast used to log an Error-level “PANIC … contact Chipkin”. In a router build without STACK_OPTION_OBJECT_TYPE_NETWORK_PORT, a received Network-Number-Is is ignored at Info; it used to be logged and returned as an unknown message type.
    • CR2-F06-042: BACnetNetworkPortCommand value 3 is named restartSubordinateDiscovery and renders as restartSubordinateDiscovery in the JSON/XML decode output. 135-2024 clause 21 names it restart-subordinate-discovery; it was restart-slave-discovery. The value on the wire is unchanged. Code that names BACnetNetworkPortCommand::restartSlaveDiscovery must change to the new name. The type is not in CASBACnetStackDLL.h.
  • rb bs-20261004-18: CR2-F06-001 (#2610): the router’s Reject-Message-To-Network rate limit is now kept per (ingress port, sender), the way the held-message quota already is. It was kept per sender only, so the same MAC, VMAC or IP on two networks shared one budget. For example, a BACnet/SC node that chose a victim’s VMAC on another hub could use up the rejects owed to the victim. This mattered on routers with two ports of the same type, such as a secure connect router’s two BACnet/SC ports. Virtual-router rejects and the unknown-message-type reject use the frame’s port when one resolves. When none does, they share one bucket per sender, as every reject did before; this applies to a build without the physical router, to a disabled router, and to a frame that cannot be attributed to a port. The limiter’s table is still 8 entries, and a sender seen on several ports now takes one entry per port.
  • rb bs-20261004-19: two items from the #2611 roll-up:
    • CR2-F07-015: a BBMD now answers with BVLC-Result X’0060’ when it cannot forward a registered foreign device’s Distribute-Broadcast-To-Network because a send to a BDT peer or another foreign device failed. Before, the sender got no answer. 135-2024 Annex J.4.5: “If the BBMD is unable to perform the forwarding function, or the message was not received from a registered foreign device, then it shall return a BVLC-Result message to the foreign device with a result code of X’0060’ indicating that the forwarding attempt was unsuccessful.” The unregistered-sender half was already fixed by #2538.
    • CR2-F07-042: BACnetStackNetworkPortBase::SetNetworkType refuses network type 8 (“formerly: non-bacnet (8), removed in version 1 revision 18”). The class-level SetNetworkPortProperty no longer writes Network_Number or Network_Number_Quality. Neither arm had a production caller. Both contradicted the live rules in BACnetDBDevice::SetNetworkPortProperty, which holds Network_Number pending and treats Network_Number_Quality as read-only (cl. 12.56.14). Through those arms, a written 65536 was narrowed to 0 and accepted.
  • rb bs-20261004-2: an update of an Access Credential’s Last_Access_Point now notifies its property-level COV subscribers even when the value is unchanged (#3479). This applies to both SubscribeCOVProperty and SubscribeCOVPropertyMultiple. 135-2024 cl. 12.35.19: “If property level COV is in effect for this property, any update of this property shall cause a COV notification to be issued, regardless of whether the value of this property changes.”
    • Two uses of a credential at the same Access Point now give two notifications; the second one used to be suppressed because the value had not changed.
    • No other property in 135-2024 carries this wording.
    • The status-flag re-check of other properties’ subscriptions is not affected, because it is not an update of those properties.
    • Host note: a host that calls BACnetStack_UpdateValue for Last_Access_Point when nothing happened now causes a notification on every call. That is the spec’s rule for this property, so call it only when the credential is used.
  • rb bs-20261004-20: #2559 (owner ruling 2026-09-29): BACnetStack_SetBACnetSCCertificateFileObjects now refuses a binding whose two issuer-certificate File instances are the same. It returns false and leaves the previous binding unchanged. 135-2024 cl. 12.56.101: “The device shall be capable of storing two issuer certificate files for this port.” With one File object in both slots, the port could hold only one issuer, so an issuer rollover could replace the issuer currently in use. The header doc line is tracked as #3521.
  • rb bs-20261004-21: BACnet/SC fixes from the #2613 roll-up:
    • CR2-F09-020:
      • BACnetSCBVLL::InsertDestinationOption and InsertDataOption now keep the ‘More Options’ chain: the appended option becomes the tail, and the previous tail is marked as continuing. Before, adding a Hello to a message that already carried a destination option encoded a chain that the receiver stopped reading after the first option.
      • An SC frame with an unknown BVLC function (X’0D’-X’FF’) now fails decode as communication / bvlcFunctionUnknown, not as an out-of-resources Abort with an “Out of memory” log.
      • BACnetSCPacket::Clone allocates with new(std::nothrow), so its out-of-memory check is reachable.
  • rb bs-20261004-22: #3523 (render half): the JSON/XML decode output of a BACnet/SC message now includes its Destination Options and Data Options. Each option shows its header option type, its More Options bit, its bit 6 (Must Understand on a destination option, Every Segment on a data option) and its data. They were omitted, so a Connect-Request rendered without its Hello option. The clone half of #3523 remains open.
  • rb bs-20261004-22: #3514: four unused-variable warnings in the embedded presets are gone: deviceInstance in BACnetNetworkLayer.cpp, objectInstance in BACnetDBDevice.cpp, and objectType/objectInstance in BACnetInterface::UpdateValue. Each is a local that is read only under an option guard or in a debug log, and is now marked with NOREF.
  • rb bs-20261004-23: renderer fixes from the #3090 roll-up:
    • CR2-F04-002: the XML decode output now writes a carriage return as &#xD;. Inside CDATA, where references are not recognised, the section is closed around it. XML end-of-line handling turned a literal CR into LF, so a parsed CharacterString was silently altered. Attribute values now carry tab, LF and CR as &#x9;, &#xA; and &#xD;, because attribute-value normalisation turned them into spaces.
    • CR2-F04-005: JSON attribute keys are now escaped like values. A " or \ in a key used to break the JSON. XML element and attribute names are now XML Name tokens: each character outside [A-Za-z0-9_.-] becomes _, and a name that does not start with a letter or _ gains a leading _. This applies to names from GetName() and to names supplied by GetElements. The empty name that marks a transparent container is left alone, so those containers still write no element. JSON element names already went through WriteJsonString. Names are sanitised at render time instead of documenting a naming contract on SetAttribute/InsertValue/AddChild/InsertElement.
  • rb bs-20261004-24: CR2-F12-006 (#2690): a CreateObject that ends in Result(-) no longer changes Database_Revision. Before, the revision was incremented once by adding the object, once more by the rollback’s delete, and a third time when an Object_Name initial value had been applied, so it rose by 2 or 3. Clients that cache the database then re-read the whole device after a request that created nothing. 135-2024 cl. 12.11.35 increments Database_Revision “when an object is created, an object is deleted, an object’s name is changed”. The revision is now saved before the object is added and restored by the rollback, through a new BACnetDBDevice::RestoreDatabaseRevision.
  • rb bs-20261004-25: #3529: if the CreateObject-ACK cannot be allocated, the object is now rolled back, including Database_Revision, before the stack answers Abort(outOfResources). Before, the client was told the request was aborted while the object existed.
  • rb bs-20261004-26: #2689 (a sibling of CR2-F11-005 that #3502 found): BACnetPriorityValue::Decode and BACnetPriorityArray::Decode now reset before they refuse a NULL or empty buffer. Before, a reused object kept its previous value, or its previous 16 slots, and IsValid().
  • rb bs-20261004-27: fixes from the #2688 roll-up:
    • CR2-F10-021: a ReadRange request’s count, in every range type, and its By Position or By Sequence Number reference must now be application-tagged; a context-tagged one is Rejected INVALID_TAG. The By Time reference (a BACnetDateTime) is not covered; that is #3534. The primitive decode checks the tag number only for an application tag, so 3E 09 05 19 0A 3F used to be read as reference 5, count 10. Two existing tests encoded the count with a context tag (0x3C, 0x3A) where they meant the application INTEGER tag. They are corrected to 0x34 and 0x32.
    • CR2-F10-024: a ReadProperty request that ends after the service choice is now Rejected with MISSING_REQUIRED_PARAMETER, not OTHER. 135-2024 cl. 18.9: “MISSING_REQUIRED_PARAMETER - Generated in response to a confirmed request APDU that is missing at least one mandatory service argument.” The same zero-length guard in BACnetReadPropertyACK::Decode and BACnetPropertyAccessResult::Decode is unchanged: both are client-side decodes, which send no reply, so the reason code reaches no peer.
  • rb bs-20261004-28: fixes from the #2688 roll-up:
    • CR2-F10-028: a ReadProperty or ReadPropertyMultiple of (Network Port, 4194303) that matches no local Network Port object is no longer Aborted with reason OTHER.
      • ReadProperty answers Error OBJECT / UNKNOWN_OBJECT. 135-2024 cl. 15.5.1.3.1: “Specified object does not exist. OBJECT UNKNOWN_OBJECT”.
      • ReadPropertyMultiple answers each property of that specification with that error, as for any absent object.
      • Every failure of the lookup gets this answer: no Network Port object, several ports and none of the arriving network’s type, or an internal failure such as a Network_Type that cannot be read. The Abort is kept only where the request reaches the processor without network parameters.
      • Tests cover the device with no Network Port object; the several-ports-no-match branch is untested.
    • CR2-F10-027: BACnetPropertyAccessResult and its abstract-syntax factory case are now compiled under STACK_OPTION_OBJECT_TYPE_GLOBAL_GROUP, not STACK_OPTION_OBJECT_TYPE_GROUP.
      • Its only consumer is the Global Group object’s Present_Value.
      • A build with Global Group but without Group had a property profile naming a datatype the factory could not create.
      • The header comment now says Global Group (cl. 12.50.7).
      • SetObjectIdentifier’s one-line body, which drew -Wmisleading-indentation, is laid out one statement per line.
  • rb bs-20261004-29: decode strictness and stale state:
    • #3534: BACnetDateTime::Decode now Rejects (INVALID_TAG) a context-tagged Date or Time. BACnetDateTime is SEQUENCE { date Date, time Time } with application tags. The primitive decodes check the tag number only for an application tag, so a context tag was accepted. This reaches every user of the type, including the ReadRange By Time reference, which #2688 CR2-F10-021 left open. Three siblings (DateRange, TimeValue, DateTimePattern) have the same laxity; that is #3537.
    • #2608 CR2-F03-012: an application Boolean in extended-length form (15 00, 15 01) is Rejected INVALID_TAG. 135-2024 cl. 20.2.3 encodes an application Boolean “within a single octet”, with the length/value/type field B’000’ (FALSE) or B’001’ (TRUE). LVT B’101’ is neither. It used to be read as an extended length, and the next octet was taken as the value.
    • #2608 CR2-F03-014, -018, -024, -045, -031: 22 Decode entry points reset the object before checking their parameters. A refused buffer (NULL, zero length, or a DATE/TIME tail shorter than 5 octets) used to return 0 with the previous value still in place and IsValid() still true. Among the primitives, #1211 had fixed this only in BACnetPrimitiveENUM.
      • The 13 primitives: BOOL, NULL, INT, UINT, UINT64, ObjectIdentifier, CharSTRING, BitSTRING, OctSTRING, REAL, DOUBLE, DATE, TIME.
      • The 8 composite entry points CR2-F03-031 lists: PropertyValue (Decode and DecodeWithObjectType), PropertyReference, ObjectPropertyReference, ObjectPropertyValue, PropertyStates, NameValue, NameValueCollection.
      • BACnetDateTime::Decode, which #3534 edits and which had the same shape (found in review).
      • BACnetPrimitiveBase::DecodeWithContext, which the finding named as belonging to another group, is not changed.
  • rb bs-20261004-30: decode strictness and value-class correctness:
    • #3537: BACnetDateRange, BACnetTimeValue and BACnetDateTimePattern now Reject (INVALID_TAG) a context-tagged Date or Time, as BACnetDateTime does since #3534. Their cl. 21 productions use application-tagged Date, Time, DatePattern ([APPLICATION 10]) and TimePattern ([APPLICATION 11]).
      • All three now reset before their parameter check, which is the #2608 shape.
      • BACnetTimeValue::Reset() never called BACnetObjectBase::Reset(), so it did not clear validity. A failed decode of a previously valid time/value pair left it reporting valid. The reset-first test found this; Reset() now calls the base.
    • #2608 CR2-F03-013:
      • BACnetTag’s three-argument constructor now resets its members before Set(), which refuses tag number 255 (cl. 20.2.1.2) before assigning anything. Those members used to be indeterminate.
      • BACnetPrimitiveBase::SetContextTagNumber(255) now leaves the tag unchanged. It used to switch the class to context while keeping the old tag number.
      • BACnetPrimitiveBOOL’s override sets LVT 1 only when the tag did become context. Otherwise an application FALSE would have encoded as TRUE once the base stopped switching the class (found in review).
    • #2608 CR2-F03-019: BACnetPrimitiveUINT::IsEqual and BACnetPrimitiveUINT64::IsEqual compare at 64-bit width, through a new BACnetPrimitiveBase::GetUnsignedValueAs64Bits virtual.
      • The two classes share GetDatatype() (UnsignedInteger), so either one could cast the other to its own class.
      • Before: UINT(5) compared equal to UINT64(0x100000005), and UINT64 read past a UINT’s 4-byte value.
    • #2608 CR2-F03-025: BACnetPrimitiveCharSTRING and BACnetPrimitiveBitSTRING::GetMinimumMemoryCount now count the extended tag-number octet a context tag number >= 15 needs, as OctSTRING has since #579.
  • Left on #2608: 044 (REAL/DOUBLE equality for NaN and -0 changes change-of-value comparisons, so it gets its own batch) and the other unticked items.
  • rb bs-20261004-31: codec correctness from the #2608 roll-up:
    • CR2-F03-020: BACnetPrimitiveENUM::Decode now refuses (Reject OTHER) a value its virtual SetValue rejects. That refusal used to be ignored, so a closed-domain subclass that overrides only SetValue returned a non-zero size with IsValid()==false and no error. Nothing observable changes today: the four subclasses that refuse values (AccessPassbackMode, DoorSecuredStatus, DoorValue, LockStatus) re-check in their own Decode. Those four re-checks are kept, although the finding says they could go: removing them is a separate change, and they carry each class’s own log text.
    • CR2-F03-048: BACnetOptionalAny::Encode switches on its choice with a default Abort, as BACnetOptionalDateTime does (CR-F03-014). An object made valid with no choice used to encode an application Null and report success. The sibling-count comments in BACnetOptionalDateTime/DateTimePattern now say 15, not 13.
    • CR2-F03-050: BACnetOptionalAny::SetAnyAndTakeControl returns false and leaves the object invalid when the stored copy is invalid or empty, which is what a failed child Clone() produces (#832) and what an invalid value produces. It used to report success with an empty Any. The setter takes ownership of value in every case, refused or not, as before. The finding’s alternative (build in place with InsertObjectAndTakeOwnership) was not taken. The test covers the invalid-value path; the failed-Clone() path reaches the same check but has no test of its own. Its decode now also resets before the parameter check (the CR2-F03-031 shape).
    • CR2-F03-053: BACnetOptionalUnsigned::GetMinimumMemoryCount returns the larger of its two choices in every state, as its 15 siblings do. An unset object used to report 0.
  • rb bs-20261004-4: BACnet/IP BVLL fixes from the #2611 roll-up:
    • CR2-F07-003: a BDT entry’s broadcast mask is decoded as OPTIONAL. 135-2024 cl. 21.6: “broadcast-mask [1] OctetString OPTIONAL – shall be present if BACnet/IP, and absent for BACnet/IPv6”. As a required field, an absent mask left a Reject on the caller’s error container although the entry decoded, and a mask that was present but truncated read as absent, so the list decode kept the malformed entry. A truncated mask now fails the entry. A Write-BDT on a BACnet/IP port still refuses an entry without a mask.
    • CR2-F07-005 / -045: Register-Foreign-Device, Delete-Foreign-Device-Table-Entry and BVLC-Result refuse a BVLC Length other than their fixed size: X’0006’ (J.2.6.1, J.2.1.1) and X’000A’ (J.2.9.1). The base checks only 4 <= declared length <= datagram length, so a declared length of 4 on a padded datagram took the Time-to-Live, FDT entry or Result Code from octets outside the message. The other fixed-length BVLL messages (Read-BDT, Read-FDT) carry no field after the header, so there is nothing to misread. The Register-Foreign-Device decode-failure log no longer says “Delete Foreign Device Table Entry”.
    • CR2-F07-006: BACnetDeleteForeignDeviceTableEntry::GetIpAddress / SetIpAddress refuse a NULL pointer, as the #745 hardening did for the BDT and FDT entries.
    • CR2-F07-048: BACnetBVLL::Decode resets the object before it refuses a NULL or too-short buffer, so a reused object no longer keeps the previous frame’s function and payload.
    • Not in this batch: CR2-F07-044 (the BVLC-Result “rendered as-is” comment) and the roll-up’s other open items.
  • rb bs-20261004-5: BACnet/IP BVLL and BBMD fixes from the #2611 roll-up:
    • CR2-F07-004: Read-Broadcast-Distribution-Table-Ack, Read-Foreign-Device-Table-Ack and Write-Broadcast-Distribution-Table decode into an empty table and reset on failure. They appended, so decoding into a reused object kept the previous table’s entries, and Encode (bounded by the BVLC Length) emitted the first old entries. Encode now refuses a table whose entry count disagrees with the BVLC Length instead of indexing past it.
    • CR2-F07-016: a BBMD no longer forwards an Original-Broadcast-NPDU or Distribute-Broadcast-To-Network that carries no NPDU (declared length 4). The Original-Broadcast one went to every BDT peer and foreign device as an empty Forwarded-NPDU. The Distribute-Broadcast arm checked only for a NULL NPDU pointer, which an empty vector need not give (with libstdc++ it does, so that half was already refused there).
    • CR2-F07-044: a BVLC-Result whose Result Code is outside Annex J’s seven renders with its hex code and the text “Unknown”, as BACnetBVLCResult’s #1238 comment says. Its JSON/XML decode used to fail.
    • CR2-F07-046: BACnetBBMD::AddForeignDevice(const BACnetBVLLFDTEntry&) keeps the address overload’s table rules: one entry per address, the newer replacing the older, and the #626 STACK_OPTION_MAX_FOREIGN_DEVICE_TABLE_ENTRIES cap. It pushed unconditionally. An entry without an address is refused.
  • rb bs-20261004-6: BACnet/IP fixes from the #2611 roll-up:
    • CR2-F07-018: the BACnet/IP Secure-BVLL (X’0C’) handler drops the frame as a retired function, instead of logging “PANIC: Not yet implemented” and returning an internal error. 135-2024 J.2.13 is a “Deleted Clause”: “This clause has been removed.” A received X’0C’ frame was already dropped at decode, so only a direct call reaches the handler. The BBMD’s Original-Broadcast-NPDU handler now checks its incoming packet and network parameters before using them, as the Distribute-Broadcast-To-Network handler does, and both drop a duplicated NULL check.
    • CR2-F07-047: a failed allocation reading a BACnet/IP Network Port’s BACnet_IP_Mode, or any Network Port’s Routing_Table, now reports resources / outOfMemory, as the other Network Port properties have since #1332. It used to fail with no error set.
  • rb bs-20261004-7: BACnet/IP and data link fixes from the #2611 roll-up:
    • CR2-F07-014: while a foreign device’s registration or periodic re-registration is pending, only Successful completion (X’0000’) and Register-Foreign-Device NAK (X’0030’) from the BBMD answer it. Before, a Distribute-Broadcast-To-Network NAK (X’0060’) for an earlier broadcast was taken as a refused registration and silently ended foreign-device status. A NAK to a re-registration now also resets the Network Port’s BACnet_IP_Mode. It stayed FOREIGN for good, because the reset cleared the flag the timeout branch tests.
    • CR2-F07-030: BACnetPacket::CreateSegment refuses a sequence number at or past the final segment, in both the Confirmed-Request and ComplexACK branches. The final-segment length wrapped to about 65k and the copy started past the payload. The finding’s other half, the ComplexACK branch leaking the new packet, was already fixed at the tip.
    • CR2-F07-031: the generic data link send (used by MS/TP) refuses a packet that encodes to zero octets even when the encoder set no error. The 0-octet frame went to the send callback, and a callback that answered 0 made the send look successful. BACnet/IP and BACnet/SC already refused it. The remaining BACnetPacket::Encode exits that return 0 without setting an error are unchanged; this guard makes them harmless.
    • CR2-F07-032: BACnetDataLink::Reset clears the responding state machine pointer, as it clears the other three service pointers.
    • A BACnet/IP BBMD build without STACK_OPTION_OBJECT_TYPE_NETWORK_PORT compiles again. The own-broadcast check (#2538) read m_localAddress, which exists only with that option; it now uses GetLocalAddressPtr(). Found by this batch’s option probes.
  • rb bs-20261004-9: MS/TP engine fixes from the #2612 roll-up:
    • CR2-F08-004: a frame addressed to this node’s own MAC address is refused by MSTP_InsertFrameToSend. A frame like that at the head of the queue is also dropped, as a defensive guard: only MSTP_Init sets ThisStation, and it empties the queue first. MSTP_SendFrame refuses such a frame every time, so it was retried on every token hold and blocked every frame queued behind it.
    • CR2-F08-006: WAIT_FOR_REPLY times out at exactly Treply_timeout. 135-2024 cl. 9.5.6.4: “ReplyTimeout If SilenceTimer is greater than or equal to Treply_timeout, then assume that the request has failed.” The test used >. ReceivedPostpone and ReceivedUnexpectedFrame are now in the same else if chain, so a still-set valid-frame flag can no longer override a timeout to IDLE in the same call.
    • CR2-F08-011: a zero-length queued frame is sent as the 8-octet header alone. Cl. 9.3: “If the Length field is zero, that is, if both length octets are zero, then the Data and Data CRC fields shall not be present.” It went out with Length=0 followed by a 2-octet Data CRC.
    • CR2-F08-012: the expecting-reply test reads the NPDU control octet only when the payload holds it (two octets). With a one-octet payload, a stale octet left in the slot by an earlier frame decided whether the frame was sent as Data Expecting Reply.
    • CR2-F08-014 (a failed HandleProcessingErrors falling through to ProcessAPDU on MS/TP) was verified already fixed at the tip by #2546.
  • rb bs-20261005-1: codec correctness from the #2608 roll-up:
    • CR2-F03-010: a failed BACnetObjectBase::DecodeWithContext now sets the error, as its callers’ “present but malformed” idiom assumes. That covers a mandatory field whose opening tag is missing or wrong, a field whose closing tag is wrong, and a wrapped decode that failed without setting one (Reject INVALID_TAG for the tag cases, OTHER for the wrapped case). BACnetSequenceOf<T>::Decode and BACnetSequenceOfPropertyValue::DecodeWithObjectType now set the error when a list element’s tag cannot be decoded. An absent optional field still leaves the error unset (#890’s probing idiom).
    • CR2-F03-021: EnumerationFactory returns BACnetAuditLevel for Audit_Level on every object type, when an audit object type is compiled in. Audit_Level is optional on every object type (e.g. cl. 12.2.38); only five types had a case, so reading it from any other produced an untyped value.
    • CR2-F03-030: BACnetObjectPropertyReference, BACnetObjectPropertyValue and BACnetDeviceObjectPropertyReference IsEqual now compare each OPTIONAL field by presence first, the #751 rule. An absent array index (the whole array) used to equal index 0. A reference built with the index flag off but a stale value used to differ from one without it.
    • CR2-F03-033: BACnetPropertyStates::GetMinimumMemoryCount no longer adds 2 octets for opening and closing tags its encoding never writes; wrapping parents count their own. The header’s two #endif labels were swapped and are corrected.
    • BACnetOptionalUnsigned::Decode resets before its parameter check (the CR2-F03-031 shape, found in bs-20261004-31’s review).
  • rb bs-20261005-10: Notification Forwarder writes and Local_Forwarding_Only, from the #2694 (family F16) roll-up:
    • CR2-F16-016 (security): with Local_Forwarding_Only TRUE, a Notification Forwarder now forwards only notifications offered through the local-origin path (BACnetDBDevice::OfferLocallyOriginatedNotificationToForwarders). cl. 12.51.12 limits it to “forwarding notifications initiated from within the same device”. Before, a wire arrival was forwarded whenever its Initiating Device Identifier was this device’s own instance. Any peer could claim that and use the forwarder to fan its notification out. A deployment can see this: a wire notification that names this device as its initiator is no longer forwarded. The BACnet testing tool’s BTL 7.3.2.30.11.1 row models its “local source” exactly that way, so it needs a coordinated change (the follow-up named in the RESULTS comment).
    • CR2-F16-021: a Notification Forwarder’s Recipient_List now holds at most CAS_BACNET_MAX_NOTIFICATION_FORWARDER_RECIPIENTS (32) entries. The cap applies to a write, the list services, and InsertDestination, and is pinned to that literal in EverySecurityBoundIsPinnedToAConcreteValue. Before, #1161’s cap reached only Subscribed_Recipients, so a peer that could write Recipient_List could grow it without bound. Note: a Recipient_List write with more than 32 entries that used to succeed is now refused, and so is a 33rd BACnetStackTestTool_AddNotificationForwarderRecipient. 32 is this stack’s choice (the Subscribed_Recipients cap’s value), not a spec number. The Notification Class Recipient_List is still uncapped.
    • CR2-F16-020: a WriteProperty or WritePropertyMultiple that fails because a list-valued property is full now reports RESOURCES / NO_SPACE_TO_WRITE_PROPERTY, as cl. 15.9.1.3.1 and 15.10.1.3.1 give it. Before, the list services’ NO_SPACE_TO_ADD_LIST_ELEMENT came back unchanged. Both paths have a test. The remap applies to every object’s WriteProperty/WritePropertyMultiple, not only the Notification Forwarder’s.
  • rb bs-20261005-11: from the #2694 (family F16) roll-up:
    • CR2-F16-019 (the object half): the stack-held Notification Forwarder’s Port_Filter write now follows cl. 12.51.11 - “Neither the size of the array nor the Port_ID portion of the BACnetPortPermission entries shall be modifiable via writes to this property” - and only the Enabled parts change.
      • A whole-array write that keeps the size and every Port_ID is now accepted, all or nothing; before, it was always refused. A size or Port_ID change, or an element that is not a BACnetPortPermission, is refused (WRITE_ACCESS_DENIED).
      • An indexed write whose Port_ID differs from its entry’s is now refused. Before, where the property had been made writable (SetPropertyWritable), it was SimpleACKed with the Port_ID silently dropped.
      • Left (#3558): the property profile still marks Port_Filter read-only, although cl. 12.51.11 says “If present, this property shall be writable.” Making it writable also reaches a Notification Forwarder held by the host (made with the generic AddObject), whose writes go to CallbackSetPropertyConstructed with no stack-side size or Port_ID check - a change to what reaches a customer callback, so the owner decides it.
  • rb bs-20261005-12: event-notification building and the Subscribed_Recipients countdown, from the #2694 (family F16) roll-up:
    • CR2-F16-008: the send-path SetEventNotificationRequest overload (the 5-argument one, used by the alarm engine and the Notification Forwarder) changed in three ways:
      • It now resets the request first.
      • For an ACK_NOTIFICATION it now omits From State and Event Values, as it already omitted AckRequired. cl. 13.8.1.1.11 / 13.8.1.1.13 say: “This parameter shall only be present if the ‘Notify Type’ parameter is EVENT or ALARM”. Table 13-3 lists both as “Not present” for an acknowledgment transition.
      • It now returns AreAllParametersValid(), so the message generator’s SetValid(true) marks only a request that may be sent.
      • The ACK_NOTIFICATION half is a guard: no sender in the stack builds acknowledgment parameters that carry From State or Event Values today (AcknowledgeAlarm’s Set() clears them, and a received ACK_NOTIFICATION carrying them is refused at decode, so the Notification Forwarder never sees one). Before, this overload would have sent them. A reused request also kept the optional fields of the notification before it.
      • The message generator’s SetValid(true) stays; the overload’s validity result (the recommendation’s “or check it”) is what now stops an invalid request first.
      • SetConfirmedEventNotificationRequest and SetUnconfirmedEventNotificationRequest now return the object’s validity. Before, they returned true for an object they had just marked invalid. Two tests pinned that and were updated.
    • CR2-F16-017: each Subscribed_Recipients entry now counts its own minutes. Before, all entries shared the forwarder’s minute boundary, so an entry added 59 s after it lost its first minute 1 s later: a Time Remaining of 1 could vanish a second after the SimpleACK. A new entry’s minute now starts within one Loop period of when it was added. Time_Remaining reads the whole minutes left, which is the ceiling of the true remaining time.
      • One step between two ticks now counts at most an hour (MAXIMUM_TICK_GAP_SECONDS, 3600). Before, a forward clock correction, such as the first host time sync after boot, was counted in full and purged every subscription; now it ages them by at most an hour. A host that ticks less often than hourly still ages its entries by the cap each tick, so they still reach zero (cl. 12.51.9: “An entry shall be removed from the list when the remaining time reaches zero”), later than real time.
      • The one-hour cap is this stack’s choice.
  • rb bs-20261005-14: from the #2750 (family F21) roll-up:
    • CR2-F21-014: an Alert Enrollment’s Notify_Type is now ALARM or EVENT on the provisioning path too, through Setup and AddAlertEnrollmentObject (the test-tool export BACnetStackTestTool_AddAlertEnrollmentObject). cl. 12.52.11 says: “shall convey whether the notifications generated by the object should be Events or Alarms”. The WriteProperty arm already refused other values (#2971). The provisioning path stored any uint8, so AddAlertEnrollmentObject(..., 2, ...) (ACK_NOTIFICATION) sent every alert as an acknowledgement notification. AddAlertEnrollmentObject checks before it creates the engine, so a refused call leaves none behind.
    • Left: the same unchecked store in BACnetStackAlarmAndEventObject::SetGeneralProperties is fed, for intrinsic objects, by the customer export BACnetStack_SetAlarmsAndEventsForObjectEnabled, so refusing a value there changes that export’s result. That is the owner’s call (RB705), named in the RESULTS comment.
    • CR2-F21-013 needed no change: #2912 and #3102 already give every Notification Class its own instance as Notification_Class (and the reporting class while armed), so the 4194303 the finding describes no longer reaches a read.
  • rb bs-20261005-15 (#2749 CR2-F20-013/-017, the remainder): twelve more event-values Decode()s now reset before the null/zero-length check, as the eleven fixed in #3526 do. They are BACnetNotificationParameters itself and its ChangeOfBitstring, ChangeOfCharacterstring, ChangeOfDiscreteValue, ChangeOfDiscreteValueNewValue, ChangeOfReliability, ChangeOfState, ChangeOfStatusFlags, ChangeOfValue, ChangeOfValueNewValue, Extended and ExtendedParameter arms. Before, a previously decoded object that was refused an empty buffer kept IsValid() == true and its old content, so a later Encode() re-sent it. Not reachable from a received frame: no path in the stack decodes an already-decoded object with an empty buffer, as with the eleven in #3526. That finishes the family’s 23 Decode bodies.
  • rb bs-20261005-16: event-values decoding, from the #2749 (family F20) roll-up:
    • CR2-F20-005: a ChangeOfReliability arm whose property-value list runs the buffer out with no [2] closing tag is now rejected (invalidTag), as Extended’s list already was. Before, the decode reported success on the truncated list. An out-of-memory element allocation now sets Abort(outOfResources); before, it returned 0 with no error. The truncated list was reachable only through a direct Decode(): inside DecodeWithContext the frame was already refused (the loop tried to decode the [19] closing tag as a property value). The out-of-memory branch has no test.
    • CR2-F20-019: a ChangeOfTimer arm whose new-state is outside BACnetTimerState (0-2), or whose last-state-change is outside BACnetTimerTransition (0-7), is now rejected (parameterOutOfRange). Both are closed enumerations in clause 21. Before, the decode accepted any 32-bit value, and the uint8_t accessors narrowed it, so 256 read as IDLE. SetNewState and SetLastStateChange now refuse the same values, so the stack cannot build a frame its own decoder rejects; the Change-of-Timer event algorithm, which builds new-state from the monitored Timer_State, then builds no notification for a state outside 0-2. The accessors stay uint8_t, which is safe now that nothing out of range reaches them.
    • CR2-F20-006: BACnetNotificationParameters::SetChangeOfReliability takes the reliability as uint16_t, like the arm’s own setter. Before, a proprietary reliability (64-65535) was truncated to its low byte.
    • CR2-F20-010 needed no change: #3482 item 7 had already range-checked the tag and value.
  • rb bs-20261005-18 (#2749 CR2-F20-004): every BACnetNotificationParameters::Set* now unwinds with Reset() when a sub-setter refuses its input after the factory has built the CHOICE arm, as SetExtended already did. This covers 23 setters, overloads included.
    • Before, the arm stayed attached while m_type kept its Reset() value (changeOfBitstring, 0), so GetType() named one CHOICE and GetNotificationParameters() held another. Several setters refuse ordinary caller input: a bad character-string encoding, an unknown property-state type, a BufferReady instance above 4194303, an out-of-range timer state.
    • Every in-tree caller already checks the return value, so this is hygiene rather than a live defect.
    • The test covers three setters (BufferReady, ChangeOfTimer, ChangeOfCharacterstring) plus an accepted-set control. The other twenty get the same one-line insertion.
    • The “Unlike its siblings” comment in SetExtended is corrected.
  • rb bs-20261005-19 (#2748 CR2-F19-008): BACnetEventParameter and all twenty of its arms’ Decode()s now reset before the null/zero-length check: the eight the finding names (AccessEvent, BufferReady, ChangeOfBitstring, ChangeOfCharacterstring, ChangeOfDiscreteValue, ChangeOfLifeSafety, ChangeOfState, ChangeOfStatusFlags) and the twelve its title’s “every Decode() in the group” covers (ChangeOfTimer, ChangeOfValue, ChangeOfValueCovCriteria, CommandFailure, DoubleOutOfRange, OutOfRange, SignedOutOfRange, UnsignedOutOfRange, FloatingLimit, UnsignedRange, Extended, ExtendedParameter). Before, an object that already held a value and was refused an empty buffer kept IsValid() == true and its old content. Arms reached through the wrapper’s DecodeWithContext were already reset; this is the direct-Decode() case. ChangeOfValue, ChangeOfValueCovCriteria, Extended and ExtendedParameter have no test of their own (no RoundTrip setup to reuse); the other sixteen and the wrapper do. The BACnetFaultParameter* family (#2747, F18) has the same shape and is not changed here.
  • rb bs-20261005-2: decode and value-class correctness:
    • #3542: BACnetVTCloseError::Decode decodes its OPTIONAL listOfVtSessionIdentifiers as optional.
      • Before, it was decoded as mandatory and the result ignored, so an absent list set Reject OTHER underneath a successful decode, and a malformed list was accepted.
      • Now an absent list leaves the error unset, and a present-but-malformed list fails the decode, per the #638/#1065 idiom.
      • The decode also resets before its parameter check.
    • #2608 CR2-F03-011: BACnetChoiceSpanLength now counts no content octets for an application Boolean, whose value is its LVT (cl. 20.2.3). It also matches every closing tag against its own opening tag. Before, 0E 11 0F swallowed the closing tag, and 0E 3E 4F 0F was accepted. Its header no longer names callers it does not have; nothing in source/ calls it.
    • #2608 CR2-F03-029: BACnetPropertyStates renders the extended-value [63] arm as an Unsigned “ExtendedValue”. It was refused, so the value vanished from DecodeAs JSON/XML; it now appears there.
      • The 258/259 decomposition (sc-connection-state, sc-hub-connector-state) now lives in one helper that both Decode and SetExtendedValue use.
      • This path is reached from the Send*EventNotificationWithEventValues exports and the Event Log insert. The visible change is that the value now renders and compares equal; what the exports return is unchanged.
      • Before, a locally built SC-state notification kept type 63, could not be rendered, and compared unequal to the same bytes decoded. The encoding on the wire is unchanged.
    • #2608 CR2-F03-051: the 14 BACnetOptional* value setters now refuse an invalid value and leave the object unchanged, as SetCharacterString does. Those are the 12 SetValues, SetDateTime and SetDateTimePattern.
      • Before, the value was stored, the object reported valid, and the failure surfaced only at Encode.
      • TestBACnetOptionalDoorValue’s test that pinned the old acceptance is inverted. No caller in source/ uses these setters.
  • rb bs-20261005-20 (#2747):
    • CR2-F18-007: a FAULT_EXTENDED Fault_Parameters write whose pParameters list holds a reference [0] element is now applied, and the reference reads back in its place. Before, the whole write was refused with VALUE_OUT_OF_RANGE, although the algorithm stores the same element type the production decodes into.
    • CR2-F18-008: Life Safety Point and Life Safety Zone now map to Present_Value in IsObjectPropertyMonitoredByIntrinsicFault and GetPropertyMonitoredByIntrinsicFaultForObject. 135-2024 cl. 12.15.4 and 12.16.4 say “If a fault algorithm is applied, then this property shall be the pMonitoredValue fault algorithm parameter.” The life-safety fault algorithm’s own CHANGE_OF_RELIABILITY Property_Values now carry Present_Value before Mode; before, it omitted the value. On the wire little changes: the intrinsic and Event Enrollment paths already rebuild Property_Values from Table 13-5 (Present_Value, Mode, Operation_Expected for these objects). The algorithm’s list is reached only by the fallback used when the host serves none of that row, and by direct use of the algorithm.
  • rb bs-20261005-21 (#2748):
    • CR2-F19-001, -009: copying an event-parameter arm that holds a SEQUENCE OF (ChangeOfState, ChangeOfTimer, ChangeOfBitstring, ChangeOfLifeSafety, ChangeOfCharacterstring, AccessEvent), or ChangeOfDiscreteValue (whose new-value copy clones a primitive), now leaves the copy invalid when a member copy fails. Before, the copy took the source’s validity and reported valid while holding an emptied list. The BACnetEventParameter wrapper’s operator= and Clone() likewise take the copied arm’s own validity.
    • CR2-F19-010: a refused cov-criteria setter (SetBitmask(NULL, n)) no longer clears the referenced-property-increment that was set. The new value is built first and committed only when it is accepted, so the arm and its ChangeOfValue parent stay valid and encode as before. SetReferencedPropertyIncrement does the same; it has no test of its own, because BACnetPrimitiveREAL::SetValue never refuses.
    • CR2-F19-011: BACnetEventParameterChangeOfTimer::InsertAlarmValueAndTakeControl refuses (and frees) an invalid alarm value, and SetUpdateTimeReference refuses an invalid reference and keeps the stored one. Before, both returned true and the arm silently became invalid. The finding is scoped to ChangeOfTimer: ChangeOfLifeSafety’s and ChangeOfCharacterstring’s alarm-value setters still accept an invalid element (and leave the arm invalid), and EveryListAlternativeAcceptsEmptyAndRefusesABadElement still pins that for them.
  • rb bs-20261005-22 (#2748):
    • CR2-F19-004: in a build with Event Enrollment but without Access Point objects, the BACnetEventParameter decoder now rejects access-event [13] as an unsupported tag (Reject invalidTag). Before, the factory’s missing class was reported as Abort out-of-resources with an “Out of memory” log. A WriteProperty of Event_Parameters still answers Error(PROPERTY, INVALID_DATATYPE) either way, because that path does not pass the decoder’s error on; the decoder’s reason and log are what change. No test build lacks Access Point, so this is verified by preprocessing (see the ledger), not by a unit test.
    • CR2-F19-005: BACnetEventParameter::GetMinimumMemoryCount() now counts the CHOICE wrapper’s own opening and closing tags (one octet each below tag 15, two from 15 up) and, for none [20], its two-octet primitive tag. Before, it returned only the arm’s count (0 for none), so a buffer sized from it was short by 2 to 4 octets. The test pins three representative arms (change-of-state [1], change-of-status-flags [18], none [20]) exactly, not every arm as the finding suggested; every arm’s own count was read and covers only its inner tags. The F18/F20 sibling wrappers (BACnetFaultParameter, BACnetNotificationParameters) have the same shape and are not changed here.
  • rb bs-20261005-23 (#2748 CR2-F19-014): BACnetEventParameterChangeOfValueCovCriteria has its own datatype, BACnetStackDatatypes::EventParameterChangeOfValueCovCriteria (appended at the end of the enum and gated on Event Enrollment, so no existing value moves). Before, it reported NotificationParametersChangeOfValueNewValue, so the cov-criteria and the notification’s new-value CHOICE each passed the other’s IsEqual datatype check and were static_cast to the wrong class; two of them holding the same Real compared equal.
  • rb bs-20261005-25 (#2750 CR2-F21-011 residual): a first-time AddEventEnrollmentObject* call that fails after its get-or-create no longer leaves a half-built Event Enrollment in the device’s map with no DB object. The affected calls are the OutOfRange, ChangeOfState (Boolean and Unsigned), ChangeOfValue, Extended and generic forms, and every failure return after the create now discards it. Every other refusal (instance range, Event_Type, notification class, the parameters’ own checks) happens before the create, so the reachable case is running out of memory in Setup; the discards before the algorithm-parameter and AddObject steps are defensive. One visible consequence: after a failed first-time Extended add, AddEventEnrollmentExtendedParameter on that instance now returns false (no such enrollment) instead of adding to the half-built one. Each records whether it created the entry and discards it on a later failure. A failed re-add of an enrollment that already existed keeps it, as before. The call’s return value is unchanged (false). The other residual, SetGeneralProperties failing after its commit point, stays with the owner (it is fed by the customer export BACnetStack_SetAlarmsAndEventsForObjectEnabled).
  • rb bs-20261005-26 (#3511): a confirmed request addressed to a remote broadcast (a DNET with a zero-length DADR, which a router normally receives as a unicast) is now treated as a confirmed request to a broadcast address. Per 135-2024 cl. 5.4.5.1 ConfirmedBroadcastReceived, it is neither answered nor rejected. Before, a well-formed ReadProperty sent as a remote broadcast to one of the stack’s virtual networks was answered with a ComplexACK by a virtual device. A malformed one reached BACnetNPDU::SetFromOnlyNetworkParameters, which could not build a reply source from the absent DADR and gave up through a chain of error logs. A new BACnetNetworkParameters::WasAddressedToABroadcast() is read by the four confirmed-request broadcast guards: the responding state machine’s two and HandleProcessingErrors’ two. The broadcast flag itself, reply addressing and the handling of unconfirmed requests (a remote-broadcast Who-Is is still answered) are unchanged.
    • Who sees it: this covers both a virtual network and a physical router’s own directly connected network. A confirmed request sent as a remote broadcast to the router’s port network is no longer answered by the router’s own device either; NoDadrAtAll_IsAnswered pinned the old answer and is now NoDadrAtAll_IsARemoteBroadcastAndIsNotAnswered, and the #437 ruling table’s “no DADR at all” row says so.
    • Not executed, not just not answered: the request is not executed at all. A WriteProperty, AcknowledgeAlarm or ReinitializeDevice sent as a remote broadcast to a virtual network used to run on a virtual device there, firing the host’s callbacks (CallbackSetProperty and the others). It no longer does. Only a client doing a non-conformant confirmed “write to everyone on network N” would notice.
  • rb bs-20261005-27 (#2746):
    • CR2-F17-027: CHANGE_OF_STATE and CHANGE_OF_CHARACTERSTRING condition (c) now restarts the time delay when the alarm value the monitored value matches changes while a (c) is pending. 135-2024 cl. 13.3.2 (c) and 13.3.16 (c) require it to “remains equal to that value for pTimeDelay”. Before, OFFNORMAL on one value, then a second value, then a third before the delay ended re-alarmed on the third value after only part of pTimeDelay, on the second value’s deadline. Condition (a), which accepts any alarm value, is unchanged.
    • CR2-F17-028: BACnetChangeOfStateEventAlgorithm::Reset() clears the status flags and both pending timers, as every sibling’s does. The constructor only calls Reset(), so they were uninitialised, and the first Run() after construction or Reset() without a Status_Flags argument reported indeterminate or stale fault/overridden/out-of-service bits. A later Run() without one still keeps the previous run’s flags, as every algorithm in the family does (pre-existing, noted on #2746). SetMonitoredValueBool now takes a bool, not a float.
    • CR2-F17-010: the OUT_OF_RANGE and FLOATING_LIMIT Reset() clear m_timeDelayTime. They used to clear m_timeDelay twice, so the offnormal timer was uninitialised after construction. This was masked: Reset() also sets the run state to noChange, which forces the timer to re-arm, so no test can observe it.
  • rb bs-20261005-28 (#2746 CR2-F17-012): the UNSIGNED_RANGE event algorithm no longer stores, serves or accepts a Deadband, and EnableIntrinsicUnsignedRangeAlgorithm no longer enables one. 135-2024 cl. 13.3.9 has no pDeadband, unlike UNSIGNED_OUT_OF_RANGE (cl. 13.3.15). The exported functions keep their deadband argument, which is ignored. This was inert on the wire: the Accumulator, the only object type mapped to it, has no Deadband in its profile, so the enable always failed silently.
  • rb bs-20261005-3: from the #2691 (family F13) roll-up:
    • CR2-F13-014: a BACnetWhoHasRequest is valid only once it holds its mandatory object CHOICE. SetDeviceInstanceRangeLimits used to mark it valid on its own, and Encode then dereferenced the NULL object and crashed.
      • Encode now Aborts when there is no object (the #1145 guard reached IsEqual and GetElements only).
      • Each new(std::nothrow) in the setters and the decode is now checked. In the copy path (operator=), a failed object copy now leaves the copy invalid.
      • The constructors set the limits and then the object, so their result is unchanged.
    • CR2-F13-015: BACnetWhoIsRequest::operator= now resets first, with a self-assignment guard, as BACnetWhoHasRequest::operator= does. Copying an unbounded Who-Is onto a bounded one used to keep the old limits, so a request meant for every device went to a range.
    • CR2-F13-007, -039: eight Decode entry points (six from the findings, plus Who-Has and Who-Is, found in review) now reset before their parameter check: TimeSynchronizationRequest (and the UTC subclass that inherits it), AddressBinding, DeviceAddressProxyTableEntry, ConfirmedPrivateTransferRequest, ConfirmedPrivateTransferACK, UnconfirmedPrivateTransferRequest, WhoHasRequest and WhoIsRequest. A refused buffer used to leave the previous value valid.
  • rb bs-20261005-30 (#2746 CR2-F17-006): an intrinsic alarm-and-event object leaves the device’s pending queue once its event algorithm has nothing pending.
    • When it applies: a re-evaluation returns noChange and the event algorithm has no time delay running (new BACnetEventAlgorithm::IsTimeDelayPending()). That covers a time delay cancelled because the value un-tripped, and a transition that committed but was not distributed because its Event_Enable bit is FALSE. Before, neither added or removed the entry, so the object stayed queued for good and Loop() re-read its monitored property through the host callback on every tick.
    • Why the algorithm is asked: several noChange returns never run the event algorithm - a Reliability-only first stage (a Reliability write or UpdateValue(reliability)), Event_Algorithm_Inhibit, Event_Detection_Enable FALSE, an update of a property only the fault algorithm monitors. A delay still running through them stays queued and fires on time.
    • A runError still keeps the object queued so the read is retried.
  • rb bs-20261005-31 (#2847 CR2-F25-005): the Life Safety Point and Life Safety Zone property profiles include the optional Floor_Number (Unsigned, read-only; Tables 12-18 and 12-19: “Floor_Number Unsigned8 O”), so a host can enable it; an enabled Floor_Number is read through the host’s CallbackGetPropertyUInt, as any other host-supplied property is (the Life Safety core does not own it). The test checks the profile entry, not an end-to-end read. The finding’s other two properties, Audit_Level and Auditable_Operations, were already added to every object type by #3448.
  • rb bs-20261005-32 (#2848 CR2-F26-005): the six lift-group decoders (BACnetAssignedLandingCalls, BACnetLandingCall, BACnetLandingCallStatus, BACnetLandingDoor, BACnetLandingDoorStatus, BACnetLiftCarCallList) reset the object before the null/empty-buffer check. A refused decode into a previously decoded object no longer leaves it reporting valid with its old content.
  • rb bs-20261005-32 (#2848 CR2-F26-003): BACnetLandingCallStatus::SetLandingCallStatusWithDirection / ...WithDestination called without floor-text clear a floor-text an earlier call set. Before, the stale text was still encoded as context [3].
  • rb bs-20261005-32 (#2848 CR2-F26-004): BACnetLiftCarCallList::InsertFloorNumber refuses a NULL or invalid floor number (not taken - the caller frees it), as the sibling lists do, and a refused insert no longer marks a populated list invalid.
  • rb bs-20261005-34 (#2849 CR2-F27-010, CR2-F27-015): BACnetLightingCommand::Decode, BACnetColorCommand::Decode and BACnetxyColor::Decode reset the object before the null/empty-buffer check, so a refused decode into a previously decoded object leaves it invalid instead of valid with its old content. The finding’s other half - the two command classes answering an out-of-range field differently - was mostly unified by #2815 (PROPERTY / VALUE_OUT_OF_RANGE); the one remaining case, an out-of-range target-color coordinate in BACnetColorCommand::Decode, kept the xyColor error only to overwrite it with Reject INVALID_TAG, and now answers PROPERTY / VALUE_OUT_OF_RANGE too.
  • rb bs-20261005-34 (#2849 CR2-F27-017): BACnetxyColor::Decode refuses a context-tagged or missing coordinate with Reject INVALID_TAG. Both fields of BACnetxyColor are application-tagged Real (cl. 21); before, a context tag of any number with a 4-octet length was decoded as a coordinate.
  • rb bs-20261005-35 (#2849 CR2-F27-011): BACnetColorCommand::SetTargetColor refuses an invalid xyColor and keeps the previous target-color; it used to return true and store the color marked invalid, silently dropping the field. The test-tool BACnetLightingCommand::CreateFromCompactString returns NULL when any field’s setter refuses it (an out-of-range target-level, priority and so on); it used to ignore the setters’ results and build the command without that field. So a test-tool WriteProperty of a Lighting_Command with an out-of-range field now fails locally instead of going out with the field missing.
  • rb bs-20261005-35 (#2849 CR2-F27-016): BACnetxyColor::SetxyColor checks both coordinates before assigning either and leaves the color as it was on a refusal; it used to store an in-range x and then mark the whole color invalid. SetXCoordinate / SetYCoordinate make the color valid once both coordinates are set; before, only SetxyColor did, so a color built coordinate by coordinate refused to encode.
  • rb bs-20261005-36 (#2849 CR2-F27-004): Lighting Output - a WARN_OFF or WARN_RELINQUISH (as a Lighting_Command or as the -3.0 / -2.0 Present_Value special values) at a lower priority than a running fade or ramp no longer snaps Tracking_Value to the fade’s target; it re-syncs only when idle, as #2348 did for RESTORE_ON and its group.
  • rb bs-20261005-36 (#2849 CR2-F27-005): Lighting Output - a Present_Value write that halts a fade or ramp but leaves Present_Value unchanged now runs the Transition from the halted Tracking_Value (cl. 12.54.19; NONE sets it at once). Before, In_Progress read IDLE with Tracking_Value != Present_Value (cl. 12.54.5) and the next tick jumped it whatever the Transition said. Visible effect: a duplicate same-value write (or relinquishing an empty higher slot) during a FADE_TO now restarts the fade from the halted point over Default_Fade_Time when Transition is FADE, and sets Tracking_Value at once when it is NONE. A STEP that halts a fade and is then ignored (Tracking_Value 0.0%) re-syncs Tracking_Value at once.
  • rb bs-20261005-37 (#2849 CR2-F27-012): Binary Lighting Output - Strike_Count (cl. 12.55.20) counts each OFF->ON change of Present_Value where it happens, in the re-evaluation after every priority-array change and egress resolution. Before, Tick() compared Present_Value with its value at the previous tick, so an OFF->ON->OFF between two ticks was not counted, and the count lagged a tick behind. The first Relinquish_Default (set when the object is created) is where Present_Value starts, so a Relinquish_Default of ON no longer counts a strike on the first tick; re-provisioning a live object (AddBinaryLightingOutputObject again; Relinquish_Default is not network-writable) from OFF to ON with an empty priority array turns the light on and counts, as any OFF->ON does.
  • rb bs-20261005-38 (#2846 CR2-F24-021): BACnetAccessToken::operator= no longer reports a copy valid when its scope copy failed. The scope invalidates itself when cloning an extended entry fails (#1203), and the token copy used to report the source’s validity anyway. SetScope likewise keeps the copy’s own validity and returns false for a failed copy; it used to re-mark the incomplete scope valid, so SetComplete() passed.
  • rb bs-20261005-38 (#2846 CR2-F24-022): BACnetAccessToken key-id [8] is Unsigned8 (cl. 21.6). SetKeyId refuses a value above 255 and keeps the previous key-id, and Decode refuses one with Reject PARAMETER_OUT_OF_RANGE, as BACnetAuthorizationEvent::Decode does for an out-of-range decision.
  • rb bs-20261005-39 (#2846 CR2-F24-016): BACnetAuthenticationFactor::SetValue refuses a value longer than 65535 octets. Its length was narrowed to the octet string’s uint16_t, so a 70000-octet value became 4464 octets and was accepted as valid. A Credential Data Input reader push (PushFactor, test-tool only) builds its factor through the same constructor and so goes through the same check; no test drives that path with an over-long value.
  • rb bs-20261005-39 (#2846 CR2-F24-023): BACnetCredentialAuthenticationFactor::SetDisable recomputes validity from both mandatory fields, as SetAuthenticationFactor does (#1206), so setting the factor first and the disable second gives a valid object. BACnetAssignedAccessRights::SetEnable checks that assigned-access-rights is set before it stores anything (a refused call used to store the enable), and SetAssignedAccessRights recomputes validity from both fields.
  • rb bs-20261005-4: from the #2691 (family F13) roll-up:
    • CR2-F13-036: ConfirmedPrivateTransfer Request, ACK and Error, and UnconfirmedPrivateTransfer Request, now refuse a vendor-id above 65535 with a PARAMETER_OUT_OF_RANGE reject code (a Reject PDU on the server side; on the client side, the ACK and Error decodes, the decode fails with that code). vendor-id is Unsigned16; it was decoded as a 32-bit Unsigned and narrowed by GetVendorId(), so 65537 rendered as 65537 and read back as 1. This changes what goes on the wire: BACnetConfirmedRequestProcessor echoes GetVendorId() into the ConfirmedPrivateTransfer-Error (#2508), so a request with vendor-id 65537 used to get an Error carrying vendor-id 1, and now gets Reject PARAMETER_OUT_OF_RANGE. No test drives this through the processor.
    • CR2-F13-016: the You-Are processor now ignores an invalid request, as every sibling processor has since #1244. It is the only one in the family that changes local identity. The Who-Am-I processor gets the same check; it has no observable effect there, since it has no callback and sends no reply.
    • CR2-F13-017: SetYouAreRequest and SetWhoAmIRequest invalidate the object first, as #848 did for SetIAmRequest. A failed rebuild used to leave a valid request mixing new and old fields.
    • CR2-F13-008: a failed CreateMessage on a Confirmed or Unconfirmed TextMessage request now leaves it invalid. BACnetTextMessage::Reset() clears the fields but not validity.
    • CR2-F13-028: BACnetReinitializeDeviceRequest::SetReinitializeDevice without a password now drops a previous one, as the DCC builder does since #847. Encode used to send the old password in a request meant to carry none.
    • CR2-F13-037: the DAB heartbeat’s range clustering compares inst - current.high against the tolerance. current.high + tolerance wrapped for a large tolerance, set through BACnetStack_SetBACnetStackSetting, so 0xFFFFFFFF (“one Who-Is for everything”) gave every entry its own Who-Is.
  • Left on #2691:
    • CR2-F13-019 is an owner question.
    • CR2-F13-027 (the ReinitializeDevice password SIZE(1..20) on decode, sharing the DCC check) is not done.
    • TestDecodeResetsFirstCR2F03.cpp‘s tests from this batch and bs-20261005-3 are now behind their classes’ STACK_OPTION_* gates. The file still does not compile in a minimal embedded build, because bs-20261005-2’s Optional* tests use classes that build compiles out; the per-class Optional test files share that, and CI builds the full target only.
    • The other unticked items.
  • rb bs-20261005-40 (#2846 CR2-F24-013): Access Door - a pulse-expiry relinquish that fails (out of memory) keeps the pulse timer armed at zero, so the next tick retries it. Before, the timer was disarmed and Reevaluate() re-armed a whole new pulse, leaving the door unlocked for another pulse time (sibling of #721).
  • rb bs-20261005-40 (#2846 CR2-F24-014): Access Door - the host’s Door_Status reaches the engine’s tick as the full 32-bit value. It was narrowed to 8 bits, so vendor values 1024 and 1025 were taken as CLOSED and OPENED by the open-too-long logic.
  • rb bs-20261005-40 (#2846 CR2-F24-015): Credential Data Input - a Present_Value write of a factor the object refuses is answered with PROPERTY / VALUE_OUT_OF_RANGE instead of being acknowledged, and a refused reader push (test-tool PushCredentialDataInputFactor) no longer runs the Update_Time value-update pipeline.
  • rb bs-20261005-41 (#2846 CR2-F24-017): Access Door - while Out_Of_Service is TRUE, the open-too-long recovery to NORMAL no longer runs, so a Door_Alarm_State written for testing stands on a CLOSED door (cl. 12.26.9(c): Door_Alarm_State “shall be writable to allow simulating specific conditions or for testing purposes”). Before, that recovery ran on every tick the door read CLOSED and overwrote the written value on the next Loop(). The open-too-long countdown still runs on the simulated Door_Status (cl. 12.26.9(d): functions that depend on Door_Status “shall respond to changes made to these properties”), so a simulated door held open raises DOOR_OPEN_TOO_LONG and a simulated close cancels the countdown. The device samples Out_Of_Service through the host getter each tick, as it does Door_Status; a declined sample is “in service”.
  • rb bs-20261005-43 (#2851 CR2-F29-016, CR2-F29-024, CR2-F29-033): BACnetValueSource, BACnetSetpointReference, BACnetShedLevel, BACnetAccumulatorRecord, BACnetStageLimitValue and BACnetVTSession decoders reset the object before the bad-parameter check, so a refused decode into a previously decoded object leaves it invalid instead of valid with its old content. (BACnetSetpointReference refuses only a NULL buffer - an empty one is its valid empty SEQUENCE.) BACnetVTCloseError, the other half of CR2-F29-033, already reset first (#3542).
  • rb bs-20261005-44 (#2851 CR2-F29-034): VT session identifiers are Unsigned8 (BACnetVTSession, cl. 21.6; the VTClose-Error production is not in the 135-2024 text, and its list holds the same identifiers). BACnetVTSession::SetLocalVtSessionId / SetRemoteVtSessionId refuse a value above 255 and keep the previous one. BACnetVTSession::Decode, and BACnetVTCloseError::Decode for each list-of-vt-session-identifiers element, reject one with Reject PARAMETER_OUT_OF_RANGE, as BACnetActionCommand::Decode does for priority.
  • rb bs-20261005-44 (#2851 CR2-F29-036): BACnetVTSession validity no longer depends on setter order. Every setter recomputes it from all three fields; before, the session became valid only when the address was set last, and never became invalid again. SetRemoteVtAddress refuses an invalid address.
  • CR2-F29-032 was already fixed at the tip: BACnetVTCloseError::Decode decodes its OPTIONAL list as optional and fails on a present-but-malformed one (#3542).
  • rb bs-20261005-45 (#2851 CR2-F29-002): BACnetHealth::Decode resets first and marks the object invalid on a refused (NULL or empty) buffer. Its Reset() leaves the object valid - a healthy default - so a refused re-decode used to keep the previous content valid. The other failure paths already cleared validity.
  • rb bs-20261005-45 (#2851 CR2-F29-017): BACnetValueSource::SetObject / SetAddress and BACnetSetpointReference::SetSetpointReference refuse an invalid child and leave the object as it was; they used to store it and report the object valid. No current caller supplies an invalid child (the WriteProperty Value_Source path always builds a valid source address), so there is no visible change to Value_Source.
  • rb bs-20261005-45 (#2851 CR2-F29-029): BACnetAccumulatorRecord’s setters mark the record valid once all four mandatory fields are set, as BACnetStageLimitValue’s do; before, no setter ever marked it valid.
  • rb bs-20261005-46 (#2851 CR2-F29-022): Load Control - the host-reported achieved value (a float) is range-checked before it is cast to the Unsigned PERCENT/LEVEL of Actual_Shed_Level. A non-finite value leaves Actual_Shed_Level unchanged; a negative one reads 0 and one past 4294967295 reads 4294967295. Before, each was an undefined float-to-uint32_t cast (observed on x86-64: NaN gave 0, -5.0 gave 4294967291, 1e12 gave 3567583232).
  • rb bs-20261005-46 (#2851 CR2-F29-027): Load Control - Shed_Levels refuses 0, which cl. 12.28.10 reserves for SHED_INACTIVE (“which is not represented in the Shed_Levels or Shed_Level_Descriptions arrays”). An element or whole-array write answers PROPERTY / VALUE_OUT_OF_RANGE and the test-tool BACnetStackTestTool_SetLoadControlShedLevels / SetShedLevels return false, each leaving the array as it was - a test-tool caller that passed a 0 level used to get true. The export’s doc also now cites cl. 12.28.18 for Shed_Levels (it said 12.28.16).
  • rb bs-20261005-5: from the #2691 (family F13) roll-up:
    • CR2-F13-005: a Confirmed or Unconfirmed TextMessage whose source is not a Device object is now Rejected (PARAMETER_OUT_OF_RANGE). cl. 16.5.1.1.1 requires the Object_Identifier of the initiating device’s Device object. Only the instance reached HookTextMessage, so a message from (Analog Input, 5) was reported as coming from device 5. The eight frames in the two TextMessage test files had encoded (Analog Output, 42) where they meant (Device, 42); they and the two expected XML strings are corrected.
    • CR2-F13-018: an I-Have now carries the Object_Name’s character set; it was always labelled UTF-8.
      • BACnetIHaveRequest takes the encoding, and GetObjectNameUsingObjectIdentifier returns it.
      • A Who-Has by name answers with the matched object’s own Object_Name and character set. The match compares octets, not character sets (found in review).
    • CR2-F13-026: a DeviceCommunicationControl time-duration above 65535 is now Rejected (PARAMETER_OUT_OF_RANGE). time-duration is Unsigned16; 65536 had become 0 minutes, which expires at once after the SimpleACK.
    • CR2-F13-027: a ReinitializeDevice password outside SIZE(1..20) is now Rejected (OTHER, as DeviceCommunicationControl does). An empty password used to be logged and accepted, and a longer one passed through.
      • The character count moved from DeviceCommunicationControl into BACnetPrimitiveCharSTRING::GetCharacterCount(), and both services share it with the 80-octet bound.
      • DeviceCommunicationControl still accepts an empty password, as before.
      • The check applies even when the device needs no password (16.4.1.1.2 says such a device ignores the parameter), as DeviceCommunicationControl’s (#1140) does: a password outside SIZE(1..20) makes the request non-conforming.
  • rb bs-20261005-50 (#3089 CR2-F02-010): BACnetDBDevice::Loop() no longer dereferences a NULL callbacks pointer when a backup or restore’s failure timer expires; it skips the host’s CompleteBackup / CompleteRestore callback and still resets the procedure. Loop(currentTime, NULL) is a legal call shape - every engine Loop runs already checks callbacks - and this branch crashed on it.
  • rb bs-20261005-51 (#3089 CR2-F02-072): BACnetStack_SetLiftHigherLowerDeck no longer leaks memory on every call that finds the elevator group’s stored Group_Members (32 bytes per call on x86-64 Linux). The identifier it removes from the list is now a stack object; RemoveObject() never deleted the heap-allocated one it was given.
  • rb bs-20261005-51 (#3089 CR2-F02-073): a whole-array read through BACnetStackObjectHelpers::GetArrayProperty (the Access Control, Life Safety, Load Control and Value Source array- and list-valued properties) no longer reports an empty array as a successful read when copying the elements fails for lack of memory; the read fails instead.
  • rb bs-20261005-52 (#3089 CR2-F02-042): a whole-array read of an application-served BACnetARRAY (State_Text, for example) whose application reports the array size and then declines one element is answered with the property’s Error (PROPERTY / VALUE_NOT_INITIALIZED) instead of Abort(OTHER), so a ReadPropertyMultiple no longer loses every other property over one element. #2164 made the same change for the size query. An error code the application names is used instead, as a single-index read of the element already did. In a test-tool build, an element the application serves as encoded bytes (the constructed-value fallback, e.g. a Staging object’s Stages) is refused the same way instead of being dereferenced as NULL - a whole-array read of such an array was a crash and is now an Error; an index read still serves it. A failed copy of an element (out of memory) is answered with Abort(OUT_OF_RESOURCES); it used to be dropped from the array silently.
  • rb bs-20261005-52 (#3089 CR2-F02-043): a ReadProperty of a Network Port’s FD_BBMD_Address, when the application reports a host-address type that is not a BACnetHostAddress choice, is answered with PROPERTY / VALUE_NOT_INITIALIZED instead of Reject(UNDEFINED_ENUMERATION) - the request was well formed, and a Reject replaced a whole ReadPropertyMultiple.
  • rb bs-20261005-54 (#3089 CR2-F02-025): a ReadProperty of the Device’s Configuration_Files (a BACnetARRAY, cl. 12.11.36) honours Property_Array_Index: index 0 returns the number of configuration files, 1..N one entry, and an index past the end PROPERTY / INVALID_ARRAY_INDEX. Every index used to return the whole list, so a backup client that sized the array with index 0 misread the reply. An entry that cannot be encoded (out of memory) fails the read with Abort(OUT_OF_RESOURCES) instead of being left out of the backup set.
  • rb bs-20261005-54 (#3089 CR2-F02-028): BACnetStack_SetIntrinsicChangeOfLifeSafetyAlgorithm and BACnetStack_SetFaultLifeSafetyAlgorithm no longer crash when given a NULL value array with a non-zero count while the other algorithm is already armed on the object. The cl. 13.4.4 overlap check now treats a NULL array as empty, as the setters themselves already did. A call that crashed now succeeds; no other return value changes.
  • rb bs-20261005-54 (#3089 CR2-F02-007): stored default values (Network Port Out_Of_Service; Trend Log Log_Interval and Trigger; Elevator Group Group_ID and Machine_Room_ID; Lift and Escalator Elevator_Group, Group_ID and Installation_ID; Lift Higher_Deck / Lower_Deck) are built in two checked steps by BACnetStackObjectHelpers::MakeStoredValue. Under allocation failure the nested form leaked the primitive, or stored an empty value and reported success. Trend Log Stop_Time and Client_COV_Increment, built in two steps already, now check the inner insert too. (The similarly shaped expressions in BACnetOutOfRangeFaultAlgorithm.cpp pass a temporary to SetValue, not a stored default, and are left.)
  • rb bs-20261005-55 (#3089 CR2-F02-058): a Lighting Output’s Event_State is optional, as Table 12-64 codes it (O1, required with intrinsic reporting). The profile made it required, so every Lighting Output reported Event_State as present (it was missing from Property_List all the same) and disarming intrinsic reporting could not remove it. BACnetStack_SetAlarmsAndEventsForObjectEnabled enables it when reporting is armed and disables it when disarmed. A Lighting Output without intrinsic reporting now answers Event_State with UNKNOWN_PROPERTY instead of asking the application.
  • rb bs-20261005-55 (#3089 CR2-F02-050, CR2-F02-062): the Device profile no longer defines SC_Primary_Hub_Connection_Status / SC_Failover_Hub_Connection_Status (Table 12-13 does not list them; they are Network Port properties), and the Audit Log and Audit Reporter profiles no longer define Out_Of_Service (Tables 12-83 and 12-82 do not list it). All were optional, so absent by default; enabling them now fails instead of listing a property the object type does not have.
  • rb bs-20261005-56 (#3089 CR2-F02-055): an Access Zone lists Occupancy_Count_Enable (read-only) alongside Occupancy_Count and Adjust_Value, which it already listed by default. Table 12-37 footnote 4 makes the three a set (“required if, and shall be present only if, the object supports occupancy counting”), and the engine already served the property and gated counting on it - a peer could see the count but not whether counting was enabled.
  • rb bs-20261005-56 (#3089 CR2-F02-056): a Global Group’s COVU_Period and COVU_Recipients are profiled writable, as Table 12-57 footnote 2 requires (“required to be writable if present”); both used to answer a WriteProperty with WRITE_ACCESS_DENIED. A COVU_Period write now reaches the application’s Unsigned set callback. COVU_Recipients has no typed set callback (a list of BACnetRecipient), so outside a test-tool build with the constructed-value callback a write of it is still refused.
  • rb bs-20261005-57 (#3089 CR2-F02-026): the Device’s Last_Restore_Time (cl. 12.11.37) records when a restore completes - an ENDRESTORE from PERFORMING_A_RESTORE that the application’s CompleteRestore callback accepts - from CallbackGetSystemTime, in the Device’s local time. It was never recorded and always read 1970-01-01 00:00:00; a device never restored now reads an all-unspecified date and time. The time is held in memory only: it reads unspecified again after a restart, and when backup and restore is re-enabled.
  • rb bs-20261005-57 (#3089 CR2-F02-031): an Access Door’s Secured_Status is UNKNOWN when Event_State cannot be read, as cl. 12.26.14 requires (“If the device cannot determine any of the previous conditions, then the property shall have a value of UNKNOWN”). It answered UNSECURED, which a security front end reads as an open or unlocked door.
  • rb bs-20261005-58 (#3089 CR2-F02-006): an MS/TP Network Port’s Max_Manager and Max_Info_Frames are writable whatever order the application enables WriteProperty and adds the port in, as cl. 12.56.55/.56 require (“If the device supports execution of the WriteProperty service, then this property shall be writable”). They were writable only if WriteProperty was enabled before the port was added. A device without WriteProperty refuses that service before any property is checked; a device with WritePropertyMultiple but not WriteProperty now accepts writes of the two through it, which the clauses allow.
  • rb bs-20261005-59 (#2844 CR2-F22-002, CR2-F22-015): Total_Record_Count of a Trend Log, Trend Log Multiple and Event Log wraps from 2^32-1 to one, as cl. 12.25.16 / 12.30.21 / 12.27.15 require (“the next value it takes shall be one”). It wrapped to 0, a sequence number the spec never assigns. The ReadRange sequence arithmetic is not yet wrap-aware (CR2-F22-003 and the rest of CR2-F22-015): By Sequence Number answers empty while the buffer straddles the wrap, as before, and - new with this change, because 0 is now skipped - ReadRange By Time and the response trim report a First Sequence Number one too high when the oldest record returned predates the wrap. Both need 2^32 records to reach.
  • rb bs-20261005-59 (#2844 CR2-F22-021): an Event Log logs at exactly its Start_Time - cl. 12.27.8, “Local_Time is on or after Start_Time”. The comparison was strict, so a notification offered at Start_Time was refused and a LOG_DISABLED record written.
  • rb bs-20261005-6: DeviceCommunicationControl and TimeSynchronization, from the #2691 (family F13) roll-up:
    • CR2-F13-023: a DCC Time Duration now runs on the host’s monotonic clock (CallbackGetSystemTimeMs) when it is registered, and on the wall clock otherwise.
      • Before, the deadline was an absolute wall-clock time. Stepping the clock back, for example through an unauthenticated TimeSynchronization or an NTP step, stretched the disable; stepping it forward ended it early.
      • On the monotonic path the deadline is a 64-bit millisecond count, which no 32-bit time_t can wrap. The wall-clock fallback still holds it as time_t, so on a target with a 32-bit time_t a post-2038 deadline can still wrap; that is left.
      • GetDisableTimeout() is now the wall-clock deadline only, and is 0 while a monotonic deadline runs. No caller in source/ uses it; HasDeviceCommunicationControlExpired is the question to ask.
      • The four copies of the expiry check (BACnetDataLink, BACnetDataLinkLayer, BACnetDataLinkIPv4) now call one BACnetDBDevice::HasDeviceCommunicationControlExpired.
    • CR2-F13-025: DCC ENABLE with a Time Duration is now accepted on a device without a clock. cl. 16.1.1.1.1: “The ‘Time Duration’ parameter shall be ignored and the time period considered to be indefinite if the ‘Enable/Disable’ parameter has a value of ENABLE.” A host with only the monotonic clock also counts as having a clock.
    • CR2-F13-029: a host’s DCC errorCode now goes through an allow-list, as ReinitializeDevice’s does. OPTIONAL_FUNCTIONALITY_NOT_SUPPORTED and SERVICE_REQUEST_DENIED are sent as SERVICES; anything else becomes SECURITY / PASSWORD_FAILURE. Before, any code was echoed as SERVICES/, including an Error-PDU carrying “success”.
      • This is a change a host can see. A callback that wrote configurationInProgress, internalError or another code now has it sent as SECURITY / PASSWORD_FAILURE. The frozen-header doc of BACnetStack_RegisterCallbackDeviceCommunicationControl (and its generated copies) still describes the old echo, and FPCallbackDeviceCommunicationControl has no \param errorCode. Both are in headers this batch may not edit; that is #3549 (needs-human-review).
    • CR2-F13-004: TimeSynchronization and UTCTimeSynchronization now refuse a date the month does not have, such as 2026-02-30 or 2026-04-31. Before, such a date reached CallbackSetSystemTime, or on the UTC path was normalised by timegm to a date nobody sent. On the TimeSynchronization path the weekday given to the host is now computed from the date; the UTC path already did so in CalculateTimeUsingUTC.
  • rb bs-20261005-60 (#2844 CR2-F22-033): the Annex AA time-series CSV codec checks column names as UTF-8 (AA.1.1: the file is UTF-8 and names are “nonempty printable strings, each limited to 80 characters”): a printable non-ASCII name is accepted, the 80 limit counts characters rather than bytes, and a malformed sequence or a C0 control, DEL or C1 control is refused. A leading UTF-8 byte order mark no longer fails the DateTime header check.
  • rb bs-20261005-60 (#2844 CR2-F22-034, CR2-F22-035): AppendDataColumn refuses a column once records exist (the encoded rows were ragged, and the codec’s own DecodeCsv rejected them) and a duplicate name, DateTime included (AA.1.3: the generating software shall enforce unique column names by default). The class comment no longer claims strict AA.1 conformance: ascending row order and time-zone indicators are the caller’s.
  • rb bs-20261005-60 (#2844 CR2-F22-038): a missing-value token whose error code does not fit an unsigned int (? 4294967296) is not a missing value; it parsed to 0, the “not missing” value, on LP64 and to 4294967295 on a 32-bit long.
  • rb bs-20261005-60 (#2844 CR2-F22-039): DecodeCsv reports a blank first line as a missing header (it said the first column was not DateTime), and the codec includes <cstdio> for its std::snprintf calls.
  • rb bs-20261005-61 (#2844 CR2-F22-003, CR2-F22-015): ReadRange of a Trend Log, Trend Log Multiple or Event Log Log_Buffer works across the Total_Record_Count wrap. Sequence numbers run 1 .. 2^32-1 and wrap to one (cl. 12.25.14: clients must handle “the case where Total_Record_Count has wrapped back to one”), and the arithmetic now runs on that ring: a By Sequence Number request answered empty for every reference while the buffer held records from both sides of the wrap, since bs-20261005-59 (wrap to one) By Time numbered the oldest record one too high there, and a trimmed ReadRange-ACK could step onto 0. Records_Since_Notification of a BUFFER_READY (Event Log, Trend Log) was likewise one too high across the wrap. Before any wrap the results are unchanged.
  • rb bs-20261005-62 (#2844 CR2-F22-017, CR2-F22-025, CR2-F22-027): an Event Log, Trend Log or Trend Log Multiple record whose datum, log data or timestamp is invalid is refused - for Event Log and Trend Log before anything changes; Trend Log Multiple refuses it at the insert (with one slot left in a stop-when-full log it still writes LOG_DISABLED first, as for a valid record). It was stored and counted in Total_Record_Count (BUFFER_READY counted it too), and every ReadRange covering it aborted until the buffer turned over. BACnetLogMultipleRecord takes control of its log data before setting the timestamp, so a timestamp that does not convert no longer leaks it.
  • rb bs-20261005-62 (#2844 CR2-F22-024): BACnetLogDatum::GetMinimumMemoryCount counts the opening and closing tags of the failure and any-value arms. Each such record was estimated two octets short, which could send a ReadRange-ACK unsegmented above the requester’s Max_APDU_Length_Accepted.
  • rb bs-20261005-62 (#2844 CR2-F22-029): BACnetLogData::SetLogDataFromString leaves the object unchanged when the string is rejected (it reset the object first), as BACnetLogDatum::SetLogDatumFromString already does.
  • rb bs-20261005-63 (#2844 CR2-F22-006): a write of TRUE to the Trigger property of a Trend Log or Trend Log Multiple whose Logging_Type is not TRIGGERED is refused with PROPERTY / NOT_CONFIGURED_FOR_TRIGGERED_LOGGING (cl. 12.25.29, 12.30.16). The stack does not log on Trigger outside triggered mode, yet it accepted the write and left Trigger TRUE.
  • rb bs-20261005-63 (#2844 CR2-F22-026): BACnetLogData::Clone copies a log-status longer than 3 bits. It returned NULL, so such a record could not be copied.
  • rb bs-20261005-63 (#2844): InsertTrendLogRecord no longer leaks the bit string it builds from the status flags on every call that passes them.
  • rb bs-20261005-64 (#2850 CR2-F28-010): reading a Schedule’s List_Of_Object_Property_References with an array index is refused with PROPERTY / PROPERTY_IS_NOT_AN_ARRAY at the object, as for the Calendar’s Date_List; it is a BACnetLIST. Index 0 read as a count and index N as an element. (WriteProperty/ReadProperty over the wire already refused the index before reaching the object.)
  • rb bs-20261005-64 (#2850 CR2-F28-020): converting a time_t whose year a BACnet Date cannot hold (after 2154) to a BACnet date-time fails; it was narrowed to a wrong, earlier year (2200 became 1944). Converting a date-time that does not fit in a time_t (a 2038+ date on a 32-bit time_t build) fails rather than returning 1969-12-31T23:59:59. Every caller of that conversion sees the failure: a BACnetDateTime constructed from such a time_t is left unspecified, and a Timer whose expiry would fall after 2154 reports Expiration_Time unspecified (it reported the start time). A Timer’s expiry arithmetic no longer overflows a 32-bit time_t near its maximum; that guard, and the time_t-does-not-fit check, need a 32-bit time_t and are not exercised by the x64 test suite.
  • rb bs-20261005-65 (#2746 CR2-F17-013): an event algorithm’s Time_Delay and Time_Delay_Normal now expire at the deadline. The condition had to hold one second longer than the configured delay (Time_Delay 1 took 2 s), in every algorithm that applies a time delay (15 algorithms). Cl. 13.3.6: Time_Delay is “the time, in seconds, that the offnormal conditions must exist before an offnormal event state is indicated”. Integrators who tuned a delay to the old timing see every delayed transition one second earlier.
  • rb bs-20261005-65 (#2850): a Timer object’s Update_Time before its first transition, and its Expiration_Time in IDLE, are the unspecified datetime (cl. 12.57.12, 12.57.14). They held a zeroed, invalid date, so reading either property aborted.
  • rb bs-20261005-66 (#2850): a Binary Lighting Output’s Time_Of_Active_Time_Reset and Time_Of_Strike_Count_Reset are the unspecified datetime until the first reset (cl. 12.55.19, 12.55.21). They held an invalid date, so reading either property aborted. The same for a Load Control’s Start_Time with no request pending: it was all X’FF’ but never marked valid, so a read of it aborted.
  • rb bs-20261005-67 (#2851 CR2-F29-028): an Averaging object whose device clock is set back (Time Synchronization, an NTP correction) re-arms its sampling one period from the new time. It took no samples until the clock reached the old schedule again, so the window went stale for the size of the jump.
  • rb bs-20261005-67 (#2851 CR2-F29-035): a BACnetActionList (a Command object’s Action element) is valid only when every command in it is; one that cannot be encoded is refused before encoding starts, not part-way through a response. An empty action list is valid.
  • rb bs-20261005-67 (#2851 CR2-F29-025): BACnetScale and BACnetPrescale - the Accumulator’s Scale and Prescale property types (cl. 12.61.11, 12.61.13) - are built when the Accumulator object is, not only with the Pulse Converter object; a build with the Accumulator and without the Pulse Converter could not construct or decode them.
  • rb bs-20261005-68 (#2747 CR2-F18-012): comparing a BACnetFaultParameter or BACnetEventParameter alternative with its CHOICE wrapper (or the wrapper with an alternative) returns “not equal”. Both report the same datatype, so each side cast the other to its own type and made a virtual call through the wrong class - undefined behaviour that crashed in the test build. The alternatives now share a class identity the wrapper does not have (GetClassIdentity(), #2871’s RTTI-free check).
  • rb bs-20261005-68 (#2845 CR2-F23-022): a stack-held Audit Reporter no longer serves Enable, which is not an Audit Reporter property (cl. 12.63, Table 12-82); a ReadProperty of it is answered by the object’s profile.
  • rb bs-20261005-69 (#2851 CR2-F29-037): a Program object’s Reason_For_Halt holds a vendor value above 255 (cl. 21.6 allows 64-65535 for others). BACnetStackTestTool_SetProgramState’s reasonForHalt is now uint16_t, as are the internal setter chain and the stored value; a value of 256 or more was truncated to 8 bits (300 became 44, which was then refused as the reserved range). The Node binding passes the whole value.
  • rb bs-20261005-7: decode and insert validity in the alarm-and-event services, from the #2694 (family F16) roll-up:
    • CR2-F16-006, -011: BACnetLifeSafetyOperationRequest, BACnetLifeSafetyOperationInfo, BACnetConfirmedEventNotificationRequest, BACnetUnconfirmedEventNotificationRequest and their shared BACnetEventNotificationRequest base (whose own Decode is reached only through the two subclasses) now reset before the NULL/zero-length check in Decode. Before, a reused object that was refused an empty buffer kept the previous decode’s contents and IsValid() == true.
    • CR2-F16-031, -037: a BACnetGetAlarmSummaryACK or BACnetGetEventInformationACK whose decode fails part-way is now invalid. Reset() marks the empty ACK valid, and the failure returns never cleared that, so a truncated ACK read as a valid, partial list. This is the #862 fix BACnetGetEnrollmentSummaryACK already had; as there, the partial list is kept but the ACK is invalid. A NULL buffer now also clears the previous list, where before it only cleared the validity.
    • CR2-F16-031, -041: InsertAlarmSummary, InsertEventSummary (both overloads) and InsertEnrollmentSummary now return false, and list nothing, when the summary they construct is not valid. Before, they always returned true, and the invalid summary failed the whole ACK’s encode later. Only InsertEventSummary can be given such a value today (an unset time stamp); the other two cannot produce an invalid summary from their parameter types, so for them the check is a guard.
    • CR2-F16-025: BACnetEventNotificationSubscription::SetRecipient now refuses an invalid recipient without replacing the one it holds. Before, it stored the invalid recipient and then returned false.
  • rb bs-20261005-70 (#2851 CR2-F29-021): a Load Control object’s Expected_Shed_Level is the level the host’s CallbackLoadControlShed reports it expects to achieve (its expectedValue output), on every tick the host answers (cl. 12.28.16). It was always a copy of Requested_Shed_Level. The host is consulted from Start_Time on, so before Start_Time Expected_Shed_Level is still the request (asking the host earlier - PrepareToShed - remains open). A host that leaves expectedValue as pre-filled, or no callback, keeps the exact request.
  • rb bs-20261005-70 (#2747 follow-up): writing a bare BACnetFaultParameter / BACnetEventParameter alternative (not wrapped in its CHOICE) to an Event Enrollment’s Fault_Parameters or Event_Parameters is refused as INVALID_DATA_TYPE. It was cast to the wrapper type - undefined behaviour, a crash in the test build. The wire decoder only builds wrappers; this guards direct callers.
  • rb bs-20261005-8: GetEventInformation and Notification Forwarder hygiene, from the #2694 (family F16) roll-up:
    • CR2-F16-038: GetEventInformation now skips a NULL entry in the alarm-and-event map, as its Event Enrollment loop already did. Before, it ended the request with no error set and no response. The stack never stores a NULL entry today, so the skip is a guard; the test plants one.
    • CR2-F16-039: both GetEventInformation loops now use the one EventInformationObjectQualifies the lookahead uses, so the cl. 13.12.2 inclusion criteria cannot drift between them. A write-only markerFoundInIntrinsicMap flag and a stale ToDo were removed.
    • CR2-F16-022: the Notification Forwarder’s two network-fed bounds, CAS_BACNET_MAX_SUBSCRIBED_RECIPIENTS (32) and the pending-delivery cap (1000), are pinned to literals in EverySecurityBoundIsPinnedToAConcreteValue, and the two cap tests assert the literal rather than the constant. The header comment on GetMaxPendingDeliveriesForTest, which argued against a literal, now points at the pin.
  • rb bs-20261005-9: GetEnrollmentSummary and event-notification decoding, from the #2694 (family F16) roll-up:
    • CR2-F16-035: GetEnrollmentSummary now reports, for each object, the Priority its Priority Filter judges: that of the most recent transition (TO_NORMAL in NORMAL, TO_FAULT in FAULT, TO_OFFNORMAL otherwise; #3242 made the filter use it). Before, it reported TO_OFFNORMAL whatever the state, so a summary could pass a filter with a Priority outside it. A client can see this (of a test-tool build, or any build that enables STACK_OPTION_AE_ENROLLMENT_SUMMARY; customer firmware does not link the service): an object in NORMAL now reports its TO_NORMAL priority.
    • CR2-F16-036: an intrinsic object now reports its event algorithm’s Event Type (NONE without one), and the Event Type Filter judges that. cl. 13.11.1.2.1.2: the Event Type “indicates the Event_Type that the object can generate”. Before, every intrinsic object reported NONE, so a filter for, say, OUT_OF_RANGE excluded an Analog Input that runs OUT_OF_RANGE. A client can see this too, under the same build condition. The BACnet testing tool’s IUT fixture models every intrinsic object as Event Type NONE, so its BTL 9.7.2.3 (Event_Type_Filter) row needs a matching fixture change; that is the follow-up issue named in the RESULTS comment.
    • CR2-F16-010: an EventNotification whose Initiating Device Identifier is not a Device is now rejected (parameterOutOfRange). cl. 13.8.1.1.2 and 13.9.1.1.2: it “shall convey the Device Object_Identifier of the device that initiated” the request. Before, (analog-input, 5) was accepted and reported as device 5. This is the same refusal TextMessage got in #2691 CR2-F13-005. This one reaches customer builds: such a ConfirmedEventNotification is now rejected, and an unconfirmed one dropped.
    • CR2-F16-040: a GetEnrollmentSummary-ACK summary whose priority is above 255 is now rejected (parameterOutOfRange); clause 21 gives it as Unsigned8. Before, GetPriority() truncated it, so 256 read as 0, the highest priority. The EventSummary half is left: GetEventInformation-ACK’s event-priorities are plain Unsigned in the grammar, and its accessors are still uint8_t.
  • spec-coverage (#2887): property-profiles/Device.tsv is rebuilt from the clause text. It had lost the last 9 rows, and it now corrects two spec typos: Auhorization_Policy and Authorization_Scopes become Authorization_Policy and Authorization_Scope.
  • spec-coverage (#2887): every other property-profiles/*.tsv is rebuilt from the clause text as well, adding 587 missing rows across 62 object types; every existing row came out unchanged. Color Temperature is kept as it was, because the extraction has no rows for Table 12-87. spec-table.cjs now handles headers split across lines, rows printed on a single line, and single-line footnotes.
  • ci (#2887): wiki.cjs did not read object-table property names that contain digits, such as BACnet_IPv6_Mode. spec-table.cjs now splits two rows printed on one line, and accepts conformance codes printed with a space (O1, 2). Those two faults had dropped rows from 11 profile TSVs, including Event Log’s Stop_Time and Stop_When_Full. NetworkPort.tsv now lists the sub-table properties.
  • ci (#2887): the verbatim check no longer scans generated text (index pages and “Used by” lists), where alphabetical lists of object names matched the spec’s own lists.
  • rb e-20260924-151 (#2533): the BVLL BDT and FDT entry codecs took their buffer capacity as uint8_t, so the remaining length of a larger message wrapped at 256. A BBMD with 25 or more FDT or BDT entries could not send Read-FDT-Ack / Read-BDT-Ack, and a 26-entry table could not be decoded. The capacity parameters are now uint16_t.
  • rb e-20260924-151 (#2546): a PDU that failed to decode was answered with a Reject-PDU or Abort-PDU carrying invoke id 0, whatever its type (I-Am, SimpleACK, ComplexACK, Error, Reject, Abort). 135-2024 cl. 20.1.8: “Only confirmed request APDUs may be rejected (see Clause 18.9).” The stack now replies only to a malformed confirmed request, and only when it was not received as a broadcast, matching how a well-formed broadcast confirmed request is ignored. On MS/TP, a frame that draws no reply is no longer passed on to APDU processing after its decode failed.
  • rb e-20260924-152 (#2751): a confirmed request that reached a device as a broadcast relayed between BBMDs (BVLL Forwarded-NPDU or Distribute-Broadcast-To-Network) or as a global broadcast (DNET X’FFFF’) in a unicast frame was not treated as a broadcast. So every CAS device on the remote subnets answered it, or sent a Reject for a malformed one. Those arrivals are now flagged as broadcasts. The responding state machine ignores the request (135-2024 cl. 5.4.5.1 ConfirmedBroadcastReceived), and a malformed one draws no Reject. J.2.5: “This BVLL message is used in broadcast messages from a BBMD as well as in messages forwarded to registered foreign devices.” Unconfirmed requests are still processed.
    • Visible side effect: a Who-Is (or Who-Has) carrying an SNET that arrives in a Forwarded-NPDU is now answered with a remote-broadcast I-Am (DNET = SNET, DLEN 0) instead of one addressed to the requester’s SADR. That is what an Original-Broadcast Who-Is already got, and cl. 16.10.4 allows it.
    • Not covered by a test: the X’FFFF’ flag on the BACnet/SC path (ProcessNPDUOnly); the tests cover the IPv4 path.
    • #1104’s regression test changed vehicle: its Forwarded-NPDU frame is now correctly ignored (135.1 10.8.3.5.1), so an MS/TP unicast test gates #1104 instead.
  • rb e-20260924-152 (#2542): on a router with two MS/TP ports bound to two MS/TP data-link instances, a global broadcast went out on only the first MS/TP network, because every MS/TP port reported the same data-link instance id. Each bound MS/TP port now reports its own id, as IPv4 and BACnet/SC ports already do (135-2024 cl. 6.3.2: “the router shall broadcast the message on all directly connected networks except the network of origin”). Two MS/TP router ports sharing one instance is a misconfiguration the stack already logs; they now both report unbound, so the broadcast goes out twice on that bus, as for IPv4.
  • rb e-20260924-153 (#2515): a virtual router on BACnet/SC now sends the Reject-Message-To-Network (reason 1) for a unicast NPDU to an unknown DNET. #1104’s fix reached only the IPv4 and MS/TP data links; the four SC callers of ProcessNPDUOnly logged an error and sent nothing. A remote broadcast (DLEN 0) to an unknown network, and on SC a frame sent to the broadcast VMAC, draw no reject (135-2024 cl. 6.6.3.1: “If the NPCI indicates either a remote or global broadcast, the message shall be processed as described in Clause 6.3.2.”). The DLEN-0 exemption applies on IPv4 and MS/TP too. It suppresses only the reject: on a gateway that is also a physical router, a remote broadcast to one of its own real networks is still delivered locally. The virtual router’s reject now goes through the same per-source rate limiter as the physical router’s (#471: a burst of 4, then 1 per second per source, keyed on the true UDP source or the SC connection’s peer). The limiter exists only in builds with STACK_OPTION_ROUTER_PHYSICAL (on in STACK_OPTION_TARGET_FULL); a virtual-router-only gateway build sends the reject unlimited (#2896). These SC frames no longer log “Error occurred when processing the NDPU” at Error level. A test now also covers #2751’s DNET X’FFFF’ broadcast flag on the SC path.
  • rb e-20260924-153 (#2512, security): the rate limiter for the unknown-network-message-type Reject-Message-To-Network is now keyed on the true UDP source of a Forwarded-NPDU. It was keyed on the Originating Device address, which the sender writes, so one host could vary that field and get unlimited rejects aimed at addresses it chose. #471’s ruling: “rate-limiting Reject-Message-To-Network per true UDP source”. The reject is still sent to the originator (J.2.5).
  • rb e-20260924-153 (#2513): a physical router’s Reject-Message-To-Network for an IPv4 Forwarded-NPDU now goes to the originating device named by the Forwarded-NPDU, not to the BBMD that relayed it. The rate-limit key stays on the BBMD (the true UDP source). This is the other half of #471’s ruling (135-2024 cl. 6.4.4: “It is directed to the node that originated the message being rejected, as indicated by the source address information in that message.”). On BACnet/SC the reject still goes to the connection’s peer (#301). The behaviour and its reflection bound are recorded in docs/CAS BACnet Stack - Routing PICS Deviations.md §12.
  • rb e-20260924-153 (#2516, security): an NPDU that fails to decode because its SLEN is invalid no longer supplies the address for its reason-6 reject. A frame with SNET 5 and SLEN 7 drew a reject routed to DNET 5 with DLEN 7, and 7 octets were copied out of the 6-octet SADR array. The reject now goes to the ingress peer as a local answer, and IsRejectRoutableToSource refuses an SLEN above 6.
  • rb e-20260925-154 (#2480): a nested empty constructed element, such as [0] opening immediately followed by its closing tag (0E 0F), now decodes inside a BACnetAbstractSyntaxType value. It failed with no error set, while the top-level empty case was accepted (cl. 20.2.1.6(d): “Following this tag shall be a sequence of zero or more tagged elements, followed by the closing tag of the pair with length/value/type = 7.”). A nested element that really fails without giving a reason now sets Reject invalid-tag.
  • rb e-20260925-154 (#2484): Decode() on BACnetPrimitiveENUM, INT, UINT, UINT64 and ObjectIdentifier now rejects a declared length of 65536 or more with Reject invalid-tag. It narrowed the length to 16 bits before the range check, so 65537 was read as 1 (and 65540 as 4 for ObjectIdentifier) and the value decoded as valid. The form is the one cl. 20.2.1.3.1 gives for “Data length in octets ranging from 65536 to 232-1 (inclusive)” (2^32-1; the extracted text drops the superscript).
  • rb e-20260925-155 (#2476): a value decoded with an unknown datatype no longer loses the elements decoded before a constructed element. DecodeByDatatype re-entered this->Decode for the constructed element, and its Reset() deleted what was already there, so [1] 5 followed by [2]{ 1 } came back holding only [2]{ 1 } and still reported success. The constructed element is now decoded into a fresh value whose elements are appended. A constructed element that fails to decode now resets the whole value, and running out of memory there sets Abort(other).
  • rb e-20260925-155 (#2904): a context-tagged BACnetPrimitiveUINT64 of 2^40 or more now encodes its 6- or 7-octet length in the extended form (1D 06 ..., 1D 07 ...) instead of as an opening tag (1E ...). cl. 20.2.1.3.1: “Data length in octets ranging from 5 to 253 (inclusive) shall be encoded by setting the length/value/type field of the initial octet to B’101’”; B’110’ and B’111’ are the opening and closing tag types (cl. 20.2.1.3.2). SetValue() now marks any length of 5 or more extended.
  • rb e-20260925-156 (#2893): over BACnet/SC, a request to a device on a remote network (DNET) sent on a hub connection now goes to the router’s VMAC instead of going out with no Destination Virtual Address, which the hub could not forward. The router’s VMAC is learned from I-Am-Router-To-Network and from the Originating Virtual Address of frames carrying that network as their SNET (at most 32 networks). With no router known, the request uses the Local Broadcast VMAC (cl. 6.5.3: “the MAC address of the BACnet router corresponding to the DNET parameter or the appropriate broadcast DA if the address of the router is initially unknown”). A VMAC named in the connection string (uri;VMAC) still takes precedence; local requests and direct connections are unchanged.
  • rb e-20260925-157 (#2752): a ReadRange-ACK for a By Position read of a Trend Log, Event Log or Trend Log Multiple Log_Buffer no longer carries First Sequence Number (context tag 6). #2221 had added it, citing 15.8.2 as requiring it for every Range form; the extracted 135-2024 text says otherwise (15.8.1.2.7: “This parameter is only included if the ‘Range’ parameter of the request was of the type ‘By Sequence Number’ or ‘By Time’ and ‘Item Count’ is greater than 0.”), and the By Position example in 15.8.1.1.4.1.4 ends “The ‘First Sequence Number’ shall not exist in the response.” By Sequence Number and By Time reads still carry it; the #2629 comments quoting 15.8.1.2.7 and 15.8.2 were checked against the extracted text and are correct.
  • rb e-20260925-157 (#2343): a Schedule’s or Timer’s write to a remote property reference is no longer lost when the target device is not yet bound in the Device Address Binding table (no I-Am heard yet, as at startup). The write is kept, one per reference with the latest value replacing an earlier one (at most 256 references per device table), and sent when that device’s I-Am binds it. Before, it was logged and dropped, and the target stayed wrong until the Schedule’s Present_Value next changed. Writes at different priorities to the same reference are kept separately. Kept writes are discarded only when the device’s Device Address Binding entry is released; deleting a Schedule or Timer does not yet drop its kept writes (follow-up #3009). WriteGroup writes are unchanged.
  • rb e-20260926-158 (#2482): BACnetObjectBase::EncodeWithContext (a constructed value wrapped in a context tag) now reports running out of buffer as Abort(segmentationNotSupported), as BACnetPrimitiveBase::EncodeWithContext already did (#1058): with no room at all (was Abort(other)), and when the opening or closing context tag does not fit (was no error at all, since BACnetTag::Encode sets none). BACnetStackMemoryBuffer::Insert grows its buffer and retries only on that reason, so a response whose size estimate ended exactly on such a tag failed instead of growing. A NULL buffer is still Abort(other), and a wrapped value that fails to encode without setting an error now gets the #1058 Abort(other) backstop.
  • rb e-20260926-158 (#2487): a host CallbackGetPropertyCharString that reports a string exactly as long as the capacity it was given (256 characters on a FULL build, 64 on EMBEDDED) now fails the read, as one character more already did (#620). BACnetPrimitiveCharSTRING::SetValue holds at most capacity - 1 characters, so that string became an invalid, empty value that GetProperty reported as a success and the response failed later at encode. Any value the CharacterString refuses (also a reserved character set) is now refused before it is inserted. A string one character shorter reads back whole.
  • rb e-20260926-158 (#2477): a WriteProperty (or any other SetProperty path) that gives more than one value for a property that is neither a list nor an array, such as [3]{REAL 100, REAL 5} to an Analog Value’s Present_Value, is now refused with INVALID_DATATYPE instead of writing the first value (135-2024 15.9.1.3.1: “The datatype of the value provided is incorrect for the specified property.”). Properties of datatype Any, proprietary properties and values the stack decodes tag by tag are unchanged. A single value is written as before. An array element written by index is not yet held to one value.
  • rb e-20260926-160 (#2552, duplicate #3095): every Network Port (IPv4, Ethernet, MS/TP, Virtual and BACnet/SC) now has Command (417) present in Property_List and writable by default. Every port executes DISCARD_CHANGES (#2326), and 135-2024 Table 12-71 footnote 3 says Command is “Required to be present if, and only if, the object supports execution of any of the values of the Command property. If present, this property shall be writable.” Before, a WriteProperty of Command returned UNKNOWN_PROPERTY unless the host enabled it by hand. Through WriteProperty, DISCARD_CHANGES now returns Result(+) and IDLE returns PROPERTY / VALUE_OUT_OF_RANGE.
  • rb e-20260926-161 (#2557): DISCARD_CHANGES written to one BACnet/SC Network Port no longer discards the pending certificate bindings of every other BACnet/SC port. Only the port written is reverted.
  • rb e-20260926-162 (#3001): a Confirmed-Request sent over BACnet/SC is no longer segmented once its APDU passes 50 octets. BACnetSCPacket now carries a 1476-octet max APDU length (BACNET_MAX_APDU_LENGTH_SC), as BACnetIPPacket does, so a request up to 1476 octets goes out in one APDU (135-2024 Table 6-1, cl. 20.1.2.5). Writes such as a PEM certificate to a hub’s File object no longer draw Abort segmentation-not-supported from a peer that does not accept segmented requests.
  • rb e-20260927-164 (#2913): an empty Loop Setpoint_Reference (135-2024 cl. 12.17.16, zero references: the empty SEQUENCE) in a WriteProperty or in a received ReadProperty / ReadPropertyMultiple ACK now decodes in every build with the Loop object, customer builds included. Before, it was dropped with no reply. A malformed value is still rejected.
  • rb e-20260928-166 (#2478): a property value typed ANY kept only its first element when decoded and still reported success. A Color object’s Default_Color (a BACnetxyColor) lost its y-coordinate, and a Staging object’s Stages entry collapsed to its first REAL. Every element of the value is now kept, in wire order (135-2024 cl. 20.2.19: “The encoding of an ANY type shall be the complete encoding specified in this standard for the type of the value substituted for the placeholder ANY”).
  • rb e-20260928-166 (#2485): the enumeration factory used to type a received ENUMERATED value fell through to later object types’ lists when a property had no entry of its own. For example, an enumerated Schedule or Channel Present_Value (cl. 12.24.4: “any primitive datatype”) decoded and rendered as a BACnetLifeSafetyState, and the class it produced depended on the build options. Such a value is now decoded as an unknown enumeration.
  • rb e-20260928-166 (#2488): a BACnetNameValueCollection, or a Tags array, holding a member with a name but no value (cl. 21, value ABSTRACT-SYNTAX.&Type OPTIONAL) failed to decode as a whole. This included the stack’s own encoding of such a collection. The tag that follows the name, whether the next member or the closing tag, is now read as “value absent”.
  • rb e-20260928-166 (#2489): a BACnetPropertyStates holding the audit-level [59] or audit-operation [60] choice (cl. 21) was rejected on decode and could not be set. Both choices are now decoded, encoded and rendered in builds that include the Audit Log or Audit Reporter object.
  • rb e-20260928-167 (#2479): a decoded value typed ANY, or any other abstract-syntax value, lost the tag number of each nested constructed element. Re-encoding it dropped the element’s opening and closing tags, so [0]{5} [1]{5} was sent as 21 05 21 05, and [0]{5} compared equal to [1]{5}. This reached the test-tool audit-notification path, whose target-value and current-value bytes were handed to the host unframed. Each nested element now keeps its tag number and re-encodes inside it (135-2024 cl. 20.2.15: “the contents octets shall be the complete base encoding, including tags”).
  • rb e-20260928-167 (#2490): the stack stored a written BACnetDeviceObjectReference or BACnetDeviceObjectPropertyReference whose optional device-identifier was not a Device object, for example (analog-input, 7). This affected an Access User’s Members or Member_Of, a Channel’s List_Of_Object_Property_References, and any other such property the stack holds or hands to a host callback. Everything the stack does with the field reads it as a device instance, so the reference was taken to mean device 7. The grammar (cl. 21.6) types the field only as BACnetObjectIdentifier, so such a value still decodes, and a received ReadProperty, ReadPropertyMultiple or ReadRange acknowledgement or event notification that carries one is not dropped. The stack’s own writes (WriteProperty, WritePropertyMultiple, CreateObject initial values and AddListElement) now refuse it with Error(PROPERTY, VALUE_OUT_OF_RANGE) and leave the stored value unchanged, because the stack needs the field to name a device.
  • rb e-20260928-167 (#2491): building a BACnetDeviceObjectPropertyReference or BACnetDeviceObjectReference from an out-of-range object type or instance, including the compact string BACnetStack_SendWriteProperty takes for a DeviceObjectPropertyReference value, silently produced a reference to a different object. For example, “5,4194321,85”, “1029,17,85” and “65541,17,85” all became analog-value 17. These values are now refused (cl. 20.2.14: “A 10-bit object type” and “A 22-bit object instance number”).
  • rb e-20260929-168 (#2871): on Windows the Node addon killed the process with 0xC0000409 whenever the stack had to answer with a segmented ComplexACK. Since SEGMENTED_BOTH became the default, this included a ReadProperty of a 50-object Object_List from a client with a max APDU of 128. The cause was the two dynamic_casts in BACnetPacket::CreateSegment, the only ones in the stack. node-gyp compiles the addon without RTTI (/GR-), and there dynamic_cast throws std::__non_rtti_object, which nothing catches. They are replaced by an RTTI-free BACnetPacket::AsIPPacket(). The crash was reproduced with a ReleaseDll built with the addon’s /GR- and no /EH. It exited 0xC0000409 on 50 objects at max APDU 128 and 206, and on 300 objects at 1476. With the fix it completes every segmented response. The segmented Confirmed-Request branch of CreateSegment used the same cast, so this probably also explains the 0xE06D7363 in #2992, but that path was not run. CreateSegment also no longer leaks the new packet on its ComplexACK failure paths.
  • rb e-20260929-168 (#2871 review): the stack’s last RTTI uses are gone. These were the typeid same-class guards in IsEqual() of BACnetAuthRequest, BACnetRawConfirmedServiceRequest and BACnetRawUnconfirmedServiceRequest, which would throw in the same /GR- addon build. They now compare a new BACnetObjectBase::GetClassIdentity(). GetName() was not usable because it exists only with STACK_OPTION_STRING_LITERALS. A new gate, ci_scripts/check-no-rtti.js, fails on any dynamic_cast or typeid in source/ and runs in pre-push.sh.
  • rb e-20260929-169 (#2501): an Abort-PDU with ‘server’ = FALSE no longer ends one of this device’s own client requests. A peer aborting its own request to this device under an invoke-id that matched one of this device’s outstanding requests to that peer used to remove that unrelated request and fire HookAbort; the real answer then arrived and was dropped. 135-2024 cl. 5.4.4.3 AbortPDU_Received applies only to an Abort “whose ‘server’ parameter is TRUE”, and cl. 5.4 lists the ‘server’ = FALSE Abort among the APDUs sent by requesting BACnet-users.
  • rb e-20260929-169 (#2502): a segmented ComplexACK whose invoke-id matches no outstanding request is no longer delivered to the application. The #1295 gate had exempted segmented ACKs, but cl. 5.4.4.1 hands them to UnexpectedSegmentInfoReceived, which delivers nothing.
  • rb e-20260929-169 (#2503, security): an unsolicited first segment of a segmented ComplexACK no longer opens a reassembly and is no longer Segment-ACKed. Any host could fill every reassembly slot (64 in a FULL build) and make legitimate segmented responses go unanswered. Per cl. 5.4.4.1 UnexpectedSegmentInfoReceived the device now answers with an Abort-PDU (‘server’ = FALSE, INVALID_APDU_IN_THIS_STATE) and stores nothing.
  • rb e-20260929-169 (#2504): a segmented Confirmed-Request now sends the whole window after each SegmentACK (cl. 5.4.4.2 NewACK_Received, “the next ActualWindowSize segments of the message”). It used to send one segment while adopting the server’s actual window, so a server that acked with a window of 3 or more waited for segments that never came and the request ended in TSM_TIMEOUT. Each segment now proposes this device’s own ProposedWindowSize (cl. 5.4.3 FillWindow step (3)), not the server’s window.
  • rb e-20260929-170 (#2505): a confirmed request now advertises the device’s Max_APDU_Length_Accepted rounded down to the nearest listed size, as 135-2024 cl. 12.11.18 requires (“the highest encodable value less than the value of this property”). A device configured for 1000 octets used to advertise 1024. A value above 1476 now encodes as B’0101’ (cl. 20.1.2.5: “or larger than 1476 octets”); it used to be refused, and the device could send no confirmed request at all. The value is no longer narrowed to 16 bits first.
  • rb e-20260929-170 (#2506): a client that accepts segmented responses and sends max-segments-accepted B’000’ now gets a segmented response instead of Abort(BUFFER_OVERFLOW). B’000’ is “Unspecified number of segments accepted” (135-2024 cl. 20.1.2.4) and was read as a limit of one segment, so cl. 5.4.5.3 CannotSendSegmentedComplexACK case (c) (“fewer than required”) aborted every response that needed segmenting. It is now treated as no limit, the same as B’111’. This device’s own Max Segments Transmittable still applies. ReadRange sizing uses the same value.
  • rb e-20260929-170 (#2507): a reassembled segmented ComplexACK whose service ack is followed by octets other than zero padding is now refused, as the same bytes sent unsegmented already were (CR-155). The completion path accepted any decodable prefix, so the extra octets were dropped and the application got a partial result reported as complete. An empty service-ack body that its decoder marks valid is now accepted on this path too, as it is unsegmented.
  • rb e-20260929-171 (#2510): a ComplexACK carrying a ConfirmedPrivateTransfer-ACK (choice 18), AuditLogQuery-ACK (choice 33) or AuthRequest-ACK (choice 34) now decodes. The ACK factory had no case for any of the three, so every such response was dropped as undecodable (the review rated the resulting retry and timeout plausible, not observed); copying a ComplexACK that held one also produced no ACK.
  • rb e-20260929-171 (#2508): the Result(-) of ConfirmedPrivateTransfer is now a ConfirmedPrivateTransfer-Error and that of AuthRequest an AuthRequest-Error, the SEQUENCEs 135-2024 cl. 21.4 gives those CHOICE arms (“confirmed-private-transfer [18] ConfirmedPrivateTransfer-Error”, “auth-request [34] AuthRequest-Error”). Both were sent as a bare Error (91 05 91 1D), which a peer decoding those arms cannot parse. The ConfirmedPrivateTransfer-Error carries the request’s vendor-id and service-number (cl. 16.2.1.3.2).
  • rb e-20260929-171 (#3306): a reassembled segmented ComplexACK that fails to decode now aborts the transaction: a BACnet-Abort-PDU with ‘server’ = FALSE and reason OTHER goes to the server (which has usually already gone IDLE after the final Segment-ACK, so the Abort is mainly a courtesy), and HookAbort (sentByServer = false, the request’s own invoke id) tells the application its request failed. It used to be dropped silently: the request was removed, so it could not even time out.
  • rb e-20260929-172 (#2531): assigning one foreign-device-table entry (BACnetFDTEntry) to another now copies it exactly. Assigning a default or never-set entry used to leave the target’s previous B/IP address in place, and x = x read from storage it had just cleared. The assignment now starts from Reset(), so it also clears the target’s own context tag number (m_context becomes -1, no context); the source’s context is not copied. The BDT entry half of the issue was already fixed by #2532; a self-assignment test now pins it.
  • rb e-20260929-172 (#2530): a BBMD whose Read-Broadcast-Distribution-Table-Ack does not fit one BACnet/IP frame (more than 152 entries) now answers with a BVLC-Result of X’0020’, as 135-2024 J.4.4.1 requires (“If the BBMD is unable to perform the read of its BDT, it shall return a BVLC-Result message to the originating device with a result code of X’0020’”). Read-Foreign-Device-Table gets the same check, with X’0040’ (J.5.2.1.1). It used to send a zero-length datagram: the BVLL table and entry encoders returned 0 without setting the error container, so the send path did not refuse them. Those encoders now report every refusal, and the IPv4 send path refuses any zero-length frame.
  • rb e-20260929-173 (#2537): when a device has more than one BACnet/IP Network Port object, a foreign-device registration’s success, and its later timeout, now set BACnet_IP_Mode on the Network Port object whose data link registered. It used to set FOREIGN, or NORMAL again, on the device’s lowest-numbered BACnet/IP port, whichever port had registered. 135-2024 12.56.25 defines FOREIGN as “The device is operating as a foreign device over this network port.” A data link with no Network Port object bound to it still uses the first BACnet/IP port, as before.
  • rb e-20260929-173 (#2539): BACnetStack_Reset now releases every BACnet MS/TP data link instance and restores their count, as it already did for BACnet/IP and BACnet/SC. The instances used to keep tracking Network Port objects the reset had deleted, so once every instance had been claimed (one in most builds), adding an MS/TP Network Port object after the reset failed with “no BACnet MS/TP data link instance is available”. The layer’s (Service initialization was also extended to loop over every claimed MS/TP instance; today its callers run it with one instance, so that part is hardening.)
  • rb e-20260929-173 (#2540): BACnetStack_ResetBroadcastDistributionTable now returns a device with several BACnet/IP Network Port objects to a plain device on every port. It used to disable the BBMD on the first IPv4 data link only, so a BBMD set up with BACnetStack_SetBBMD on another port kept its broadcast distribution table and kept acting as a BBMD, even though the call returned true. The call now clears every port’s table, and ResetBBMD (the same device-type change without clearing a table) disables the BBMD on every port as well.
  • rb e-20260929-173 (#2541): a BACnet/SC router with two or more SC data links no longer sends a routed packet for an SC router port that is not bound to a data link through the first SC data link. An SC port is unbound when, for example, its Network Port object’s Network_Number is not yet set. The fallback sent that port’s copy of a global broadcast back onto the first port’s network, even when that was the network the broadcast came from, and the port’s own network never got it. 135-2024 6.3.2 requires that “the router shall broadcast the message on all directly connected networks except the network of origin”. Such a packet is now refused, as the BACnet/IP and MS/TP ports already refuse it. A router with a single SC data link still uses it for every SC port.
  • rb e-20260929-174 (#2543): a client receiving a segmented response now aborts the transaction when a BACnet-SegmentACK-PDU with ‘server’ = TRUE arrives for it. 135-2024 5.4.4.4 (SEGMENTED_CONF) UnexpectedPDU_Received lists five unexpected PDUs, “BACnet-SimpleACK-PDU, BACnet-ComplexACK-PDU with ‘segmented-message’ = FALSE, BACnet-Error-PDU, BACnet-Reject-PDU, or BACnet-SegmentACK-PDU with ‘server’ = TRUE”, and the stack handled the first four. The SegmentACK was ignored, so no Abort-PDU was sent, HookAbort did not fire and the reassembly went on. The stack now sends an Abort-PDU with ‘server’ = FALSE and reason INVALID_APDU_IN_THIS_STATE, fires HookAbort, and drops the transaction. A SegmentACK with ‘server’ = FALSE, and one that acknowledges a segmented request this client is sending, are handled as before.
  • rb e-20260929-174 (#3309): a build without BBMD support now records the local IP address and subnet mask of a BACnet/IP Network Port object on its data link, as a BBMD build has done since #2536. Without them, the check that refuses a Forwarded-NPDU whose Originating Device is this subnet’s directed-broadcast address (#1884) could not run in that build. Subnets of /31 and /32 are still exempt, since they have no directed-broadcast address.
  • rb e-20260929-174 (#3311): when a router’s periodic rebind finds two router ports of one network type resolving to the same data link, it now marks that data link ambiguous, as adding the second port already did. It used to leave both ports unbound and clear the mark, so after the first router tick a routed packet for either port went out through the shared data link instead of being refused. This applies to BACnet/IP, BACnet/SC and MS/TP.
  • rb e-20260929-174 (#3312): BACnetStack_ResetForeignDeviceTable now clears the foreign device table on every BACnet/IP Network Port. It used to clear the first IPv4 data link’s table only, so foreign devices registered with a BBMD on another port stayed registered.
  • rb e-20260929-175 (#2547): a BBMD again sends a Forwarded-NPDU from a sender on its own IP subnet to each registered foreign device, whether or not that sender is in its BDT. 135-2024 J.4.5 names the case, “a situation which can occur if two or more BBMDs are installed on the same IP subnet”, and says “the BBMD merely retransmits the message directly to each foreign device currently in the BBMD’s FDT”. J.4.3 says two BBMDs on one subnet have BDTs that “shall not contain any common entries”, so such a peer is normally not in the BDT. The #467 loop guard returned before the FDT for every same-subnet sender, and the #2535 BDT-peer check also dropped these frames. Now only a frame from the BBMD’s own exact B/IP address is dropped. A same-subnet frame is sent to the foreign devices only and is never re-broadcast locally. A sender on another subnet must still be a BDT peer.
  • rb e-20260929-175 (#2548): after SetBBMD, the port’s Network Port object now reads BACnet_IP_Mode = BBMD (135-2024 12.56.25, “The device is operating as a BBMD over this network port”). It used to read NORMAL. ResetBBMD sets it to NORMAL, not to any FOREIGN mode the port had before it became a BBMD.
  • rb e-20260929-175 (#2549, #3041): a WriteProperty to BBMD_Broadcast_Distribution_Table now validates the whole list before the live table changes. Duplicate (address, mask) entries are merged first, so a duplicate ahead of the BBMD’s own entry no longer binds a peer as the BBMD itself. A list longer than STACK_OPTION_MAX_BROADCAST_DISTRIBUTION_TABLE_ENTRIES is refused with Error(RESOURCES, NO_SPACE_FOR_OBJECT) and the table is left unchanged; before, it was left truncated. The BBMD’s own entry is found by the Network Port’s own IP address and UDP port, as SetBBMD finds it, so a correct list is accepted even when the BBMD currently has no host entry. A list with no entry for that address is refused with Error(PROPERTY, VALUE_OUT_OF_RANGE).
  • rb e-20260929-176 (#2551): copying a BACnetBVLL that has no payload (default-constructed, a SetForwardedNPDU/SetDeleteForeignDeviceTableEntry call that rejected its address length, or an allocation failure) no longer dereferences NULL; the copy is left empty and invalid. BACnetIPPacket::SetBVLL makes that copy unconditionally. The seven Set* mutators that dereference the payload right after allocating it (SetResultCode, reached from the wire through every BVLC NAK, and six siblings) now leave the object invalid when that allocation fails instead of crashing.
  • rb e-20260929-176 (#2544, also fixes #3291): an unknown (reserved) network layer message type that carries a DNET - addressed through this node to another network, or the global broadcast X’FFFF’ sent unicast - no longer draws a Reject-Message-To-Network. 135-2024 6.5.2.2: “If the DNET field is absent and the message cannot be interpreted and the message was directed specifically at the router, a Reject-Message-To-Network shall be returned to the device that sent the message.” When the rejected message carries SNET/SADR, the reject is now addressed to that SNET/SADR (hop count 255) and sent to the router that relayed it, per 6.4.4: “It is directed to the node that originated the message being rejected, as indicated by the source address information in that message.” It used to go to that router as a local message, so the originator was never told. The message is now dropped silently; it is not relayed. That was already true before, because the reserved type fails to decode before routing, but a router should act per cl. 6.5.4 (“If the DNET is present and the NE resides in a BACnet router, the NE shall take the actions specified in Clause 6.5.4.”). That gap is tracked separately.
  • rb e-20260929-176 (#3317): SetBBMD now enables BBMD_Broadcast_Distribution_Table, BBMD_Accept_FD_Registrations and BBMD_Foreign_Device_Table on the BBMD’s Network Port, so they appear in its Property_List. 135-2024 Table 12-71.3 footnote 4: “Required to be present if, and only if, the port is capable of functioning as a BBMD.” 12.56.38 and 12.56.39 require the first two “if either of the BACnet_IP_Mode or BACnet_IPv6_Mode properties have the value BBMD”. The host used to have to enable them itself. ResetBBMD leaves them enabled, because the port is still capable of functioning as a BBMD.
  • rb e-20260929-176 (#3314): a client with no request outstanding for the invoke id now answers a BACnet-SegmentACK-PDU with ‘server’ = TRUE, and a segmented ComplexACK segment other than segment 0, with a BACnet-Abort-PDU (‘server’ = FALSE, INVALID_APDU_IN_THIS_STATE). 135-2024 5.4.4.1 IDLE, UnexpectedSegmentInfoReceived: “If an unexpected PDU indicating the existence of an active server TSM (BACnet-ComplexACK-PDU with ‘segmented-message’ = TRUE or BACnet-SegmentACK-PDU with ‘server’ = TRUE) is received from the network layer, then issue an N-UNITDATA.request with ‘data_expecting_reply’ = FALSE to transmit a BACnet-Abort-PDU with ‘server’ = FALSE and ‘abort-reason’ = INVALID_APDU_IN_THIS_STATE and enter the IDLE state.” Both used to be dropped silently; #2503 already did this for segment 0. A partial reassembly left behind after the server aborted its response is now discarded. Before, a later segment could complete it and be Segment-ACKed with no request outstanding.
  • rb e-20260929-177 (#2553): a BACnet/SC Network Port’s SC_Minimum_Reconnect_Time and SC_Maximum_Reconnect_Time now accept any value in their own ranges (2..300 and 2..600), whatever the other property holds. 135-2024 12.56.82: “If the value of the SC_Minimum_Reconnect_Time is greater than the value of the SC_Maximum_Reconnect_Time, it is a local matter how long to wait between each connection attempt, but the time shall not exceed the value of the SC_Maximum_Reconnect_Time.” A write of SC_Maximum_Reconnect_Time = 5 against the default minimum of 10 used to answer VALUE_OUT_OF_RANGE, and the hub connector refused an activated maximum below the minimum. The hub connector and direct-connect reconnect waits now never exceed the maximum; they used to raise the ceiling to the minimum.
  • rb e-20260929-177 (#2555): a BACnet/SC Network Port’s Operational_Certificate_File and Certificate_Signing_Request_File now read as File object instance 4194303 until the host binds a File object with SetBACnetSCCertificateFileObjects. Both are Required in 135-2024 Table 12-71 and are always in Property_List, but a read used to fail with PROPERTY / VALUE_NOT_INITIALIZED. 135-2024 12.1.1.1: “Object properties that contain values whose datatype is BACnetObjectIdentifier may use 4194303 as the instance number to indicate that the property is uninitialized, disabled, or unused”. Current_Health still reports the missing Operational_Certificate_File.
  • rb e-20260929-177 (#2554, completes #1114): a BACnet/SC Network Port’s Issuer_Certificate_Files now always reads as two elements, and array index 0 reads 2. 135-2024 12.56.101: “This read-only property, of type BACnetARRAY[2] of BACnetObjectIdentifier … This property contains two array elements”. With none or one File object bound it used to read as an empty or one-element array, index 0 answered 0 or 1, and index 2 answered INVALID_ARRAY_INDEX. A slot with no File bound reads as File object instance 4194303 (12.1.1.1, as #2555). SetBACnetSCCertificateFileObjects still accepts 0 to 2 issuer files, and Current_Health still reports anything other than two.
  • rb e-20260929-177 (#2472): a BACnet/SC Network Port’s Current_Health Timestamp is now the device’s local date and time when the value was last updated: CallbackGetSystemTime at the ReinitializeDevice ACTIVATE_CHANGES / WARMSTART that activated it, shifted by the Device’s UTC_Offset and Daylight_Savings_Status. It is the unspecified datetime (every field X’FF’) when the device has no clock callback. The start-up value is built before the clock can be read, so it starts unspecified, and the first BACnet/SC data link pass that can read the clock stamps it. 135-2024 12.56.17: “This field, of type BACnetDateTime, indicates the local date and time when this property was last updated. If the device has no knowledge of local time or date, this field shall contain an unspecified datetime.” It always read 1970-01-01 00:00:00.00.
  • rb e-20260929-177 (review of #2554/#2555): 135-2024 12.56.101 says each Issuer_Certificate_Files element “shall reference a File object within the device”, and 12.1.1.1 allows instance 4194303 “except where noted in individual clauses”. A port with an unbound slot already breaks the first requirement, whatever the read returns. The slot reads as 4194303 anyway because that value cannot name a real object and Current_Health reports the gap. The NM-SCCM client (SCCertificateAddIssuer / RemoveIssuer / ReplaceOperational) now treats a (File, 4194303) reference as “no file” and fails at the ReadCertificateFileReferences step with failureInvalidResponse, as the Error it used to receive made it do. It no longer goes on to write GENERATE_CSR_FILE (a new key pair on the target), ask the host to sign a certificate, or write File 4194303. A clock that cannot be converted now leaves the BACnet/SC Current_Health Timestamp unspecified.
  • rb e-20260929-178 (#2556): a ReinitializeDevice ACTIVATE_CHANGES or WARMSTART that activates new file data in a BACnet/SC Network Port’s bound Operational_Certificate_File or an Issuer_Certificate_Files File object now restarts the port, so its connections are made again with the new certificate. 135-2024 12.56.101: “A successful change in the file data of a File object referenced by this property will set the Changes_Pending property to TRUE and the new file data shall become effective when the device receives a ReinitializeDevice service request with a ‘Reinitialized State of Device’ of ACTIVATE_CHANGES or WARMSTART.” (12.56.100 says the same for the operational certificate.) Only a change of the File object BINDING used to restart the port, so after a cl. 19.8.3 procedure (which writes new data into the same File objects) Changes_Pending cleared while the existing connections kept the old certificate. A Certificate_Signing_Request_File data change still sets Changes_Pending without restarting the port, and DISCARD_CHANGES forgets the data change.
  • rb e-20260929-178 (#2558): a WriteProperty to a BACnet/SC Network Port property (SC_Heartbeat_Timeout, SC_Primary_Hub_URI, Max_BVLC_Length_Accepted and the rest of the pending SC properties) that is refused with a Result(-) no longer sets Changes_Pending. 135-2024 12.56.80: “If this property is writable, then a successful write to this property shall set the Changes_Pending property to TRUE.” The pending-changes snapshot used to be opened before the value was checked, so a wrong datatype or an out-of-range value left Changes_Pending TRUE with nothing changed, and RESTART_PORT then answered INVALID_VALUE_IN_THIS_STATE. A refused write made while other changes are already pending leaves those pending.
  • rb e-20260929-178 (#2559): the NM-SCCM client’s SCCertificateAddIssuer and SCCertificateRemoveIssuer now stop at the ReadCertificateFileReferences step with failureInvalidResponse when the target’s Issuer_Certificate_Files names the same File object twice, before any file is read or written. With one shared file the “other issuer certificate” file is the one holding the current issuer, so AddIssuer wrote the new issuer over it and RemoveIssuer cleared it, and both reported success. 135-2024 19.8.3.1: “it is important to not remove or replace the currently used issuer certificate, otherwise the nodes will no longer be able to connect to the network”.
  • rb e-20260929-179 (#2566): a BACnet/SC node that generates its own VMAC (no VMAC configured on the hub connector, or a new one after a Connect-Request is refused with NODE_DUPLICATE_VMAC) now generates a Random-48 VMAC. 135-2024 H.7.3: “The Random-48 VMAC is a 6-octet VMAC address in which the least significant 4 bits (Bit 3 to Bit 0) in the first octet shall be B’0010’ (X’2’), and all other 44 bits are randomly selected to be 0 or 1.” All 48 bits used to be random, so 15 in 16 generated VMACs did not have the Random-48 form. The UUID-derived hub-function VMAC is unchanged.
  • rb e-20260929-179 (#2567): a BACnet/SC VMAC generated on a thread that had not generated one before is no longer the same on every device. The Microsoft C runtime keeps rand()/srand() state per thread, and the #1117 seed-once flag was process-wide, so the NODE_DUPLICATE_VMAC regeneration (135-2024 AB.6.2.2: “the initiating peer’s node shall choose a new Random-48 VMAC”) on the Loop() thread, after the first VMAC was generated on the configuration thread, drew from an unseeded rand(). VMACs now come from a per-thread std::mt19937 seeded from std::random_device, a clock and a per-thread address, and the host application’s own rand() stream is no longer reseeded or consumed.
  • rb e-20260929-179 (#2562): a BACnet/SC node no longer answers a malformed response message (BVLC-Result, Address-Resolution-ACK, Connect-Accept, Disconnect-ACK, Heartbeat-ACK) with a BVLC-Result NAK. 135-2024 AB.2: “No response message shall be sent when a broadcast or response message is received.” A truncated response message, a Disconnect-ACK or Heartbeat-ACK with trailing octets, and a response message with a refused destination option (on the hub connector this includes a Connect-Accept without the Hello option when ACCEPT_CONNECT_ACCEPT_WITHOUT_HELLO is off) used to draw a NAK on the hub function, direct-connect and hub-connector paths; they are now discarded. The hub connector also drops, rather than NAKs, a Connect-Accept, Disconnect-ACK or Heartbeat-ACK carrying a Virtual Address, as the other two paths already did. The same defects on a request message are still NAKed.
  • rb e-20260929-179 (#2565): a BACnet/SC node’s hub connector now refuses a BVLC message with a reserved control flag bit set, and all three ingress paths (hub connector, hub function, direct connect) NAK it with PARAMETER_OUT_OF_RANGE instead of HEADER_ENCODING_ERROR. 135-2024 AB.2.2: control flag “Bit 7..4 Reserved Shall be zero.”; AB.3.1.5: “If a BVLC message is received in which any control flag has an unexpected value, then a BVLC-Result NAK shall be returned if it was a unicast message, indicating an ‘Error Class’ of COMMUNICATION and an ‘Error Code’ of PARAMETER_OUT_OF_RANGE. The message shall be discarded and not be processed.” The hub connector used to process such a message normally. A broadcast or response message with a reserved bit set is discarded without a NAK.
  • rb e-20260929-180 (#3328): a BACnet/SC node’s hub connector now answers a Heartbeat-Request from its hub with a Heartbeat-ACK carrying the request’s Message ID. 135-2024 AB.5.3.1: the hub function “shall test with a Heartbeat-Request prior to terminating the connection”; AB.2.15: the Heartbeat-ACK “is the response to the Heartbeat Request message.” The request used to be ignored, so a hub probing the connection saw no answer.
  • rb e-20260929-180 (#2571): a BACnet/SC node that accepts direct connections but has no accept URI configured (accept URIs cleared after accept was enabled) now answers an Address-Resolution with an Address-Resolution-ACK carrying zero URI octets, on both the hub-connector and the hub-function paths, instead of a BVLC-Result NAK with OPTIONAL_FUNCTIONALITY_NOT_SUPPORTED. 135-2024 AB.3.3: “An empty string shall be returned if no such WebSocket URIs are currently known but the node supports accepting direct connections.” The hub connector’s Advertisement likewise reports ‘Accept Direct Connections’ X’01’ whenever accept is enabled, as the hub-function and direct-connect Advertisements already did.
  • rb e-20260929-180 (#2570): a unicast Connect-Request or Advertisement whose payload holds an out-of-range value (for example a Hub Connection Status of X’03’ or a Maximum NPDU Length of 0) is now NAKed with PARAMETER_OUT_OF_RANGE instead of MESSAGE_INCOMPLETE, on the hub-connector, hub-function and direct-connect paths. 135-2024 AB.3.1.5: “If a BVLC message is received in which any parameter, field of a known header, or parameter in a BACnet/SC defined payload, is out of range, then a BVLC-Result NAK shall be returned if it was a unicast message, indicating an ‘Error Class’ of COMMUNICATION and an ‘Error Code’ of PARAMETER_OUT_OF_RANGE.” A truncated message is still NAKed MESSAGE_INCOMPLETE. A Connect-Accept with an out-of-range field is still discarded without a NAK: it is a response message, and 135-2024 AB.2 says “No response message shall be sent when a broadcast or response message is received.”
  • rb e-20260929-180 (#3329): the VMAC a BACnet/SC node derives from its device UUID, used on the hub-function and direct-connect paths, now has the Random-48 form: the low nibble of its first octet is X’2’. 135-2024 AB.1.5.2: nodes are identified “by their 6-octet virtual MAC address as defined in Clause H.7.3”; H.7.3: in a Random-48 VMAC “the least significant 4 bits (Bit 3 to Bit 0) in the first octet shall be B’0010’ (X’2’)”. The other five octets are still the last five octets of the UUID. A device whose UUID ends in a first octet with a low nibble other than X’2’ presents a different hub-function VMAC after this change. A generated VMAC now also mixes the device UUID into the per-thread generator, so two devices whose generators start in the same state still draw different VMACs.
  • rb e-20260929-181 (#2581): a BACnet/SC node’s Address-Resolution-ACK now lists every configured direct-connect accept URI, separated by a single space, on both the hub-connector and the hub-function paths; it used to carry only the first. 135-2024 AB.2.7.1: WebSocket-URIs is a “UTF-8 string containing a list of WebSocket URIs as of RFC 3986, separated by a single space character (X’20’), where the source BACnet/SC node accepts direct connections.” The hub connector also answers an Address-Resolution that carries no Originating Virtual Address (sent by the hub itself) with an Address-Resolution-ACK with no Destination Virtual Address, instead of sending nothing. 135-2024 AB.3.1.2: “If a response BVLC message is returned on a BVLC message with no ‘Originating Virtual Address’ parameter, then the response BVLC message ‘Destination Virtual Address’ parameter shall be absent and the response BVLC message shall be sent through the connection from which that BVLC message was received.”
  • rb e-20260929-181 (#2579): changing a BACnet/SC hub connector’s primary or failover hub URI while that hub’s WebSocket is open or opening, or clearing the URI, now closes that WebSocket under its old URI (CallbackDisconnectWebsocket), clears the hub connection status if it carried the hub connection, and restarts the connector so it dials the new URI. It used to replace only the URI string: the connector stayed “connected to primary”, sent its heartbeats and NPDUs addressed to the new URI (a socket the host never opened), and never closed the socket to the old hub; clearing the primary URI also froze the connector in that state. Re-applying an unchanged URI, as every Network Port synchronisation does, leaves the connection alone.
  • rb e-20260929-181 (#2579, review): clearing a BACnet/SC hub connector’s primary hub URI (for example by emptying SC_Primary_Hub_URI) while connected to the failover hub now also closes the failover connection and returns the connector to its start state. With no primary URI the connector stops running its state machine, so the failover connection was otherwise never serviced or closed again.
  • rb e-20260929-181 (#2581, review): the hub connector no longer answers an Address-Resolution sent to the Local Broadcast VMAC. 135-2024 AB.2: “No response message shall be sent when a broadcast or response message is received.” The hub function already did not. When the configured accept URIs do not all fit in the Address-Resolution-ACK (1600 octets on the hub connector, 1497 on the hub function), the ACK carries as many whole URIs as fit and the truncation is logged; before, the ACK failed to encode and nothing was sent.
  • rb e-20260929-182 (#2588): the BACnet/SC hub function’s Connect-Accept and Advertisement now carry the configured Maximum BVLC Length and Maximum NPDU Length (default 1600 / 1497, set by SetBACnetSCSettings or SC_Max_BVLC_Length_Accepted / SC_Max_NPDU_Length_Accepted) instead of 65535 / 1497. Every incoming BACnet/SC frame is discarded above the configured maximum, so a peer that trusted 65535 had larger frames dropped without a NAK. 135-2024 AB.2.11: Maximum BVLC Length is “The maximum BVLC message size that can be received and processed by the accepting node, in number of octets.” The 1600-octet floor itself is unchanged (#2572).
  • rb e-20260929-182 (#2598): the same for direct connections: the node switch’s Connect-Request, Connect-Accept and Advertisement now carry the configured Maximum BVLC Length and Maximum NPDU Length instead of 65535 / 1497. 135-2024 AB.2.10.1: Maximum BVLC Length is “The maximum BVLC message size that can be received and processed by the requesting node, in number of octets.”
  • rb e-20260929-182 (#2599): a direct-connect URI longer than 255 octets in an Address-Resolution-ACK is no longer recorded. Connection strings are carried with a one-octet length, so such a URI was cut to its length modulo 256 and the Connect-Request, heartbeats, Disconnect-Request and NPDUs were sent to a connection string that matched no socket. The configuration entry points already refuse a URI of more than 255 octets.
  • rb e-20260929-182 (#2600): a unicast Encapsulated-NPDU sent through a direct connection, initiated or accepted, now omits both the Originating and the Destination Virtual Address; it carried both. 135-2024 AB.4.2.1: “Unicast BVLC messages being sent through a direct connection shall omit both the ‘Destination VMAC Address’, and the ‘Originating VMAC Address’ parameters.” A broadcast Destination Virtual Address is still sent as it is.
  • rb e-20260929-182 (#2600, review): a unicast BVLC message received over a direct connection without an Originating Virtual Address now reports the connection peer’s VMAC (learned in the connect handshake) as its source; it had no source address. 135-2024 AB.4.2.2: “For unicast BVLC messages received from a direct connection in which the originating VMAC address is absent, the hub switch shall forward the connection peer node’s VMAC address as the originating VMAC address to the local BVLL entity.”
  • rb e-20260929-182 (#2599, review): an Address-Resolution-ACK whose first URI is unusable (not wss, or longer than 255 octets) now records the first usable URI in its list instead of none.
  • rb e-20260929-183 (#3340): a BACnet/SC broadcast addressed through a direct-connect connection string (uri;FFFFFFFFFFFF), or through any URI other than the hub connection or this node’s own hub function, is now sent to the connected hub instead of over the direct connection (and no direct connection is started for it); it is refused when no hub connection is established. A broadcast addressed through the hub URI with direct-connect initiation enabled is now sent rather than held back behind an Address-Resolution for X’FFFFFFFFFFFF’. 135-2024 AB.4.2.1: “On receipt of a broadcast BVLC message from the local BVLL entity, the BVLC message shall be forwarded to the hub connector.”
  • rb e-20260929-183 (#3338): a BACnet/SC hub or direct-connect URI longer than 255 octets is now refused where it is stored (BACnetSCWebsocket::SetWebsocketUri, the hub connector’s primary and failover setters) and by the SC_Primary_Hub_URI / SC_Failover_Hub_URI setters that a WriteProperty calls, which then answers VALUE_OUT_OF_RANGE. Connection strings carry a one-octet length, so such a URI would have been used cut to its length modulo 256. A refused hub URI no longer closes the connection to the configured hub. 135-2024 cl. 12.56.80/12.56.81 set no maximum; 255 is the implementation’s limit, as the customer API setters already enforced. A WriteProperty can carry a CharacterString of any length: decoding has no size limit, and only SetValue has one (batch e-20260929-184, #3345). So the VALUE_OUT_OF_RANGE refusal applies to a written URI in any build.
  • rb e-20260929-183 (#2590): SC_Failed_Connection_Requests now records the Connect-Requests this node refuses as the accepting side: the hub function’s NODE_DUPLICATE_VMAC and peer-table-full NAKs and the direct-connect acceptor’s NODE_DUPLICATE_VMAC NAK, with the refused peer’s VMAC and UUID from its Connect-Request. It held only the hub connector’s own failed outbound attempts, which stay in the list as a local matter. The stack does not know an accepted peer’s network address, so Peer Address is the ‘none’ host, port 0, and such an entry overwrites no other. 135-2024 cl. 12.56.99: the property “indicates information about failed connection requests received by an enabled hub function or a node switch enabled for accepting a BACnet/SC direct connection.”
  • rb e-20260929-183 (#2572, part): a peer’s Connect-Request, Connect-Accept or Advertisement that advertises a Maximum BVLC Length of 1513 to 1599 octets is now decoded; it failed to decode, so such a peer could not connect. The 1600-octet floor stays for what this stack is configured to accept (hub connector, SC_Max_BVLC_Length_Accepted); the value received from a peer is held only to the 16-octet BVLC header plus the 1497-octet minimum routed NPDU for BACnet/SC (Table 6-1). 135-2024 AB.2.10.1: Maximum BVLC Length is “The maximum BVLC message size that can be received and processed by the requesting node, in number of octets”, with no minimum given. The hub function’s AB.5.1 ingress size is not addressed yet.
  • rb e-20260929-184 (#3346): the BACnet/SC hub connector’s primary and failover URI setters now refuse a URI that is not wss:// before touching the live hub connection. The scheme was checked only inside BACnetSCWebsocket::SetWebsocketUri, after the connection to the configured hub had been closed for the URI change, so a refused write still dropped the hub. #3338 did the same for the length check.
  • rb e-20260929-184 (#3343): SC_Failed_Connection_Requests now also records a Connect-Request that the hub function or direct-connect acceptor NAKs because it does not decode (MESSAGE_INCOMPLETE, PAYLOAD_EXPECTED or PARAMETER_OUT_OF_RANGE). The entry has no Peer VMAC or Peer UUID, since they were not determined; it is matched by the socket the request arrived on, so one socket sending undecodable Connect-Requests updates its own entry rather than emptying the list. An accepted-side entry now overwrites the earlier entry for the same peer VMAC and Device UUID; before, each refusal added an entry, so one peer that kept sending a colliding Connect-Request pushed every other entry out, including the hub connector’s own outbound failures. 135-2024 cl. 12.56.99: “If a new failed connection request occurs that matches the IP Address of the Peer Address parameter of an existing entry, that entry shall be overwritten with the information from the new failed connection request.” The stack does not know an accepted peer’s IP address, so the VMAC and UUID stand in for it.
  • rb e-20260929-184 (#3339): a frame for a BACnet/SC direct connection, initiated or accepted, is now refused when its encoded BVLC message exceeds the peer’s Maximum BVLC Length or its NPDU exceeds the peer’s Maximum NPDU Length. Before, it was sent and the peer dropped it without telling the sender. The accepted peer’s maximums are now recorded from its Connect-Request; the initiated peer’s already were, from its Connect-Accept. An Advertisement received on either direct connection now replaces them. 135-2024 AB.2.10.1: Maximum BVLC Length is “The maximum BVLC message size that can be received and processed by the requesting node”; AB.7.5.3: “If the length of a BVLC message received through a WebSocket connection exceeds the maximum BVLC length supported by the receiving node, the BVLC message shall be discarded and not be processed.” The hub connector does not yet hold egress to the hub’s advertised maximums, and the hub function does not hold egress to its peers’ maximums.
  • rb e-20260929-184 (#3345): a WriteProperty, WritePropertyMultiple or CreateObject value that is a CharacterString of STACK_OPTION_MAX_CHARACTER_STRING_SIZE octets or more (256 by default) is now refused with PROPERTY / VALUE_OUT_OF_RANGE, before any host callback or stack-held setter is called. The reported clearing to an empty string did not reproduce: BACnetPrimitiveCharSTRING::Decode has no size limit, so the value arrived whole. The actual defect was that the stack accepted the value but could not return it. The Get callback is offered a buffer of that size and a value that fills it is refused (#2487), so ReadProperty of the property, and the Object_Name lookup, failed after the write. The longest value that can be written is now the longest the read path returns: 255 octets by default.
  • rb e-20260929-185 (#3326): a failed SetBACnetSCCertificateFileObjects call no longer discards Secure Connect Network Port changes that were staged before it, for example a Network Port write received over the wire and not yet activated. The call now discards the pending changes only when it opened them itself, as a refused WriteProperty has since #2558. Otherwise it undoes only its own edits to the certificate File references, so a failed call changes nothing.
  • rb e-20260929-185 (#3348): the #3345 length limit now also applies to a CharacterString nested in a written constructed value. A string of STACK_OPTION_MAX_CHARACTER_STRING_SIZE octets or more (256 by default) is refused with PROPERTY / VALUE_OUT_OF_RANGE before anything is stored, and the property keeps its value. This covers a BACnetNameValue name or CharacterString value (including one in a BACnetNameValueCollection), a BACnetOptionalCharacterString, the host name in a BACnetHostNPort or BACnetHostAddress (for example FD_BBMD_Address), a Landing_Call_Control floor text, the fault values of a fault-characterstring Fault_Parameters, and the alarm values of a change-of-characterstring Event_Parameters. Before, a fault-characterstring Fault_Parameters write with a 300-octet fault value was accepted, and Fault_Parameters then read back as a single empty string.
  • rb e-20260929-185 (#3347): a frame this node sends to its BACnet/SC hub is now refused when it exceeds the Maximum BVLC Length or Maximum NPDU Length that the hub advertised in its Connect-Accept or a later Advertisement. The hub function now also holds each frame it relays, or sends itself, to the maximums that the receiving peer advertised in its Connect-Request. A relayed frame that is too large for a peer is dropped for that peer without a BVLC-Result. 135-2024 AB.2.4: “For standard BVLC messages, it is only used to indicate an unsuccessful result as described in subsequent clauses.” No clause describes a NAK for a hub relay, and AB.7.5.3 has the receiving node discard such a frame anyway. The #3339 direct-connect check and these checks share one comparison.
  • rb e-20260930-186 (#2623): a ReadPropertyMultiple specification that names another property together with OPTIONAL, for an object with no optional properties, now returns one ReadAccessResult for that object instead of a second, empty one after it. 135-2024 cl. 15.7.3.1.2: “If no optional properties are present then no entries associated with OPTIONAL property identifier shall be in the ‘List of Results’ and if no other properties were requested, then the ‘List of Results’ shall be present and empty.” OPTIONAL on its own still returns one empty result.
  • rb e-20260930-186 (#2624): a ReadPropertyMultiple request containing a Read Access Specification whose List of Property References is empty (1E 1F) is now rejected with Reject(MISSING_REQUIRED_PARAMETER). 135-2024 cl. 15.7.3.1.2: “This parameter shall be a list of one or more BACnetPropertyReferences”. Before, the specification decoded and the response left it out without a result or an error. The refusal applies to ReadPropertyMultiple requests only. The ASN.1 (cl. 21.6) sets no minimum size, so a Group List_Of_Group_Members member with an empty reference list, written or added with AddListElement, still decodes.
  • rb e-20260930-186 (#2625): a property named explicitly in a ReadPropertyMultiple request whose read fails without a BACnet error now gets an embedded Error(PROPERTY, OTHER), and the rest of the request is still answered, as #2175 already did for ALL/REQUIRED/OPTIONAL. Before, the whole request was aborted with Abort(OTHER). 135-2024 cl. 15.7.2: “If any of the specified properties of the specified objects can be accessed, then a ‘Result(+)’ primitive shall be issued, which returns all accessed values and error codes for all properties that could not be accessed.” An explicit abort raised by a callback still aborts the request.
  • rb e-20260930-187 (#2626): the REQUIRED and OPTIONAL property identifiers in ReadPropertyMultiple no longer return proprietary properties (512-4194303), whether they were added with required true or false. 135-2024 cl. 15.7.3.1.2: “The property identifier REQUIRED means that only those standard properties having a conformance code of “R” or “W” shall be returned. The property identifier OPTIONAL means that only those standard properties present in the object that have a conformance code “O” shall be returned.” ALL still returns them (“including any proprietary properties”). The same rule now applies to the request-size estimate and to a Group member’s REQUIRED/OPTIONAL reference in Present_Value.
  • rb e-20260930-187 (#3369): a WritePropertyMultiple request containing a Write Access Specification whose List of Properties is empty (1E 1F) is now rejected with Reject(MISSING_REQUIRED_PARAMETER). 135-2024 cl. 15.10.3.2: “This parameter shall specify a list of one or more properties of the object identified above”. Before, the decoder failed without setting a reason, so the request fell back to Abort(OTHER). The refusal is made for the WritePropertyMultiple request; the ASN.1 (cl. 21.6) sets no minimum size, so BACnetWriteAccessSpecification on its own decodes an empty list.
  • rb e-20260930-188 (#2640): a CreateObject initial value for a commandable property (e.g. an Analog Output’s Present_Value) with no Priority is now written at priority 16 instead of being refused with CreateObject-Error {SERVICES, PARAMETER_OUT_OF_RANGE}. 135-2024 cl. 19.2.1: “If an attempt is made to write to a commandable property without explicitly specifying the priority, a default priority of 16 (the lowest priority) shall be assumed.” An initial value whose Priority is outside 1-16 is rejected with Reject(PARAMETER_OUT_OF_RANGE) before the object is created, as WritePropertyMultiple does (#637).
  • rb e-20260930-188 (#2641): a CreateObject List of Initial Values carrying an Object_Identifier other than the one being created is refused with CreateObject-Error {PROPERTY, WRITE_ACCESS_DENIED, n} and the object is not created. Before, it renamed the new object, so the CreateObject-ACK conveyed an identifier that no longer existed and a later failure’s rollback left the renamed object behind. 135-2024 cl. 15.3.1.3.1: “A property specified by the Property_Identifier in the List of Initial Values does not support initialization during the CreateObject service.” An Object_Identifier equal to the new object’s is still accepted.
  • rb e-20260930-188 (#2642): CreateObject for the reserved instance 4194303, or for an object the device cannot add, answers CreateObject-Error {OBJECT, DYNAMIC_CREATION_NOT_SUPPORTED, 0} instead of Abort(OTHER), and CallbackCreateObject is no longer called for the reserved instance. 135-2024 cl. 15.3.2: “If such an object does not exist and it cannot be created, a ‘Result(-)’ primitive shall be returned and the ‘First Failed Element Number’ parameter shall have a value of zero.”
  • rb e-20260930-188 (#2643): DeleteObject answers Error {OBJECT, OBJECT_DELETION_NOT_PERMITTED} instead of Abort(OTHER) when CallbackDeleteObject returns false. 135-2024 cl. 15.4.2: “If the specified object does not exist or cannot be deleted, then the ‘Result(-)’ primitive shall be issued.” The BACnetStack_RegisterCallbackDeleteObject documentation still describes the old Abort(OTHER).
  • rb e-20260930-189 (#2644): a CreateObject List of Initial Values is decoded by the new object’s type and each property’s datatype, as WritePropertyMultiple does. Before, it was decoded by application tags alone, so a constructed initial value (an Event Enrollment’s Object_Property_Reference, a Notification Class’s Recipient_List, …) became an unknown value and was refused with CreateObject-Error {PROPERTY, INVALID_DATATYPE, n}. A value that is not of its property’s datatype is still decoded by its own tags and refused with that error and its element number (135-2024 cl. 15.3.1.3.1: “A datatype of a property value specified in the List of Initial Values does not match the datatype of the property specified by the Property_Identifier.”).
  • rb e-20260930-189 (#3376): a CreateObject List of Initial Values carrying an Object_Type other than the new object’s type is refused with CreateObject-Error {PROPERTY, WRITE_ACCESS_DENIED, n} and the object is not created, as #2641 does for Object_Identifier. 135-2024 cl. 15.3.2: “The Object_Type property shall be initialized to the value of the ‘Object Specifier’ parameter.” The same Object_Type is still accepted.
  • rb e-20260930-189 (#3357): AddListElement applies the element checks WriteProperty applies (the #2490 device-identifier check and the #3345/#3348 CharacterString length ceilings). An element holding a CharacterString the read path cannot return is refused with Error {PROPERTY, VALUE_OUT_OF_RANGE} and its First Failed Element Number (135-2024 cl. 15.1.1.3.1: “The element value is out of range for the property.”), and nothing is written. Before, a host-served list answered {RESOURCES, NO_SPACE_TO_ADD_LIST_ELEMENT, 0}.
  • rb e-20260930-189 (#3375): the ReadPropertyMultiple preflight size estimate prices a REQUIRED/OPTIONAL expansion that resolved to an empty list (since #2626, an object whose only optional properties are proprietary) as the object’s framing alone, not as 40 unresolved properties, so a small-APDU request for it no longer fails the size check. The device/network-port wildcard instance keeps the conservative fallback.
  • rb e-20260930-190 (#2523): BACnetNetworkParameters::operator= (and the copy constructor) copies every member. Before, it dropped the NPDU priority, max-APDU, as-client flag, received-as-broadcast flag, sent-confirmed flag and the segmentation fields, so the responding state machine’s stored copy of a request answered its timers (for example segmented-response retransmissions) at Normal priority. Assigning an instance with no connection string also left the target’s old connection string, network type, networks and addresses in place.
  • rb e-20260930-190 (#2524): BACnetAddress::Decode refuses a network-number over 65535 and a mac-address over 255 octets with a Reject (parameter-out-of-range) and leaves the value invalid (135-2024 cl. 21: “network-number Unsigned16”). Before, both were stored while GetNetworkNumber() and GetMacAddressLength() narrowed them, so a Recipient_List entry could read back as network 65538 while notifications went to network 2.
  • rb e-20260930-190 (#2525): BACnetHostNPort::Decode refuses a port over 65535 with a Reject (parameter-out-of-range) and leaves the value invalid (135-2024 cl. 21: “port [1] Unsigned16”). Before, a port of 113344 was stored and GetPort() handed the application 47808, for example from a WriteProperty of FD_BBMD_Address or a BDT entry.
  • rb e-20260930-191 (#2514): a virtual network can no longer take a network number the physical router serves. BACnetStack_UpdateVirtualNetworkNumber now refuses a router port’s network or an operator route’s (BACnetStack_AddRouterRoute) network. BACnetStack_AddVirtualNetwork now also refuses an operator route’s network (it already refused a router port’s). Activating a virtual Network Port’s pending Network_Number now refuses an operator route’s network too. Before, a virtual network could take that number, and every message routed to the real devices on it was answered locally instead of being forwarded.
  • rb e-20260930-191 (#2517): a physical router now broadcasts Network-Number-Is out each enabled port at start-up, with the port’s network number and the “configured” flag (135-2024 cl. 6.4.20: “Upon startup, routers that have been configured to know their network numbers shall broadcast out each port a Network-Number-Is message”; 135.1-2023 10.2.7). It uses the same one-shot trigger as the cl. 6.6.2 start-up I-Am-Router-To-Network. Before, only the I-Am-Router-To-Network was sent.
  • rb e-20260930-191 (#2518): an Initialize-Routing-Table query (Number of Ports = 0) to a device that is both a gateway (virtual router) and a physical router now returns its complete routing table (135-2024 cl. 6.6.3.8). The Ack lists the physical networks under their real port IDs, then the virtual networks under one port ID that no physical port uses. Before, it listed only the virtual networks, numbered 1..N, and those numbers collided with the physical port IDs. A gateway with no physical router answers as before.
  • rb e-20260930-192 (#2527): an Initialize-Routing-Table-Ack “without data” is now decoded and sent. 135-2024 cl. 6.4.8 says the data portion is “returned only in response to a routing table query”, and cl. 6.6.3.9 says a router acknowledging a table update “shall return an Initialize-Routing-Table-Ack without data”. An Ack that ends at its Message Type octet now decodes, so the test-tool Initialize-Routing-Table-Ack hook fires for it. A virtual router’s reply to a table update is now that data-less form. Before, the data-less Ack failed to decode, and the update reply carried a Number of Ports octet of 0, which is the query answer for an empty table.
  • rb e-20260930-192 (#2528): network layer message types X’0A’ to X’11’ are now handled as unknown message types. 135-2024 cl. 6.2.4 lists them as “removed”. A message of one of these types sent directly to a router or gateway is now answered with Reject-Message-To-Network reason 3 (“It is an unknown network layer message type”, cl. 6.4.4; cl. 6.5.2.2), as the reserved types X’14’ to X’7F’ already were. Before, they were logged as “not yet implemented” and dropped with no reply.
  • rb e-20260930-192 (#2529): the Forwarded-NPDU and Distribute-Broadcast-To-Network encoders now set the error container on every refusal, and the BBMD treats a zero-length encode as a failure whether or not an error was set. The BBMD no longer sends zero-length datagrams for a broadcast too large to forward: batch 172 (#2530) had already fixed that end to end, and a new BBMD test now pins it.
  • rb e-20260930-192 (#3382): BACnetStack_AddRouterPort now refuses port ID 0. 135-2024 cl. 6.4.7 says “If the Port ID field has a value of zero, then all table entries for the specified DNET shall be purged from the table”, so a port with ID 0 would have appeared in the router’s Initialize-Routing-Table-Ack as a purge entry. New tests pin that a disabled port sends no start-up Network-Number-Is, that bringing a port back up sends it again, and that with physical ports 1 and 3 the virtual networks get port ID 2.
  • rb e-20260930-193 (#2474): a segmented confirmed request to a remote network kept its routing when retried. 135-2024 cl. 5.4.4.3 TimeoutSegmented retransmits “the first segment of the message”, but the retry rebuilt segment 0 from the unsent parent packet, whose NPDU the data link had never prepared, so the retried segment 0 went out with no DNET/DADR and no expecting-reply bit, to the local network. The retry now resends the segment 0 that first went out, byte for byte.
  • rb e-20260930-193 (#2511): a Network Port object’s Routing_Table now holds only that port’s routes. 135-2024 cl. 12.56.62 defines it as “the table of first hop routers to remote networks reachable through this port”. Every Network Port object used to get the whole router table, including networks reached through other ports (with a MAC on another link) and directly connected networks. A Network Port object now gets the table of the router port whose network number equals its Network_Number (a Network Port whose Network_Number was never set, 0, gets every router port’s remote routes, since it cannot be matched to one port; one with any other unmatched number gets none); directly connected networks are left out; and a route through a disabled port reads DISCONNECTED instead of AVAILABLE.
  • rb e-20260930-194 (#2521): a Network-Number-Is (the What-Is-Network-Number reply, and BACnetStack_SendNetworkNumberIs) for a LEARNED_CONFIGURED network number now carries flag 0 (learned), not 1 (configured). 135-2024 cl. 6.4.20: “a value of 1 indicates that the network number was configured, and a value of 0 indicates that the network number was learned by receipt of a previous Network-Number-Is message”; LEARNED_CONFIGURED (cl. 12.56.14) is a number learned from a Network-Number-Is that carried flag 1, so it was learned.
  • rb e-20260930-194 (#3319): a router now takes cl. 6.5.4’s actions for a network layer message of an unknown type (removed X’0A’-X’11’ or reserved X’14’-X’7F’) that carries a DNET, instead of dropping it. 135-2024 cl. 6.5.2.2: “If the DNET is present and the NE resides in a BACnet router, the NE shall take the actions specified in Clause 6.5.4.” It is relayed with its body unchanged (to a directly connected network, to the next router, or fanned out as a global broadcast), and a DNET the router has no route to is searched for with Who-Is-Router-To-Network, as for a data NPDU: the message is held and relayed if a router answers, and rejected with reason 1 (per-source rate limited) if none does within TOmin. A received broadcast is neither searched for nor rejected. A virtual-router gateway with no physical router now answers such a message to a DNET that is not one of its virtual networks with reason 1, as it does any NPDU (a received broadcast and a remote broadcast, DLEN 0, still draw nothing). A non-router still discards it, and a frame with no DNET is still answered with reason 3.
  • rb e-20260930-194 (#3152): a virtual network can no longer take a network number one of the device’s other Network Port objects carries, active or pending: a virtual network port’s Network_Number write is refused with PROPERTY / VALUE_OUT_OF_RANGE, activation re-checks it, and BACnetStack_UpdateVirtualNetworkNumber returns false. A received Network-Number-Is that names one of the device’s virtual network numbers is ignored rather than learned. The check is against the other port’s number as it stands, active or pending, so swapping a BACnet/IP port’s number and a virtual network’s number in one activation is refused; make the change in two activations.
  • rb e-20260930-195 (#3384): a ReadProperty-, ReadPropertyMultiple- or ReadRange-ACK (and any other value decoded by datatype) no longer fails when a property the stack has no datatype for carries an EMPTY value, its opening tag followed at once by its closing tag (135-2024 cl. 20.2.1.6(d): “a sequence of zero or more tagged elements”). The closing tag was consumed as the value’s first element, so decoding ran into the next result and the whole ACK failed; for a segmented reply the client then sent Abort (other) after acknowledging the last segment. Found at Plugfest 2026 on a LOYTEC device’s ReadPropertyMultiple(Device, ALL) reply, whose empty Subordinate_Address_Binding (171) broke it; the proprietary property 559 in the same reply already decoded.
  • rb e-20260930-195 (#3320): an Abort-PDU with ‘server’ = TRUE partway through a segmented ComplexACK now discards the partial reassembly for that invoke-id and peer at once (135-2024 cl. 5.4.4.4 SEGMENTED_CONF, AbortPDU_Received), instead of leaving it until the reassembly timeout.
  • rb e-20260930-195 (#2470): valid traffic no longer produces ERROR log lines. A well-formed GetEventInformation-ACK no longer logs “Not a closing tag with the correct tag number=[0]” once per event summary (the same end-of-list probe in the Extended event/fault/notification parameters and the change-of-reliability notification parameters is also quiet); a ComplexACK that has no typed processor but decoded and reached the application logs at debug level, not “Unsupported complex ack type” at ERROR; and a well-formed unconfirmed request for a service no device here executes (for example the device’s own broadcast Time Synchronization coming back) is discarded with one INFO line instead of an ERROR cascade through the network layer, data link and interface. Malformed input still logs at ERROR.
  • rb e-20260930-196 (#3397): the Abort-PDU a client sends for a stray segmented ComplexACK or a server Segment-ACK with no request outstanding (135-2024 cl. 5.4.4.1, #2503, #3314) is now rate-limited per source: a burst of 4, then 1 a second, refilled from whichever advances more of the wall clock and the Tick count (5000 Ticks per period). A peer flooding stray segments used to draw one Abort per frame. The bucket is keyed on the link peer (on BACnet/IP its IP without the UDP port), which is the sender only for a direct peer: through a BACnet/SC hub, a BBMD or a router it is the forwarding hop, as for #471’s rejects. The token bucket is #471’s Reject-Message-To-Network limiter, moved out of the physical router into BACnetStackPerSourceRateLimiter, which is compiled in every configuration; the router’s rejects use the same class, unchanged.
  • rb e-20260930-197 (#3400): a confirmed WritePropertyMultiple or WriteProperty of a vendor-proprietary property (or of any property of a proprietary object type) whose value is empty, constructed or context-tagged got no reply at all - the request decode refused the value with PROPERTY / DATATYPE_NOT_SUPPORTED, from which no reply is built. The value now decodes opaquely and the service handler answers it: an unsupported property gets the same Error the application-tagged value gets (PROPERTY / UNKNOWN_PROPERTY; for WritePropertyMultiple with the first-failed-write-attempt naming the property), and a proprietary property the host registered with a concrete datatype (for example Unsigned) refuses such a value with PROPERTY / INVALID_DATA_TYPE without calling the host. A proprietary property registered as Any or Proprietary is not type-checked by the stack: such a value now reaches the host’s write path (in test-tool builds CallbackSetPropertyConstructed, with the raw bytes), where it used to be dropped at decode. AddListElement and RemoveListElement are unchanged.
  • rb e-20260930-197 (#3401): BACnetStackUnknownAPDU::GetTag() returned NULL after every successful Decode, because the stored tag was cleared when the value’s buffer was set. It now returns the decoded tag (class, tag number and length), a copy or Clone() keeps it, and a decode that fails reports no tag.
  • rb e-20260930-197 (#3321): a Network Port’s BBMD_Accept_FD_Registrations is now writable when the port is a BBMD (135-2024 cl. 12.56.39). A write sets Changes_Pending and takes effect at ReinitializeDevice ACTIVATE_CHANGES or WARMSTART. While it is FALSE the BBMD answers every Register-Foreign-Device, a renewal included, with the Register-Foreign-Device NAK (BVLC-Result 0x0030). Foreign devices already in the table stay until their time-to-live runs out. A write to a port that is not a BBMD is refused with PROPERTY / WRITE_ACCESS_DENIED.
  • rb e-20261001-198 (#3290): an object the application does not name (no CallbackGetPropertyCharacterString answer for its Object_Name) now reads as "Object <type>,<instance>", for example "Object 56,100" for Network Port 100, instead of the literal "undefined". Object_Name must be unique within the device (135-2024 cl. 12.1.1.2), and every unnamed object used to share the same name. The Device object keeps its own default name. The frozen-header documentation of the old value (CASBACnetStackDLL.h), the generated adapter bindings that repeat it and the property-profile docs still say "undefined"; #3293 (reopened) tracks updating them.
  • rb e-20261001-198 (#3409): a confirmed AddListElement or RemoveListElement of a vendor-proprietary property whose element list was empty, constructed or context-tagged got no reply at all, the same request-decode refusal #3400 fixed for WriteProperty and WritePropertyMultiple. The elements now decode opaquely and the service handler answers: a property the object does not have gets the same ChangeList-Error an application-tagged element gets (PROPERTY / UNKNOWN_PROPERTY), and a host-registered proprietary list of a concrete datatype refuses a constructed or context-tagged element with PROPERTY / INVALID_DATA_TYPE, element 1.
  • rb e-20261001-198 (#3378): a CreateObject initial value that has its property’s datatype’s shape but is not a valid encoding of it (for example an Event Enrollment Object_Property_Reference missing its property identifier) is now refused with PROPERTY / INVALID_DATA_ENCODING against its element number (135-2024 cl. 15.3.1.3.1), not INVALID_DATATYPE. A value of another datatype still answers INVALID_DATATYPE. New tests also pin a wire-decoded Object_Type that differs from the new object’s type (WRITE_ACCESS_DENIED) and a context-tagged proprietary initial value (UNKNOWN_PROPERTY). Only a constructed datatype is checked this way; a primitive value that fails to decode keeps its previous answer. Not covered: the issue’s item 2c (#3357 on a CharacterString list held in stack memory), filed separately.
  • rb e-20261001-198 (#3288): under DeviceCommunicationControl, ReinitializeDevice now refuses only STARTBACKUP, ENDBACKUP, STARTRESTORE, ENDRESTORE and ABORTRESTORE, and only under DISABLE, with SERVICES / COMMUNICATION_DISABLED (135-2024 cl. 16.4.1.1.1). ACTIVATE_CHANGES under either mode, and the backup/restore states under DISABLE_INITIATION, used to be refused too; they now take the normal path. WARMSTART and COLDSTART are unchanged.
  • rb e-20261001-199 (#3114): renaming a Notification Class object (Object_Identifier write or UpdateObjectIdentifier) now repoints every stored reference to it - the alarm-and-event record of each intrinsic reporting object, Event Enrollments, Alert Enrollments and every other Notification Class whose Notification_Class named it - to the new instance. They used to keep the old instance, a class that no longer exists, so their notifications and ACK_NOTIFICATIONs were not distributed. A reference to a different class is unchanged. Owner ruling on #3114, option 1.
  • rb e-20261001-199 (#3420): RemoveListElement of a constructed or context-tagged element on a list whose datatype it does not match, and that the stack does not hold (for example a host-registered proprietary list of Unsigned), now answers PROPERTY / INVALID_DATATYPE with First Failed Element Number 1, as AddListElement does (135-2024 cl. 15.2.1.3.1), instead of DEVICE / OPERATIONAL_PROBLEM. A well-typed element on such a list still answers DEVICE / OPERATIONAL_PROBLEM (#465). A new test also pins #3357’s over-long CharacterString refusal on a CharacterString list held in the stack’s stored-value store.
  • rb e-20261001-200 (#3275): GetEventInformation now sizes each page by what the responder can deliver - one APDU, or (Max APDU - 5) x segments when this device transmits segments and the requester accepts a segmented response - the same budget ReadRange uses (#2944), instead of the requester’s Max APDU alone. A client at 50 octets that accepts 4 segments now receives two event summaries per page (segmented), with More Events still correct. A client that accepts no segments pages as before. When no summary fits, the Abort reason follows 135-2024 cl. 5.4: SEGMENTATION_NOT_SUPPORTED when either side cannot segment, BUFFER_OVERFLOW when both can but too few segments are allowed.
  • rb e-20261001-200 (#3240): an Event Enrollment whose FAULT_STATUS_FLAGS or FAULT_LISTED referent cannot be read (deleted, unreadable or not local) now reports Reliability UNRELIABLE_OTHER and re-enters FAULT with a TO_FAULT notification (135-2024 cl. 12.12.21 step (1)), instead of skipping the fault algorithm and holding its last MEMBER_FAULT or FAULTS_LISTED forever. Once the referent reads again, the fault algorithm judges its current value.
  • rb e-20261001-200 (#3244): OUT_OF_RANGE, DOUBLE_OUT_OF_RANGE and FLOATING_LIMIT now ignore a NaN monitored value (and FLOATING_LIMIT a NaN setpoint) explicitly: the event state is held and any pending time delay restarts, as CHANGE_OF_VALUE does (#2720). A limit whose enable flag is FALSE still returns the object to NORMAL, and a NaN high limit still lets a value below the low limit reach LOW_LIMIT. The outcome is unchanged; it is now pinned by tests.
  • rb e-20261001-200 (#3252, question 2): a write to a stack-held Load Control (Requested_Shed_Level, Start_Time, Shed_Duration or Duty_Window) that leaves a shed request in SHED_REQUEST_PENDING with a level requested and its Start_Time already reached now consults the host’s CallbackLoadControlShed at the write, so Present_Value reads SHED_COMPLIANT or SHED_NON_COMPLIANT straight away rather than after the next Loop() tick (135.1-2023 7.3.2.38.1 steps 13-14). A write that leaves an active shed as it is (Enable, Shed_Levels) does not consult the host; Loop() re-consults an active shed as before. The callback can therefore now fire from inside a WriteProperty service; the host must not call back into the stack from it.
  • rb e-20261001-201 (#3296): a WriteProperty to a Schedule’s Schedule_Default, or to its Present_Value while Out_Of_Service is TRUE, that carries more than one value is now refused with PROPERTY / INVALID_DATATYPE (135-2024 cl. 15.9.1.3.1), matching #2477, instead of silently storing the first value and answering success. A single value is written as before. The enumeration decoder now also types the Audit Log and Audit Reporter’s Object_Type, Event_State, Reliability, Notify_Type and Audit_Level, the Device’s VT_Classes_Supported, a Staging object’s Units, and Audit_Level on Color Temperature, Staging and Accumulator objects, which were decoded as unknown values (UnknownAPDU). Test-tool consumers: the DecodeAs JSON / XML output for these properties changes from an unknown-APDU rendering to the named enumeration.
  • rb e-20261001-201 (#3277, item 1): a confirmed event or audit notification to a subnet-directed IPv4 broadcast address on the local network (for example 192.168.1.255:47808 on a 192.168.1.0/24 port) is now refused like one to 255.255.255.255 (#385, cl. 5.4). The subnet is known only when the BACnet/IP data link has its local address and subnet mask (recorded when the Network Port object is added); without them the address is treated as unicast, as before. Still open: the Notification Forwarder’s loop-prevention rule (a) recognises only the all-0xFF form, not a subnet-directed broadcast.
  • rb e-20261001-201 (#3427): AddListElement and RemoveListElement now change a list kept in the device’s stored-value store, the last place WriteProperty writes (after the host callbacks). The whole list is replaced in one write. RemoveListElement finds every element before removing any, and answers SERVICES / LIST_ELEMENT_NOT_FOUND for one that is not there. Before, a valid element answered PROPERTY / OTHER (add) or DEVICE / OPERATIONAL_PROBLEM (remove), and the list did not change.
  • rb e-20261001-201 (#3161, owner ruling 2026-09-29): the stack now holds the Device’s Time_Synchronization_Recipients and UTC_Time_Synchronization_Recipients itself, as it does a stack-held Calendar’s Date_List. Once the host makes either property present, WriteProperty (an empty list clears it), AddListElement and RemoveListElement work on the stored list, and ReadProperty reads it back without asking the host. Both lists start empty. The host can no longer serve these two properties through its constructed-value Get callback. Still open: the customer setters the ruling asks for (to load initial values) need a change to the public interface; the host has no way to see or be notified of changes to either list; and the stack does not yet send TimeSynchronization / UTCTimeSynchronization to the recipients.
  • rb e-20261001-202 (#2650, owner ruling 2026-09-29): when a matching You-Are assigns this device a new Device instance, the stack now asks the host first through the existing CallbackSetPropertyObjectIdentifier: the Device object at its old instance, with the new identifier as the value. The assignment is applied, and the I-Am sent, only if the callback returns true, so the host can refuse the change or persist it (135-2024 cl. 16.11.4). The host is asked only after the stack’s own checks pass (an instance another local device holds, or 4194303). If the virtual router then refuses the move, the stack reports the undo the same way. With no callback registered, the assignment is applied as before.
  • rb e-20261001-202 (#3085, code half): ReadTrendLogRecord and ReadTrendLogMultipleRecord now return false (0 through the export) when a required output parameter is NULL: timestamp, datum/data type, datum/data string (when its maximum length is non-zero), datum/data length, or more. Before, they wrote through the NULL pointer and crashed, and a NULL timestamp through the export returned the record length as if it had succeeded.
  • rb e-20261001-202 (#2997, item 1): a received CharacterString of STACK_OPTION_MAX_CHARACTER_STRING_SIZE octets or more (256 in a full build) now survives being copied. Decode accepts such a string, but six copy sites rebuilt it through SetValue, which refuses it. Those sites were the copies of a BACnetHostAddress name and a BACnetHostNPort host name (which also lost the name’s character set), a Landing Call Status floor text, the Clone of a Confirmed or Unconfirmed EventNotification with a long Message Text, and the Event Log’s capture of a received notification (which dropped the record). Each now copies the string exactly. SetValue is unchanged and still refuses a long string built locally. The contract is documented on both methods in BACnetPrimitiveCharSTRING.h: Decode is unbounded, and SetValue is the local-construction cap.
  • rb e-20261001-203 (#3184): deleting a Network Port object with RemoveObject no longer leaves a data-link claim that blocks a later port. The claim is kept, so frames the host still names by that port are answered (#311), but it is marked stale. A new Network Port with the same key re-uses its data link, and one with another key takes the stale data link over (with the old address, subnet mask or MAC cleared) instead of being refused when every BACnet/IP, MS/TP or BACnet/SC instance is claimed. A live claim is never taken.
  • rb e-20261001-204 (#2720): BACnetStack_SetIntrinsicChangeOfValueAlgorithm returns false for a NaN, infinite or negative increment and leaves the object’s previous arming unchanged (an unarmed object stays unarmed). 135-2024 cl. 13.3: pIncrement is “the positive increment”. Before, a NaN silenced the algorithm for good and a negative value notified on every change. An increment of 0 is still accepted (“any change”).
  • rb e-20261001-204 (#3411 item 2): a Schedule or Timer reference write that is skipped is now logged instead of dropped with no trace: a write to another device from a device with no interface to send it on (a virtual device), and a local reference the host does not write (no Set callback for the datatype, or the callback refused it). Sending a virtual device’s write to another device (#3411 item 1) is not part of this change.
  • rb e-20261001-205 (#2609 CR2-F05-033): an Unconfirmed-Request PDU that cannot be encoded (no buffer, a buffer below the two-octet header, an invalid PDU, or no service request) now reports Abort OTHER in the error container instead of leaving it unset. A valid PDU holding no service request is refused instead of dereferencing NULL.
  • rb e-20261001-205 (#2609 CR2-F05-034): a received Unconfirmed-Request (I-Am, Who-Is, Who-Has, COV and event notifications, …) with octets after the service request that are not zero padding is refused instead of being acted on, through both decode entry points. Zero padding is still accepted. Nothing is sent in reply.
  • rb e-20261001-205 (#2609 CR2-F05-035): a Complex-ACK that does not fit an APDU smaller than its header is answered with an Abort-PDU, as the other Complex-ACK encode failures are; it was reported as a Reject and nothing was sent.
  • rb e-20261001-205 (#2609 CR2-F05-038): a received Complex-ACK whose segmented-message bit is clear now reads more-follows as FALSE (135-2024 cl. 20.1.5.2: more-follows “shall be ignored by the decoder”).
  • rb e-20261001-205 (#2609 CR2-F05-039): copying or cloning a Complex-ACK that holds an encoded payload no longer produces a valid copy with an empty payload when the copy runs out of memory; the clone fails and the assigned object is invalid.
  • rb e-20261001-205 (#2609 CR2-F05-045): Abort, Reject and Error PDU decoding, and the Error and error-choice decoders, reset the object before refusing a NULL or too-short buffer, so a reused object no longer keeps the previous decode’s values and stays valid.
  • rb e-20261001-206 (#2609 CR2-F05-001): a client Abort-PDU (server = FALSE) received while the server is awaiting the application’s response now returns that transaction to IDLE (135-2024 cl. 5.4.5.3 AbortPDU_Received). It was never evaluated, so the transaction stayed open until its request timer expired. The ABORT.indication to the application that the clause also requires is still not raised (#2501).
  • rb e-20261001-206 (#2609 CR2-F05-005): while receiving a segmented request, acknowledging a full window now resets the duplicate-segment count (cl. 5.4.5.2 LastSegmentOfGroupReceived). A duplicate in one window no longer makes the server send a negative SegmentACK early in the next.
  • rb e-20261001-206 (#2609 CR2-F05-022): when sending a segmented request, the segment timer rounds APDU_Segment_Timeout up to whole seconds instead of truncating it. A sub-second timeout such as 500 ms no longer expires as soon as it is armed and spends a retry on an immediate retransmission.
  • rb e-20261001-206 (#2609 CR2-F05-026): a segment retransmission that fails to leave the host no longer spends a segment retry, matching the other client retry paths (#923). A transient local send failure no longer ends a segmented request with a TSM_TIMEOUT abort. A segment that cannot be re-encoded still spends a retry, and more than Number_Of_APDU_Retries failed retransmissions in a row end the request (TSM_TIMEOUT abort, or the timeout hook for an unsegmented request), so a dead local link no longer retransmits forever.
  • rb e-20261001-206 (#2609 CR2-F05-029): a Confirmed-Request PDU whose settings are refused (for example a proposed window size of 0 on a segmented message) is now invalid and cannot be encoded. It was left valid and put the refused value on the wire.
  • rb e-20261001-206 (#2609 CR2-F05-030, #3452 item 1): an unsegmented Confirmed-Request PDU is now always encoded with more-follows clear, and a received one reads more-follows as FALSE whatever the bit holds (cl. 20.1.2.2).
  • rb e-20261001-207 (#3452 item 2): assigning an Unconfirmed-Request PDU whose service request cannot be copied (an invalid service request, or the copy running out of memory) now leaves the target invalid and empty, instead of valid with no service request or with its own previous one.
  • rb e-20261001-207 (#3452 item 3): an Unconfirmed-Request that fails to decode (an undecodable service request, a malformed Who-Is, or no memory for the service request) now resets the PDU on every failure path, through both decode entry points, so the refused service request is not left allocated.
  • rb e-20261001-207 (#3452 item 4): cloning a BACnet/SC packet whose APDU cannot be cloned now gives an invalid packet, as cloning other packets does, instead of a valid one with no APDU.
  • rb e-20261001-207 (#3452 item 7): the responding state machine’s AWAIT_RESPONSE state no longer carries an unreachable incoming-PDU block after its send transitions. No behaviour change.
  • rb e-20261001-208 (#2609 CR2-F05-009): the responding state machine’s SEGMENTED_RESPONSE state no longer dereferences a NULL outgoing packet when a Confirmed-Request arrives, or when the next segment of a response cannot be built, through the data link’s RunRespondingStateMachine entry (which passes none). The #2003 guard already covered IDLE and SEGMENTED_REQUEST.
  • rb e-20261001-208 (#2609 CR2-F05-018): a ConfirmedCOVNotification, ConfirmedCOVNotificationMultiple, DeleteObject, ConfirmedTextMessage, ConfirmedEventNotification or ConfirmedAuditNotification whose SimpleACK cannot be allocated is now answered with Abort OUT_OF_RESOURCES, as the other services are, instead of nothing. A DeleteObject in that state no longer deletes the object first.
  • rb e-20261001-208 (#2609 CR2-F05-019): a WritePropertyMultiple that the processor answered with its own Abort or Reject, but whose response PDU was not built, is no longer re-labelled Abort OUT_OF_RESOURCES.
  • rb e-20261001-208 (#2609 CR2-F05-040): BACnetComplexACKPDU::SetServiceAckAndTakeControl refuses an invalid service ack (the caller keeps it) instead of producing a valid PDU, and no longer frees the argument when it is the ack the PDU already holds. The AtomicReadFile/AtomicWriteFile processors free a refused ack, and a reassembled segmented Complex-ACK whose service ack is refused is aborted like one that does not decode.
  • rb e-20261001-208 (#2609 CR2-F05-041): the Complex-ACK memory-buffer setters now discard a previously held typed service ack, so GetServiceAck() no longer reports an ack that Encode does not send; SetComplexAckAsSegment also replaces, rather than appends to, an earlier payload.
  • rb e-20261001-208 (#2609 CR2-F05-049): BACnetErrorBase::SetError(const BACnetError&) refuses an invalid error and sets the object’s validity, as its two sibling overloads do.
  • rb e-20261001-209 (#2609 CR2-F05-012): a responding transaction’s state now clears its device, data link and network parameters on Reset() in every build, not only with STACK_OPTION_SEGMENTATION; the AWAIT_RESPONSE timeout scan reads them in every build.
  • rb e-20261001-209 (#2609 CR2-F05-025): a duplicate segment of a segmented Complex-ACK within the current window (135-2024 cl. 5.4.4.4 DuplicateSegmentReceived) is discarded without a SegmentACK; it used to be answered with a positive one. Not reachable while the receive window is fixed at 1.
  • rb e-20261001-209 (#2609 CR2-F05-042): the Complex-ACK, Simple-ACK, Segment-ACK and Unconfirmed-Request decoders reset the PDU before refusing a NULL or too-short buffer, so a reused PDU no longer keeps the previous decode’s validity and fields.
  • rb e-20261001-209 (#2609 CR2-F05-055): a raw confirmed request’s service-choice octet above 63 is no longer converted to BACnetConfirmedServiceChoiceEnum (undefined behaviour; its value range is 0..63). GetServiceChoice() reports such an octet as 63, and the Confirmed-Request PDU takes a raw request’s wire octet from GetRawServiceChoice(), so the caller’s octet still reaches the wire for an unsegmented request. The segmented send path still goes through BACnetConfirmedRequestPDU::GetServiceChoice()’s own cast; that is #3459.
  • rb e-20261001-210 (#3459): a Confirmed-Request’s service-choice octet above 63 is never converted to BACnetConfirmedServiceChoiceEnum (undefined behaviour; its value range is 0..63). BACnetConfirmedRequestPDU::GetServiceChoice() reports such an octet as the new BACnetConfirmedServiceChoice::unknownServiceChoice (63, unassigned, no dispatcher case), and the new GetServiceChoiceOctet() (on the PDU, and virtual on BACnetConfirmedServiceRequest) is the wire octet. The segmented send path now encodes a raw request’s octet unchanged in every segment (it encoded 63 for an octet above 63); the Error-PDU and audit-initiation checks also use the wire octet.
  • rb e-20261001-210 (#2609 CR2-F05-056): BACnetStackUnknownAPDU::Reset() clears validity, so a reused value whose decode fails is no longer left valid; IsEqual compares the tag ([1] 0x05 and [2] 0x05 were equal); ToString() renders the tag number and the bytes as hex instead of an empty string; the invalid-parameters log names the right class. Visible change: DecodeAs JSON/XML now renders an unknown context-tagged value as text such as [1] 0x0AF5, where it rendered empty before.
  • rb e-20261001-210 (#2609 CR2-F05-010/032/047/052, comments only): SEGMENTED_RESPONSE cited as cl. 5.4.5.4; abort and reject reasons as cl. 21.4; the audit services as cl. 13.19/13.20/13.21; the confirmed and unconfirmed service-choice, abort-reason, reject-reason and error-code ASN.1 comment blocks refreshed from 135-2024; a stale “#1844 not implemented” note and the Confirmed-Request service-request ... OPTIONAL removed.
  • rb e-20261001-211 (#3461, #2609 CR2-F05-056 item 5): BACnetStackUnknownAPDU held its value in a fixed 1497-octet (one NPDU) array, so a context-tagged value longer than that - which a reassembled segmented message can carry - was refused and the whole ANY failed to decode. The value is now stored sized to its length, up to BACnetStackUnknownAPDU::MAX_VALUE_LENGTH, the larger of the two reassembly caps (65000 octets on the FULL target, 16384 embedded); a longer value is refused by SetBuffer() and Decode(). Each instance no longer embeds and clears a 1497-octet array.
  • rb e-20261001-211 (#3461): a copy (copy constructor, assignment or Clone()) of an invalid BACnetStackUnknownAPDU came out valid; it now keeps the source’s validity.
  • rb e-20261001-211 (#2609 CR2-F05-054): the AuditNotification / AuditLogQuery / AuthRequest cases in the confirmed and unconfirmed service-request factories and request processors, and BACnetAuthRequestProcessor, are guarded by their classes’ own options (STACK_OPTION_OBJECT_TYPE_AUDIT_LOG || _AUDIT_REPORTER, STACK_OPTION_AUTH_REQUEST) as well as BACNET_STACK_TESTTOOL. No change to a test-tool build at the default protocol revision.
  • rb e-20261001-211 (#2609 CR2-F05-046): removed BACnetErrorBase::BACnetErrorFactory(const BACnetErrorBase&), which had no caller and downcast on the error choice alone; Clone() is the polymorphic copy.
  • rb e-20261001-212 (#2609 CR2-F05-031): BACnetConfirmedRequestPDU::DecodeUsingSupportedServices refused a recognised service that no device supports with Reject(unrecognized-service) but left HasDecodeRejectReason() false, the one reject site in either decode entry point that did not record its reason. It now records it, so a caller that answers from the recorded reason sends the Reject rather than Abort(other).
  • rb e-20261001-212 (#2609 CR2-F05-053): the clone-by-reference service factories (BACnetConfirmedRequestFactory(const BACnetConfirmedServiceRequest&), BACnetConfirmedServiceAckFactory(const BACnetConfirmedServiceACK&), BACnetUnconfirmedRequestFactory(const BACnetUnconfirmedServiceRequest&)) refused a valid BACnetGetAlarmSummaryRequest, so a Confirmed-Request PDU built from one was invalid. They now copy any valid request or ACK through its own Clone().
  • rb e-20261003-213 (#3454): a ReadPropertyMultiple-ACK (or any Complex-ACK, segmented or not, or a received COV notification) carrying one Date or Time whose octets are outside 135-2024 cl. 20.2.12 / 20.2.13’s ranges - a field device sending a4 00 00 00 00 (month, day and weekday 0) for “never set” - failed to decode as a whole, so DecodeAs returned nothing and the client lost every property of the object, Object_Name included. On these client decode paths the value now decodes with its raw octets kept and IsOutOfRange() set, and the rest of the response decodes. Copies (operator=, Clone, and BACnetTimeStamp copies) keep the octets and the flag. Such a value is never encoded (Encode refuses it), and BACnetDateTime::ToTimeT/OnOrAfterTime/BeforeTime refuse it. Incoming requests stay strict: a WriteProperty, WritePropertyMultiple or any other request carrying such a value is refused, and received event and audit notifications (which local Event Log, Notification Forwarder and Audit Log objects keep and re-send) are unchanged.
  • rb e-20261003-213 (#2608 CR2-F03-043): BACnetPrimitiveDATE/BACnetPrimitiveTIME::Decode refused an out-of-range octet without setting an error, so a request carrying one was answered Abort(other). It is now Reject PARAMETER_OUT_OF_RANGE; a buffer too short for the value or an undecodable tag is Reject INVALID_TAG (was OTHER), as for REAL and DOUBLE.
  • rb e-20261003-213 (#2608 CR2-F03-042): REAL, DOUBLE, DATE and TIME Encode bound the contents octets against the space the tag left, so the content write no longer depends only on GetMinimumMemoryCount() (which counts an extended context tag since #2905).
  • rb e-20261003-215 (#3470 item 1): the four COV-notification test-tool send exports log an error and return false when the stack is built without STACK_OPTION_SUBSCRIBE_COV (the Multiple forms: STACK_OPTION_SUBSCRIBE_COV_PROPERTY_MULTIPLE), instead of leaving an unresolved reference that failed the link. Every supported test-tool build turns both options on, so no shipped configuration changes. IFC-074.
  • rb e-20261003-215 (#3470 item 4): the test-tool header described the packed COV value buffer’s multi-byte fields as little-endian; they are read in host byte order, which is little-endian on every supported target. IFC-074.
  • rb e-20261003-217 (#3474 item 3): BACnetStackTestTool_SetAccessPointLockout pushes its LOCKOUT_OTHER / LOCKOUT_RELINQUISHED, Lockout and Failed_Attempts through the value-update path, so COV subscribers and the ACCESS_EVENT algorithm see them, as they already did for the Tick’s relinquish and a WriteProperty of Lockout. IFC-076.
  • rb e-20261003-218 (#3476 item 5): the Access Credential’s changes - its test-tool setters, UseAccessCredential / AccessPointRequestAccess, a WriteProperty of Credential_Disable and the credential Tick - are pushed through the value-update path, so COV subscribers (Last_Access_Point, cl. 12.35.19) and intrinsic reporting see them. IFC-077.
  • rb e-20261003-221 (#3483 item 8): an Access Point the application serves (BACnetStack_AddObject, no stack-held engine) whose host does not serve Access_Event_Time now answers it with the cl. 12.31.29 no-update value (dateTime, X’FF’ in every octet) instead of aborting the ReadProperty; Credential Data Input Update_Time likewise (cl. 12.36.11). A WriteProperty of an Event Enrollment’s Fault_Parameters under a property-states choice it cannot serve is refused with PROPERTY / VALUE_OUT_OF_RANGE. IFC-080.
  • rb e-20261008-1 (#3666, CR3-F09-021): with SC_Direct_Connect_Initiate_Enable TRUE, a unicast to a hub peer with no established direct connection was not sent. This covered “hubUri;VMAC” and every reply to a frame the hub relayed. Instead the stack sent an Address-Resolution, and a peer that accepts no direct connections was never reached. 135-2024 AB.4.2.1: “the unicast BVLC message shall be sent through the direct connection with the connection peer node matching the destination VMAC, if one exists. If no such connection exists, the unicast BVLC message shall be forwarded to the hub connector.” The frame now goes through the Connected direct connection whose handshake peer VMAC is the destination; that connection can be initiated or accepted, and the accepted one was never selected before. Otherwise the frame goes through the hub. The Address-Resolution and the connection attempt now follow the send as a side effect. Only one Address-Resolution per VMAC is outstanding at a time. Wire effect: such a send now returns success and reaches the hub, followed by the Address-Resolution when one is due. Not changed: only one initiated direct connection exists, so other destinations stay on the hub while it is in use. A peer whose Address-Resolution-ACK lists no usable URI is still sent a new Address-Resolution after each unicast to it.
  • rb e-20261008-2 (#3659, CR3-F09-006): the BVLC-Result NAKs sent by the hub function and the direct-connect acceptor never carried a Destination-Virtual-Address, even when the refused message had an Originating-Virtual-Address. 135-2024 AB.3.1.2: “In all other response messages, the ‘Destination Virtual Address’ shall be the VMAC indicated in the ‘Originating Virtual Address’ parameter of the message being responded to.” These NAKs are now addressed to the request’s Originating-Virtual-Address when it has one, as the hub connector’s NAKs have been since #2574. This covers the decode-failure NAK, the header-check NAKs and the destination-option NAK.
  • rb e-20261008-2 (#3660, CR3-F09-007): PAYLOAD_EXPECTED was sent only for a frame of exactly 4 octets. A message that requires a payload, carried VMACs or header options, and had no payload was NAKed MESSAGE_INCOMPLETE instead. The commonest case is a Connect-Request carrying only its Hello option. The hub connector applied the rule to Encapsulated-NPDU only, so a header-only Advertisement got MESSAGE_INCOMPLETE even at 4 octets. 135-2024 AB.3.1.5: “If a BVLC message is received for which a payload is required, but no payload is present, a BVLC-Result NAK shall be returned if it was a unicast message indicating an ‘Error Class’ of COMMUNICATON and ‘Error Code’ of PAYLOAD_EXPECTED.” AB.2.1: “The remaining octets of the BVLC message, if any, are the variable size ‘Payload’ parameter”. The payload is now taken to start after the Originating- and Destination-Virtual-Address and both header option lists. The hub function, the direct-connect acceptor and the hub connector all use this one rule. 135.1-2023 12.5.1.2.2 accepts either error code for its truncated Encapsulated-NPDU.
  • rb e-20261008-2 (#3664, CR3-F09-014): SC_Failed_Connection_Requests did not record a Connect-Request that the hub function or the direct-connect acceptor refused in its header and option checks. Those checks cover reserved control flags, a Virtual Address, Data Options, trailing octets, a missing Hello option and an unknown Must-Understand option. The refusal was NAKed but never recorded. The most common case is a peer that sends no Hello option. 135-2024 cl. 12.56.99: the property “indicates information about failed connection requests received by an enabled hub function or a node switch enabled for accepting a BACnet/SC direct connection”, and “only errors resulting from the exchange of BVLC messages are required to be included in this property.” Each of these refusals is now recorded once, with the VMAC and Device UUID the request carried. A Connect-Request that does not decode is still recorded once, without them.
  • rb e-20261008-2 (#3662, CR3-F09-012): the hub function answered a Disconnect-Request from a connected peer with a Disconnect-ACK and forgot the peer, but it never closed the WebSocket. With the peer gone from every table, nothing ever closed that socket. A peer could repeat connect and Disconnect-Request to hold host WebSockets beyond the hub’s connection limit. 135-2024 AB.6.2.3: “On receipt of a Disconnect-Request message from the initiating peer node, respond with a Disconnect-ACK message, close the WebSocket connection, and enter the IDLE state.” The WebSocket is now closed after the Disconnect-ACK is sent. The direct-connect acceptor already did this (#2596).
  • rb e-20261008-3 (#3667, CR3-F09-027): a write to the file data of the BACnet/SC Network Port’s Certificate_Signing_Request_File (AtomicWriteFile, or File_Size) set Changes_Pending. The port then refused GENERATE_CSR_FILE and RESTART_PORT with PROPERTY / INVALID_VALUE_IN_THIS_STATE until the next ReinitializeDevice, and activation changed nothing. 135-2024 gives the Changes_Pending sentence to the Operational_Certificate_File (cl. 12.56.100: “A successful change to the file data in the File object referenced by this property shall set the Changes_Pending property to TRUE”) and Issuer_Certificate_Files (cl. 12.56.101) only; cl. 12.56.102 has none, and cl. 12.56.16 GENERATE_CSR_FILE “shall not affect the Changes_Pending property”. A CSR file write now leaves Changes_Pending unchanged. Writes to the operational and issuer certificate files still set it.
  • rb e-20261008-4 (#3658, CR3-F09-005): in a build without STACK_OPTION_ROUTER_PHYSICAL, the BACnet/SC hub function NAKed and dropped an Encapsulated-NPDU between two other nodes whenever this stack could not decode its APDU, such as a service it has no decoder for. The NPCI-only decode the relay needs was compiled only for the router. 135-2024 AB.5.1: “Unicast BVLC messages received from a hub connection shall be forwarded by the hub function to the hub connection where the destination VMAC address matches the VMAC address of the connection peer node.” and “Broadcast BVLC messages received from a hub connection shall be duplicated and a copy shall be sent to all hub connections except the one from which it was received.” The fallback is now compiled in every BACnet/SC build, and the hub function always uses it. An undecodable APDU addressed to this node is still never delivered to the application layer; the sender gets the decode-failure NAK, as before. Verification note: the CI build (STACK_OPTION_TARGET_FULL) has STACK_OPTION_ROUTER_PHYSICAL on, and there the old code already compiled the fallback, so the new tests pass with or without the fix. Their failure without the fix was shown in a local build with STACK_OPTION_DISABLE_ROUTER_PHYSICAL. That build needed one local patch to compile: the #3347 tests in tests/BACnetSC_Tests/TestBACnetInterface_BACnetSC.cpp call SetForwardedApdu, which exists only with STACK_OPTION_ROUTER_PHYSICAL, without a guard.
  • rb e-20261008-4 (#3661, CR3-F09-011): a broadcast from a hub-function peer with no Originating-Virtual-Address, which is the usual case, reached no other node at all when it carried an APDU this stack cannot decode. To add the missing address the relay decoded and re-encoded the whole frame, without the NPCI-only fallback. This happened in every build. 135-2024 AB.5.3.3: “On receipt of a broadcast BVLC message from a hub connection, the hub function shall send a copy of the received message through each current hub connection, except the hub connection through which it was received.” The relay now rewrites only the header. It inserts the peer’s VMAC as the Originating-Virtual-Address, keeps the Destination-Virtual-Address, and copies the options and the NPDU unchanged, as the unicast relay has done since #2584.
  • rb e-20261008-4 (#3674, CR3-F09-045): a request to a remote network sent through this node’s own hub function (its accept URI) was not addressed to the router for that network. A 6-octet DADR became the Destination-Virtual-Address, and any other DADR left the frame with none. Either way the frame went only to the accept URI and was lost. The hub function also never learned router VMACs. 135-2024 cl. 6.5.3: “The DA portion of the ‘destination_address’ parameter passed to the data link layer shall be the MAC address of the BACnet router corresponding to the DNET parameter or the appropriate broadcast DA if the address of the router is initially unknown.” The #2893 rule now applies on this path as well. The request is relayed to the learned router while that router is still connected to the hub function. Otherwise it is broadcast to every peer, and that broadcast is the whole delivery: nothing is sent to the accept URI, so the request is not reported failed and its transaction is kept. The hub function’s local delivery now learns router VMACs from frames carrying a source network in every build, and from I-Am-Router-To-Network in builds with STACK_OPTION_ROUTER. On a peer’s own connection string an absent Destination-Virtual-Address still names that peer, and a 6-octet DADR no longer becomes the Destination-Virtual-Address there: with a destination network present, the DADR is never a VMAC on this network.
  • rb e-20261008-5 (#3671, CR3-F09-032): when the hub connector failed back from the failover hub to the primary hub, it closed the established failover connection without sending a Disconnect-Request, so the failover hub recorded a lost connection. 135-2024 AB.6.2.2, in state CONNECTED: “On locally determined disconnection of the connection, send a Disconnect-Request message to the connection peer node, start the disconnect wait timer, and enter the DISCONNECTING state.” The failover connection now gets a Disconnect-Request and is closed when its Disconnect-ACK or a NAK arrives, or when the disconnect wait timer expires. The primary hub connection carries traffic throughout. Heartbeat-Requests still pending on the failover connection are dropped at fail-back, so the primary’s heartbeat check does not count them as its own. Closing a hub websocket because its URI changed, and SuspendCommunication, are not changed; they are #3524.
  • rb e-20261008-5 (#3722): follow-ups to #3666 and #3672 in the BACnet/SC hub connector.
    • A peer whose Address-Resolution brings no usable URI is no longer resolved again after every unicast sent to it. That covers an ACK listing no URI (a peer that accepts no direct connections), a NAK, or no answer within 10 s. The peer is backed off for 60 s from the request, doubling with each consecutive failure up to 600 s. A usable URI clears that peer’s backoff. Any hub connection being made, including the fail-back from the failover hub to the primary, clears every peer’s backoff.
    • Pending Address-Resolutions are now looked up by VMAC instead of by scanning every tracked message. At most 64 VMACs are held; when the table is full, the oldest request is dropped.
    • A response matches only a request sent on the same hub websocket (primary or failover).
    • An Address-Resolution-ACK that does not answer the tracked request (wrong or missing Originating Virtual Address, or the ID of a request that is not an Address-Resolution) is ignored, and the request stays tracked, as #3672 does for a BVLC-Result whose Originating Virtual Address does not match. Previously, a forged ACK cleared the pending Address-Resolution. The other responses (BVLC-Result, Connect-Accept, Disconnect-ACK, Heartbeat-ACK) still untrack a request whose BVLC function they do not match; they can reach that check only from the hub or from the resolved peer.
    • An Address-Resolution left pending by a hub NAK that has no Originating Virtual Address now expires.
    • The connection string is now rewritten onto the direct connection in a copy, not in the caller’s network parameters. A caller that sent again with the same parameter object went to the direct connection’s socket after it had closed, and the frame was dropped instead of going through the hub; that caller now goes through the hub. This does not fix the transaction state machine’s retries of a confirmed request: the transaction is created from the rewritten copy, its key stores the direct URI, and its retries still resend to that URI.
  • rb g-20260924-18 (#2481): sizing a confirmed audit-notification request, which a segmented ConfirmedAuditNotification send does, no longer calls through an invalid cast. BACnetSequenceOf<T>::GetMinimumMemoryCount now calls each element’s own method; before, BACnetSequenceOf<BACnetAuditNotification> crashed (a segmentation fault in the new test).
  • rb g-20260924-19 (#2519): a router that receives Who-Is-Router-To-Network for a network it does not know now searches onward (cl. 6.6.3.2). It broadcasts the request out every other port, adding SNET/SADR, rate-limited by the search bucket. There is no onward search when the request’s SNET is directly connected (loop guard) or when it arrived as a global broadcast.
  • rb g-20260924-19 (#2520): network-layer messages whose DNET is present and not local are relayed like data NPDUs (cl. 6.5.2.2/6.5.4) instead of being executed locally; the BACnet/SC receive path now hands them to the router too. What-Is-Network-Number and Network-Number-Is are never routed; Reject-Message-To-Network keeps its own relay. See docs/CAS BACnet Stack - Routing PICS Deviations.md entries 6 and 13-15 for the remaining gaps.
  • rb g-20260924-20 (#2646, security): a UTCTimeSynchronization whose date converts before 1970 or outside the BACnet year range no longer crashes Windows builds (a NULL gmtime() result was dereferenced), and no longer passes year 0xFF to CallbackSetSystemTime. The conversion is reentrant (gmtime_r/gmtime_s), and an unconvertible or out-of-range date is ignored. On Windows the platform’s _mkgmtime cannot convert dates before 1970, so those dates are refused there, while POSIX builds accept 1900-1969.
  • rb g-20260924-20 (#2645): the time-change records that UTCTimeSynchronization writes to Trend Log, Trend Log Multiple and Event Log now use the DST-aware UTC offset (as #2450 did for the other log records).
  • rb g-20260924-20 (#2648): a You-Are that assigns a Device instance now re-keys the device’s own Device object and its virtual-router entry, instead of overwriting the instance and losing the Device object. Instance 4194303, or an instance already used by another device on the data link, is refused.
  • rb g-20260924-21 (#2631): ReadRange By Time with a negative Count near the oldest record no longer returns records newer than Reference Time, and on a buffer of more than 32767 records it no longer restarts from record 0. This is fixed in Trend Log, Event Log and Trend Log Multiple, where the count is clamped and the start index is now computed in size_t (no int16 narrowing).
  • rb g-20260924-22 (#2711): a time-delayed LIFE_SAFETY_ALARM transition now happens. pendingTimeDelay_LifeSafetyAlarm, added by #632, was dropped as noChange by the alarm-and-event object, and was never queued by CheckForAlarmsAndEvents. So a Life Safety Point or Zone whose Time_Delay was non-zero never reached LIFE_SAFETY_ALARM (cl. 13.3.8 (b), as quoted in the issue). A new IsPendingTimeDelayResult() helper now replaces the four hand-written case lists, so they cannot drift apart again.
  • rb g-20260924-22 (#2717): CHANGE_OF_STATE keeps its own copy of the alarm value that caused the OFFNORMAL transition, instead of a pointer into Alarm_Values. Re-arming Alarm_Values while OFFNORMAL no longer reads freed memory or causes a spurious TO_OFFNORMAL.
  • rb g-20260924-22 (#2727): CHANGE_OF_LIFE_SAFETY no longer evaluates while the object is in FAULT (or any state other than NORMAL, OFFNORMAL or LIFE_SAFETY_ALARM). Before, a repeat evaluation during a persisting fault returned runError and could drop Event_State to NORMAL (cl. 13.3, as quoted in the issue). The pending timers are cleared, and Mode is still tracked.
  • rb g-20260924-23 (#2732): the CHANGE_OF_RELIABILITY notifications from FAULT_STATE, FAULT_CHARACTERSTRING, FAULT_LIFE_SAFETY, FAULT_STATUS_FLAGS and FAULT_EXTENDED were invalid because they had no status-flags, so their Event Values were dropped. They now carry status-flags: IN_ALARM and FAULT describe the state being entered, as the first-stage builder already did (#2349), and OVERRIDDEN and OUT_OF_SERVICE come from the object.
    • FAULT_STATE and FAULT_CHARACTERSTRING also carry the monitored value, labelled with the property the object type’s fault detection monitors. On an Access Door that is Door_Alarm_State.
    • FAULT_LIFE_SAFETY carries Mode.
    • The Table 13-5 values that are still missing are tracked in #2763.
  • rb g-20260924-23 (#2735): FAULT_OUT_OF_RANGE no longer treats a NaN sample, or a sample whose datatype differs from the limits, as in range. Before, such a sample cleared an UNDER_RANGE or OVER_RANGE fault to NO_FAULT_DETECTED. A configuration whose datatypes differ is still silently inert; see #2780.
  • rb g-20260924-23 (#2737): FAULT_OUT_OF_RANGE refuses fault parameters whose min and max are different kinds of value (Real, Unsigned, Double or Integer). A Fault_Parameters write with a mixed pair returns Result(-) PROPERTY / VALUE_OUT_OF_RANGE and keeps the previous configuration.
  • rb g-20260924-24 (#2469): reading Lighting_Command on a Lighting Output that has never been commanded now returns { operation: none } (cl. 12.54.6, as quoted in the issue) instead of Error(property, other). The stored command is now created with operation NONE, so ReadPropertyMultiple ALL on such an object returns a value for Lighting_Command. The spec quote has not yet been checked against the extracted 135-2024 text, because the text was not on the machine that made the fix.
  • rb g-20260924-25 (#2738, security): BACnetNotificationParametersChangeOfReliability::Decode no longer leaks the property-value element when that element fails to decode. Before this fix, every malformed CHANGE_OF_RELIABILITY notification received from the wire leaked one allocation.
  • rb g-20260924-26 (#2739): in builds where the access-event [13] or change-of-timer [22] arm is compiled out (no ACCESS_POINT or TIMER object, for example EMBEDDED B_ALSC), BACnetNotificationParameters::Decode now reports Error(services, optional-functionality-not-supported) instead of a false Abort(out-of-resources). The reply sent to the peer is unchanged: BACnetEventNotificationRequest still maps any eventValues decode error to Reject(invalid-tag), and the notification is still dropped. Fixing that is #2869.
  • rb g-20260924-27 (#2741): CHANGE_OF_TIMER notifications from a stack-owned Timer’s intrinsic reporting now carry the Timer’s values:
    • Update_Time, which was always sent as “unspecified” in production;
    • Last_State_Change, Initial_Timeout and Expiration_Time, which were never sent at all. An Update_Time that was never set is still sent as the unspecified wildcard, and an optional field the Timer has no value for is omitted. CHANGE_OF_TIMER through an Event Enrollment is not wired (#2876).
  • rb g-20260924-28 (#2675, security): LifeSafetyOperation no longer treats a present but malformed object identifier [3] as absent, whether it arrives primitive or constructed (opening tag). Before, a request with a malformed [3] was accepted as an untargeted operation (for example RESET), which unlatched every Life Safety Point and Zone on the device. It is now rejected with reason invalid-tag.
  • rb g-20260924-28 (#2674): the LifeSafetyOperation Request field is no longer narrowed to 8 bits after decode.
    • A value above 65535 is rejected at decode, with reason parameter-out-of-range.
    • A legal proprietary value from 256 to 65535, which the 8-bit callback cannot carry, is answered Error(OBJECT, VALUE_OUT_OF_RANGE) before the callback or the stack’s own reset runs. Before, it was delivered modulo 256, and could act as a different operation.
    • Copying a request keeps the full Request and Requesting Source.
  • rb g-20260925-1 (#2805): a Life Safety Zone that is Out_Of_Service keeps its written Tracking_Value. Before, a roll-up reached without callbacks overwrote it: a member’s tracking change, a broadcast LifeSafetyOperation, or SetLifeSafetyZoneMembers. The zone is still not recomputed when Out_Of_Service goes back to FALSE (#2907).
  • rb g-20260925-1 (#2807): the CHANGE_OF_LIFE_SAFETY feed no longer overwrites the host-armed pOperationExpected with NONE. The object’s Operation_Expected is fed only once it has been set. ReadProperty Operation_Expected still reads NONE unless it is set, so for now it can differ from the value in notifications (#2907).
  • rb g-20260925-1 (#2808): Silenced returns to UNSILENCED when Present_Value makes a new transition to a non-NORMAL value, whether from new input, one of the three RESET operations, or the zone roll-up. A transition on a zone driven by the roll-up needs a spec ruling (#2907).
  • rb g-20260925-2 (#2664, security): the COV-multiple subscription table is now bounded per (object, property). A new reference is refused once MAX_SUBSCRIPTION_PER_OBJECT references, counted across all subscribers, already watch that property of that object. Before, one peer could grow the table without limit. The refusal is reported as SubscribeCOVPropertyMultiple Result(-) with the First Failed Subscription (RESOURCES / NO_SPACE_TO_ADD_LIST_ELEMENT). References earlier in the same request stay stored and get their initial notification, and a renewal is never refused.
  • Known limits: the single-property and multiple tables are bounded separately, so the effective per-property limit is 2 x MAX. There is no per-peer bound.
  • rb g-20260925-2 (#2663): regression tests only. The COV-multiple context key was already fixed by #2757.
  • rb g-20260925-3 (#2412, critical, security): a ReinitializeDevice ACTIVATE_CHANGES, WARMSTART or COLDSTART with a wrong or missing password no longer activates pending Network Port changes (Network_Number, and the virtual router’s routing map) or BACnet/SC changes. This implements the owner ruling of 2026-09-25 (option 2):
    1. every pending value is checked for admissibility, and nothing is changed;
    2. CallbackReinitializeDevice validates the password;
    3. only if it returns true are both activation passes applied, and then the SimpleACK is sent. A value that would fail is refused before the host is called, so the two passes are effectively atomic. The BACnet/SC activation status text was also shortened so it fits the EMBEDDED 64-character string limit. Before, it could fail partway through activation.
  • rb g-20260925-4 (#2907, part 1): writing Out_Of_Service = FALSE to a Life Safety Zone now recomputes the zone from its members at once, together with any zone that contains it. Before, the zone kept its written Tracking_Value until a member changed. A host that clears Out_Of_Service in its own database, without a WriteProperty, still leaves the zone stale; v6 has no setter for this.
  • rb g-20260925-4 (#2907, part 3): arming CHANGE_OF_LIFE_SAFETY on a stack-held Life Safety Point or Zone now seeds the object’s Operation_Expected with the armed pOperationExpected. ReadProperty Operation_Expected therefore agrees with the value carried in notifications.
  • rb g-20260925-5 (#2925, part 2): a successful WriteProperty or WritePropertyMultiple of Reliability to an object on a virtual-router child device is now evaluated at the next Tick, against that child’s own device. Its notification comes from the child and goes to the child’s recipients. Before, only the main device’s record was drained, so a write to a child’s object was recorded but never evaluated.
  • rb g-20260925-5 (#2925, part 3): a Reliability write to an object that only an Event Enrollment monitors, with no intrinsic alarming, now re-evaluates that Event Enrollment at the next Tick (cl. 13.2.2.3, as quoted in the issue).
  • Still open: #2925 part 1 (owner question: whether a written Reliability under Reliability_Evaluation_Inhibit with Out_Of_Service TRUE drives Event_State) and #3003 (child devices’ time-delay queue).
  • rb g-20260925-6 (#2499): the responding TSM’s request timer (AWAIT_RESPONSE / SEGMENTED_REQUEST) armed APDU_Timeout, which is in milliseconds, on a seconds clock. It therefore expired about 1000 times too late. It now expires after APDU_Timeout, rounded up to whole seconds with a 1-second floor.
  • rb g-20260925-6 (#2494): a SegmentACK during a segmented response now fills the whole window, up to the final segment, instead of sending a single segment. Every segment carries the proposed window size stored for that transaction.
  • rb g-20260925-6 (#2495): while a segmented request is being reassembled, a stray SimpleACK, ComplexACK, Error or Reject, or a SegmentACK or Abort with server=TRUE, is now ignored instead of aborting the transaction.
  • rb g-20260925-7 (#2493): the choice between a segmented and an unsegmented message now compares the whole APDU (header and payload) with the peer’s Max-APDU. This applies in the responding TSM, and in both the ComplexACK and confirmed-request branches of BACnetPacket::CreateSegment. Before, the check compared only the payload, with two effects:
    • a ComplexACK 1-3 octets over the client’s limit was refused with Abort(segmentation-not-supported), even to a client that accepts segmentation;
    • on IP, a confirmed request whose APDU was 1477-1480 octets failed to send, with nothing sent.
  • rb g-20260925-7 (#2500): a negative SegmentACK during a segmented response now acknowledges the segments up to its sequence number and sends from the next one onward: held segments are resent, then new segments are cut. Before, the stack went back to the start of the window and retransmitted it.
  • rb g-20260926-2 (#2718): CHANGE_OF_STATUS_FLAGS now clears its time-delay timers on its final no-change return, as the sibling algorithms do. An abandoned pending transition no longer fires later.
  • rb g-20260926-2 (#2723): the CHANGE_OF_STATUS_FLAGS notification no longer sends the monitored flags as “present-value”. That field is optional and is now omitted.
  • rb g-20260926-2 (#2719): CHANGE_OF_VALUE (both forms) and CHANGE_OF_DISCRETE_VALUE now evaluate only while the object is NORMAL. While it is in FAULT they send no change notifications and drop any pending time delay. On FAULT to NORMAL they take a fresh baseline, so a value that drifted during the fault does not produce a notification right after recovery.
  • rb g-20260926-2 (#2721, #2722): CHANGE_OF_CHARACTERSTRING keeps each value’s character set. The TO-NORMAL notification reports the alarm value that matched, with its own character set, and changed-value carries the monitored value’s character set.
  • Still open: #2720 (NaN baseline, and an owner question on rejecting a NaN or negative increment).
  • rb g-20260926-3 (#2712): the event algorithm no longer runs while the object is in FAULT, so an event-algorithm result can no longer take the object out of FAULT.
    • An EXTENDED indication made during the fault is discarded.
    • CHANGE_OF_LIFE_SAFETY resets its Mode baseline when the object leaves FAULT, so a Mode change made while faulted does not produce a spurious TO_NORMAL.
  • rb g-20260926-3 (#2713): writing Event_Algorithm_Inhibit TRUE now takes an OFFNORMAL object to NORMAL, with a TO_NORMAL notification if Event_Enable allows. The write queues an evaluation, so this happens at the next Tick even when the monitored value is steady. Clearing the inhibit, or disabling event detection, discards any pending time delay.
  • rb g-20260926-3 (#2714): a NORMAL-to-NORMAL transition (for example BUFFER_READY, or a life-safety Mode change) is now handled as a full TO_NORMAL transition, with its time stamp, Acked_Transitions and stored notification parameters. An AcknowledgeAlarm that uses the notification’s time stamp now succeeds instead of returning INVALID_TIME_STAMP.
  • rb g-20260926-4 (#2801, security): Access Rights no longer matches a rule’s Time-Range or Location that names another device against the local object with the same number. Such a reference now evaluates FALSE (cl. 12.34.9.1, as quoted in the issue), and so does a device identifier used when there is no local device.
    • A positive rule with a remote reference therefore never grants access (fail-closed).
    • A negative rule with a remote reference never denies access (fail-open); each skip is logged. The owner is asked on #2801 whether this is acceptable.
    • Access Zone Entry_Points and the presented Access Point still ignore the device identifier; see #3020.
  • rb g-20260926-4 (#2803): each BACnetAccessRule setter re-computes validity. A rule is valid only when Enable is set and the rule is consistent, so a previously valid rule made inconsistent is no longer reported, or encoded, as valid.
  • rb g-20260926-4 (#2799): a reader push to a Credential Data Input that is Out_Of_Service is now ignored. Present_Value and Update_Time are left unchanged, and no value update runs.
  • rb g-20260926-5 (#3020, security): Access Zone Entry_Points and the presented Access Point now compare the device identifier as well as the object type and instance. Another device’s Access Point with the same number no longer matches a local Entry_Point or a local Access Point rule.
    • A positive rule therefore does not grant access at another device’s point (fail-closed).
    • A negative rule does not deny there (fail-open, consistent with #2801), and each such case is logged.
    • Same class, still open: an Access Credential’s Assigned_Access_Rights reference also ignores the device identifier (#3023).
  • rb g-20260926-5 (#2804): BACnetAccessToken::SetComplete() now returns bool, and marks the token valid only when all eight mandatory fields are present: issuer, issued, audience, client, constraint, scope, key-id and signature. An empty audience counts as present.
    • BACnetStack_InsertAuthorizationToken (test tool) now fills the fields it has no parameters for, the same way the AuthRequest policy path does: issued unspecified, constraint any-network/certified, key-id 0, empty signature and a scope with no bits set. A token with no scope bits is never found by a scope-bit lookup (#3022).
    • InsertAuthorizationToken no longer leaks the audience element when the insert fails.
  • rb g-20260926-6 (#3023, security): an Access Credential’s Assigned_Access_Rights entry that names another device is no longer evaluated against the local Access Rights object with the same instance. It is ignored, like a non-existent object (12.35.18).
    • Its positive rules do not grant (fail-closed).
    • Its negative rules do not deny, and the local object’s negative rules no longer deny through it (fail-open, consistent with #2801); each case is logged.
    • Access Zone Entry_Point matching now also requires a present device identifier to be of object type device. This feeds positive and negative zone rules alike: two identical non-device identifiers no longer match, so a zone positive rule does not grant (fail-closed) and a zone deny rule does not deny (fail-open, consistent with #2801) on that malformed input.
  • rb g-20260926-6 (#2790): when an Access Door’s pulse unlock (or unlock delay) ends on a Loop tick and Present_Value changes, it is pushed through UpdateValue, so COV subscribers, COV trend logs and intrinsic reporting see the change (135-2024 cl. 13.1 Table 13-1).
  • rb g-20260926-6 (#2791): an Access Point’s Out_Of_Service changing FALSE to TRUE sets Access_Event to OUT_OF_SERVICE, and TRUE to FALSE sets OUT_OF_SERVICE_RELINQUISHED, each as a new Access_Event_Tag (cl. 12.31.8). Out_Of_Service is host-owned, so it is sampled once per Loop tick through the host’s bool getter; the first sample is a baseline, and a declined read is no sample.
    • Known limitation (same class as the open #2795): when the Access Point’s own tick fires an event (for example LOCKOUT_RELINQUISHED) and an Out_Of_Service edge fires in the same Loop tick, Access_Event_Tag moves by 2 but only the second event reaches the ACCESS_EVENT notification.
  • rb g-20260926-6 (#2792): Access_Event_Time is served as a BACnetTimeStamp (the dateTime choice), and is all X’FF’ before the first access event, after Reset(), and for an event stamped with no valid time (cl. 12.31.29). It could not be encoded before the first event.
  • rb g-20260926-6 (#2802): BACnetAccessRule accepts time-range-specifier ALWAYS / location-specifier ALL with a present reference that is unspecified (instance 4194303, and device instance 4194303 if present), in SetEnable and Decode (cl. 12.34.9.1). A specified reference under ALWAYS/ALL is still rejected, and SPECIFIED still requires the reference (#946).
  • rb g-20260928-1 (#3003): BACnetStack_Tick now re-evaluates the time-delay pending queue of every device, virtual-router children included. Before, only the main device’s queue was drained, so a child device’s Time_Delay and Time_Delay_Normal transitions never fired while the condition lasted.
    • An empty queue on one device no longer ends the drain for the devices after it.
    • Still main-device-only (outside this fix): the COV subscription purge and the SubscribeCOVPropertyMultiple Loop that Tick drives.
  • rb g-20260928-1 (#3053): COV evaluation treats a Status_Flags shorter than four bits as FALSE in the missing positions. Before, those positions were uninitialised memory, compared against each subscription’s baseline and stored, so a host that answered Status_Flags with fewer bits could cause spurious or missed COV notifications.
    • The revert-red test fills the stack with TRUE before the call; the pre-fix failure depends on that uninitialised memory, not on a test seam.
  • rb g-20260928-1 (#3054): a COV subscription whose property read fails (for example a SubscribeCOVProperty array index past the end of an array that has since shrunk) no longer stops the update. The subscriptions after it, the status-flag recheck and the Value_Source subscriptions are still evaluated.
    • Such a subscription is now also removed at the first update after its lifetime ends. Before, only Tick’s periodic purge could remove it, and that runs for the main device only, so on a virtual-router child device it was never removed.
  • rb g-20260928-1 (#3056): Status_Flags IN_ALARM for BitString Value, Lighting Output and Binary Lighting Output is TRUE when Event_State is present and not NORMAL (135-2024 cl. 12.40.7, 12.54.9, 12.55.6). Before, it was always FALSE, the Channel rule (cl. 12.53.8, unchanged).
    • For these three types, an Event_State the host declines leaves IN_ALARM FALSE instead of failing the Status_Flags read, so COV keeps working for a host that never served Event_State.
    • Binary Lighting Output has no Event_State in the stack’s property profile yet, so its IN_ALARM stays FALSE until it does.
  • rb g-20260928-2 (#2595): the direct-connect initiator now closes the WebSocket when its Connect-Request is NAKed (cl. AB.6.2.2), as the hub connector does. Still open: after a NODE_DUPLICATE_VMAC NAK it does not choose a new VMAC, because which VMAC a direct connection carries is an open question (CAND-F09-E-09).
  • rb g-20260928-2 (#2597): with both direct-connect roles in use, ending one connection no longer reports the other as down or holds its traffic. This covers the accepted peer’s Disconnect-Request, the initiated peer’s disconnection, disabling either role, and ClearDirectConnectAcceptUris. Each role now reads its own connection state.
  • rb g-20260928-2 (#2593): after a failed attempt the direct-connect initiator waits out the configured reconnect timeout before opening a new WebSocket to the same peer (cl. AB.4), using the hub connector’s back-off. Before, it re-initiated on the next Tick, and from inside SetBACnetSCWebSocketStatus when the failure was reported there. Each failure counts once, even when the host reports the close from inside CallbackDisconnectWebsocket, and no WebSocket is opened from inside that callback.
    • A Disconnect-Request that this device sent and the peer acknowledged is not counted as a failure, but the next attempt to that peer still waits the base reconnect timeout. Otherwise two devices that each replace the other’s initiated connection could reconnect in a loop with no back-off.
    • A Hello capabilities change still reconnects on the next Tick.
  • rb g-20260928-2 (#2594, partial): the direct-connect initiator now applies the connect wait timer while its Connect-Request is unanswered (cl. AB.6.2), and sends a Heartbeat-Request every heartbeat timeout while connected (cl. AB.6.3). A Connect-Request that fails to send now closes the socket instead of leaving the initiator waiting forever. Still open: a WebSocket-connect timeout, and disconnecting when a Heartbeat-Request goes unanswered.
    • Both timers reuse the hub connector’s settings: the connect wait timer uses its connect timeout (SC_Connect_Wait_Timeout), and the heartbeat interval uses SC_Heartbeat_Timeout. A value tuned for the hub connection now also applies to direct connections.
  • rb g-20260928-3 (#2568): the BACnet/SC hub function no longer applies Must-Understand destination-option rejection to frames it only forwards (135-2024 AB.2.3: the options are “addressed to the destination node or nodes addressed by the ‘Destination VMAC Address’ parameter”).
    • A unicast for another node is forwarded with its destination options untouched. Before, it was NAKed with HEADER_NOT_UNDERSTOOD and never delivered.
    • A broadcast carrying a Must-Understand option this node does not understand is forwarded to every other connection. Only this node’s own copy is dropped, and it is still never NAKed. Before, the whole broadcast was discarded.
    • A frame for the hub node itself (no DVA, or the hub function VMAC) is checked and NAKed as before.
  • rb g-20260928-3 (#2589): an Address-Resolution sent to the hub host node (no DVA, or the hub function VMAC) now gets a reply (AB.3.3); before, nothing was sent back.
    • With direct-connect accept enabled and an accept URI configured, the reply is an Address-Resolution-ACK carrying the first direct-connect accept URI, as the hub connector already did.
    • Otherwise it is a BVLC-Result NAK: COMMUNICATION / OPTIONAL_FUNCTIONALITY_NOT_SUPPORTED.
    • Known limitation: the ACK is sent whenever accept is enabled and a URI is configured, even if no direct-connect listener actually started. The hub connector’s reply has the same gap.
  • rb g-20260928-3 (#2606): these calls now close the accepted connections (CallbackDisconnectWebsocket) instead of only forgetting them:
  • rb g-20260928-3 (#2618): the hub function now has the AB.6.2.3 connect wait timer. A connection that has not completed Connect-Request within SC_Connect_Wait_Timeout seconds of its first frame is closed with CallbackDisconnectWebsocket, and no BVLC message is sent.
    • The timer reuses the existing SC_Connect_Wait_Timeout setting: the connectTimeout argument of BACnetStack_SetBACnetSCSettings, or the Network Port’s SC_Connect_Wait_Timeout. The direct-connect initiator’s connect wait timer already uses the same setting (g-20260928-2). Integrators who set connectTimeout now also set how long an accepted hub-function connection may wait. The header still describes that parameter as a TCP connect timeout (#3126).
    • Disabling the hub function also closes connections that are still waiting.
    • When the host reports a waiting connection Disconnected or Error (BACnetStack_SetBACnetSCWebSocketStatus), the connection is forgotten. It no longer holds a place in the table, which is bounded at the hub function’s maximum connection count, and a later socket that reuses its connection string gets a full wait. That report now returns true for such a connection.
    • Not covered: a websocket that never sends any frame, because the stack is not told when the host accepts one (#2302).
  • rb g-20261001-1 (#3389): a SubscribeCOVPropertyMultiple request that ends in an Error (plain, or naming the First Failed Subscription) now completes the client’s transaction. The Error used to bypass the responding state machine, so the client’s entry stayed in AWAIT_RESPONSE: when its timer ran out the stack sent Abort(APPLICATION_EXCEEDED_REPLY_TIME) for a request it had already answered. The PlugFest report, on an older build, also had that client’s later confirmed requests dropped until then.
  • rb g-20261001-1 (#3334): when the stack changes an object’s Event_State itself (intrinsic reporting or an Event Enrollment, including the transition made at BACnetStack_Tick when a Time_Delay ends), it now re-evaluates that object’s COV subscriptions. A Status_Flags change caused by the transition (IN_ALARM, FAULT) is sent as a COV notification, and as a COVNotificationMultiple to a SubscribeCOVPropertyMultiple subscription to Status_Flags or Event_State (135-2024 Table 13-1). Before, it was never sent; a host could only work around it by calling BACnetStack_UpdateValue with Event_State.
    • An Event Enrollment’s transition re-evaluates the Event Enrollment’s own subscriptions. The monitored object’s Status_Flags do not change with it.
    • A transition that leaves Status_Flags unchanged (HIGH_LIMIT to LOW_LIMIT) sends nothing.
    • Known double COV: with no Time_Delay, a report that crosses a limit now sends two COV notifications - the one BACnetStack_UpdateValue already sent, with the Status_Flags from before the transition (IN_ALARM FALSE), then one with the new Status_Flags (IN_ALARM TRUE). A test pins this until a follow-up decides whether BACnetStack_UpdateValue should evaluate alarms before COV.
  • rb g-20261001-1 (#3300): an Event Enrollment’s Time_Delay and Time_Delay_Normal transitions now complete at the BACnetStack_Tick that ends the delay, on the main device and on virtual-router child devices. Before, they completed only when the host next called BACnetStack_UpdateValue for the monitored property, so a value that stepped and held never raised its alarm. Hosts that call BACnetStack_UpdateValue periodically to work around it can stop.
    • While an Event Enrollment’s delay is running, each Tick reports its monitored property to the alarm evaluation once, as BACnetStack_UpdateValue would without the COV part - not again when the monitored object’s own intrinsic time delay already had it evaluated in that Tick. The monitored object’s own intrinsic algorithm runs again with the unchanged value too.
    • An Event Enrollment whose Object_Property_Reference names another device is not re-evaluated by Tick.
  • rb g-20261001-10 (#2387, item 2): a SubscribeCOVProperty on a Staging object’s Value_Source now reports on the Staging row of Table 13-1: a Present_Value change smaller than the object’s COV_Increment is no longer reported, and a change to Present_Stage alone now is. Before, every Present_Value change was reported and Present_Stage was not checked. This only matters for a Staging object whose host serves Value_Source, which since #3146 is in the Staging property profile. The host can serve it only through CallbackGetPropertyConstructed, which exists in test-tool builds only, so a production build cannot reach this path. Revert-red: with the fix reverted, the below-increment and Present_Stage-only tests fail.
  • rb g-20261001-10 (#2387, item 4): no source change. The owner ruling (2026-09-25) that an unchanged Present_Value is no change when the increment is 0 (no COV_Increment, or a COV_Increment of 0) was already implemented by #3117. End-to-end tests now cover a Positive Integer Value with no COV_Increment and an Analog Input with a COV_Increment of 0, and HasPropertyValueChangedPresentValueRealCOVIncrementNULL has been rewritten to match the ruling.
  • rb g-20261001-10 (#3112, item 2): no behaviour change. The stack already conforms: an AcknowledgeAlarm for a transition that occurred while its Event_Enable bit was FALSE sends the ACK_NOTIFICATION if the bit is TRUE when the acknowledgment arrives (cl. 13.2.3, 13.2.5). A test now pins this.
  • rb g-20261001-11 (#3441): the COV Value_Source helpers give each property read its own error container. Before, they shared one. CaptureValueSourceCOVBaseline no longer loses the Table 13-1 monitored-value baseline when the object’s Value_Source read fails and its monitored value is host-served. In HasValueSourceCOVCriteriaChanged the shared container was latent and could not change a result: Current_Command_Priority is answered before GetProperty checks the container, and a test pins that. Revert-red: with the fix reverted, BaselineKeepsTheMonitoredValueWhenTheValueSourceReadFails fails.
  • rb g-20261001-12 (#3393, item 5): a ReadProperty of an Audit Log’s Log_Buffer answers PROPERTY / READ_ACCESS_DENIED, as Trend Log, Trend Log Multiple and Event Log already do. Before, it fell through to the generic path: the issue saw UNKNOWN_PROPERTY over the wire, and with no host callbacks it was an error of class 0, code 0. A test-tool host can no longer serve it through ReadProperty, as #998 already did for Event Log. ReadRange of it is unchanged and still refused: the Audit Log profile marks Log_Buffer an array, not a list, so ReadRange stops at the list check before any Log_Buffer handler (CR2-F02-063, #3089). Fixing that needs the profile change. Revert-red: with the fix reverted, AuditLogLogBufferReadPropertyIsReadAccessDenied fails with class 0 and code 0.
  • rb g-20261001-2 (#2637): security - a WritePropertyMultiple request’s List of Write Access Specifications is now capped at STACK_OPTION_MAX_SEQUENCE_OF_ELEMENTS entries (4096 on the FULL target, 128 on EMBEDDED, 1024 otherwise), the cap #637 already applied to each object’s list of properties. A request with more entries is refused whole with Abort(BUFFER_OVERFLOW) and nothing in it is written. Before, every entry was decoded and allocated, bounded only by the received request size (about 5,400 minimal entries in a 65000-octet segmented request).
  • rb g-20261001-2 (#2635): a WritePropertyMultiple request whose out-of-range Priority (0 or 17-255) is reached after an earlier property in the same request was written now gets a WritePropertyMultiple-Error with SERVICES/PARAMETER_OUT_OF_RANGE, the code 135-2024 cl. 15.10.1.3.1 gives “The Priority parameter is not within the defined range of 1..16”. It used to report SERVICES/INVALID_TAG. When no property has been written yet the request is still Rejected with PARAMETER_OUT_OF_RANGE.
  • rb g-20261001-2 (#2636): BACnetStack_SendWriteProperty with serviceType 16 (WritePropertyMultiple) now sends the writes in the order they were given (135-2024 cl. 15.10). A write is grouped with the one before it only when both name the same object; before, it joined any earlier entry for its object, so writes [AV1.PV, AV2.PV, AV1.Description] went on the wire as [AV1{PV, Description}, AV2{PV}] and the server wrote AV1’s Description before AV2’s Present_Value.
  • rb g-20261001-2 (#2638): a WriteGroup change-list value of Null now relinquishes the Channel’s members at the given priority (135-2024 cl. 15.11.2, cl. 12.53.5). Members in this device get CallbackSetPropertyNull; members in another device get a WriteProperty of Null. Before, only the Channel’s own Present_Value was relinquished and every member kept its commanded value.
    • Other value types the fan-out does not handle (Double, Time, Date, CharacterString, OctetString, BitString, ObjectIdentifier, lighting command) are still not sent to members.
  • rb g-20261001-2 (#2639): a BACnetPriorityArray that is copied, cloned, or filled slot by slot is now valid and encodes. Since #1126 each of those produced an array that reported itself invalid and failed to encode, which also made a copy of a decoded Priority_Array ReadProperty-ACK value invalid. A copy whose slot fails to clone (out of memory) is now empty and invalid instead of partly copied.
  • rb g-20261001-3 (#3417): security - a WritePropertyMultiple request may now carry at most STACK_OPTION_MAX_SEQUENCE_OF_ELEMENTS property values in total (4096 on the FULL target, 128 on EMBEDDED, 1024 otherwise), however they are spread across objects. A request with more is refused whole with Abort(BUFFER_OVERFLOW) and nothing in it is written. Before, the per-object cap (#637) and the object-count cap (#2637) multiplied, so one 16384-octet request on an EMBEDDED target could make the decode allocate about 3,200 values within both caps. The decode stops before allocating another object once the budget is spent, so it holds at most the budget plus one object’s list.
  • rb g-20261001-3 (#2836): a WriteGroup no longer writes a Channel member whose reference is empty - an object or device instance of 4194303 (135-2024 cl. 12.53.11.1) - as cl. 12.53.5 requires (“except those members containing an empty reference”). Before, an empty object instance reached the host’s set-property callback, and a reference with an empty device instance wrote the local object it named.
  • rb g-20261001-3 (#3418, #2836): a WriteGroup now commands a Channel member whose Present_Value priority array the stack holds - a Lighting Output, Binary Lighting Output or Access Door added through the stack - through the same write path as WriteProperty, so the member’s priority array is written at the WriteGroup priority and a Null relinquishes that slot. Before, the write went to the host’s set-property callbacks and never reached the stack-held array, so the member ignored every Channel write and relinquish.
    • Host-visible change for a Binary Lighting Output member: the host used to get CallbackSetPropertyEnum on every WriteGroup; it now gets only CallbackBinaryLightingOutputPresentValueChanged, and only when Present_Value changes, as for a WriteProperty.
    • Still not done for #2836: Table 12-63 coercion of the Channel’s value to the member’s datatype, Execution_Delay, Write_Status, and Value_Source naming the Channel. A value whose datatype the member does not take is refused for a stack-held member and passed to the host callbacks unchanged for the others.
    • Still not done for #3418: a member naming a Lighting Output’s Lighting_Command. The Channel’s own Present_Value write refuses a BACnetLightingCommand value with INVALID_DATATYPE (BACnetBusinessLogic::SetProperty admits only primitive values for a Channel’s Present_Value), so the WriteGroup stops before the fan-out; a fix needs BACnetBusinessLogic.cpp.
  • rb g-20261001-4 (#3180): on a device with several BACnet/IP Network Ports, a read of a port’s BBMD_Broadcast_Distribution_Table, BBMD_Foreign_Device_Table, BBMD_Accept_FD_Registrations, FD_BBMD_Address or FD_Subscription_Lifetime, and a write of its BBMD_Broadcast_Distribution_Table, is now served from the BACnet/IP data link that port has claimed, whichever port the request arrived on (Annex J.4.4: “the Network Port Object which represents this B/IP port”). Before (since #3055), a request naming port B that arrived on port A got the ordinary answer - an empty list or the not-configured value - and a BDT write to port B never reached port B’s table. A Network Port that has claimed no BACnet/IP data link is answered as before #3180: from the arrival data link only when no port has claimed that link (the single-port case), otherwise the ordinary answer.
    • Host-visible change: a request for a claimed port’s BBMD properties that arrives over MS/TP or BACnet/SC, or an internal read made with no data link, now reaches that port’s BBMD table and foreign device registration. Before, it fell through to the host’s callbacks or stored value.
  • rb g-20261001-4 (#3298): an Event Enrollment with the CHANGE_OF_STATE algorithm on a Binary Output’s Present_Value now evaluates and goes OFFNORMAL on its alarm value; the Present_Value is a BACnetBinaryPV (cl. 12.7.4), armed and compared as a binary-value property state as for a Binary Input or Binary Value (#3295). Before, every evaluation was refused and Event_State stayed NORMAL. The intrinsic CHANGE_OF_STATE algorithm is still not offered for a Binary Output.
    • Host-visible change: BACnetStackTestTool_AddEventEnrollmentObjectWithChangeOfStateAlgorithm now returns false, and creates nothing, for a monitored property whose datatype is Unsigned (a Multi-state Present_Value) or Enumerated other than a binary Present_Value (for example Reliability). Its Boolean alarm value can never equal such a value (13.3.2: “The datatype of the values of this parameter and of pMonitoredValue shall be the same.”), so the enrollment it used to create never left NORMAL.
    • Still not done for #3298: an Unsigned alarm-value form, so an Event Enrollment that fires on a Multi-state state number.
  • rb g-20261001-4 (#3415): a value update that crosses an alarm limit with no Time_Delay now sends one COV notification, carrying the new value and IN_ALARM TRUE. BACnetStack_UpdateValue used to run COV before the alarm evaluation, so it sent two: the first with the Status_Flags from before the transition, the second from #3334’s re-evaluation. The alarm evaluation now runs first. With a Time_Delay, nothing changes: the update reports the old Status_Flags and the Tick that ends the delay reports the change.
    • Host-visible changes of the new order, all within one BACnetStack_UpdateValue call: with no Time_Delay, the EventNotification for the transition is now sent before the COV notification; a COV Trend Log record written by the same call now records the Status_Flags after the transition; and a SubscribeCOVPropertyMultiple subscriber to both properties now gets the Status_Flags change before the Present_Value change.
  • rb g-20261001-4 (#3415): writing Event_Detection_Enable FALSE on an object or Event Enrollment that is off-normal sets Event_State NORMAL and now sends the COV notification for the Status_Flags change it makes (IN_ALARM FALSE). Before, the subscriber was not told.
    • Still not done for #3415: an Event_State reset made by the host API’s alarm setup calls (re-arming an object’s alarming or an Event Enrollment), or by an Alert Enrollment’s Event_Detection_Enable write, still sends no COV.
  • rb g-20261001-5 (#3423): a Channel created with the BACnetLightingCommand datatype now accepts a BACnetLightingCommand written to its Present_Value. Before, every such write failed with INVALID_DATATYPE, so a WriteGroup to a lighting Channel stopped before it wrote any member. Each Lighting_Command member is now written: a stack-held Lighting Output through the stack’s lighting engine, a host-held one through CallbackSetPropertyLightingCommand, and one in another device by a WriteProperty carrying the command. A member whose property is not Lighting_Command is skipped. A Channel of any other datatype still refuses a BACnetLightingCommand.
  • rb g-20261001-6 (#3429): a test-tool build without the physical router (STACK_OPTION_TARGET_FULL with STACK_OPTION_DISABLE_ROUTER_PHYSICAL, or a virtual-router-only build with BACNET_STACK_TESTTOOL) now compiles. Before, BACnetNetworkLayer.cpp and BACnetTestToolInterface.cpp failed with 12 errors. In such a build BACnetStackTestTool_GetRouterRouteCount returns 0 and BACnetStackTestTool_GetRouterRoute returns false, since without the physical router there is no route table. Builds with the physical router are unchanged.
  • rb g-20261001-7 (#2572): a BACnet/SC hub function can now relay frames of up to 6144 octets (135-2024 cl. AB.5.1 requires at least 5705: 1497 octets of NPDU, 4192 of data and destination options, and the 16-octet header). Before, every BACnet/SC frame larger than 1600 octets was dropped on receipt, whatever the configured maximum. The BACnet/SC ingress buffer grows from 1600 to 6144 octets.
    • Memory cost, builds with BACnet/SC compiled in (TARGET_FULL, and embedded builds that add STACK_OPTION_DATA_LINK_LAYER_SC): the buffer is now a member of the BACnet interface object rather than a local in Tick(), so each interface object is 6144 octets larger (static RAM), and Tick() uses about 6 KB less stack (measured at -O0: 7296 to 1168 octets for TARGET_FULL, 6944 to 800 for an embedded build with SC). Builds without BACnet/SC are unchanged: no member, and the same 1529-octet buffer on Tick()’s stack.
    • BACnetStack_SetBACnetSCSettings still accepts a maximum BVLC length above 6144, such as 65535, but now uses 6144, the size of the buffer, and applies the call’s other settings as before. Before, the larger value was advertised to peers, and frames between 6145 octets and that value were dropped.
    • A Network Port write of Max_BVLC_Length_Accepted above 6144 is now refused with value-out-of-range. Before, values up to 65535 were accepted.
    • Known gap: the hub connector’s default maximum BVLC length is still 1600, so a hub that must meet AB.5.1 calls BACnetStack_SetBACnetSCSettings with at least 5705. Raising the default would change the Advertisement that BTL test 12.5.1.1.15 checks.
  • rb g-20261001-7 (#2340): reading a Network Port’s IP_DNS_Server now returns a one-entry array holding X’00000000’ (“not available or not configured”, cl. 12.56.31) when the application does not provide the property. Before, the read failed with Error(property, unknown-property), or with Abort(other) when no unsigned-integer callback was registered, although Property_List lists the property. An application that provides IP_DNS_Server entries is unaffected.
  • rb g-20261001-8 (#3218): every Access_Event an access request fires is now pushed in order, so each reaches the ACCESS_EVENT algorithm, COV subscribers and trend logs (135-2024 cl. 12.31.27.1, 13.3.12 (a)). Before, only the request’s last event was pushed: with AUTHENTICATION_FACTOR_READ and GRANTED both in pAccessEvents a grant sent one notification (GRANTED), and the attempt that reached Max_Failed_Attempts notified LOCKOUT_MAX_ATTEMPTS but not the denial before it.
    • The events of one request share one time, so a later event is stamped one hundredth of a second after the previous one and Access_Event_Time changes with each Access_Event (cl. 12.31.29). A granted request’s Access_Event_Time is now 0.01 s after the request’s time.
    • Access_Event, Access_Event_Tag, Access_Event_Time and Access_Event_Credential end as before: the request’s last event.
    • Volume: a COV subscriber or COV Trend Log on Access_Event (or its companions) now receives one notification or record per event, typically 2 or 3 per access request instead of 1.
    • Access_Event_Time never goes backwards: every event is stamped at the later of the request’s time and 0.01 s after the previous event’s time, so two requests in the same clock second stay in order. If the host clock steps back, events are stamped 0.01 s apart until it catches up.
    • The ACCESS_EVENT algorithm treats a different Access_Event value as a new event even with the same tag and time, so a request with no valid time, or at 23:59:59.99, still notifies each event.
  • rb g-20261001-8 (#3219): when a door command fails after GRANTED (another command holds a higher priority), LOCKED_BY_HIGHER_PRIORITY is now its own access transaction, with the next Access_Event_Tag, so GRANTED is the final event of the request’s transaction (cl. 12.31.27.1; owner ruling on the issue). Before, it carried the request’s tag, so the transaction ended on it.
    • Beyond the owner’s ruling (which covers only the new transaction): it is now fired once per request, after every door was commanded, where it was fired once for each door that failed. With a transaction each, two locked doors would otherwise be two transactions. Flagged to the owner for confirmation.
  • rb g-20261001-8 (#3123, part C): a decoded BACnetAuthorizationConstraint renders its origin and authentication by name (directConnect, sameNetwork, anyNetwork; certified, securePath, anyMethod; cl. 21). Before, both rendered as “Unknown Enumeration Value (N)”. The encoding is unchanged.
  • rb g-20261001-9 (#2461): the Rust adapter’s and the C++ adapter’s (adapters/cpp/CASBACnetStackAdapter.h, hand-written) default library names now match what the Linux build actually produces: the lib prefix, x86 instead of x32, and (Rust only) the correct architecture tag (x64/x86/arm64/arm7) from the actual target instead of a hard-coded x64. Windows is unchanged. The Rust file was regenerated via ci_scripts/generate-rust-adapter.js; Rust is still not compiled by any gate here (#2112), and is now covered locally by node ci_scripts/generate-rust-adapter.js --self-test, wired into ci_scripts/pre-push.sh.
  • rb g-20261001-9 (#2458): adapters/python/CASBACnetStackAdapter.py now names the correct library on Linux aarch64 (libCASBACnetStack_arm64_Release.so, previously the x64 name) and 32-bit x86 (libCASBACnetStack_x86_Release.so, previously missing the lib prefix), via a new resolve_libname(system, machine, bits) keyed on platform.machine(). An unsupported platform (including Darwin, which has no build) now sets libname = None instead of leaving it unbound; DecodeAsJSON_sample.py’s default_library_path() now raises a clear OSError naming the platform/machine in that case instead of a generic TypeError. Covered locally by adapters/python/CASBACnetStackAdapter_libname_selftest.py, wired into ci_scripts/pre-push.sh.
  • rb g-20261001-9 (#3438): tests/TestBACnetStackAccessPoint.cpp now compiles in an EMBEDDED build that enables STACK_OPTION_OBJECT_TYPE_ACCESS_POINT without STACK_OPTION_ALARMS_AND_EVENTS or STACK_OPTION_OBJECT_TYPE_ACCESS_RIGHTS (a gap the FULL-only CI gate never saw). Option guards added; no test behaviour changed under FULL.
  • rb g-20261004-1 (#3477): the Node addon no longer silently wraps a JavaScript number passed as a uint8_t / uint16_t / int8_t / int16_t argument; before, an object type of 65568 reached C as 32 and a priority of 256 as 0. Every generated napi wrapper (ci_scripts/generate-napi-wrappers.js, regenerated with --write) and every narrow argument of the hand-written ones (event / audit / Who-Am-I / You-Are / AuthRequest / AuditLogQuery sends, InsertNotificationLogRecord and its WithEventValues variant, the Add/RemoveListElement sends including the ReadAccessSpecification / subscription / Destination element forms, notification-class, BDT/FDT, DecodeAs and others) now re-reads the value at full width with ValidateIntegerArgumentAtFullWidth (interface_common.h, over the #3474 ReadIntegerArgumentAtFullWidth).
    • Behaviour change: an out-of-range, negative or non-integer value (for example -1 used as “all ones”, or 255.5) now throws a TypeError naming the argument (with its C range), and the export is not called. Element-object fields of the list-element sends (objectType, recipientNetworkNumber, validDays, the Destination time fields) return undefined, matching those wrappers’ existing invalid-element contract.
    • Date octets stay with their bacnet-date-field check. Values a JavaScript callback returns to the stack (interface_callbacks.cc) are not covered here.
    • New generator --self-test case narrow-integer #3477 and node-adapter-selftest.mjs leg 12 narrow-argument-width-guards (generated, signed int16_t, hand-written and list-element cases against the built addon). Revert-red: with the generator change reverted it fails with “no full-width range check for objectType, priority, offset, trim”.
  • rb g-20261004-2 (#3489): the Node addon no longer silently narrows a number a JavaScript callback returns to the stack. CallbackReceiveMessage’s length, CallbackSendMessage’s sent length and the test-tool invoke-id generator’s id were cast with static_cast<uint16_t/uint8_t>(Uint32Value()), so 65536 + n delivered an n-octet frame or reported an n-octet send, and 263 issued invoke id 7. They are now read at full width (ReadArrayElement), and a result outside the C type, a fraction, NaN or a non-Number fails the callback as a throw does: nothing received, 0 octets sent (the send export answers false), invoke id 0.
    • A rejected invoke-id generator result issues id 0. That is what a throwing handler already produced: the uint8_t typedef has no failure value, and 0 is a legal invoke id.
    • A non-Number callback result now fails quietly, without leaving a pending N-API exception (it used to reach Uint32Value(), which raised napi_number_expected).
    • New node-adapter-selftest.mjs leg 13 callback-result-width-guards, with in-range controls next to each refused case. Revert-red, one trampoline at a time on the built addon:
      • ReceiveMessage: “ReceiveMessage answering 65536 + length: expected nothing processed, got [“810a00090100200514”]”
      • SendMessage: “SendMessage answering 65536 + length: expected a failed send (false)”
      • invoke-id generator: “invoke-id generator answering 263: expected 0 (a failed generator), got 7”
  • rb g-20261004-2 (#3452 item 6): the client SEGMENTED_REQUEST SegmentTimer is honoured to the millisecond when the optional CallbackGetSystemTimeMs is registered. Before, a 1500 ms APDU_Segment_Timeout was rounded up to 2 s (CR2-F05-022). All five arms of the timer also keep a millisecond deadline: SendConfirmedSegmented, the SegmentACK processor’s DuplicateACK and NewACK re-arms, and Loop()’s Timeout and TimeoutSegmented re-arms. Without the millisecond clock the seconds behaviour is unchanged. The server-side SegmentTimer (#918) is still whole seconds.
    • New gtests TestBACnetInterfaceSegmentTimerMilliseconds.* (5, test-tool builds): a 1500 ms Tseg has not expired 1400 ms after the arm and has expired 1600 ms after it. Revert-red, one call site at a time (its callbacks argument dropped, so it falls back to seconds); each fails at its 1600 ms assertion in tests/TestBACnetInterface_SegmentedRequestClientState.cpp:
      • SendConfirmedSegmented (BACnetInterface.cpp): line 985, “1600 ms after segment 0, a 1500 ms Tseg has expired”
      • Timeout re-arm (RetrySegmentTimer): line 1003, “1600 ms after the Timeout re-arm, the second retry fires”
      • NewACK re-arm (AdvanceSegmentationWindow): line 1025, “1600 ms after the SegmentACK re-arm, segment 1 is retransmitted”
      • DuplicateACK re-arm (RestartSegmentTimer): line 1050, “Which is: 2 / Which is: 3”
      • TimeoutSegmented re-arm (RestartSegmentedRetry): line 1073, “Which is: 3 / Which is: 4”
  • rb g-20261004-3 (#2693, CR2-F15-030): a ConfirmedCOVNotification with an empty List of Values, or a ConfirmedCOVNotificationMultiple with an empty List of COV Notifications, is now answered with Reject MISSING_REQUIRED_PARAMETER instead of a SimpleACK (cl. 13.6.1.1.5 / 13.17.1.1.5 “one or more”; owner ruling, following #2845). An unconfirmed notification with an empty list is discarded, since nothing is sent for it. The processor’s old checks compared a member’s address with NULL, so they could never fire; their unreachable Abort OTHER branch is gone. The only observable change is SimpleACK becoming Reject on an empty confirmed list.
    • New gtests BACnetConfirmedRequestProcessorCR2F05.Issue2693_* (2), each with a one-element control that is still SimpleACKed. Revert-red: both fail with source/BACnetCOVNotificationProcessor.cpp reverted.
  • rb g-20261004-3 (#2692, CR2-F14-003): a file access-method CHOICE whose arm decodes but then has the wrong closing tag no longer leaves the AtomicReadFile / AtomicWriteFile request (or ACK) reporting IsValid()==true after Decode returned 0. BACnetFileAccessMethodParameters / BACnetFileAccessMethodData now reset the failed arm. Before, the arm’s validity was computed from members that had decoded, so DecodeWithContext’s SetValid(false) had no effect.
    • New gtests BACnetAtomicReadFileRequest.Issue2692_* / BACnetAtomicWriteFileRequest.Issue2692_* (4: stream and record arm for each). Revert-red: all 4 fail with the two access-method sources reverted.
  • rb g-20261004-3 (#2690, CR2-F12-007/008/009): CreateObject and list-service robustness.
    • CR2-F12-007: BuildCreateObjectErrorPacket answers Abort OUT_OF_RESOURCES when the CreateObject error, its Error-PDU, or inserting the error fails. Before, a failed error allocation produced an Error-PDU with no error, so nothing was sent. A failed Error-PDU allocation produced a short-form CreateObject Error with no first-failed-element-number.
    • CR2-F12-008: BACnetCreateObjectRequest::Clone returns NULL when an initial value fails to clone. Before, it returned a truncated clone that was still marked valid.
    • CR2-F12-009: a NULL or zero-length Decode now leaves IsValid()==false on BACnetCreateObjectRequest, BACnetDeleteObjectRequest, BACnetCreateObjectACK, BACnetAddListElementRequest and BACnetRemoveListElementRequest. Before, a reused object that was previously valid stayed valid.
    • New gtests *.Issue2690_* (7). The OOM cases use MemTracker::SetFailAfter. Its >= semantics fail every later allocation too, so the error == NULL guard cannot be isolated from the Error-PDU guard: Issue2690_ErrorAllocationFailure_AbortsOutOfResources passes through whichever guard fires first, so deleting the error == NULL guard alone would not turn it red. The insert-failure guard after them is defensive and unreachable (labelled in the source). Revert-red: all 7 fail with the six sources reverted.
  • rb g-20261004-4 (#2689, CR2-F11-005): a NULL or zero-length Decode now leaves IsValid()==false on BACnetWriteAccessSpecification, BACnetWritePropertyMultipleRequest, BACnetWritePropertyRequest and BACnetWritePropertyMultipleError. Before, it returned 0 before Reset(), so a reused object that was previously valid stayed valid. Only the validity is cleared: the old contents are still there, but IsValid() gates every use of them. Sibling of the #2690 CR2-F12-009 fix.
    • New gtests BACnetWPMWriteGroupCR2F11.Issue2689_EmptyDecodeOf* (4). Revert-red: all 4 fail with the four sources reverted.
  • rb g-20261004-4 (#2689, CR2-F11-012): copying a BACnetPriorityValue (assignment, copy-construct, Clone()) whose value Clone() fails now leaves the destination empty and invalid. Before, the destination kept its OLD value and reported IsValid()==true, so the failed copy looked like a successful copy of a different value. A copy of a value whose Decode failed part-way is now invalid too; before, the copy reported IsValid()==true although the source did not.
    • New gtests BACnetWPMWriteGroupCR2F11.Issue2689_CopyOfAHalfDecodedPriorityValueIsNotValid and Issue2689_PriorityValueCopyWhoseCloneFailsDropsTheOldValue (the latter uses MemTracker::SetFailAfter). Revert-red: both fail with source/BACnetPriorityValue.cpp reverted.
  • rb g-20261004-4 (#2689, CR2-F11-011): a BACnetWriteGroupRequest copy whose change-list copy fails (OOM) is now invalid. Before, it reported IsValid()==true with 0 change-list entries.
    • New gtest BACnetWPMWriteGroupCR2F11.Issue2689_WriteGroupCopyWhoseChangeListCopyFailsIsNotValid (fails each allocation of the copy in turn with MemTracker::SetFailAfter). Revert-red: fails with source/BACnetWriteGroupRequest.cpp reverted.
  • rb g-20261004-4 (#2689, CR2-F11-006): the client WPM builder BACnetListOfWriteAccessSpecifications::Insert(objectType, objectInstance, ...) now returns false, and adds nothing, when the write’s BACnetPropertyValue is invalid, for example a priority of 0 or 17..255. Before, it returned true and the request failed later at Encode with the generic “Invalid BACnetPropertyReference object” log. Integrator-visible: the WritePropertyMultiple path of the send export (BACnetInterface::CreateWritePropertyMultiple) now fails at build time for such a priority, and no frame is sent. Before, it also sent nothing, failing at encode instead, so no valid input changes. Both branches’ InsertProperty failure checks are defensive; no test can reach them (BACnetSequenceOf::Insert fails only on NULL).
    • New gtests BACnetWPMWriteGroupCR2F11.Issue2689_BuilderRejectsAnInvalidPriority* (2: new object, same object), each with a priority-8 control. Revert-red: both fail with source/BACnetListOfWriteAccessSpecifications.cpp reverted.
  • rb g-20261004-4 (#2689, CR2-F11-015): BACnetGroupChannelValue::Decode (WriteGroup change-list entries) now rejects an opening context tag above [2] with Reject INVALID_TAG. BACnetChannelValue’s constructed arms are [0] lightingCommand, [1] xyColor and [2] colorCommand. Before, an opening tag [3]..[14] decoded successfully and was handed on to the Channel Present_Value write as a context-tagged value.
    • New gtests BACnetWPMWriteGroupCR2F11.Issue2689_GroupChannelValueRejectsAnOpeningTagAboveTwo and the control Issue2689_GroupChannelValueStillDecodesOpeningTagTwo. Revert-red: the first fails with source/BACnetWriteGroupRequest.cpp’s CR2-F11-015 change reverted.
  • rb g-20261004-5 (#2688, CR2-F10-009): copying a BACnetReadPropertyMultipleRequest whose list copy fails (OOM) now gives an invalid request, and its Clone() returns NULL. Before, the copy reported IsValid()==true with 0 read access specifications, and Clone() returned that empty request. A BACnetReadResult copy whose value Clone() fails is now invalid and holds no value. Before, it reported IsValid()==true, kept the destination’s old value (or no value), and failed later at Encode with “Both propertyValue and PropertyAccessError are not set”. Through the confirmed-request PDU, BACnetConfirmedRequestPDU::Clone() already handles the NULL; a failed PDU copy (operator=) now holds no service request instead of an empty RPM. Both states were already refused at Encode with Abort(other), as for every other request type (noted on #2688 as a follow-up).
    • New gtests BACnetReadServicesCR2F10.Issue2688_RPMRequestCopyWhoseListCopyFailsIsNotValid, Issue2688_RPMRequestCloneWhoseListCopyFailsReturnsNull and Issue2688_ReadResultCopyWhoseValueCloneFailsIsNotValid (all use MemTracker::SetFailAfter). Revert-red: all 3 fail with source/BACnetReadPropertyMultipleRequest.cpp and source/BACnetReadResult.cpp reverted.
  • rb g-20261004-5 (#2688, CR2-F10-010): BACnetReadResult::IsEqual now returns false when only the other result has an array index. Before, a.IsEqual(b) returned true while b.IsEqual(a) returned false. This reaches the RPM-ACK comparisons built on it (BACnetReadAccessResult, BACnetListOfReadAccessResults, BACnetReadPropertyMultipleACK::IsEqual). Same fix as #751.
    • New gtest BACnetReadServicesCR2F10.Issue2688_ReadResultIsEqualIsSymmetricOnArrayIndex. Revert-red: fails with source/BACnetReadResult.cpp reverted.
  • rb g-20261004-5 (#2688, CR2-F10-026): BACnetReadPropertyACK::SetPropertyValue now deletes the value it replaces. Before, replacing a value (for example after Decode) leaked the old one. BACnetReadPropertyACK::SetReadPropertyACK and BACnetReadPropertyRequest::SetReadPropertyRequest called with usePropertyArrayIndex=false now clear an earlier array index. Before, a reused object kept it: IsPropertyArrayIndexSet() returned true and the request’s Encode emitted the old [2] field. Only constructors and tests call these setters today, so no stack-built frame changes.
    • New gtests BACnetReadServicesCR2F10.Issue2688_ReadPropertyAckSetPropertyValueFreesTheOldValue (MemTracker net retained bytes), Issue2688_ReadPropertyAckSetterClearsAStaleArrayIndex and Issue2688_ReadPropertyRequestSetterClearsAStaleArrayIndex. Revert-red: all 3 fail with source/BACnetReadPropertyAck.cpp and source/BACnetReadPropertyRequest.cpp reverted.
  • rb g-20261004-5 (#2688, CR2-F10-029): BACnetPropertyAccessResult (Group object’s Present_Value element):
    • IsEqual now returns false when one result has an array index and the other does not. Before, index 0 present compared equal to no index, because a Reset unsigned holds 0.
    • SetPropertyValueAndTakeControl and SetPropertyAccessError now return false when the CHOICE they set is not valid (a failed deep copy of the value, or an invalid BACnetError). Before, they returned true. No caller in the stack reads either return value.
    • A NULL or zero-length Decode now resets the object, so IsValid()==false, here and in BACnetReadPropertyRequest::Decode and BACnetReadPropertyACK::Decode. Before, these returned 0 before Reset(), so a previously valid object stayed valid with its old contents. An object that was not already valid behaves as before.
    • Partly done: the review’s impact text also names BACnetPropertyAccessResult.cpp’s shortened license header, its compressed one-line style and its short names (t, i, v). Only the functions touched here were renamed; the rest is left for a follow-up on #2688.
    • New gtests BACnetReadServicesCR2F10.Issue2688_PropertyAccessResultIsEqualSeesArrayIndexPresence, Issue2688_PropertyAccessResultSetPropertyAccessErrorReportsAnInvalidError, Issue2688_PropertyAccessResultValueSetterReportsAFailedCopy (MemTracker), Issue2688_EmptyDecodeOfPropertyAccessResultIsNotValid, Issue2688_EmptyDecodeOfReadPropertyRequestIsNotValid and Issue2688_EmptyDecodeOfReadPropertyAckIsNotValid. Revert-red: all 6 fail with source/BACnetPropertyAccessResult.cpp, source/BACnetReadPropertyRequest.cpp and source/BACnetReadPropertyAck.cpp reverted.
  • rb g-20261004-5 (#2688, CR2-F10-011): encoding a BACnetReadPropertyMultipleRequest into a buffer too small for it now reports the child’s Abort(segmentationNotSupported). Before, BACnetListOfReadAccessSpecifications::Encode replaced it with Abort(other). It now keeps an abort the child set and defaults to Abort(other) only when there is none, as BACnetListOfReadAccessResults::Encode already did. No wire change: this is the client-side request encode, and no frame is sent either way. The segmented-send buffer (BACnetStackMemoryBuffer::Insert) retries with a larger buffer only on segmentationNotSupported. It starts at GetMinimumMemoryCount(), which matched the encoded length exactly for the RPM requests probed (9 and 520 octets), so no send outcome was observed to change (probed, not proven). Only the error handling the review recommended was changed; the other differences between the two list classes it names (no copy constructor/operator= on the results list, no indexed accessor on the specifications list, a dead NULL check in the results list’s IsEqual) are left for a follow-up on #2688.
    • New gtest BACnetReadServicesCR2F10.Issue2688_RPMRequestEncodeIntoTooSmallBufferKeepsSegmentationNotSupported. Revert-red: fails with source/BACnetListOfReadAccessSpecifications.cpp reverted.
  • rb g-20261004-5 (#2688, CR2-F10-023): BACnetReadRangeACK::GetElements now returns true. Before, it returned false after rendering every element. No caller reads the return value (JSONRenderer and XMLRenderer ignore it), so DecodeAsJSON/XML output does not change.
    • New gtest BACnetReadServicesCR2F10.Issue2688_ReadRangeAckGetElementsReturnsTrue. Revert-red: fails with source/BACnetReadRangeACK.cpp reverted.
  • rb g-20261004-6 (#2750, CR2-F21-004): assigning a BACnetRecipient that has neither a device nor an address over one that has a child now clears the child. Before, only the validity flag was cleared: IsValid() was false while IsDevice(), GetDevice(), IsEqual() and GetElements() still reported the old device or address. No stack caller assigns such a recipient today (BACnetRecipientProcess and BACnetDestination already worked around it), so no frame changes.
    • New gtests BACnetRecipient.Issue2750_AssignInvalidRecipientClearsTheOldDevice, Issue2750_AssignInvalidRecipientClearsTheOldAddress and Issue2750_SelfAssignmentKeepsTheDevice. Revert-red: the first 2 fail with source/BACnetRecipient.cpp reverted.
  • rb g-20261004-6 (#2750, CR2-F21-006): BACnetDestination::Reset() now clears validity, so a failed Decode into a previously valid destination gives IsValid()==false. Before, Decode returned 0 and the object still reported valid. The stack decodes into fresh objects, so no received frame is handled differently.
    • New gtests BACnetDestination.Issue2750_FailedDecodeIntoAValidDestinationIsNotValid and Issue2750_ResetClearsValidity. Revert-red: both fail with source/BACnetDestination.cpp reverted.
  • rb g-20261004-6 (#2750, CR2-F21-005): BACnetTimeStamp::IsEqual now returns false for two stamps that use different CHOICE arms, and compares only the active arm. Before, a time-arm 00:00:00.00 compared equal to a sequence-number-arm 0. The change applies to every BACnetTimeStamp::IsEqual caller, including AcknowledgeAlarm’s Time Stamp match, audit record and notification equality, and list-service matching. Stamps the stack stores and compares use the same arm on both sides, so no observed outcome changes. Only a comparison between two different arms that both hold their zero value now gives false, for example a peer’s AcknowledgeAlarm Time Stamp against an Event_Time_Stamps entry, which the stack stores in the datetime arm.
    • New gtest BACnetTimeStamp.Issue2750_IsEqualComparesTheChoiceArm. Revert-red: fails with source/BACnetTimeStamp.cpp reverted.
  • rb g-20261004-6 (#2750, CR2-F21-009): BACnetProcessIdSelection::GetProcessIdentifier() returns 0 unless the process-identifier arm is chosen, and each setter clears the other arm. Before, after SetProcessIdentifier(5) then SetNullValue() it still returned 5. The Notification Forwarder’s Process_Identifier_Filter checks GetChoice() first, so forwarding does not change. Short names renamed.
    • New gtest BACnetProcessIdSelection.Issue2750_SwitchingToNullValueClearsTheProcessIdentifier. Revert-red: fails with source/BACnetProcessIdSelection.cpp reverted.
  • rb g-20261004-6 (#2750, CR2-F21-008, test-tool build only): BACnetDestination::CreateFromCompactString (the test-tool’s Recipient_List write value) returns NULL in any of these cases:
    • a field that is not plain decimal digits (a sign, a space, or trailing text);
    • a field above the width of the parameter it feeds;
    • a boolean or recipient choice other than 0 or 1;
    • a MAC that is odd-length, non-hex or longer than 64 hex digits;
    • a value the destination refuses. The recipient choice is read as a number, so “01” is still the address choice. Before, values were narrowed by cast (Valid Days 257 became Monday only), a sign or trailing text was accepted, non-numeric text read as 0, choice 2 meant address, the MAC was truncated or read non-hex as 0, and a refused value gave an invalid destination.
    • The only caller is the BACNET_STACK_TESTTOOL Destination write-value builder in BACnetInterface.cpp. A bad ;-separated destination now fails the whole Recipient_List write, and no frame is sent. Before, the invalid entry was inserted and the write failed later at Encode with Abort(other).
    • The test-tool encoder (rawValueToWriteStr) emits only 0/1, 0 for unused fields and lowercase hex, so it is compatible.
    • The header comment now documents the 18-field, two-arm format.
    • New gtests BACnetDestinationCompactString.Issue2750_ValidDaysAbove255_ReturnsNull, Issue2750_NonNumericField_ReturnsNull, Issue2750_OddLengthMac_ReturnsNull, Issue2750_RefusedTimeReturnsNullNotAnInvalidDestination, Issue2750_MalformedFieldsReturnNull (7 cases), Issue2750_RecipientChoiceWithLeadingZeroIsTheAddressChoice and Issue2750_ProcessIdentifierUInt32MaxStillParses. The address-choice positive control is the existing AddressChoice_WithMacAndNetworkNumber_ParsesCorrectly.
    • Revert-red: the first 4 fail with source/BACnetDestination.cpp reverted to the pre-batch parser. MalformedFieldsReturnNull fails there on all 7 cases. RecipientChoiceWithLeadingZero fails against this batch’s first-round parser, which built a device recipient from “01”.
  • rb g-20261004-6 (#2750, CR2-F21-015, comments only): the InsertDestination note that said no path sends to a device recipient now points at the DAB resolution in BACnetStackMessageGenerator.cpp. Two empty ToDo placeholder lines in BACnetStackNotificationClass were removed.
  • rb g-20261004-6 (#2750, CR2-F21-011): already fixed at the tip by #3482 item 4 (Setup builds the reference and the algorithm before committing Event_Type and Object_Property_Reference; test Issue3482EventEnrollmentExportTest.ARefusedReAddLeavesTheEventTypeAndTheAlgorithmUnchanged). No change here. Residuals, left open on #2750:
    • SetGeneralProperties can still fail after the commit point, leaving the new Event_Type and reference with the old general properties.
    • A first-time create whose Setup fails can leave a bare entry in the device’s Event Enrollment list. The fix needs BACnetDBDevice.cpp, which was out of bounds for this batch.
  • Follow-up (#2750, CR2-F21-015): about 80 case-insensitive ToDo markers in source/ are invisible to check-todo-markers.js, whose pattern is case-sensitive. A gate change or a sweep is left for a new issue.
  • rb g-20261004-7 (#2747, CR2-F18-011): a copy of a BACnetFaultParameter (operator= or Clone()) whose fault-life-safety, fault-characterstring or fault-state list copy fails under allocation exhaustion now reports IsValid()==false. Before, the alternative and the wrapper both took validity from the source, so the copy reported valid while holding an emptied fault-value list, and Encode aborted partway through at the inner list. Clone() still returns a non-NULL object in this case, now marked invalid. Allocation failure only; no frame changes otherwise.
    • New gtests BACnetFaultParameter.Issue2747_CopyAssign_AlternativeListCopyFails_CopyIsNotValid, Issue2747_Clone_AlternativeListCopyFails_CloneIsNotValid, Issue2747_LifeSafetyCopyAssign_ListCopyFails_CopyIsNotValid, the control Issue2747_CopyAndClone_NoFaultInjection_StayValid, and BACnetFaultParameterFault{Characterstring,State}.Issue2747_CopyAssign_ListCopyFails_CopyIsNotValid. Revert-red: the 5 fault-injection tests fail with the four source files reverted.
  • rb g-20261004-7 (#2747, CR2-F18-017): the fault-out-of-range, fault-characterstring, fault-state, fault-status-flags and fault-listed Decode()s now take validity from the decoded content and refuse (Reject other) when it is invalid, as fault-life-safety and fault-extended already did. Defensive: no member decoder is known to return a non-zero size for an invalid member, so no received frame is handled differently. No revert-red test.
  • rb g-20261004-7 (#2747, CR2-F18-016): a default-constructed BACnetFaultParameterFaultState or BACnetFaultParameterFaultCharacterstring (empty list-of-fault-values, a SEQUENCE OF with no size bound) is now valid and encodes. Before, it stayed invalid until a value was inserted or decoded, so an integrator or test-tool path that built one directly and handed it to BACnetFaultParameter got Encode returning 0. Reset() still leaves the object invalid, so a failed Decode reports invalid; a host that calls Reset() on an empty alternative and then encodes still gets 0. The old invalid default was a defect, not a design: both in-tree builders had to patch it with “empty list: SetValid(true)” to encode a legal empty list, and BACnetSequenceOf itself treats an empty sequence as valid. The sibling classes that keep DefaultConstructorIsNotValid have required fields, so they are unchanged. The replaced tests are cited by no BTL/conformance step (RB655). The State and Characterstring fault algorithms’ BuildFaultParameters no longer need their “empty list: SetValid(true)” workarounds, which are removed. Fault_Parameters bytes are unchanged (the workarounds already encoded the empty list), and the fault algorithms never read the alternative’s validity.
    • The test DefaultConstructorIsNotValid in both classes is replaced by Issue2747_DefaultConstructorIsValidWithAnEmptyList. New gtests Issue2747_EmptyAlternativeEncodesThroughTheWrapper and Issue2747_FailedDecodeOfADefaultObjectIsNotValid in both classes. Revert-red: all 6 fail with the four source files reverted (FailedDecode on its validity precondition).
  • rb g-20261004-7 (#2747, CR2-F18-006): hardening of the direct C++ API. BACnetExtendedFaultAlgorithm::SetExtendedFaultReliability now returns bool and refuses NO_FAULT_DETECTED, keeping the reliability configured before (UNRELIABLE_OTHER by default). Through the test-tool export this was already closed: BACnetStackTestTool_SetFaultExtendedAlgorithm refuses reliability 0 while faultDetected is TRUE since #3483 (IsExtendedFaultReliabilityValid), so there is no integrator-visible change there. Code that called the algorithm directly with NO_FAULT_DETECTED and the fault flag set got a FAULT to NORMAL transition on every evaluation although the object never left NORMAL; it now goes to FAULT with the kept reliability once, then reports no change. The refusal is reported by the return value only, with no error log, because the documented clear call (faultDetected FALSE, reliability 0) reaches this setter too.
    • New gtests BACnetExtendedFaultAlgorithmTest.Issue2747_NoFaultDetectedIsRefusedAsTheFaultReliability and Issue2747_RefusedNoFaultDetectedKeepsTheEarlierReliability. Revert-red, by disabling the guard by hand (the return type changed, so the script cannot revert the header): both Run() calls return stateChange_ToNormalFromFault.
  • rb g-20261004-7 (#2747, CR2-F18-013): BACnetFaultParameterFaultOutOfRangeValue::GetDatatype() returns a new BACnetStackDatatypes::FaultParameterFaultOutOfRangeValue (appended, gated on STACK_OPTION_OBJECT_TYPE_EVENT_ENROLLMENT, so no existing value moves) instead of NotificationParametersOutOfRange. IsEqual against a BACnetNotificationParametersOutOfRange now returns false at the datatype check. Before, it cast that object to the wrong layout. Both in-tree callers compare two values of this class, so no outcome changes.
    • New gtest BACnetFaultParameterFaultOutOfRangeValue.Issue2747_IsEqualRefusesANotificationParametersOutOfRange. Revert-red: fails with source/BACnetStackDatatypes.h and source/BACnetFaultParameterFaultOutOfRangeValue.cpp reverted.
  • rb g-20261004-7 (#2747, CR2-F18-014 and CR2-F18-015, comments only): the grammar blocks in BACnetFaultParameterFaultOutOfRange.h and BACnetFaultParameterFaultOutOfRangeValue.h no longer show context tags [0]-[3] on the min/max CHOICE arms, matching the codec after #2085. The BACnetFaultParameterFaultExtended::IsEqual comment now names BACnetFaultParameterBase, the FaultParameter datatype and BACnetFaultParameter::IsEqual.
  • rb g-20261004-8 (#2749, CR2-F20-013 and CR2-F20-017): the out-of-range, double-out-of-range, signed-out-of-range, unsigned-out-of-range, unsigned-range, floating-limit, buffer-ready, change-of-timer, change-of-life-safety, access-event and command-failure notification-parameter Decode()s now Reset() before rejecting a null or zero-length buffer. Before, the rejected decode returned 0 with a Reject but left an object that already held content reporting IsValid()==true with its old fields, so a later Encode() re-sent them. Not reachable from a received frame: the BACnetNotificationParameters CHOICE always decodes into a fresh arm. The other notification-parameter Decode()s in the family share the shape and are not changed by this batch. Partly done: 11 of the family’s 23 Decode() bodies. The 12 still checking before Reset() are BACnetNotificationParameters (the wrapper), ChangeOfBitstring, ChangeOfCharacterstring, ChangeOfDiscreteValue, ChangeOfDiscreteValueNewValue, ChangeOfReliability, ChangeOfState, ChangeOfStatusFlags, ChangeOfValue, ChangeOfValueNewValue, Extended and ExtendedParameter.
    • New gtests BACnetNotificationParametersCR2F20.Issue2749_RejectedDecodeLeavesEveryRangeLimitAndBufferReadyArmInvalid, Issue2749_RejectedDecodeLeavesTimerLifeSafetyAccessEventAndCommandFailureInvalid, Issue2749_NullBufferDecodeLeavesArmInvalid. Revert-red: all 3 fail with the 11 sources reverted.
  • rb g-20261004-8 (#2749, CR2-F20-002): a copy of a BACnetNotificationParameters (operator= or Clone()) whose change-of-reliability or extended arm comes back emptied after a nested copy failure now reports IsValid()==false. Before, the wrapper took validity from the source and reported valid while holding an arm with no property values, and Encode aborted the whole notification at the arm. Clone() still returns non-NULL here, now marked invalid. Allocation failure only.
    • New gtests Issue2749_CopyWhoseArmCameBackEmptiedIsNotValid, Issue2749_CloneWhoseArmCameBackEmptiedIsNotValid. Revert-red: both fail with source/BACnetNotificationParameters.cpp reverted.
  • rb g-20261004-8 (#2749, CR2-F20-008): a copy of a change-of-discrete-value notification whose new-value primitive clone fails now reports IsValid()==false, for both the new value and the arm. Before, both copied the source validity and reported valid holding no new value, and Encode failed later with “Invalid state”. Allocation failure only.
    • New gtest Issue2749_DiscreteValueCopyWhosePrimitiveCloneFailsIsNotValid. Revert-red: fails with the two sources reverted.
  • rb g-20261004-8 (#2749, CR2-F20-011): BACnetNotificationParametersChangeOfState, ...ChangeOfBitstring and ...ChangeOfValue are valid only once both the new value and the status flags are set, in either order. Before, SetStatusFlags alone marked the arm valid, so a half-built arm passed the wrapper and Encode returned 0. ChangeOfState::SetNotificationParameters now returns false for a NULL or invalid new state; the change-of-state event algorithm’s event-values builder therefore returns false where it used to hand on an arm whose notification already failed to encode. Arms built completely are unchanged. On the wire (a flagged modelling decision): BACnetEventAlgorithm::ChangeState logs a false from CreateEventValues and carries on, so the transition, time stamps and notification still happen with eventValues NULL. The one reachable case is a change-of-state algorithm whose monitored value is unset, e.g. an owed inhibit-to-normal after the algorithm was reset. Before, that notification carried an arm marked valid that could not encode, so the whole notification failed to encode and nothing was sent. Now it is sent with Event_Values omitted. No test drives this path.
    • New gtests Issue2749_ChangeOf{State,Bitstring,Value}StatusFlagsAloneIsNotValid, Issue2749_ChangeOfStateRefusesAnInvalidMonitoredValue. Revert-red: all 4 fail with the three classes reverted.
  • rb g-20261004-8 (#2749, CR2-F20-018): BACnetNotificationParametersCommandFailure::SetCommandValueAndTakeControl and SetFeedbackValueAndTakeControl free the value they replace, and Clone() frees a child clone the setter refuses. Before, a second set call leaked the first value, and a refused clone (allocation failure) leaked. Leak only.
    • New gtests Issue2749_CommandFailureSecondSetFreesTheReplacedValues, Issue2749_CommandFailureCloneFreesARejectedChildClone. Revert-red: both fail with the source reverted.
  • rb g-20261004-8 (#2749, CR2-F20-009): BACnetNotificationParametersChangeOfStatusFlags can no longer be copied by value (copy constructor and operator= deleted; it is duplicated through Clone()). Before, a value copy shared the owned present-value pointer and both destructors freed it. Nothing in the stack copied it by value, so this is a compile-time guard for C++ API users. SetPresentValueAndTakeControl now frees the value it replaces (a second call leaked it), and Clone() frees a refused present-value clone. The matching Clone() change (a refused present-value clone is freed) has no dedicated test.
    • New gtests Issue2749_ChangeOfStatusFlagsCannotBeValueCopied, Issue2749_ChangeOfStatusFlagsSecondPresentValueFreesTheFirst. Revert-red: both fail with the .h and .cpp reverted.
  • rb g-20261004-8 (#2749, CR2-F20-015 and CR2-F20-020, comments and ordering only): #pragma once in BACnetNotificationParametersDoubleOutOfRange.h now sits below the license header, and CommandFailure::Decode labels the feedback-value decode “Context 2”. The review suggested deleting the redundant #pragma once (the include guard covers it); it was moved instead, which fixes the reported problem (the license header first) with the smallest change.
  • rb g-20261005-1 (#2850, CR2-F28-022): BACnetTimerStateChangeValue::Decode() now Reset()s before rejecting a null or zero-length buffer. Before, the rejected decode returned 0 with a Reject but left an object that had already decoded a value reporting IsValid()==true with its old choice and value. The other two classes the review names, BACnetDateTime and BACnetDateTimePattern, were already fixed at the tip (#2608, #3537).
    • New gtest BACnetTimerStateChangeValue.Issue2850_RejectedDecodeLeavesAPreviouslyDecodedValueInvalid. Revert-red: fails with the source reverted.
  • rb g-20261005-1 (#2850, CR2-F28-023): BACnetDateTimePattern::SetDate and SetTime return the child’s validity. Before, both returned true for an invalid DATE or TIME. m_date and m_time are now private; nothing in the stack, tests or adapters wrote them directly. C++ API users that wrote the members directly must use the setters. The class compiles only with STACK_OPTION_OBJECT_TYPE_DATE_TIME_PATTERN_VALUE.
    • New gtests BACnetDateTimePattern.Issue2850_SettersReportAnInvalidChild, Issue2850_DateAndTimeMembersAreNotPublic. Revert-red: both fail with the .h and .cpp reverted.
  • rb g-20261005-1 (#2850, CR2-F28-011): BACnetSpecialEvent::SetCalendarEntry reports an invalid or no-choice calendar entry: it still stores the entry and switches the period choice, and now returns false where it returned true. Every in-tree caller checks the result (BuildSpecialEvent already chains it), so there is no host-visible change from the setter itself. The test-tool compact-string factories BACnetSpecialEvent::CreateFromCompactString and BACnetCalendarEntry::CreateFromCompactString return NULL when any setter refuses a field (a non-Calendar calendar reference, an event priority outside 1..16, a date, date range or WeekNDay the setter rejects). Before, they returned a partly built object, which the test tool’s Date_List / Exception_Schedule write builder inserted and which then failed at encode; now that builder refuses the write value locally. Test-tool builds only (BACNET_STACK_TESTTOOL). The testing tool’s encoders were checked: every string now refused already produced an object that could not encode.
    • New gtests BACnetSpecialEvent.Issue2850_SetCalendarEntryRefusesAnInvalidEntry, Issue2850_CompactStringFactoryRefusesARejectedField, Issue2850_CalendarEntryCompactStringFactoryRefusesARejectedField. Revert-red: all 3 fail with the two sources reverted.
  • rb g-20261005-1 (#2850, CR2-F28-008): a Weekly_Schedule write (one day or the whole array) whose copy of the written day fails to allocate, and a Schedule_Default write whose value clone fails, now return Error RESOURCES / NO_SPACE_TO_WRITE_PROPERTY and leave the property unchanged. Before, the client got Result(+) while a truncated day was committed, or Schedule_Default was cleared. The whole-array commit now swaps the validated days in, each side’s validity flag travelling with its own list, (new BACnetDailySchedule::Swap, BACnetSequenceOf<T>::Swap) and cannot fail part-way. Allocation failure only. SetWeeklyScheduleDayAndTakeControl, which has no callers, still does not check its copy. Follow-up, predating this batch: the Present_Value arm of the same BACnetStackSchedule::SetProperty answers allocation failure with Abort(OUT_OF_RESOURCES), while these arms use Error(RESOURCES, NO_SPACE_TO_WRITE_PROPERTY).
    • New gtest BACnetSequenceOf.Issue2850_SwapCarriesEachFlagWithItsOwnList. Revert-red: fails with BACnetSequenceOf.h and BACnetDailySchedule.cpp reverted.
    • New gtests BACnetStackSchedule.Issue2850_WeeklyScheduleDayWriteRefusesAFailedCopy, Issue2850_WeeklyScheduleWholeArrayWriteRefusesAFailedCopy, Issue2850_ScheduleDefaultWriteRefusesAFailedClone. Revert-red: all 3 fail with the sources reverted.
  • rb g-20261005-1 (#2850, CR2-F28-024): a ReadProperty / ReadPropertyMultiple of any scalar or date-time Timer property (Present_Value, Timer_State, Timer_Running, Last_State_Change, Update_Time, Expiration_Time, Initial_Timeout, Default_Timeout, Min_Pres_Value, Max_Pres_Value, Resolution, Priority_For_Writing, and State_Change_Values[0], the array size) whose value allocation fails is now answered with Abort reason OTHER, as the State_Change_Values and List_Of_Object_Property_References arms already were (#547). Before, the engine returned false with no error, and the device fell through to its generic dispatch and served whatever that path returned. Allocation failure only.
    • New gtest BACnetStackTimerTest.Issue2850_GetPropertyScalarOomSetsErrorContainer. Revert-red: fails with the source reverted.
  • rb g-20261005-1 (#2850, CR2-F28-012): no change. BACnetTimeValue::Reset() already clears validity at the tip (#3537).
  • rb g-20261005-1 (#2850, CR2-F28-013): not done. BACnetStackSchedule::SetEffectivePeriod still stores unvalidated fields. Refusing them so an integrator sees it needs BACnetDBDevice::SetScheduleEffectivePeriod to pass a refusal on, and that file was out of scope for this batch.
  • rb g-20261005-2 (#2844, CR2-F22-020/037): Decode() now Reset()s before rejecting a null or zero-length buffer in BACnetEventLogDatum, BACnetEventLogRecord and BACnetSingleLogData (the classes the review names) and in the four log classes with the same early return (BACnetLogDatum, BACnetLogRecord, BACnetLogData, BACnetLogMultipleRecord). Before, the rejected decode returned 0 with Reject(other) but left an object that had already held a value reporting IsValid()==true with its old content. The return value and reject reason are unchanged.
    • New gtests <Class>.Issue2844_RejectedDecodeLeavesAPreviouslyValidValueInvalid and Issue2844_RejectedNullDecodeLeavesAPreviouslyValidValueInvalid in each of the seven classes. Revert-red: all 14 fail with the seven sources reverted.
  • rb g-20261005-2 (#2844, CR2-F22-008): deleting a Trend Log now frees its COV snapshot. Before, BACnetStackTrendLog had no destructor and the base destructor’s Reset() call reached only the base Reset(), so every COV Trend Log that had logged a value leaked its last-value clone when it was deleted (BACnetStack_DeleteObject) or the device was torn down.
    • New gtest BACnetStackTrendLog.Issue2844_DeletingATrendLogFreesItsCovSnapshot (MemTracker net retained bytes after delete). Revert-red: fails with the .h and .cpp reverted.
  • rb g-20261005-2 (#2844, CR2-F22-018): BACnetEventLogDatum::SetNotification returns false and leaves the datum invalid for an invalid notification. Before, it returned true and marked the datum valid. A direct BACnetStackEventLog::InsertNotificationLogRecord call with an invalid notification now returns false and logs nothing; before, it returned true and stored and counted a record that could not be encoded. The confirmed/unconfirmed notification path and the test-tool path already refuse an invalid notification, so they are unchanged.
    • New gtest BACnetEventLogDatum.Issue2844_SetNotificationReportsAnInvalidNotification. Revert-red: fails with the source reverted. Five existing tests in TestBACnetEventLogDatum.cpp that passed an empty notification while asserting success now build a complete one.
  • rb g-20261005-2 (#2844, CR2-F22-019): a BACnetEventLogRecord copy whose notification copy fails to allocate now reports IsValid()==false, and Clone() returns NULL for it. Before, the copy reported valid around a half-copied datum, so a ReadRange of the Event Log put it in the ACK and the ACK encode then failed; now the existing clone-failure path is taken.
    • New gtests BACnetEventLogRecord.Issue2844_CopyAssignWithFailedNotificationCloneIsInvalid, Issue2844_CloneWithFailedNotificationCloneReturnsNull. Revert-red: both fail with the source reverted.
  • rb g-20261005-2 (#2844, CR2-F22-025): BACnetLogRecord::SetLogDatum and SetLogRecord return false for an invalid datum, and a copy whose datum copy fails reports IsValid()==false. Before, both returned true and the record reported valid. Partial: BACnetStackTrendLog::InsertLogRecord still stores and counts such a record (now reporting invalid); its encode failed before and still fails. Refusing it before it is stored is not done.
    • New gtests BACnetLogRecord.Issue2844_SetLogRecordRefusesAnInvalidDatum, Issue2844_CopyAssignWithFailedDatumCloneIsInvalid. Revert-red: both fail with the source reverted.
  • rb g-20261005-2 (#2844, CR2-F22-026): copying or cloning a Trend Log Multiple record whose log-data is an empty list keeps the log-data arm. Before, BACnetLogData::Clone() returned an object on the log-status arm reporting invalid, and the record copy reported valid around it, could not be encoded and did not compare equal to the original. BACnetLogMultipleRecord also refuses a non-NULL log-data clone that lost its source’s validity (defensive: no current path produces one). Partial: a decoded log-status longer than 3 bits still makes BACnetLogData::Clone() return NULL.
    • New gtest BACnetLogMultipleRecord.Issue2844_CopyOfAnEmptyLogDataRecordKeepsTheLogDataArm. Revert-red: fails with the two sources reverted.
  • rb g-20261005-2 (#2844, CR2-F22-028/036): BACnetLogDatum::SetAnyValue and BACnetSingleLogData::SetAnyValue return false when they leave the datum invalid (a failed nested copy, or an empty value). Before, both returned true. The constructors’ “failed to set any value” error log now fires on that path.
    • New gtests BACnetLogDatum.Issue2844_SetAnyValueReportsAnEmptyValue, BACnetSingleLogData.Issue2844_SetAnyValueReportsAnEmptyValue; the two SetAnyValue_NestedCloneFails_* tests, which pinned the true return, now expect false. Revert-red: all 4 fail with the two sources reverted.
  • rb g-20261005-2 (#2844, found in review): a Trend Log Multiple no longer leaks one log-data allocation per logged sample once its buffer wraps. BACnetLogMultipleRecord::operator= cleared its log-data pointer before Reset(), so assigning onto a populated record orphaned the old log data. The Trend Log Multiple stores each sample by assigning into its circular buffer, and once the buffer is full with Stop_When_Full FALSE every sample overwrites an occupied slot, so the leak grew without bound on a long-running object (a reviewer measured 216 bytes per assignment). This is a separate defect from CR2-F22-027 (the time_t constructor’s conditional take-over of the log data), which is not fixed here.
    • New gtest BACnetLogMultipleRecord.Issue2844_AssigningOverAPopulatedRecordFreesItsLogData (MemTracker net retained bytes). Revert-red: fails with the source reverted.
  • rb g-20261005-2 (#2844): BACnetStackTrendLog’s copy constructor and copy assignment are deleted. With the new destructor a compiler-generated copy would free the COV snapshot twice; nothing in the stack copies a Trend Log.
  • rb g-20261005-3 (#2691, CR2-F13-019): the I-Am builder refuses a vendor ID above 65535, as Decode already does (#693). BACnetIAmRequest::SetVendorId returns false for a value above UINT16_MAX, so SetIAmRequest and the value constructor leave the request invalid. The vendor ID comes from the Device’s Vendor_Identifier, which the host supplies through its GetPropertyUInt callback, and all four I-Am builders use it: the Who-Is and You-Are processors, the foreign-device I-Am in BACnetDataLinkIPv4, and the I-Am send in BACnetInterface. Wire: with a Vendor_Identifier above 65535, no I-Am is built or sent, and each builder logs its existing “Failed to set the IAmRequest” error. Before, the stack sent an I-Am with a 3- or 4-octet vendor ID, which this stack’s own decoder rejects. A Vendor_Identifier of 65535 or less is unchanged.
    • New gtest BACnetIAmRequest.Issue2691_SetIAmRequest_VendorIdExceedsUnsigned16_Rejects (65535 accepted; 65536 and 70000 refused). Revert-red: fails with the source reverted.
  • rb g-20261005-3 (#2694, CR2-F16-040, EventSummary half): BACnetEventSummary’s priority accessors (GetEventPriorityToOffNormal/ToFault/ToNormal) and both SetEventPriority overloads now take and return uint32_t instead of uint8_t. In the GetEventInformation-ACK grammar, event-priorities is SEQUENCE SIZE (3) OF Unsigned. Before, a decoded priority of 300 (22 01 2C) read back as 44, and the value constructor narrowed a 300 to 44 and encoded 21 2C. Now 300 is read and encoded as 300. Decode is not tightened, because the grammar is plain Unsigned. Wire: the GetEventInformation-ACK this stack sends is unchanged, because its priorities come from the Notification Class’s uint8_t getters. The change affects client-side (test-tool) decoding and direct construction. The EnrollmentSummary half was already fixed at the tip by bs-20261005-9 (a priority above 255 is rejected with parameterOutOfRange).
    • New gtests BACnetEventSummary.Issue2694_DecodedPriorityAbove255IsNotNarrowed, Issue2694_ConstructedPriorityAbove255IsNotNarrowed. Revert-red: both fail with the .h and .cpp reverted.
  • rb g-20261005-3 (#2608, CR2-F03-039): BACnetDeviceObjectPropertyReference::CreateFromCompactString rejects an empty field anywhere in the string. Before, an empty field ended parsing, so "5,17,85," and "5,17,85,,9" were accepted as a 3-field reference (the ,9 was discarded), while "5,17,85,3," was rejected. The only caller is the write-property-async value path for datatype DeviceObjectPropertyReference. Wire: a value string like "5,17,85," now fails to build and no WriteProperty is sent. Before, a WriteProperty carrying the 3-field reference was sent. The test-tool encoder (referenceToCompactString) never emits a trailing comma, so it is unaffected.
    • New gtest BACnetDeviceObjectPropertyReference.Issue2608_CreateFromCompactString_EmptyFieldAfterThreeReturnsNull. Revert-red: fails with the source reverted.
  • rb g-20261005-3 (#2608, CR2-F03-007, partial): BACnetAbstractSyntaxType::ObjectFactory now builds a BACnetBinaryLightingPV for datatype BinaryLightingPV, and a BACnetCOVMultipleSubscription for COVMultipleSubscription under STACK_OPTION_SUBSCRIBE_COV_PROPERTY_MULTIPLE. Before, both fell to BACnetStackUnknownAPDU with an “unknown datatype” log. A decoded BinaryLightingPV still reports datatype Enumerated, so the Binary Lighting Output commandable-write path is unchanged. Partial: SecurityKeySet and NetworkSecurityPolicy still have no arm, because no class exists for either. The review’s sweep test over every profile datatype is not added. The AI/AO Interface_Value and Loop Low_Diff_Limit profiles had already moved to OptionalReal at the tip (#3266/#3264). DecodeAs shape change: Device Active_COV_Multiple_Subscriptions now renders as a typed CovMultipleSubscription tree, and Binary Lighting Output Relinquish_Default / Feedback_Value render with the element name BinaryLightingPV (still _datatype 9), where both went through the generic or unknown path before. A peer that sends a non-Enumerated Relinquish_Default or Feedback_Value now fails the whole ACK decode; the spec types them Enumerated.
    • New gtests BACnetAbstractSyntaxType.Issue2608_ObjectFactoryBuildsBinaryLightingPV, Issue2608_ObjectFactoryBuildsCOVMultipleSubscription. Revert-red: both fail with the source reverted.
  • rb g-20261005-4 (#3088, CR2-F01-055): CErrorContainer’s two-argument SetError, and SetAbort, SetReject and SetBvllError, clear a First-Failed-Element-Number left by an earlier three-argument SetError. Before, the old number stayed set, so a request-level error written over a list-element error would have reported the old position in an AddListElement/RemoveListElement ChangeList-Error. The three-argument overload still records its number. No path at the tip is known to have reached the stale value on the wire.
    • New gtest CErrorContainerFirstFailedElement.Issue3088_TwoArgumentSettersClearStaleElementNumber. Revert-red: fails with the source reverted.
  • rb g-20261005-4 (#3088, CR2-F01-013): SendConfirmedTextMessage (BACnetStack_SendConfirmedTextMessage) no longer leaks its request when the send fails before the request is handed over, for example on an unclaimed networkPortInstance. The call still returns false. Before, each failed call left one text-message request and its message string on the heap. The unconfirmed sender’s “no parameters” leak the review also names was already fixed at the tip.
    • New gtest BACnetInterfaceTest.Issue3088_SendConfirmedTextMessageFailedSendDoesNotLeakRequest (MemTracker). Revert-red: fails with the source reverted.
  • rb g-20261005-4 (#3088, CR2-F01-043, CR2-F01-053): the five test-tool EventNotification senders that take timestampForm (SendConfirmedEventNotificationWithTimestampForm, and the confirmed and unconfirmed WithEventValues and WithOutOfRangeValues) return false for a timestampForm other than 0 (time), 1 (sequenceNumber) or 2 (dateTime), and send nothing. Before, any other value sent a dateTime eventTimeStamp and returned true. A time, date or sequence number the timestamp refuses is now reported with its own log line, not as the generic send failure.
    • New gtest BACnetInterfaceEventNotificationTransmitTest.Issue3088_UnknownTimestampFormIsRefused (forms 3 and 7 on two of the five senders, which share one builder). Revert-red: fails with the source reverted.
  • rb g-20261005-4 (#3088, CR2-F01-053): BACnetStackTestTool_InsertAuditLogRecord returns false unless the buffer holds exactly one encoded record. Before, a record followed by any further octets was inserted and the call returned true.
    • New gtest BACnetTestToolInterfaceAuditLog.Issue3088_InsertAuditLogRecordRefusesTrailingOctets. Revert-red: fails with the source reverted.
  • rb g-20261005-4 (#3088, CR2-F01-053): BACnetStackTestTool_GetRouterRoute returns false when nextHopMac is smaller than the route’s next-hop MAC, and sets *nextHopMacLength to the length needed. dnet and portId are still written. Before, the MAC was cut to the buffer size and the call returned true, so a 6-octet B/IP next hop read into a 4-octet buffer came back as a wrong 4-octet MAC. A large enough buffer, or a NULL nextHopMac, behaves as before.
    • New gtest BACnetInterfaceTestToolConnectionToNetworkTest.Issue3088_GetRouterRouteRefusesATooSmallNextHopBuffer. Revert-red: fails with the source reverted.
  • rb g-20261005-4 (#3088, CR2-F01-049, partial): BACnetStackTestTool_SetAccessCredentialAssignedAccessRights and BACnetStackTestTool_SetAccessCredentialAuthenticationFactors return false and store nothing when an element is refused: an Access Rights instance above 4194303, or an authentication-factor value longer than 65535 octets. Before, the invalid element was stored, the call returned true, and a later read of the property could not encode it. SetCredentialDataInputSupportedFormats now checks its setters too, but none of them can refuse a value this export can pass today. Partial: the access-rule half (SetAccessRights{Negative,Positive}AccessRules) was already fixed at the tip by #3476 item 4. The header text the review asks to correct is in CASBACnetStackTestToolDLL.h, which this batch did not edit.
    • New gtest BACnetInterfaceTest.Issue3088_CredentialBuildersRefuseAnInvalidElement. Revert-red: both halves fail with the source reverted.
  • rb g-20261005-5 (#3088, CR2-F01-039, partial at the tip): BACnetStackTestTool_SetLifeSafetyZoneMembers, ...SetAccessDoorMembers, ...SetAccessZoneEntryPoints, ...SetAccessZoneExitPoints, ...IsAccessZonePassbackViolation and ...SendAuditLogQuery (with useQueryObject) return false when an object type is above 1023 or an instance above 4194303; lists are left unchanged and the audit query is not sent. Before, the pair was stored as an invalid or aliased reference, or the query went out for another object (type 1024 became analog-input), and the call returned true. Partial: nine of the review’s fifteen exports were already fixed at the tip by later work (SetAccessCredentialBelongsTo, UseAccessCredential, EvaluateAccessRights, AccessZoneEnter, AccessZoneExit, SetAccessPointAccessDoors, both SetAccessRights*AccessRules, and SetIntrinsicAccessEventAlgorithm’s credential pair). The two raw-pair packing sites (BACnetAuditLogQueryParameters::SetObjectIdentifier(uint16_t, uint32_t), BACnetDatabase::EnableIntrinsicAccessEventAlgorithm) are unchanged; these exports no longer reach them with an out-of-range pair.
    • New gtests Issue3472AccessZoneExportTest.OutOfRangeEntryExitPointAndPassbackCredentialAreRefused, AccessDoorMembersExport.OutOfRangeMemberPairIsRefused, LifeSafetyZoneMembersExport.OutOfRangeMemberPairIsRefused, BACnetAuditLogQuerySendTest.OutOfRangeQueryObjectIdentifierIsRefusedAndNothingIsSent, each with an in-range control. The three list tests store a valid list first and read it back by ReadProperty after every refusal (new test helper tests/ExportReadPropertyHelper.h). Revert-red: all four fail with the source reverted.
  • rb g-20261005-5 (#3088, CR2-F01-044): BACnetStackTestTool_SendAuthRequest with a NULL scope or a zero scope length is refused before the request is built, with its own log line (“SendAuthRequest: no scope given”), and nothing is sent. It returned false before as well, but only after building an invalid request, logging the generic “Failed to send the confirmed request”. The header already documented the refusal. Sending the absent-Scope “default scope” form (cl. 17.6.2) stays unsupported.
    • New gtest BACnetInterfaceSendAuthRequestTest.Issue3088_AbsentScopeIsRefusedUpFrontWithItsOwnLog. Revert-red: fails with the source reverted. Its log assertions are under STACK_OPTION_API_DEBUG_LOGGING; without debug logging it cannot tell the fix from the old path, since both return false with nothing sent.
  • rb g-20261005-5 (#3088, CR2-F01-023): BACnetStack_SendSubscribeCOVPropertyMultiple decodes each reference’s covIncrement from little-endian bytes explicitly, instead of a host-order memcpy. This matches the function’s own text in CASBACnetStackDLL.h (the references byte map and the WriteGroup note that cites it), which says little-endian. No reachable build changes behaviour: BACnetStackCommon.h stops a big-endian build with #error, and on a little-endian host both forms read the same value. For the owner: the frozen header contradicts itself - its global byte-order note (~lines 256-263) says these buffers are read in host order, which is now stale for this field.
    • New gtest BACnetInterfaceSubscribeCOVPropertyMultipleTest.CovIncrementIsDecodedFromLittleEndianBytes (2.5f packed by hand as 00 00 20 40). A pin, not a revert-red: it cannot fail without the fix on a little-endian host.
  • rb g-20261005-6 (#3088, CR2-F01-031): an error code a host CallbackSetProperty* / CallbackGetProperty* names is answered with its 135-2024 clause-18 class for four more codes: DEVICE_BUSY with DEVICE (was PROPERTY), UNKNOWN_OBJECT with OBJECT (was PROPERTY), NO_SPACE_FOR_OBJECT and NO_SPACE_TO_ADD_LIST_ELEMENT with RESOURCES (was PROPERTY). Wire effect: a WriteProperty or ReadProperty answer for those host-named codes now carries the corrected Error Class, e.g. Error(DEVICE, DEVICE_BUSY) instead of Error(PROPERTY, DEVICE_BUSY). WriteProperty still rewrites NO_SPACE_TO_ADD_LIST_ELEMENT to RESOURCES / NO_SPACE_TO_WRITE_PROPERTY (#2694). This follows BACnetCallbackInterface.h, which says “any code the callback sets is emitted with its canonical BACnetErrorClass”. Partial: the review’s other codes (CONFIGURATION_IN_PROGRESS, OPERATIONAL_PROBLEM, BUSY, SERVICE_REQUEST_DENIED, PASSWORD_FAILURE, …) keep the PROPERTY fallback until their class is checked word for word against the extracted spec. That spec was not available to this batch, which used the review’s clause quotes.
    • New gtest BACnetWritePropertyProcessorTest.Issue3088_SetPropertyCallbackErrorCode_TakesItsClause18Class, with a COMMUNICATION_DISABLED to PROPERTY control. Revert-red: fails with the source reverted.
  • rb g-20261005-6 (#3088, CR2-F01-048, CR2-F01-054, partial): the test-tool BACnetTestToolInterface::AddAuditLog, InsertAuditLogRecord (gated on STACK_OPTION_OBJECT_TYPE_AUDIT_LOG), SendUnconfirmedAuditNotification and SendConfirmedAuditNotification (gated on AUDIT_LOG || AUDIT_REPORTER, the S159-GATE) compile their bodies only when the types they use exist. A build without the option gets a “not compiled” stub that logs and returns false. A test-tool build with -DBACNET_PROTOCOL_REVISION=19 no longer fails in these four (44 errors to 0 in BACnetTestToolInterface.cpp). Nothing changes in the shipped FULL / revision-30 configuration. The BACnetStackTestTool_* declarations in CASBACnetStackTestToolDLL.h and the member declarations are deliberately still ungated, so the declared test-tool surface and the adapter generators’ input do not change. Partial: the NOREF sweep of this file’s other “not compiled” stubs is not done. A revision-19 build still fails elsewhere, at BACnetPropertyIdentifier.h:1694 (MAX_IMPLEMENTED_BACNET_PROPERTY_IDENTIFIER names maxProxiedIAmsPerSecond, which does not exist below revision 30). That is out of scope here.
  • rb g-20261005-6 (#3088, CR2-F01-064): BACnetCallbackInterface::Reset() NULL-initialises CallbackEventNotificationReceived under BACNET_STACK_TESTTOOL, the same guard as its declaration. Before, the reset also required STACK_OPTION_ALARMS_AND_EVENTS. No shipped configuration changes, because the test tool forces FULL, which turns alarms on.
    • New gtest BACnetEventNotificationReceive.Issue3088_EventNotificationReceivedCallbackIsNullAfterConstruction. It is a pin, not a revert-red; the alarms-off arm was shown with a g++ -E probe (see the commit).
  • rb g-20261005-8 (#2608, CR2-F03-044): REAL and DOUBLE IsEqual compare the bit pattern instead of IEEE ==. A NaN now equals a NaN with the same bits (NaNs with different payloads stay unequal), and -0 and +0 differ. Finite non-zero values compare as before. Values the stack builds itself (GetProperty) hold encoded bytes, and since #2368 those were already compared by their bytes, so for them +0 and -0 already differed and a NaN already equalled itself. This change makes decoded values, such as ones a host callback supplies, compare the same way. +0 and -0 are deliberately not normalised. Caller effects:
    • Value_Source COV, any representation: HasValueSourceMonitoredValueChanged decodes both values before comparing, so it used IEEE == even for stack-built values. With no COV_Increment, a Present_Value that stays NaN no longer reports a change on every evaluation, and a move between +0 and -0 is now reported.
    • SubscribeCOV / SubscribeCOVProperty / SubscribeCOVPropertyMultiple, decoded values, no COV_Increment or an increment of exactly 0: a REAL that flips between +0 and -0 now sends a COV notification, for example a host callback that computes roundf(-0.3f), or raw * negativeGain at raw 0. Stack-built values already did. A value that stays NaN is no change. With an increment above 0, nothing changes.
    • Command Failure, decoded values: a command of 0.0 against a computed feedback of -0.0 now reads as a mismatch and can go OFFNORMAL; before, it read as matched. A command and feedback holding the same NaN now read as matched.
    • Schedule write-through, decoded values: fires on a move between +0 and -0, and not on NaN to NaN.
    • List services: AddListElement / RemoveListElement matching and the cl. 15.1.2 ignore-if-identical option can now match a list element that holds a NaN REAL.
  • rb g-20261008-1 (#3702, CR3-F15-002): Active_COV_Multiple_Subscriptions listed a numeric COV reference that had no subscriber-supplied increment without a cov-increment, although the object’s COV_Increment was in use (cl. 12.11.65). It now lists the increment in use: the object’s COV_Increment for Present_Value when the object has one, otherwise 0, as Active_COV_Subscriptions does since #2707.
  • rb g-20261008-10 (#3711): RemoveListElement on a Notification Forwarder’s Recipient_List now removes the entry from the stack-held list the forwarder sends from. It used to answer DEVICE/OPERATIONAL_PROBLEM, or in a test-tool build answer SimpleACK and rewrite the host’s copy. Host-visible: a successful removal fires CallbackNotificationForwarderRecipientListChanged, and the forwarder’s DAB references are reconciled at once. The Recipient_List is still read-only by default. cl. 15.2.2 from the review finding, unverified.
  • rb g-20261008-10 (#3694): ABORTRESTORE is now accepted from RESTORE_FAILURE. It calls CallbackCompleteRestore with aborted=true, returns Backup_And_Restore_State to IDLE and sets System_Status OPERATIONAL, so the next STARTRESTORE or STARTBACKUP is admitted. Before, the device stayed in RESTORE_FAILURE after a failed restore preparation and answered every later start with CONFIGURATION_IN_PROGRESS. cl. 19.1.3.2 / 19.1.3.4 from the review finding, unverified.
  • rb g-20261008-10 (#3697): every valid I-Am now feeds the Device Address Binding table, whether or not the host enabled I-Am execution (off by default). Before, a device with the default service set never bound any entry, so device-form event recipients, Channel/Schedule/Timer external writes and the audit reporter recipient were never sent. Host-visible: those sends now happen, Device_Address_Binding lists the bound peers, and pending external writes go out when their target’s I-Am arrives. HookIAm is still called only when I-Am execution is enabled, and Protocol_Services_Supported is unchanged.
  • rb g-20261008-10 (#3698): the audit operation-target notification for a request from a peer on the local BACnet/IP or MS/TP link (no SNET/SADR) now names that peer as Source Device and includes the Invoke Id. The peer is named by its device identifier when the DAB has bound it from a local I-Am, otherwise by network 0 and its connection string. Before, the notification named this device and dropped the Invoke Id. Table 19-4 from the review finding, unverified.
  • rb g-20261008-10 (#3711): RemoveListElement on a stack-held forwarder’s Subscribed_Recipients also no longer reaches the host’s constructed-Set callback, because IsKnownStackHeldListProperty now covers both forwarder lists.
  • rb g-20261008-10 (#3697, security note): on a default configuration an I-Am now (re)binds any DAB entry it names. Device-form notifications, external writes and audit attribution therefore follow the most recent I-Am. This was already the case with the I-Am service enabled (I-Am is unauthenticated), and an I-Am never adds a DAB entry. Also, once the first I-Am sets the broadcast template, the DAB heartbeat now sends Who-Is for Registered entries.
  • rb g-20261008-11 (#3644): a WritePropertyMultiple whose property value is truncated or mistagged (absent after the property identifier, a primitive context [2] in place of the opening tag, or a missing closing [2]) now carries Reject(INVALID_TAG). BACnetPropertyValue::DecodeWithObjectType and BACnetNameValue::Decode used to fail there with no error set, so the request got no Reject reason (Abort OTHER or no reply). Wire-visible: those requests now get a Reject-PDU with reason INVALID_TAG. cl. 18.9 from the review finding, unverified.
  • rb g-20261008-11 (#3645): a reassembled segmented Confirmed-Request now gets the unsegmented decode’s completion rules. Octets after the service request other than zero padding are Reject(INVALID_TAG) (before, they were dropped and the request was executed), and a reassembled request that fails to decode is answered with the decoder’s Reject reason, or for WriteProperty with the #2817 Error PROPERTY/VALUE_OUT_OF_RANGE, instead of Abort(OTHER). Abort(OTHER) remains when no reason was recorded. cl. 5.4.5.2 / 5.4.5.3 from the review finding, unverified.
  • rb g-20261008-11 (#3643): REAL and DOUBLE ToString() and their value attribute, which DecodeAsJSON / DecodeAsXML render, now give the fewest digits, from 6 up, that read back exactly to the same value (up to 9 / 17 significant digits) instead of 6 significant digits: 1234.567 renders “1234.567”, not “1234.57”, and 123456789.123 renders “123456789.123”, not “1.23457e+08”. Host-visible: the text is written in the classic locale (always a ‘.’ decimal separator) and NaN renders “nan”. ChipkinConvert::DoubleToString is new.
  • rb g-20261008-12 (#3654): a BBMD no longer executes a Forwarded-NPDU it hears back from its own address (its two-hop local re-broadcast, or its local broadcast of a foreign device’s Distribute-Broadcast-To-Network, looped back by the host). Wire-visible: a Who-Is relayed by a BBMD draws one I-Am from it, not two, and every other unconfirmed service delivered across BBMDs runs once on the BBMD’s own device. J.4.5, from the review finding, unverified.
  • rb g-20261008-12 (#3651): under two-hop distribution (all-ones BDT host mask) a Forwarded-NPDU from another BBMD on this BBMD’s IP subnet now reaches every registered foreign device; the J.4.5 same-subnet test now uses the Network Port’s local address and subnet mask in preference to the BDT host mask, when the mask is valid (non-zero, contiguous, at least 2 host bits), and the BDT host entry otherwise - so a 0.0.0.0 or non-contiguous port mask cannot make an off-subnet sender reach the foreign devices. Wire-visible: foreign devices receive those broadcasts; the frame is still not re-broadcast locally. J.4.3 / J.4.3.2 / J.4.5, from the review finding, unverified.
  • rb g-20261008-12 (#3653): a backward step of the wall clock (CallbackGetSystemTime) re-bases a foreign-device table entry’s timer, so the entry is cleared at most Time-to-Live + 30 s after the step (a tick at time 0, an unset clock, is ignored rather than re-basing every entry) instead of when the clock catches up with its old creation stamp. Wire-visible: Read-Foreign-Device-Table-Ack reports at most TTL + 30 s remaining after a backward step, and a full FDT frees up. A forward step still expires entries early (unchanged). J.5.2.3, from the review finding, unverified.
  • rb g-20261008-2 (#3688, CR3-F10-014): cloning a valid decoded ReadAccessSpecification with an empty property list no longer gives an invalid copy. A Group List_Of_Group_Members holding such a member used to Abort OTHER on every later read of the property.
  • rb g-20261008-2 (#3686, CR3-F10-012): ReadPropertyMultiple with ALL or REQUIRED on an existing object of a proprietary object type no longer aborts the whole request. 135-2024 cl. 15.7.2: “The request shall continue to be executed until an attempt has been made to access all specified properties.” Wire effect: before, Abort OTHER for the whole request; after, a Result(+) that answers the other objects and carries one result for that object, Error(PROPERTY, OTHER).
  • rb g-20261008-2 (#3683, CR3-F10-004): a ReadRange whose first item does not fit what the requester can receive now picks its abort reason by 135-2024 cl. 5.4.5.3: “SEGMENTATION_NOT_SUPPORTED for case (a) and (b), or BUFFER_OVERFLOW for case (c) and (d)”. Wire effect: when the response would be segmented (this device transmits segmented, the request accepts segments and a clock callback is registered), the abort reason is now BUFFER_OVERFLOW instead of SEGMENTATION_NOT_SUPPORTED. Unsegmented cases keep SEGMENTATION_NOT_SUPPORTED. The rule lives in the new BACnetBusinessLogic::IsSegmentedResponsePossible, which GetTransmittableServiceAckLength also uses.
  • rb g-20261008-3 (#3746, CR3-F24-005): an Access Door re-commanded to an unlocking value (UNLOCK, or PULSE_UNLOCK / EXTENDED_PULSE_UNLOCK during its Door_Unlock_Delay_Time) no longer raises DOOR_OPEN_TOO_LONG from a countdown an earlier LOCK armed. 135-2024 cl. 12.26.19: “A door-open-too-long condition occurs when the Present_Value has a value of LOCK and one of the following conditions exist:”. Wire effect: no false Door_Alarm_State DOOR_OPEN_TOO_LONG, CHANGE_OF_STATE alarm or COV for a door that is deliberately commanded open; a later LOCK arms a fresh countdown.
  • rb g-20261008-3 (#3748, CR3-F24-007): an Access Point denying an inactive credential now sets Access_Event to the Table 12-41 event for the credential’s Reason_For_Disable, not always DENIED_CREDENTIAL_DISABLED. 135-2024 cl. 12.35.8: the Access_Event “shall be set to the value which corresponds to the reason this credential is disabled, as specified in the Reason_For_Disable property.” With several reasons, the first in enumeration order is used (a local matter per the clause). New BACnetStackAccessCredential::GetDenialAccessEvent(). Wire effect: the Access_Event value reported (and carried in ACCESS_EVENT notifications) changes, for example to 156 DENIED_CREDENTIAL_MAX_USES or 153 DENIED_CREDENTIAL_MANUAL_DISABLE instead of 158; an expired credential reports 152 DENIED_CREDENTIAL_EXPIRED. Integrators keying Access Alarm Events or logs on 158 for these cases see the specific value instead.
  • rb g-20261008-4 (#3648, CR3-F06-002): on a device that is both a virtual router and an enabled physical router, the stack’s own start-up I-Am-Router-To-Network now lists its virtual networks as well as the physical router’s reachable networks, each once. 135-2024 cl. 6.6.2: “each router shall broadcast out each port an I-Am-Router-To-Network message containing the network numbers of each accessible network except the networks reachable via the network on which the broadcast is being made.” Wire effect: on a gateway+router the start-up (and re-armed) announcement on every port carries the virtual network numbers, and a port with no physical network to list now announces the virtual networks instead of sending nothing. A physical-only router’s announcement is unchanged.
  • rb g-20261008-4 (#3647, CR3-F06-001): a physical router no longer executes a known network layer message (Who-Is-Router, I-Am-Router, I-Could-Be-Router, Router-Busy/Available, Initialize-Routing-Table(-Ack)) that is addressed through it to a DNET it has no route to, or whose route is busy. 135-2024 cl. 6.5.4: “If the next router is unknown, an attempt shall be made to identify it using a Who-Is-Router-To-Network message”; cl. 6.6.3.6: a message for a temporarily unreachable DNET draws “a Reject-Message-To-Network message with a reject reason of 2”. Wire effect: such a unicast message now draws a Who-Is-Router-To-Network on the other ports and is held; it is relayed if an I-Am-Router-To-Network answers, and rejected with reason 1 when the hold times out with no route found. A busy DNET draws reason 2 out the ingress port. Before, the router answered it itself (for example an Initialize-Routing-Table-Ack with its own table) and nothing was searched for or rejected. On a gateway that is also a router, the blanket virtual-router reason-1 reject no longer fires for these messages; they are searched for instead. A known-type message received as a link broadcast whose unicast DNET names an unknown network is now dropped unanswered, matching #2515’s rule for a data NPDU; before, the router executed it locally.
  • rb g-20261008-5 (#3738, CR3-F22-001): shrinking a Trend Log or Trend Log Multiple Buffer_Size below Record_Count now keeps the newest records instead of the oldest. The ReadRange builders number each record from Total_Record_Count on the premise that the buffer holds the newest Record_Count records (135-2024 cl. 12.25.14; clause number taken from the review finding CR3-F22-001, not verified against the extracted spec, which was not available on this machine), so after a shrink By Sequence Number and By Time ReadRange served the oldest records under the newest records’ sequence numbers, and the newest records (including the LOG_DISABLED record the Enable=FALSE write appended) were unreachable. ChipkinCircularBuffer::Resize changed; its only callers with records present are these two Buffer_Size writes.
  • rb g-20261008-5 (#3739, CR3-F22-002): a Trend Log Multiple WriteProperty of Log_Interval = 0 is now refused with Error(PROPERTY, VALUE_OUT_OF_RANGE) and Log_Interval keeps its value. Before, the write was accepted and the object, which is always POLLED on this path, appended a record on every BACnetStack_Loop() call. Trend Log is unchanged (its write of 0 switches to COV).
  • rb g-20261008-5 (#3742, CR3-F22-008): the Trend Log Multiple record’s time-change arm (BACnetLogData) now exports with RealToString and imports with TryParseReal, as #2776 did for the other REAL log text sites. Before, BACnetStack_ReadTrendLogMultipleRecord exported 86400.125 as “86400.1” and 1234567 as “1.23457e+06”, in the host’s global locale, so a record read out and written back stored a different value.
  • rb g-20261008-6 (#3728, CR3-F17-002): replacing an object’s event algorithm (an Event Enrollment re-armed through AddEventEnrollmentObjectWith*Algorithm) while its fault algorithm holds a fault no longer leaves Event_State NORMAL with Reliability still reading the fault. The next evaluation re-enters FAULT once, with a CHANGE_OF_RELIABILITY TO_FAULT carrying that Reliability, before the event algorithm can run, including when the host also reports that same Reliability (the #2359 “already reported” guard now applies only while Event_State shows the fault). Before, the fault algorithm reported no change and the new event algorithm ran from NORMAL with the fault present, for example going TO_OFFNORMAL. This is the second-stage half of #2349 (135-2024 cl. 13.2.2, cited from the review finding and not verified against the extracted spec, which was not available on this machine).
  • rb g-20261008-6 (#3729, CR3-F17-003): a first-stage (host or CONFIGURATION_ERROR) fault that clears while the fault algorithm holds the same Reliability no longer sends a second TO_FAULT. Before, the handover always re-entered FAULT, overwrote Event_Time_Stamps[TO_FAULT] and cleared Acked_Transitions[TO_FAULT] again, though Reliability did not change. This is the exit-edge twin of the #2359 entry guard (cl. 13.2.2.1.3, cited from the review finding, unverified).
  • rb g-20261008-6 (#3730, CR3-F17-005): CHANGE_OF_LIFE_SAFETY takes the current Mode as its baseline on every transition into NORMAL, not only when leaving FAULT, provided a baseline exists (a #3230 mode-reference change still clears it). A Mode changed while Event_Algorithm_Inhibit was TRUE is no longer reported at the first evaluation after the inhibit clears. Before, that evaluation sent a TO_NORMAL mode-change notification for a change nobody evaluated, or went OFFNORMAL without the Time_Delay (cl. 13.2.2.1.5, cited from the review finding, unverified).
  • rb g-20261008-6 (#3733, CR3-F17-016, partial): CHANGE_OF_CHARACTERSTRING now respects character boundaries when an alarm value and the monitored value are in the same UCS-2 or UCS-4 character set. A match must start at an offset that is a whole number of characters, and an alarm value whose length is not a whole number of characters never matches. Before, a UCS-2 “A” (00 41) matched the UCS-2 string U+4100 U+4142 (41 00 41 42) at octet offset 1, and a partial character (00 41 00) matched UCS-2 “AB”. When the two character sets differ, the raw octet match from before this batch is unchanged and pinned by a test, pending an owner ruling (#3787) (cl. 13.3.16 (c), cited from the review finding, unverified).
  • rb g-20261008-7 (#3692): a WritePropertyMultiple whose WriteAccessSpecification had a missing or wrong [1] opening or closing tag around its list of properties failed to decode with no error set, so it was answered with an Abort or not at all. Both tag checks now set Reject(INVALID_TAG) when nothing deeper already set an error (135-2024 cl. 18.9, from the review finding, unverified here).
  • rb g-20261008-7 (#3692): the same for a WriteGroup whose [2] change-list opening or closing tag is missing or wrong (BACnetWriteGroupRequest::Decode): it now fails with Reject(INVALID_TAG) instead of no reason.
  • rb g-20261008-7 (#3690): a WriteGroup still propagated a Channel’s value to every member while the Channel was Out_Of_Service. The Channel’s own Present_Value is still written, but nothing is forwarded to its members while the Channel reports Out_Of_Service TRUE (135-2024 cl. 12.53.10 (a), from the review finding, unverified here). An Out_Of_Service the stack cannot read is taken as FALSE, as before.
  • rb g-20261008-7 (#3691): a Null written to a writable BACnetOptional* property other than OptionalReal (Audit_Priority_Filter, an element of Alarm_Values/Fault_Values, Interface_Value, …) took the non-commandable “relinquish” path: Result(+) and the host’s CallbackSetPropertyNull was never called. Null is one of these datatypes’ values (cl. 21, from the review finding, unverified here), so it now reaches the Null callback, as #3264 did for OptionalReal. Wire change: a host that has made such a property writable but registered no CallbackSetPropertyNull now gets Error(PROPERTY, WRITE_ACCESS_DENIED) where it got Result(+); that matches #3264’s OptionalReal behaviour.
  • rb g-20261008-8 (#3734): a BitString Value armed with CHANGE_OF_BITSTRING now serves Alarm_Values (a one-element BACnetARRAY of BIT STRING) and Bit_Mask from the armed algorithm, and a CharacterString Value armed with CHANGE_OF_CHARACTERSTRING serves Alarm_Values (BACnetARRAY of BACnetOptionalCharacterString, Null for a Null entry, array index 0 = count); arming enables them, as #3131 did for CHANGE_OF_STATE. They read UNKNOWN_PROPERTY or the host’s answer before (cl. 12.37.14, 12.40.15, 12.40.16 - from the review finding, unverified). A host that served Alarm_Values or Bit_Mask itself now gets the armed algorithm’s values instead, as #3131 does for CHANGE_OF_STATE.
  • rb g-20261008-8 (#3741): the Event Log evaluates its Enable / Start_Time / Stop_Time window on every Loop tick, so a window opening or closing appends its log-status record at that tick (and reports it for BUFFER_READY), not at the next notification, if ever (cl. 12.27.13 - from the review finding, unverified). An enabled Event Log with no Start_Time / Stop_Time whose log-status still shows LOG_DISABLED appends its re-enabled status record on the first tick.
  • rb g-20261008-8 (#3747): an access attempt with a disabled authentication factor fails with the Table 12-42 event for its Disable value (DENIED_AUTHENTICATION_FACTOR_DISABLED / _LOST / _STOLEN / _DAMAGED / _DESTROYED), stamped with the credential it belongs to and counted as a failed attempt, not DENIED_UNKNOWN_CREDENTIAL with no credential (cl. 12.35.10 - from the review finding, unverified). A disabled factor on an inactive credential reports the factor’s Table 12-42 event, not the credential-status (Table 12-41) event; an enabled match on another credential still wins.
  • rb g-20261008-8 (#3750): every authorized grant records the zone exit / entry (Credentials_In_Zone, Last_Credential_Added/Removed, the passback record) whatever the Access Point’s Occupancy_Count_Adjust; that property now gates only the Occupancy_Count change, so HARD_PASSBACK / SOFT_PASSBACK work with its default FALSE (cl. 12.31.25, 12.32.21 - from the review finding, unverified). Authorization_Mode NONE still skips the zone enter / exit.
  • rb g-20261008-9 (#3708): a received EventNotification (confirmed or unconfirmed) whose Notify Type is above 2 fails its decode with Reject UNDEFINED_ENUMERATION, and one whose Event Type, From State or To State is above 65535 fails with Reject PARAMETER_OUT_OF_RANGE, as #2679 does for Priority. Before, they decoded and the accessors narrowed them, so Notify Type 258 read as ACK_NOTIFICATION and To State 65538 as OFFNORMAL (cl. 18.9 and the BACnetNotifyType definition - from the review finding, unverified). The confirmed form is now Rejected instead of SimpleACKed; the unconfirmed form is dropped.
  • rb g-20261008-9 (#3709): the Notification Forwarder keeps and forwards a received notification whose Message Text is 256 octets or more (64 in the small build) with the text intact, provided the notification fits the outgoing APDU. Before, the rebuild refused the text, the notification was not sent, and Drain counted it as delivered. The same 5-argument SetEventNotificationRequest also serves local-origin sends, which now put such a text on the wire instead of dropping the notification - in practice the AcknowledgeAlarm ACK_NOTIFICATION, which copies the acknowledged notification’s text (BACnetAcknowledgeAlarmProcessor). The character-set check still applies.
  • rb g-20261008-9 (#3710): on a device with no router port, a Notification Forwarder Recipient_List entry addressed as a local broadcast (network 0) is no longer a target for a notification received over the wire, so it is not re-broadcast onto the network it came from (rule (a), cl. 12.51 - from the review finding, unverified). A notification originated by this device can still be broadcast locally.
  • rb g-20261009-1 (#3656): when a new BACnet/IP or MS/TP Network Port takes over the data link of a removed one (RemoveObject, no free instance), the old port’s router-port binding is dropped and every binding is resolved again, as the BACnet/SC takeover already did. Before, the taken-over data link stayed bound to the old router port, so that router port’s routed traffic (global broadcasts included) went out the new port’s network, and ingress on the new port was attributed to the old router port. Now a router port with no Network Port on its network is bound to no data link, as for BACnet/SC; when the new port is on the old port’s network, the router port is bound to it again (cl. 6.3.2 - from the review finding, unverified).
  • rb g-20261009-1 (#3655): a data link released by RemoveDevice, or by AddDeviceToVirtualNetwork’s rollback, is reset before the next Network Port of any device reuses it, as a stale takeover is. BACnet/IP drops the removed port’s foreign-device registration, BDT, FDT and BBMD mode. BACnet/SC closes the removed port’s hub-connector, hub-function and direct connections and drops its per-port state (VMAC, accept URIs, hub-function, hub-connector and direct-connect configuration), and KEEPS the device-wide settings the host sets stack-wide (UUID, Hello capabilities, compatibility flags, maximum BVLC and NPDU lengths, the four timeouts), so an SC port added after the removal still starts. Every type drops its router-port binding and the router bindings are resolved again. Before, the new port inherited all of it: it acted as a BBMD forwarding to the old BDT peers and kept re-registering as a foreign device with the old BBMD. An instance that was never claimed keeps its configuration. Host-visible: RemoveDevice of a device with a BACnet/SC Network Port now calls CallbackSCStopListening and CallbackDisconnectWebsocket synchronously, from inside RemoveDevice; a host that holds its own websocket lock while calling RemoveDevice must not do so. The #3184 stale-takeover arm is unchanged: it still takes device-wide settings from the other BACnet/SC instance.
  • rb g-20261009-1 (#3657): a client send over BACnet/SC naming a Network Port goes out that port’s data link. Before, the SC arm of ProcessOutgoingPacket ignored the port the Send exports resolved and chose the data link from the connection string, which for a VMAC-addressed unicast (“url;VMAC” or a bare VMAC) matches no local URI, so the send went out instance 0: that port’s hub and out-of-service state, and CallbackSendMessageForPort was given the first port’s instance. Routed SC egress (which passes no port and addresses by the egress port’s accept URI) is unchanged.
  • rb g-20261009-10 (#3800): request decodes that failed with no reason now set one (the #3644 shape). Wire-visible: a WriteProperty that ends before its Property_Value is answered with Reject MISSING_REQUIRED_PARAMETER; one whose value tag is cut short, ends at the opening [3], or has no closing [3] gets Reject INVALID_TAG. Before, each of these requests got no reply at all (the decode failed without a reason the reply could be built from). AddListElement / RemoveListElement ending before List_Of_Elements get Reject MISSING_REQUIRED_PARAMETER; ending at the opening [3], or with a tag or element cut short inside it, they get Reject INVALID_TAG (also no reply before). A ReadRange whose opening Range tag is cut short or whose Range has no closing tag gets Reject INVALID_TAG (no reply before). A malformed Setpoint_Reference WriteProperty now gets Reject INVALID_TAG instead of no reply.
  • rb g-20261009-10 (#3800 part B): BACnetAbstractSyntaxType::EncodeString renders REAL and DOUBLE with RealToString / DoubleToString, so the text reads back to the same value (“12345.67”, was “12345.7”). EncodeString has no production caller, so nothing host- or wire-visible changes.
  • rb g-20261009-10 (#3832): the #3807 out-of-range check now covers the Priority_Array readers. Wire-visible: a ReadProperty of Priority_Array (the whole array, or the indexed slot) where the host returns a Date / Time / DateTime slot with a field out of range is answered Error PROPERTY / VALUE_OUT_OF_RANGE. A commandable Present_Value resolved from the Priority_Array refuses only when the WINNING (highest-priority non-Null) slot is out of range - VALUE_OUT_OF_RANGE then; a bad slot below the winner does not affect it and Present_Value is the winning value. Before, the bad value was encoded invalidly / Abort OTHER, or the read answered VALUE_NOT_INITIALIZED. Host-visible: BACnetAbstractSyntaxType::DecodeString and DecodeBinary refuse a Date, Time or DateTime with a field out of range (StringToDate / StringToTime do not range-check).
  • rb g-20261009-10 (#3784): tests/TestChipkinUtilities.cpp, TestBACnetRespondingStateMachine.cpp, TestBACnetColorCommand.cpp and TestBACnetAuthorizationEventStatus.cpp now compile under STACK_OPTION_TARGET_EMBEDDED + IPv4, using the option guards their subjects use. Build-only change.
  • rb g-20261009-11 (#3842): an armed stack-held Notification Class now reads its own intrinsic-reporting properties through the generic alarm-and-event reader, as every other armed object does. Wire-visible, the properties that changed are Event_Enable, Acked_Transitions, Event_Time_Stamps and Event_Detection_Enable: a ReadProperty of Event_Enable returns the record value (B’111’ after arming, B’010’ after a WriteProperty of B’010’), and Event_Time_Stamps returns three unspecified DateTime timestamps. Observed before: Event_Enable read as an empty bit string (2 encoded octets) whatever was armed or written. Event_State already read NORMAL before the fix. Reliability keeps the host’s first-stage value (#2359 / #2384 / #3169): a host Reliability of 7 reads 91 07 armed and unarmed, as before. Priority, Ack_Required, Recipient_List and Notification_Class are still served by the Notification Class block; an unarmed class is unchanged (Event_Enable UNKNOWN_PROPERTY). RPM and COV of these properties go through the same reader (ReadProperty tested).
  • rb g-20261009-11 (#3792): BACnetStackMessageGenerator::DeliverEventNotificationToRecipient no longer ignores ProcessOutgoingPacket’s result. It reports a refused send through a new optional out-parameter; its return value is unchanged (false only on an allocation failure), so a NotificationClass Recipient_List walk is not cut short. The Notification Forwarder’s Drain no longer counts a refused send as sent; the entry is still dropped (not retained) and is counted in the new BACnetStackNotificationForwarder::GetUndeliverableDeliveryCount(). That count covers both a permanent refusal (a notification too large for the destination APDU) and transient ones (SC direct connection not ready, no SC hub, SC port out of service, DCC disabled, a short IPv4 send); both were already lost before, counted as sent. Observed before: a received notification with a 3000-octet Message Text forwarded to an unconfirmed BACnet/IPv4 recipient put nothing on the wire and Drain returned 1 (sent). Nothing more is sent on the wire than before. Revert-red was a mutation (the line setting sendFailed replaced by (void)sendFailed), observed failing at tests/TestBACnetStackNotificationForwarder.cpp:1112 (oversizeSentCount was 1).
  • rb g-20261009-12 (#3803): a network-layer reply to a request that arrived as an Original-Broadcast-NPDU (or a Distribute-Broadcast-To-Network a BBMD executes locally) is now sent as an Original-Unicast-NPDU (81 0A) to the sender. Wire-visible: an Initialize-Routing-Table-Ack answering a broadcast Initialize-Routing-Table was observed going out as 81 0B with broadcast = true; it is now 81 0A to the UDP source. Only Initialize-Routing-Table-Ack changes on the wire: no Reject-Message-To-Network reaches this path for a broadcast (#2890 and the virtual-router checks suppress them, and a physical router’s rejects are routed egress, unchanged). I-Am-Router-To-Network and Network-Number-Is stay local broadcasts (unchanged, pinned by a control arm). Routed egress (a router relaying a broadcast network message) is excluded and unchanged. Clauses from the review finding, unverified.
  • rb g-20261009-12 (#3803): a backward step of the wall clock (CallbackGetSystemTime) now re-bases the foreign-device registration deadline to the stepped-back time plus Time-to-Live, so the device stops treating itself as registered at most one Time-to-Live after the step (the #3653 bound). Observed before: registered at t = 1000 with TTL 60 and stepped back to t = 100, the device still reported itself registered at t = 161 (until t = 1061), and kept sending broadcasts as Distribute-Broadcast-To-Network to its BBMD. Host-visible through the foreign-device state and the Network Port FD properties. A forward step still ends the registration early (unchanged). A clock reading of 0 (unset RTC) is not treated as a step.
  • rb g-20261009-12 (#3795): a WriteProperty or WritePropertyMultiple from a writer on this device’s own link (no SNET/SADR, a connection string of at most BACNET_NPDU_MAX_SLEN octets) now records that writer in Value_Source / Value_Source_Array as network 0 plus the connection string as MAC. Observed before: the slot stayed none. Wire-visible in Value_Source reads, and a local-link writer can now write its own Value_Source slot (it was refused WRITE_ACCESS_DENIED because no owner was recorded). A routed writer is recorded by SNET/SADR as before. Cl. 19.5.1.3 from the review finding, unverified.
  • rb g-20261009-12 (#3803 review): a single clock reading of 0 (an unset RTC) no longer re-bases a BBMD’s foreign-device table entry. Observed before: FDTEntryTick(0) moved the entry’s creation stamp to 0, so the next good reading expired every FDT entry (the zero guard g-20261008-12 described for #3653 was not in the code). Wire-visible: foreign devices stay in Read-Foreign-Device-Table-Ack and keep receiving Forwarded-NPDUs across a 0 reading.
  • rb g-20261009-12 (#3651, #3653, #3654, #3780, #3655, #3657): restores the g-20261008-12 and g-20261009-1 review fix rounds, which were missing from their merged squashes (cherry-picked 660f9432, 67cfaf57 and faac6a51; those commits also update their own batches’ fragments). The g-20261008-12 round restores the #3651 SECURITY guard IsUsableLocalSubnetMask: an all-zeros or otherwise unusable port mask falls back to the BDT host entry. Without it, any internet sender’s Forwarded-NPDU reached every foreign device, reopening #2535. It also restores the #3653 tick-at-0 guard in FDTEntryTick (the same guard this batch’s #3803 review re-added; one guard is kept, with both tests), the #3780 DBTN-reply test and the #3654 bound-address test. The g-20261009-1 round restores this: RemoveDevice on a device with a single BACnet/SC data link keeps the stack-wide SC settings (UUID, Hello capabilities, compatibility flags, maximum BVLC/NPDU lengths, the four timeouts) and drops only per-port state (VMAC, accept URIs, hub/direct-connect configuration, connections). Before, those settings were wiped and the next SC port refused to start (#3655). It also restores the release-path router-port test and the #3657 url;VMAC client-send test.
  • rb g-20261009-13 (#3744): an AuditLogQuery-ACK is now cut to what the requester can receive (one APDU, or the segmented budget when both sides segment), as ReadRange (#2944) and GetEventInformation (#3275) are: the records that fit are returned, latest first, with No More Items FALSE. Observed before: the page was capped by Requested Count alone, so ten matching records for an unsegmented 128-octet requester were all built into one ACK larger than the requester’s APDU; now 3 records and No More Items FALSE, and the next query from the last sequence number continues. When not even one record fits, the processor answers Abort SEGMENTATION_NOT_SUPPORTED (or BUFFER_OVERFLOW when the ACK would be segmented), as ReadRange does. Wire-visible; no host API change. Cl. 13.19.1.2.3 (from the review finding, unverified).
  • rb g-20261009-14 (#3693): a confirmed AddListElement / RemoveListElement whose first list element tag does not decode (e.g. ... 3E F9, an extended tag number with its octet missing) is now answered with one Reject-PDU, INVALID_TAG. Observed before: the decode set Error(PROPERTY, DATATYPE_NOT_SUPPORTED), which the confirmed-request path does not answer, so no reply was sent (the new test failed without the fix). The issue exact frame ending at the opening [3] was already answered Reject INVALID_TAG by #3800; it is now pinned end to end. Wire-visible; no host API change. 135.1-2023 cl. 13.4 (from the review finding, unverified).
  • rb g-20261009-14 (#3735): ReadProperty(Fault_Parameters) for fault-life-safety, fault-status-flags and fault-listed now returns the stored reference with its propertyArrayIndex [2] and deviceIdentifier [3]. Observed before: both optional fields were dropped from the read-back although the written reference was stored and used whole. Wire-visible; no host API change.
  • rb g-20261009-14 (#3732): the OUT_OF_RANGE, DOUBLE_OUT_OF_RANGE, FLOATING_LIMIT, SIGNED_OUT_OF_RANGE, UNSIGNED_OUT_OF_RANGE and UNSIGNED_RANGE algorithms now check, per property, only the host Set callback that property uses (the #1562 fix for CHANGE_OF_STATE). Observed before: a NULL callback of another type made SetProperty return false with no error for every property, so Time_Delay (with only CallbackSetPropertyUInt), the Signed Deadband, and Limit_Enable (no callback) were not applied to the algorithm and the write fell through to the host. Host-visible: such writes are now applied by the algorithm; a property whose own callback is not registered is refused with PROPERTY / WRITE_ACCESS_DENIED (was: fell through to the generic host path).
  • rb g-20261009-15 (#3752): a LifeSafetyOperation RESET, RESET_ALARM or RESET_FAULT addressed to a Life Safety Zone that is not Out_Of_Service now resets the Zone’s members and recomputes the Zone’s roll-up, instead of also setting the Zone’s Present_Value to its Tracking_Value. Observed before: when a member’s Alarm_Values/Fault_Values classified the latched value differently from the Zone’s own lists, the member stayed latched while the Zone dropped to its Tracking_Value; the Zone’s CHANGE_OF_LIFE_SAFETY went TO_NORMAL, then the roll-up restored the member’s value and the Zone went TO_OFFNORMAL again, and that new transition cleared a SILENCE applied to the Zone (the new test failed on all three). An Out_Of_Service Zone keeps the unlatch. Wire-visible (event notifications, Present_Value, Silenced); no host API change. 135-2024 cl. 12.16.4, 12.16.23.
  • rb g-20261009-15 (#3740): a polled Trend Log or Trend Log Multiple with Align_Intervals TRUE now aligns its Log_Interval boundaries to the device’s local clock (UTC_Offset and Daylight_Savings_Status, the same local time its record timestamps use). Observed before: boundaries were measured from the UTC epoch, so a day-aligned log at UTC_Offset 480 sampled at 16:00 local, and an hour-aligned log at UTC+5:30 sampled at :30 local. When the effective offset changes at run time, a boundary must have passed on both the old and the new local clock, so the boundary just logged is never re-fired: a DST fall-back logs every real hour (no gap and no duplicate); after a fall-back a day-aligned log takes one extra early sample at 23:00 local and is back on midnights from 00:00; a spring-forward never skips an hourly sample and delays the first day-aligned sample after the jump by at most one hour. A run-time offset increase can bring the next sample earlier (as at fall-back); a decrease delays it by at most the size of the change. No sample is ever later than one Log_Interval after the last. Host-visible: on a device whose effective local offset (UTC_Offset, less 60 minutes while Daylight_Savings_Status is TRUE) is not 0, aligned samples move to the local second/minute/hour/day boundary; a device whose effective local offset is 0 is unchanged. 135-2024 cl. 12.25.27, 12.30.14.
  • rb g-20261009-16 (#3689): a WriteGroup whose change list carries a lighting command (BACnetChannelValue lighting-command [0]), an xy color [1] or a color command [2] is now decoded as a BACnetLightingCommand, BACnetxyColor or BACnetColorCommand. Observed before: the arm decoded as opaque fields, the Channel’s Present_Value write refused it, and a lighting command received over the wire never reached the Channel or its members (a hand-written WriteGroup frame left the member Lighting Output’s slot 6 NULL; it now holds 70.0). A constructed arm whose content is not the named type now fails the WriteGroup decode instead of being passed on. A value built in code with a typed child and no recorded context now encodes inside its [0]/[1]/[2] arm, the octets a peer sends; it used to encode with no arm wrapper. The xycolor [1] and color-command [2] arms are covered by decode and byte-identical re-encode tests; only the lighting-command [0] arm is tested end to end through the Channel. Wire-visible; no host API change. 135-2024 cl. 15.11.1.1.3, 15.11.2, 21 (BACnetChannelValue).
  • rb g-20261009-16 (#3707): a COV-multiple notification whose queued changes do not fit one APDU is now sent as several Confirmed/UnconfirmedCOVNotificationMultiple requests, each within the smaller of the data link’s Max_APDU (480 octets on MS/TP, 1476 otherwise) and the subscriber’s own Max_APDU_Length_Accepted when it is known, less the APDU header; each confirmed request has its own invoke ID. Observed before: all changes went into one request; for 15 values of 200 octets on BACnet/IP nothing was transmitted and the queue was emptied, so every change was lost (now 3 requests carrying all 15; a subscriber that accepts 480 octets over BACnet/IP now gets every change in requests of at most 480). A single value too large for any request to that subscriber can never be sent: it is now dropped with an error log before a request is built (before, the request carrying it failed to encode), and the other values are still sent. A queue that fits one APDU is unchanged (one request). Wire-visible; no host API change. 135-2024 cl. 13.18.1.1.5.
  • rb g-20261009-16 (#3646): an Unconfirmed-Request (Who-Is, I-Am, Who-Has, COV or event notification, TimeSynchronization, …) from a peer this device is in a transaction with is now served, and that peer’s transaction continues. Observed before: it was routed to that peer’s transaction entry; in SEGMENTED_REQUEST it reached the state’s catch-all, which sent the peer an Abort (reason OTHER, server = TRUE) for its segmented request, reset the entry to IDLE and did not serve the Unconfirmed-Request (if the SegmentTimer had already expired the entry was reset silently instead), and in SEGMENTED_RESPONSE or AWAIT_RESPONSE it was dropped (in each case a Who-Is got no I-Am). It is now dispatched as UnconfirmedReceived without touching the entry, whose state and invoke ID are unchanged. Wire-visible; no host API change. 135-2024 cl. 5.4, 5.4.5.1.
  • rb g-20261009-17 (#3673): a BACnet/SC broadcast whose connection string names a direct-connect peer is now sent to the connected hub when it is made with a send function’s broadcast flag - with the bare direct-connect URI, with “directUri;VMAC”, or with “hubUri;VMAC” where a direct connection to that VMAC is Connected - or when its NPDU carries DNET X’FFFF’ without a destination VMAC (no “;VMAC” suffix). The “;FFFFFFFFFFFF” spelling #3340 already handled is unchanged. Observed before: with the direct connection established the broadcast went over it (the host’s send callback was given the direct URI, the frame carrying the broadcast DVA), and with initiate enabled and no connection up the send returned false and started a direct connection to that URI instead. With no hub connection such a broadcast is not sent at all (the send returns false) and starts no direct connection. Behaviour change: SendWhoIs(directUri, port, false) with the default destinationNetwork 65535 is a global broadcast and now goes to the hub; passing destinationNetwork 0 still sends it over the direct connection. When the NPDU carries a DNET, the destination address is ignored for this classification (it is the DADR, which the send path never uses as a VMAC there), so DNET X’FFFF’ with a 6-octet destination address now goes to the hub, where it previously stayed on the direct connection. A unicast to the same URI is unchanged. Wire-visible; no host API change. 135-2024 AB.4.2.1, AB.1.6, cl. 6.5.3.
  • rb g-20261009-17 (#3675): a BACnet/SC node now answers an Advertisement-Solicitation received on a direct connection it initiated (with an Advertisement), and an Address-Resolution received on an initiated or an accepted direct connection with an Address-Resolution-ACK listing its direct-connect accept URIs when accepting direct connections is enabled. On an initiated connection with accepting disabled it answers with a BVLC-Result NAK (COMMUNICATION / OPTIONAL_FUNCTIONALITY_NOT_SUPPORTED); an accepted connection exists only while accepting is enabled, so it always gets the ACK. The answer goes back on the connection the request came in on. Observed before: each was dropped with no reply. Wire-visible; no host API change. 135-2024 AB.3.2, AB.3.3.
  • rb g-20261009-18 (#3640): a SubscribeCOV notification on a Staging object that is triggered by something other than Present_Value meeting COV_Increment now carries the current Present_Value, and that value becomes the baseline for the next COV_Increment comparison. Observed before: Present_Value 100.0 -> 103.0 (below COV_Increment 5.0) with Present_Stage 1 -> 2 sent a notification with Present_Stage 2 but Present_Value 100.0 (REAL 4442C80000), the value last reported; and a later 107.0 was notified although it is only 4.0 from the 103.0 the subscriber should have received. Now that notification carries 103.0, 107.0 is not notified and 108.0 is. The same holds for a Status_Flags-only trigger (Event_State to OFFNORMAL with Present_Value 100.0 -> 102.0 now reports 102.0, then 106.0 is not notified and 107.0 is) and for a Door_Alarm_State-only trigger on an Access Door. A SubscribeCOVProperty notification already read the live value, so for it only the baseline changes; SubscribeCOVPropertyMultiple is unaffected. Wire-visible; no host API change. 135-2024 Table 13-1.
  • rb g-20261009-2 (#3806): a whole-array WriteProperty or WritePropertyMultiple to a host-held BACnetARRAY whose array-length read (CallbackGetPropertyUInt at index 0) fails is answered Result(-), not Abort(OTHER). The answer is the code the host named, or Error(PROPERTY, WRITE_ACCESS_DENIED) when the host named none or no CallbackGetPropertyUInt is registered. In a WPM, only that write fails, not the whole response (135-2024 cl. 15.9.2, from the review finding, unverified).
  • rb g-20261009-2 (#3807): the Date, Time, DateTime, TimeStamp and ObjectIdentifier get-callback paths refuse a value the host returns with a field out of range: a month or day of 0, an hour of 24, an object type above 1023 or an instance above 4194303. They answer Error(PROPERTY, VALUE_OUT_OF_RANGE). Before, GetProperty reported success, and the ReadProperty-ACK, the RPM (whole response), the COV notification or the event read then failed at encode with Abort(OTHER). The value is treated as a host decline with VALUE_OUT_OF_RANGE, so no stack-stored fallback is tried.
  • rb g-20261009-3 (#3818): a Timer armed for CHANGE_OF_TIMER now serves Alarm_Values, and an Access Point armed for ACCESS_EVENT serves Access_Alarm_Events, from the armed algorithm (a list of Enumerated values; an array index answers PROPERTY_IS_NOT_AN_ARRAY); arming enables the property. Both answered UNKNOWN_PROPERTY, or whatever a host callback returned, which a host that served them now no longer reaches (135-2024 cl. 12.57.27, 12.31.39, from the review finding, unverified).
  • rb g-20261009-3 (#3817): SetSegmentationEnabled(NO_SEGMENTATION, …, enabled TRUE) now removes Max_Segments_Accepted and APDU_Segment_Timeout from the Device object (Property_List, ReadProperty UNKNOWN_PROPERTY), as enabled FALSE already did; any other Segmentation_Supported value still enables both (Table 12-13 footnote 1, from the review finding, unverified).
  • rb g-20261009-3 (#3819): property-profile default change - Access Point Lockout_Relinquish_Time is now present by default (read-only, in Property_List and the required list), matching Lockout; it answered UNKNOWN_PROPERTY (cl. 12.31.16, from the review finding, unverified).
  • rb g-20261009-3 (#3816): an Adjust_Value write, an Occupancy_Count write under Out_Of_Service, and the Access Zone count-direct / count-enable / upper- and lower-limit setters now push Occupancy_State and Occupancy_Count when they change, so the zone CHANGE_OF_STATE algorithm transitions and notifies and COV subscribers are told; before, nothing was evaluated until an unrelated zone enter/exit (cl. 12.32.6, 12.32.13, from the review finding, unverified).
  • rb g-20261009-4 (#3814): a Reject-PDU or server Abort-PDU sent for a malformed confirmed request, and the #2817 Error-PDU for an out-of-range WriteProperty, now carry the invoke ID of the request itself. A decoder that probed an absent OPTIONAL constructed field (e.g. GetEnrollmentSummary with enrollmentFilter absent, then an out-of-range eventStateFilter) reset the error container and the Reject went out with invoke ID 0, so the client never matched it and could have an unrelated transaction with invoke ID 0 terminated (wire-visible).
  • rb g-20261009-4 (#3811): ResetBroadcastDistributionTable now returns every Network Port that read BACnet_IP_Mode = BBMD to NORMAL, as ResetBBMD has since #2548; it turned the data link BBMD off but left the port reading BBMD (cl. 12.56.25, from the review finding, unverified).
  • rb g-20261009-4 (#3813): SetBACnetSCCertificateFileObjects now returns false, changing nothing and opening no pending-changes transaction, for an operational or CSR File instance (when its flag is set) or an issuer File instance above 4194303. It checked the masked instance for existence, stored an invalid identifier and returned true (host-visible: a call that used to succeed now fails).
  • rb g-20261009-4 (#3808): a Channel whose Priority_Array slots are all Null answers Present_Value with Null instead of Error PROPERTY / UNKNOWN_PROPERTY (it fell back to a Relinquish_Default the Channel does not have) (cl. 12.53.5, from the review finding, unverified; wire-visible on ReadProperty / RPM).
  • rb g-20261009-5 (#3823): ChipkinConvert::ToString renders a uint8_t / int8_t as its decimal value, and TryParse<uint8_t> / TryParse<int8_t> read a number with a range check. operator<< on an unsigned char is the character inserter, so six DecodeAs attributes (I-Could-Be-Router-To-Network performanceIndex, Initialize-Routing-Table numberOfPorts / portId / PortInfo length, router entry status, BACnet/SC BVLC-Result resultForBVLCFunction) received a raw byte; the issue reports that 5 became a control character (an empty XML attribute) and 65 became “A”. Host-visible in BACnetStack_DecodeAs output: XML (tested) and JSON (tested), both on performanceIndex; the other five attributes go through the same template and are not individually tested. Plain char still renders as the character.
  • rb g-20261009-5 (#3822): the Color object profile resolves Value_Source (optional, read-only, BACnetValueSource), as #1293 did for Color Temperature; SetPropertyEnabled(color, valueSource, true) used to fail and ReadProperty answered UNKNOWN_PROPERTY (Table 12-84, from the review finding, unverified; host-visible).
  • rb g-20261009-5 (#3815): on a device with more than one Network Port, Network Port instance 4194303 resolves to the port the frame arrived on (the ingress instance the receiving data link stamps) when that is one of the receiving device’s own Network Ports, falling back to the first port of the packet’s network type otherwise. It always took the first port of the type, so with two ports of one type a ReadProperty / RPM of (NetworkPort,4194303), the What-Is-Network-Number answer and Network-Number-Is learning used the wrong port (cl. 15.5.2, from the review finding, unverified; wire-visible). The stamp is the physical data link’s port, owned by the main device, so a virtual device (AddDeviceToVirtualNetwork) ignores it and keeps the first-of-type rule. Tested at the resolver and end to end through ProcessNetworkNumberIs; ReadProperty / What-Is-Network-Number are covered only through the shared resolver.
  • rb g-20261009-6 (#3705): a SubscribeCOV for an object that has some subscribable property but not its Table 13-1 COV property (for example an Analog Input with Out_Of_Service subscribable and Present_Value not) is answered Error OBJECT / OPTIONAL_FUNCTIONALITY_NOT_SUPPORTED (1/45). It was answered PROPERTY / NOT_COV_PROPERTY (2/44), which is only in the SubscribeCOVProperty error table. SubscribeCOVProperty keeps 2/44. The same check runs for a proprietary object type, so a SubscribeCOV for one whose subscribed property is not subscribable now also answers 1/45 (cl. 13.14.1.3.1 and 135.1 9.10.2.1, from the review finding, unverified; wire-visible).
  • rb g-20261009-6 (#3704): Door_Alarm_State is captured at subscribe time only for an Access Door subscription to Present_Value (SubscribeCOV, or SubscribeCOVProperty on Present_Value) or to Value_Source; that capture is the single decision point, since a subscription with no captured Door_Alarm_State never reports a Door_Alarm_State change. A SubscribeCOVProperty on any other door property (Door_Status, Out_Of_Service, …) used to be notified when only Door_Alarm_State changed, although neither its property nor Status_Flags had changed; it no longer is (Table 13-1 / Table 13-1a, from the review finding, unverified; wire-visible). Present_Value and Value_Source subscriptions are still notified on a Door_Alarm_State change (both tested; the Value_Source helpers keep no Door_Alarm_State baseline of their own).
  • rb g-20261009-6 (#3790): no change on the wire. Alarm_Values of every object that arms CHANGE_OF_STATE (Multi-state Input / Value, Access Door, Access Zone, Timer) is a BACnetLIST in its property profile, so an array index is already answered PROPERTY_IS_NOT_AN_ARRAY before the algorithm ignores it; a test now pins that for indexes 0, 1 and past the end.
  • rb g-20261009-7 (#3756): A Color_Command that carries an out-of-range fade-time, ramp-rate or step-increment its operation does not use is now accepted and the field ignored (cl. 12.66.6, from the review finding, unverified); it was refused with Error PROPERTY / VALUE_OUT_OF_RANGE. Wire-visible: e.g. STEP_UP_CCT with fade-time 50, or STOP with ramp-rate 0, now succeeds. A field the operation uses is still refused out of range.
  • rb g-20261009-7 (#3745): An Authorization_Status with no error / error-source / authentication or authorization lists now decodes inside an enclosing wrapper numbered 1, 2, 4, 5, 6 or 7 (an RPM-ACK property-value [4], a BACnetPropertyValue value [2]); it failed, so a client could not read Authorization_Status by ReadPropertyMultiple. Wire-visible: a closing tag where error [1] / error-source [2] would start is now the wrapper’s end, not a malformed field, so such a frame decodes with the trailing bytes left to the enclosing decoder instead of being refused. Other decoders with the same look-ahead shape are tracked in #3843.
  • rb g-20261009-7 (#3736): A WriteProperty to a stack-held Notification Class property other than Recipient_List, Priority, Ack_Required and Notification_Class is no longer answered WRITE_ACCESS_DENIED by the stack. Host-visible: such a write (Description, …) now reaches the alarm-and-event block (an armed class’s own Event_Enable, Notify_Type, Event_Detection_Enable) and then the host’s set-property callback, where it used to be refused before either. Priority, Ack_Required and Notification_Class stay stack-owned and are still refused with WRITE_ACCESS_DENIED, armed or not (Notification_Class falling through would have bypassed the #2745 class-exists check). An armed class’s own intrinsic-reporting properties are now writable but read back empty - a pre-existing read-side gap, #3842.
  • rb g-20261009-8 (#3812): the packed-buffer Send paths (ReadProperty, ReadPropertyMultiple, WriteProperty, WritePropertyMultiple through SendReadProperty/SendWriteProperty, and SendSubscribeCOVPropertyMultiple) now refuse an element whose object type is above 1023 or instance above 4194303: the export returns false and nothing is sent. Before, the identifier was masked onto a different object (instance 0x400005 went out as instance 5, type 1026 as type 2) and the export returned true. Host-visible: a call that used to succeed with a wrong object now fails; wire-visible: no request for the masked object is sent. Not a frozen-header change (the #3059 WP-6 rule).
  • rb g-20261009-8 (#3754): Lighting Output FADE_TO, RAMP_TO and a Present_Value write under Transition FADE/RAMP start from the output (Tracking_Value, trims applied) as it stood before the priority-array write. Before, a command at priority 1 or 2 over a trimmed output started from the unclamped value, so the light jumped (e.g. 80 -> 100) before fading. In the relinquish direction (an exempt priority 1/2 relinquished so the trims apply again) the fade starts from that output clamped by the new winner’s trim state, so it runs linearly from High_End_Trim rather than holding flat at it. Wire-visible: Tracking_Value read right after and during such a command (Table 12-67 FADE_TO, cl. 12.54.19 - from the review finding, unverified).
  • rb g-20261009-9 (#3793): the #3708 range checks reach the remaining Event State / Notify Type / Event Type decodes. Wire-visible: a received AcknowledgeAlarm-Request with Event State Acknowledged above 65535 is answered with Reject PARAMETER_OUT_OF_RANGE instead of being acknowledged as the narrowed value. Host-visible: a GetEventInformation-ACK entry with Event State above 65535 or Notify Type above 2, a GetEnrollmentSummary-ACK entry with Event Type or Event State above 65535, a GetAlarmSummary-ACK entry with Alarm State above 65535, and an AcknowledgeAlarmInfo with Event State above 65535 now fail to decode instead of being reported as a truncated value. What the host observes for the client-side ACKs (as for #3708): one out-of-range entry fails the whole ComplexACK decode and the ACK is dropped - no Reject is sent and the host callback is not called - so the outstanding request runs to APDU timeout and retry, and the host sees a timeout, losing the valid entries in that ACK as well. Wire-visible: a received GetEnrollmentSummary-Request with an Event Type filter above 65535 is answered with Reject PARAMETER_OUT_OF_RANGE instead of filtering on the narrowed value.
  • rb g-20261009-9 (#3835): AccessZoneEnter / AccessZoneExit push a changed Occupancy_Count through the update pipeline, so a SubscribeCOVProperty subscriber or an Event Enrollment on Occupancy_Count is told of a counting entry or exit. Occupancy_State is now pushed only when it changed (it was re-pushed unchanged on every entry/exit); Last_Credential_Added / _Removed are pushed as before.
  • rb g-20261009-9 (#3834): a Life Safety Point or Zone the host serves, armed for CHANGE_OF_LIFE_SAFETY, now answers ReadProperty Alarm_Values and Life_Safety_Alarm_Values with the armed lists (an array index answers PROPERTY_IS_NOT_AN_ARRAY) and lists both in Property_List; they answered UNKNOWN_PROPERTY or reached the host callback. A stack-held Point/Zone is unchanged (it already served its own copy).
  • rb g-20261013-1 (#3768 CR3-F28-006): a BACnetSpecialEvent (an Exception_Schedule element) built with no period, or with a refused one, no longer reads valid because its event-priority was set last; it is valid only when its period is usable and its priority is in 1..16 (such an event already failed to encode; only IsValid() changed). The event’s validity no longer depends on setter order: a period set after the priority completes the event. A refused period leaves the event invalid until the next setter call; a refused calendar reference keeps the previous period, so a later priority set re-validates against that earlier period. Events built through the stack’s own paths (the Schedule’s host API, CreateFromCompactString, Decode) always set a valid period first and are unaffected. No wire or host API change.
  • rb g-20261013-3 (#3757 CR3-F17-010): FLOATING_LIMIT’s CONFIGURATION_ERROR check (cl. 12.12.21 (e), Loop cl. 12.17.8) now reads the stored Low_Diff_Limit / High_Diff_Limit (Error_Limit) directly instead of the float-rounded band edges, so a negative diff limit too small to move a large setpoint (-1 at a setpoint of 1e8), or one paired with a NaN setpoint, is reported as CONFIGURATION_ERROR; before, it went undetected.
  • rb g-20261013-3 (#3757 CR3-F17-020): CHANGE_OF_DISCRETE_VALUE’s internal GetDatatype() answers the datatype of the last monitored value (#2725 monitors ten datatypes) instead of always OCTET STRING; OCTET STRING until a value has been monitored.
  • rb g-20261013-5 (#3717 CR3-F15-024): SetConfirmedCOVNotificationRequest / SetUnconfirmedCOVNotificationRequest (and the base SetCOVNotificationRequest) check the initiating device and monitored object identifiers before changing anything. A refused value now leaves the request and its validity as they were, instead of half-updating it and marking it invalid. Internal setters only; nothing on the wire changes.
  • rb g-20261013-6 (#3905): SubscribeCOVPropertyMultiple no longer refuses a valid array index on an array served as encoded bytes, such as the Device’s Object_List. The length check counted the objects a whole-array read held, which is none for such an array, so every index of 1 or more was refused with First Failed PROPERTY / INVALID_ARRAY_INDEX. It now takes the length from the array’s element 0, as ReadProperty does. On the wire, Object_List[1] of a three-entry list now gets SimpleACK; index 4 is still refused with INVALID_ARRAY_INDEX. Two other effects a host can see. First, an out-of-range index that the old check let through when its whole-array read failed is now refused with INVALID_ARRAY_INDEX; this happens when a host declines one element, or when a Priority_Array has no Boolean Get callback. Second, each indexed subscription now reads only the array length, so the host’s Get callbacks are called fewer times (no longer once per element). If the length read itself fails, the index is left unchecked, as before.
  • rb g-20261013-6 (#3826 CR3-F04-005): ChipkinCircularBuffer’s sized constructor is explicit and sizes through Resize, so a size the allocation cannot satisfy leaves an empty buffer of max size 0 instead of throwing. Only tests use that constructor; no behaviour change for a host.
  • rb g-20261013-7 (#3681 CR3-F09-001): a refused BACnet/SC frame no longer leaves half-decoded fields in the decoder object. The BVLL header decode and the BVLC-Result, Proprietary-Message and Address-Resolution-ACK decoders reset the object on every refusal, as Advertisement, Connect-Request and Connect-Accept already did. No change on the wire: a refused frame’s object is discarded, and the decode-failure NAK reads the Message ID from the raw frame.
  • rb g-20261013-7 (#3681 CR3-F09-018): BACnetSCHubFunctionConnection’s data members are private, so the 6-octet peer VMAC and 16-octet peer UUID checks in its setters cannot be bypassed. No behaviour change for a host.
  • rb g-20261013-8 (#3678 CR3-F06-027): a refused NPDU decode or encode records a BACnet abort reason (buffer-overflow 1, or other 0) in the error container, instead of the error codes 51 and 59 that no abort-reason table defines. No change on the wire: a frame whose NPDU does not decode is not answered.
  • rb g-20261013-8 (#3678 CR3-F06-018): BACnetPortPermission::Decode resets the object before refusing a NULL or empty buffer, so a reused object no longer stays valid with its previous value. No change on the wire.
  • rb g-20261013-8 (#3678 CR3-F06-011): an empty-body decode of a raw network layer message replaces a reused object’s previous payload. No change on the wire.
  • rb g-20261013-8 (#3678 CR3-F06-012): BACnetWhoIsRouterToNetwork::Decode accepts an empty body as the no-DNET form instead of refusing it. No change on the wire: the NPDU decoder already handled the empty form before calling it.
  • rb g-20261013-8 (#3677 CR3-F05-008): a Confirmed-Request refused for a segmented header that is too short, or for a proposed window size outside 1-127, records its Reject reason on the PDU, so BACnetConfirmedRequestProcessor answers it with that Reject rather than Abort(other). No change on the wire for a live device: the data link already answered these with the Reject.
  • rb g-20261013-8 (#3676 CR3-F03-037): a SEQUENCE OF BACnetAuditNotification is valid only when every notification in it is. No change on the wire: an invalid notification already failed to encode.
  • rb g-20261013-9 (#3712 CR3-F10-003): the object-type overload of BACnetReadResult::Decode frees a property value that failed to decode, as the other overload already did, so a failed decode no longer leaves a value on an invalid object. No change on the wire.
  • rb g-20261013-9 (#3712 CR3-F10-008): BACnetReadRangeRequest::GetMinimumMemoryCount counts the By Position and By Sequence Number opening and closing tags, so it equals the encoded length for all three range forms. No change on the wire.
  • rb g-20261013-9 (#3713 CR3-F11-013): BACnetWriteAccessSpecification::Clone keeps the original’s validity, so a copy of a valid decoded specification with an empty list of properties is valid too. No change on the wire.
  • rb g2-20261009-1 (#3850): a build without BACNET_STACK_TESTTOOL links again. #3807 (PR #3833) made the always-compiled date/time getter path call RecordTypedGetDecline, which was defined only inside the test-tool block, so every customer build failed with LNK2019 (unresolved external RecordTypedGetDecline). The definition now sits outside the test-tool block. Revert-red: a non-test-tool MSVC compile of BACnetBusinessLogic.cpp left the symbol UNDEF in the object; with the fix it is defined there in both configurations.
  • rb g2-20261012-2 (#3831, security): the #3775 property-reference budget of a Group List_Of_Group_Members value is counted per request, not per value: the values of one WritePropertyMultiple, or one CreateObject’s initial values, share it (4096 on FULL, 128 on EMBEDDED), so splitting references over many values no longer multiplies the bound. Over it, the request is answered Abort BUFFER_OVERFLOW. A reply (ReadPropertyMultiple-ACK) keeps a per-value budget pending the owner’s ruling (#3829). New defence-in-depth bounds, below the reassembly limit in practice: at most 32500 elements in one value of any datatype, including Any, unknown-datatype and untyped values, and at most 16250 ReadResults across a message’s typed ReadAccessResult values. A CreateObject initial value that a bound refuses is answered with that Abort instead of being decoded again by its tags.
  • rb g2-20261012-2 (#3486 item 1): a REQUIRED BACnetTimeStamp or BACnetDateTime property that the host does not serve and that has no stack-held value reads as its “no value yet” form, an unspecified date and time (X’FF’ in every octet), instead of Abort(other). This covers Event_Time_Stamps (three entries), Load Control Start_Time, Access Credential Activation_Time / Expiration_Time and Pulse Converter Update_Time / Count_Change_Time.
  • rb g2-20261012-2 (#3486 items 3, 5, 6): the IPv4 data link refuses to send with a connection string that is not 6 octets, unless it is a broadcast with no connection string; this affects every BACnetStack_Send* export. The test-tool SendIHave refuses a non-UTF-8 object name, and SendAuthRequest a scope above 24 bits. IFC-091.
  • rb g2-20261012-2 (#3824): BACnetStack_SendNetworkNumberIs refuses a unicast or a destination network (cl. 6.4.20: a local broadcast, never routed); it used to send a DNET. Host-named refusals of AddListElement / RemoveListElement keep the host’s error code. Relinquish_Default / Feedback_Value writes are range-checked: 0..1 for binary objects, 1..Number_Of_States for multi-state objects. A failed Lift getter reports an error instead of aborting. Alarm evaluation no longer skips COMMAND_FAILURE when another property read is refused. The DateTime getter forwards the array index. SendExternalWriteProperty checks its value copy. DateTime Pattern Value builds alone. IFC-091.
  • rb g2-20261012-2 (#3677): an Error whose class/code are context-tagged is refused, and an Error’s choice survives Decode. ConfirmedRequestPDU copies refuse a lost payload, and its two payload forms are exclusive. ComplexAck SetServiceAck keeps the held ack until the copy succeeds. ArmTimeout no longer wraps near UINT32_MAX. A SegmentACK ends a run of local send failures, and an empty retransmit window counts as a failed send. An empty RemoveListElement is not audited.
  • rb g2-20261012-2 (#3712): ReadProperty-family decoders (ReadResult, ReadRange request/ACK, ReadAccessSpecification/Result, RPM request/ACK) are invalid after refusing an empty buffer. A ReadPropertyMultiple with no parameters is Reject MISSING_REQUIRED_PARAMETER. A ListOfResults tag failure is Reject INVALID_TAG and leaves the list empty.
  • rb g2-20261012-2 (#3713): a remote WriteGroup lighting command is freed when its send fails, and is sent only to Lighting_Command members. WriteGroup decoders reset before refusing an empty buffer. A partially written Priority_Array encodes its unwritten slots as Null. WriteProperty async ToBytes refuses a length above 65535. A failed WritePropertyMultiple-Error allocation answers Abort OUT_OF_RESOURCES.
  • rb g2-20261012-2 (#3714): an Add/RemoveListElement copy or Clone() that loses an element is invalid / NULL. CreateObject keeps the list decoder’s error reason.
  • rb g2-20261012-2 (#3716): BACnetFileRecordAccessData is invalid after a partial decode failure.
  • rb g2-20261012-3 (#3680): an MS/TP node that receives its own transmissions now ignores their echo (cl. 9.5.4). PASS_TOKEN no longer treats the echo of its own Token as “saw token user”. The echoed octets no longer reset the silence timer or count as events. On a line that does not echo, at most four header octets of another station’s frame are held and then replayed. The SendByte / TX-enable contract (cl. 9.5.5.1 steps 10-11) is documented.
  • rb g2-20261012-3 (#3871): MS/TP (the base data link) refuses a connection string that is not the 1-octet MAC, unless it is a broadcast with none. An all-NULL segmented-request retransmit window counts as a failed send. The routed IPv4 broadcast with no connection string is pinned by a test.
  • rb g2-20261012-3 (#3764): an Access Rule is consistent only once both specifiers are set. A Credential Data Input refuses Supported_Format_Classes of a size other than Supported_Formats (IFC-092), and its Present_Value Format-Class follows the current classes. Authorization_Mode NONE no longer consumes Uses_Remaining or Days_Remaining (cl. 12.31.14, 12.35.15).
  • rb g2-20261012-3 (#3762): Event Log records BUFFER_PURGED once when a purge meets a two-slot stop-when-full. Log record copies and clones are invalid / NULL on a failed nested copy. A Log Multiple record refuses an invalid timestamp. The minimum-memory counts of log data and event-log records match their encodings. Switching an Event Log datum away from a notification drops it.
  • rb g2-20261012-3 (#3760): NotificationParameters minimum-memory counts include the CHOICE tags. A refused arm setter re-validates. Reliability and life-safety state / mode above 65535 are refused on decode (cl. 21.6). Decode failures set a reason. Extended copies with an invalid clone are not valid. An extended parameter refuses a context-tagged primitive.
  • rb g2-20261012-3 (#3758): FAULT_LIFE_SAFETY refuses more than 64 fault values instead of truncating them. FAULT_LISTED treats a reordered list of enumerated faults as unchanged. FaultParameter minimum-memory counts include the wrapper tags. FaultExtended running out of space at its parameter tags is SEGMENTATION_NOT_SUPPORTED. An invalid fault value is refused on insert, and so is a refused reference setter (the held reference is kept).
  • rb g2-20261012-3 (#3768): Exception_Schedule and Weekly_Schedule writes no longer lose time values when a copy fails. EvaluateWeekly keeps Present_Value when its copy fails. Calendar entry, special event and daily schedule decoders reset before refusing an empty buffer. Unspecified hundredths compare as .00.
  • rb g2-20261012-3 (#3759): a refused event-parameter setter keeps the held value (DiscreteValue, Bitstring, LifeSafety, AccessEvent, CommandFailure, FloatingLimit). An extended parameter refuses a context-tagged primitive.
  • rb g2-20261012-3 (#3769): the Load Control shed deadline is computed in 64 bits, so it does not overflow in 2038 on a 32-bit time_t. A Network Port Health default timestamp is unspecified (cl. 12.56.17). The Program object refuses a Description_Of_Halt or Program_Location that does not fit. A Prescale built field by field is valid. An Averaging object with an unusable reference counts a missed sample each period.
  • rb g2-20261012-4 (#3676): primitive, Optional*, abstract-syntax and Device*Reference decodes reset a reused object before refusing a NULL or empty buffer. BOOL and NULL encodes report an extended tag that does not fit as SEGMENTATION_NOT_SUPPORTED, and an invalid NULL is refused. A bit string longer than 65535 octets (524280 bits) is refused. A tag that does not decode in PropertyStates or an abstract-syntax value is Reject INVALID_TAG, not an unexplained Abort OTHER. OptionalAny keeps its validity across a failed copy or a refused setter. Property and object-property reference encodes keep a child’s abort reason. A failed allocation in the list and property-value decoders is Abort OUT_OF_RESOURCES. An absent optional array index or device identifier no longer compares equal to a present one.
  • rb g2-20261012-4 (#3678): a relayed Reject-Message-To-Network keeps its network priority and, on an SC-to-SC hop, its data options. Network layer messages any peer can send log at Info, and a What-Is-Network-Number or Network-Number-Is with no matching Network Port is discarded (cl. 6.4.19). An Initialize-Routing-Table answer that leaves entries out logs which. A failed vendor-proprietary decode leaves a reused object unchanged. Adding a port to a data-less Initialize-Routing-Table-Ack switches it back to the table form. Empty-form network messages encode into a zero-length buffer. A router port is refused over a network that has an operator route. Adding a route or port at run time releases messages held for that network. The BVLL Original-Unicast/Broadcast encoders report their refusals, and the BVLL NPDU decoders reset a reused object first.
  • rb g2-20261012-4 (#3679): Read-BDT and Read-FDT require BVLC Length 4 (J.2.3.1, J.2.7.1). A BDT or FDT list that is not a whole number of entries is refused. BVLL encoders and wrapper accessors report a refusal. The foreign-device first-registration I-Am comes from the device the Network Port serves. A failed send to one BBMD peer no longer stops local processing or the rest of the fan-out. Hex dumps render two digits per octet. The stray-segment Abort sent when segments are not accepted is rate-limited per source. Packet clones return NULL on allocation failure. The 5-argument IPv4 Network Port constructor initialises its mode.
  • rb g2-20261012-4 (#3761): a written Notification Class Recipient_List holds at most 32 entries. A Destination with a Transitions bitstring shorter than 3 bits is rejected. Event Enrollment Setup refuses a Notify_Type other than ALARM or EVENT and clears the replaced reference’s fault. An extended parameter refuses a context-tagged value.
  • rb g2-20261012-4 (#3765): an input to a Life Safety Point while Out_Of_Service no longer changes Tracking_Value (cl. 12.15.11).
  • rb g2-20261012-4 (#3766): a Landing Door floor above 255 is Error VALUE_OUT_OF_RANGE, not Reject. SetFloorText with NULL text and a length is refused.
  • rb g2-20261012-4 (#3767): a Lighting Output write that halts a pending egress compares against the value before the halt. A trim fade ends only when Present_Value or the winning priority changes.
  • rb g2-20261012-4 (#3826): numeric ToString/TryParse use the classic locale. An octet string of 65536 octets or more renders in full.
  • rb g2-20261012-4 (#3763): a disabled Audit Reporter does not drain its queue. Dropped audit notifications are reported as AUDITING_FAILURE (cl. 19.6.6). A ConfirmedAuditNotification the processor never handled is aborted, not acknowledged. AuditLogQuery matches a device identifier by object type as well as instance, and its ACK refuses a non-Audit-Log identifier. A token issued under a time-limited policy entry carries its validity range. The any-method authentication constraint is refused (cl. 17.4.7). An AuthRequest audience must be application-tagged. AuthenticationFactorType and AuthenticationDecision values outside their closed ranges are refused on decode. An opening tag where an optional field belongs is rejected. A failed deep copy of an authorization policy or event invalidates the copy.
  • rb g2-20261012-5 (#3676): an application tag with length/value/type 6 or 7 is refused (cl. 20.2.1.3). Octets that are not a tag where an optional constructed field may start are Reject INVALID_TAG, not an absent field. A top-level closing tag no longer counts as a choice element. An unsigned 64 with an unextended length above 4 is refused. A refused REAL, Double, Date or Time decode no longer keeps the wire tag. Audit-level and audit-operation property states decode in any alarms-and-events build. The SC connection-state property-state arms’ minimum memory matches their encoding. A character-set octet is checked before it is converted. An IBM/Microsoft DBCS string’s code page is not counted as a character. An unknown-datatype value refuses a mismatched closing tag. DecodeAs output shows a nested constructed value’s tag number (a new context attribute). A refused re-set of a device object (property) reference keeps the previous value.
  • rb g2-20261012-5 (#3825): a per-instance SetPropertyEnabled(false) reports failure while the object-type tier still enables the property. A Notification Forwarder retries a refused DAB Acquire once the table has room. A failed Averaging re-provision keeps the previous reference and samples. Changing a Life Safety Zone’s members re-settles a zone that contains it. AddTimerObject refuses min > max or a Default_Timeout outside [min, max] (cl. 12.57.16; IFC-094). Disarming alarms and events puts Reliability back as it was before arming. A refused CHANGE_OF_BITSTRING setup attaches no algorithm. Re-enabling backup and restore keeps Last_Restore_Time and a running procedure. A failed allocation while building Object_List, Property_List or an array property is Abort OUT_OF_RESOURCES, not a short array. A host UDP port above 65535 is refused for FD_BBMD_Address and MAC_Address. A refused non-device Object_Identifier rename answers Error VALUE_OUT_OF_RANGE. Backup_Failure_Timeout and Max_Proxied_I_Ams_Per_Second are writable when present. An object identifier above 4194303 is refused when a Lift or Elevator Group stores it. A repeat AddNotificationClassObject no longer re-applies Priority and Ack_Required.
  • rb g2-20261012-5 (#3677): an unconfirmed service choice above the enumeration reads as unknown rather than being converted. PDU clones return NULL on allocation failure. A Who-Is whose range limits do not fit is refused instead of going out as a global Who-Is. A refused MessageClass decode leaves a reused object invalid. AWAIT_RESPONSE answers a client Segment-ACK with Abort and a duplicate segment with a Segment-ACK (cl. 5.4.5.3). The server-side IDLE stray-PDU Aborts share the per-source limiter, which is now keyed per ingress port. The Max_Segments_Accepted guard counts received segments, so it holds past segment 255.
  • rb g2-20261012-5 (#3824): a protocol revision below 24 no longer names revision-24 property identifiers in the property-identifier tables (a full build below revision 24 still does not compile; owner question). The unconfirmed event-values senders no longer leak a packet when the timestamp is refused, and the plain event senders report an invalid timestamp. A zero-length MAC is treated as a broadcast recipient (cl. 21.6). StringToDate refuses characters between the day and the “(”. APIDebugLog format strings are checked by GCC and Clang; 40 size_t arguments are cast. The Alert Enrollment test-tool exports refuse an instance above 4194303, and AddBinaryLightingOutputObject refuses a Polarity other than NORMAL or REVERSE (IFC-094).
  • rb g2-20261012-5 (#3712): four debug-log calls no longer pass size_t to %u.
  • rb g2-20261012-5 (#3760): the CommandFailure and ChangeOfDiscreteValue notification-parameter setters take their values, so they no longer build a wrapper that can never encode. CommandFailure equality compares feedback-value when both command values are NULL.
  • rb g2-20261012-5 (#3762): an Enable TRUE written outside Start_Time/Stop_Time keeps LOG_DISABLED and records nothing (cl. 12.25.14, 12.30.19, 12.27.13).
  • rb g2-20261012-6 (#3717): a COV subscription renewal that fails leaves its lifetime and notification form unchanged, and a new subscription that cannot be filled is not stored (RESOURCES / OUT_OF_MEMORY). The door-alarm and COV-multiple reference setters clone before replacing. A SubscribeCOVProperty on Status_Flags lists it once. The COV-multiple subscription, first-failed-subscription, property-multiple error, COV value and COV notification decodes reset a reused object first. A cov-increment that is absent no longer equals one that is present, and re-setting a reference without one clears it. Issue-confirmed-notifications is mandatory in the SubscribeCOVPropertyMultiple request class. A COV-multiple subscription is valid only with its mandatory fields. A COV Notification Multiple with an empty inner List of Values is rejected (cl. 13.17.3.1.2). An initial COV-multiple notification that could not be built is skipped, not queued invalid.
  • rb g2-20261012-6 (#3718): the AcknowledgeAlarm, alarm/enrollment/event summary, GetEnrollmentSummaryACK and EventNotificationSubscription decodes reset a reused object first. LifeSafetyOperationInfo::SetComplete refuses unless both fields are valid. Every compact-string recipient field is checked against its own width.
  • rb g2-20261012-6 (#3757): Event_Algorithm_Inhibit set while NORMAL discards a running time delay. A TO_FAULT transition no longer logs PANIC in the CHANGE_OF_LIFE_SAFETY, CHANGE_OF_TIMER, COMMAND_FAILURE, CHANGE_OF_BITSTRING, CHANGE_OF_CHARACTERSTRING, CHANGE_OF_STATE and CHANGE_OF_STATUS_FLAGS algorithms. An abandoned time delay no longer leaves the algorithm pending. CHANGE_OF_STATE skips alarm values of another choice and reports a run error only when none matches the monitored value’s choice.
  • rb g2-20261012-6 (#3715): the UTC offset and DST step is computed in 64 bits and refused when it does not fit time_t. DeviceCommunicationControl refuses an empty password and keeps a long one when copied. The I-Am, I-Have, Who-Am-I and You-Are decodes reset first, and You-Are enforces its SEQUENCE order. The DAB heartbeat survives a clock step back and a clock reading of 0. The PrivateTransfer messages report a failed copy of their abstract-syntax field.
  • rb g2-20261012-6 (#3772): a test helper that calls SetForwardedApdu is guarded by STACK_OPTION_ROUTER_PHYSICAL, so the test build without physical routing compiles.
  • rb g2-20261012-6 (#3875): #2872’s “fits once encoded” ReadRange path has a test again, on an Event Log fixture whose size estimate still overshoots.
  • rb g2-20261012-6 (#3681): BACnet/SC. A peer’s BVLC-Result Error Details is stored as well-formed UTF-8 (AB.2.4.1). The header-option setter refuses option type 0, and a received list holds at most 4192 header options. A second Connect-Request on an already accepted socket is discarded and changes nothing (AB.6.2.3). Direct-connect message IDs are never 0. The initiating role discards every frame but Connect-Accept and BVLC-Result until the connection is accepted. Only the connected hub socket restarts the heartbeat. A close while DISCONNECTING drops the pending Disconnect-Request. A hub URI without a well-formed host and port is refused before anything changes (AB.5.4), and a new hub URI starts without the old backoff. Close statuses in 1000-4999 that AB.7.5.5’s table does not list map to WEBSOCKET_ERROR. A refused Connect-Request send fails the attempt, and a host with no initiate callback leaves the connector still. A Hello-capability change closes every peer through DisconnectAllConnections. A local broadcast through the hub function’s accept URI is relayed to the peers only. Only a final 12-hex-digit suffix after the last ‘;’ of a connection string is taken as a VMAC.
  • rb g2-20261013-1 (#3089): SetObjectTypeSupported accepts a proprietary object type, so the Supported flag that SetObjectTypeCreatable sets can be cleared again (the reserved gap is still refused). A refused first AddBinaryLightingOutputObject leaves no engine behind. Lighting and Binary Lighting Output properties served by the stack-held object (Relinquish_Default, Blink_Warn_Enable, Egress_Time, Polarity) are written to it. An Audit Log’s Member_Of is a single BACnetDeviceObjectReference, not a list (cl. 12.64.13).
  • rb g2-20261013-1 (#2851): while Enable is FALSE, a Load Control write of Requested_Shed_Level, Start_Time, Shed_Duration or Duty_Window is checked but not stored (cl. 12.28.14). A Start_Time that is neither a point in time nor the unspecified datetime is refused with VALUE_OUT_OF_RANGE. A non-zero LEVEL that no Shed_Levels entry can serve is refused instead of being taken as a cancel.
  • rb g2-20261013-1 (#3088): the main (router) device cannot be added to a virtual network. A NULL next-hop address with a non-zero length is refused when adding a router route. IsObjectTypeCompiledAndEnabled covers the Staging, Audit Log, Audit Reporter, Color and Color Temperature types, and an Audit Reporter rename keeps its engine. One InsertStackHeldPriorityArray serves the stack-held priority array reads and reports a failed slot copy as a failed read. SendExternalWriteProperty refuses an integer that does not fit its datatype, and NaN, and sends nothing. A Multi-state write whose Number_Of_States cannot be read answers VALUE_OUT_OF_RANGE. STACK_OPTION_MAX_SUBSCRIPTION_PER_OBJECT (0..65535) and STACK_OPTION_MAX_CURRENT_STATES (0..255) fail the build with #error instead of narrowing silently. Dead code and stale doc comments are corrected (AuthRequest K.9.1, BVLL J.2.2 / J.2.9, Shed_Levels 12.28.18; IFC-095).
  • rb g2-20261013-1 (#3647): with the physical router enabled, a network layer message whose arrival port cannot be identified is discarded unless its DNET is local, a gateway virtual network, or one the physical router does not own.
  • rb g2-20261013-1 (#3277): the Notification Forwarder does not forward a received notification to the subnet-directed broadcast of a known IPv4 subnet (loop-prevention rule (a), cl. 12.51).
  • rb g2-20261013-1 (#2694): Notification Forwarder loop-prevention rules (a) and (d) recognise the arrival port’s own Network_Number, not only network 0, including a directed broadcast written with that number (CR2-F16-018).
  • rb g2-20261013-2 (#3878): an Initialize-Routing-Table query whose answer does not fit one message is answered with several Initialize-Routing-Table-Acks, each a whole Ack carrying a portion of the table (cl. 6.6.3.9), instead of being cut off at the first Ack. A message carries at most 255 entries on BACnet/IP and BACnet/SC and 121 on MS/TP; an answer routed to a remote requester uses the MS/TP size, since a router on the way may put it on MS/TP. A virtual router sends the continuation Acks too, because the network layer is given its data link layer in every router build.
  • rb g2-20261013-2 (#3277): the Notification Forwarder checks a subnet-directed broadcast target against the subnet of the arrival port’s own BACnet/IP link when that link is known, so on two IPv4 ports with overlapping subnets a host address is no longer dropped as another port’s broadcast (loop-prevention rule (a), cl. 12.51).
  • rb g2-20261013-2 (#3884): the SubscribeCOVPropertyMultiple initial notification records its baselines only after it was sent, so after a failed send the next evaluation reports the values again (CR3-F15-015). A global-broadcast I-Am or I-Have delivers HookIAm / HookIHave once per frame, not once per local device (CR3-F13-015). A removed BACnet/SC Network Port is no longer reported missing on every Loop (CR3-F09-050), and a hub connection attempt already in progress still times out when CallbackInitiateWebsocket is unregistered (CR3-F09-042).
  • rb g2-20261013-2 (#3879): the Audit Reporter follows its ConfirmedAuditNotification requests to the end of their transactions: a SimpleACK sets Reliability to NO_FAULT_DETECTED, and a timeout, Error, Reject or Abort sets COMMUNICATION_FAILURE (cl. 12.63.6). Sending a confirmed notification no longer clears a fault; unconfirmed ones still do. The pending and outcome lists are each bounded at 256.
  • rb g2-20261013-2 (#3796): a virtual-router child reports a local requester in its audit notifications on the physical network the request arrived from, not network 0, when the requester is not bound; a bound requester is still reported by device identifier.
  • rb g2-20261013-2 (#3871): an OPERATIONAL_PROBLEM error code the host names is sent as error class DEVICE (cl. 18.1), not PROPERTY.
  • rb g2-20261013-2 (#3088): STACK_OPTION_PROFILE_B_BBMD without STACK_OPTION_DATA_LINK_LAYER_IPV4 fails the build with #error. Every option the ENABLE_ALL_BIBBS, cascade and ENABLE_ALL_OBJECT_TYPES blocks turn on is #ifndef-guarded, so a customer -DSTACK_OPTION_X is not redefined. BACnetStackConstants.h includes the options header itself. A static_assert fails the build if the auto-numbered BACnetStackDatatypes would reach Proprietary. DEFAULT_TREND_LOG_TOTAL_RECORD_COUNT is a uint32_t. Six unreferenced macros are removed from BACnetStackCommon.h, and stale clause citations and comments are corrected.
  • rb g2-20261013-2 (#3859): the SC direct-connect status entry’s optional constructed fields (peer-address, error) do not take an enclosing wrapper’s closing tag as their own, so a NOT_CONNECTED entry inside a ReadPropertyMultiple-ACK [4] is no longer rejected. Triage of the 97 optional-field look-ahead sites found the other 96 safe.
  • rb g2-20261013-3 (#3888): on a virtual-only gateway the Initialize-Routing-Table answer numbers Port IDs 1..255 in order, and the virtual networks after the 255th share Port ID 255, instead of the 256th wrapping to Port ID 0. A Port ID of zero means “purge the entries for that DNET” (cl. 6.4.7), so a requester replaying the table would have deleted that network’s route.
  • rb g2-20261013-3 (inherited gate fix): tests/TestBACnetNotificationParametersCR3F20.cpp called BACnetObjectIdentifier32::TryMake below the #1592 boundary, so check-object-identifier-boundary.sh failed. It now builds the identifier with FromRaw(GenerateObjectIdentifier(0, 1)). Test-only.
  • rb g2-20261013-4 (#3891): a DeviceCommunicationControl disable with a Time Duration now ends on time: BACnetInterface::Tick checks every device, where before the disable ended only when the device next sent or received something. The four copies of the expiry check are one routine, BACnetDataLink::ExpireDeviceCommunicationControl.
  • rb g2-20261013-5 (#3894): BACnetStack_SetAuthorizationPolicyEntry refuses a scope longer than a bit string (more than 524280 bits) and leaves the entry unchanged, instead of storing an empty scope and returning true (IFC-099).
  • rb planner-20261014-docs-2 (#3914): The API Reference in the release package (docs/doxygen/) did not document any exported function. CASBACnetStackDLL.h had no file page, so none of the 245 BACnetStack_* functions appeared, and the output consisted of more than a thousand pages of internal classes (#3913). The reference now covers the Customer API only: a new main page, every exported function with its one-line summary, and every callback signature in BACnetCallbackInterface.h that has a doc block. Summaries are no longer cut off at “cl.” or “e.g.”, signatures no longer show the DllExport macro, and internal classes and the test-tool interface are no longer included. The headers themselves are unchanged. Revert-red: without the fix, generate_doxygen.sh stopped with “the API Reference has no page for CASBACnetStackDLL.h”.
  • rb planner-20261014-docs-4 (#3925 item 4): The BACnet/SC Manual (docs/CAS BACnet Stack - BACnet SC Manual_v6.md) said a build without BACnet/SC leaves the SC functions out of the DLL and does not define secure-connect (11). Both are wrong. The BACnetStack_*BACnetSC*, BACnetStack_SCCertificate* and BACnetStack_RegisterCallbackSC* exports are present in every build and return false (or do nothing) with a “not compiled” debug message, and the secure-connect value is always defined. The manual now says so.
  • rb planner-20261015-docs-5 (#3924): The BACnetStack_SetServiceEnabled documentation gave the wrong number for seven services: it listed their service-choice numbers instead of the BACnetServicesSupported bit numbers the function takes. A caller who followed it enabled a different service. The corrected numbers are 38 = SubscribeCOVProperty, 6 = AtomicReadFile, 17 = DeviceCommunicationControl, 20 = ReinitializeDevice, 35 = ReadRange, 39 = GetEventInformation and 2 = ConfirmedEventNotification. Documentation only.
  • rb planner-20261015-docs-5 (#3925): Header documentation that described the wrong behaviour is corrected. Documentation only.
    • The FPCallbackGetProperty* docs said a declined read with errorCode unset always answers readAccessDenied. A named code is sent; otherwise the stored value or default is used, except for LIST properties and the properties the stack will not make up (PresentValue and the rest), which answer readAccessDenied.
    • The API conventions now limit the code-to-error-class rule to the property callbacks; the service callbacks map codes as their own docs state.
    • RegisterCallbackCompleteRestore said a restore abandoned by the Backup_Failure_Timeout arrives with wasAborted false, and that the return value is ignored. In fact wasAborted is true on that path, and Last_Restore_Time is recorded only when the callback returns true.
    • RegisterCallbackGetListOfEnumerations said a false return produces Abort(other). It produces READ_ACCESS_DENIED.
    • RegisterCallbackGetPropertyObjectIdentifier said Get callbacks have no errorCode.
    • AddObject now states the PriorityArray NULL-slot contract it used to point elsewhere for.
  • rb planner-20261015-docs-5 (#3917): The API Reference builds with no Doxygen warnings (479 when #3913 was filed).
    • 34 callback registrars documented a parameter name the signature does not use.
    • 12 callback signatures documented 13 parameters they do not have.
    • The four backup and restore callback types had no documentation and were missing from the reference.
    • Links to types that do not exist are corrected, and placeholder text such as <acceptUri> is no longer read as HTML.
    • ci_scripts/doc-lint.sh now also checks callback registrars and rejects a \param that names no parameter; without these header fixes it fails on 36 customer exports and 1 test-tool export.
    • ci_scripts/generate_doxygen.sh now also fails the release if a customer callback type is missing from the reference.
  • rb planner-20261015-docs-9 (#3913): Fixes from the review of the generated API Reference.
    • Rendering. Byte-layout diagrams and tables render as written. Lines no longer go missing. No Doxygen command is printed as text.
    • Build-option notes. Each note names only the options that turn the function on. The COV functions no longer list 32 options.
    • Callbacks.
      • 13 callback types now say what the stack does when they return true or false. This includes which error codes it sends and when it answers with an Abort.
      • Eight callback summaries now use the current type names.
    • Summaries and links.
      • Seven functions have a real one-line summary.
      • The SubscribeCOVPropertyMultiple clause reference is corrected to 13.16.
      • Function names in the text link to their pages.
    • Enumeration pages.
      • An enumeration is no longer linked to a parameter that only mentions it in passing. The AtomicReadFile/AtomicWriteFile fileAccessMethod, whose numbering is the inverse of BACnetFileAccessMethod, is no longer linked to it.
      • Test-tool callback types are no longer listed.
      • The 18 error codes withdrawn from BACnet are marked with the protocol revision that removed them.
    • Removed text. Internal file paths, test names and maintainer notes were taken out of the customer text.
    • Debug logging. The guides no longer say STACK_OPTION_API_DEBUG_LOGGING is off by default. The prebuilt libraries, the npm package and the default build settings all turn it on.
    • Documentation only; no declaration changed.
  • rb planner-20261015-docs-9: Tooling for the API Reference.
    • The Doxygen input filter recognises “Cl.” at the start of a sentence and leaves \verbatim blocks alone.
    • doc-lint.sh no longer counts an \ingroup from the block before an export. Its self-test now also covers:
      • an undefined group;
      • a second \ingroup;
      • an owner-ruling note;
      • the rules the customer header gets by default.
    • The release build of the reference now fails when:
      • a function is missing from its topic page;
      • the callback page lists a type no export registers, or misses one that it does;
      • a Doxygen command is printed as text.
  • rb sm-20260929-2 (#3295): an Event Enrollment created with AddEventEnrollmentObjectWithChangeOfStateAlgorithm (test-tool BACnetStackTestTool_AddEventEnrollmentObjectWithChangeOfStateAlgorithm, Node AddEventEnrollmentObjectWithChangeOfStateAlgorithm) to monitor a Binary Input or Binary Value Present_Value never left NORMAL. The alarm value was always armed as a Boolean property state, but a Present_Value of those types is a BACnetBinaryPV (cl. 12.6.4, 12.8.4), which the Event Enrollment evaluates as a binary-value property state, and CHANGE_OF_STATE compares like datatypes only (cl. 13.3.2). The alarm value is now armed as binary-value (true = ACTIVE, false = INACTIVE) for those properties, as the intrinsic algorithm has done since #1837; every other monitored property keeps the Boolean alarm value (IFC-067). A Binary Output or Multi-state Present_Value is not covered (#3298).
  • rb v-20260924-1 (#2582): a BACnet/SC hub function rejecting a Connect-Request for a duplicate VMAC now sends its BVLC-Result NAK with Error Class COMMUNICATION, as 135-2024 AB.6.2.3 requires (“an ‘Error Class’ of COMMUNICATION and an ‘Error Code’ of NODE_DUPLICATE_VMAC”). Before, it sent RESOURCES. A rejection because the peer table is full keeps RESOURCES / NO_SPACE_TO_ADD_LIST_ELEMENT.
  • rb v-20260924-1 (#2591, security): the hub function now discards every frame except Connect-Request from a connection that has not completed Connect-Request (AB.6.2.3 AWAITING_REQUEST). Before, such a connection could broadcast or unicast Encapsulated-NPDUs under any Originating-Virtual-Address, bypassing the #344 check, and have Heartbeat and Advertisement-Solicitation answered. After a Connect-Request NAK the hub also closes that websocket and forgets any entry the connection held (“close the WebSocket connection and enter the IDLE state”). Not changed: the hub function still has no connect wait timer, tracked as #2618.
  • rb v-20260924-1 (#2583): the hub function now forwards a BVLC-Result, Advertisement, Advertisement-Solicitation or Proprietary-Message whose Destination Virtual Address names another connected node, as AB.5.3.2 requires (“the BVLC message shall be sent out through the hub connection with the connection peer node VMAC matching the destination VMAC”). Before, it dropped them, and it answered a solicitation meant for another node with its own Advertisement. A broadcast Proprietary-Message, the only one of the four AB.2.16 allows to be broadcast, is distributed to every other hub connection (AB.5.3.3). These forwards get the same Originating-Virtual-Address check as #344 applies to Encapsulated-NPDU. The Destination Virtual Address is still kept on a unicast forward, tracked as #2584.
  • rb v-20260924-2 (#2574): a BACnet/SC hub connector’s BVLC-Result NAK to a message the hub relayed now carries that message’s Originating Virtual Address as its Destination Virtual Address. 135-2024 AB.3.1.2 requires this (“the ‘Destination Virtual Address’ shall be the VMAC indicated in the ‘Originating Virtual Address’ parameter of the message being responded to”). This applies to option, Address-Resolution and unexpected-address NAKs, trailing-data NAKs and decode-failure NAKs. Before, these NAKs had no DVA, so the hub consumed them and the originating node never learned its message was rejected (135.1-2023 12.5.1.2.1, 12.5.1.2.2).
  • rb v-20260924-2 (#2575): the Disconnect-ACK a hub connector sends to the hub now carries the Disconnect-Request’s Message ID (AB.2.13.1: “The message identifier of the message for which this message is the response”). Before, it carried a fresh ID (135.1-2023 12.5.1.1.1 steps 8-9).
  • rb v-20260924-2 (#2576): the Advertisement a hub connector sends in answer to an Advertisement-Solicitation changed in three ways (135.1-2023 12.5.1.1.15 step 2):
    • Maximum BVLC Length and Maximum NPDU Length are now the node’s configured values, the same ones its Connect-Request sends, instead of 65535 / 1497.
    • Accept Direct Connections is now X’01’ when direct-connect accept is enabled with a URI. Before, it was always X’00’.
    • The Advertisement no longer copies the solicitation’s Message ID (AB.3.2). The hub-function and direct-connect roles still have this defect; it is tracked as #2706.
  • rb v-20260924-3 (#2561): the Network Port’s SC_Hub_Connector_State is now a BACnetSCHubConnectorState (cl. 12.56.87): no-hub-connection (0), connected-to-primary (1), connected-to-failover (2). Before, it reported the hub connector’s internal 0..6 state machine. A node connected to its primary hub read 2 (connected-to-failover), and a node still trying read 3, which is outside the enumeration.
  • rb v-20260924-3 (#2573): when an established primary hub connection is lost, the hub connector now re-attempts the primary before trying the failover hub. It moves on to the failover only when that attempt fails. 135-2024 AB.5.2 requires this (“the hub connector shall first attempt to re-establish the primary hub connection”; 135.1-2023 12.5.1.1.2 step 4). Before, it went straight to the failover hub.
  • rb v-20260924-3 (#2592): a refused primary-hub probe while connected to the failover hub no longer clears the hub connection status. Before, the node reported no hub connection and failed every outgoing BACnet/SC NPDU although the failover connection was up.
  • rb v-20260924-3 (#2592): when the failover hub closes the connection cleanly (Disconnect-Request, heartbeat or disconnect timeout), the hub connector now clears its hub connection status. Before, it kept reporting connected-to-failover and refused to re-dial the failover hub.
  • rb v-20260924-4 (#2569): a BACnet/SC header option whose Header Data Flag is set but whose data is empty now re-encodes with its 2-octet Header Length. 135-2024 AB.2.3 says the field “Shall be present if and only if the ‘Header Data Flag’ of the header marker is set (1). If zero data octets are present, the ‘Header Data’ field is considered empty.” Before, re-encoding dropped the length. Every relayed or re-encoded frame carrying such an option then had the flag set with nothing after it, so the next receiver read the following two octets as the length.
  • rb v-20260924-4 (#2564): a Connect-Request, Connect-Accept or Advertisement with octets after its fixed payload now gets a BVLC-Result NAK UNEXPECTED_DATA on every ingress path (hub function, direct connect, hub connector), as AB.3.1.5 requires (“longer than expected”). Before, it was accepted; an over-long Connect-Request established the connection.
  • rb v-20260924-4 (#2563): Data Options on any BVLC function other than Encapsulated-NPDU now get a BVLC-Result NAK UNEXPECTED_DATA on every ingress path. AB.2.3 says Data Options “shall only be present in BVLC messages that convey an NPDU”, and each other format table says “Shall be absent”. Before, only Connect messages were checked. A Heartbeat-Request carrying Data Options was ACKed by the hub function and direct connect, and ignored by the hub connector. No NAK is sent for a broadcast or a response message, and a frame addressed to another node is still silently discarded.
  • rb v-20260924-5 (#2603): each element of the Network Port’s SC_Direct_Connect_Connection_Status is now encoded as the 135-2024 cl. 21 BACnetSCDirectConnection. The fields are uri [0], connection-state [1], connect-timestamp [2], disconnect-timestamp [3], then the OPTIONAL peer-address [4], peer-vmac [5], peer-uuid [6], error [7] and error-details [8].
    • Before, the class encoded peer-uuid at [2], where a client expects a BACnetDateTime, and peer-vmac at [3], and added non-standard maximum-bvlc-length [4] and maximum-npdu-length [5] fields. Conformant decoders rejected the whole list.
    • The two timestamps are always present: the unspecified datetime until known. The connect timestamp is stamped when a connection is established.
    • Peer fields are absent until determined, and error fields are sent only in DISCONNECTED_WITH_ERRORS and FAILED_TO_CONNECT, as cl. 12.56.98 requires.
    • The accepted connection’s URI, the disconnect timestamp and the failed-attempt timestamp remain with #2604.
  • rb v-20260924-6 (#2584): a BACnet/SC hub function forwarding a unicast now removes the Destination Virtual Address and sets the Originating Virtual Address to the sending connection peer’s VMAC. 135-2024 AB.5.3.2 says “the ‘Destination Virtual Address’ parameter shall be removed”, and 135.1-2023 12.5.2.1.7 step 2 checks for its absence. The header is rewritten in place, so a payload the stack cannot decode is still forwarded byte for byte, whatever its size. Broadcasts keep their DVA (AB.5.3.3). The hub device’s own unicasts to its peers are tracked as #2862.
  • rb v-20260924-6 (#2585): the hub function now discards a forwarded Address-Resolution or Address-Resolution-ACK whose Originating Virtual Address names a node other than its connection peer, the check #344 applies to Encapsulated-NPDU. Before, such an ACK was relayed unchanged, binding another node’s VMAC to the sender’s WebSocket URIs.
  • rb v-20260924-6 (#2586): the hub function ignores a Connect-Request on an already-registered connection. AB.6.2.3 defines no Connect-Request transition in CONNECTED, and AB.5.3.1 allows “at most one hub connection from each BACnet/SC node”. Before, the request re-bound the peer’s VMAC. If it named another peer’s UUID, it disconnected that peer and left a second row for the same connection, and the row outlived the connection.
  • rb v-20260924-6 (#2587): a Connect-Request that reuses a registered Device UUID but claims an unrelated peer’s VMAC is now rejected (NODE_DUPLICATE_VMAC) before anything is torn down. Before, the connection holding that UUID was disconnected and removed first, so both connections were lost. Rejecting is the stricter of the two readings the issue names, and is recorded there for the owner.
  • rb v-20260925-1: responding transaction state machine (F05).
    • A second request from the same client no longer aborts the first (#2498). 135-2024 cl. 5.4: “A transaction is uniquely identified by the client BACnetAddress, the server BACnetAddress, and the Invoke ID (if any).” The table of server transactions was keyed per peer, so a Confirmed-Request with a new invoke ID reached the entry of the client’s transaction in progress. During a segmented response it drew Abort(other) and the segmented response was discarded. During a segmented request the reassembly was aborted, and during AWAIT_RESPONSE the new request was dropped.
      • Each peer keeps one primary entry. A Confirmed-Request, or a SegmentACK or Abort from the client, whose invoke ID differs from the busy primary’s transaction now gets its own entry, and timers drive every entry.
      • One peer may hold at most 8 entries, the primary included (MAX_CONCURRENT_TRANSACTIONS_PER_PEER); beyond that a new transaction is answered with Abort(OUT_OF_RESOURCES).
      • While the primary is busy, a client Abort naming no held transaction is dropped without a reply.
      • Unconfirmed-Requests, Errors, Rejects and server-side PDUs still go to the peer’s primary entry (#2495, #2497).
    • Aborts name the transaction they abort (#2492). 135-2024 cl. 20.1.9.2: the original-invoke-id “shall be the ‘invoke-id’ of the transaction being aborted.” The responding state machine took it from the network parameters, which carry an invoke ID only for a Confirmed-Request and lost it when copied. So the APDU_Timeout Abort, and the Aborts that answer a client SegmentACK, carried an indeterminate byte. They now use the invoke ID of the entry’s transaction. BACnetNetworkParameters initialises the invoke ID to 0 and copies it; the other members #2523 lists are unchanged.
    • A failed transaction allocation no longer crashes the next Tick (#2496). The failed allocation was stored as a NULL entry, which Loop() dereferenced. Nothing is stored now, and Loop() skips a NULL entry.
  • rb v-20260925-10 (#2872): a confirmed request whose ComplexACK the responder wrongly judged too long for one APDU got Abort(outOfResources) instead of an answer. The responder sized the ACK before encoding it, from an estimate that over-counts some ACKs. CreateSegment() then measured the encoded bytes, found they fit, and refused to segment.
    • Seen as a ReadRange on a Trend Log Multiple, where 50 two-value records (the per-ACK item cap) fit in 1476 octets but were estimated not to.
    • The responder now measures the encoded ACK, the same length CreateSegment() uses. An ACK that fits is sent unsegmented, and one that does not still segments.
    • The same measurement also fixes two other cases. An over-estimated ACK to a peer that does not accept segments now gets its answer rather than Abort(segmentationNotSupported). An under-estimated ACK now segments rather than failing to encode into one frame.
  • rb v-20260925-11 (#2944): a ReadRange answer is now cut to what the requester can receive. 135-2024 cl. 15.8.1.2.4: “The MORE_ITEMS flag indicates whether more items matched the request but were not transmittable within the PDU.”
    • Before, every ReadRange handler capped the answer at 50 items whatever the requester’s APDU size, and the requester got an Abort when those items did not fit an unsegmented response. Fifty Recipient_List destinations do not fit even 1476 octets.
    • Now the items that fit the requester’s max APDU and segments are answered, with MORE_ITEMS set. A backward read keeps the items nearest the reference.
  • rb v-20260925-11 (#2944): a ReadRange of Active_COV_Subscriptions pages through the subscription table instead of answering Error(RESOURCES, OUT_OF_MEMORY) when the table holds more than maxActiveCOVSubscriptionsInAResponse. ReadProperty keeps that cap.
  • rb v-20260925-12 (#2780): an Event Enrollment whose FAULT_OUT_OF_RANGE limits differ in datatype from the monitored value now reports Reliability CONFIGURATION_ERROR.
    • The example is Real limits monitoring a Large Analog Value (Double) or an Unsigned property. Before, it reported nothing: since #2735 every comparison refuses a mismatch, so the object could neither enter nor leave a fault.
    • 135-2024 cl. 13.4.7: “all of the numeric parameters, pMinimumNormalValue, pMaximumNormalValue, and pMonitoredValue shall have the same data type”.
    • It is judged on the latest sample, and only once limits are set and a value has been sampled. The sample that exposes the mismatch moves the object to FAULT with a TO_FAULT notification on that same evaluation, and a later sample of the matching datatype returns it to NORMAL.
  • rb v-20260925-13 (#2538, owner ruling 2026-09-25): a BBMD now distributes its own broadcasts, and routed broadcasts leaving through it, to its BDT peers and FDT entries when it sends them. Each goes out as a Forwarded-NPDU whose Originating Device is the BBMD. 135.1-2023 12.3.2.2.1 step 4: “RECEIVE DA = Directed IP Broadcast to IP Subnet 2, Forwarded-NPDU, Originating-Device = IUT, NPDU = I-Am”.
    • Before, they reached remote subnets and foreign devices only if the host happened to loop the broadcast back to the stack. Otherwise a foreign device’s Who-Is was answered by every device except the BBMD it registered with.
    • A BBMD now drops, and does not forward again, a received Original-Broadcast-NPDU from its own B/IP address, so a host that does loop broadcasts back does not cause a double forward. The frame is still processed locally, as before.
    • The check recognises the BBMD’s bound local address as its own too. That covers a NAT’d BBMD, whose BDT entry holds its public address.
    • A Distribute-Broadcast-To-Network from an unregistered sender is NAKed (J.4.5) and no longer also answered as a received broadcast. Before, it was answered, and now that a BBMD distributes its own broadcasts, any sender could have made it announce itself to every BDT peer and foreign device.
    • Tests that pinned the loop-back design were rewritten as the ruling authorises: ten BTL BBMD tests, the BBMDReplyDistributionTest frame counts and three BACnetInterfaceBBMDTest cases.
  • rb v-20260925-14 (#2702 items 1-2): re-keying the local Device (a You-Are assignment or BACnetStack_UpdateDeviceInstance) now also moves state that was cached under the old instance.
    • A Network Port’s data link (BACnet/IP, MS/TP, BACnet/SC) is owned by the new instance. Before, FindIpv4DataLinkByNetworkPort and its MS/TP and SC mirrors found nothing under the new instance, so later configuration of that port (BBMD and foreign-device settings, SC hub settings) no longer reached its data link.
    • Notification Forwarder objects are owned by the new instance, so local-only forwarding compares notifications against the device’s current instance.
    • Both follow from BACnetDBDevice::UpdateObjectIdentifier, which every re-key path goes through. A re-key refused before the Device object moves changes nothing. Renaming any other object, even one with the device’s instance number, does not move them.
    • Not covered: a virtual device’s own Network Port data link does not follow a re-key, because virtual devices carry no interface back-pointer (#2979).
    • Not changed: the host application is still not told the new instance (#2702 item 3, #2650). That needs a new callback, and the callback interface is frozen without owner review.
  • rb v-20260925-15 (#2978): renaming an object, or re-keying the Device, no longer moves the stack-held state of a different object that happens to share the old instance number.
    • BACnetDBDevice::UpdateObjectIdentifier moves 26 per-type engine maps keyed by bare instance, among them Notification Forwarder, Program, Trend Log, Event Log, Schedule, Calendar, Timer, and the life-safety and access-control objects. Each map now moves only when the renamed object is of its own type.
    • Before, a Device re-key from 12345 to 33333 moved Notification Forwarder 12345’s engine to 33333, so NF:12345 lost its forwarding state. When an engine already sat at 33333, NF:12345’s engine was dropped instead. Renaming AV 5 moved Program 5’s engine the same way.
    • The test-tool authorization policy and cache storage has no object type behind it (#766), so no rename moves it any more.
  • rb v-20260925-15 (#2975): BACnetStack_UpdateDeviceInstance refuses a new instance of 4194303 or above, with the same message AddDevice uses. Before, it accepted the reserved 4194303 and kept only the low 22 bits of a larger value, re-keying the device to an instance the caller never asked for.
  • rb v-20260925-16 (#2983): the remaining gaps in renaming an object.
    • No rename can move an object to the reserved instance 4194303 any more; AddObject and AddDevice already refused it. A WriteProperty of Object_Identifier with that instance is answered PROPERTY / VALUE_OUT_OF_RANGE. Before, it was accepted.
    • A WriteProperty of the Device’s own Object_Identifier now goes through BACnetInterface::UpdateDeviceInstance, as BACnetStack_UpdateDeviceInstance does. The instance of another local or virtual device is refused with PROPERTY / DUPLICATE_OBJECT_ID. Before, only the device’s own object list was checked. This covers the device added with AddDevice. A virtual device’s own rename by WriteProperty still takes the old direct path, because virtual devices have no interface back-pointer (#2979).
    • Property values the stack stores for an object, such as a Trend Log’s Stop_Time, move with the object when it is renamed. Before, the renamed object lost them, and an object later created at the old identifier found them.
    • Not changed: an Audit Reporter cannot be renamed at all, because the rename refuses object type 62 as not enabled. The engine-map gap reported for it cannot happen.
  • rb v-20260925-2 (F01).
    • A Reliability write evaluates the event state (#2384). 135-2024 cl. 13.2.2.3: “A successful write to the Reliability property shall cause the event state to be evaluated”. Before, nothing evaluated it until the host called BACnetStack_UpdateValue(..., Reliability). After a successful WriteProperty or WritePropertyMultiple of Reliability on an object with intrinsic alarming, the next Tick now evaluates that object once, exactly as UpdateValue(Reliability) does. Not covered:
      • Writes to a virtual-router child device’s objects: Tick walks the main device only.
      • An object watched only by an Event Enrollment, with no intrinsic alarming of its own. Both are #2925.
    • Reliability_Evaluation_Inhibit with Out_Of_Service TRUE serves the written Reliability (#2384). cl. 12.2.33: under the inhibit, Reliability reads NO_FAULT_DETECTED “unless Out_Of_Service is TRUE and an alternate value has been written to the Reliability property”. The read now serves the host’s value in that case. The event-state evaluation under the inhibit still uses NO_FAULT_DETECTED; that divergence is #2925.
    • Device-originated confirmed notifications advertise 480 octets on MS/TP (#2383). Confirmed COV, COV-multiple, event and audit notifications advertised Max_APDU 1476 on every data link. MS/TP carries non-encoded frames of at most 501 octets (cl. 9.3), so on MS/TP they now advertise 480, the cap #2357 applied to client requests.
  • rb v-20260925-3: ReadProperty, ReadPropertyMultiple and ReadRange errors and decoding (F10).
    • READ_ACCESS_DENIED carries the PROPERTY class (#2622). 135-2024 cl. 15.5.1.3.1 and 15.7.1.3.1 answer “The property is not accessible using this service.” with PROPERTY / READ_ACCESS_DENIED, and cl. 18.3 lists READ_ACCESS_DENIED under PROPERTY. Two cases reported OBJECT: a ReadProperty (or ReadPropertyMultiple entry) of a Trend Log, Trend Log Multiple or Event Log Log_Buffer, and a Get callback that declines with readAccessDenied or with no code (the #974 default). Both now report PROPERTY. CASBACnetStackDLL.h still documents OBJECT for Log_Buffer; that doc change is handed to session D in #2928.
    • A ReadRange Count of 0 is refused (#2632). cl. 15.8.1.1.4.1.2: “‘Count’ may not be zero.” (and “shall not be zero” for By Sequence Number and By Time).
      • Such a request used to decode, and the processor answered it with Abort(other). It is now rejected at decode with Reject PARAMETER_OUT_OF_RANGE, as an out-of-range count already was (#636).
      • SetRangeByPosition, SetRangeBySequenceNumber and SetRangeByDateTime refuse 0 and leave the request unchanged, so the BACnetStack_SendReadRangeWith* exports return false for a count of 0.
    • An empty ‘List of Results’ decodes (#2633). cl. 15.7.3.1.2: “the ‘List of Results’ shall be present and empty”. BACnetListOfResults::Decode read the closing tag as a first element, so an RPM-ACK carrying [1]{} could not be decoded, including one this stack encoded itself. A truncated tag right after the opening tag is still refused, now as Reject INVALID_TAG.
  • rb v-20260925-4 (#2628, F10): ReadRange serves every BACnetLIST, not only the three Log_Buffers.
    • 135-2024 cl. 15.8: “The service may be used with any BACnetLIST or BACnetARRAY of BACnetLIST property.” Before, a ReadRange of any other list (a Notification Class Recipient_List, an Elevator Group’s Landing_Calls, and so on) was answered with Abort(other).
    • Such a list is now read the way ReadProperty reads it, then windowed:
      • No Range returns the list, up to MAX_READ_RANGE_RECORDS (50) items, with MORE_ITEMS set for the rest.
      • By Position follows cl. 15.8.1.1.4.1: the Result Flags of cl. 15.8.1.2.4, no First Sequence Number, and a capped backward read returns the items nearest the reference. Reference Index 0, or one past the end, is an empty Result(+).
      • By Sequence Number and By Time answer PROPERTY / LIST_ITEM_NOT_NUMBERED and PROPERTY / LIST_ITEM_NOT_TIMESTAMPED (cl. 15.8.1.3.1), since the items carry neither.
    • A list the host serves as encoded bytes through the test-tool constructed callback is decoded into items. If it cannot be decoded, the answer is Result(-) DEVICE / OPERATIONAL_PROBLEM.
    • Not covered:
      • A reply is still capped by item count rather than by the requester’s APDU size, and Active_COV_Subscriptions refuses above its per-response cap instead of paging (#2944).
      • Device_Address_Binding reads empty through ReadProperty, and so through ReadRange, even when the DAB table has entries (#2942).
  • rb v-20260925-8 (#2919): an empty BIT STRING built with the default constructor encoded as 80 00: a tag declaring no contents octets, then one. It now encodes as 81 00 (135-2024 cl. 20.2.10). Any BitString property served from the fabricated default reached this path.
  • rb v-20260925-8 (#2952): while DeviceCommunicationControl was DISABLE_INITIATION, every network-layer message sent with the local device as originator was dropped (“Pdu does not exist”); routed traffic was not affected. Network-layer messages are not BACnet-user service requests, so they now go out.
    • The second copy of the check, in BACnetDataLink, dereferenced the missing APDU without a NULL check. Both copies now use one rule, BACnetPacket::IsRefusedWhenInitiationDisabled.
  • rb v-20260925-8 (#2691, CR2-F13-022): DISABLE_INITIATION no longer suppresses I-Have or Confirmed/UnconfirmedAuditNotification, which cl. 16.1.2 exempts. An Audit Reporter’s notifications are now sent while initiation is disabled, where before they were held until DCC was re-enabled.
  • rb v-20260925-9 (#2942): the Device object’s Device_Address_Binding always read as an empty list, whatever the device’s address-binding table held. When the host registered CallbackGetPropertyConstructed for any property, the read failed instead. The stack holds the table, so it now answers the property itself (cl. 12.11.34), ahead of any callback, for ReadProperty, ReadPropertyMultiple and ReadRange alike. Only entries with a known address are listed: an entry still waiting for its I-Am would otherwise read as a broadcast address (a zero-length MAC, cl. 21.6).
  • rb v-20260925-9 (#2949, test tool): BACnetStackTestTool_{Add,Remove}ListElementReadAccessSpecifications sent an entry whose object type was above 1023, or whose instance was above 4194303, as a different object identifier. Such an entry now fails the call and nothing is sent. IFC-059.
  • rb bs-20260924-22 (#2168): BACnetUnconfirmedRequestPDU stores its service choice as an octet, as BACnetConfirmedRequestPDU does. A raw request’s choice therefore reaches the wire as given, even with no enumerator. Copying a PDU that carries a raw request keeps it raw rather than downcasting it to a typed request.
  • rb bs-20260924-23 (#2408, owner ruling 2026-09-24): BACnetStack_SetBACnetSCHubConnector (both the device-wide and per-Network-Port forms) now clears the BACnet/SC hub connector’s reconnect backoff: both hubs’ reconnect timers and reconnect-attempt counts. The next connection attempt is made at once, even when the URI is unchanged. Before, after repeated failures the connector waited up to the maximum reconnect timeout (600 s by default) before dialling again. That included the case where the application reconfigured it, for example the CAS BACnet Explorer’s Connect button after the user fixed a trust anchor. An application call is a user-initiated connect, not a retry. Behaviour change: a host that re-applies its hub configuration on a timer now also resets the backoff each time. The owner accepted this. Network Port synchronisation inside the stack does not reset it. A connector that was waiting to try its failover hub tries the failover hub first, then the primary (AB.5.2 order).
  • rb bs-20260928-57: Event Enrollment Event_Parameters (F20).
    • An Event Enrollment’s CHANGE_OF_TIMER can be armed over the wire (#2876, part 1).
      • A WriteProperty of Event_Parameters with the change-of-timer [22] alternative now configures the time-delay, alarm-values and update-time-reference of an Event Enrollment whose event type is CHANGE_OF_TIMER.
      • Before, Event_Parameters was read-only for every algorithm (#2745), so an Event Enrollment’s CHANGE_OF_TIMER could never be given alarm values.
      • The write is validated before anything changes. An alarm value that is not a BACnetTimerState, or a missing update-time-reference, is refused with PROPERTY / VALUE_OUT_OF_RANGE. Time_Delay_Normal, a property of its own, is kept.
      • Every other alternative, and a write that would change Event_Type (read-only, cl. 12.12.5), is still refused with PROPERTY / WRITE_ACCESS_DENIED.
      • A new update-time-reference is a new pUpdateTime source. Its first value is taken as the baseline, not reported as a change (condition (c)). A pending time delay restarts with the new parameters.
      • The property is still read-only unless the host flags it writable.
      • Cl. 12.12.7: Event_Parameters “determines the algorithm used to monitor the referenced object and provides the parameter values needed for this event algorithm.” Its writability is a local matter.
  • rb bs-20260928-58: a post-store hook (F25) and a test-tool doc (F23).
    • A post-store hook for stored writes (#2995).
      • BACnetDBDevice::AfterPropertyStored is now called by BACnetBusinessLogic::SetProperty after a write is stored, whether by the host’s Set callback or as a stack-stored value.
      • The Life Safety Zone recompute on Out_Of_Service written FALSE (#2907, cl. 12.16.11 (a)) moved into it, and the zone branch’s hand copy of SetProperty’s store tail was deleted. The two copies had already started to drift in form.
      • No behaviour change; the #2907 tests are the gate.
    • The InsertAuditLogRecord test-tool doc names the Enable-FALSE refusal (#3204, IFC-066).
  • rb bs-20260929-98: internal clean-ups from the #2613 roll-up:
    • The NM-SCCM certificate procedures use the stack’s BACnetNetworkPortCommand / BACnetReinitializedStateOfDevice enumerations instead of local copies and a literal "9" (CR2-F09-009).
    • BACnetStackNetworkPortSC::Reset() calls InitSCFields() instead of repeating it line for line (CR2-F09-010).
  • rb bs-20260929-99: BACnet/SC Maximum-BVLC/NPDU bound tests use literal values (1496/1497, 1512/1513, 61327/61328) and check both NPDU bounds on the wire, in all three message classes. The five bounds are pinned in EverySecurityBoundIsPinnedToAConcreteValue (#2613, CR2-F09-026).
  • rb bs-20261004-1: BACnet/SC internals and tests from the #2613 roll-up:
    • CR2-F09-058: the hub-function relay allocates its ~5.7 KB re-encode buffer only for a broadcast that needs an Originating Virtual Address added, not for every relayed frame. Failed broadcast forwards are logged as a count instead of being discarded.
    • CR2-F09-076: the duplicate-VMAC test asserts that the VMAC changed and has the Random-48 form (135-2024 H.7.3: “the least significant 4 bits (Bit 3 to Bit 0) in the first octet shall be B’0010’ (X’2’)”). The finding’s second half is not done: asserting that the BTL 12.5.3.3.2.3 test makes no retry before the reconnect timeout would edit a BTL conformance test, and whether a failed attempt waits out the timeout is the open owner question on #2578.
    • CR2-F09-085: the destination-absent fallback is now tested for the direct-connect accept list, with a control.
  • rb bs-20261004-11: MS/TP engine documentation (#2612, CR2-F08-015):
    • The silence-timeout recovery fires at Tno_token + 254 × Tslot (3,040 ms with defaults), not “1 seconds”.
    • MSTP_VERSION now matches the latest revision in the header’s history: it predated revision 7, and is now “2026-Oct-04”, the date of the new revision 8.
    • The Tusage_timeout comment gives the spec’s 20 ms (cl. 9.5.3).
    • The mstp_SendByte comment gives uint16_t for the length.
    • A revision-8 entry is added to the MSTP.h header.
  • rb bs-20261004-15: documentation from the #2610 roll-up:
    • CR2-F06-025: the event Priority clause is cited as 12.21.6, not 12.21.15 (Acked_Transitions). A SetSource comment pointed a still-open defect at closed #274; it is now filed as #3511 and the comment cites that.
    • CR2-F06-035: the Router-Available-To-Network decode comments cite and quote cl. 6.6.3.7, not cl. 6.6.3.6 (Router-Busy).
    • CR2-F06-043: the BACnetPortPermission comment follows Table 12-60 and cl. 12.51.11. Port_ID is the Clause 6 Port ID, Enabled filters event notifications received through the port, and only Port_Filter uses the type. The compile gate, which also includes the Audit Reporter, is not narrowed.
  • rb bs-20261004-17: CR2-F06-032: BACnetInitializeRoutingTable::AddPorts is renamed SetPorts. It replaces the port list, while AddPort and every sibling Add*s append. The vector constructors now log when they refuse a list of more than 255 ports. Code that calls AddPorts must change to SetPorts. The type is not in CASBACnetStackDLL.h.
  • rb bs-20261004-20: BACnet/SC hygiene from the #2613 roll-up:
    • CR2-F09-044: corrected comments and log strings. The hub connector state machine now cites AB.5.2 instead of AB.2. The BACnetSCHubConnectorState transcription no longer has the extension marker and reserved-range note that the cl. 21 production lacks. The Hello-option comments now say at least one Hello option is required and more are accepted. The failover transition no longer logs “Connected to Primary Hub”. The send-failure log now names any BVLC-SC message, not only Connect-Request.
    • CR2-F09-087: removed the hubConnector != NULL conditions, since the hub connector is a member and is never NULL. Its SC_Failed_Connection_Requests and SC_Primary_Hub_Connection_Status were always enabled; the code now says so directly. Renamed networkPortBase2/networkPortBase3 and the s/o/ws/dl… snapshot names, per the naming rule. EmitBACnetSCStateChange now checks m_callbacks for NULL, as its sibling does.
  • rb bs-20261004-21: BACnet/SC maintenance from the #2613 roll-up:
    • CR2-F09-020:
      • The two ToDo markers are now TODO(#3523). The clone does not copy the BVLC-SC header, and BVLC-SC rendering omits header options.
      • size_t log arguments are cast for %u.
      • BACnetSCBVLLValidator.h/.cpp are indented with tabs, and the header includes CASBACnetStackOptions.h before testing its guard.
      • Not done: the review also asked for the TODO-marker gate to become case-insensitive. That would bring about 70 other ToDo markers in source/ under the gate, so it is left for its own change. Item (5), the Hello citation, already reads AB.2.3.1.2 at this tip.
    • CR2-F09-070: removed HandleConnectedState’s “Local disconnection” branch. Run() handles a closed websocket in every state first, so the branch could never run. The AB.6.2.2 Disconnect-Request on a local teardown (SuspendCommunication) is tracked as #3524.
  • rb bs-20261004-25: #3523 (clone half): BACnetSCPacket::Clone still does not copy the BVLC-SC header, by design, and the code now says why. The TODO that called this a missing feature is gone. A clone that is sent goes through the SC outgoing path, which builds a fresh header for a packet that has none. A copied header would be wrong there: the responding state machine sends its timeout Abort from its clone of an incoming request, and the request’s header would carry this node’s VMAC as the destination, plus the request’s data options. A test pins the behaviour.
  • rb bs-20261004-26: documentation from the #2689 roll-up:
    • CR2-F11-007: the WriteProperty processor’s clause comments are now the verbatim 135-2024 text of 15.9.1.1.5, 15.9.1.3.1 and 15.9.2. That adds the INVALID_ARRAY_SIZE and INVALID_DATA_ENCODING rows, the “greater than the current length of the array” wording, the Clause 19.2 reference and the relinquish paragraph. #3502 had only labelled the earlier-edition copy.
    • CR2-F11-010 (interim): the BACnetGroupChannelValue comments now cite Annex F.3.11, which the code follows, instead of clause 21. They also quote clause 21’s disagreeing production. Which text governs remains an owner question.
  • rb bs-20261004-27: documentation from the #2688 roll-up:
    • CR2-F10-012: the ReadAccessResult, ReadResult, ReadPropertyMultiple-Request and list-of-specifications comments now follow the 135-2024 clause 21 productions. list-of-results is mandatory, and the request’s list has no [0] tag. The known interop limit with pre-2016 peers, which may omit list-of-results, is recorded.
    • CR2-F10-020: the ReadRange processor’s 15.8.1.3.1 table and 15.8.2 procedure are now verbatim 135-2024. That adds the LIST_ITEM_NOT_NUMBERED and LIST_ITEM_NOT_TIMESTAMPED rows.
    • CR2-F10-030: the ReadProperty processor’s INVALID_ARRAY_INDEX row is now the 135-2024 wording.
  • rb bs-20261004-28: test gates for BACnetPropertyAccessResult follow the class to STACK_OPTION_OBJECT_TYPE_GLOBAL_GROUP (TestBACnetPropertyAccessResult.cpp, three tests in TestBACnetPropertyAccessResultPolicy.cpp). That file’s AuthenticationPolicy tests are still ungated and do not compile without STACK_OPTION_OBJECT_TYPE_ACCESS_POINT; that predates this batch and is left.
  • rb bs-20261004-28: CR2-F10-022: BACnetResultFlags.h includes BACnetPrimitiveBitSTRING.h, the header of its base class.
    • It included BACnetPrimitiveENUM.h, so it compiled only after something else had declared the base class.
    • The unused STACK_OPTION_STRING_LITERALS block is deleted. It held BACNET_RESULT_FLAGS_NAME and a static name map built in every translation unit that included the header.
  • rb bs-20261004-29: #2608 CR2-F03-046: the REAL, DOUBLE, DATE, TIME and ObjectIdentifier decodes cited a non-existent “cl. 18.3.1(6)/(9)” for their reject reasons. They now cite cl. 18.9 (INVALID_TAG, PARAMETER_OUT_OF_RANGE), as do the same comments copied into six test files. Whether INVALID_DATA_ENCODING would fit a wrong-length fixed-width primitive better is the finding’s optional owner question, not asked here.
  • rb bs-20261004-30: comments from #2608:
    • CR2-F03-027: the cl. 20.2.9 character-set table in BACnetPrimitiveCharSTRING.h is now the 135-2024 one: X’00’ ISO 10646 (UTF-8), X’02’ JIS X 0208.
    • CR2-F03-028: CharSTRING::Reset’s comment no longer claims it sets the tag valid, and OctSTRING::Encode’s no longer claims an encoding octet.
  • rb bs-20261004-31: from #2608:
    • CR2-F03-040: tests now pin BACnetDeviceObjectPropertyReference::Decode’s two #638 present-but-malformed guards, for the array index and the device identifier. Each has a well-formed control. Disabling both guards fails both tests.
    • CR2-F03-041: the DeviceObjectReference and DeviceObjectPropertyReference headers name their own class in the sequence-enumeration comment, and BACnetDeviceObjectReference::Encode logs its own class name.
  • Left on #2608:
    • CR2-F03-039 is an owner question. Refusing a trailing-comma compact string would make BACnetStack_SendWriteProperty return false on input it accepts today.
    • CR2-F03-049 (licence headers) waits for the release-time licence sweep.
    • Seen in review, not changed here: BACnetOptionalUnsigned::Decode checks its parameters before Reset() (the CR2-F03-031 shape), and BACnetOptionalAny::GetElements renders “Null” when no choice is set.
    • The other unticked items.
  • rb bs-20261004-5: internals from the #2611 roll-up:
    • CR2-F07-007: BACnetFDTEntry / BACnetBDTEntry log strings name their own class and fields; they named BACnetRecipientProcess, “recipient” and “processIdentifier”. The FDT entry’s stale #1101 comment (operator= copying exactly 6 octets) is corrected, and its two address-length constants are static const.
    • CR2-F07-046: the BBMD’s unused m_hostMask / SetHostMask and m_srcAddress are removed; nothing has read them since #467.
  • rb bs-20261004-6: comments and dead code from the #2611 roll-up:
    • CR2-F07-017: the Write-Broadcast-Distribution-Table NAK comment quotes J.4.4.2 (“B/IP devices shall always return a BVLC-Result message to the originating device with a result code of X’0010’”) and cites 135.1-2023 cl. 12.3.3.7 (“Write-BDT service is required to return Write-BDT-NAK”). It used to say the service was optional and asked the owner to settle a conflict the standards do not have. Behaviour is unchanged.
    • CR2-F07-025: BACnetDataLinkLayer::BindRouterPort loses its unreachable single-instance tail. Its NOREFs now say they are for a build with no data link and no logging, where the parameters are not read.
    • CR2-F07-033: HandleProcessingErrors names the producer and callers of unknownNetworkMessage by symbol, instead of a line number that did not hold them.
  • rb bs-20261004-8: build hygiene and documentation, with no behaviour change:
    • #3498: 63 dead objectType/objectInstance locals are removed from BACnetInterface.cpp. These were declared and never read, left over from the BACnetObjectIdentifier32 migration; they warned under -Wall in every preset, including FULL. A local was removed only if its name appears nowhere else in its function. Six that are read only under an option guard still warn when that option is off.
    • #3498: UpdateCOVAfterEventStateChange’s forward declaration in BACnetBusinessLogic.cpp is under STACK_OPTION_ALARMS_AND_EVENTS, like its definition. A build without alarms and events declared a static function it never defined.
    • #2611 (CR2-F07-024): BACnetDataLinkLayer doc comments and three router-port log strings now describe the multi-instance, wired state. They still said MS/TP held a single untracked instance, that ResolveOutgoingDataLink, ProcessIncomingPacketForPort and FindDataLinkByNetworkPortInstance were not yet called, and that a second router port of a type had “only one instance”.
  • rb bs-20261005-1: from #2608:
    • CR2-F03-026: the CharacterString OperatorEquals test now asserts on the copy: value, length, encoding, validity, IsEqual, and byte-for-byte Encode. New tests cover an ISO 8859-1 copy and a copy of the longest accepted string. With operator= made a no-op, all three fail.
    • CR2-F03-032: BACnetPropertyValue cites cl. 21.6 (priority [3] Unsigned (1..16)) for the Priority range, where it cited cl. 15.1.1.3, AddListElement’s Result(-). A “Context 3” label on the property-value decode now says Context 2. Two log lines name their own class.
  • Left on #2608: 039 (owner question), 049 (release licence sweep), 044, and the other unticked items. 030’s priority [4] in BACnetObjectPropertyValue and the device identifier in BACnetDeviceObjectPropertyReference get the same presence-first comparison, but no test isolates either. After 010, BACnetTimeStamp.cpp’s #2743 comment, which says a primitive datetime fails the opening-tag check with the error unset, is out of date (its guard is now redundant and harmless). BACnetVTCloseError decodes an OPTIONAL list as mandatory and ignores the result, which predates this batch; that is #3542. 033 also notes that the STACK_OPTION_ALARMS_AND_EVENTS gate in BACnetPropertyStates.h has no PANIC #else; that is left.
  • rb bs-20261005-13 (#2694 CR2-F16-024): the Notification Forwarder’s test seams are now compiled only into a test-tool build (BACNET_STACK_TESTTOOL) or the unit-test build (CAS_BACNET_STACK_UNIT_TESTS, which projects/linux/test/makefile now defines). Before, they shipped in every build. The seams are AddSubscribedRecipientForTest, GetSubscribedRecipientForTest, GetPendingDeliveryCountForTest and GetMaxPendingDeliveriesForTest.
    • AddSubscribedRecipientForTest bypassed the #1161 Subscribed_Recipients cap and every validation.
    • The new define keeps the seams in the make NO_TESTTOOL=1 test build. The gating sweep compiles only source/, which has no caller of them.
    • The MSVS test project links the library’s test-tool configuration, so it needs no change.
  • rb bs-20261005-17, from the #2749 (family F20) roll-up:
    • CR2-F20-012: diagnostics in the event-values family now name their own class and arm. ChangeOfState no longer names OutOfRange, and ChangeOfValueNewValue’s REAL and bit-string arms no longer say “unsigned”, “null” or “changed bits” for the wrong arm.
      • BACnetNotificationParametersChangeOfBitstring::SetNotificationParametersChangeOfBitstring is the correctly spelled setter, and its two callers use it. The misspelled SetNotificationParmaetersChangeOfBitstring stays as a forwarding alias.
      • The ChangeOfDiscreteValueNewValue “Not enough memory” message the finding names is accurate now: a later fix moved the factory-rejection case ahead of it, so it fires only on a real allocation failure.
    • CR2-F20-014: the seven range, limit and buffer-ready arms (OutOfRange, DoubleOutOfRange, SignedOutOfRange, UnsignedOutOfRange, UnsignedRange, FloatingLimit, BufferReady) now have negative decode tests.
      • Every proper prefix of a valid encoding is refused, which covers a truncation anywhere, including a missing last field. So is the encoding with its first context tag changed to [2]. The complete encoding is the control.
      • The Signed arm also decodes a negative exceeding value directly.
      • Deviations from the recommendation: the tests are in TestBACnetNotificationParametersCR2F20.cpp rather than the seven per-arm files; the three cases it asked for per arm (truncated by one, wrong context tag, missing last field) are one every-prefix loop plus one wrong-tag case; and the wrong tag is rewritten on the first field only. They pin existing behaviour; a mutation evidences the truncation half, not the wrong-tag half.
      • The misspelled-setter alias has a test that it builds the same arm as the correctly spelled setter.
    • CR2-F20-021: test comments cite CHANGE_OF_TIMER as cl. 13.3.19 (was 13.3.29), ACCESS_EVENT as cl. 13.3.12 (was 13.3.28, “ChangeOfAccessEvent”), and the BACnetLifeSafetyOperation reserved range as cl. 21.6 (was 21.2).
  • rb bs-20261005-19 (#2748):
    • CR2-F19-006, -012: eight event-parameter arms’ GetType() return the named BACnetEventParameter CHOICE enumerator instead of a literal: DoubleOutOfRange, Extended, FloatingLimit, None, OutOfRange, SignedOutOfRange, UnsignedOutOfRange and UnsignedRange. The other eleven arms already did (#1249 converted six of them). Every value is unchanged.
    • CR2-F19-007: BACnetEventParameterNone’s comments now say the wrapper codes none as a primitive context-tagged NULL (F8 14) and does not call this class’s Decode or Encode. Before, they - and GetMinimumMemoryCount()’s - described a constructed [20] wrapper the stack does not emit.
  • rb bs-20261005-2: #2608 CR2-F03-052: tests now cover the wrong-class reject branch of every application-tagged BACnetOptional* sibling. Twelve of them get the flip-the-class-bit test (two already had one). OptionalAny, DateTime and DateTimePattern each get a wrong-context-number case and a non-Null application-tag case. With OptionalReal’s branch disabled, the sibling test fails.
  • Not covered by a test: the 259 arm of 029 (258 is tested); a primitive context [0]/[1] case for OptionalAny/DateTime/DateTimePattern (052 tests a wrong constructed number and a non-Null application tag); the rendered element’s content (only that GetElements succeeds). The two existing SC decode tests in TestBACnetPropertyStates.cpp now build their wire bytes from a raw Unsigned in [63], since SetExtendedValue decomposes 258/259 and would go through the compose logic they exclude.
  • Left on #2608: 007 (factory arms for five profile datatypes), 039 (owner), 044, 049 (licence sweep), and the other unticked items.
  • rb bs-20261005-20 (#2747 CR2-F18-009): six fault-family comments and log texts corrected (the finding’s five, and one 008 made stale):
    • OUT_OF_RANGE condition (f) reads “greater than or equal to pMinimumNormalValue”, as cl. 13.4.7 says.
    • BACnetFaultAlgorithm.h no longer cites cl. 12.12.21 (d), an event-algorithm rule, for a fault CONFIGURATION_ERROR.
    • The life-safety algorithm describes its monitored value as a BACnetLifeSafetyState.
    • The FAULT_LISTED log names faultSignals.
    • HasInconsistentParameters explains why an unarmed algorithm never reports (equal zero limits), not “unset” datatypes. The finding also offered making Reset() set the limits’ datatypes to Invalid so the old comment became true; Reset() is left as it is (it zeroes the values and keeps the datatypes) and the comment now describes that.
    • The shared Table 13-5 comment in the State, Characterstring and LifeSafety fault algorithms no longer names Life Safety Point/Zone as unmapped.
  • rb bs-20261005-22 (#2748 CR2-F19-002): the RoundTrip_TypedChangeOfStatusFlags and RoundTrip_TypedFaultListed tests now fill the alternative before wrapping it, assert the wrapper is valid instead of skipping their whole body behind if (IsValid()), and pin the exact wire bytes (FE 12 09 05 1A 04 40 FF 12; 7E 0E 0C 00 80 00 01 19 55 0F 7F). Before, both bodies never ran.
  • rb bs-20261005-23 (#2748 CR2-F19-003): new wrapper-level tests.
    • Known-answer vectors: one hand-derived clause 21 wire vector each for the eight group-A arms: change-of-bitstring [0], change-of-state [1], change-of-life-safety [8], buffer-ready [10], access-event [13] (with Access Point), change-of-characterstring [17], change-of-status-flags [18] and change-of-discrete-value [21]. Each is encoded through BACnetEventParameter and decoded back through it.
    • Negative decodes: the wrapper’s CHOICE dispatch rejects [6], [7], [12], [19], [23], a constructed [20] and a primitive [20] with content, each with Reject invalidTag.
  • rb bs-20261005-24 (#2748 CR2-F19-013): hand-derived clause 21 known-answer wire vectors are now pinned for the fixed-shape event-parameter arms, each encoded through the BACnetEventParameter wrapper and decoded back through it. They cover change-of-value [2] (both cov-criteria choices), command-failure [3], floating-limit [4], out-of-range [5], unsigned-range [11], double-out-of-range [14], signed-out-of-range [15], unsigned-out-of-range [16] and change-of-timer [22]. Their own tests only round-trip, so a symmetric mistake passed them all, for example two field tags transposed in a *SequenceEnum, or a field encoded and decoded as the wrong integer type (the integer fields use values from 0x80 up, where Unsigned and Integer differ). With this batch every arm except extended [9] and none [20], which already had wire-byte tests, has a wrapper-level vector.
  • rb bs-20261005-28 (#2746):
    • CR2-F17-007: the never-set BACnetStackAlarmAndEventObject::m_eventPendingTimeDelay and its uncalled accessor IsEventPendingTimeDelay() are removed.
    • CR2-F17-008, -035: corrected citations and comments:
      • COMMAND_FAILURE is cited as cl. 13.3.4, not 13.3.3 (which is CHANGE_OF_VALUE).
      • The Event_Detection_Enable test comments cite cl. 13.2.2 / 13.2.2.1, not 13.2.2.3 (Out_Of_Service).
      • The stale “pg 642” page references are dropped.
      • The CHANGE_OF_LIFE_SAFETY condition list uses the clause’s own lettering, (a) to (m), marking the optional (f)/(j) and the local-matter (l)/(m) as not implemented. Each condition is a one-line paraphrase, deliberately not the clause’s text verbatim as the finding suggested, to avoid copying a long passage of the standard.
      • A further COMMAND_FAILURE citation in EnableIntrinsicCommandFailureAlgorithm is corrected to 13.3.4 too.
      • The CHANGE_OF_TIMER list gains its implemented (c).
      • The BUFFER_READY class doc says Run() returns notification_NoStateChange when it notifies.
  • rb bs-20261005-29 (#2746 CR2-F17-014, -029): stale and copy-pasted comments across the event algorithms are corrected; no code changes.
    • FLOATING_LIMIT: the notification-parameter list names cl. 13.3.5’s Reference_Value, Status_Flags, Setpoint_Value and Error_Limit (it listed OUT_OF_RANGE’s), and the class doc no longer calls it the OutOfRange algorithm.
    • Out-of-range family: the OUT_OF_RANGE (13.3.6), DOUBLE_OUT_OF_RANGE (13.3.13), SIGNED_OUT_OF_RANGE (13.3.14) and UNSIGNED_OUT_OF_RANGE (13.3.15) class docs cite their clauses.
    • Datatype comment: the “OutOfRangeFault … Real, Double, SignedInteger or UnsignedInteger” comment, copied into three event algorithms, now says what each accepts: Real for OUT_OF_RANGE and FLOATING_LIMIT, the discrete types for CHANGE_OF_STATE.
    • SIGNED_OUT_OF_RANGE: the signed comparisons are described as int64 (#1171), not int32.
    • CHANGE_OF_CHARACTERSTRING: the docs describe the substring/empty/NULL matching of #1165 rather than exact equality, and its condition list gains the implemented (c).
    • CHANGE_OF_VALUE: cites Figure 13-10, not 13-4.
    • Condition lists: CHANGE_OF_STATUS_FLAGS gains its implemented (c); CHANGE_OF_BITSTRING marks its optional (c) as not implemented.
    • CHANGE_OF_STATE: the “12.28.?” placeholder is gone (Load Control has no Alarm_Values property).
  • rb bs-20261005-3: CR2-F13-040: BACnetUnconfirmedPrivateTransferRequest::Encode’s log lines name the class’s own object and the vendorId field.
  • Left on #2691:
    • CR2-F13-019 is an owner question. Refusing a vendor ID above 65535 in BACnetIAmRequest::SetVendorId would make BACnetStack_SendIAm return false for a host whose Vendor_Identifier is out of range.
    • The other unticked items.
  • rb bs-20261005-31 (#2847 CR2-F25-006): Life Safety comments corrected.
    • Citations: Silenced 12.15.23 / 12.16.23 (was 12.15.7, Device_Type), Operation_Expected 12.15.24 / 12.16.24 (was 12.15.9, Event_State), and Zone_Members 12.16.26 (was 12.16.11, Out_Of_Service).
    • Conformance: Zone_Members’ code is “R” in Table 12-19, not “R1”.
    • List properties: the array-dispatch helper’s comment notes that the Life Safety list properties are BACnetLISTs, whose callers never pass an index.
    • Enumerations: the BACnetLifeSafetyState and BACnetLifeSafetyMode ASN.1 comments now run to test-oeo-unaffected (34) and deactivated (19), as the code already does.
  • CR2-F25-007 was already fixed at the tip: BACnetLifeSafetyFaultAlgorithm::ApplyFaultParameters checks the fault values before storing the mode-property-reference (the #3230 review change).
  • rb bs-20261005-32 (#2848 CR2-F26-006): lift-group log text names the right object and field (BACnetLandingCall, BACnetLandingCallStatus, BACnetLandingDoor, floorNumber, floorText), the floor-text comment says Context 3, and BACnetLandingDoor’s sequence enum is BACnetLandingDoorSequenceEnum (it was a copy of BACnetLandingCall’s name).
  • rb bs-20261005-34 (#2849 CR2-F27-018): the BACnetLightingOperation, BACnetLightingInProgress and BACnetBinaryLightingPV ASN.1 comments now list every value cl. 21 defines (restore-on .. toggle-default, trim-active, toggle), as the enums already did; a PropertyStates test comment names LightingInProgress 2 as rampActive.
  • rb bs-20261005-38 (#2846 CR2-F24-025): the BACnetCredentialAuthenticationFactor ASN.1 comment lists disabled-destroyed (5) and the extension marker, and cites cl. 12.35.10 (Authentication_Factors) instead of the Authentication/Authorization service clause.
  • CR2-F24-024 was already fixed at the tip: BACnetAccessToken::GetElements renders Audience, NotBefore and NotAfter (#3123).
  • rb bs-20261005-47 (#2851 CR2-F29-003): the BACnetEngineeringUnits ASN.1 comment shows active-energy-pulse-value as 47918, as the enum already does, noting that 135-2024’s thematic list prints 47910 (an erratum: 47910 is millisiemens-per-centimeter); it cites 135-2024 cl. 21.6 and notes the 47968-47981 additions. The BACnetReliability comment lists multi-state-out-of-range (25).
  • rb bs-20261005-47 (#2851 CR2-F29-015): Channel comments cite cl. 12.53 / 12.53.5 (they cited cl. 12.10, the Command object), and the commandability comment’s reasoning is corrected - Channel has no Priority_Array or Relinquish_Default (Table 12-62); its Present_Value is commandable by definition (cl. 12.53.5). List_Of_Group_Members is cited as cl. 12.14.5 (it said 12.14.4, Description), here and in CASBACnetStackOptions.h.
  • rb bs-20261005-48 (#2750 CR2-F21-012): documentation only. BACnetStackTestTool_SendAlert’s \return and the RaiseAlert comment now say what a false return covers since #1187: nothing sent, and either nothing changed (missing object, bad parameters, Event_Detection_Enable FALSE) or - with Event_Enable[TO_NORMAL] FALSE - the alert was recorded (Present_Value, Event_Time_Stamps, Acked_Transitions) without a notification. They used to say false always meant nothing changed.
  • rb bs-20261005-48 (#2750 CR2-F21-007): comment only. BACnetDestination’s From/To Time wildcard handling now quotes cl. 12.21.8 Table 12-25 (“These values shall be specific times.”) and states the reading this implementation follows - a wildcard octet is accepted and interpreted, not refused. Whether to refuse is left for the owner.
  • rb bs-20261005-5: CR2-F13-038: CAS_BACNET_DAB_MAX_ENTRIES and CAS_BACNET_INVOKE_ID_MAX_TRACKED_PEERS are pinned to 4096 in EverySecurityBoundIsPinnedToAConcreteValue, and the DAB table-full test’s bound is now a literal.
  • Left on #2691:
    • CR2-F13-019 is an owner question.
    • CR2-F13-009 is left: it edits BACnetCallbackInterface.h, the frozen callback header.
    • The other unticked items.
  • rb bs-20261005-50 (#3089 CR2-F02-011, CR2-F02-021): debug-log messages that named the wrong property now name the right one - Network_Number / Network_Number_Quality / APDU_Length in the IPv4, MS/TP and Virtual Network Port adders (they said “bacnet ip mode” / “udp port”), Operational_Certificate_File (it said commandValidationResult), the foreign-device reset (it said “enable”), the Trend Log Trigger default (it said “stop when full”), Installation_ID (it said “group id”), and AddValueToStoreInDB now logs the property, object type and instance (it said “elevator group” for every property). The Virtual adder’s comment no longer says “MSTP network type”.
  • CR2-F02-005 was already fixed at the tip: FindForeignDeviceNetworkPort (#2537) increments only in the arms that do not erase.
  • rb bs-20261005-51 (#3089 CR2-F02-033): BACnetStack_SetIntrinsicChangeOfStateAlgorithmBool / Enum / Unsigned check that the variant matches the object type (and, for Enum, that the object type has an enumerated monitored value) before creating the object’s event algorithm, so a refused call no longer leaves an unconfigured algorithm attached. The return values are unchanged. BACnetChangeOfStateEventAlgorithm::GetEnumTypeByObject is now static.
  • CR2-F02-027 and CR2-F02-029 were already fixed at the tip: #3482 item 4 checks the bitstring length before the Event Enrollment get-or-create, and #3483 item 3 builds the FAULT_CHARACTERSTRING list before touching anything. CR2-F02-033’s FAULT_STATE site was already ordered correctly by #3483 item 1.
  • rb bs-20261005-52 (#3089 CR2-F02-065, -079, -080): comments only. The Color and Color Temperature property-profile comments cite Tables 12-84 and 12-87 (they cited Lighting Output’s Tables 12-65 and 12-66); BACnetObjectType.h’s copy of the cl. 21 production lists color (63) and color-temperature (64) and gives object type 38 as removed; BACnetObjectSelector.h says a Null entry is ignored (cl. 12.63.13) rather than calling it a wildcard.
  • rb bs-20261005-53 (#3089 CR2-F02-008): BACnetDBDevice::EraseFromEngineMaps is private, as its comment always said - only DeleteObject calls it. Header comments corrected: the FAULT_STATE / FAULT_CHARACTERSTRING / FAULT_EXTENDED setters are reached only through the test-tool interface (they are no longer unreachable); the priority paragraph now sits on SetGeneratedPropertyValue, which it describes; a duplicated “Helper Functions” banner is gone.
  • rb bs-20261005-53 (#3089 CR2-F02-004, CR2-F02-048): comments only. AddObject’s #906 comment says the check rejects only the reserved instance 4194303 and the range check belongs to the caller before it packs the identifier (packing masks the instance to 22 bits). The Trend Log and Trend Log Multiple Align_Intervals / Interval_Offset comments no longer quote “shall be writable if present” from cl. 12.25.27/.28 - that sentence is the Device’s cl. 12.11.45/.46; the writable default stays, as an implementation choice (#1009).
  • rb bs-20261005-56 (#3089 CR2-F02-064): Lighting Output and Binary Lighting Output profile Current_Command_Priority only above protocol revision 16, as Access Door’s does (#1233) and as their required lists already did. No effect at the default revision; it matters only in a configuration built at revision 16 or below (which excludes the Network Port object).
  • rb bs-20261005-56 (#3089 CR2-F02-076): comment only - BACnetPropertyIdentifier::IsValidPropertyIdentifier is documented as accepting every value up to 4194349, the reserved gaps included, with its extended-identifier helper unreachable.
  • rb bs-20261005-57 (#3089 CR2-F02-078): BACnetObjectType::IsValidObjectType rejects 38 (network-security, removed at protocol revision 22), and a revision 20 or 21 build accepts Staging, Audit Log and Audit Reporter (added at revision 20; that branch is not built by the gate, which runs at revision 30). No caller in the stack. Its tests are now pinned to literals.
  • CR2-F02-024 was already fixed at the tip: #3143 refuses non-zero writes of Elapsed_Active_Time and Strike_Count.
  • rb bs-20261005-58 (#3089 CR2-F02-017): AddScheduleObject, AddTimerObject, AddLightingOutputObject, AddBinaryLightingOutputObject and EnsureFaultAlgorithm log a SetPropertyEnabled / SetPropertyWritable they depend on that fails (37 calls), instead of discarding the result. Return values are unchanged; nothing fails today.
  • rb bs-20261005-58 (#3089 CR2-F02-044): comments only - the Network Port MAC_Address note cites cl. 12.56.19 (it quoted an older edition’s 12.56.15, Changes_Pending in 135-2024; the IPV4 and VMAC rules it quoted are now Table 12-71.3 / 12-71.4 / 12-71.11 footnotes), and the Binary Lighting Output write dispatch no longer calls Present_Value its only writable property. The Schedule Present_Value citation the finding names was already corrected at the tip.
  • rb bs-20261005-59 (#2844 CR2-F22-009): comments only - Trend Log Multiple cites its own Start_Time / Stop_Time clauses (12.30.9 / 12.30.10; it quoted Trend Log’s 12.25.6 / 12.25.7), and the Trend Log and Trend Log Multiple Enable notes say that in 135-2024 only log-status records with BUFFER_PURGED set are recorded whatever Enable is.
  • rb bs-20261005-6: CR2-F13-022 and CR2-F13-024 are ticked as verified at the tip.
    • 022 was done in #2952: BACnetPacket::IsRefusedWhenInitiationDisabled exempts I-Am/I-Have replies and both AuditNotifications.
    • 024 was done in #3288: COMMUNICATION_DISABLED is now limited to the backup/restore states under DISABLE.
  • Left on #2691:
    • 006, 009 and 030 touch the frozen headers.
    • 019 and 031 are owner questions.
    • The rest of 023 is left: expiry is still checked lazily, on traffic, never from Loop(), and the wall-clock fallback deadline is still a time_t.
  • rb bs-20261005-64 (#2850 CR2-F28-007, CR2-F28-025): the Schedule and Timer engine comments cite the right 135-2024 clauses (Effective_Period 12.24.6 through Priority_For_Writing 12.24.11, array index 0 12.1.5.1, Update_Time 12.57.12, command prioritization 19.2.1). CR2-F28-009: tests now pin Exception_Schedule priority order independent of list order, and the equal-priority lowest-index rule.
  • rb bs-20261005-66 (#2688 CR2-F10-008): ReadPropertyMultiple, its response-size estimate and the Group object’s Present_Value expand ALL / REQUIRED / OPTIONAL through one helper (BACnetDBDevice::ExpandWildcardPropertyReference). The estimate and the Group expansion also dropped Property_List for OPTIONAL; no default profile lists Property_List as optional, so nothing observable changes today.
  • rb bs-20261005-66 (#2850): BACnetDateTime::SetUnspecified() sets the unspecified datetime; the Averaging, Load Control and Timer objects’ own copies of that helper are removed.
  • rb bs-20261005-70 (#2850 follow-up): BACnetSCHubConnection::GetUnspecifiedDateTime() is built with BACnetDateTime::SetUnspecified(), the one definition of the unspecified datetime.
  • rb d-20261008-1 (#3412): BACnetStack_SetBACnetSCCompatibilityFlags now documents the third flag, 0x04 SEND_CONNECT_WITHOUT_HELLO (#3398). It is send-side only: the Connect-Request and Connect-Accept this device sends omit the Hello destination option. Documentation only. IFC-084.
  • rb d-20261008-1 (#3126): the connectTimeout argument of BACnetStack_SetBACnetSCSettings was documented as a TCP connect timeout. It is now documented as what the stack uses it for: the AB.6.2 connect wait timeout (5 to 300 seconds), also served as SC_Connect_Wait_Timeout. Documentation only. IFC-084.
  • rb d-20261008-1 (#3434): the CASBACnetStackDLL.h docs now state three current behaviours. Documentation only. IFC-084.
    • BACnetStack_SetBACnetSCSettings: below 1600, maximumBVLCLength is refused; above 6144 it is saturated to 6144. maximumNPDULength accepts 1497 to 61327.
    • An unserved Network Port IP_DNS_Server reads [X'00000000'].
    • FD_Subscription_Lifetime answers the stack’s registration time-to-live, or 0. It no longer returns VALUE_NOT_INITIALIZED.
  • rb d-20261008-1 (#3355): the NM-SCCM certificate procedure docs now describe the #2560 write order. For AddIssuer and ReplaceOperational that is AtomicWriteFile, then File_Size := length. The docs also give each procedure’s step order and what a step-6 failure means. Documentation only. IFC-084.
  • rb d-20261008-1 (#3521): the \return of BACnetStack_SetBACnetSCCertificateFileObjects now says the call refuses one File object in both issuer slots (owner ruling on #2559). Documentation only. IFC-084.
  • rb d-20261008-2 (#2866): ReinitializeDevice now runs its pending Network Port check and activation through the routine the new export shares (BACnetDBDevice::CheckPendingNetworkPortActivation / ApplyPendingNetworkPortActivation). The checks, their order and the error responses are unchanged.
  • rb d-20261008-3 (#2353): VALIDATE_CHANGES on an IPv4, Ethernet, MS/TP or virtual Network Port is now supported (it answered PROPERTY / OPTIONAL_FUNCTIONALITY_NOT_SUPPORTED), as cl. 12.56.16 requires of a port with pending changes: “If the device supports pending changes then the port shall support this command.” Command_Validation_Result is now present on, and listed in the Property_List of, every Network Port object.
  • rb d-20261015-1 (#2032): the customer header’s BACnetStackSetting and BACnetStack_SetBACnetStackSetting docs no longer refer to the removed SetDABHeartbeatBroadcast, and say the Device_Address_Binding resolver does not proxy I-Ams for other devices: a BACnet/SC Network Port stores Device_Address_Proxy_Enable, but no proxy acts on it (IFC-106). Documentation only.
  • rb e-20260926-162 (#2992, owner decision): behaviour change - the default Segmentation_Supported is now SEGMENTED_BOTH (was NO_SEGMENTATION) in every build with STACK_OPTION_SEGMENTATION; a build without it stays NO_SEGMENTATION. Every device that does not call SetSegmentationEnabled now reports SEGMENTED_BOTH in its I-Am and Segmentation_Supported, exposes Max_Segments_Accepted (16) and APDU_Segment_Timeout (5000 ms), accepts segmented requests and ComplexACKs, advertises segmented-response-accepted on its own requests, and answers with a segmented ComplexACK when a response exceeds the client’s max APDU. A default device keeps sending a request larger than one APDU segmented.
  • rb e-20260926-162 (#2992): a device set to NO_SEGMENTATION or SEGMENTED_RECEIVE can no longer send a Confirmed-Request larger than one APDU. The call returns false, logs why, sends nothing, and reports HookAbort with APDU_TOO_LONG (cl. 12.11.19). This applies to test-tool sends too. SEGMENTED_BOTH and SEGMENTED_TRANSMIT devices still segment it.
  • rb e-20260926-163 (#2912): a Notification Class object’s own Notification_Class property (optional, 135-2024 cl. 12.21 “O1,2”), when the host enables it, now reads as the object’s own instance rather than 4194303. The object reports its own faults through itself (owner decision, 2026-09-26). It follows the object if its Object_Identifier is renamed. There is no new setter and no header change.
  • rb e-20260929-185 (#3344): not changed, now documented. A frame this node sends to its hub still omits the Originating Virtual Address. 135-2024 AB.4.2.1 says frames forwarded to the hub connector “shall include both” VMACs, but the conformance tests for this send, 135.1-2023 12.5.1.1.5 and 12.5.1.1.7, expect “‘Originating Virtual Address’ absent”. The frame interoperates as sent: AB.3.1.2 says “If absent, the message originated at the connection peer node”, and the hub adds the address when it forwards the frame (AB.5.3.2 and AB.5.3.3). The code now records this reasoning where the address is removed.
  • rb e-20260930-193 (#3385): the test-tool Initialize-Routing-Table-Ack hook (BACnetStackTestTool_RegisterHookInitializeRoutingTableAck, Node RegisterHookInitializeRoutingTableAck) gains a hasRoutingTableData argument after portsCount (IFC-068). It is false for the Ack “without data” that acknowledges a table update (cl. 6.6.3.9) and true for a query’s answer (cl. 6.4.8), which can itself be an empty table. Both reached the hook as portsCount 0 before, so a caller could not tell them apart. Test-tool callers of this hook must add the parameter.
  • rb e-20261001-211 (#3461, IFC-072, comments only): the test-tool headers cite the audit notifications as cl. 13.20 (ConfirmedAuditNotification) and cl. 13.21 (UnconfirmedAuditNotification), not cl. 13.13.x (LifeSafetyOperation).
  • rb e-20261001-211 (#2609 CR2-F05-036/043, #3461; docs and hygiene): BACnetComplexACKPDU::SetApduMemoryBuffer documents that it does not set the service-ack choice or validity; references to a non-existent CR-F05-040 note removed; the UnconfirmedRequest minimum-size doc, the SegmentACK enum title and diagram, a dead NOREF and an unused local fixed; SegmentACK Encode uses the bit-offset enum; “move follows flaag” log text corrected; the segmented-request accumulator documents why it may use the enum service choice.
  • rb e-20261001-212 (#2609 CR2-F05-031, CR2-F05-037; refactor): BACnetConfirmedRequestPDU::Decode and DecodeUsingSupportedServices share one body (DecodeHeaderAndServiceRequest), and BACnetUnconfirmedRequestPDU’s two entry points share their post-header body (DecodeServiceRequest), so a decode fix can no longer land in one entry point only. The service-choice switches in the three clone-by-reference factories are removed (CR2-F05-053).
  • rb e-20261003-213 (#2608 CR2-F03-047, #3454): DATE and TIME ToString, and so the _text of a Date or Time in DecodeAs JSON and XML output, changed: day 32 renders “Last day of month” (was “32”); an X’FF’ (unspecified) Time field renders “” (an all-wildcard time is “::.*”, was “255:255:255.255”); an out-of-range Date renders “Out of range: 0/0/1900, weekday 0” and an out-of-range Time is prefixed “Out of range: “, and both add an outOfRange attribute. No existing test golden string changed.
  • rb e-20261003-214 (#1637 §6, part 4): every parameter of the four COV-notification and two PrivateTransfer test-tool send exports is documented. Those are BACnetStackTestTool_Send{Confirmed,Unconfirmed}COVNotification, BACnetStackTestTool_Send{Confirmed,Unconfirmed}COVNotificationMultiple and BACnetStackTestTool_Send{Unconfirmed,Confirmed}PrivateTransfer: 78 parameters, none documented before. Before, the five after the first had no Doxygen block.
    • Each \return names when the call fails; each \param states the inputs that fail it (an out-of-range object or device identifier, an overstated count, an undecodable value or service-parameters buffer, a broadcast COV notification with a non-zero process identifier).
    • The banner above the PrivateTransfer pair cited cl. 16.11 / 15.8; it now cites cl. 16.2 / 16.3.
    • Documentation only; IFC-073.
  • rb e-20261003-215 (#3470 item 3): BACnetStackTestTool_Send{Confirmed,Unconfirmed}COVNotificationMultiple refuse an object type above 1023 or an instance above 4194303 in a notifications entry header when the header is read, as the single forms refuse it, rather than when the finished request fails to encode. The result is the same (false, nothing sent). They also now check the return of the value and notification inserts and free what they built on failure; no buffer content reaches those failures today. IFC-074.
  • rb e-20261003-216 (#3472 item 1): BACnetStackTestTool_SetAccessZonePassbackMode refuses a passbackMode above 2 (not a BACnetAccessPassbackMode, cl. 12.32.21) and keeps the stored mode. It used to store the value, after which every read of Passback_Mode aborted. IFC-075.
  • rb e-20261003-216 (#3472 item 2): BACnetStackTestTool_AccessZoneEnter / AccessZoneExit refuse an out-of-range credential object type or instance, or an out-of-range credential device instance when useCredentialDeviceIdentifier is TRUE, and change nothing. Such a reference used to be stored keyed (0, 0), so every out-of-range credential was the same credential in Credentials_In_Zone and the passback record. IFC-075.
  • rb e-20261003-216 (#3472 item 3): BACnetStackTestTool_AccessZoneEnter / AccessZoneExit refuse a credential time that is not an actual date and time (an unspecified octet, a month or day pattern, an out-of-range octet, or a day the month does not have), because Last_Credential_Added_Time / _Removed_Time convey an actual time (cl. 12.32.18, 12.32.20, 20.2.12, 20.2.13). A read of the time property used to abort after such a call. IFC-075.
  • rb e-20261003-217 (#3474 item 1): BACnetStackTestTool_SetAccessPointZoneTo / SetAccessPointZoneFrom refuse an instance above 4194303, and the zone the other property already references (cl. 12.31.35 / .36: Zone_To “shall not reference the same Access Zone object as the Zone_From property”); both may still be 4194303. An out-of-range zone used to be stored at instance 0, so a read aborted and access requests acted on Access Zone 0. IFC-076.
  • rb e-20261003-217 (#3474 item 2): BACnetStackTestTool_SetAccessPointAccessDoors refuses a list with an out-of-range entry or an entry that is not an Access Door (cl. 12.31.32), and keeps the stored list. Such an entry used to command Access Door 0, or the Access Door sharing its instance. IFC-076.
  • rb e-20261003-217 (#3474 item 4): BACnetStackTestTool_AccessPointRequestAccess refuses, and does nothing for, a request whose time is not an actual date and time - the check AccessZoneEnter / AccessZoneExit apply (#3472). Occupancy_Count_Adjust used to hand the unchecked time to the zones. IFC-076.
  • rb e-20261003-217 (#3474 item 5): the Node addon’s AccessZoneEnter, AccessZoneExit, IsAccessZonePassbackViolation, EvaluateAccessRights, SetAccessRights*AccessRules, AccessPointAuthenticateFactor and AccessPointRequestAccess throw a TypeError for an object type, time octet or rule element outside its C parameter’s range (and for rule arrays of different lengths), where a (uint16_t) / (uint8_t) cast used to wrap it silently - 65568 became 32. New node-adapter-selftest leg access-argument-width-guards. IFC-076.
  • rb e-20261003-218 (#3476 item 1): BACnetStackTestTool_SetAccessCredentialAssignedAccessRights refuses an instance above 4194303 and keeps the stored array; such an entry used to be stored as Access Rights 0, which access requests then evaluated. IFC-077.
  • rb e-20261003-218 (#3476 item 2): BACnetStackTestTool_SetAccessCredentialBelongsTo refuses an out-of-range reference and any object type but Access User (cl. 12.35.17); BACnetStackTestTool_UseAccessCredential refuses an out-of-range access point and anything but an Access Point other than the 4194303 “never used” instance (cl. 12.35.19). IFC-077.
  • rb e-20261003-218 (#3476 item 3): BACnetStackTestTool_SetAccessCredentialActivationTime / ExpirationTime accept X’FF’ in every octet (‘start of time’ / ‘end-of-time’, cl. 12.35.11 / .12) or an actual date and time, and BACnetStackTestTool_UseAccessCredential only an actual date and time; anything else used to be stored and never took effect. IFC-077.
  • rb e-20261003-218 (#3476 item 4): BACnetStackTestTool_SetAccessRightsNegativeAccessRules / PositiveAccessRules refuse a specifier other than 0 / 1 and an out-of-range SPECIFIED reference, and store SPECIFIED with the 4194303 unspecified reference as cl. 12.34.9.1 allows (served by a read, evaluated to FALSE); such rules used to be stored unservable. IFC-077.
  • rb e-20261003-218 (#3476 item 6): BACnetStackTestTool_EvaluateAccessRights builds the location of authentication with accessPointDeviceInstance (another device’s Access Point no longer matches a rule naming this device’s) and refuses an out-of-range location; its date and time remain unused. IFC-077.
  • rb e-20261003-218 (#3476 item 7): BACnetStackTestTool_AccessPointAuthenticateFactor refuses, and does nothing for, a time that is not an actual date and time, as AccessPointRequestAccess does (#3474). IFC-077.
  • rb e-20261003-219 (#3480 item 1): BACnetStackTestTool_SetEventLogStartStopTime checks both halves before storing either; a refused Stop_Time used to leave the new Start_Time in place. IFC-078.
  • rb e-20261003-219 (#3480 item 2): BACnetStackTestTool_InsertNotificationLogRecord / InsertNotificationLogRecordWithEventValues refuse a Notify Type above 2 (cl. 21 BACnetNotifyType has no proprietary range); the Node wrappers read it at full width. IFC-078.
  • rb e-20261003-219 (#3480 item 4): a notification-log record whose supplied time stamp is a pattern, X’FF’ or a day the month does not have is stamped with the stack’s clock (CallbackGetSystemTime) in the device’s local time, as captured notifications are; it used to be the host’s clock in UTC. An actual supplied time still stamps the record. IFC-078.
  • rb e-20261003-219 (#3480 item 5): the inserts refuse a UCS-2 Message Text with an odd number of octets and a UCS-4 one that is not a multiple of 4 (cl. 20.2.9). IFC-078.
  • rb e-20261003-219 (#3480 item 6): BACnetStackTestTool_InsertNotificationLogRecordWithEventValues encodes boolean-value [0] instead of refusing it, refuses an extended-value tag / value whose sum does not fit Unsigned32 or whose value is 100000 or more (it used to wrap or alias), and range-checks Boolean, binary-value and notify-type values. IFC-078.
  • rb e-20261003-220 (#3482 items 1-2): the six BACnetStackTestTool_AddEventEnrollmentObject* exports refuse a Notify_Type above 1 (cl. 12.12.6, as WriteProperty already did) and a monitored property identifier above 4194349, the highest 135-2024 assigns; the Node ChangeOfValue wrapper reads monitoredObjectType and notifyType at full width. IFC-079.
  • rb e-20261003-220 (#3482 item 3): the OUT_OF_RANGE Event Enrollment adder refuses a low limit above the high limit, a negative deadband and FALSE limit-enable flags (Event_Parameters cannot express them); the adders now serve Time_Delay_Normal (cl. 12.12.19) when they are given a pTimeDelayNormal. IFC-079.
  • rb e-20261003-220 (#3482 item 4): an Event Enrollment adder call that returns false no longer changes an existing enrollment (Event_Type, algorithm). IFC-079.
  • rb e-20261003-220 (#3482 item 5): the Unsigned CHANGE_OF_STATE adder refuses alarm value 0 for a Multi-state Present_Value (cl. 12.18.4 / 12.19.4 / 12.20.4). IFC-079.
  • rb e-20261003-220 (#3482 item 7): BACnetStackTestTool_SendConfirmedEventNotificationWithEventValues / SendUnconfirmedEventNotificationWithEventValues refuse an extended-value whose value is 100000 or more or whose sum does not fit Unsigned32 (it used to be sent wrapped), and encode boolean-value [0] instead of refusing it; the check lives in the shared builder, so the Event Log insert uses the same one. IFC-079.
  • rb e-20261003-221 (#3483 items 1-3): BACnetStackTestTool_SetFaultStateAlgorithm and BACnetStackTestTool_SetFaultCharacterstringAlgorithm check every argument before they replace an Event Enrollment’s fault algorithm or an armed list (a FAULT_CHARACTERSTRING value must be well-formed UTF-8); BACnetStackTestTool_SetEventEnrollmentFaultStatePropertyStatesType, and a WriteProperty of Fault_Parameters, refuse a BACnetPropertyStates choice Fault_Parameters cannot serve. IFC-080.
  • rb e-20261003-221 (#3483 items 4-5): BACnetStackTestTool_SetFaultExtendedAlgorithm refuses a faultReliability that is not a fault reliability (0, 11, 26..63) while a fault is detected; the seven exports that name an existing Event Enrollment by instance refuse an instance above 4194303 instead of folding it to 22 bits. IFC-080.
  • rb e-20261003-221 (#3483 items 6-7): BACnetStackTestTool_SetIntrinsicAccessEventAlgorithm refuses a NULL access-event list with a count and an out-of-range access-credential reference, and an ACCESS_EVENT Event Enrollment no longer serves Time_Delay_Normal (cl. 13.3.12 has no pTimeDelayNormal); BACnetStackTestTool_SetAccessEventContext requires an actual date and time and now accepts an Event Enrollment that applies ACCESS_EVENT. IFC-080.
  • rb e-20261008-3 (#3669, CR3-F09-029): the BACnet/SC Network Port’s Max_NPDU_Length_Accepted could be set up to 61327, ten times the largest NPDU the port can receive. 135-2024 cl. 12.56.79: “This property, of type Unsigned, indicates the maximum size in octets of the NPDU message the network port can receive and process.” Every BACnet/SC frame is received into a 6144-octet buffer, and a broadcast relayed by a hub carries a 16-octet BVLC header (AB.5.3.3: the hub adds the ‘Originating Virtual Address’ and the ‘Destination Virtual Address’ remains), so the largest NPDU sure to fit, without header options, is 6128. A WriteProperty of Max_NPDU_Length_Accepted above 6128 now answers PROPERTY / VALUE_OUT_OF_RANGE, as Max_BVLC_Length_Accepted above 6144 does (#2572). BACnetStack_SetBACnetSCSettings still accepts a maximumNPDULength of 1497 to 61327, but a value above 6128 is now saturated to 6128 and logged; the call still succeeds. The value advertised to peers and Max_NPDU_Length_Accepted then read 6128. IFC-085.
  • rb g-20260924-20 (#2648): a You-Are whose Device Identifier is refused is now refused as a whole. Previously an acceptable Device MAC Address in the same request was still applied, and an I-Am went out under the old instance. Integrators that send You-Are to the stack should expect no I-Am in reply to a refused request. The “become unconfigured” behaviour for 4194303 is not implemented; see #2649. Other cached copies of the old instance (data-link port owners, the notification forwarder, the host) are tracked as #2702.
  • rb g-20260924-21 (#2621): ReadPropertyMultiple ALL / REQUIRED on a Trend Log now returns a Log_Buffer error entry, as Event Log and Trend Log Multiple already did, instead of silently leaving the property out.
  • rb g-20260924-21 (#2629): a ReadRange By Time that matches no record returns an empty result with no First Sequence Number (context tag 6), instead of First Sequence Number = 0. This applies to Trend Log, Event Log and Trend Log Multiple. An ACK the stack builds with Item Count 0 never carries First Sequence Number: BACnetReadRangeACK::SetFirstSequenceNumber refuses it while the list is empty, SetItemCount(0) clears it, and the encoder omits it. A decoded peer ACK still reports tag 6 exactly as it was sent, so the test tool can see a peer that breaks the rule.
  • rb g-20260924-23 (#2732): integrators receiving CHANGE_OF_RELIABILITY from the five fault algorithms above now get Event Values (status-flags plus property values) where before they got none.
  • rb g-20260924-25 (#2740): the new-value of a change-of-discrete-value notification now accepts only the application-tagged CHOICE arms (plus the context-tagged datetime). A peer that sends Null, Real, Double, Bit String or a reserved application tag in that field now gets Reject(invalid-tag), where before the notification decoded. SetPrimitiveAndTakeControl refuses a primitive outside the CHOICE; when it returns false, the caller keeps ownership.
  • rb g-20260924-28 (#2674): a LifeSafetyOperation whose Requesting Source is 257 to 300 octets is now answered Abort(other) instead of being silently truncated. See #2880.
  • rb g-20260925-2 (#2664): an 11th COV-multiple subscriber to the same object and property (FULL build; 5 or 2 in the smaller profiles) is now refused rather than accepted.
  • rb g-20260925-3 (#2412): order of host callbacks during ReinitializeDevice.
    • Hosts now see the old Network_Number and BACnet/SC settings inside CallbackReinitializeDevice; the pending ones are applied afterwards.
    • CallbackSetPropertyUInt for a pending Network_Number is now called after CallbackReinitializeDevice returns true, not before.
    • If the host refuses that value, the request is answered PROPERTY / INVALID_CONFIGURATION_DATA and every change stays pending, even though the reinitialize callback returned true.
    • The DLL header’s “On a true return the stack sends the SimpleAck” is no longer always true. That frozen text needs an owner-reviewed update.
  • Known remaining bypass: the DeviceCommunicationControl override still runs before the password callback (#2985, owner question).
  • rb g-20260925-6 (#2497): when the transaction table is full:
    • unconfirmed requests (for example Who-Is) are processed and answered;
    • broadcast Confirmed-Requests are dropped silently;
    • server-role PDUs are ignored;
    • only a unicast Confirmed-Request, a client SegmentACK or a packet with no APDU still gets Abort(out-of-resources).
  • rb g-20260925-6 (#2499): hosts that relied on the old, far longer effective request timeout will now see requests time out after APDU_Timeout.
  • rb g-20260926-1 (#2677): with no LifeSafetyOperation callback registered, a request for NONE (0) or for a proprietary operation (64-255) is answered Error(OBJECT, VALUE_OUT_OF_RANGE) instead of SimpleACK. With a callback registered, the host still decides.
  • rb g-20260926-1 (#2679): an EventNotification whose Priority is above 255 is now rejected (Reject PARAMETER_OUT_OF_RANGE); it used to be truncated silently.
  • rb g-20260926-1 (#2678): a confirmed EventNotification whose ACK_NOTIFICATION carries AckRequired, From State or Event Values is now rejected (Reject INCONSISTENT_PARAMETERS). The issue suggested INVALID_PARAMETER_DATA_TYPE and left the choice to the owner. The unconfirmed form is still decoded exactly as sent, so the test tool can see it; it is flagged invalid and logged. Its local processing is tracked in #3014.
  • rb g-20260926-1 (#2682): AcknowledgeAlarm on an object, or an Event Enrollment, whose Event_Detection_Enable is FALSE now returns Error(OBJECT, NO_ALARM_CONFIGURED).
  • rb g-20260926-1 (#2683): acknowledging a transition that never occurred (its time stamp is still all-FF) now returns Error(SERVICES, INVALID_TIMESTAMP). The ACK_NOTIFICATION’s Notification Class now comes from the object’s current Notification_Class.
  • rb g-20260926-1 (#2685): no ACK_NOTIFICATION is sent or offered locally when that transition’s Event_Enable bit is FALSE. The SimpleACK and the Acked_Transitions bit are unchanged. A transition that has been disabled since boot still cannot be acknowledged (#3013).
  • rb g-20260926-3 (#2713): between an Event_Algorithm_Inhibit TRUE write and the next Tick, Event_State still reads the previous state. The same window already applies to Reliability_Evaluation_Inhibit.
  • rb g-20261001-11 (#2427, item 1): Node behaviour change. A RegisterCallbackSetPropertyBool handler now receives value as a JavaScript boolean (true/false), matching the C typedef (const bool) and the value: boolean that api.ts already declared. Before, it received the Number 0 or 1. A handler that compares value === 1 must change to value === true. The registration is now generated by ci_scripts/generate-napi-callbacks.js. Revert-red: with Napi::Number restored, generate-napi-callbacks.js --gate and generate-node-api-params.js --gate both fail.
  • rb g-20261001-11 (#2427, item 2): Node behaviour change. RegisterCallbackSetSystemTime and RegisterCallbackGetListOfEnumerations now honour the handler’s return value. Before, the stack was always told the call succeeded. A handler that returns false, or throws, now fails the operation: the time synchronisation is reported as not applied, or the list read fails and enumeration/more are not copied back. Any other returned value, including none (undefined), still counts as success (owner ruling, 2026-09-25). The GetListOfEnumerations out-buffers now start from the stack’s own values rather than uninitialised memory, so a handler that succeeds without writing more no longer hands back garbage. Evidence: dist/callback-results.self-test.js run against a built addon. With the trampolines reverted, the SetSystemTime false case fails, and the GetListOfEnumerations false case loops until killed. With only the seeding reverted, an empty read returns a garbage element (ack 3e92e48f3f, expected ack 3e3f).
  • rb g-20261001-11 (#2427): the return rules now differ by registration. The generated SetPropertyBool trampoline, like every generated Get/Set property trampoline, treats any non-boolean return as failure. SetSystemTime and GetListOfEnumerations treat a returned undefined as success, as the ruling allows for those two.
  • rb g-20261001-11 (#2427, item 4): in api.ts the callback parameter of RegisterCallbackInvokeIdGenerator, RegisterCallbackFSMTransition and RegisterCallbackProgramChangeRequested (all three are test-tool exports, declared in CASBACnetStackTestToolDLL.h) is now optional (callback?:). Omitting it unregisters, as null does, which the addon already accepted. This changes the TypeScript type only, with no runtime change. Revert-red: with api.ts reverted, generate-node-api-params.js --gate fails.
  • rb g-20261005-2 (#2844, CR2-F22-030): removed BACnetLogRecord::m_logDatumType, a private member that was never initialised, set or read. No behaviour change.
  • rb g-20261005-3 (#2608, CR2-F03-007): the internal BACnetStackDatatypes enumerators OptionalREAL (which shadowed OptionalReal) and BACnetLogMultipleRecord (which duplicated LogMultipleRecord) are renamed to UnusedLegacyOptionalREAL and UnusedLegacyLogMultipleRecord. Nothing used either one. They are kept rather than deleted, so no enumerator changes value.
  • rb g-20261005-4 (#3088, CR2-F01-067): removed the internal swsutilities::_snprintf_s and swsutilities::memcpy_s helpers, and the non-Windows #define sscanf_s sscanf in BACnetStackCommon.h. They had the MSVC names but not the MSVC contracts (memcpy_s returned 1 on success, _snprintf_s returned the untruncated length). Only memcpy_s had a caller, the CharacterString encoder, which ignored the return and checks capacity itself; it now calls memcpy. No wire or API change. Code that included BACnetStackCommon.h and relied on the sscanf_s macro outside Windows must call sscanf (or a bounded parser) itself.
  • rb g-20261005-7 (#3088, CR2-F01-010): BACnetInterface.cpp no longer computes objectType / objectInstance locals it does not use in AddTrendLogObject, SetTrendLogTypeToPolled / ToCOV / ToTriggered and AddLoggedObjectToTrendLogMultiple. Their compiled-out arms NOREF the identifier parameter instead. AddElevatorGroupObject’s compiled-out arm now NOREFs its six parameters. No behaviour change. In this file, -Wunused-variable warnings under FULL + TESTTOOL go from 8 to 0, and -Wextra warnings under a narrow EMBEDDED + IPv4 preset go from 384 to 378. The other ~27 sites the review counted were already clean at the tip. Partial: most of the remaining 378 -Wunused-parameter warnings under that preset come from other compiled-out wrappers the review did not name.
  • rb g-20261005-7 (#3088, CR2-F01-058): DeliverAuditNotificationToRecipient’s doc (BACnetStackMessageGenerator.h) now says what the code does. A confirmed audit notification gets a client TSM transaction from the data link when it is sent (#927), so it is retried per Number_Of_APDU_Retries and its acknowledgement is correlated. DeviceCommunicationControl does not defer it (#2691). Every data link refusal is Dropped. Comments only.
  • rb g-20261005-7 (#3088, CR2-F01-034, -038, -041, -047, -051; IFC-083): the test-tool header and interface doc comments match the code. Stale BACnetStackTestTool_* cross-references now name the BACnetStack_* exports that exist. Blocks for functions that moved out of BACnetTestToolInterface (GetEventInformation, WriteGroup, AddChannelObject, AcknowledgeAlarm, Who-Am-I / You-Are) are now redirects. SendIHave’s \params, the Initialize-Routing-Table-Ack note (#363), the audit-notification “deferred” note, InsertNotificationLogRecord’s timestamp / eventValues text and the #1232 SendAuditLogQuery rationale are corrected. Two unused includes are dropped from BACnetTestToolInterface.h. No declaration changes.
  • rb g-20261005-7 (#3088, CR2-F01-037, -045, -050, partial; IFC-083): clause and object-type citations in the test-tool surface are corrected where the review quotes the extracted spec’s clause title. Audit Log is now 12.64, Shed_Levels / Shed_Level_Descriptions / Full_Duty_Baseline 12.28.18 / .19 / .15, Zone_Members 12.16.26, List_Of_Group_Members 12.14.5, Credential Data Input object type 37, GetEventInformation 13.12, SubscribeCOVPropertyMultiple 13.16, Staging 12.62, PrivateTransfer 16.2 / 16.3, the audit notifications 13.20 / 13.21, and the AuthRequest audience rule 17.6.2. Comments only.
  • rb g-20261005-8 (#3463 item 1): a test-tool build (BACNET_STACK_TESTTOOL) with BACNET_PROTOCOL_REVISION below 20 now stops at an #error in CASBACnetStackOptions.h that names the requirement. It used to fail with scattered errors deep in the source (maxProxiedIAmsPerSecond, revision-gated enums in the Device object and the property profile, the BACnet/SC Network Port, some audit bodies). The test-tool build requires revision 20 or later; the default is 30. Customer builds (no BACNET_STACK_TESTTOOL) are not affected by the check.
  • rb g-20261013-3 (#3757 CR3-F17-014): the FLOATING_LIMIT algorithm no longer serves or accepts Low_Limit / High_Limit. No object using it has those properties since #3246 (a Loop’s band is Error_Limit and Low_Diff_Limit), and a write through them made the band asymmetric without Low_Diff_Limit becoming present. A read or write of either is now not handled by the algorithm (no error, and the host’s set-property callback is not called). No change on the wire: the business logic already answers a ReadProperty or WriteProperty of Low_Limit / High_Limit on these objects with Error PROPERTY / UNKNOWN_PROPERTY before the algorithm is reached.
  • rb g-20261013-4 (#3761 CR3-F21-010, #3762 CR3-F22-015): the unused per-translation-unit name tables (*_NAME, Create*EnumValueString(), *EnumNames) are removed from BACnetEventTransitionBits.h, BACnetLimitEnable.h and BACnetLogStatus.h, as #1258 did for BACnetStatusFlags.h. Nothing referenced them; no behaviour change.
  • rb g-20261013-7 (#3679 CR3-F07-016): a Register-Foreign-Device whose Time-to-Live cannot be read would be answered with the Register-Foreign-Device NAK, and the value starts at 0. The read cannot fail today, so there is no behaviour change on the wire.
  • rb g-20261013-7 (#3681 CR3-F09-019): the hub function’s unreachable broadcast checks for Connect-Request, Disconnect-Request and Heartbeat-Request are removed; the Virtual Address check before them already discards these. No behaviour change.
  • rb g-20261013-8 (#3676 CR3-F03-007): the unused BACnetTag::DecodeAndCheckContextTag is removed. No behaviour change.
  • rb g-20261013-8 (#3677 CR3-F05-025): BACnetSimpleACKPDU::Encode logs “Invalid simple ack pdu object” rather than “Invalid abort pdu object”. No behaviour change.
  • rb g-20261013-9 (#3714 CR3-F12-003): the RemoveListElement processor checks BACnetBusinessLogic::RemoveListElement’s result and answers Abort(other) if it failed without setting an error, as the AddListElement processor does. No behaviour change: every failure sets an error today.
  • rb g-20261013-9 (#3714 CR3-F12-007): BACnetCreateObjectError::Encode logs “Invalid BACnetCreateObjectError object” rather than “Invalid BACnetChangeListError object”. No behaviour change.
  • rb g2-20261009-2 (#3851, test tool): the test-tool BACnetStack_RegisterCallbackEventNotificationReceived (34-parameter callback) is replaced by the customer export above, and BACnetStackTestTool_RegisterCallbackInvalidEventNotificationReceived (same signature) receives the notifications that fail the parameter check, with the Event Values as captured from the wire. Node: RegisterCallbackEventNotificationReceived’s handler receives the new 37-argument order; RegisterCallbackInvalidEventNotificationReceived is new (test-tool build).
  • rb g2-20261012-1 (#1637, test tool, docs): every parameter and return of the Notification Forwarder (AddNotificationForwarderRecipient, SetNotificationForwarderProcessIdentifierFilter, SetNotificationForwarderPortFilter), Life Safety value-list (SetLifeSafetyFaultValues, SetLifeSafetyLifeSafetyAlarmValues) and Credential Data Input exports is documented in CASBACnetStackTestToolDLL.h, traced to the implementation. Corrected: SetCredentialDataInputSupportedFormats does resize Supported_Format_Classes to match (the classes export’s doc said nothing did), and the two BDT/FDT Ack hooks documented a networkType parameter that is networkPortInstance. Comments only (IFC-090).
  • rb g2-20261012-1 (#3022, test tool, docs): BACnetStack_InsertAuthorizationToken is documented: the token it seeds has no scope bits, so a lookup by scope bit (BACnetStackAuthorizationCache::FindToken) never finds it; its fixed fields, FIFO eviction and refusals. Comments only (IFC-090).
  • rb g2-20261013-3 (#1874, internal refactor, behaviour unchanged): BACnetBusinessLogic::GetProperty and SetProperty take one packed BACnetObjectIdentifier32 instead of an object type and instance pair. The 1038 call sites in 80 files are converted and no pair-taking forwarder remains; BACnetBusinessLogic.h has no pair declarations left, so its allowlist line is removed. Neither export header changes.
  • rb g2-20261013-4 (#1637, IFC-098): every test-tool export in CASBACnetStackTestToolDLL.h passes doc-lint’s rules (16 completed). Documentation only.
  • rb g2-20261013-4 (#2288): generate-node-api-params.js derives *Minus1900 parameter names, so 27 more api.ts parameter lists are byte-checked (allowlist 100 to 73). api.ts is unchanged.
  • rb g2-20261013-5 (#1637): ci_scripts/doc-lint.sh takes --header and --macro and has a --self-test; pre-push.sh now lints the test-tool header too (198 exports, all documented). The matching tests.yml step is left to the owner.
  • rb g2-20261013-5 (#2288): generate-node-api-params.js derives callback parameter types through Buffer locals, copied buffers and IsBoolean()-guarded results, and pins a | null the wrapper does not accept as nullable@k; the shape allowlist goes from 73 to 67 entries and api.ts is unchanged.
  • rb g2-20261013-6 (#3824, #3715, #3718, #3717, #3759, #3769): documentation and comment corrections from the CR3 review, with no behaviour change. Spec citations corrected and quoted (BBMD self-entry J.4.3.2, BDT self-exclusion J.4.5, Network-Number-Is cl. 6.4.20, Who-Is cl. 16.10.2, You-Are cl. 9.3, alarm summary cl. 13.10.2, array index 0 cl. 12.1.5.1, Channel Table 12-62 footnote 1), the BACnet/SC row is added to the Table 6-2 transcription, and the Table 13-1 transcription gains the Staging row. The test-tool Lighting Output docs list the priority-6 and range refusals, and BACnetInterface.h documents the no-hook TextMessage answer, the CompleteRestore return and the ReinitializeDevice error mapping (IFC-100). Commented-out COV notification copy operations are removed, and the base COV notification sequence enum is renamed BACnetCOVNotificationRequestSequenceEnum (it was named after the Unconfirmed class).
  • rb g2-20261013-6 (#3762): the spec-coverage register says the Annex AA time-series codec is source-level only, reachable from no export (DM-TSDI-A / DM-TSDE-A rows).
  • rb planner-20261015-docs-6 (#3916): The API Reference is organized by topic instead of as one alphabetical list of 245 functions.
    • There are 14 groups: version and settings, main loop, device and services, objects and properties, object configuration, alarms and events, the three kinds of callbacks, client requests, client hooks, BBMD, routing and BACnet/SC. Each group page opens with an overview of how its functions fit together.
    • The API conventions are a rendered page: return values, repeat Add* calls, threading, callbacks, errorCode and byte order. Until now they were plain comments at the top of CASBACnetStackDLL.h, which the generated reference did not show.
    • 20 functions have a clearer one-line summary.
    • ci_scripts/doc-lint.sh now requires every export to name exactly one defined topic group.
    • Documentation only; no declaration changed.
  • rb planner-20261015-docs-7 (#3918, #905): The API Reference and the generated C#, Java, Python and Rust bindings no longer carry maintainer notes in the function documentation.
    • Issue numbers, review IDs, sprint tags, dates and ruling attributions are removed from about 160 lines. Every behavioural statement is kept.
    • Two contradictions are corrected: SetCOVSettings called a still-exported function “removed”, and SendReadPropertyWithLength described a difference from SendReadProperty that does not exist.
    • FPCallbackGetPropertyDate no longer describes its date fields as time fields.
    • All 22 alarm-algorithm setters now give the same recommended value for enable.
    • The file-access callbacks now say which errorCode values become an Error (any other value answers Abort OTHER).
    • ci_scripts/doc-lint.sh rejects maintainer metadata in the customer header’s doc text.
    • Documentation only; no declaration changed.
  • rb v-20260925-2 (#1637 §5, §2): the 17 test-tool export definitions take const scalar inputs, as their declarations already did (no ABI change). BACnetStackTestTool_SendAlert is documented as undirected by design: its recipients are the Alert Enrollment’s Notification Class Recipient_List (cl. 12.52.8). See IFC-055.
  • rb v-20260925-5 (#1637 §6, part 1): every parameter of the seven EventNotification test-tool exports is documented. Those are BACnetStackTestTool_SendConfirmedEventNotification, ...SendUnconfirmedEventNotification, ...WithTimestampForm, and the confirmed and unconfirmed ...WithEventValues and ...WithOutOfRangeValues. Before, 4 of their 260 parameters were documented.
    • Each line names its 135-2024 clause (13.8.1.1.x for confirmed, 13.9.1.1.x for unconfirmed) and states what the export accepts. That includes the values that make the call return false: an ACK_NOTIFICATION carrying AckRequired, From State or Event Values; an out-of-range timestamp octet; a character set above 5.
    • Documentation only; IFC-056.
  • rb v-20260925-6 (#1637 §6, part 2): every parameter of eight test-tool exports is documented. Those are the six typed list-element exports (BACnetStackTestTool_{Add,Remove}ListElement{ReadAccessSpecifications,EventNotificationSubscriptions,Destinations}), ...SendGetAlarmSummary and ...SendGetEnrollmentSummary. Before, 10 of their 99 parameters were documented.
    • The six typed exports also gain a \return that names when the call fails: NULL refs or refCount 0, an out-of-range object identifier, an invalid entry, or a build without the family’s options.
    • SendGetEnrollmentSummary’s combined useX / x lines are split into one \param per parameter.
    • Documentation only; IFC-057.
  • rb v-20260925-7 (#1637 §6, part 3): every parameter of the six network-layer message test-tool exports is documented. Those are BACnetStackTestTool_Send{ICouldBeRouterToNetwork,RejectMessageToNetwork,RouterBusyToNetwork,RouterAvailableToNetwork,WhatIsNetworkNumber,UnknownNetworkLayerMessage}. Before, five had no Doxygen block and the sixth documented 2 of its 14 parameters.
    • Each \return names when the call fails, including the build option each export needs.
    • Documentation only; IFC-058.
  • rb bs-20260924-21 (#2473): Node adapter: RegisterHookIAmRouterToNetwork and RegisterHookICouldBeRouterToNetwork, typed in api.ts. These are napi bindings for the test-tool hooks added in #496 (IFC-052). A Node test tool can now receive I-Am-Router-To-Network and I-Could-Be-Router-To-Network answers as structured data. Pass null to unregister.
  • rb bs-20260924-22 (#2168): test tool: BACnetStackTestTool_SendUnconfirmedRequestRaw, also SendUnconfirmedRequestRaw in the Node api.ts. It sends an unconfirmed request with a caller-supplied service-choice octet and pre-encoded payload, and is the unconfirmed sibling of SendConfirmedRequestRaw. A test tool can now announce an I-Am for a device instance the stack does not hold, with its own Max APDU Length Accepted, Segmentation Supported and Vendor ID (135-2024 cl. 16.10.3). It can also send unknown service choices or malformed payloads. It takes a broadcast flag, a destination network and address, a network priority and a hop count. IFC-053.
  • rb bs-20260924-24 (#2222): test tool: BACnetStackTestTool_AddListElementDestinations and BACnetStackTestTool_RemoveListElementDestinations, also SendAddListElementDestinations / SendRemoveListElementDestinations in the Node api.ts. They send AddListElement / RemoveListElement (cl. 15.1 / 15.2) whose elements are BACnetDestination records, the element type of a Notification Class object’s Recipient_List. A client can now add or remove itself as an event recipient without rewriting the whole list, and without hand-encoding the element as the CAS BACnet Explorer had to. An invalid element (no valid day, a time out of range, an invalid recipient) is refused and nothing is sent. IFC-054.
  • rb bs-20260924-7 (#2455, second part of #2304): the Audit Reporter object model (135-2024 cl. 12.63, Table 12-82). Test-tool builds with STACK_OPTION_OBJECT_TYPE_AUDIT_REPORTER only.
    • Auditable_Operations (cl. 12.63.10), with IsOperationAuditable. The default reports every operation except READ, NOTIFICATION and SUBSCRIPTION, the values the clause requires when the property is not configurable. ForwardAuditEvent does not apply it itself. Cl. 19.6.3 gives an object’s own Auditable_Operations precedence and always reports a change to one, so the notification generator (#2456, #2457) applies the rule. Proprietary operations (32-63) have no flag.
    • Monitored_Objects (cl. 12.63.13) selects objects by identifier or by type; Null entries are ignored. BACnetDBDevice::GetAuditReporterForObject applies the clause’s rule: “only the one with the lowest object instance shall generate notifications for the object” (enabled reporters only).
    • Audit_Source_Reporter (cl. 12.63.9) is held, and BACnetDBDevice::GetAuditSourceReporter returns that reporter.
    • Maximum_Send_Delay (cl. 12.63.14, 0 to 3600) is held. Send_Now (cl. 12.63.15) resets to FALSE once nothing is waiting. Notifications are never delayed on purpose.
    • A stack-held Audit Reporter (AddAuditReporter) now serves these properties, plus Audit_Level, Audit_Priority_Filter and Issue_Confirmed_Notifications, through the Device’s generated-property path. A ReadProperty reaches it once the object is in the object list and the property is enabled. For such an object the stack’s value takes precedence over the host callback’s. Audit_Priority_Filter now defaults to a 16-bit, all-clear BACnetPriorityFilter rather than an empty bit string.
  • rb bs-20260924-8 (#2456, first slice; the rest is #2616): the stack now generates operation-target audit notifications for WRITE (135-2024 cl. 19.6.1 (a), Table 19-5). A successful WriteProperty, and each target of a WritePropertyMultiple (“each target shall be reported in a separate notification”, cl. 19.6.5), queues one notification on the Audit Reporter responsible for the object (lowest instance, cl. 12.63.13). Test-tool builds with STACK_OPTION_OBJECT_TYPE_AUDIT_REPORTER only.
    • Rules (cl. 19.6.3, Table 19-3):
      • A reporter at NONE reports nothing but Audit_Level changes, whatever the object’s own level. That includes a change to an object’s own Audit_Level, not only the reporter’s: cl. 19.6.3 also says changes to Audit_Level “shall always result in an audit notification”.
      • Otherwise the object’s own Audit_Level applies unless it is absent or DEFAULT, in which case the reporter’s applies.
      • A write to an Audit Reporter object is reported by that reporter whenever its level is not NONE (cl. 19.6.2), whatever its Monitored_Objects, Auditable_Operations or Audit_Priority_Filter.
      • NONE reports nothing, except a change to Audit_Level itself, which is always reported.
      • AUDIT_CONFIG treats every property except Present_Value as configuration. The table leaves this “a local matter”.
      • The object’s own Auditable_Operations applies, or else the reporter’s. A change to Auditable_Operations is always reported unless the level is NONE.
      • Audit_Priority_Filter applies to commandable-property writes only: the object’s own, or else the reporter’s. An object filter that is Null or shorter than 16 bits falls back to the reporter’s. An unprioritised command is priority 16.
    • Table 19-4 fields:
      • Target Timestamp, when the host supplies the time.
      • Source Device: the requester’s device identifier when the Device Address Binding knows its address (“If known, the BACnetObjectIdentifier form shall be provided”), otherwise its address. For a local write it is this device.
      • Operation WRITE.
      • Invoke Id, left out when the source is this device.
      • Target Device, Object and Property.
      • Target Priority, for commandable writes.
      • Target Value.
    • Local choices (cl. 19.6.1): failed writes are not reported. In a WritePropertyMultiple that fails part-way, the targets already written are reported. Current Value is omitted, because the property is not read before the write. A priority given on a non-commandable write is ignored by the write, so it is not reported either.
    • New: BACnetDBDevice::ReportAuditTargetWrite and BACnetStackAuditReporter::EnqueueNotification. The latter queues a notification whose rules the caller has already applied.
    • Only the Color Temperature and Audit Log profiles carry per-object Audit_Level, Auditable_Operations and Audit_Priority_Filter today. Other objects use the reporter’s settings (#2616).
  • rb bs-20260924-9 (#2616, first slice): operation-target audit notifications for CreateObject (CREATE), DeleteObject (DELETE) and AtomicWriteFile (WRITE), following WriteProperty/WritePropertyMultiple (#2456). Test-tool builds with STACK_OPTION_OBJECT_TYPE_AUDIT_REPORTER only.
    • The rules and fields are as in #2456, from the new BACnetDBDevice::ReportAuditTargetOperation; ReportAuditTargetWrite is now a wrapper for it.
    • None of the three carries a Target Property: Table 19-4 says “This field shall be absent if the target is not a property”.
      • CREATE: “Initial values are not provided in a CREATE operation audit record”. The target is the object the CreateObject-ACK names.
      • DELETE: the object is already gone when the notification is built, so the reporter’s Audit_Level and Auditable_Operations apply, not the object’s own.
      • AtomicWriteFile: “no ‘Target Property’, ‘Target Value’, or ‘Current Value’ values are provided”.
    • Deleting an Audit Reporter object is reported by the reporter that monitors Audit Reporters. The cl. 19.6.2 self-reporting rule covers writes only. A CREATE of an Audit Reporter adds no stack-held reporter (CreateObject does not call AddAuditReporter).
    • Local choices: AUDIT_CONFIG reports CREATE, DELETE and file writes (only Present_Value is excluded). An operation that succeeds but whose acknowledgement cannot be allocated aborts, and is not reported.
    • Still in #2616: AddListElement and RemoveListElement, WriteGroup, the other Table 19-5 operations, local writes (cl. 19.6.1 (b)), the recipient-change notification, and per-object audit properties on more objects.
  • rb bs-20261005-33 (#2848 CR2-F26-007): BACnetLandingCallStatus::CreateWithDirection and CreateWithDestination pick the command CHOICE arm by name. The two constructors pick it by the integer width of the third argument (uint32_t = direction, uint8_t = destination), so a destination held in a wider integer silently built a direction. CreateWithDestination takes the destination as a uint32_t and returns NULL for one above 255 (Unsigned8) rather than truncating it to a different floor; either factory returns NULL on an allocation failure and an invalid object on a setter failure, as the constructors do. The constructors are kept; the Elevator Group Landing_Calls and Landing_Call_Control reads now build their elements through the factories.
  • rb bs-20261005-55 (#3089 CR2-F02-049, CR2-F02-066): the Schedule profile has Write_Every_Scheduled_Action (Boolean, optional, cl. 12.24.25) and the Network Port profile Additional_Reference_Ports (BACnetLIST of Unsigned, optional, Table 12-71), so an application can enable them. The schedule engine does not yet act on Write_Every_Scheduled_Action TRUE. Once enabled, Additional_Reference_Ports reads as an empty list - there is no host callback for a list of Unsigned (an empty list is valid, cl. 12.56.12) - and its O2 rule (present only with Reference_Port) is not enforced.
  • CR2-F02-046 was already fixed at the tip: Analog Output and Analog Value profile Profile_Location / Profile_Name as scalar CharacterString.
  • rb d-20261008-2 (#2866): new export BACnetStack_ActivateNetworkPortPendingChanges(deviceInstance, networkPortInstance). It activates the pending changes of one Network Port object, or of every Network Port object of the device when networkPortInstance is 4194303, as a received ReinitializeDevice ACTIVATE_CHANGES does: the same admissibility check and activation routine (#2412), but without CallbackReinitializeDevice and so without a password check, because the caller is the local host. Call it after start-up configuration such as BACnetStack_SetBACnetSCCertificateFileObjects, so the device does not boot with Changes_Pending TRUE. It returns false, with nothing activated, for an unknown device or Network Port, a pending value that is not admissible, or a value the host refuses at activation. Bound in the six generated adapters; Node also has a napi wrapper and a typed api.ts function, ActivateNetworkPortPendingChanges. IFC-086.
  • rb d-20261008-3 (#2353): new export BACnetStack_RegisterCallbackValidateNetworkPortChanges and callback FPCallbackValidateNetworkPortChanges(deviceInstance, networkPortInstance, failedPropertyIdentifier, errorClass, errorCode) -> bool. VALIDATE_CHANGES written to a Network Port’s Command property (135-2024 cl. 12.56.16) now runs the stack’s own checks of the port’s pending values (the admissibility checks ReinitializeDevice ACTIVATE_CHANGES runs), then the host’s callback when registered - on a BACnet/SC port that is where the operational certificate is validated (cl. 12.56.100). The outcome is recorded in Command_Validation_Result (cl. 12.56.18) and the write answers Result(+); Changes_Pending and the pending values do not change. A BACnet/SC port with no callback still answers PROPERTY / OPTIONAL_FUNCTIONALITY_NOT_SUPPORTED. Bound in the six generated adapters; Node also has a napi trampoline and a typed api.ts function, RegisterCallbackValidateNetworkPortChanges. IFC-087.
  • docs (#2887): BACnet wiki batch B0.5, the shared properties. It adds pages for the 26 Global Group properties that had none: status (Status_Flags, Event_State, Reliability, Out_Of_Service, Reliability_Evaluation_Inhibit), intrinsic reporting (Notification_Class, Event_Enable, Acked_Transitions, Notify_Type, Event_Time_Stamps, Event_Message_Texts and its Config, Event_Detection_Enable, Event_Algorithm_Inhibit and its Ref, Time_Delay, Time_Delay_Normal), COV (COV_Resubscription_Interval, Client_COV_Increment, COVU_Period, COVU_Recipients, Update_Interval, Requested_Update_Interval) and Group_Members, Group_Member_Names and Member_Status_Flags. It also adds the Intrinsic reporting and Change of value concept pages. Each of these properties appears on 20 to 55 object types, so later object batches can link to them instead of writing them.
  • docs (#2887): docs/wiki/, the first 69 pages of a read-only BACnet encyclopedia for the BACnet Explorer’s contextual help (batch B0). It covers the Group and Global Group objects, the 12 Group properties, the 37 datatypes and enumerations those tables use (each constructed datatype with a ## Definition), supporting concepts, services and indexes. Written from 135-2024, with no encoding detail and no product-specific content.
  • ci (#2887): ci_scripts/wiki/wiki.cjs validates the wiki (check), rebuilds its generated parts (sync, where sync --check is the gate) and writes the Explorer bundle (export --out: the pages plus manifest.json). --self-test plants 14 defects and requires each to be caught. It is wired into pre-push.sh and tests.yml.
  • docs (#2887): BACnet wiki batches B12 to B20.
    • Objects: Lighting Output, Binary Lighting Output, Channel, Color and Color Temperature; the seven access control objects; Life Safety Point and Zone; Elevator Group, Lift and Escalator. They come with 160 property pages and their datatypes and enumerations.
    • The Network Security concept page: object type 38, removed with Clause 24 in revision 22.
    • Every BACnet service: 47 pages, including full ReadProperty and ReadPropertyMultiple pages.
    • The remaining enumerations: abort and reject reasons, and the service-choice lists.
    • All 177 BIBBs in Annex K and all 40 standardized device profiles in Annex L. The profile-to-BIBB requirements come from one parsed reading of the Annex L tables.
    • Concept pages on lighting control, access control, life safety, elevators, confirmed and unconfirmed services, BIBBs and device profiles.
    • Every page set was fact-checked against the 135-2024 extraction.
  • ci (#2887): wiki.cjs sync now generates an “Object types” section on every property page. It lists each object type whose Properties table has the property, and links that object’s own section for it where there is one. The verbatim check skips that section, and the BIBB and profile index pages now get their lists generated.
  • docs (#2887): BACnet wiki batch B1a, the Device object. It adds objects/device.md, with all 80 rows of Table 12-13 and the footnotes written out in words, and pages for the 58 Device properties that had none. It also adds 34 datatype and enumeration pages for the types those properties use, including the secure-authorization types and everything they reference, and 9 concept and service pages: segmentation, MS/TP, device address proxy, time synchronization, backup and restore, device restart, authorization, Who-Is and I-Am.
  • ci (#2887): ci_scripts/wiki/spec-table.cjs prints a Clause 12 object property table from the licensed extraction as TSV, so each batch can rebuild its property-profile TSV. Many of the tracked TSVs are missing rows; Device had 71 of 80. wiki.cjs now accepts constrained datatypes such as Unsigned(0..127) and OctetString (Size(16)), linking the base type, and has a self-test case for them. sync now also links the Property cell of object tables and the names in the Object types and Property identifier lists whenever the page exists.
  • docs (#2887): BACnet wiki batches B1b to B4. They add the Network Port object and the Analog, Binary and Multi-state Input, Output and Value objects. Network Port’s table carries every property of the per-network-type sub-tables, with the network types and protocol levels written out, and the page has a section per network type. Also added: 116 property pages, 27 datatype and enumeration pages (among them BACnetEngineeringUnits with all 427 units, and the BACnet/SC connection types), and 4 concept pages: BACnet/IP, BBMDs, BACnet/SC and command prioritization. Each page set was fact-checked against the 135-2024 extraction.
  • docs (#2887): BACnet wiki batch B21, the final pass.
    • A glossary of 92 terms.
    • Concept pages for the special property identifiers ALL, REQUIRED and OPTIONAL, and for the removed or unused property identifiers.
    • Links from 531 plain-text mentions of services and multi-word object names to their pages.
    • Fixes: the Date and DateTime pages no longer describe pattern values; Min_Pres_Value and Max_Pres_Value now cover every object type that has them; Priority_For_Writing has one datatype, because Table 12-36 prints Unsigned (1..16) with a space; the service and object-type flag tables link each name.
    • The wiki covers every object type, every property used in an object table, every service, every Annex K BIBB and every Annex L profile.
  • ci (#2887): spec-table.cjs --footnotes now prints every footnote, up to the first property subclause.
  • docs (#2887): BACnet wiki batches B5 to B11. They add the twelve value objects; Calendar, Schedule and Timer; Trend Log, Trend Log Multiple, Event Log, Audit Log and Audit Reporter; Notification Class, Event Enrollment, Alert Enrollment and Notification Forwarder; Loop, Program, Command, Averaging, Accumulator, Pulse Converter, Load Control and Staging; and File and Structured View. Also added: their property, datatype and enumeration pages (among them BACnetPropertyStates and all the enumerations it uses, and BACnetEventParameter and BACnetFaultParameter), and concept pages on scheduling, trending, event notification, algorithmic reporting, PID control, demand response and object hierarchies.
  • rb e-20260926-159 (#3097): BACnet/SC compatibility flag ACCEPT_CONNECT_REQUEST_WITHOUT_HELLO (0x02, BACnetStack_SetBACnetSCCompatibilityFlags), off by default. With it set, this device’s hub function and its direct-connect accepting peer accept a Connect-Request that omits the Hello destination option, which 135-2024 AB.2.3.1.2 makes mandatory (“This header option shall be included with the ‘Connect Request’ and ‘Connect Accept’ messages”), and record the peer’s Hello capabilities as 0. Only the missing-Hello refusal is relaxed: a Connect-Request carrying Data Options is still refused, and a Connect-Accept still needs Hello (flag 0x01 relaxes that only at the hub connector). Without the flag such a request is still NAKed with INCONSISTENT_PARAMETERS. Comments citing AB.2.2 for the Hello requirement now cite AB.2.3.1.2.
  • rb e-20260926-161 (#2557, #2976, IFC-061): new export BACnetStack_RegisterCallbackNetworkPortCommand(bool (*)(deviceInstance, networkPortInstance, command, errorCode)). A WriteProperty of a Network Port’s Command now reaches the host for exactly two values, after the stack’s own checks. DISCARD_CHANGES (1), on every port type, calls the host before the stack reverts the port, so the host can revert the certificate file data it holds (135-2024 cl. 12.56.101). A refusal answers the host’s error and leaves Changes_Pending TRUE. GENERATE_CSR_FILE (9), on a BACnet/SC port with Changes_Pending FALSE, has the host generate the key pair and CSR synchronously and answers Result(+) with Command back at IDLE and Changes_Pending untouched (cl. 12.56.16). Without the callback, DISCARD_CHANGES behaves as before and GENERATE_CSR_FILE still answers PROPERTY / OPTIONAL_FUNCTIONALITY_NOT_SUPPORTED. All six generated adapters were regenerated, and the Node addon gained a binding and a typed api.ts wrapper.
  • rb e-20260926-163 (#2913, IFC-062): test-tool exports BACnetStackTestTool_RegisterCallbackGetPropertyObjectPropertyReference and BACnetStackTestTool_RegisterCallbackSetPropertyObjectPropertyReference. A host now serves and receives a BACnetObjectPropertyReference-valued property field by field: Loop Controlled_Variable_Reference and Manipulated_Variable_Reference, Pulse Converter Input_Reference, and Event_Algorithm_Inhibit_Ref on every object type that has it. The stack encodes and decodes the SEQUENCE, including the optional property-array-index. They are asked before the constructed-BLOB callbacks. A refusal that names no error falls through to the constructed callback, and the constructed callbacks are unchanged when these are not registered. An out-of-range object identifier from the host answers Error(PROPERTY, VALUE_OUT_OF_RANGE). The Node addon gained bindings and typed api.ts wrappers, and the generated adapters were regenerated. BACnetDateTime properties (Pulse Converter Update_Time, DateTime Value Present_Value) needed no new export in a build with STACK_OPTION_OBJECT_TYPE_DATE_TIME_VALUE: there the existing Date and Time callback pair already serves them, now pinned by service-level tests. A build with Pulse Converter but without DateTime Value does not serve Update_Time.
  • rb e-20260927-164 (#2913, IFC-063): test-tool exports BACnetStackTestTool_RegisterCallbackGetPropertySetpointReference / ...SetPropertySetpointReference, ...GetPropertyScale / ...SetPropertyScale and ...GetPropertyDeviceObjectReference / ...SetPropertyDeviceObjectReference. A host now serves and receives, field by field, a Loop’s Setpoint_Reference (BACnetSetpointReference, including the empty SEQUENCE), an Accumulator’s Scale (BACnetScale, float-scale or integer-scale) and BACnetDeviceObjectReference properties the stack does not hold (Structured View Subordinate_List and Represents, Staging Target_References, Access Rights Accompaniment). The stack encodes and decodes the values. BACnetARRAY properties follow the existing array convention: the length through the UnsignedInteger callback at index 0, then one typed call per element; a whole-array write of a BACnetARRAY DeviceObjectReference property (Subordinate_List, Target_References) arrives as one Set call per element, after every element has been checked, so a write the stack refuses changes nothing. BACnetLIST properties of the datatype are not routed to the new callbacks. Like the ObjectPropertyReference pair (IFC-062), they are asked before the constructed-BLOB callbacks, a refusal that names no error falls through to the constructed callback, and an out-of-range identifier or scale choice from the host answers Error(PROPERTY, VALUE_OUT_OF_RANGE). The Node addon gained bindings and typed api.ts wrappers, and the generated adapters were regenerated. A test-tool host can now serve the empty Setpoint_Reference SEQUENCE; before, the read answered Abort(other). BACnetActionList (Command Action) is not included; IFC-063 records a proposed shape.
  • rb e-20261001-203 (#3107, IFC-070): test-tool exports BACnetStackTestTool_RegisterCallbackGetPropertyActionCommand / ...SetPropertyActionCommand serve and receive a Command object’s Action (135-2024 cl. 12.10.8, a BACnetARRAY of BACnetActionList) command by command: the optional fields flattened with use flags, the property-value as application-tagged bytes. The array length comes from the UnsignedInteger callback at index 0. A Get with an object type above 1023, an instance above 4194303 or a priority outside 1..16 answers VALUE_OUT_OF_RANGE, and an empty, oversized or partial property-value answers Abort(other). A decline that names no error falls through to the constructed callback, and every element of a whole-array write is checked before any is written. Without the callbacks, Action is served by the constructed callbacks as before. Matching BACnetTestToolInterface methods, Node trampolines and typed api.ts wrappers.
  • rb e-20261001-203 (#3075, profile part): every object type the stack models now knows Audit_Level (Enumerated) and Auditable_Operations (BitString), and the commandable value types (Analog/Binary/Multi-state Output and Value, Access Door, the twelve value types of cl. 12.37-12.48, Channel, Lighting Output, Binary Lighting Output) know Audit_Priority_Filter (BACnetOptionalPriorityFilter). 135-2024 lists them in every object table, “present only if the device supports audit reporting” (Table 12-2 footnote 9). They are optional and read-only, so a host can now enable them (SetPropertyEnabled returned false before) and they join Property_List only when it does. Audit Reporter, Audit Log and Color Temperature keep their own entries. The Device’s Audit_Notification_Recipient is not part of this change (see #3075).
  • rb e-20261001-204 (#3298 part 3, IFC-071): test-tool export BACnetStackTestTool_AddEventEnrollmentObjectWithChangeOfStateAlgorithmUnsigned creates a CHANGE_OF_STATE Event Enrollment with an Unsigned alarm value (BACnetPropertyStates unsigned-value), so an Event Enrollment on a Multi-state Present_Value goes OFFNORMAL when the state number equals the alarm value (135-2024 cl. 13.3.2: the alarm values and the monitored value have the same datatype). A known monitored property that is not Unsigned is refused. Matching BACnetTestToolInterface method, Node wrapper and typed api.ts function.
  • rb g-20261001-12 (#2960): the authorization server’s issuance policy (BACnetStackAuthorizationPolicy, test-tool builds) now carries the cl. 17.4.1 terms on each client entry: the audiences the client may be issued a token for, a validity range, and the constraint written into issued tokens. AuthRequest refuses a requested audience the entry does not list, or a request outside the entry’s range, with Error(SERVICES, NO_POLICY) (cl. 17.6.4.1: “The server does not have a policy that allows the requested combination of parameters”). An entry with a range refuses every request when the server has no clock (fail-closed). Audiences match by exact value only (and -1 listed admits any); audience group membership is not modelled, so it fails closed. Every term defaults to the old behaviour: any audience, no range, any-network / certified. So an entry set only through BACnetStack_SetAuthorizationPolicyEntry behaves as before. The terms can be set from C++ only; no export reaches them yet. Revert-red: with the processor change reverted, all four BACnetAuthRequestProcessorIssue2960Test cases fail.
    • The validity range is in epoch seconds, not the plan’s BACnetDateTime. It is compared directly with CallbackGetSystemTime’s clock, which the processor already reads; comparing BACnetDateTime values would need wildcard handling and a UTC-offset conversion.
    • Beyond the plan: SetPolicyEntry on an existing client now replaces only the scope and keeps the audience, range and constraint. Before, it rebuilt the entry, and so would have reset any restriction to unrestricted. The calls that widen an entry again are SetPolicyEntryAudiences with an empty list, ClearPolicyEntryValidity, SetPolicyEntryConstraint, RemovePolicyEntry followed by SetPolicyEntry, and Reset.
  • rb g-20261001-6 (#2616, slice 2): test-tool builds with an Audit Reporter now generate an operation-target audit notification for a successful AddListElement or RemoveListElement (135-2024 Table 19-5, WRITE). The notification names the list property as Target Property and carries the request’s invoke ID. For AddListElement, Target Value is the request’s whole List of Elements, including any element that was already present; RemoveListElement has no Target Value (Table 19-4: the field “contains the new value”). A failed request is not reported, and neither is an AddListElement with an empty List of Elements. The usual Audit_Level and Auditable_Operations rules apply.
    • Known gap: an AddListElement whose elements are all already present, and identical, writes nothing but is still reported.
    • Still not done for #2616: WriteGroup; local operations (cl. 19.6.1 (b)); the ACKNOWLEDGE_ALARM, LIFE_SAFETY, DEVICE_*, SUBSCRIPTION, NOTIFICATION and READ rows; the cl. 12.11.66 recipient-change notification; per-object audit properties on more object profiles; stack-held reporters for CreateObject(auditReporter).
  • rb g2-20261009-2 (#3851): BACnetStack_RegisterCallbackEventNotificationReceived is a customer export (built with STACK_OPTION_ALARMS_AND_EVENTS): an application receives the contents of every valid Confirmed/UnconfirmedEventNotification a local device receives (AE-N-A / AE-VN-A, for an operator display) without decoding frames with BACnetStack_DecodeAs(). The callback names the receiving device, gives the time stamp’s form as the BACnetTimeStamp CHOICE tag, separates an absent AckRequired from FALSE, gives the Event Values’ CHOICE tag (0xFFFF when absent) and their context-12 encoding (re-encoded by the stack), and reports a routed sender’s network and address. It is not called for a notification that fails the stack’s parameter check or that has no receiving device; the confirmed form is answered with a SimpleACK either way. The receiving device must execute the services (BACnetStack_SetServiceEnabled bits 2 and 29), which it does not by default. IFC-089.
  • rb g2-20261009-2 (#3851): ci_scripts/check-non-testtool-link.sh builds every source/*.cpp without BACNET_STACK_TESTTOOL and links them with a main() that registers the callback above, so a symbol only the test-tool build defines (#3850) fails a local check. Not wired into CI (owner).
  • rb g2-20261013-3 (#2616): the audit trail reports the device-level Table 19-5 operations that carry no Target Object. DeviceCommunicationControl reports DEVICE_DISABLE_COMM (DISABLE and DISABLE_INITIATION) and DEVICE_ENABLE_COMM (ENABLE). ReinitializeDevice reports DEVICE_RESET (WARMSTART, COLDSTART), DEVICE_BACKUP and DEVICE_RESTORE. Each is reported without Target Object, Target Property, Target Value or Current Value, through the reporter that monitors the Device object. WriteGroup reports one WRITE notification per Channel written (cl. 19.6.5). Not done: DEVICE_ENABLE_COMM when a DeviceCommunicationControl lifetime expires.
  • rb g2-20261013-3 (#3445, IFC-097): four test-tool exports set an authorization policy entry’s #2960 terms: BACnetStackTestTool_SetAuthorizationPolicyEntryAudiences, ..._SetAuthorizationPolicyEntryValidity, ..._ClearAuthorizationPolicyEntryValidity and ..._SetAuthorizationPolicyEntryConstraint. Each returns false and changes nothing when the device, policy or client entry does not exist or the value is refused; the constraint export refuses authentication 2 (any-method), per cl. 17.4.7 (d). The Node adapter and napi wrappers are regenerated.
  • rb g2-20261013-4 (#2616): the audit trail reports ACKNOWLEDGE_ALARM (the event object, the acknowledged state and Timestamp, the Acknowledgement Source as Source Comment), LIFE_SAFETY (the request’s object, the operation, the Requesting Source) and SUBSCRIPTION (SubscribeCOV and SubscribeCOVProperty; Target Value FALSE for a cancellation) on the operation-target side (Table 19-5). Each is reported only for a request that succeeded. The WRITE-specific audit rules (Audit_Level and Auditable_Operations changes, the commandable priority) apply to WRITE only.
  • rb g2-20261013-4 (#3891): when a DISABLE or DISABLE_INITIATION lifetime expires, the audit trail reports DEVICE_ENABLE_COMM with no Target Object and no Invoke ID (Table 19-5).
  • rb planner-20261014-docs-2 (#3915): ci_scripts/generate_doxygen.sh fails the release if the API Reference has no page for CASBACnetStackDLL.h, or if that page documents fewer functions than doc-lint counts as exports. It also requires Doxygen 1.9.8 or later and prints the version and a warning summary. Warnings are reported, never fatal, and the full log is written under build-artifacts/, so it is not shipped in the packages. Mutation-checked: excluding one export from the output stopped the script with “244 of 245”.
  • rb planner-20261014-docs-3 (#3920): The API Reference (docs/doxygen/ in the release package) has a new Enumerations section with 103 tables: every BACnet enumeration the stack implements, the bit numbers of the bit strings (including the BACnetServicesSupported numbers that BACnetStack_SetServiceEnabled takes) and the primitive datatype codes. Each table lists its values in numeric order, names the Customer API parameters whose documentation refers to it, and links to the matching BACnet Reference page where one exists. The tables are generated at documentation time from the stack’s own headers by ci_scripts/generate-api-reference-enums.js, so they always match the build; internal range markers such as MAX_ENUM are left out. Links such as CASBACnetStack::BACnetObjectType in the function documentation now resolve.
  • rb planner-20261014-docs-3 (#3919): The API Reference has five how-to guides for advanced integrators: stack lifecycle, serving property values, acting as a client, COV / alarms and events / trend logs, and data links. Each covers how to do the task with this stack and links to the BACnet Reference for the underlying BACnet concepts.
  • rb planner-20261015-docs-8 (#3918, #905): The API Reference and the generated bindings say more about each function.
    • 167 functions name the build option a customer sets to get them. Where several options work, all are listed, together with the internal macro the function checks. Without the option, the function fails; a build with debug logging also reports that the feature was not compiled in. The new “Build options” section of the API conventions explains the notes.
    • 157 functions name the ANSI/ASHRAE 135-2024 clause for their service, object type or algorithm.
    • Callback registrars link to the callback type they take, Add*Object functions link to ObjectExists and RemoveObject, and confirmed requests link to the response hooks.
    • The BACnetStack_GetInfo documentation no longer claims its macro list covers every build option.
    • Documentation only; no declaration changed.
  • rb sm-20261001-1 (#3398): BACnet/SC compatibility flag BACnetSCCompatibilityFlags::SEND_CONNECT_WITHOUT_HELLO (0x04), off by default — with it set, the Connect-Request (hub connector, direct-connect initiator) and Connect-Accept (hub function, direct-connect acceptor) this device sends omit the Hello destination option, for peers that reject a Connect message carrying a destination option (a deviation from 135-2024 AB.2.3.1.2); takes effect at the next connection. New BACnetSCBVLL::RemoveDestinationOptionsOfType. With the bit clear every message is unchanged — revert-red: with the four call sites removed, all five with-flag tests failed (Hello still present).
  • rb bs-20261004-18: CR2-F06-028 (#2610): the unused BACnetSourceAddress class (source/BACnetSourceAddress.{h,cpp}) is deleted, along with its test and its Visual Studio project entries. Only an #include in BACnetDatabase.h referenced it.
  • rb bs-20261004-23: CR2-F04-003: the unchecked ChipkinConvert::ToValue<T> is removed. It had no caller left, and #567 replaced it with TryParse. Its positive test cases now go through TryParse. CR2-F04-004: the 120-line commented-out ChipkinCircularBuffer implementation is removed from ChipkinUtilities.cpp.
  • rb bs-20261005-53 (#3089 CR2-F02-074, CR2-F02-077): source/BACnetDBPropertyOptions.cpp, whose entire body was commented out, and source/BACnetObjectTypesSupported.h, which no translation unit included (it carried a misspelled, unguarded bit 38 and a header-scope static name table). Both are dropped from the MSVS project; the other builds glob source/. BACnetDBPropertyOptions.h keeps the BACnetDBPropertyOptions typedef (it is BACnetDBProperty) without the retired class’s commented-out copy. Protocol_Object_Types_Supported is built elsewhere and is unaffected.
  • rb bs-20261005-68 (#2851 CR2-F29-006): the unused BACnetLocationSpecifier class (nothing referenced it; BACnetAccessRule carries its location-specifier as an enumeration of its own).
  • rb g-20261013-2 (#3769 CR3-F29-013): the unused BACnetAccumulatorStatus class (source/BACnetAccumulatorStatus.{h,cpp}) is removed, with its MSVS project entries. Nothing in the stack referenced it; the Accumulator’s status is BACnetAccumulatorRecord’s own enumeration. A source-code customer must remove the include and any build-file entry for BACnetAccumulatorStatus.{h,cpp}.
  • rb bs-20261005-11 (#2694):
    • CR2-F16-005: the LifeSafetyOperation comments now name INVALID_OPERATION_IN_THIS_STATE (cl. 13.13.1.3.1; 135.1 9.9.2.2) for a request inappropriate to the object’s state, not VALUE_OUT_OF_RANGE. A host following the old comment would fail BTL 9.9.2.2.
      • BACnetDBDevice.h now cites cl. 13.13.1.1.4 and says the in-stack apply runs after the host callback.
      • CASBACnetStackOptions.h cites cl. 13.13 in both places (one said 16.5, which is ConfirmedTextMessage; the other 13.8, ConfirmedEventNotification), and so do BACnetDBDevice.cpp and BACnetInterface.h, which said 13.8.
      • The frozen BACnetCallbackInterface.h’s “ASHRAE 135 clause 13.8” is left for the frozen-header session.
    • CR2-F16-012: the EventNotification comment no longer says the standard is “silent” on whether EVENT/ALARM notifications carry AckRequired, From State and Event Values. Table 13-12 marks them C, and Table 13-3 gives each a value for every event state transition. Accepting a notification without them is a deliberate receive-side tolerance. Whether notifications this stack originates should require them is a question for the owner (#3558).
    • CR2-F16-030 (the AcknowledgeAlarm callback’s order and error class) is not changed here; it changes when a customer callback fires, so it went to the owner as #3558, with the Port_Filter profile question.
  • rb bs-20261005-8 (#2694):
    • CR2-F16-013: three stale EventNotification comments were corrected. The processor does hand a received notification to CallbackEventNotificationReceived in a test-tool build. BACnetStackEventNotificationParameters::messageText is sent as Message Text when non-empty, and its ToDo markers (which the TODO gate does not catch) are gone. An oversized captured eventValues slice is discarded, not truncated.
    • CR2-F16-023: OfferEventNotification, its header and the BACnetEventNotificationOfferConsumer contract now say that a locally originated notification reaches the Notification Forwarder (#534) with wasReceived true and networkParameters NULL. A needless NOREF(utcOffset) was dropped.
    • CR2-F16-024 is left: gating the forwarder’s *ForTest seams on BACNET_STACK_TESTTOOL means gating about 55 uses in its test file too.
  • rb e-20261003-215 (#1637 §6, part 5): every parameter of the twelve Access Zone test-tool exports (BACnetStackTestTool_AddAccessZoneObject, SetAccessZone*, AccessZoneEnter / AccessZoneExit, IsAccessZonePassbackViolation) is documented: 73 parameters, 2 of them documented before. Each \return names when the call fails. IFC-074.
  • rb e-20261003-216 (#3472): the COV-notification test-tool sends document that a value’s propertyIdentifier is sent as given, all 32 bits: cl. 21.6 reserves 4194304 and up for ASHRAE rather than making it invalid, and 135-2024 defines identifiers there. No range check was added. IFC-075.
  • rb e-20261003-216 (#1637 §6, part 6): every parameter of the seventeen Access Point test-tool exports (BACnetStackTestTool_AddAccessPointObject, SetAccessPoint*, AccessPointAuthenticateFactor, AccessPointRequestAccess) is documented: 87 parameters, 16 of them documented before. Each \return names when the call fails. Two wrong statements are corrected: AddAccessPointObject does not reconfigure, and AccessPointAuthenticateFactor does not record the credential’s use. IFC-075.
  • rb e-20261003-217 (#1637 §6, part 7): every parameter of the sixteen Access Credential and Access Rights test-tool exports (BACnetStackTestTool_AddAccessCredentialObject, SetAccessCredential*, UseAccessCredential, AddAccessRightsObject, SetAccessRights*, EvaluateAccessRights) is documented: 110 parameters, 29 of them documented before. Each \return names when the call fails. Corrected: the two Add*Object exports do not reconfigure, and EvaluateAccessRights does not use its date, time or accessPointDeviceInstance. IFC-076.
  • rb e-20261003-218 (#1637 §6, part 8): every parameter of the three Event Log record-seeding test-tool exports (BACnetStackTestTool_InsertNotificationLogRecord, InsertNotificationLogRecordWithEventValues, SetEventLogStartStopTime) is documented: 79 parameters, 4 of them documented before. Each \return names when the call fails. IFC-077.
  • rb e-20261003-219 (#3480 item 3): the notification-log inserts are documented as direct injection - appended whatever Enable, Start_Time and Stop_Time say. IFC-078.
  • rb e-20261003-219 (#1637 §6, part 9): every parameter of the six Event Enrollment adder test-tool exports is documented: 90 parameters, 2 of them documented before. Each \return names when the call fails. IFC-078.
  • rb e-20261003-220 (#3482 items 6, 8): api.ts JSDoc for the Event Enrollment adders; CHANGE_OF_VALUE comments cite cl. 13.3.3. IFC-079.
  • rb e-20261003-220 (#1637 §6, part 10): every parameter of ten fault / EXTENDED / ACCESS_EVENT algorithm test-tool exports is documented: 62 parameters, none documented before. Each \return names when the call fails. IFC-079.
  • rb e-20261003-221 (#3483 items 9-10): the generic Event Enrollment adder documents that the customer BACnetStack_SetIntrinsic*Algorithm route applies none of the OUT_OF_RANGE adder’s checks and always serves Time_Delay_Normal (left unchanged for the owner). IFC-080.
  • rb e-20261003-221 (#1637 §6, part 11): every parameter of nine outgoing-request test-tool exports (Initialize-Routing-Table, I-Have, raw confirmed / unconfirmed requests, AuthRequest, the invoke-id seed and generator, the responding FSM query and transition callback) is documented: 61 parameters; header-wide undocumented parameters go from 111 to 50. IFC-080.
  • rb g-20261004-3 (#2690, CR2-F12-010/011): the AddListElement / RemoveListElement processor and request headers no longer say “test-tool-only” or describe a per-element SetProperty append. They now say the processors delegate to BACnetBusinessLogic::AddListElement / RemoveListElement (a single whole-property write), compiled under STACK_OPTION_DM_LM_LIST_MANIPULATION. The CreateObject processor’s copy of the cl. 15.3.1.3.1 Error table gains its missing PROPERTY / INVALID_DATA_ENCODING row.
  • rb g-20261004-4 (#2689, CR2-F11-007): the WriteProperty processor’s copy of cl. 15.9.1.3.1 / 15.9.2 / 15.9.1.1.5 is now labelled as earlier-edition text that is not verbatim 135-2024, with the differences the review found listed. The “new(std::nothrow) value” search-and-replace artefact in the table reads “new value” again. No spec text was re-quoted: the extracted 135-2024 text was not reachable from this session. CR2-F11-007 is therefore only partly done and stays open on #2689: the block still needs replacing with the verbatim 135-2024 text by someone with the extracted spec.
  • rb g-20261004-4 (#2689, CR2-F11-013): the WriteGroup processor’s file comment, header comment and loop comment now describe the device-level group membership #1135 implemented, not the old per-Channel rule.
  • rb g-20261004-4 (#2689, CR2-F11-014): the BACnetPriorityValue class comment now describes the three shapes Decode/Encode handle (a bare application-tagged primitive, [0] constructed-value, [1] datetime). It used to say two, with primitives collapsed into [0].
  • rb g-20261005-4 (#3088, IFC-081): the test-tool header docs now describe the refusals above: the five EventNotification senders’ timestampForm (“Any other value fails the call”), GetRouterRoute’s too-small next-hop buffer (false, length needed, dnet/portId still written) and InsertAuditLogRecord’s trailing octets; BACnetTestToolInterface.h likewise for GetRouterRoute. Interface entry docs/interface/IFC-081.md.
  • rb g-20261005-5 (#3088, IFC-082): the test-tool header docs for the six CR2-F01-039 exports describe the out-of-range refusal (SetAccessZoneEntryPoints/ExitPoints said such a pair “is not refused”). Interface entry docs/interface/IFC-082.md.
  • rb g-20261013-1: source comments brought up to the implemented contract - the test-tool BACnetStackTestTool_SetLifeSafetyTrackingValue latching rule (#3765 CR3-F25-004); the Lighting Output and Binary Lighting Output write return values and value-source recording on a relinquish (#3767 CR3-F27-005); the Lighting_Command decode/range and BACnetLightingOperation gate comments (#3767 CR3-F27-007); the Schedule/Calendar engine comments and Date-octet citations (#3768 CR3-F28-007); the Timer State_Change_Values setter’s ownership of its value (#3768 CR3-F28-009); BACnetDateUtils.h’s Weekly_Schedule and weekday citations (#3768 CR3-F28-010). No behaviour change.
  • rb g-20261013-2: source comments brought up to the implemented contract - the Access Rights gate now also covers #2802’s helper in BACnetAccessRule.cpp (#3764 CR3-F24-002, compile structure only); why BACnetLockStatus.h stays ungated - only tests use it outside the Access Door and Alarms and Events gates (#3764 CR3-F24-003); the Access Door pulse-expiry, Access Zone occupancy-count (read as 0 while counting is disabled, unless Out_Of_Service) and Access Rights evaluator comments (#3764 CR3-F24-017); a Landing Call Status test comment (#3766 CR3-F26-005); the value-source relinquish model (#3769 CR3-F29-002); Load Control’s EnterPending (#3769 CR3-F29-003); the full license header on BACnetStackAveraging.{h,cpp} (#3769 CR3-F29-014). No behaviour change.
  • rb g-20261013-3: source comments brought up to the implemented contract - the ACCESS_EVENT banner and time-delay comment describe #2730’s Access_Event_Time edge rather than the removed Access_Event_Tag edge, and BACnetAccessEventEventAlgorithm.cpp gets the full license header (#3757 CR3-F17-008); the FAULT_STATE, FAULT_CHARACTERSTRING and FAULT_LIFE_SAFETY CreateEventValues comments no longer describe the Event Enrollment labelling limitation #2763 removed, and the FAULT_STATE class documentation names Boolean (#3758 CR3-F18-005). No behaviour change.
  • rb g-20261013-4: source comments brought up to the implemented contract - change-of-status-flags cites cl. 21.6 / 13.3.11 for the OPTIONAL present-value (#3760 CR3-F20-010); the command-failure *AndTakeControl setters say ownership transfers only when they return true (#3760 CR3-F20-016); the Event Enrollment comments describe the step-1 faults that outrank MONITORED_OBJECT_FAULT, the properties GetProperty serves and a derived Fault_Type (#3761 CR3-F21-008); the Alert Enrollment Notify_Type write cites cl. 12.52.11 (#3761 CR3-F21-009); the Annex AA time-series header says encode is RFC 4180 and decode is lenient (#3762 CR3-F22-018). No behaviour change.
  • rb g-20261013-5: diagnostics and source comments brought up to the code - BACnetAddressBinding::Encode logs its own class and “encode” (#3715 CR3-F13-023); the DAB heartbeat’s DNET 65535 is described as the global broadcast it is (#3715 CR3-F13-024); BACnetFirstFailedSubscription and BACnetSubscribeCOVPropertyMultipleError log their own class (#3717 CR3-F15-017); the ProcessMultiple header describes its empty-list Rejects and the absent hook, and the multiple-request Clone OOM logs name the class being cloned (#3717 CR3-F15-027, part); the Table 13-1 comment names GetSubscriptionProperty and PrepareCOVNotificationPropertyValues (#3717 CR3-F15-010, part); the event-notification offer-consumer interface names the Event Log and Notification Forwarder consumers (#3718 CR3-F16-006); the GetEnrollmentSummary Enrollment Filter comment describes the both-forms match (#3718 CR3-F16-009, part); the forwarder’s OfferEventNotification and CollectTargets comments say a NULL networkParameters is the local-origin path, and which loop-prevention rules still apply on it (#3718 CR3-F16-015). No behaviour change.
  • rb g-20261013-6: JSONRenderer.h describes the shape the renderer emits - an empty element is {}, and every child of a list container is an array (#3826 CR3-F04-002); SanitizeUtf8’s head comment states the one-U+FFFD-per-ill-formed-prefix rule its body implements (#3826 CR3-F04-006). No behaviour change.
  • rb g-20261013-7: MSTP.h gives the callbacks’ uint16_t length, ThisStation’s 0-127 range and what Nbaud_rate is kept for (#3680 CR3-F08-004); BACnetBBMD.h says RemoveBroadcastDistributionTableEntry refuses the host entry while BBMD mode is enabled (#3679 CR3-F07-005); a direct-connect comment cites #2601 instead of a review candidate id (#3681 CR3-F09-026). No behaviour change.
  • rb g-20261013-8: comments corrected in BACnetPrimitiveBase::EncodeWithContext (it checks the context tag; it does not set it, #3676 CR3-F03-006), at the three #3454 out-of-range Date/Time decode sites (the value stays valid and Encode refuses it, #3676 CR3-F03-015) and on an #endif in BACnetUnconfirmedRequestProcessor.h (#3677 CR3-F05-017); the BACnetPrimitiveTIME constructor’s hundredths parameter is named hundrethSeconds, as in the header (#3676 CR3-F03-017). No behaviour change.
  • rb g-20261013-9: comments corrected for the Reject Reason clause of MISSING_REQUIRED_PARAMETER (cl. 18.9) in the ReadPropertyMultiple and WritePropertyMultiple list decoders (#3712 CR3-F10-016, #3713 CR3-F11-014), for what Reset() does to validity and a stale sibling claim in the WritePropertyMultiple files (#3713 CR3-F11-014), for the BACnetPriorityFilter ASN.1 in BACnetPriorityFilterBitString.h (#3713 CR3-F11-008), for the cl. 15.3 quote in the CreateObject processor (#3714 CR3-F12-004) and for the element field order in BACnetStackCreateObjectAsync.h (#3714 CR3-F12-005). No behaviour change.
  • rb bs-20260924-14 (#2708): the BACnetStackActiveCOVSubscriptions tests that sat in the #else of STACK_OPTION_SUBSCRIBE_COV_PROPERTY now build and run in every configuration. The full build had never run them. Two of them had to be repaired:
    • the Active_COV_Subscriptions readback tests now decode the encoded entries (the value holds encoded bytes, so GetSize() was 0);
    • the no-space test fills the per-object limit of the build in use (5 by default, 2 in the embedded build, 10 in the full build).
  • The flat-export tests share one global device (bs-20260925-22). After #2958 the full suite depended on test order: the new SendListElementReadAccessSpecifications tests could take the one device the process-wide stack instance holds, and SendInitializeRoutingTableExportTests then failed to add theirs. All three flat-export test files now share tests/GlobalExportDevice.h.
  • rb e-20260930-196 (#3395): verification only, no code change. A ConfirmedCOVNotification or UnconfirmedCOVNotification whose List of Values carries a vendor-proprietary property with an empty, constructed or context-tagged value already decodes - the COV List of Values is decoded by each value’s own tags (BACnetPropertyValue::Decode), not through GetPropertyValueDatatype - and a property after it still decodes. Tests now pin the decoded values for both services, with a malformed control. The WritePropertyMultiple half (such a request gets no reply at all today) is now #3400.
  • rb e-20260930-196 (#3396): verification only, no code change. A valid UnconfirmedCOVNotificationMultiple, with or without the optional timestamp [3] and time of change [3], logs no ERROR line, whether decoded directly or received on the wire; the absent-optional-field probe was already quiet since #2998. Tests capture the log and pin it, with a malformed control that still logs at ERROR.
  • rb e-20260930-197 (#3387): verification only, no code change. FD_BBMD_Address already reads as the default { host none, port 0 } (encoded 0E 08 0F 19 00) when the application provides no address (#2336/#2340): when CallbackGetPropertyEnum is not registered, or when it declines without naming an error code. An application that answers the host type but declines the port still gets an Error, and a decline that names an error code is still honoured. New tests pin the default on the wire through ReadPropertyMultiple, naming the property and with ALL, and add a control that the name path is unchanged. FD_BBMD_Address is not a required property, so ReadPropertyMultiple REQUIRED does not return it. Writes of Null, 0 are unchanged.
  • rb g-20261005-4 (#3088, CR2-F01-056, partial): four gtests in BACnetStackMemoryBufferAdoptAndPrepend pin AdoptAndPrepend’s empty-source, in-place and fallback paths, and the refusals that leave the source untouched. These pin current behaviour, so no revert-red applies. Partial: the review’s recommendation to refuse this == &src, and to document that the prefix must not alias the source, is not done.
  • rb g-20261013-1 (#3765 CR3-F25-002): #2808’s Silenced reset on the RESET_ALARM and RESET_FAULT unlatch paths is now pinned by a test each (a latched alarm unlatched onto fault, and a latched fault unlatched onto alarm, both read Silenced 0).
  • rb g-20261013-1 (#3767 CR3-F27-008): BACnetLightingCommand::AreUsedFieldsInRange is pinned for every operation 0..14 against Table 12-67’s field use, each of target-level, ramp-rate, step-increment and fade-time out of range in turn, with an in-range control per operation.
  • rb g-20261013-1 (#3768 CR3-F28-006): the #2911 refused-element Exception_Schedule test now refuses an element that reaches the write’s domain check (a DateRange starting in month 13), not an event with no period.
  • rb g-20261013-2 (#3766 CR3-F26-002): the Landing_Door_Status codec (BACnetLandingDoorStatus) is pinned against literal bytes - decode and encode of one and two elements, and a refused wrong outer tag - so a wrong context tag or field order fails the suite.
  • rb g-20261013-2 (#3769 CR3-F29-008): #2851’s rule that a BACnetHealth decode failing on its timestamp [0] or result [1] leaves the object invalid is now pinned by a test for each field.
  • rb g-20261013-3 (#3757 CR3-F17-013): OUT_OF_RANGE’s limit-disabled returns (c)/(f), cross transitions (d)/(g) and #2746 at-the-deadline Time_Delay / Time_Delay_Normal checks now also run against DOUBLE_OUT_OF_RANGE, SIGNED_OUT_OF_RANGE, UNSIGNED_OUT_OF_RANGE and UNSIGNED_RANGE, and the deadline checks against FLOATING_LIMIT.
  • rb g-20261013-3 (#3759 CR3-F19-004): #2748’s reset-before-the-bad-parameter-check in BACnetEventParameterExtended::Decode and BACnetEventParameterExtendedParameter::Decode is now pinned: an empty decode after a good one leaves the object invalid and empty.
  • rb g-20261013-4 (#3760 CR3-F20-004): the BACnetNotificationParameters CHOICE dispatch’s refusals are now pinned: reserved tags [7], [12] and [20] and a tag above [22] are rejected INVALID_TAG, complex-event-type [6] is aborted OTHER, and a first tag that is not an opening tag is rejected INVALID_TAG.
  • rb g-20261013-4 (#3760 CR3-F20-005, CR3-F20-013): #2749’s truncation and wrong-first-tag decode sweep now also covers the change-of-bitstring, change-of-characterstring, change-of-discrete-value, access-event and command-failure notification parameters.
  • rb g-20261013-5 (#3715 CR3-F13-016): Who-Is Decode’s rejection of a low limit above the high limit is now pinned (09 0A 19 05).
  • rb g-20261013-5 (#3717 CR3-F15-011): BACnetSubscribeCOVPropertyRequest::IsEqual is now tested: Issue Confirmed Notifications, Lifetime and COV Increment absent-vs-present or different, and a different or absent array index, are unequal.
  • rb g-20261013-5 (#3717 CR3-F15-026): three tests in TestBACnetCovValue.cpp and TestBACnetUnconfirmedCOVNotificationMultipleRequest.cpp moved inside their STACK_OPTION_SUBSCRIBE_COV_PROPERTY_MULTIPLE guard, so a build without the option compiles.
  • rb g-20261013-7: new tests pin the BACnet/SC header-option chain’s two rejection paths (#3681 CR3-F09-010), the four untested PARAMETER_OUT_OF_RANGE arms of the Advertisement and Connect-Request decoders (#3681 CR3-F09-002), and the MS/TP sole-master Npoll threshold (#3680 CR3-F08-003). No behaviour change.
  • rb g-20261013-8: new tests pin the D’254 and D’255 extended-length truncation guards in BACnetTag::Decode (#3676 CR3-F03-008), the present-but-malformed property-array-index guard in BACnetObjectPropertyReference (#3676 CR3-F03-038), and the port-id 0-255 bound in BACnetPortPermission (#3678 CR3-F06-019). The unmatched-invoke-id Abort test now sends a server Abort, so it fails if HookAbort fires with no outstanding request (#3677 CR3-F05-018). No behaviour change.
  • rb g-20261013-9: a new test reads the Device 4194303 wildcard’s Object_Identifier through the ReadProperty processor (#3712 CR3-F10-002), and the ListOfResults decode-failure test uses a buffer that reaches the read-result failure it names again (#3712 CR3-F10-021). No behaviour change.
  • rb bs-20260923-1 (#2327): the GoogleTest cache in tests.yml, sanitizers.yml and coverage.yml is now keyed on hashFiles('ci_scripts/fetch-gtest.sh'). The old fixed key kept restoring an entry filled from the pre-#2303 unverified download, so the SHA-256 check never ran in CI. Now the first run after this change fetches through the checksum-verified path, and any later change to the pin invalidates the cache.
  • rb bs-20260923-10 (#2357): an integrator that claims an MS/TP Network Port and uses the per-port API can now send and receive on it. The Chipkin BACnet Explorer found this live on a 38400-baud trunk behind a router. Before, no confirmed request reached an MS/TP device:
    • BACnetDataLinkLayer::ResolveOutgoingDataLink and ResolveIncomingDataLink resolved only IPv4 and BACnet/SC ports, so a send naming the MS/TP port failed with “does not match any claimed Network Port’s data link”, and every frame reported on that port was dropped. Both now fall back to FindMstpDataLinkByNetworkPortInstance when the shared lookup finds nothing, so IPv4/SC resolution and router-port attribution are unchanged.
    • ProcessIncomingPacketForPort now processes an MS/TP frame on the instance the port resolved to. It used instance 0, so with two MS/TP ports a request that arrived on the second was answered as the first.
    • BACnetDataLinkMSTP now overrides GetNetworkPortInstanceForSend, as the IPv4 and SC data links do. A claimed MS/TP data link used to name the untracked sentinel (4194303) to CallbackSendMessageForPort, so an integrator routing by port could not place the frame.
  • rb bs-20260923-10 (#2357): a confirmed request sent on MS/TP now advertises a Max_APDU_Length_Accepted of 480 octets (B’0011’), not the device’s 1476. This stack’s MS/TP data link carries only non-encoded frames, whose Length “shall be between 0 and 501 octets” (cl. 9.3). The server path already bounds MS/TP responses to the same BACNET_MAX_APDU_LENGTH_MSTP. Advertising 1476 invited replies this data link cannot receive, so a ReadPropertyMultiple(ALL) to a device behind a router timed out. Requests on BACnet/IP and BACnet/SC are unchanged.
  • rb bs-20260923-10 (#2380): the Linux test build compiles again. tests/TestBACnetEncodedValueEquality.cpp (from #2375) included BACnetSimpleACKPDU.h, but the file is BACnetSimpleAckPDU.h. That only resolves on a case-insensitive filesystem.
  • rb bs-20260923-3 (#2347): a stack-held Schedule now refuses a WriteProperty of Weekly_Schedule that has two entries with the same time in one day, with PROPERTY / DUPLICATE_ENTRY (cl. 12.24.7). The #1037 check only covered the host API, and a day decoded from the wire does not go through it.
  • rb bs-20260923-3 (#2348, rev 28 addendum cj): writing a Lighting Output Present_Value special value at a priority higher than or equal to a running FADE_TO or RAMP_TO now halts it, per cl. 12.54.4. The special values are -1.0 to -7.0: WARN, WARN_RELINQUISH, WARN_OFF, RESTORE_ON, DEFAULT_ON, TOGGLE_RESTORE and TOGGLE_DEFAULT.
    • Before, the fade kept interpolating over the new value.
    • The halt is the same one a normal-range write gets, so the fade’s own priority slot is left unchanged (cl. 12.54.6.1).
    • A RESTORE_ON, DEFAULT_ON or TOGGLE_* written at a lower priority no longer makes Tracking_Value jump to a still-running fade’s target.
    • The Lighting_Command form still rewrites the fade’s slot when it halts it. That is #2354.
  • rb bs-20260923-4 (#2349, rev 28 addendum co audit for #868): the reliability-evaluation process now has a first stage (cl. 13.2).
    • An intrinsic-reporting object whose range event algorithm has High_Limit below Low_Limit, with both limits enabled in Limit_Enable, now reads Reliability CONFIGURATION_ERROR. This applies to OUT_OF_RANGE for the analog objects (cl. 12.2.9 (a) and siblings), and to DOUBLE_OUT_OF_RANGE, SIGNED_OUT_OF_RANGE, UNSIGNED_OUT_OF_RANGE and UNSIGNED_RANGE for Large Analog Value, Integer Value, Positive Integer Value and Accumulator (e.g. cl. 12.39.8 (a)).
    • So does an object whose Fault_High_Limit is below its Fault_Low_Limit (cl. 12.2.9 (b)), and an Event Enrollment whose range algorithm’s Low_Limit is above its High_Limit. The Event Enrollment rule (cl. 12.12.21 (d)) does not depend on Limit_Enable.
    • The object makes a ToFault transition with a CHANGE_OF_RELIABILITY notification, and holds FAULT without running its event or fault algorithm until the configuration is consistent again. It then makes a ToNormal transition, or a ToFault back into the fault algorithm’s own reliability if that algorithm still reports a fault.
    • Entering CONFIGURATION_ERROR while already in FAULT from the fault algorithm is also a ToFault (cl. 13.2.2.1.3 Fault, ToFault: “If reliability-evaluation indicates a different Reliability value … re-enter the Fault state”).
    • These notifications carry the monitored property value in Property_Values (cl. 13.2 Table 13-5), with IN_ALARM and FAULT describing the state being entered.
    • Before this fix nothing in the stack ever produced CONFIGURATION_ERROR.
  • rb bs-20260923-4 (#2349): Reliability_Evaluation_Inhibit now takes an object out of FAULT. Cl. 13.2.2.1.3 says: “If reliability-evaluation indicates a value of NO_FAULT_DETECTED, then perform the corresponding transition actions and enter the Normal state”.
    • Before the fix, an object its fault algorithm had put in FAULT stayed in FAULT after the inhibit, while its Reliability read NO_FAULT_DETECTED.
    • InhibitReliabilityEvaluation(true) now also resets the fault algorithm’s reliability, as the property write already did.
  • rb bs-20260923-4 (#2349): an Event Enrollment’s Reliability now reads NO_FAULT_DETECTED while reliability-evaluation is inhibited (cl. 12.12.25). It used to report MONITORED_OBJECT_FAULT regardless. CONFIGURATION_ERROR now takes precedence over MONITORED_OBJECT_FAULT (cl. 12.12.21 step (1) before step (2)).
  • Still open:
    • #2358: CONFIGURATION_ERROR for overlapping alarm/fault value sets, and for FLOATING_LIMIT and Loop.
    • #2359: whether a host-reported first-stage reliability should take precedence. Owner question.
  • rb bs-20260923-5 (#2358, rev 28 addendum co, part 2 of #2349): Reliability now reads CONFIGURATION_ERROR in these cases (the #2349 first-stage ToFault and ToNormal behaviour applies to each):
    • A value in both lists. A value is in both the event algorithm’s alarm values and the fault algorithm’s fault values:
      • CHANGE_OF_STATE with FAULT_STATE: Multi-state Input (cl. 12.18.9), Multi-state Value (cl. 12.20.8), Access Door (cl. 12.26.8), and Event Enrollment (cl. 12.12.21 (c)). Values are compared by their Unsigned, Enumerated or Boolean (TRUE = 1, FALSE = 0) number, and by the property-state choice when an Event Enrollment’s Fault_Parameters gives one.
      • CHANGE_OF_CHARACTERSTRING with FAULT_CHARACTERSTRING: CharacterString Value (cl. 12.37.8) and Event Enrollment (cl. 12.12.21 (a)). The match is exact; the fault algorithm’s substring match is not used here.
    • Life safety. A value is in more than one of Alarm_Values, Life_Safety_Alarm_Values and Fault_Values (Life Safety Point and Zone, cl. 12.15.10 and 12.16.10, which includes the two event-side lists on their own; Event Enrollment, cl. 12.12.21 (b), when it has a FAULT_LIFE_SAFETY fault algorithm).
    • FLOATING_LIMIT. A negative Low_Diff_Limit or High_Diff_Limit (cl. 12.12.21 (e); Loop cl. 12.17.8, Error_Limit or Low_Diff_Limit below zero).
  • rb bs-20260923-7 (#2361): customer builds without the test tool (BACNET_STACK_TESTTOOL off) compile again, including TARGET_FULL, the NoSC release package and the customer DLL. Since bs-20260923-2 (#1758, stack-held Calendar), BACnetBusinessLogic::SetProperty called IsKnownStackHeldListProperty, which was declared only in test-tool builds. CI and pre-push.sh build only with the test tool, so neither saw the break. The function is now compiled in every build. The 6.x promotion (#2334) predates that batch and is unaffected.
  • rb bs-20260923-7 (#2361): TARGET_EMBEDDED builds compile again with a single object type or feature enabled. The gating sweep’s object and feature groups went from 100 FAIL / 1 XFAIL to 0 FAIL at the issues/runbook tip. The first failure hid everything behind it, so each fix exposed the next layer:
    • Every embedded build that lacked Notification Class failed. BACnetDBDevice::ReconcileNotificationClassRecipientDAB (#2076) was declared outside the guard that its parameter type and its definition sit under.
    • BACnetIngressContext, ResolveOwnMacAddressForIngressPort and IsRejectRoutableToSource are used by code every build compiles, so they are now outside the ROUTER_PHYSICAL guard.
    • The segmented-request retry (#2014) is compiled only with STACK_OPTION_SEGMENTATION. The xyColor datatype test follows xyColor’s own gate.
    • The two local-origin Notification Forwarder hand-offs, and the Value_Source (DS-VSI) write dispatch, are guarded by the options they need. The AcknowledgeAlarm send wrappers return false without STACK_OPTION_AE_ACKNOWLEDGE_ALARMS.
    • The GetEventInformation and AcknowledgeAlarm processors are gated like their request classes and the dispatcher.
    • BACnetTimeStamp is now also compiled for DS-VSI (Command_Time_Array) and Credential Data Input (Update_Time). Access Door includes BACnetValueSource.h.
    • Access Point’s ACCESS_EVENT context calls are guarded on alarms and events.
  • rb bs-20260923-8 (#2363, rev 29 addendum cp): BACnet/SC now removes a relayed Identity data option from a peer that is not capable of relaying identity. 135-2024 cl. 17.3.4 rule (2) says: “When a datalink receives a forwarded or routed message, if the “Identity” data attribute is present and the sending connection peer is not authorized to relay identity, then the attribute shall be removed.”
    • The check runs on receipt, before the NPDU reaches the network layer (and so the router). It covers every ingress: hub connector, both direct-connect directions, and the hub function.
    • A frame counts as forwarded or routed when it has SNET or an Originating Virtual Address.
    • Authorization needs a capability and a permission (cl. 17.3.3). The capability is the peer’s Hello ‘identity relay’ bit, which is enforced. The permission is the peer certificate’s SAN (?router / ?hub); TLS is the host’s, so the stack never sees it and it is not enforced.
    • The hub function’s own relay to other nodes is unchanged. AB.3.1.4 says: “The hub function and the source and destination node’s BVLL entity shall forward and not alter any of the data options.” Removing the attribute on receipt, before the network layer, is what the more specific cl. 17.3.4 rule (2) requires.
    • Today only the router consumes a received Identity, and only the hub-function ingress routes, so that call site is the only one whose effect a test can observe. It is gated by ScToScHop_DropsIdentityFromAPeerWithoutIdentityRelay. The other three sites (hub connector, both direct-connect directions) apply the same helper, which the unit tests cover, but no test gates the wiring there.
    • The accepted direct-connect peer’s Hello capabilities are now cleared when that peer disconnects.
  • rb bs-20260923-9 (#2354): a Lighting Output Lighting_Command that halts an in-progress FADE_TO or RAMP_TO now leaves the fade’s priority-array slot unchanged, as cl. 12.54.6.1 requires: “the corresponding slot in the priority array remains unchanged”. The commands affected are WARN, RESTORE_ON, DEFAULT_ON, TOGGLE_RESTORE, TOGGLE_DEFAULT and vendor operations.
    • Before this fix (#1384), those commands first wrote the fade’s interpolated Tracking_Value into its slot. That had knock-on effects: it moved Last_On_Value, so a following RESTORE_ON restored the interpolated level, and with DS-VSI it stamped the slot’s Value_Source and Command_Time with the halting writer.
    • Visible behaviour change: a halted fade now goes to its target. After the halt the object is IDLE, and cl. 12.54.5 says “When the value of In_Progress is IDLE, Tracking_Value shall be equal to Present_Value”. Present_Value is the fade’s target in its unchanged slot, so, for example, a higher-priority WARN at 25% of a FADE_TO 100 drives the output to 100. That follows the spec literally, and it is what #1384’s reconciliation write was avoiding. Tracking_Value is now re-synced at once for WARN and vendor operations (and for the Present_Value -1.0 WARN), not at the next tick.
    • STOP still writes Tracking_Value back, as its own Table 12-67 text says. The halt now matches the Present_Value path (#2348).
  • rb bs-20260923-9 (#2355): writing RENEW_FD_REGISTRATION to an IPv4 Network Port whose IP mode is not FOREIGN now returns PROPERTY / VALUE_OUT_OF_RANGE (cl. 12.56.16) instead of OPTIONAL_FUNCTIONALITY_NOT_SUPPORTED. Not implemented: RENEW_DHCP’s “IP_DHCP_Enable is FALSE” rule and RESTART_AUTONEGOTIATION’s “Link_Speed_Autonegotiate is FALSE” rule. The stack does not maintain those properties, so a device that does not have them answers OPTIONAL_FUNCTIONALITY_NOT_SUPPORTED, as the clause allows.
  • rb bs-20260924-3 (#2399): SubscribeCOVPropertyMultiple now uses the monitored object’s COV_Increment for a numeric Present_Value when the subscriber gives no ‘COV Increment’, as 135-2024 cl. 13.16.3.1.2.2 requires (“the COV increment to use is taken from the COV_Increment property of the monitored object”). It used to report every change. SubscribeCOVProperty already did this (cl. 13.15.1.1.6). A subscriber’s increment still takes precedence, and other properties with no increment still report any change.
  • rb bs-20260924-3 (#2417): the authorization cache compares an access token’s Not_Before / Not_After against the device’s local time (UTC minus the Device UTC_Offset), not UTC. UTC_Offset is the offset of local standard time (cl. 12.11.25), so during daylight saving the window is an hour off, as for every other stack-generated local time (#2339, #2410; tracked in #2450). 135-2024 cl. 17.4: “Times shall be evaluated relative to the server device’s local time.” BACnetStackAuthorizationCache::FindToken / FindTokenForAudience take the UTC offset. No production caller uses the cache yet (server-side token authorization is deferred, #2365).
  • rb bs-20260924-4 (#2448): the first value change after a SubscribeCOVPropertyMultiple now honours the COV increment. The value sent in the initial COV notification becomes the reference’s baseline. Before, a reference started with no baseline, and its first change was always reported, whatever the increment (135-2024 cl. 13.16.3.1.2.2).
  • rb bs-20260924-4 (#2450): stack-generated local times apply Daylight_Savings_Status. While the Device serves it as TRUE, local time is one hour ahead of the UTC_Offset standard time (cl. 12.11.25, 12.11.26), as the UTCTimeSynchronization path already did (#1138). Every stack-generated local time now goes through one offset (BACnetDBDevice::GetUtcOffsetForLogs):
    • event and alarm timestamps, the event-notification offer and the Notification Forwarder;
    • Trend Log and Trend Log Multiple polling and records, COV trend logs, and host-inserted log records;
    • COV notification timestamps and TimeSynchronization time-change records;
    • the Timer, Averaging, Binary Lighting Output and Access Credential time bases. Behaviour changes:
    • A host that serves Daylight_Savings_Status = TRUE sees these timestamps move one hour later.
    • Less log noise: the sites that used to read UTC_Offset directly no longer log a failed lookup when the Device does not have the property (#2407). The offset they get is unchanged.
    • Daylight_Savings_Status is read only when the Device has the property and the host registered a Boolean callback. A host that registers one but declines the property still logs the failed lookup whenever a local time is generated. Serve the property, or disable it.
    • Log records a host inserts into a device other than the first (for example a virtual device) use that device’s offset.
  • rb cloud-20260924-1 (#2434): DecodeAs (JSON/XML) and BACnetReadRangeACK::Decode decode a Trend Log Multiple ReadRange-ACK’s item-data as BACnetLogMultipleRecord (timestamp plus log-data values). Before, each record decoded as an empty UnknownAPDU, because the datatype factory had no LogMultipleRecord case.
    • This applies in builds with STACK_OPTION_T_VMMV_TRENDING_VIEWING_AND_MODIFYING_MULTIPLE_VALUES, which STACK_OPTION_TARGET_FULL enables. Builds that enable the Trend Log Multiple object without T-VMMV still show the old behaviour: #2435.
    • A malformed Trend Log Multiple record is now a decode error, as it already was for Trend Log and Event Log records.
    • Event Log item-data already decoded as BACnetEventLogRecord. That half of the issue did not reproduce, and a test now pins it.
    • Revert-red: DecodeAsJSONRendersTrendLogMultipleReadRangeRecordValues found UnknownAPDU in the JSON, and DecodeTrendLogMultipleItemDataAsLogMultipleRecords saw 2 item-data entries for 1 record.
  • rb cloud-20260924-2 (#2435): builds that enable STACK_OPTION_OBJECT_TYPE_TREND_LOG_MULTIPLE without T-VMMV (for example STACK_OPTION_ENABLE_ALL_OBJECT_TYPES on STACK_OPTION_TARGET_EMBEDDED, together with T-VMT for ReadRange) now decode a remote Trend Log Multiple’s Log_Buffer as BACnetLogMultipleRecord. Before, the records decoded as empty UnknownAPDU, the #2434 symptom, because the record codecs were compiled only under T-VMMV.
    • The following now follow the object-type option, which T-VMMV already defines, so no configuration loses them: BACnetLogMultipleRecord, BACnetLogData, BACnetSingleLogData, the BACnetSequenceOf<BACnetSingleLogData> instantiation, the datatype factory case, and the SingleLogData/LogData/BACnetLogMultipleRecord datatype enum values.
    • Cost: builds with the Trend Log Multiple object type but no T-VMMV compile these three codec classes (about 2.9k lines).
    • Evidence: a preprocessor probe of the #2435 configuration showed the factory case going from 0 to 1. In the same configuration the codecs, BACnetReadRangeACK.cpp and BACnetInterface.cpp pass -fsyntax-only, and the gating-sweep rows pass for the object-type one-hot, T-VMMV, T-VMT, antimask-no-T_VMMV/-no-T_VMT and target-full.
  • rb e-20260923-138 (#2207): the primary and failover hub connection statuses now follow cl. 12.56.88.
    • connect-timestamp is recorded when the connection is established or an attempt fails, and disconnect-timestamp when a connected hub connection ends; both are the unspecified datetime until the event happens. Hub-function connections record their connect-timestamp when accepted.
    • Failures the hub connector detects itself — a Connect-Request NAK (including duplicate VMAC), a websocket-connect or connect-accept timeout — are now reported as FAILED_TO_CONNECT with their error; previously the websocket was reset before the status was refreshed and they showed as NOT_CONNECTED.
    • A connected hub connection lost to a websocket error or a heartbeat-ACK timeout is reported as DISCONNECTED_WITH_ERRORS (previously FAILED_TO_CONNECT or NOT_CONNECTED). A closed websocket reported by the host, or a Disconnect-Request/ACK exchange, stays NOT_CONNECTED.
    • FAILED_TO_CONNECT / DISCONNECTED_WITH_ERRORS and their error stay in place while the hub connector waits to retry, instead of reverting to NOT_CONNECTED. A failed state always carries an error (communication / websocket-error when the transport reported no code), and a websocket’s stale last-error code is cleared when its status is reset.
    • Timestamps come from CallbackGetSystemTime converted as UTC (the Device UTC_Offset is not applied), the same conversion SC_Failed_Connection_Requests uses. A failure the hub connector detects is stamped with the time it was detected — the same time as its SC_Failed_Connection_Requests entry — even when the status is refreshed later, and it is attributed to the primary or failover status of the websocket it happened on; a failure pending for a hub URI is discarded when that URI changes.
  • rb e-20260923-138 (#2207): BACnetSCHubConnection::Decode() no longer leaves a rejected decode marked valid. BACnetSCHubConnection::Decode() and BACnetSCHubFunctionConnection::Decode() drop (and log, via the API debug log) error / error-details received in a NOT_CONNECTED or CONNECTED value instead of keeping members the clause says are absent.
  • rb e-20260923-141 (#2368): COV now reports changes to property values that the stack holds itself. Before this fix, two such values always compared as equal, so these changes were never reported.
    • The cause: a value the stack builds for a stack-held property, such as a Lighting Output’s properties, Current_Command_Priority or Value_Source, holds only its encoded bytes. BACnetAbstractSyntaxType::IsEqual compared only decoded values, so any two such values compared as equal. The numeric COV comparisons (REAL, DOUBLE, INTEGER, UNSIGNED Present_Value, and the COV_Increment) could not read these values at all.
    • IsEqual now gives the same result whichever form each value is held in:
      • two encoded values compare their bytes;
      • two decoded values compare as they did before;
      • one of each: the decoded value is encoded, into a buffer exactly as long as the other value’s bytes, and the two encodings are compared.
    • The numeric Present_Value comparison decodes encoded values, including the COV_Increment, so COV_Increment now applies whichever form the values are held in. The SubscribeCOVPropertyMultiple REAL comparison does the same.
    • Behaviour change: these subscriptions now notify when they previously did not:
      • SubscribeCOVProperty on a stack-held Lighting Output’s Tracking_Value and Relinquish_Default;
      • SubscribeCOVProperty on Current_Command_Priority;
      • SubscribeCOV on a relinquished stack-held Lighting Output, whose Present_Value is its Relinquish_Default. It now reports a Present_Value change of at least COV_Increment, and a change between commanded and relinquished in either direction;
      • SubscribeCOVProperty on In_Progress, Last_On_Value, Egress_Active and similar stack-held properties, and a Binary Lighting Output’s Present_Value while relinquished (not separately tested; same IsEqual path).
    • The same IsEqual fix applies to every other caller. The Command Failure event algorithm now sees a disagreement between a stack-held Present_Value and Feedback_Value (tested on a Binary Lighting Output). Before, two such values always compared as equal. A decoded value and an encoded value that are the same no longer compare as unequal.
    • Unchanged:
      • an ordinary Present_Value subscription with no COV_Increment still reports on every update of that property;
      • how two decoded values compare.
    • #1851’s Value_Source workaround now uses the fixed IsEqual. BACnetStackCOVSubscription::AreValuesIdentical forwards to it. The separate Value_Source re-comparison is removed, because the subscription’s own comparison now detects the change.
    • The test helper P0CreationPathComparator compared Object_Identifier and Object_Name values between two different instances. That only passed because of this bug, so it now checks those two properties for presence and readability but not value.
    • Follow-ups: #2371 (the Listed Fault algorithm compares decoded list elements only) and #2372 (a SubscribeCOVProperty COV_Increment on Integer/Positive Integer Value is read as Unsigned).
  • rb e-20260923-142 (#2372): a SubscribeCOVProperty with a COV Increment on an Integer Value, Positive Integer Value or Large Analog Value now reports a Present_Value change of at least that increment. Before, it never reported any change.
    • The cause: the subscriber’s COV Increment is always a REAL (135-2024 cl. 13.15.1.1.6), but the Present_Value comparison for these three object types read the increment only in the datatype of the object’s own COV_Increment property (Unsigned for Integer Value and Positive Integer Value, Double for Large Analog Value). The REAL was never found, and the comparison returned “no change”.
    • The comparison now reads the increment in whichever numeric datatype it arrives in (REAL, Double, Unsigned or Signed, decoded or encoded), for every numeric object type that has a case in the comparison (Staging has none; see the follow-up issue), and compares the change against the increment in double precision. A fractional subscriber increment on an integer object is not truncated (5.5 is not read as 5).
    • Unchanged: SubscribeCOV, and SubscribeCOVProperty without a COV Increment, still use the object’s own COV_Increment property, as before.
  • rb e-20260923-142 (#2371): the FAULT_LISTED fault algorithm now gives the same result for a monitored list whichever form it is held in (decoded elements or encoded bytes only).
    • The cause: BACnetListedFaultAlgorithm::Run read only decoded elements (GetSize() / GetObject(i)). For a list held as encoded bytes only, GetSize() is 0 even when the list has elements. So clause (a) never indicated FAULTS_LISTED, clause (b) returned to NO_FAULT_DETECTED although the list was not empty, and clause (c) saw no change between two such lists.
    • Emptiness now also checks the encoded bytes, and clause (c) compares the whole list with the representation-independent BACnetAbstractSyntaxType::IsEqual (#2368). Element order still matters, as before.
    • Reachability: a list reaches the algorithm encoded-only when GetProperty serves it through CallbackGetPropertyConstructed (test-tool builds; stored with InsertEncodedBytes), for example an Event Enrollment with FAULT_LISTED on a constructed-element list property. Lift/Escalator Fault_Signals, served by CallbackGetListOfEnumerations, is decoded and was not affected. Tested at the algorithm level only; no end-to-end test.
  • rb e-20260923-143 (#2377): a COV increment now applies to every numeric monitored value, and Staging reports COV on every Table 13-1 criterion.
    • SubscribeCOVPropertyMultiple: the subscriber’s COV Increment now applies to Unsigned, Signed and Double values (Integer Value, Positive Integer Value, Large Analog Value), not only to REAL. Before, these reported any change.
    • SubscribeCOVProperty: the subscriber’s COV Increment now applies to any numeric monitored property, not only Present_Value (for Present_Value itself, see below) (135-2024 cl. 13.15.1.1.6: the increment “is ignored if the datatype of the monitored property is not numeric”). Before, any other numeric property reported any change, and the COV-single and COV-multiple paths disagreed. Both paths now use one comparison.
    • Both paths: a COV Increment of 0 means “any change”: an unchanged value is not reported. A BACnetLIST, or a whole BACnetARRAY read without an index, is not compared by the increment; any change to it is reported, including a change to a later element or to an empty list.
    • Unchanged: the object’s own COV_Increment property still applies only to Present_Value (cl. 13.15.1.1.6). A numeric property other than Present_Value with no subscriber increment still reports any change.
    • Present_Value: an increment (the subscriber’s or the object’s COV_Increment) applies only for the object types whose Table 13-1 row reads “If Present_Value changes by COV_Increment”: Analog Input, Analog Output, Analog Value, Integer Value, Large Analog Value, Lighting Output, Positive Integer Value, Loop, Pulse Converter and Staging. Every other object’s Present_Value reports any change, whatever increment the subscriber sends. For example, a Multi-state Input, Output or Value state change is always reported (Table 13-1: “If Present_Value changes at all”).
    • Unchanged: other Unsigned properties, such as Staging’s Present_Stage or a commandable object’s Last_Priority, are still compared by a subscriber’s increment when one is given, as for any other numeric property.
    • Staging: Present_Value is now compared against COV_Increment (a Real, Table 12-80), and a Present_Stage change is reported even when Present_Value is unchanged or moved by less than the increment (Table 13-1: “If Present_Value changes by COV_Increment or Status_Flags changes at all or Present_Stage changes at all”). Before, every Present_Value change was reported and COV_Increment was ignored, and a change to Present_Stage alone was never reported.
  • rb e-20260924-147 (#2397): ci_scripts/check-object-identifier-boundary.sh (the #1592 object-identifier boundary gate) now counts pair declarations whose parameter list is wrapped across lines. It used to grep one physical line at a time, so bool DoThing(const uint16_t objectType, / const uint32_t objectInstance); was invisible to it and the gate under-reported how much of #1592 is left. The gate now strips comments (// and multi-line /* */) and the contents of string and character literals, joins every line that leaves a ( open with the following lines until it closes, and matches each logical line once. The public-header and two exempt-file skips and the “one count per line” unit are unchanged; a line that never wraps is its own logical line. Three tightenings, beyond what #2397 asked for: only declarations count now (a type token must precede the objectType name), so a call passing objectType, objectInstance is no longer counted (no checked header had one); a header whose parentheses do not balance after stripping is a FAIL, not a silent under-count; and the Register*/Hook* mirror exemption is decided by the declarator name (an identifier starting with Register or Hook immediately before a () instead of the substring register anywhere in the line, which exempted real declarations such as SetThing(BACnetRegisterMap* map, const uint16_t objectType, ...). The same 39 logical lines are exempt in the tree under both rules (BACnetInterface.h 35, BACnetTestToolInterface.h 4). New --self-test corpora: 1g-1k (wrapped declaration, comment-only mentions, single-line plus wrapped = 2, calls not counted, unbalanced parentheses), all five of which fail against the old one-line logic; and 1l-1n (declarator-name mirror exemption, a stray ) before its (, escaped quotes inside literals), each shown to fail when its own parser branch is removed. The gate is also faster: one awk pass over every header and one read of the allowlist, instead of a grep pipeline per header and an awk per listed header (measured roughly under Git Bash on Windows with other load running at the same time: about 3 s against about 3.5 min for the old gate; a reviewer saw 4-5 s).
  • ci_scripts/object-identifier-boundary-allowlist.txt re-baselined ONCE with the true counts: 217 across 87 headers -> 301 across 108. 9 counts rose: BACnetBusinessLogic.h 58 -> 75, BACnetDBDevice.h 8 -> 16, BACnetStackActiveCOVSubscriptions.h 2 -> 5, BACnetAcknowledgeAlarmRequest.h 1 -> 3, BACnetCOVNotificationRequest.h 1 -> 3, BACnetEventNotificationRequest.h 1 -> 3, BACnetInterface.h 3 -> 5, BACnetAccessEventEventAlgorithm.h 1 -> 2, BACnetTestToolInterface.h 9 -> 10. Those 9 rises add 38; 21 headers were added carrying 46 more (the largest BACnetSubscribeCOVProcessorHelpers.h 9), for the total rise of 84. Every one of the 217 previously counted lines is still counted. The file records that this was the one upward move; from here on a count may only go down.
  • rb e-20260924-148 (#2336, #2340; follow-ups to #2196): a BACnet/IP Network Port’s Property_List lists FD_BBMD_Address (418) and FD_Subscription_Lifetime (419), because 135-2024 Table 12-71.3 footnote 5 requires both “if, and only if, the port is capable of functioning as a foreign device”. Reading them failed when the application did not serve them: Abort(other), Error(property, value-not-initialized) or Error(object, read-access-denied). Both now read back successfully. The application’s own value wins. If the application does not serve one and the IPv4 data link the request arrived on is registered as a foreign device (and is not claimed by a different Network Port), the stack answers the BBMD address and time-to-live it registered with. Otherwise FD_BBMD_Address answers host ‘none’, port 0 (the encoding 12.56.37 gives BACnet_IP_Global_Address for “not yet configured”) and FD_Subscription_Lifetime answers 0 (the stack’s choice; 12.56.42 gives 0 no special meaning). An explicit error code the application’s callback names is still honoured for both, and an allocation failure answers Abort(out-of-resources). Limitation: a read arriving over a different data link (BACnet/SC, routed) sees the fallback even if another IPv4 data link is registered. IP_DNS_Server (406) is unchanged: 12.56.31 requires “at least one entry”, and serving one would reverse the deliberate #2164 test ReadUnimplementedArrayProperty_ReportsErrorNotAbort; that is a question for the owner on #2340. The existing section comment that labelled FD_BBMD_Address as 12.56.37 now cites 12.56.41.
  • rb e-20260924-148 (#2420): ci_scripts/check-object-identifier-boundary.sh now counts object-identifier pair parameters in every shape the issue listed: references and pointers, other and typedef’d types, T const name, default arguments, pairs separated by other parameters, reversed pairs, out-parameters, parallel arrays, and <prefix>ObjectType with <prefix>Instance (e.g. deviceObjectType, deviceInstance). Pairs are matched by parameter name (rule A: adjacent type then instance; rule B: same prefix and number, any position or order). Bare objectType with deviceInstance, numberOf/count prefixes, call arguments, member accesses and scope-qualified names are not pairs. These rules are the resolver’s reading of the owner’s #1592 ruling, written in the script header with its known limits. The Register*/Hook* mirror exemption now covers only that declarator’s own parameter list. Self-test 16 -> 28 cases; the new shape cases fail against the previous parser. ci_scripts/object-identifier-boundary-allowlist.txt re-baselined once more, 301 across 108 headers -> 317 across 112; no count went down and all 16 newly counted lines are listed in its note (BACnetTestToolInterface.h 10 -> 17 arrays, BACnetAuditLogQueryParameters.h 1 -> 3, BACnetYouAreRequest.h 0 -> 2, BACnetDBDevice.h 16 -> 17, BACnetInterface.h 5 -> 6, and one each in BACnetAtomicReadFileRequest.h, BACnetAtomicWriteFileRequest.h, BACnetCreateObjectProcessor.h).
  • rb g-20260923-1 (#2381): copying a BACnetDateTime, BACnetDateRange or BACnetUTCTimeSynchronizationRequest whose date/time was never set no longer logs Error: Failed to set the date/time / start/end date / datetime. The copy now reproduces the source’s fields and validity directly. This removes the error lines every SC-enabled build logged at startup (from BACnetSCHubConnector::Reset()), and the pair logged on each Date_List access of a stack-held Calendar with a Date-form entry. Revert-red: the stdout-capture tests Issue2381_UnspecifiedValueCopy.* failed on the pre-fix code.
  • rb g-20260923-3 (#2341): a BACnet/SC data link that the application has configured nothing on (no UUID, no hub URI, hub function and direct connect off), which is the normal state of every BACnet/IP- or MS/TP-only device built with SC, no longer logs UUID has not been set on the first tick. It also no longer latches the SC start-up error. The latch was only cleared by Reset(), so an application that ticked before configuring SC used to be left with SC stopped. A configured hub with no UUID still reports the error. Revert-red: Issue2341_UnconfiguredScTickIsSilentAndDoesNotLatch failed on the pre-fix code.
  • rb g-20260923-3 (#2337): decoding a remote device’s Trend Log / Trend Log Multiple Client_COV_Increment (datatype ClientCOV) or Schedule Effective_Period (DateRange) no longer logs unknown datatype=[77] / [76]. The value now decodes as a BACnetClientCOV / BACnetDateRange in every build configuration, where it used to come through as untyped raw primitives. Visible change: decode-as-JSON/XML output for these two properties now shows one ClientCOV / DateRange element instead of the bare REAL/NULL or two DATEs. A malformed value (for example an Unsigned where ClientCOV allows only REAL or NULL, or a DateRange with no end date) is now rejected, as with every other typed datatype. Revert-red: Issue2337_DecodeByDatatype* failed on the pre-fix factory.
  • rb g-20260923-4 (#2387 item 1; the issue stays open for item 4): a SubscribeCOVProperty that names a Property Array Index now captures its starting value from that element, the same read every later change check makes. It used to read the whole array, so the first comparison was element against array. That could send a spurious first notification or suppress a real one. Behaviour change: a SubscribeCOVProperty with an index that the property can’t serve is now refused with the same error ReadProperty returns (for example an index on a property that is not an array returns Error(PROPERTY, PROPERTY_IS_NOT_AN_ARRAY)). It used to be accepted with a SimpleACK, and then every check of that subscription failed. Revert-red: Issue2387_* in TestBACnetSubscribeCOVProcessorHelper.cpp failed on the pre-fix helper.
  • rb g-20260924-11 (#2414): when a router port’s pending Network_Number is activated (ReinitializeDevice ACTIVATE_CHANGES or WARMSTART, #2326), the physical router now moves that port and its directly connected route to the new number.
    • A route to the new number that was learned from the network is replaced by the direct one.
    • The start-up I-Am-Router-To-Network is re-armed, so the next router tick announces the new number.
    • Before this fix the router kept routing and announcing the old network after activation. Before the host’s CallbackSetPropertyUInt is called, activation is refused with PROPERTY / INVALID_CONFIGURATION_DATA if a router port’s new number is any of:
    • 0 or 65535;
    • another router port’s network or an operator route (BACnetStack_AddRouterRoute);
    • a virtual network;
    • the pending number of another port;
    • a number another Network Port object on the device already uses. A swap or chain of numbers must therefore be activated in two steps. Revert-red: the four activation tests in TestBACnetRouterPortNetworkNumberActivation.cpp fail with the activation hook reverted. Known gap: messages held while searching for the new number are not released after the move (#2421).
  • rb g-20260924-12 (#2421): when a router port’s pending Network_Number is activated onto a network N (#2414), the router now releases any messages it was holding while searching for a route to N, and delivers them through the port that now serves N.
    • Before, they stayed held and were later rejected as unknown-network.
    • A released message carries its ingress port’s current network number as its source network. Before, it carried the number stored when it was held, which could name a network that no longer exists.
    • A held message whose own ingress port moves onto its destination is dropped without a reject, as the network layer already does for any message whose destination turns out to be local. Revert-red:
    • Issue2421_HeldMessageForTheNewNetworkIsReleasedAtActivation failed without the release (nothing delivered, 2 still held).
    • With the previous source-network code it failed on SNET (200, not 400). Open question (#2424): whether peers should be told a renumbered port’s old network is gone.
  • rb g-20260924-5 (#2342): adapters/cpp/CMakeLists.txt now globs the stack sources with CONFIGURE_DEPENDS. A consumer that moves its submodule pin to a commit that adds a source/*.cpp no longer gets unresolved externals on an incremental cmake --build with the Makefiles or Ninja generators. Visual Studio and Xcode projects may still need a manual cmake re-run, as the comment at the glob says. Verified in a scratch copy: a file added after configure was missed before the change; after it, CMake reports GLOB mismatch! and builds the file.
  • rb g-20260924-6 (#2297, #152 follow-up): a segmented ComplexACK no longer exists twice in memory when the segmented response starts. The transaction state keeps the ACK itself, moved out of the outgoing packet, instead of a clone of it, and every segment is cut from that retained packet. Measured on the whole server path for a ReadProperty of Object_List (max-APDU 480, window 1): 703 objects (8 segments) peak heap 9059 -> 6393 bytes; 2000 objects (22 segments) 24299 -> 20985 bytes. Segments on the wire are unchanged: all 8 are pinned byte-for-byte as digests that were identical before and after the fix. Revert-red: Issue2297SegmentedResponseTest.SegmentedTransmitAddsNoSecondCopyOfTheAck_703Objects failed on the pre-fix code. Internal API: BACnetPacket::ReleaseAPDUOwnership().
  • rb bs-20260923-1 (#2051): the BACnetStack_SetTrendLogStartStopTime documentation (header, IFC-044, and the Node api.ts JSDoc) now says which clock Start_Time and Stop_Time are compared against. That clock is the GetSystemTime callback’s value, which is UTC, shifted by the Device’s UTC_Offset; with UTC_Offset unserved, build the window with gmtime (or JavaScript’s UTC getters), not local time. #2051’s “Record_Count never increments” was the B-BC example building the window from local time, fixed by chipkin/BACnetProfileExample-B-BC-CPP#9. The doc also now says that a 255 in any time field of a bound disables that whole bound. Found while verifying and filed as #2339: a served UTC_Offset is applied with the opposite sign to cl. 12.11.25.
  • rb bs-20260923-11 (#2359, rev 28 addendum co, owner ruling option 1): a host-reported Reliability is now a first-stage fault for an object with intrinsic alarming, and it takes precedence over the fault algorithm. 135-2024 cl. 13.2.2.2: “The first stage checks for internal faults such as configuration errors, hardware faults or communication failures. … Faults detected in the first stage shall take precedence over faults detected in the second stage.”
    • Before: for an object with a stack-held alarm-and-event object, the stack served Reliability, Status_Flags FAULT and Event_State from its own fault algorithm and never called CallbackGetPropertyEnumerated for Reliability. A fault only the host can see (NO_SENSOR, OPEN_LOOP, a lost field bus) was invisible.
    • Reads: Reliability and Status_Flags FAULT now reflect the host’s answer on every read.
    • Transitions:
      • Event_State moves to FAULT, with a CHANGE_OF_RELIABILITY notification, when the algorithms next run. That is on BACnetStack_UpdateValue of Reliability, which now runs the first stage at once and only the first stage, or on an update of the monitored property.
      • It returns to NORMAL when the host stops reporting the fault. If the fault algorithm, brought up to date on the current value, still sees its own fault, the object re-enters Fault with that Reliability instead.
      • A change from one host fault to another re-enters Fault. A host reporting the same fault the fault algorithm already reported is not a second ToFault.
      • Event_State reads FAULT only on an object with an intrinsic event algorithm attached; without one it always reads NORMAL, as before.
    • Precedence:
      1. Reliability_Evaluation_Inhibit: NO_FAULT_DETECTED.
      2. Otherwise the stack’s own CONFIGURATION_ERROR (#2349).
      3. Otherwise the host’s fault.
      4. Otherwise the fault algorithm’s result.
    • Event Enrollments watching such an object now see the host’s fault as MONITORED_OBJECT_FAULT. They read the object’s Reliability, which now includes it.
    • CASBACnetStackDLL.h: the BACnetStack_UpdateValue doc comment says all of the above. It is a comment-only change: no signature or export changed. See IFC-046.
    • Integrators:
      • A host whose Reliability callback already answers something other than NO_FAULT_DETECTED for an alarmed object will now see FAULT transitions and notifications for it.
      • A host with no enumerated-property callback, or one that does not answer Reliability (returns false), is unchanged.
    • Not changed:
      • Under Reliability_Evaluation_Inhibit a Reliability written while Out_Of_Service still reads NO_FAULT_DETECTED. cl. 12.2.33 carves out that case.
      • A WriteProperty to Reliability does not trigger an evaluation. cl. 13.2.2.3 asks for one; that gap is pre-existing. Both are filed as #2384.
  • rb bs-20260923-12 (#2352, #868 part G): the default BACnet Protocol_Revision is now 30 (was 26). BACNET_PROTOCOL_REVISION in source/CASBACnetStackOptions.h still defers to a -D / CIBuildSettings.h override.
    • Every rev 27-30 addendum item was audited before the flip:
      • rev 27: bx (#2346), ci (#2347)
      • rev 28: cj (#2348, #2354), co (#2349, #2358, #2359)
      • rev 29: cp (#2350, #2363)
      • rev 30: cm (#2351)
    • The audit is in _spec/spec-coverage/00-index.md, “Rev 27-30 addenda findings”. The clause-21 enumerations were already complete at revision 30.
    • What a peer sees: the Device object’s Protocol_Revision, BACnetStack_GetProtocolRevision and the capabilities XML now report 30. No >= / > BACNET_PROTOCOL_REVISION threshold in the stack is above 25, so no other compiled behaviour changes with the flip itself.
    • Not claimed at 30: target authorization (addendum cp, optional per cl. 17.5.3). The AA-AT-B, AA-NAT-B and AA-SAC-A BIBBs are ❌; #2364 and #2365 are deferred as features.
    • Known open gaps at 30, disclosed and not blocking by owner ruling:
      • #2353: VALIDATE_CHANGES, cl. 12.56.16 “If the device supports pending changes then the port shall support this command”. The owner ruled option 1; it is not implemented yet.
      • #2384: a Reliability written while Out_Of_Service. cl. 13.2.2.3 says the write shall cause the event state to be evaluated, and cl. 12.2.33 exempts that case from the inhibit rule. Both texts are likely from addendum co.
    • Integrators: a device certified or listed at an earlier revision can keep reporting it with -DBACNET_PROTOCOL_REVISION=26 (or 24).
  • rb bs-20260923-14 (#2284): client segmented receive now follows 135-2024 cl. 5.4.4.3 / 5.4.4.4:
    • Segmented response with receive not enabled (the default): answered with Abort INVALID_APDU_IN_THIS_STATE and reported to HookAbort, as cl. 5.4.4.3 UnexpectedPDU_Received requires. It used to be reassembled anyway. Behaviour change for a client left at the default whose peer segments despite SA = FALSE.
    • No retries while segments arrive. Once the first segment arrives, the request is no longer retried. Each next segment must arrive within four times APDU_Segment_Timeout, or HookAbort(TSM_TIMEOUT) fires (cl. 5.4.4.4 Timeout). Before, the request’s APDU timer kept running and could resend the request mid-response.
    • Unexpected PDUs while segments arrive (cl. 5.4.4.4 UnexpectedPDU_Received): a SimpleACK, non-segmented ComplexACK, Error or Reject for a request whose response is arriving in segments is answered with Abort INVALID_APDU_IN_THIS_STATE and HookAbort, and the partial reassembly is discarded. It used to be delivered as an ordinary confirmation.
    • A first segment whose sequence number is not 0 (cl. 5.4.4.3 UnexpectedPDU_Received) aborts the outstanding request (INVALID_APDU_IN_THIS_STATE, HookAbort). It used to be dropped while the request kept retrying. With no request outstanding, the segment is still dropped.
    • Too many segments. A segment beyond Max_Segments_Accepted, or beyond the reassembly buffer, is answered with Abort BUFFER_OVERFLOW and reported to HookAbort (cl. 5.4.4.4 NewSegmentReceived_NoSpace). It used to be dropped silently.
    • Builds without STACK_OPTION_SEGMENTATION can never receive segments, so they now abort every segmented ComplexACK. Before, they reassembled it.
    • BACnetConfirmedRequestPDU::MaxSegmentsAcceptedToEnum rounds down (3 accepted encodes as 2, not 4) and no longer truncates the count to 8 bits.
  • rb bs-20260923-2 (#1758): behaviour change. A Schedule exception event added with BACnetStack_AddScheduleExceptionEventWithCalendarReference now takes effect on the dates the referenced Calendar’s Date_List contains, when that Calendar is local and stack-held. Before, such events never matched.
  • rb bs-20260923-2 (#1758): behaviour change (135-2024 cl. 12.1.7). In a BACnetDateRange, whether a Calendar Date_List entry or a Schedule DateRange exception period, a fully unspecified endpoint (year 0 with month and day 255) is now open-ended: an unspecified start means “up to and including the end”, an unspecified end means “on or after the start”, and both unspecified means “always”. Before, such an endpoint was compared literally: an open end happened to work, but a range with an open start never matched.
  • rb bs-20260923-2 (#1758): behaviour change (135-2024 cl. 21). BACnetWeekNDay week-of-month 7, 8 and 9 (“the 7 days prior to the last 7 / 14 / 21 days of this month”) now match, in Calendar Date_List entries and in Schedule WeekNDay exception periods. Before, they silently never matched.
  • rb bs-20260923-2 (#1758): a WriteProperty that empties a stack-held Calendar’s writable Date_List is accepted; before, an empty list failed the generic datatype check. A DateRange Date_List element whose endpoint is neither unspecified nor a specific date (cl. 12.1.7) is refused with PROPERTY / VALUE_OUT_OF_RANGE on WriteProperty and AddListElement.
  • rb bs-20260923-3 (#2346, rev 27 addendum bx audit for #868): the BACnet/SC Network Port no longer lists or serves Device_Address_Proxy_Enable and Device_Address_Proxy_Timeout. Table 12-71.8 footnote 9 makes them “Required to be present and writable if the port supports device address proxying as specified in Clause 19.9”. The stack has never done that proxying: it never answers Who-Is from a Device_Address_Proxy_Table and never changes routing. Writing TRUE was accepted and read back, but did nothing.
    • They are no longer in Property_List. A ReadProperty or WriteProperty of either returns UNKNOWN_PROPERTY, and so does an internal SetNetworkPortProperty write.
    • RESTART_DEVICE_DISCOVERY is refused with PROPERTY / OPTIONAL_FUNCTIONALITY_NOT_SUPPORTED.
    • The spec-coverage claims for DM-DAP-B and B-DAP are withdrawn (❌). Implementing proxying is #2032.
  • rb bs-20260923-3 (#2347, rev 27 addendum ci): more Network Port Command errors now follow cl. 12.56.16:
    • An unsupported command reports error class PROPERTY, not SERVICES.
    • Writing IDLE returns PROPERTY / VALUE_OUT_OF_RANGE.
    • RENEW_FD_REGISTRATION or RENEW_DHCP on a non-IP port, RESTART_SUBORDINATE_DISCOVERY on a non-MS/TP port, and DISCONNECT on a non-PTP port return PROPERTY / VALUE_OUT_OF_RANGE.
    • VALIDATE_CHANGES and GENERATE_CSR_FILE on a BACnet/SC port return PROPERTY / OPTIONAL_FUNCTIONALITY_NOT_SUPPORTED instead of WRITE_ACCESS_DENIED.
    • RESTART_PORT while Changes_Pending is TRUE returns PROPERTY / INVALID_VALUE_IN_THIS_STATE. It used to be accepted.
    • Still open: VALIDATE_CHANGES support (#2353), and the port-state rules for FOREIGN mode and disabled DHCP or autonegotiation (#2355).
  • rb bs-20260923-7 (#2361): three object-type options now imply the options their code already needed, the same way Alert Enrollment does:
    • STACK_OPTION_OBJECT_TYPE_NOTIFICATION_FORWARDER now also defines STACK_OPTION_OBJECT_TYPE_NOTIFICATION_CLASS.
    • STACK_OPTION_OBJECT_TYPE_ACCESS_POINT now implies STACK_OPTION_OBJECT_TYPE_ACCESS_CREDENTIAL.
    • STACK_OPTION_OBJECT_TYPE_TIMER now implies STACK_OPTION_OBJECT_TYPE_SCHEDULE. Each of those option-only builds failed to compile before this change, so no configuration that worked gains an object type.
  • rb cloud-20260924-3 (#2288, round-2 note of PR #2428): ci_scripts/generate-node-api-params.js --gate now decides the callback-type exemption per slot.
    • An allowlisted entry’s derived callback type is compared with api.ts for every Function slot, unless that same slot carries a reason other than unnamed/required.
    • Before, one unrelated reason on another slot (for example default@3) switched the check off for the whole entry.
    • No entry in today’s allowlist is affected: 292 byte-checked, 95 allowlisted, 0 mismatches. The rule guards future entries.
    • Revert-red: new self-test corpus 27 failed 3 of 4 cases under the old rule.
  • rb cloud-20260924-5 (#625): adapters/java/pom.xml’s exec plugin now reads its main class from the exec.mainClass property, still defaulting to SimpleServerExample, so -Dexec.mainClass=... selects another example. Before, the hard-coded <mainClass> ignored the override.
  • rb e-20260923-138 (#2207): BACnet/SC wire-format change.
    • SC_Primary_Hub_Connection_Status and SC_Failover_Hub_Connection_Status now encode the ASHRAE 135-2024 cl. 21.6 BACnetSCHubConnection: connection-state [0], connect-timestamp [1], disconnect-timestamp [2], error [3] OPTIONAL, error-details [4] OPTIONAL. The previous encoding put a non-standard hub-uri at [0], shifted every other member by one context tag and had no timestamps, so a conformant peer mis-read the value. The hub URI is no longer part of this value; it is still available from SC_Primary_Hub_URI / SC_Failover_Hub_URI.
    • SC_Hub_Function_Connection_Status is now a list of the cl. 21.6 BACnetSCHubFunctionConnection (connection-state [0] … peer-address [3], peer-vmac [4], peer-uuid [5], error [6], error-details [7]), as cl. 12.56.93 requires, instead of a list of BACnetSCHubConnection carrying the peer’s connection string in hub-uri. The stack does not know an accepted peer’s network address (it sees only the host’s <acceptUri>|client=<token> connection string), so peer-address is reported as host none, port 0.
    • Both structures omit error / error-details when the state is NOT_CONNECTED or CONNECTED (cl. 12.56.88 / 12.56.93: “this field shall be absent”), on the wire and in DecodeAs JSON/XML output — BACnetSCHubFunctionConnection previously rendered empty Error/ErrorDetails members even when they were absent.
    • C++ API: BACnetSCHubConnection::GetHubUri(), SetHubUri() and the BACnetSCHubConnection(uri, state) constructor are removed, and the BACnetSCHubConnectionSequenceEnum values are renumbered to the cl. 21.6 tags (hubUri removed; connectTimestamp/disconnectTimestamp added). BACnetDataLinkSC::GetHubFunctionConnectionStatuses() and BACnetStackNetworkPortSC::Get/SetSCHubFunctionConnectionStatus() now use BACnetSCHubFunctionConnection. No change to CASBACnetStackDLL.h.
  • rb e-20260923-139 (#927): Integrator-facing behaviour change. Confirmed-request Send* functions now return false and do not transmit if the stack cannot track the transaction (no CallbackGetSystemTime registered, or the sent-request table is full); previously they returned true for an untracked request.
    • The client transaction (the sent-confirmed-request entry that drives the APDU timeout and retries, ASHRAE 135-2024 cl. 5.4) is now created before the frame is handed to CallbackSendMessageForPort, and removed again if that send fails. This applies to BACnet/IP, MS/TP and BACnet/SC. Unconfirmed requests, replies and router-forwarded traffic are unaffected, and so are the later segments of a segmented confirmed request, which belong to the transaction created with the first segment.
    • A normal send now refuses a second outstanding request to the same peer with the same invoke id (the sent-request table refuses the duplicate (invoke-id, destination) key), returning false without transmitting. The test-tool invoke-id generator (BACnetStackTestTool_RegisterCallbackInvokeIdGenerator, #84) may still force such a duplicate: it is sent untracked, and the first request’s transaction is left as it is. This exception is the owner’s ruling on #927 (2026-09-23, option 1): test-tool builds only, only for an id the generator produced for that same peer, and only when the refusal is the duplicate-key one. Every other refusal still blocks the send. It is marked TEST-TOOL ONLY ... NEVER promote this to the customer interface (CASBACnetStackDLL.h) at BACnetStackTestTool_RegisterCallbackInvokeIdGenerator, the exception block in BACnetDataLink::CreateConfirmedRequestTransactionBeforeSend, BACnetDatabase::IsInvokeIdForcedByGenerator and BACnetStackInvokeIds::IsLastGeneratedInvokeId (comment-only; IFC-049). The #84 test’s citation now names 135.1-2023 13.1.4, where the TD re-sends C1 with the same invoke id while it is outstanding, in place of 13.9.1 / 13.9.2.
    • A host that does not serve APDU_Timeout / Number_Of_APDU_Retries through CallbackGetPropertyUInt still gets the stack’s Device defaults (10000 ms, 0 retries). No public API (CASBACnetStackDLL.h) signature changes. Internal C++ signatures BACnetStackSentConfirmedRequests::InsertRequest, BACnetDatabase::AddSentConfirmedRequest and BACnetDataLink::HandleConfirmedRequest each gained a trailing, defaulted refusal-reason out-parameter.
    • Checklist item 3 (resend exact-length check) was already fixed by #1573 (1ff6ad8b6); no change here.
    • Verification: the suite figures for this batch come from the MSBuild Debug x64 build on Windows (no WSL).
  • rb e-20260923-143 (#2378): Not reproduced; tests only, no behaviour change. SubscribeCOVProperty and SubscribeCOVPropertyMultiple already reject a negative, NaN or infinite (+/-Inf) COV Increment with PROPERTY / VALUE_OUT_OF_RANGE and create no subscription (#688). The spec’s error tables (135-2024 cl. 13.15.1.3.1, cl. 13.16.3.2.2.3) have no row for a bad increment, so the error class and code are an implementation choice; this batch keeps #688’s choice. New tests cover the rejection on both service paths, including -Inf, with the error class and code asserted, and cover that a COV Increment of 0 or a positive value is accepted.
  • rb e-20260923-144 (#1872): internal API only, no public change. BACnetDBDevice::IsPropertyEnabled now takes a packed BACnetObjectIdentifier32 instead of an (objectType, objectInstance) pair. This was the last pair-form method in #1872’s list. All 50 source callers and 251 test callers are switched, and no pair forwarder remains for it. (Two earlier #1872 methods keep pair forwarders only for their session-D callers in BACnetTestToolInterface.cpp: AddAveragingObject and AddTimerObjectPropertyReference; #1876 deletes them.) Callers that already hold a packed identifier pass it through. Other callers pack with GenerateObjectIdentifier, the same function the method used to call internally, so an object lookup gives the same result as before. One unreachable difference: an object type above 1023 is now read as its 10-bit value (as the already-packed sibling methods such as IsPropertyWritable do); no caller can pass one. ci_scripts/object-identifier-boundary-allowlist.txt: BACnetDBDevice.h 46 -> 45.
  • rb e-20260923-145 (#1873, part C of #1592): internal API only, no public change. Partial slice: #1873 stays open. The 20 BACnetDBDevice::EnableIntrinsic*Algorithm methods (OutOfRange, SignedOutOfRange, UnsignedOutOfRange, DoubleOutOfRange, FloatingLimit, UnsignedRange, BufferReady, ChangeOfStateAlgorithmBool/Enum/Unsigned, ChangeOfLifeSafety, AccessEvent, ChangeOfStatusFlags, ChangeOfBitstring, ChangeOfBitstring64, ChangeOfCharacterstring, ChangeOfDiscreteValue, CommandFailure, ChangeOfValue, ChangeOfTimer) now take a packed BACnetObjectIdentifier32 instead of an (objectType, objectInstance) pair. The issue body says “22 variants”, but its own list names 20, and 20 is what the header held. EnableIntrinsicAccessEventAlgorithm also takes its access-credential identifier packed. Every BACnetDatabase wrapper already held a packed identifier and now passes it straight through instead of unpacking it for the leaf to repack. The wrappers’ unpack locals are gone; the error logs read objectIdentifier.ObjectType()/ObjectInstance() inline, so no local is left unused when APIDebugLogError compiles to nothing. The leaves’ own 66 internal repacks for SetPropertyEnabled now use the parameter directly. 12 test call sites pack with FromRaw(GenerateObjectIdentifier(...)). No session-D file calls these leaves, so no pair forwarder is kept (the existing BACnetDatabase::EnableIntrinsicAccessEventAlgorithm pair forwarder for BACnetTestToolInterface.cpp is unchanged). No behaviour change on any reachable path: the BACnetDatabase callers were already packing at the boundary, and every test identifier is in range. ci_scripts/object-identifier-boundary-allowlist.txt: BACnetDBDevice.h 45 -> 25.
  • Still pair-form on #1873 (23 of the 25 remaining BACnetDBDevice.h pair declarations; the other 2, AddAveragingObject and AddTimerObjectPropertyReference, are #1872’s session-D forwarders): EnableFaultOutOfRangeAlgorithmReal, EnableFaultCharacterstringAlgorithm, SetAlarmsAndEventsForObjectEnabled, SetAccessEventContext, GetAlarmAndEventObject, GetAlarmAndEventTarget, RaiseAlert, the five AddEventEnrollmentObject* methods, SetEventEnrollmentAlgorithmPropertyReference, SetEventEnrollmentFaultAlgorithmPropertyReference, ApplyLifeSafetyOperation, ApplyLifeSafetyOperationToOneObject, FeedLifeSafetyIntrinsicAndReport, NotifyLifeSafetyStateChanged and SetLifeSafety{AcceptedModes,AlarmValues,FaultValues,LifeSafetyAlarmValues,TrackingValue}. (EnsureFaultAlgorithm, the other five EnableFault*Algorithm methods, EraseFromEngineMaps, the pending-queue methods and GetLifeSafetyObject were converted by earlier slices.) The issue’s (1023, 4194302) acceptance test and its pair-forwarder control are not yet added.
  • rb e-20260923-146 (#1873, part C of #1592): internal API only, no public change. This completes #1873’s method list. The last 23 BACnetDBDevice methods on it now take a packed BACnetObjectIdentifier32 instead of an (objectType, objectInstance) pair: EnableFaultOutOfRangeAlgorithmReal, EnableFaultCharacterstringAlgorithm, SetAlarmsAndEventsForObjectEnabled, SetAccessEventContext, GetAlarmAndEventObject, GetAlarmAndEventTarget, RaiseAlert (its alert source), the five AddEventEnrollmentObject* methods (the monitored object; the Event Enrollment’s own instance was never a pair), SetEventEnrollmentAlgorithmPropertyReference and SetEventEnrollmentFaultAlgorithmPropertyReference (the referenced object), ApplyLifeSafetyOperation, ApplyLifeSafetyOperationToOneObject (the #1786 depth-first zone walk is unchanged: same visit order, same visited guard, now keyed by objectIdentifier.Raw()), FeedLifeSafetyIntrinsicAndReport, NotifyLifeSafetyStateChanged and SetLifeSafety{AcceptedModes,AlarmValues,FaultValues,LifeSafetyAlarmValues,TrackingValue}.
  • Callers: every BACnetDatabase wrapper already held a packed identifier and now passes it straight through (11 sites); their unpack locals are gone and their error logs read objectIdentifier.ObjectType()/ObjectInstance() inline. The same was done for the five older EnableFault*/SetFault* wrappers (Listed, StatusFlags, LifeSafety, State, Extended) whose locals were already log-only. Inside BACnetDBDevice, 27 leaves (the 20 EnableIntrinsic* and 7 EnableFault* methods) pass objectIdentifier to GetAlarmAndEventTarget directly. That left an objectInstance local used only by log macros in 24 of them; those locals, and EnsureFaultAlgorithm’s already-unused one, are removed (25 in all). The two newly converted EnableFault* methods never had such a local. Stack-internal callers that only hold a pair (access-point context updates, the life-safety roll-up and WriteProperty paths, AcknowledgeAlarm, CheckForAlarmsAndEvents, LifeSafetyOperation) pack with FromRaw(GenerateObjectIdentifier(...)), which is exactly what the methods did internally. 140 test call sites were rewritten the same way.
  • Pair forwarders kept, because BACnetTestToolInterface.cpp (session D) still calls them with a pair: BACnetDBDevice::RaiseAlert and the five SetLifeSafety* setters. #1876 deletes them. ci_scripts/object-identifier-boundary-allowlist.txt: BACnetDBDevice.h 25 -> 8 (these 6 plus #1872’s AddAveragingObject and AddTimerObjectPropertyReference).
  • No behaviour change on any reachable path. Every caller either held an in-range packed identifier or packs the same way the method used to. GetAlarmAndEventObject’s key is still FromRaw(GenerateObjectIdentifier(type, instance)). New tests in BACnetDBDeviceAlarmAndEventPackedKeyTest. They deviate from the issue’s acceptance wording, which asks to enable an intrinsic algorithm on (1023, 4194302). Type 1023 is proprietary and cannot carry a standard intrinsic algorithm, so the test is split in two:
    • OUT_OF_RANGE is armed on (analogValue, 4194302) and found in GetAlarmAndEventObjects() under the literal key 12582910.
    • (1023, 4194302) is registered and found under the literal key 4294967294. The lookup is asserted to succeed first, so the refused arming can only be the object-type check.
    • The issue’s “pair-forwarder control” is the pair-form packing expression FromRaw(GenerateObjectIdentifier(type, instance)), because the alarm-and-event engine has no pair forwarder. The control asserts that expression reaches the same map entry.
    • Revert-red: keying the lookup by ObjectInstance() instead of Raw() fails both tests.
  • Gate blind spot, not fixed here: check-object-identifier-boundary.sh counts pair declarations one line at a time, so a pair split across lines (for example SetGeneratedPropertyValue and ObjectAndPropertyExists in BACnetDBDevice.h) is not counted. None of those methods is on #1873’s list. Tracked in #2397.
  • rb e-20260924-149 (#2424): no behaviour change. 135-2024 clause 6 defines no network-layer message that tells peer routers a network is no longer reachable through a router, so a router port renumbered by activating its Network_Number (#2414) sends none for its old network. Router-Busy-To-Network (cl. 6.6.3.6) is temporary congestion control that clears on a 30-second timer, and Initialize-Routing-Table with a Port ID of zero (cl. 6.4.7) is a purge a management node sends to a router. Neither is a withdrawal the renumbering router is required to send. What the standard does define is cl. 6.6.3.5: “Reject-Message-To-Network is generated by a router when it receives a message that it is unable to relay to the DNET specified in the NPCI”, and a router that receives reason 1 marks that network “permanently unreachable”. Two regression-guard tests in tests/TestBACnetRouterPortNetworkNumberActivation.cpp (Issue2424RetiredNetworkTest) pin what the stack’s router already does. Before activation, a message for the old network is relayed. After activation, the router first searches with Who-Is-Router-To-Network (cl. 6.6.3.1), holds the message, and after TOmin answers the originator with Reject-Message-To-Network, reason 1, for the old network, asserted as exact wire octets.
  • rb e-20260924-150 (#1865; IFC-051): breaking for C callers. BACnetStackTestTool_SendConfirmedRequestRaw and BACnetStackTestTool_SendUnknownNetworkLayerMessage take two new trailing parameters, networkPriority (0..3, NPDU control bits 1-0) and hopCount (0..255, on the wire only when DNET is present, per 135-2024 cl. 6.2.3). A priority above 3 returns false and nothing is sent. Both are marked TEST-TOOL ONLY: never promote them to the customer interface, because an originating device’s Hop Count “is initialized to a value of X’FF’”. For a segmented raw request they apply only to the first segment. In Node, api.ts makes both optional (defaults 0 and 255), so existing JavaScript callers are unchanged. Customer sends are unchanged (normal priority, hop count 255).
  • rb e-20260924-150 (#2416; IFC-050): comment only. The invoke-id generator hook’s doc comments now cite 135.1-2023 13.1.4 instead of BTL 13.9.1 / 13.9.2.
  • rb g-20260923-1 (#2339): behaviour change for hosts that serve the Device’s UTC_Offset. The stack now applies it with the sign in 135-2024 cl. 12.11.25: local time = UTC − UTC_Offset, with west of Greenwich positive (Pacific standard time is +480). It used to add the offset, so a host serving the spec value got stack-generated local timestamps 2 × offset wrong. That covered Trend Log / Trend Log Multiple / Event Log record timestamps and Start_Time/Stop_Time windows, COV-multiple timestamps, and access credential/zone times. A host that served a negated value to compensate (for example −420 for UTC−7) must now serve the spec value (+420). With UTC_Offset unserved (0), nothing changes. BACnetStack_ReadTrendLogRecord still exports the stored local time as an epoch value without the offset (#2392), and some stack-generated timestamps still ignore the offset (#2393). Revert-red: Issue2339_UtcOffsetSign.* failed on the pre-fix code, with the offset-0 control passing.
  • rb g-20260923-2 (#2393, parts 2–4 of 5; the issue stays open): more stack-generated local timestamps now apply the Device’s UTC_Offset (135-2024 cl. 12.11.25, as in #2339). These are the time-change records that TimeSynchronization and UTCTimeSynchronization write into Trend Log, Event Log and Trend Log Multiple, and the Timestamp in a COVNotificationMultiple the stack sends. Access Credential Days_Remaining (12.35.14) and Absentee_Limit (12.35.16) now count local calendar days. They used to count UTC days, so at +480 a day ended at 16:00 local. With UTC_Offset unserved (0), nothing changes. Revert-red: Issue2393* failed on the pre-fix code, with the offset-0 controls passing.
  • rb g-20260924-10 (#2410): these stack-generated local timestamps now use the Device’s UTC_Offset (135-2024 cl. 12.11.25, as in #2339, #2393 and #2407) instead of UTC:
    • Timer: Update_Time and Expiration_Time, set on a tick or a write.
    • Averaging: Minimum_Value_Timestamp and Maximum_Value_Timestamp.
    • Binary Lighting Output: Time_Of_Active_Time_Reset and Time_Of_Strike_Count_Reset.
    • Access Point: Access_Event_Time, from a tick or a write.
    • Last_Command_Time and Command_Time_Array for Lighting Output (Present_Value and Lighting_Command), Binary Lighting Output and Access Door. Behaviour change: on a Device that serves UTC_Offset, these values are now local time rather than UTC. With UTC_Offset not served, they are unchanged (UTC). The Access Credential engine now reads UTC_Offset the same way. It no longer logs two errors on every tick when the Device does not serve UTC_Offset. Left as they are:
    • BACnet/SC connection timestamps (#2330);
    • the access-token validity comparison (#2417). Revert-red: the 10 served-offset Issue2410_* tests failed on the pre-fix code (hour 20, not 21, at UTC_Offset −60).
  • rb g-20260924-14 (#2288, partial): ci_scripts/generate-node-api-params.js now derives the TypeScript type of a callback parameter from the Node addon trampoline’s JS call list, cross-checked against the C typedef, and byte-checks it against api.ts. There is no product change.
    • 20 more api.ts functions are byte-checked: 262 → 282.
    • The shape allowlist shrank from 118 to 98 entries.
    • For 11 more callbacks, whose parameter is named FPCallback… or declared required although the wrapper accepts undefined, the callback’s type is still compared; only the name or the optionality is exempt.
    • Mismatches the derivation exposed between trampolines, typedefs and api.ts are filed as #2427.
  • rb g-20260924-15 (#2427, item 3): the Node addon’s Date/Time and SetSystemTime callback trampolines now name their parameters as source/BACnetCallbackInterface.h does (yearMinus1900, hundredthSecond). There is no behaviour change. The four Get/SetPropertyDate/Time registrations now match the generator’s model and are generated (ci_scripts/generate-napi-callbacks.js). Their api.ts callback types are now byte-checked (282 → 286). The JS-visible fixes are waiting for an owner ruling on #2427:
    • SetPropertyBool’s value should arrive as a boolean, not 0/1;
    • SetSystemTime and GetListOfEnumerations should honour the handler’s result;
    • three callbacks that accept undefined should be marked optional.
  • rb g-20260924-16 (#1874, slice 1 of part D of #1592): the 38 private UseCallback* helpers in BACnetBusinessLogic now carry a BACnetObjectIdentifier32 instead of an object-type/instance pair. They unpack only where the host callback is called. This is an internal refactor with no API or behaviour change. The pair-declaration allowlist for BACnetBusinessLogic.h drops from 75 to 37. A new test pins the object type and instance each host callback receives, for 29 helpers, on (analogInput, 4194302) and on (1023, 0).
  • rb g-20260924-17 (#1874, slice 2 of part D of #1592): 33 more BACnetBusinessLogic methods now carry a BACnetObjectIdentifier32 instead of an object-type/instance pair. They are the typed getters, the priority-array getters, the validation, COV/alarm, list-element and BBMD helpers, and SendExternalWriteProperty. Their callers in 18 source files move with them. This is an internal refactor with no API or behaviour change. The pair-declaration allowlist for BACnetBusinessLogic.h drops from 37 to 2, leaving only GetProperty/SetProperty for slice 3.
  • rb g-20260924-7 (#2393, the remaining parts; the issue is now complete): the Device’s UTC_Offset (135-2024 cl. 12.11.25) now also applies to the three places below. With UTC_Offset unserved (0), the first two are unchanged.
    • Load Control Start_Time: a shed starts at its local Start_Time, not UTC_Offset early.
    • Log-status records: the records a Trend Log, Trend Log Multiple or Event Log inserts when Enable, Stop_When_Full or Record_Count is written.
    • COV-multiple time of change: the time of change of each value in a COVNotificationMultiple.
  • Behaviour change: each COV-multiple value’s time of change used the host’s time zone (localtime(), result not checked for NULL). It now uses UTC_Offset, like the notification’s own Timestamp, so with UTC_Offset unserved it is UTC rather than the host zone.
  • Revert-red: Issue2393_* (Load Control, log-status record, time of change) failed on the pre-fix code. Event timestamps and Recipient_List time windows still use the host zone (#2407).
  • rb g-20260924-8 (#2407): these timestamps and windows now use the Device’s UTC_Offset (135-2024 cl. 12.11.25, as in #2339 and #2393) instead of the host’s time zone (localtime()):
    • Event_Time_Stamps and event notification timestamps from the intrinsic alarm engine and Event Enrollment;
    • acknowledgement notifications;
    • alert enrollment alerts;
    • Recipient_List Valid_Days / From_Time / To_Time windows. Behaviour change:
    • With UTC_Offset unserved, all of these are now UTC rather than the host zone.
    • The one-argument BACnetTimeStamp(time_t) conversion is now UTC too, so Last_Command_Time and Last_Restore_Time are UTC until #2410 passes them the offset.
    • The stack no longer calls localtime() anywhere. Revert-red: Issue2407_EventTimeStamps.ToOffnormalTimeStampIsLocalAtTheServedOffset failed on the pre-fix code (hour 12, host US Pacific, instead of 21 at −60).
  • rb g-20260924-9 (#2326): Behaviour change for integrators. A Network Port Network_Number write now takes effect only after ReinitializeDevice ACTIVATE_CHANGES or WARMSTART (135-2024 cl. 12.56.13, owner ruling on #2326). This covers every port type, including router and virtual-router ports.
    • CallbackSetPropertyUInt for Network_Number is called at activation, not at the write.
    • The write is refused (Abort) unless both CallbackSetPropertyUInt and CallbackReinitializeDevice are registered, because without them the value could never be activated. The ReinitializeDevice service must also be enabled for a client to activate the value.
    • Between the write and activation:
      • Changes_Pending reads TRUE. It used to be a constant FALSE on every port except BACnet/SC.
      • A read of Network_Number returns the written value.
      • Routing and Network_Number_Quality keep using the old value.
      • A host Get callback registered for Network_Number or Changes_Pending still takes precedence over the stack’s own value (#2413).
    • Activation is all or nothing across the device. If the host refuses any value:
      • nothing is activated and the values stay pending;
      • ports the host had already accepted in that pass are called again with the value that stays in use;
      • the ReinitializeDevice request answers PROPERTY / INVALID_CONFIGURATION_DATA.
    • A virtual network’s number moves in the virtual router’s routing map at activation. The stack refuses the activation, before the host is asked, when:
      • the new number is already another virtual network, or
      • two virtual ports are pending the same number. A swap or chain of numbers must be activated in two steps.
    • COLDSTART does not activate pending values.
    • Known gaps, filed as follow-ups:
      • #2412: activation runs before the host’s CallbackReinitializeDevice checks the password.
      • #2414: the physical router’s own port table keeps the old number.
    • Writing DISCARD_CHANGES to a port’s Command property now drops a pending Network_Number on any port type. Before, a non-SC port answered OPTIONAL_FUNCTIONALITY_NOT_SUPPORTED. Revert-red:
    • BACnetIPVirtualNetworkPortTests.VirtualNetworkPort_WritingToNetworkNumber_WithCallbackSetPropertyUInt failed on the pre-fix code: the host had already been given 5000 at the write.
    • The new NetworkPortPendingNetworkNumberTest cases and the virtual-router clash tests fail under mutation.
  • rb bs-20260923-14 (#2284, #1847): the stack can now receive a segmented response as a BACnet client, so a read larger than one APDU (a large Object_List, ReadPropertyMultiple) no longer has to be split into per-index reads. See IFC-047.
    • BACnetStack_SetSegmentationEnabled accepts SEGMENTED_BOTH (0) and SEGMENTED_RECEIVE (2); maxSegmentsAccepted must then be greater than 1 (cl. 12.11.20). Before, both returned false.
    • With reception enabled, outgoing Confirmed-Requests set segmented-response-accepted = TRUE (cl. 20.1.2.3) and max-segments-accepted from Max_Segments_Accepted (cl. 20.1.2.4, rounded down). The advertised count is capped at what the reassembly buffer holds: STACK_OPTION_MAX_RECEIVED_SEGMENTED_RESPONSE_SIZE (65000 on a full build, 16384 on embedded) divided by Max_APDU_Length_Accepted.
    • The same setter also turns on server-side segmented-request receive (#1844-#1846) for customers. It needs CallbackGetSystemTime. The internal test-only seam SetSegmentationEnabledForTesting is removed.
    • The peer must support segmented transmit. A device reporting Segmentation_Supported = NO_SEGMENTATION, like the FieldServer in #2284, must still abort a response that does not fit one APDU (cl. 5.4.5.3).
  • rb bs-20260923-15 (#498): the stack can now run the BACnet/SC certificate management procedures (NM-SCCM-A, 135-2024 cl. 19.8.3) against a remote device, under the new build option STACK_OPTION_NM_SCCM_A. The option requires STACK_OPTION_DATA_LINK_LAYER_SC, and STACK_OPTION_ENABLE_ALL_BIBBS enables it. See IFC-048.
    • BACnetStack_SCCertificateAddIssuer (cl. 19.8.3.1): finds the issuer file that does not hold the current issuer certificate, clears it, writes the new issuer certificate into it, then ReinitializeDevice.
    • BACnetStack_SCCertificateReplaceOperational (cl. 19.8.3.2): an optional GENERATE_CSR_FILE, then it reads the CSR and hands it to the sign-request callback (BACnetStack_RegisterCallbackSCCertificateSignRequest). Once the host passes the signed certificate to BACnetStack_SCCertificateProvideSignedCertificate, it clears and writes the operational certificate file, then ReinitializeDevice.
    • BACnetStack_SCCertificateRemoveIssuer (cl. 19.8.3.3): clears the issuer file that does not hold the current issuer certificate, then ReinitializeDevice.
    • BACnetStack_RegisterHookSCCertificateProcedure reports each step as InProgress, Succeeded or Failed. A Failed report carries the target’s Error class and code, or the stack’s own failure code (reject, abort, timeout, current issuer not found, signing abandoned, send failed, invalid response). BACnetStack_SCCertificateCancel stops a procedure.
    • After GENERATE_CSR_FILE, the procedure reads Command until it is IDLE again (cl. 12.56.16), polling once a second for up to two minutes. Only then does it read the CSR, so the CA signs the new key pair’s CSR.
    • The hook and the sign request fire from BACnetStack_Tick after the procedures are updated. Cancel, ProvideSignedCertificate or a new start may be called from inside them.
    • Replies, timeouts and aborts for a procedure’s own requests go to the procedure only. They no longer reach the generic HookError / HookReject / HookAbort / HookTimeout callbacks. This includes a segmentation abort this device raises on a procedure’s transaction.
    • STACK_OPTION_NM_SCCM_A also enables STACK_OPTION_ATOMIC_FILE_IO and STACK_OPTION_DM_RD_REINITIALIZE_DEVICE, the client services the BIBB initiates. DM_RD also enables ReinitializeDevice execution.
    • The Node bindings are deferred (WP-9).
  • rb bs-20260923-2 (#1758, #2034; IFC-045): stack-held Calendar objects. BACnetStack_AddCalendarObject hands a Calendar to the stack. The new BACnetStack_AddCalendarDateEntry, BACnetStack_AddCalendarDateRangeEntry and BACnetStack_AddCalendarWeekNDayEntry populate its Date_List, and BACnetStack_ClearCalendarDateList empties it. The stack then:
    • serves Date_List on ReadProperty/RPM (cl. 12.9.6);
    • derives Present_Value from the device’s Local_Date, TRUE if any entry matches (cl. 12.9.5), reporting a change for COV;
    • supports WriteProperty, AddListElement and RemoveListElement on Date_List once the host makes it writable. The feature is opt-in: Calendars only added with BACnetStack_AddObject are still served by the host callbacks. Node: new typed functions of the same names in api.ts.
  • rb bs-20260924-1 (#2430): the Node adapter binds the seven BACnet/SC certificate procedure exports from #2406 (IFC-048), so a Node host can run NM-SCCM-A.
    • Added to api.ts and the addon: SCCertificateAddIssuer, SCCertificateReplaceOperational, SCCertificateRemoveIssuer, SCCertificateProvideSignedCertificate, SCCertificateCancel, RegisterCallbackSCCertificateSignRequest and RegisterHookSCCertificateProcedure.
    • The five non-callback wrappers are generated from new ci_scripts/adapter-roles.json entries. The two callback registrations are hand-written, like the other SC callbacks, and fire from Tick.
    • procedureId is an out-Buffer, as sentInvokeId is for SendReinitializeDevice: pass Buffer.alloc(4) and read the id with readUInt32LE(0).
    • The seven dll-only: lines are gone from ci_scripts/node-adapter-parity-allowlist.txt. adapter.self-test.ts now checks the seven names on the api and in the production addon manifest, and calls two of them in the loaded addon.
  • rb bs-20260924-6 (#2454, first part of #2304): an Audit Reporter now sends the audit notifications it accepts. Before, ForwardAuditEvent only counted them and kept the last one in memory. Test-tool builds with STACK_OPTION_OBJECT_TYPE_AUDIT_REPORTER only.
    • Each notification goes to the Device’s Audit_Notification_Recipient (135-2024 cl. 12.11.66), sent from BACnetDBDevice::Loop().
    • Issue_Confirmed_Notifications (cl. 12.63.12) chooses a ConfirmedAuditNotification or an UnconfirmedAuditNotification request.
    • A device recipient is resolved through the Device Address Binding, as for event notifications. Until it answers Who-Is, notifications wait in a queue of up to 256 per reporter.
    • While DeviceCommunicationControl disables initiation (cl. 16.1), notifications stay queued and are sent once it is enabled again. A notification the data link refuses for any other reason (for example, it does not fit in a frame) is dropped, so it cannot block the notifications behind it.
    • Notifications that can never be sent are dropped rather than retried: a confirmed request to a broadcast address, and a recipient that is this device. Sending to this device’s own Audit Log is not implemented.
    • ForwardAuditEvent refuses a notification missing its source device, target device or operation.
    • A confirmed notification is sent the way confirmed event notifications are: it is not retried, and its acknowledgement is not tracked. Reliability (cl. 12.63.6) is not maintained yet.
    • New BACnetDBDevice members hold the reporters (AddAuditReporter, GetAuditReporter) and the recipient (SetAuditNotificationRecipient, ClearAuditNotificationRecipient, GetAuditNotificationRecipient). Behaviour change: BACnetStackAuditReporter::SetNotificationRecipient and GetNotificationRecipient are removed. The recipient is a Device property, not an Audit Reporter property; use BACnetDBDevice::SetAuditNotificationRecipient. The header’s clause citations are corrected to cl. 12.63.8, 12.63.11 and 12.63.12. Still to come:
    • Serving Audit_Notification_Recipient through ReadProperty and WriteProperty.
    • The rest of the Audit Reporter properties (#2455).
    • Generating notifications for the operations this device performs (#2456) or requests (#2457). Until then, a notification reaches the reporter only when a caller builds one.
  • rb cloud-20260924-4 (#625, Python and Go): a worked v6 BACnetStack_DecodeAs JSON sample and a shared-fixture smoke test for each language.
    • Python: adapters/python/DecodeAsJSON_sample.py and DecodeAsJSON_sample_selftest.py.
    • Go: adapters/golang/decodeasjsonsample/, its own module. Run it with go test -count=1 ./; Go’s test cache does not hash the external .so.
    • Each decodes the shared I-Am fixture (adapters/README.md) through the real built library. The test asserts the JSON equals the fixture byte-for-byte and checks the v6 markers.
    • A missing library, a zero return or a wrong network type makes the test fail. Messages over 65535 bytes are rejected, not truncated.
    • cpp, csharp, java, rust and node remain open on #625.
  • rb cloud-20260924-5 (#625, Java and Rust): a worked v6 BACnetStack_DecodeAs JSON sample and a shared-fixture smoke test for each language.
    • Java: DecodeAsJSONSample and DecodeAsJSONSampleTest. The test fails, rather than skips, if the native library is missing. Run the sample with mvn -q compile exec:java -Dexec.mainClass=com.chipkin.bacnet.DecodeAsJSONSample.
    • Rust: examples/decode_as_json.rs and tests/decode_as_json_fixture.rs.
    • Each decodes the shared I-Am fixture through the real built library, asserts the JSON byte-for-byte and checks the v6 markers.
    • cpp, csharp and node remain open on #625.
  • rb cloud-20260924-6 (#625, C++): a worked v6 BACnetStack_DecodeAs JSON sample and shared-fixture smoke test for the C++ adapter in DLL mode, adapters/cpp/selftest/decode_as_json_selftest.cpp.
    • It loads the built library through LoadBACnetFunctions(), decodes the shared I-Am fixture and asserts the JSON byte-for-byte plus the v6 markers.
    • Its build line passes -DBACNET_LIB_FILE=..., because the adapter’s Linux default name lacks the lib prefix (#2461).
    • Node and C# remain open on #625.
  • rb cloud-20260924-7 (#625, Node): a worked v6 BACnetStack_DecodeAs JSON sample and shared-fixture self-test for the Node adapter: adapters/node/@chipkin/cas-bacnet-stack/src/decode-as-json.sample.ts and decode-as-json.self-test.ts.
    • The self-test loads the napi addon through LoadBACnetFunctions(), decodes the shared I-Am fixture and asserts the JSON byte-for-byte plus the v6 markers. It fails, and does not skip, when the addon is missing.
    • It runs as the new decode-as-json leg (leg 10) of ci_scripts/node-adapter-selftest.mjs (npm run test:adapter).
    • C# remains open on #625.
  • rb e-20260923-140 (#1851): COV reporting with value source information (ASHRAE 135-2024 cl. 19.5.2, Table 13-1a-2; DS-VSI-B, STACK_OPTION_DS_VSI_VALUE_SOURCE with STACK_OPTION_SUBSCRIBE_COV_PROPERTY). A SubscribeCOVProperty on Value_Source is no longer treated as a subscription to that one property.
    • When it notifies:
      • The object’s monitored property changes. That is Present_Value, except for Access Point (Access_Event_Time) and Credential Data Input (Update_Time). A numeric Present_Value is compared by the object’s own COV_Increment when that property is enabled; without one, and for any other value, any change counts.
      • A NaN Present_Value is compared as the floating-point value it is. Without a COV_Increment it counts as changed on every update, because NaN is unequal to itself; an ordinary Present_Value subscription never fires on it. With a COV_Increment it never meets the increment, as in an ordinary Present_Value subscription. Value_Source, Status_Flags and Current_Command_Priority changes still notify.
      • Status_Flags changes.
      • Door_Alarm_State changes, on an Access Door (the existing D86 comparison).
      • Value_Source changes.
      • Current_Command_Priority changes, on a commandable object.
    • Not compared: the other Table 13-1 criteria (Staging Present_Stage, the Load Control shed properties, Pulse Converter COV_Period).
    • A ValueUpdated of any property of the object now also evaluates the object’s Value_Source subscriptions.
    • What it reports: the object’s Table 13-1 values and Status_Flags, then Value_Source, then (commandable objects only) Last_Command_Time and Current_Command_Priority. The initial notification sent on subscribe has the same list.
    • A direct write of Value_Source (cl. 19.5.1.3) produces one notification, carrying the updated Value_Source. cl. 19.5.2 leaves the choice between one and two notifications to the implementation.
    • Value_Source is still not subscribable by default. The host enables it per object with SetPropertySubscribable, as for other properties.
    • Unchanged: subscriptions to other properties, and SubscribeCOVPropertyMultiple, including on Value_Source, which does not get the Table 13-1a-2 behaviour.
  • rb e-20260924-150 (#496, slice (a); IFC-052): test-tool hooks BACnetStackTestTool_RegisterHookIAmRouterToNetwork and BACnetStackTestTool_RegisterHookICouldBeRouterToNetwork. A test tool that is not a router can now receive I-Am-Router-To-Network (its DNET list) and I-Could-Be-Router-To-Network (DNET and performance index) along with the sender’s address; the stack previously dropped both. A router, which consumes I-Am-Router for route learning, does not call them. They exist only in builds with STACK_OPTION_ROUTER. No Node binding yet (#2473). Promoting the router senders to the customer interface (#496 part (b)) is not included.
  • rb bs-20260923-3 (#2351, rev 30 addendum cm): recorded that the BACnet Energy Services Interface (Annex AD) does not apply to this stack. It is delivered over BACnet Web Services and CSML, and it adds no object, property, service or BIBB. The rev 27–30 audit findings for #868 (bx, ci, cj, cm) are in _spec/spec-coverage/00-index.md.
  • rb bs-20260923-6 (#2350, rev 29 addendum cp audit for #868): recorded the Authentication and Authorization audit in _spec/spec-coverage/00-index.md.
    • Conformant: error codes, data structures, enumerations, and the SC data-option types.
    • Claims corrected. The Device object serves none of the Authorization_* properties. No protected operation is checked against a policy, no received Token data option is processed, and no configured token is presented. So AA-AT-B (Authorization Target), AA-NAT-B (Non-secure Authorization Target) and AA-SAC-A (Static Authorization Client) are withdrawn to ❌. The policy and token storage that exists is test-tool-only.
    • Per cl. 17.5.3 target authorization is optional, so this does not block claiming revision 29. Building it is #2364 (Device properties) and #2365 (token authorization).
    • The STACK_OPTION_DEVICE_AUTHORIZATION comment no longer claims the Device properties are served.
    • SC Identity relay stripping (cl. 17.3.4 rule 2) is #2363.
  • rb g-20260924-13 (#2416, partial): comments on the test-tool invoke-id generator in BACnetStackInvokeIds, BACnetDatabase and two test files now cite 135.1-2023 13.1.4 (“TD Initiated Segmented Request and Unsegmented Response (Non-Window)”), the test that re-sends an outstanding request with the same invoke id. They used to cite BTL 13.9.1 / 13.9.2. Comment-only, with no behaviour change. The same citation in the frozen test-tool headers is left for session D.

Released as source/version.h 6.0.22 and npm package @chipkin/cas-bacnet-stack 6.1.1 (patch bumps, owner request 2026-09-22). The npm package now requires Node >=24.21.0 <25 (breaking for Node 22) and ships prebuilt binaries for win32-x64 and linux-x64 - see the bs-20260922-5 entries below.

  • rb bs-20260922-4 (#2303): ci_scripts/test.sh and ci_scripts/test-sanitized.sh now verify the googletest v1.14.0 download against a pinned SHA-256 before building it, through one shared helper, ci_scripts/fetch-gtest.sh. The pin is for the .tar.gz archive (the .zip is no longer used) and matches the Bazel Central Registry’s. A mismatch or HTTP error stops the script. A warm /tmp/gtest-1.14.0 cache skips the download and the check. CI is not yet covered: the workflows’ existing gtest-1.14.0-<os> cache entry was filled from the old unverified download and is restored as-is until its key is changed (#2327).
  • rb 1464-addon-compile (#1464): the Node addon did not compile, and nothing in the repository compiled it. adapters/node/.../interface_objects.cc carried a hand-copied extern void BACnetStack_SetBACnetSCWebSocketStatus(...) against an export #940 changed to return bool, so npm install and node-gyp rebuild both failed outright. Revert-red: the new gate exits 1 on the pre-fix tree, failing interface_objects.cc in both build configurations.
  • rb 1464-addon-compile (#1464): two further signature defects that the stale declarations were hiding from the compiler, both found by removing them and neither visible to any name-level or count-level gate. BACnetStack_RegisterCallbackGetListElevatorGroupLandingCallStatus: the napi lambda declared const uint16_t floorTextMaxLength, uint16_t* floorTextLength where the header declares uint32_t* floorTextLength, const uint32_t floorTextMaxLength — the two are swapped, and both the wrong width, so the stack would have written a uint32_t through a uint16_t*. BACnetStack_SendUnsubscribedUnconfirmedCOVNotification: the call passed broadcast last instead of between networkType and destinationNetwork, shifting three arguments one slot left so a uint8_t* landed in the uint16_t destinationNetwork parameter. These are #390’s and #584’s classes respectively.
  • rb 1464-addon-compile (#1464): removed all 302 hand-copied extern declarations of BACnetStack_* / BACnetStackTestTool_* from interface*.cc. Every one was redundant — interface_common.h includes both export headers and every translation unit includes it — and three had drifted from the header. A local extern is the defect class, not just its instances: it tells the compiler the wrong thing and the compiler then agrees with itself.
  • rb 1487-swap (#1487, #933): the Node addon still used time_t after #933 converted the ABI to CASBACnetTime. #933 changed five exports, two test-tool exports and two callback typedefs to a fixed int64_t so the stack compiles the same 32-bit and 64-bit; the addon’s napi layer was not part of that sweep and kept time_t in 13 places. On 64-bit Linux the two are the same type, so it compiled and nothing failed — which is exactly why it survived. Converted: ReadTrendLogRecordWrapped and ReadTrendLogMultipleRecordWrapped (Napi::Buffer<time_t> → Napi::Buffer<CASBACnetTime>, so the out-buffer is 8 bytes on a 32-bit build too), InsertTrendLogRecordWrapped and InsertEventLogRecordWrapped, both callback trampolines (FPCallbackGetSystemTime, FPCallbackValidateBACnetSCOperationalCertificate — a trampoline whose signature stops matching its typedef is a silent registration failure), and the three api.ts doc comments that told integrators the buffer holds “a C time_t”.
  • rb 1487-swap (#1487, #933): InsertTrendLogRecordWrapped and InsertEventLogRecordWrapped read their 64-bit timestamp with Uint32Value(), truncating it. Both now use Int64Value(). Present before #933 and unrelated to it: a time_t on any 64-bit build was already being narrowed to 32 bits on the way in.
  • rb 1487-roles-decide (#1487): the role extractor preferred a validation’s display-name string over the positional map unconditionally, which attributed a buffer role to the length parameter beside it wherever a wrapper labels its check by the length. SendWriteProperty and SendCreateObject both write ValidateBufferLength(info, "propertyArraysLength", propertyArrays.Length(), propertyArraysLength) — the label names the length, the buffer is propertyArrays. The display name now wins only when it lands on a pointer, and the positional map answers otherwise.
  • rb 1487-roles-decide (#1487): ValidatePackedElementBuffer roles now carry the countParameter, bytesPerElement and fixedStride the check actually uses, rather than only recording that one exists — a generator cannot reproduce the check without them. Recovering those also resolved SendWriteGroup.changeList and SendSubscribeCOVPropertyMultiple.references, whose roles had previously been refused.
  • rb 1487-swap (#1487): the behaviour signature compared a feature list, and a feature list only catches what it was told to look for. It missed two whole categories, and the swap would have shipped both:
    • DecodeAs null-terminates its output buffer after the call so JS consumers can read it as a C string. Nothing in the signature covered a statement after the export call, so it reported a match against a generated wrapper that does not. The signature now also compares the residue — everything left once the reads, the guards, the call and the return are subtracted — and requires it to be empty. Ten more wrappers turned up with it, all hand-rolling their argument check instead of calling CheckParametersCountAndType.
    • The signature never compared the return. GetFDTEntry is declared uint32_t and the hand-written wrapper handed JavaScript Napi::Boolean::New(env, results). Six exports disagree with the type-derived mapping, and none is a clean call: ReadTrendLogRecord, ReadTrendLogMultipleRecord, GetBDTEntry and GetFDTEntry are declared uint32_t but documented as returning true/false; AddVirtualNetwork is bool and returns a Number; and SetBACnetSCWebSocketStatus discards its bool result entirely, so a JS caller cannot tell success from failure. All six are exempted rather than swapped, because each change is customer-visible. Flagged on #1487 for a ruling.
  • rb 1487-swap (#1487): the match rate this tool reports fell from 111 (77%) to 94 (65%) as a result. The wrappers did not change — the measurement got honest. Every one of the 17 was a wrapper doing something real that the comparison could not see.
  • rb 1487-testtool-gate (#1507): ci_scripts/check-header-standalone.sh no longer reports two states as passing on a host that cannot reach them. A Windows-hosted compiler predefines _WIN32 and _WIN64 unconditionally, so the two “no _WIN32” states silently duplicated the _WIN32 + _WIN64 state there and 2 of the 11 checked nothing. They now pass -U_WIN32 -U_WIN64 where the host can preprocess that way, and print [host] … NOT CHECKED where it cannot (MinGW’s own headers #error out) rather than [ok]. The gate also falls back to gcc/g++ when cc/c++ are absent, so it runs at all on a MinGW host instead of exiting 2.
  • rb 1487-testtool-gate (#1507): ci_scripts/check-header-standalone.sh recognises __attribute__((dllexport)) as well as __declspec(dllexport). MinGW g++ predefines __declspec(x) as __attribute__((x)), so on such a host the preprocessed text the gate greps never contained the spelling it looked for and four of its eleven linkage-expansion states failed against a header that is correct. Gate-only; source/CASBACnetStackDLL.h is unchanged.
  • 604-build-matrix (#604): a tagged release no longer ships the DLL’s auto-generated import library under the name a customer would expect the static library to have. ReleaseDll|x64 and ReleaseLib|x64 share OutDir and TargetName, so both wrote bin/CASBACnetStack_x64_Release.lib; because only DLL configurations were ever built, the bin/*.lib in every release zip was the import stub, which needs the DLL at runtime. ReleaseDll|x64 now keeps its import library in $(IntDir), and the workflow stages Windows artifacts into staging/dll/ and staging/lib/ rather than globbing bin/. The .pdb collision between the two configurations is avoided the same way.
  • 604-build-matrix (#604): ci_scripts/check-header-standalone.sh now covers both halves of the issue. It gained a linkage-expansion check that preprocesses both headers in each of their eleven reachable states - with and without _WIN64, since every 64-bit MSVC build defines it and this stack ships x64 only - and asserts which of them yield dllexport — checkable on Linux because -E needs no understanding of __declspec, and the expansion is the contract. Its --self-test gained a second corpus that restores the pre-fix unconditional export and asserts the precise regression signature, not merely that something failed. The script’s own “runs from pre-push.sh only” note was stale; it has run in tests.yml’s fast-gates job for some time. The gate also covers the test-tool header’s three reachable states, so “mirrors DllExport” is enforced rather than asserted in a comment.
  • 604-build-matrix: BACnetAnnexAATimeSeriesRecord.cpp’s absent-value sentinel is split into adjacent string literals ("\x01\x02" "BACNET-..." "\x02\x01"). The \x02 hex escape greedily consumed the following BAC as hex digits, producing \x02BAC — out of range for char, and rejected outright by MSVC 14.51 (error C7744), the v145 toolset this project pins. This changes the emitted bytes on compilers that accepted the old form: MSVC rejects it outright, but GCC/Clang accept it with warning: hex escape sequence out of range and truncate, so every Linux build to date emitted a 29-byte sentinel (01 ac 4e ..., the BAC swallowed by the escape) instead of the intended 32-byte one (01 02 42 41 43 ...). The new form emits what the literal was always meant to say. Uses are confined to comparisons against the constant itself, so nothing depends on the old value. Unrelated to the rest of this change, but the Windows build does not complete without it. Swept source/, tests/ and adapters/ (1,983 files) for siblings of both this shape and the octal equivalent: no other site.
  • rb 894-adapter-generation (#894): the Java adapter did not compile, and no gate compiles it. SimpleServerExample.java implemented GetPropertyReal, GetPropertyCharacterString and GetPropertyEnumerated without the trailing errorCode parameter their own interface declared. Control: mvn -o compile on a git archive of HEAD fails with the same three does not override errors before any change here. mvn -o compile is clean after.
  • rb 894-adapter-generation (#894): the C# adapter did not compile either, for an unrelated reason — four CS0121 ambiguous-overload errors on BitConverter.GetBytes(byte) between the short and System.Half overloads (System.Half arrived in .NET 5). Control: dotnet build on a git archive of HEAD fails with 4 × CS0121. dotnet build is clean after: 0 warnings, 0 errors.
  • rb 894-adapter-generation (#894): ci_scripts/adapter-binding-symbols.js --python mode did not understand triple-quoted strings, so a docstring body was scanned as code and reported 8 dead Python bindings that do not exist — every one an export name the generator’s line wrapping had split across two lines.
  • rb 894-adapter-generation (#894): countArgs in adapter-callback-signature-check.js counted a trailing comma as an extra argument, reporting the generated 8-parameter Python CFUNCTYPE as 9. A false FAIL on a correct file trains the reader to disbelieve the gate.
  • rb 894-adapter-generation (#894), from code review: Doxygen’s comma-grouped \param a,b,c <text> form was not handled, and the header uses it on 168 lines. The shared text reached only the first name, and the rest of the comma list was emitted as that parameter’s own doc text - so a generated <param name="connectionString"> opened with ,connectionStringLength The datalink address..., which reads as documentation for the wrong parameter. Fixing it recovers 289 parameters: documented customer-surface parameters go 948 -> 1,237 of 1,271, and exports with every parameter documented go 134 -> 185 of 229. An earlier revision of docs/decisions/2026-09-07-adapter-generation.md reported those 289 as undocumented in the header; that was this parser’s bug, and the claim is corrected there.
  • rb 894-adapter-generation (#894), from code review: \parblock/\endparblock (a multi-paragraph parameter description) had its body hoisted into the function summary, leaving the parameter with one sentence plus a literal \parblock. BACnetStack_InsertTrendLogMultipleRecord’s dataAsString lost its entire grammar that way, including the header’s warning that the unquoted form [{1,123.45}] is rejected outright.
  • rb 894-adapter-generation (#894), from code review: the generated Rust adapter dropped the hand-written verify_all_referenced_symbols_resolve() (added for #585/#586) with no replacement. libloading resolves lazily per call, so without it a header/binary mismatch surfaces at whichever binding the customer reaches first - and the file went from 17 symbol lookups to 229 while nothing in this repository compiles Rust. It is regenerated, and now exhaustive by construction rather than a hand-maintained list.
  • rb 894-adapter-generation (#894), from code review: countArgs in adapter-callback-signature-check.js decremented depth on every >. Harmless until Rust joined the gate, where -> is an ordinary token: countArgs('a -> b, c, d') returned 1 instead of 3, and once depth goes negative no later comma splits, so the rest of the list collapses into one segment - a false PASS whenever the swallowed count lands on the header’s number. Angle-bracket nesting is now opt-in and only Java asks for it; the self-test gained Rust fixtures including one carrying a nested fn(u8) -> u8 parameter.
  • rb 894-adapter-generation (#894), from code review: generate-python-adapter.js wrote header prose into # comments unsanitised, while adapter-binding-symbols.js --python does not understand #. A triple quote reaching a comment would open a fence there and silently hide every later binding from the coverage gate - which has no report_missing arm for Python, so the loss would not fail anything.
  • 933-cas-bacnet-time: adapters/cpp/CASBACnetStackAdapterTypes.h is compiled standalone and, in CAS_BACNET_STACK_LIB_TYPE_DLL mode, deliberately does not include the customer header — so it needs its own definition of CASBACnetTime. Caught by ci_scripts/adapter-compile.sh. The same applies to source/BACnetCallbackInterface.h, which does not include CASBACnetStackDLL.h either. The type is therefore defined in three places behind a shared CASBACNETTIME_DEFINED guard, each with a comment saying why it is not redundant; the guard is required because C99 rejects a repeated typedef even though C11 and C++ allow it.
  • rb a-20260905-1 (#1057): BACnetPrimitiveNULL::Decode now returns the bytes it actually consumed (the tracked buffer offset) instead of a guessed literal, so a context-tagged NULL with tag number exactly 15 (F8 0F, the two-byte extended form) reports 2 bytes, not 1 — callers no longer re-read the extension octet as the next field. Revert-red: Issue1057_DecodeContextTagFifteen_ReturnsBothBytes fails on the pre-fix code (returns 1).
  • rb a-20260905-1 (#1274): the Linux test build compiles again at the issues/runbook baseline (b5138666 did not) — 37 Get* callback mocks under tests/BACnetSC_Tests/ and tests/BTL_Tests/ gained the trailing uint32_t* errorCode parameter that #974 (batch 120) added to the typedefs in BACnetCallbackInterface.h. Tests-only; no source/ change.
  • rb a-20260905-10 (#1191): BACnetStackTrendLogMultiple::InsertLogRecordAndTakeControl no longer double-frees the log record/log-status data on a CopyAdd failure (e.g. Buffer_Size=0) — the temporary BACnetLogMultipleRecord already owns and frees it.
  • rb a-20260905-10 (#1192): BACnetStackTrendLogMultiple::GetTrendLogProperty’s Log_Device_Object_Property array-index bounds check now correctly rejects propertyArrayIndex == size()+1, closing an out-of-bounds vector read reachable via an ordinary ReadProperty.
  • rb a-20260905-10 (#1193): BACnetSingleLogData::SetAnyValue and BACnetLogDatum::SetAnyValue now set the object valid on success, matching every other CHOICE-arm setter in each class — the any-value CHOICE arm could previously never be encoded despite construction reporting success.
  • rb a-20260905-10 (#943): BACnetEventLogDatum::GetNotification() no longer dereferences a null pointer when the active CHOICE arm isn’t notification; BACnetEventLogRecord::SetTimestamp now correctly invalidates the parent record when given an invalid timestamp instead of always reporting success.
  • rb a-20260905-11 (#1072): BACnetConfirmedRequestPDU::DecodeUsingSupportedServices now tolerates trailing Ethernet padding and tracks its own reject reason, matching the sibling Decode() — a legitimate padded request no longer gets rejected as invalidTag through this entry point.
  • rb a-20260905-11 (#1080): BACnetConfirmedRequestProcessor::Process no longer dereferences a failed nothrow allocation for lifeSafetyOperation/confirmedPrivateTransfer/auditLogQuery/authRequest — an allocation failure now produces a graceful Abort(outOfResources) instead of a NULL-pointer crash.
  • rb a-20260905-11 (#1081): BACnetConfirmedServiceRequest::BACnetConfirmedRequestFactory(const&) now clones a confirmedCovNotificationMultiple request instead of silently dropping it during Clone()/operator=.
  • rb a-20260905-11 (#1087): BACnetComplexACKPDU::SetComplexAckAsSegment now correctly reports failure when its memory-buffer insert fails, instead of always reporting success.
  • rb a-20260905-13 (#1204): Access Door’s Door_Alarm_State is now writable while Out_Of_Service is TRUE, per cl. 12.26.9(c)/12.26.20 — a write now actually updates the value ReadProperty answers from.
  • rb a-20260905-13 (#1205): Access Zone’s Occupancy_Count now has a WriteProperty dispatch case, wired to the stack’s internal occupancy tracking, per cl. 12.32.11.
  • rb a-20260905-13 (#1206): BACnetCredentialAuthenticationFactor::SetAuthenticationFactor now composes validity from both mandatory SEQUENCE fields (disable, authentication-factor) instead of marking the composite valid unconditionally.
  • rb a-20260905-14 (#1163): BACnetChangeOfStatusFlagsEventAlgorithm now implements cl. 13.3.11 condition (c) — a fresh OFFNORMAL notification when the set of TRUE selected flags changes while already OFFNORMAL, indicated immediately (this condition has no pTimeDelay gating in the spec).
  • rb a-20260905-14 (#1164): BACnetChangeOfCharacterstringEventAlgorithm now implements cl. 13.3.16 condition (c) — a fresh OFFNORMAL notification when the monitored value moves to a different matching alarm value while already OFFNORMAL.
  • rb a-20260905-14 (#1168): BACnetBufferReadyEventAlgorithm now implements cl. 13.3.7 condition (b)’s wraparound formula — a genuinely wrapped 32-bit record counter whose delta still crosses the notification threshold now notifies instead of being silently treated as a buffer reset.
  • rb a-20260905-14 (#1169): BACnetUnsignedRangeEventAlgorithm’s recovery conditions no longer apply a deadband cl. 13.3.9 doesn’t define — a monitored value exactly at the high/low limit now correctly recovers to NORMAL.
  • rb a-20260905-15 (#1165): BACnetChangeOfCharacterstringEventAlgorithm now matches per cl. 13.3.16’s Null/empty/substring-containment rule instead of exact-string equality.
  • rb a-20260905-15 (#1166): BACnetChangeOfTimerEventAlgorithm now tracks Update_Time and implements cl. 13.3.19 condition (c) — a fresh OFFNORMAL notification when a Timer re-expires while already OFFNORMAL.
  • rb a-20260905-15 (#1167): BACnetChangeOfLifeSafetyEventAlgorithm now reaches cl. 13.3.8’s mode-change self-loop conditions (c)/(g)/(k), previously permanently unreachable.
  • rb a-20260905-16 (#1295): BACnetSimpleAckProcessor and BACnetComplexAckProcessor no longer raise a client-facing Hook for a SimpleACK/non-segmented-ComplexACK whose invoke-id matches no outstanding sent request, per cl. 5.4.4.1. A segmented ComplexACK still dispatches on an unmatched invoke-id (its own reassembly/tracking state, #1091).
  • rb a-20260905-16 (#1084): BACnetErrorCode gains the 30 enumerators (200-229) ASHRAE 135-2024 cl. 21.4 defines for certificate/OAuth-scope error codes, which were entirely missing.
  • rb a-20260905-2 (#1062): BACnetPrimitiveCharSTRING::Decode holds the declared length at uint32_t through the bounds check, so a declared length of 65537 is rejected instead of wrapping to an empty string — revert-red: pre-fix, Decode() returned bytesProcessed=7 with IsValid()==true on the crafted buffer.
  • rb a-20260905-2 (#1063): BACnetPrimitiveBitSTRING::Decode holds the declared length at uint32_t through the bounds check, so a declared length of 65537 (unusedBits=0) is rejected instead of wrapping to an empty bit string — revert-red: pre-fix, Decode() returned bytesProcessed=7 with IsValid()==true on the crafted buffer.
  • rb a-20260905-2 (#1064): GetMinimumMemoryCount() in BACnetPrimitiveOctSTRING/CharSTRING/BitSTRING now budgets the spec’s 3-tier length-encoding form (135-2024 cl. 20.2.1.3.1: 1/3/5 octets) instead of a flat “+1 once past the short form” — revert-red: pre-fix, all three returned 256 for a declared length crossing the 253 boundary, 2 bytes short of the correct 258.
  • rb a-20260905-2 (#1065, CR-F03-028): BACnetPropertyValue::Decode/DecodeWithObjectType reject a present-but-malformed Priority [3] and PropertyArrayIndex [1]/[2] instead of silently treating them as absent, matching BACnetPropertyReference::Decode’s existing #638 fix — revert-red: pre-fix, a present-but-truncated field reported the enclosing element valid with the malformed bytes left unconsumed.
  • rb a-20260905-3 (#1137): BACnetYouAreProcessor::Process now checks the incoming You-Are’s Vendor ID/Model Name/Serial Number against this device’s own identity before honouring a directed device-identifier assignment (135-2024 cl. 16.11.4) — revert-red: previously any broadcast You-Are naming any device-identifier reassigned every listening device’s Device_Identifier unconditionally, per the code’s own comment that the identity match was “intentionally out of scope”.
  • rb a-20260905-3 (#1138): BACnetUTCTimeSynchronizationProcessor::CalculateTimeUsingUTC now actually applies Daylight_Savings_Status (+1 hour) instead of setting a tm_isdst field that timegm()/gmtime() ignore — revert-red: a device with Daylight_Savings_Status = TRUE had its Local_Time/Local_Date silently one hour off on every UTCTimeSynchronization it received.
  • #1139 (BACnetYouAreProcessor MAC-address assignment branch) was claimed alongside #1137/#1138 but unclaimed and left in the pool: implementing it needs a mutable reference to the local datalink/station address that Process()’s current signature has no path to, which is an architecture decision beyond a same-shaped mechanical fix.
  • rb a-20260905-4 (#1033): BACnetDBDevice::BuildArrayProperty’s stack-stored-value branch now bounds-checks propertyArrayIndex before dereferencing GetObject(), mirroring the sibling callback-backed branch’s existing check — revert-red: previously any ReadProperty naming an out-of-range index on a stack-stored array property (e.g. Elevator Group’s Group_Members) crashed the device via a NULL dereference, a remote unauthenticated DoS.
  • #1042, #1043, #1044, #1045, #1046 (F02 Property_List omissions for LoadControl/AccessZone/AccessUser/Staging/Color) were claimed but found already fixed by batch 121 (d31791d5, tagged “#1001 audit”) before this review finding was filed — the finding’s own “re-checked” note was stale. Evidence posted on each issue; closed as not-reproduced.
  • rb a-20260905-5 (#1024): Lift Registered_Car_Call/Landing_Door_Status GET loops now terminate on a mid-list callback decline instead of spinning forever — revert-red: a genuine infinite loop (DoS), since more was only ever written on a successful callback and held its last TRUE forever once the host declined partway through a list.
  • rb a-20260905-5 (#1029): removed orphaned test-tool doc blocks for GetEventInformation and AcknowledgeAlarm (both moved to the public interface some time ago) from BACnetTestToolInterface.h/.cpp and CASBACnetStackTestToolDLL.h/.cpp, replaced with one-line pointers matching the existing WriteGroup convention; corrected tests/TestBACnetInterface_GetEventInformation.cpp’s stale header comment and removed three dead BACnetTestToolInterface testTool variables that the test never used.
  • rb a-20260905-5 (#1030): the three Unconfirmed EventNotification senders (SendUnconfirmedEventNotification, ...WithEventValues, ...WithOutOfRangeValues) no longer override the constructor’s own computed validity with an unconditional SetValid(true) — revert-red: a request with a genuinely invalid sub-field (e.g. an out-of-range timestamp weekday) previously reported valid anyway and still transmitted default/corrupted content.
  • rb a-20260905-6 (#1047): Color’s default property profile no longer claims eight properties ASHRAE 135-2024 Table 12-84 does not define for this object type (Status_Flags, Reliability, Out_Of_Service, Event_State, Default_Color_Temperature, Color_Override, Color_Reference, Override_Color_Reference) — verified directly against the extracted spec. Three of the removed properties (Color_Override, Color_Reference, Override_Color_Reference) actually belong to Lighting Output (cl. 12.54.46-48) and Binary Lighting Output (cl. 12.55.40-42), which previously had no case for any of them at all; both now resolve them correctly.
  • rb a-20260905-7 (#1049): ColorTemperature’s default property profile no longer claims four properties ASHRAE 135-2024 Table 12-87 does not define for this object type (Status_Flags, Reliability, Out_Of_Service, Event_State) — verified directly against the extracted spec, the identical defect class as #1047’s Color finding.
  • rb a-20260905-8 (#1076, #1077, #1078): Abort/Reject/Error-PDU handling no longer fires HookAbort/HookReject/HookError for an invoke-id that matches no outstanding confirmed request — ASHRAE 135-2024 cl. 5.4.4.1: “There is no reason to issue REJECT.indication, ABORT.indication, etc., as the client has no knowledge of the transaction in question.” BACnetStackSentConfirmedRequests::RemoveRequest/BACnetDatabase::RemoveSentConfirmedRequest now return bool so callers can tell whether the invoke-id was genuinely outstanding.
  • rb a-20260906-1 (#1149): BACnetSubscribeCOVPropertyProcessor rejects an explicitly present Lifetime of 0, per cl. 13.15.1.1.4 (unlike plain SubscribeCOV, where zero means indefinite).
  • rb a-20260906-1 (#1150): BACnetSequenceOf<T>::Decode no longer reports success at a truncated length after a mid-list element fails to decode - a malformed/truncated PDU is now rejected outright instead of silently accepted with a corrupted tail.
  • rb a-20260906-1 (#1151): SubscribeCOVPropertyMultiple subscriptions are now compensated for a device clock jump (TimeSynchronization/UTCTimeSynchronization), matching the plain-COV table’s existing behavior.
  • rb a-20260906-1 (#1152): the plain-COV subscription table now purges expired entries on a periodic sweep, rather than only as a side effect of an optional client ReadProperty - closing a permanent-capacity-exhaustion DoS in deployments that never read that property.
  • rb a-20260906-10 (#1021): a Notification Class’s device-identifier recipient now resolves via its owning device’s own Device Address Binding table, and Tick’s DAB heartbeat now chases every device’s table, not just the main device’s - a virtual device’s recipient could previously never be resolved.
  • rb a-20260906-10 (#1022): SendReadBroadcastDistributionTable, SendWriteBroadcastDistributionTable, SendReadForeignDeviceTable and SendDeleteForeignDeviceTableEntry now reject a connection string whose length is nonzero but not exactly CONNECTION_STRING_LENGTH_IPV4, matching SendRegisterForeignDevice (#739).
  • rb a-20260906-10 (#1023): GetHostInfoFromNetworkPort now rejects a NULL or wrong-length ipAddress/subnetMask buffer instead of writing past it, matching GetBDTEntry (#745/#982).
  • rb a-20260906-10 (#1345): BACnetCOVSubscription, BACnetPriorityValue and BACnetRecipientProcess copy-assignment operators now guard against self-assignment, matching the rest of the codebase (#373/#396/#1107).
  • rb a-20260906-11 (#1074): the responding state machine now implements cl. 5.4.5.3’s AWAIT_RESPONSE Timeout transition - a confirmed request whose local application never responds now aborts and returns to Idle instead of being parked forever.
  • rb a-20260906-11 (#1083): Error-PDUs carrying the confirmedAuditNotification, auditLogQuery or authRequest error-choice (135-2024 cl. 21.4) now decode correctly instead of being rejected as malformed; authRequest’s own AuthRequest-Error SEQUENCE (error-type + optional error-details) is now implemented as BACnetAuthRequestError.
  • rb a-20260906-12 (#1222): BACnetSpecialEvent::operator=’s second, previously-unchecked time-value commit pass now safely leaves the list empty and invalid on an allocation failure, instead of silently truncating it while still reporting valid.
  • rb a-20260906-12 (#1223): BACnetTimerStateChangeValue::SetDateTime/SetLightingCommand now check their child Clone()’s result, instead of reporting IsValid()==true with a NULL payload on an allocation failure.
  • rb a-20260906-12 (#1224): BACnetStackTimer::Start()’s Expiration_Time now correctly rolls the calendar date forward for a timeout that crosses midnight (cl. 12.57.14), instead of wrapping the time-of-day back onto the same day.
  • rb a-20260906-12 (#1225): BACnetWeekNDay::SetWeekNDay now validates its month/week-of-month/day-of-week domain and wildcard bounds (cl. 21.6), matching the sibling BACnetPrimitiveDATE setters.
  • rb a-20260906-13 (#945, #1215): BACnetAssignedLandingCalls::InsertLandingCall/BACnetLiftCarCallList::InsertFloorNumber now reject a NULL or invalid child instead of unconditionally reporting success, and both classes’ Clone() (plus BACnetLandingDoorStatus::Clone(), #1216) is now fully transactional - a per-element clone/insert failure now fails the whole Clone() instead of silently returning a shorter-but-“valid” copy.
  • rb a-20260906-13 (#1216): BACnetLandingDoorStatus::InsertLandingDoor now rejects a NULL or invalid child, matching #945’s identical fix - a caller’s own explicit failure check was previously dead code.
  • rb a-20260906-13 (#1217): BACnetLandingDoor::Decode now rejects a wire-decoded floor-number outside its Unsigned8 domain, instead of silently truncating it on read.
  • rb a-20260906-14 (#1315): BACnetDataLink::ProcessAPDU’s PDU-type switch now breaks after complexACK/simpleACK instead of falling through into the abort/reject/error default case and double-processing the same inbound ACK.
  • rb a-20260906-14 (#1321): HandleTimeChange on both COV subscription tables (BACnetStackCOVSubscription, BACnetStackCOVMultipleContextValue) now exempts the Lifetime=0 indefinite-subscription sentinel instead of perturbing it into a finite, potentially already-expired value.
  • rb a-20260906-14 (#1322): BACnetSequenceOfPropertyValue::DecodeWithObjectType’s error.IsSet() branch now calls Reset() before SetValid(false), matching the sibling elementDecodeFailed branch, so a later element’s decode failure no longer leaves earlier already-inserted elements reachable.
  • rb a-20260906-14 (#1325): BACnetHostAddress::operator= now preserves an invalid rhs’s invalidity instead of silently promoting it into a valid ‘none’, mirroring BACnetTimeStamp’s equivalent fix (#1189).
  • rb a-20260906-14 (#1328): a Recipient_List WriteProperty now Release()s the DAB entry for any device-identifier recipient dropped by a full-list rewrite, instead of permanently leaking its refcount.
  • rb a-20260906-15 (#1291): GetDefaultPropertyProfileForLightingOutput now has a case for eventState, matching 135-2024 Table 12-64’s O1 conformance code.
  • rb a-20260906-15 (#1293): GetDefaultPropertyProfileForColorTemperature now has cases for Min_Pres_Value/Max_Pres_Value/Value_Source/Audit_Level/Auditable_Operations, matching 135-2024 Table 12-87.
  • rb a-20260906-15 (#1309): BACnetPacket::GetAttributes() now has a case for BACnet/SC, matching the existing IP/MSTP networkType attribute rendering (same gap shape as #1034).
  • rb a-20260906-15 (#1307): every F17 event algorithm sharing the HandleTimeDelay pending-timer pattern (BACnetChangeOfLifeSafetyEventAlgorithm, BACnetChangeOfTimerEventAlgorithm, BACnetChangeOfCharacterstringEventAlgorithm, BACnetChangeOfStateEventAlgorithm, BACnetChangeOfBitstringEventAlgorithm) now resets its pending-timer fields when a classification flip-flop abandons a pending transition, instead of leaving a stale expiry that could cause a later re-arm to commit prematurely.
  • rb a-20260906-16 (#1314): a WriteProperty to a Network Port’s BBMD_Broadcast_Distribution_Table now actually replaces the BBMD’s real broadcast distribution table, instead of finding no handler at the value-dispatch stage despite the property being declared writable since #1102.
  • rb a-20260906-17 (#1284): a present-but-malformed optional propertyArrayIndex, priority, timeOfChange, or deviceIdentifier field (a valid context tag whose own value is truncated/corrupt) is now correctly rejected on decode, instead of being silently treated the same as a genuinely-absent field across 11 sites in BACnetObjectPropertyValue, BACnetDeviceObjectPropertyValue, BACnetAddListElementRequest, BACnetRemoveListElementRequest, BACnetCovValue, BACnetPropertyAccessResult, BACnetReadResult, and BACnetWritePropertyRequest.
  • rb a-20260906-18 (#1259, #1263): a present-but-malformed optional deviceIdentifier, overridingPriority, or inhibitDelay field is now correctly rejected on decode instead of being silently treated the same as a genuinely-absent field, in BACnetDeviceObjectReference, BACnetGroupChannelValue, and BACnetWriteGroupRequest — closing out the last remaining sites in the #767 family-split audit for families F03 and F11.
  • rb a-20260906-19 (#1226): BACnetVTCloseError::Clone() no longer silently drops the wrapped errorClass/errorCode, reachable via BACnetErrorPDU’s own copy/assign/Clone.
  • rb a-20260906-19 (#1229): BACnetActionCommand::SetPriority/Decode now enforce the spec’s 1-16 Priority range, matching every sibling class.
  • rb a-20260906-19 (#1230): a Load Control object’s Shed_Duration is now reset to zero once a shed command completes, per cl. 12.28.12.
  • rb a-20260906-19 (#1271): a present-but-malformed setpointReference is now correctly rejected on decode instead of reporting success with 0 bytes consumed, closing the last remaining site of the #767 family-split audit for family F29.
  • rb a-20260906-2 (#1147): COV subscription cancellation and re-subscription/renewal-detection now match on the requesting BACnet address, per cl. 13.14.2, not just the subscriber process identifier and monitored object/property - closing a hole where any peer that learned another subscriber’s subscriber process identifier could cancel or take over their subscription from a different address. Also closes a more severe variant found while fixing this: a colliding subscriber process identifier from a different address could previously silently overwrite an existing subscriber’s live subscription outright.
  • rb a-20260906-20 (#1266): a present-but-malformed optional timestamp, issueConfirmedNotifications, lifetime, covIncrement, or maxNotificationDelay field is now correctly rejected on decode instead of being silently treated the same as a genuinely-absent field, across BACnetCOVNotificationMultipleRequest, BACnetSubscribeCOVRequest, BACnetSubscribeCOVPropertyRequest, and BACnetSubscribeCOVPropertyMultipleRequest — closing the last remaining family (F15) in the #767 absent-vs-malformed audit set.
  • rb a-20260906-21 (#996): a repeat BACnetStack_AddTrendLogObject/BACnetStack_AddTrendLogMultipleObject call for the same object instance no longer leaks a buffer or silently keeps the original maxBufferSize; maxBufferSize == 0 is now rejected rather than creating a log object that can never log.
  • rb a-20260906-22 (#1364): repeat EnableNetworkPort(IPv4|Ethernet|MSTP|Virtual|SecureConnect)Properties calls for the same object instance no longer leak a network port object.
  • rb a-20260906-22 (#1365): EnableEventLogProperties/AddAuditLog now reject a maxBufferSize/bufferSize of 0, matching the same fix already applied to the Trend Log constructors.
  • rb a-20260906-3 (#1188): BACnetRecipientProcess::SetRecipient now rejects an invalid BACnetRecipient instead of accepting it as usable and failing only when actually encoded or notified. Also fixes the same unfixed bug shape in BACnetCOVSubscription/BACnetCOVMultipleSubscription::SetRecipient, found by review, plus a stale-recipient inconsistency the fix newly exposed in the affected classes’ own operator=.
  • rb a-20260906-3 (#1187): Alert Enrollment objects now correctly record Present_Value/Event_Time_Stamps/Acked_Transitions whenever Event_Detection_Enable is true, per cl. 12.52.7/12.52.9/13.2.5 - previously this state update was also incorrectly gated on Event_Enable[TO_NORMAL], which per spec governs only notification distribution, not the underlying bookkeeping.
  • rb a-20260906-3 (#1189): BACnetTimeStamp::operator= no longer silently no-ops when assigned an invalid/default-constructed value - it now resets the target object instead of leaving it with its stale prior state while incorrectly still reporting valid.
  • rb a-20260906-4 (#1037): BACnetDailySchedule/BACnetSpecialEvent::InsertTimeValueAndTakeControl now reject a duplicate Time within the same day/event, per cl. 12.24.7/12.24.8 - closes the host-API-side gap (AddScheduleWeeklyTimeValue/AddScheduleExceptionTimeValue). Does not cover the WriteProperty wire path for Weekly_Schedule, which bypasses this check via a separate copy-assignment path - tracked in #978.
  • rb a-20260906-4 (#1038): SetAlarmsAndEventsForObjectEnabled now validates the assigned notification class exists before registering a new alarm-and-event object, instead of after - a nonexistent notification class no longer leaves a permanently-registered, half-configured object behind for a first-time registration that reports failure.
  • rb a-20260906-4 (#1039): RaiseAlert now validates everything that can still fail (notification-class lookup, vendorId/extendedEventType, extended-parameter allocation) before calling RecordAlert, which commits Present_Value/Event_Time_Stamps/Acked_Transitions - a later failure no longer leaves the object’s state changed while the function reports total failure.
  • rb a-20260906-4 (#1041): SetNetworkPortProperty’s Network_Number write no longer requires the host callback to set an error code in order to reject the write - a callback that declines without setting one (a legitimate, documented pattern) is no longer silently ACKed.
  • rb a-20260906-5 (#1025): 12 sibling UseCallbackGetProperty* functions (BitString, Bool, CharString, Date, Double, Enum, OctetString, Int, Real, Time, UInt, ObjectIdentifier) now check InsertObjectAndTakeOwnership’s return value - an allocation failure while building the read value is now correctly reported as a failure instead of a fabricated success.
  • rb a-20260906-5 (#1026): UseCallbackSetPropertyDateTime now refuses the whole write up front when no Time callback is registered, instead of committing the Date half first and only then discovering the Time write can never be attempted.
  • rb a-20260906-5 (#1027): reading an empty BACnet/IP Broadcast_Distribution_Table now returns a valid empty list instead of an invalid AST that made Encode() abort the response.
  • rb a-20260906-6 (#1088): BACnetComplexACKPDU::InsertEncodedServiceAndCleanUpMemory now reports IsValid() matching its own return value, instead of always reporting valid regardless of whether the internal encode succeeded.
  • rb a-20260906-6 (#1089): BACnetComplexACKPDU::EstimateApduLength() no longer dereferences a NULL m_serviceAck on a fresh or Reset() object.
  • rb a-20260906-6 (#1092): BACnetStackUnknownAPDU::Decode’s closing-tag branch now correctly reports the byte it consumed, instead of reporting 0 (the codebase’s own “decode failed” convention) while marking the object valid.
  • rb a-20260906-6 (#1093): BACnetStackUnknownAPDU::Encode and BACnetAbstractSyntaxType::Encode’s per-object loop now set an abort reason on failure instead of leaving the error container untouched.
  • rb a-20260906-7 (#1079): BACnetErrorPDU::Encode no longer dereferences a NULL m_error when an object is made valid without going through InsertErrorAndTakeOwnership.
  • rb a-20260906-7 (#1090): BACnetSegmentACKPDU::Encode now sets an abort reason on every failure path, instead of failing silently.
  • rb a-20260906-7 (#922, #1073): the responding state machine’s required-segment-count computation no longer overcounts (wrong divisor on the remainder check) or silently wraps (uint8_t narrowing) for a large ComplexACK response.
  • rb a-20260906-8 (#1207): BACnetStackAccessCredential::SetProperty validates Credential_Disable against its max legal value before narrowing to uint8_t, instead of narrowing first.
  • rb a-20260906-8 (#1208): AccessDoor’s Present_Value WriteProperty dispatch validates the wire value before narrowing to uint8_t, instead of narrowing first. Review found the identical bug shape in BinaryLightingOutput’s Present_Value dispatch and AccessDoor’s Door_Alarm_State dispatch (both in the same function) - fixed alongside.
  • rb a-20260906-8 (#1209): BACnetStackAccessDoor::WritePresentValue now still reevaluates internal state when a pulse cleanup-relinquish fails, instead of leaving the array’s actual content unaccounted for.
  • rb a-20260906-9 (#1050): NotificationClass’s required-property list no longer over-requires the object’s own Notification_Class property, matching the profile function’s own required=false classification (135-2024 Table 12-24: O1,2).
  • rb a-20260906-9 (#1051): TrendLog’s Log_Buffer required-property list entry now shares the same STACK_OPTION_TRENDING build-flag guard as the profile function’s own case, instead of disagreeing in a build configuration where the object type is enabled without that flag.
  • rb a-20260907-1 (#1177): BACnetFaultParameter::operator= no longer reports IsValid()==true when Clone() fails under allocation exhaustion — revert-red: the copy reported valid with a NULL m_parameterBase on the pre-fix code.
  • rb a-20260907-1 (#1178): BACnetCharacterstringFaultAlgorithm::MatchesFaultValue now implements cl. 13.4.2’s empty-fault-value match rule (b) — an empty fault-value string now matches an empty monitored string, which the pre-fix code unconditionally skipped.
  • rb a-20260907-1 (#1179): BACnetStateFaultAlgorithm::SetFaultStateValues now returns bool and rejects a fault-value list beyond the 64-entry ceiling instead of silently truncating it, matching ApplyFaultParameters’s own guard on the identical array.
  • rb a-20260907-10 (#1098): BACnetBVLL::Decode no longer marks the wrapper valid when the delegated sub-decode fails and returns 0.
  • rb a-20260907-10 (#1097): BACnetDistributeBroadcastToNetwork::Encode no longer swallows GetNPDU’s failure signal — a buffer too small for the stored NPDU is now correctly rejected instead of reporting a header-only “success”.
  • rb a-20260907-10 (#1099): BACnetStackNetworkPortBase now cross-checks Protocol_Level/Reference_Port (cl. 12.56.11) — a self-contradictory combination (Protocol_Level=PHYSICAL with Reference_Port != 4194303) is rejected regardless of which field is set first.
  • rb a-20260907-11 (#1032): BACnetTestToolInterface::SetAccessCredentialAuthenticationFactors now rejects a NULL valuesConcat buffer paired with a nonzero declared valueLengths[i] instead of silently building an invalid authentication-factor entry.
  • rb a-20260907-11 (#1031): corrected 8 clause mis-citations in CASBACnetStackTestToolDLL.h/.cpp — 7 GetAlarmSummary doc comments wrongly cited cl. 13.10 (GetAlarmSummary’s own clause) instead of cl. 13.8 (ConfirmedEventNotification), plus a sibling GetEventInformation mis-citation of cl. 13.8 instead of cl. 13.12.
  • rb a-20260907-11 (#924): BACnetErrorProcessor::Process now reports the real object type/instance/property identifier via HookError for a WritePropertyMultiple-Error’s first-failed-write-attempt, instead of always reporting useObjectProperty=false with all-zero fields.
  • rb a-20260907-12 (#1106): BACnetInitializeRoutingTable::Encode now guards against bufferOffset exceeding maxBufferLength before each port entry’s encode, mirroring Decode()’s existing cl. 6.4.7 overflow guard (#300).
  • rb a-20260907-12 (#1390): BACnetForwardedNPDU::Encode now rejects rather than truncates when GetNPDU/GetIpAddress cannot write the full originating NPDU/address into the given buffer, mirroring #1097’s fix for the sibling BACnetDistributeBroadcastToNetwork.
  • rb a-20260907-12 (#1108): BACnetRoutingTableEntry now reports its own BACnetStackDatatypes value instead of colliding with the unrelated BACnetRouterEntry’s.
  • rb a-20260907-13 (#1180): BACnetOutOfRangeFaultAlgorithm::SetProperty now implements the Double, SignedInteger, and UnsignedInteger datatypes for FaultLowLimit/FaultHighLimit (cl. 13.4.7), symmetric with GetProperty’s existing handling, instead of failing with no BACnet error set.
  • rb a-20260907-13 (#1181): BACnetListedFaultAlgorithm::CreateEventValues now rejects cleanly instead of crashing when called before any successful Run(), guarding a previously-unchecked NULL m_monitoredValue.
  • rb a-20260907-14 (#1155): BACnetConfirmedEventNotificationRequest/BACnetUnconfirmedEventNotificationRequest now reject a NotifyType of ACK_NOTIFICATION carrying AckRequired, From State, or Event Values (cl. 13.8.1.1.10-13 / 13.9), on both the construction and decode paths.
  • rb a-20260907-14 (#1267): BACnetGetEnrollmentSummaryRequest::Decode now rejects a present-but-malformed optional enrollmentFilter/eventStateFilter/eventTypeFilter/priorityFilter/notificationClassFilter instead of silently treating it as absent.
  • rb a-20260907-15 (#1231): BACnetEngineeringUnits::activeEnergyPulseValue corrected from 47910 (colliding with millisiemensPerCentimeter) to the spec’s own numeric cross-reference value of 47918.
  • rb a-20260907-15 (#944): BACnetAccumulatorRecord::SetAccumulatorStatus/Decode now reject an accumulator-status value outside its closed 5-value ENUMERATED domain (cl. 21.6).
  • rb a-20260907-15 (#1228): BACnetActionCommand/BACnetActionList/BACnetChannelValue/BACnetPrescale/BACnetScale’s Decode() now runs Reset() before the null/zero-length-buffer reject, so a rejected re-decode invalidates a previously-valid object instead of leaving its stale state standing.
  • rb a-20260907-16 (#1211): BACnetPrimitiveENUM::Decode now runs Reset() before its null/zero-length-buffer reject, closing the R56 pattern at its shared root cause for every BACnetPrimitiveENUM subclass.
  • rb a-20260907-16 (#1213): BACnetAccessPassbackMode, BACnetDoorSecuredStatus, BACnetDoorValue, and BACnetLockStatus now reject a value outside their closed ENUMERATED domain instead of silently accepting and marking valid.
  • rb a-20260907-17 (#1182): BACnetEventParameter::operator= no longer reports valid after a failed Clone() under allocation exhaustion, mirroring the identical fix already applied to the sibling BACnetFaultParameter::operator= (#1177).
  • rb a-20260907-17 (#1190): BACnetDestination now rejects an all-false Valid Days mask (SetValidDays(0x00), or the equivalent on the wire) instead of silently accepting a destination that can never fire on any day.
  • rb a-20260907-18 (#1214): a Life Safety Zone’s Tracking_Value WriteProperty (while Out_Of_Service is TRUE) no longer gets silently discarded by the zone roll-up recompute the same call triggers (135-2024 cl. 12.16.5) — the roll-up now skips a zone whose own Out_Of_Service is TRUE, instead of unconditionally overwriting Tracking_Value/Present_Value from the zone’s members before the WriteProperty response is even built.
  • rb a-20260907-19 (#1384): BACnetStackLightingOutput::ApplyLightingCommand now halts an in-progress FADE_TO/RAMP_TO for WARN and the vendor-extension default: case too (135-2024 cl. 12.54.6.1(a)), reconciling Tracking_Value into the fade’s own priority slot before halting — extending #1218’s fix, which had deliberately excluded these two operations since neither writes to the priority array on its own.
  • rb a-20260907-19 (#1220, CR-F27-003): Last_On_Value/Default_On_Value (135-2024 cl. 12.54.44/.45, both unconditionally Required) now have storage, ReadProperty/WriteProperty support, and a Property_List entry — previously completely unimplemented (a ReadProperty of either returned unknownProperty, and both were absent from Property_List). Last_On_Value is read-only, latches the most recent Present_Value that was ≥1.0% (defaulting to 100.0% per the clause, not 0.0%, until Present_Value first reaches that threshold); Default_On_Value is writable, range [1.0%, 100.0%].
  • rb a-20260907-2 (#1170): BACnetStackAlarmAndEventObject::HandleTo{OffNormal,Fault,Normal} now update Acked_Transitions/Event_Time_Stamps unconditionally, before the Event_Enable check — cl. 13.2.2/13.2.3 only gate notification distribution (cl. 13.2.5) on Event_Enable, not this bookkeeping.
  • rb a-20260907-2 (#1171): BACnetSignedOutOfRangeEventAlgorithm’s deadband arithmetic now compares in int64_t, avoiding the signed-integer-overflow/implementation-defined-cast risk the sibling Unsigned variants already guarded against (#631).
  • rb a-20260907-2 (#1172): BACnetChangeOfStateEventAlgorithm’s optional condition (c) — re-indicating OFFNORMAL on a different alarm value — is no longer permanently dead; m_lastAlarmValue now only updates on an actually-committed transition to OFFNORMAL.
  • rb a-20260907-2 (#1173): BACnetStackAlarmAndEventObject::HandleToOffNormal/HandleToNormal now guard m_eventAlgorithm != NULL before dereferencing, matching HandleToFault’s existing pattern.
  • rb a-20260907-2 (#1174): BACnetStackAlarmAndEventObject’s copy constructor and operator= are now explicitly deleted, closing a Rule-of-Three gap on its two owned raw pointers.
  • rb a-20260907-20 (#1386): BACnetSpecialEvent::operator= now gates the object’s own SetValid() on whether the List_Of_Time_Values copy actually committed — a rare second-pass Clone() allocation failure inside that copy previously left the list empty and internally invalid while the whole object still unconditionally reported itself valid, the same “reports valid, doesn’t match reality” defect class as #699/#1222, one line further down.
  • rb a-20260907-21 (#1391): BACnetDBDevice::EnableNetworkPortVirtualProperties/EnableNetworkPortSecureConnectProperties now reject a Protocol_Level=PHYSICAL / Reference_Port != 4194303 combination (135-2024 cl. 12.56.11) before constructing the network port object, instead of letting the constructor’s own rejection silently pass unnoticed — previously this left Network_Number/Network_Number_Quality at their reset defaults while the enclosing function still reported success.
  • rb a-20260907-22 (#1061, CR-F03-017): BACnetDateTime::operator< no longer silently discards the Time comparison when two BACnetDateTimes share Year/Month/Day but carry different Weekday octets (135-2024 cl. 20.2.12 — an inconsistent Weekday is “a local matter”, not a decode error, so this is a legal wire-reachable state) — the comparison now gates Time on neither Date being less than the other, matching BACnetPrimitiveDATE::operator<’s own semantics, instead of on IsEqual() (which also compares Weekday).
  • rb a-20260907-3 (#1194): BACnetEventLogDatum/BACnetLogDatum/BACnetLogMultipleRecord’s operator= implementations no longer report valid when a nested Clone()/copy fails under allocation exhaustion (the 7th confirmed instance of this session’s recurring “unconditional validity propagation past a fallible nested Clone()” defect).
  • rb a-20260907-3 (#1195): BACnetLogMultipleRecord::GetElements no longer inserts a NULL m_logData into the renderable-elements list.
  • rb a-20260907-3 (#947): BACnetAnnexAATimeSeriesRecord::GetValue’s absent-column sentinel is now distinct from a present-but-empty CSV cell.
  • rb a-20260907-3 (#948, F22 scope): BACnetAnnexAATimeSeriesStream::ValidateColumnName now enforces the documented printable-byte range (0x20-0x7E), rejecting control bytes and DEL in column names.
  • rb a-20260907-4 (#1040): BACnetDBDevice::UpdateObjectIdentifier now migrates 4 specialized per-object-type maps (Event Enrollment, Alert Enrollment, Notification Forwarder, Program) on rename, not just the generic object-list/property-override maps.
  • rb a-20260907-4 (#1054): BACnetDatabase::SetLiftHigherLowerDeck no longer dereferences a failed Clone() of the elevator group’s group members.
  • rb a-20260907-4 (#1055): BACnetDatabase::Reset() no longer unconditionally dereferences m_sentConfirmedRequests, matching every other consumer’s existing NULL guard.
  • rb a-20260907-40 (#1247, CR-F17-014, CR-F17-015, CR-F17-016): corrected wrong ASHRAE 135-2024 spec-clause citations in 5 event-algorithm files, verified against the extracted spec index — BACnetChangeOfValueEventAlgorithm is 13.3.3 (was cited as 13.3.2), BACnetChangeOfCharacterstringEventAlgorithm is 13.3.16 (was cited as 13.3.6), BACnetChangeOfStatusFlagsEventAlgorithm is 13.3.11 (was cited as 13.3.3), BACnetChangeOfTimerEventAlgorithm is 13.3.19 (was cited as 13.3.17), BACnetCommandFailureEventAlgorithm is 13.3.4 (was cited as 13.3.3). Also added the missing citation (13.3.2) to BACnetChangeOfStateEventAlgorithm.h’s class doc comment, which previously cited none at all. Comment-only, no behavior change. CR-F17-017 (a bare return 0; instead of a named constant) was found already fixed by an earlier batch (a-20260907-2, d9877c26) and is not-repro. CR-F17-013 (a filename typo) is deliberately excluded per the finding’s own recommendation to flag it for the owner rather than fix unilaterally, given rename risk.
  • rb a-20260907-41 (#1247, CR-F17-020): BACnetStackAlarmAndEventObject::CheckAndRunAlgorithms now explicitly resets its shared eventValues local to NULL between the fault-algorithm and event-algorithm Run() calls. Traced BACnetFaultAlgorithm::ChangeReliability and all 7 concrete fault-algorithm subclasses’ Run() implementations and confirmed the current contract already guarantees eventValues is NULL at that point — every path that writes it also returns immediately via a committal transition. This is a defensive hardening change with no behavior change today, added so the function stays self-defending if a future fault algorithm ever violates that contract. tests/TestBACnetStackAlarmAndEventObject.cpp exists (added by an earlier batch, d9877c26) but doesn’t cover CheckAndRunAlgorithms/eventValues; no test added here since this is a verified-no-op hardening change, not a bug fix — dedicated coverage for CheckAndRunAlgorithms remains part of CR-F17-019’s scope.
  • rb a-20260907-42 (#1434): BACnetFaultAlgorithm::GetPropertyMonitoredByIntrinsicFaultForObject(lift) and (escalator) now correctly return BACnetPropertyIdentifier::faultSignals instead of INVALID_PROPERTY_IDENTIFIER. This function had silently disagreed with the (correct) IsObjectPropertyMonitoredByIntrinsicFault(lift/escalator, faultSignals), which already returned true. Found while adding test coverage below; this static helper is not currently called anywhere in source/, so the bug was latent/unreachable in shipped behavior.
  • rb a-20260907-43 (#1175, CR-F18-001, partial): BACnetLifeSafetyFaultAlgorithm now implements 135-2024 cl. 13.4.4 condition (c) — while already MULTI_STATE_FAULT, a life-safety Mode change re-indicates the MULTI_STATE_FAULT transition even though the monitored value’s own fault-membership hasn’t changed. Adds a SetMode(uint32_t) live-feed mutator, mirroring BACnetChangeOfLifeSafetyEventAlgorithm::SetMode’s already-established pattern, plus the Mode-at-last-fault-transition tracking condition (c) needs (a different comparison basis than the event algorithm’s own “changed since last Run” — condition (c) compares against the mode recorded at the last transition into MULTI_STATE_FAULT, not the previous Run() call). Condition (d) (optional) is not implemented — it is a separate trigger (a different fault value while already faulted).
  • This is the algorithm-side half of the fix only. Nothing currently calls the new SetMode() — the wiring that feeds it (BACnetDBDevice::FeedLifeSafetyIntrinsicAndReport, which already does the equivalent for the sibling BACnetChangeOfLifeSafetyEventAlgorithm::SetMode three lines away) lives in a different review family (F02) and is out of scope for an F18-only batch per the runbook’s family-boundary rule. Filed as #1440 with the exact wiring needed. The S1 defect CR-F18-001 describes therefore remains live in shipped behavior until #1440 lands — #1175’s checkbox is intentionally left unticked, disclosed here.
  • rb a-20260907-44 (#1440, CR-F18-001 followup): BACnetDBDevice::FeedLifeSafetyIntrinsicAndReport now feeds BACnetLifeSafetyFaultAlgorithm::SetMode with the object’s current life-safety Mode before each Run(), symmetric to the pre-existing feed to the sibling BACnetChangeOfLifeSafetyEventAlgorithm::SetMode a few lines above it. This completes the wiring left open by a-20260907-43 (#1175, CR-F18-001) — the algorithm-side cl. 13.4.4 condition (c) logic (a life-safety Mode change while already MULTI_STATE_FAULT re-indicates the transition) is now actually reachable in shipped behavior, closing out the S1 defect CR-F18-001 describes.
  • Adds BACnetLifeSafetyFaultAlgorithm::GetMode(), a trivial read-back accessor mirroring the base class’s own GetCurrentReliability(), needed to make the wiring above directly testable (there was no other externally-observable way to confirm the live-feed actually reached the algorithm, mirroring how the sibling BACnetChangeOfLifeSafetyEventAlgorithm also has no GetMode() of its own — this fault-algorithm class gets one specifically for its own wiring’s test).
  • Scope note: this batch touches both F02 (BACnetDBDevice.cpp, the wiring) and F18 (BACnetLifeSafetyFaultAlgorithm.h/.cpp, the GetMode() accessor only — no behavior change to condition (a)/(b)/(c) logic itself, which a-43 already implemented and tested). The F18 addition is a trivial, behavior-preserving read-only accessor needed solely for this batch’s own integration test observability, not a substantive algorithm change — judged in-scope for the batch that completes the two-family fix a-43 explicitly deferred, rather than filing a third issue for a one-line getter.
  • rb a-20260907-46 (#946, CR-271, partial — F24 scope only): BACnetAccessRule (135-2024 cl. 21.6) now enforces its declared grammar instead of accepting any shape. SetTimeRangeSpecifier/SetLocationSpecifier reject any value other than specified(0)/always(1) and specified(0)/all(1) respectively (previously any uint32_t was accepted). SetEnable (the final mandatory field) and Decode now both compose overall validity from every field, rejecting a SEQUENCE where time-range/location is present when its specifier says the rule applies “always”/“all”, or absent when the specifier says “specified” — previously either function unconditionally marked the whole object valid regardless of this iff relationship, letting an object with undefined policy meaning reach access-rights evaluation.
  • BACnetAssignedAccessRights (cl. 21.6): SetAssignedAccessRights now rejects an invalid reference instead of unconditionally accepting it and returning true; SetEnable now checks assigned-access-rights was validly set before marking the two-field required SEQUENCE valid. New discriminating tests added to the existing tests/TestBACnetSecuritySuite.cpp, which already had basic round-trip/factory/decode-reject coverage for this class.
  • Partial: this finding (#946) also names BACnetActionCommand (family F29) and BACnetAddressBinding (family F13, lane transport) for the identical validity-composition gap. Both are out of scope for this F24-only batch per the runbook’s family-boundary rule; follow-up issues to be filed for each.
  • rb a-20260907-47 (#1445, CR-271-followup-F29): BACnetActionCommand (135-2024 cl. 12.10) SetDeviceIdentifier now rejects any object type other than Device — cl. 12.10’s own prose (“If the Device_Identifier is not present, then the write shall be performed on objects residing in the device that contains the Command object”) establishes this field can only ever name a Device. Decode now enforces the same restriction on the wire path. SetComplete() (now returning bool, matching the established sibling-class idiom, e.g. BACnetAcknowledgeAlarmInfo) composes validity from every mandatory field (object-identifier, property-identifier, property-value, quit-on-failure, write-successful) instead of marking the SEQUENCE valid unconditionally.
  • Re-verified #1445’s third named gap (SetPriority’s domain check) at the tip and found it already fixed by an unrelated earlier issue, #1229/CR-F29-008 — not re-fixed here, no action needed.
  • rb a-20260907-48 (#1446, CR-271-followup-F13): BACnetAddressBinding::Decode now rejects a decoded deviceIdentifier whose object type is not Device — SetDeviceIdentifier (the builder path) already hardcodes BACnetObjectType::device and takes no objectType parameter at all, so a structurally well-formed non-Device identifier arriving on the wire was the only way to construct an Address-Binding entry naming a non-Device object, contradicting BACnet-Address-Binding’s own definition (135-2024 cl. 21, BACnetAddressBinding ::= SEQUENCE { device-identifier BACnetObjectIdentifier, ... }, always instantiated in this stack against Device-only endpoints). This closes the last of the three follow-ups filed from #946/CR-271 (F24 via #1444/a-46, F29 via #1449/a-47, F13 here).
  • rb a-20260907-49 (#1144, CR-F13-010): BACnetUnconfirmedRequestPDU::Decode/DecodeUsingSupportedServices’s whoIs special-case now also checks the inner Who-Is request’s own IsValid() (not just size == 0) before marking the outer PDU valid. BACnetWhoIsRequest::Decode returns size == 0 both for the legal “both limits omitted” case (IsValid()==true) and for a garbled Who-Is with only one limit present (135-2024 cl. 16.10.1.1.1-.2 requires both limits or neither; IsValid()==false) — the wrapper’s whoIs special-case previously inspected only size, so it accepted the garbled case identically to the legal one and unconditionally called SetValid(true). Currently masked in the shipped processor path (BACnetWhoIsProcessor::Process independently re-checks the inner IsValid()), but any other consumer trusting the outer PDU’s own IsValid() would have treated a garbled Who-Is as a fully valid, no-limits one. The IsValid() check is scoped to whoIs only, matching this finding’s evidence — an earlier draft checked it for every service choice and broke 11 AcknowledgeAlarmExecutionTests BTL cases, because BACnetUnconfirmedEventNotificationRequest maintains its own, separately-evolved validity contract (AreAllParametersValid()) that this issue was never about; caught locally before the official build.
  • rb a-20260907-5 (#1196): BACnetAuditLogQueryProcessor::Process now returns records latest-first and filters strictly before the Start At Sequence Number cursor, per cl. 13.19.1.1.3 — both halves of the pagination contract were previously inverted.
  • rb a-20260907-5 (#1197): BACnetStackAuditLog’s Record_Count ReadProperty answer is now Unsigned64 on the wire, matching Total_Record_Count (cl. 12.64).
  • rb a-20260907-5 (#1198, SECURITY): BACnetStackAuthorizationCache::FindToken/FindTokenForAudience now reject a cached token whose Not_Before/Not_After fields don’t cover the current time (cl. 17.4).
  • rb a-20260907-5 (#926): BACnetDBDevice::IsUnconfirmedServiceSupported gains the missing unconfirmedAuditNotification case — the receive path was unconditionally rejected even with the capability bit explicitly enabled.
  • rb a-20260907-5 (#1377): BACnetAuthenticationFactor::SetAuthenticationFactor is now transactional — a partial failure leaves the object completely unchanged instead of a valid hybrid of new and stale fields.
  • rb a-20260907-50 (#950, CR-F28-001/002/003): three domain/validity gaps in the Schedule/calendar family (135-2024 cl. 21.6/12.24.8). BACnetCalendarEntry::SetDate/SetDateRange now compose the parent CHOICE’s validity from the supplied child instead of marking it valid unconditionally, matching the file’s own SetWeekNDay shape (CR-F28-001). BACnetSpecialEvent::SetCalendarReference/Decode now restrict calendar-reference to BACnetObjectType::calendar (CR-F28-002). BACnetSpecialEvent::SetEventPriority/Decode now enforce the declared Unsigned(1..16) domain (CR-F28-003). This closes #950’s F28 portion only — the F24 portion (BACnetStackCredentialDataInput, CR-F24-013), the BACnetColorCommand domain gaps, and BACnetSpecialEvent::operator=’s second-clone-pass allocation-failure gap remain, and #950 stays open with those noted.
  • Deliberately out of scope, noted for a future finding: BACnetCalendarEntry::Decode() decodes directly into m_date/m_dateRange/m_weekNDay (not through the fixed setters) and unconditionally calls SetValid(true) without checking the decoded child’s own validity — a separate, Decode()-specific instance of the same class of bug CR-F28-001 fixes at the setter level, but not covered by this finding’s own evidence (which cites only the setters), so not fixed here.
  • rb a-20260907-51 (#950, CR-F24-013): BACnetStackCredentialDataInput::PushFactor/SetPresentValueDirect no longer unconditionally report success and advance Update_Time when the resulting/supplied Present_Value is itself invalid (e.g. a non-NULL-length value with a NULL pointer). Both now check the factor’s own IsValid() before committing it, matching BACnetAuthenticationFactor’s already-public IsValid() accessor (inherited from BACnetObjectBase, no new accessor needed). Corrected the class’s own doc comment, which previously claimed “there is no rejectable input shape at this layer” — true only for the unrecognized-formatType-becomes-UNDEFINED case, not this one. This closes the last portion of #950 this session picked up; #950 stays open with the ColorCommand domain gaps and BACnetSpecialEvent::operator=’s second-clone-pass gap still outstanding.
  • Deliberately out of scope, noted for a future finding: BACnetDBDevice.cpp:13119’s WriteProperty handler for credentialDataInput’s Present_Value calls SetPresentValueDirect and ignores its return value entirely, unconditionally returning true. In practice the written BACnetAuthenticationFactor already passed the normal decode pipeline before reaching this call site, so it is very likely always valid here — but this wasn’t independently confirmed, and CR-F24-013’s own cited evidence is scoped to PushFactor/SetPresentValueDirect themselves, not their callers, so this was flagged rather than fixed.
  • rb a-20260907-52 (#950, ColorCommand domain-gap portion): BACnetColorCommand::SetFadeTime/SetRampRate/SetStepIncrement and Decode() now enforce cl. 21.6’s own declared domains (fade-time Unsigned(100..86400000), ramp-rate/step-increment Unsigned(1..30000)) instead of accepting any Unsigned value — the same defect shape, and for fade-time the same bounds, as the sibling BACnetLightingCommand‘s own #1219 fix. #950’s other ColorCommand evidence (the five optional-field decode probes’ missing error.IsSet() checks) was already fixed by an unrelated issue, #1270/#767-F27, confirmed still present at the tip — not re-fixed here. BACnetSpecialEvent::operator=’s second-clone-pass allocation-failure gap remains #950’s only outstanding item; #950 stays open.
  • Test scope note: the Decode()-level domain checks are not covered by a dedicated wire-byte test — the setter fix in the same commit blocks constructing an out-of-domain value through the public API at all, the same situation as #1455/a-50’s SpecialEvent fixes. Setter-level tests directly cover the identical domain logic reused in Decode().
  • rb a-20260907-53 (#949, CR-F23-003, CR-F23-004): BACnetAuditLogRecordResult::GetDatatype() and BACnetAuthenticationPolicyElement::GetDatatype() no longer return the shared BACnetStackDatatypes::Invalid sentinel — each now has its own unique, unconditionally-appended enumerator (AuditLogRecordResult, AuthenticationPolicyElement), the same fix shape as the existing #1108/CR-F06-010 precedent for BACnetRoutingTableEntry. Sharing Invalid made IsEqual()’s other->GetDatatype() != this->GetDatatype() type guard a no-op between any two Invalid-returning classes compared through a common BACnetObjectBase*, letting the subsequent static_cast proceed unconditionally — undefined behavior if the two concrete types actually differ. New test file tests/TestBACnetAuthenticationPolicyElement.cpp (no prior coverage existed for this class at all). This closes #949’s two confirmed sub-findings only; 12 other classes still return the shared Invalid sentinel (BACnetAuditLogQueryACK, BACnetAuditLogQueryParameters, BACnetAuditLogQueryRequest, BACnetAuditLogRecord, BACnetAuthRequest, BACnetAuthRequestACK, BACnetConfirmedAuditNotificationRequest, BACnetPriorityValue, BACnetRawConfirmedServiceRequest, BACnetTimeValue, BACnetUnconfirmedAuditNotificationRequest, plus BACnetAbstractSyntaxType’s own fallback), and #949’s many other named CR-030 “marks valid without checking every mandatory child” gaps (across AuthenticationPolicy/AuthenticationClient/AuthenticationPeer/ChannelValue/AddListElementRequest/RemoveListElementRequest) remain — #949 stays open.
  • rb a-20260907-54 (#1260, #767/F06): BACnetRouterEntry::Decode()’s optional performance-index [3] field treated a present-but-malformed wire value identically to a genuinely absent one — DecodeWithContext(..., true) returning size==0 is ambiguous between the two, and the code only checked size, never error.IsSet(). Added the standard if (size == 0 && error.IsSet()) { return 0; } per-call-site check (per #767’s established rule, never via the shared BACnetObjectBase::DecodeWithContext — #890’s attempt at a shared fix broke ~20 tests and was reverted). Last remaining site in family F06.
  • rb a-20260907-55 (#1265, #767/F13): BACnetWhoHasRequest::Decode/BACnetWhoIsRequest::Decode’s optional Device_Instance_Range_Low/High_Limit fields treated a present-but-malformed wire value identically to a genuinely absent one (error.IsSet() never checked after DecodeWithContext(..., true)). Both files now check error.IsSet() at each of their two call sites, per #767’s established per-call-site pattern. BACnetWhoIsRequest::Decode in particular had a real wrongly-ACCEPTED gap: a malformed-but-present low limit (IsValid()==false, same as a genuinely absent one) combined with a genuinely absent high limit would satisfy the function’s own low/high presence-symmetry check and be wrongly accepted as the legal “both omitted” case.
  • Re-verified at the tip, found already fixed: #1265’s third listed site, source/BACnetReinitializeDeviceRequest.cpp’s optional Password field, was already fixed by #1141/CR-F13-007 (closed) — not re-fixed here.
  • Closes #1265, the last remaining F13 site from #767’s audit.
  • rb a-20260907-56 (#1261, #767/F09): BACnetSCHubConnection::Decode/BACnetSCHubFunctionConnection::Decode’s optional error/error-details fields treated a present-but-malformed wire value identically to a genuinely absent one — DecodeWithContext(..., true) returning size==0 is ambiguous between the two, and neither file checked error.IsSet(). Added the standard if (size == 0 && error.IsSet()) { return 0; } per-call-site check at all 4 sites (2 per file), per #767’s established rule. Closes the last remaining F09 sites from #767’s audit.
  • Flagged, not fixed: while building BACnetSCHubConnection’s discriminating test, found that Decode()’s rejection paths — the pre-existing mandatory hubUri/connectionState ones, not just this fix’s new optional-field check — never leave the object reporting IsValid()==false after a rejected decode. Reset() (called at the top of Decode()) itself calls SetHubUri(""), which unconditionally calls SetValid(true) as a side effect, and no rejection branch afterward ever explicitly re-invalidates. A real, class-wide gap, but outside #767/F09’s own scope (it would need touching the mandatory-field failure paths too, not just the optional ones this issue is about) — not fixed here.
  • rb a-20260907-6 (#1270): BACnetColorCommand::Decode’s 5 optional fields (target-color, target-color-temperature, fade-time, ramp-rate, step-increment) now reject a present-but-malformed value instead of silently treating it as absent, closing out the #767/F27 absent-vs-malformed decode audit.
  • rb a-20260907-6 (#1218): BACnetStackLightingOutput::ApplyLightingCommand now halts an in-progress FADE_TO/RAMP_TO for STEP_*/WARN/WARN_OFF/WARN_RELINQUISH/vendor-extension operations at an equal-or-higher priority (135-2024 cl. 12.54.6.1(a)), matching the halt check the direct Present_Value write path already had.
  • rb a-20260907-6 (#1219): BACnetLightingCommand’s 5 optional fields (target-level, ramp-rate, step-increment, fade-time, priority) now range-check against their 135-2024 cl. 21.6 domains on both the setter and decode paths, mirroring BACnetxyColor’s reference pattern.
  • rb a-20260907-7 (#1210): BACnetAuthenticationPolicy::operator= no longer force-marks the copy valid on a Clone() failure — a first-pass failure now leaves the target’s previous Policy list completely unchanged instead of silently truncated.
  • rb a-20260907-7 (#1212): BACnetAccessRule/BACnetAssignedAccessRights/BACnetCredentialAuthenticationFactor/BACnetAccessToken’s Decode() now Reset() before the null/zero-length-buffer reject check, so a rejected re-decode after a prior successful decode correctly invalidates the object instead of leaving its stale valid state standing.
  • rb a-20260907-7 (#1269): BACnetAccessToken::Decode’s 2 optional fields (not-before, not-after) now reject a present-but-malformed value instead of silently treating it as absent, closing out the #767/F24 decode audit.
  • rb a-20260907-7 (#1254): BACnetAccessAuthenticationFactorDisable gains the STACK_OPTION_OBJECT_TYPE_ACCESS_CREDENTIAL feature guard its sole consumer already requires; BACnetAccessUserType gains dedicated test coverage (previously had none).
  • rb a-20260907-8 (#1086): BACnetErrorCode’s string table gains its 96 missing entries — every enumerator declared in the header now resolves to its name instead of falling through to "Unknown Enumeration Value (N)".
  • rb a-20260907-8 (#1082): BACnetStackSentConfirmedRequests::RemoveRequest’s BACnet/SC fallback now also matches destinationNetwork/destinationAddress, so it can no longer cancel an unrelated device’s still-outstanding confirmed request when two devices reachable through the same SC hub connection share an invoke ID.
  • rb a-20260907-9 (#977): BACnetDBDevice::SetObjectTypeCreatable/IsObjectTypeSupported no longer reject vendor-proprietary object types (128..1023) — a proprietary object type can now be made creatable and actually created via CreateObject, without being written into the Protocol_Object_Types_Supported bitstring (which has no room for it).
  • rb a-20260908-57 (#1268, #767/F20, CR-F20-004): BACnetNotificationParametersChangeOfTimer::Decode()’s three optional fields (last-state-change/initial-timeout/expiration-time) treated a present-but-malformed wire value identically to a genuinely absent one. Fixed with the tag-peek technique the finding’s own dedupe note points to — the sibling BACnetNotificationParametersAccessEvent::Decode()’s already-shipped fix for its own optional authenticationFactor field (peek the field’s context tag before decoding; if the tag is genuinely present, a subsequent size == 0 is a real malformed-field error, not a legitimate omission) — rather than the plainer #767-generic if (size == 0 && error.IsSet()) check, since a closing-tag mismatch (a different malformed shape found and worked around in an earlier session batch) does not reliably set error.IsSet() on its own. New test file tests/TestBACnetNotificationParametersChangeOfTimer.cpp — no prior coverage existed for this class at all, per the finding’s own note. Closes #1268, the last remaining child of #767’s whole audit.
  • rb a-20260908-58 (#1496): two adjacent memory leaks on unrelated failure branches in elevator-group/lift database functions — BACnetDatabase::SetLiftHigherLowerDeck’s RemoveObject() failure branch and BACnetDBDevice::EnableLiftOrEscalatorProperties’s InsertObjectAndTakeOwnership() failure branch both returned false without freeing the already-allocated/cloned groupMembers object, mirroring the identical already-shipped CR-F02c-005 pattern a few lines below each in the same functions. Fixed with SAFE_DELETE(groupMembers) before each return false;. Both branches are reachable only when the failing call’s own inner new(std::nothrow) BACnetPrimitiveObjectIdentifier(...) argument allocation fails — BACnetAbstractSyntaxType::RemoveObject()/InsertObjectAndTakeOwnership() both return true unconditionally otherwise (confirmed by reading their implementations; the issue’s alternate “object not found” trigger for RemoveObject() does not apply) — the same un-fault-injectable-in-this-harness shape this file’s own prior #1054 fix already documented as an HONEST LIMITATION nearby. Verified via existing success-path tests (SetLiftHigherLowerDeck_NoFaultInjection_Succeeds, EnableLiftOrEscalatorProperties’s several passing tests) that both modified lines are still reached and still succeed on the normal path, plus direct code reading of the fix itself.
  • rb a-20260908-60 (#1094, CR-F05-044, SECURITY): BACnetStackInvokeIds::m_invokeIds was an unbounded, never-evicted per-peer map — every distinct destination identity this device has ever sent a confirmed request to permanently occupied one map entry for the process lifetime. Traced the finding’s own open impact question (whether an external actor can drive an unbounded number of distinct addresses into this map): BACnetBusinessLogic::SendExternalWriteProperty (a caller of the map-based path) can pass a DAB-derived destination address, and DAB entries are themselves derived from inbound I-Am traffic — the identical unbounded-table shape already fixed in #1142 (DABTable::Acquire(), reachable from a WriteProperty to a NotificationClass’s Recipient_List, capped at CAS_BACNET_DAB_MAX_ENTRIES). Fixed with a new CAS_BACNET_INVOKE_ID_MAX_TRACKED_PEERS cap (4096, matching DAB’s own scale) and least-recently-used eviction, mirroring BACnetPhysicalRouter’s already-shipped MAX_ROUTER_REJECT_RATE_LIMIT_SOURCES LRU discipline rather than inventing a new technique. Two new tests using SetDefaultSeed(0) for a deterministic revert-red (fill the table to capacity, evict via one more new peer, confirm the evicted peer reseeds to 0 on its next use rather than resuming its old, unbounded-table counter at 1) plus a below-capacity control confirming ordinary reuse never resets.
  • rb a-20260908-61 (#1161, CR-F16-009, SECURITY): BACnetStackNotificationForwarder’s Subscribed_Recipients (cl. 12.51.9) had only a per-entry Time_Remaining expiry, no maximum entry count — a peer that can reach AddListElement on Subscribed_Recipients could add distinct Recipient+Process Identifier entries faster than the per-minute expiry sweep reclaims them, growing the list without bound. Fixed with a new CAS_BACNET_MAX_SUBSCRIBED_RECIPIENTS cap (32, sized like the device-wide MAX_ROUTER_ROUTES rather than the smaller per-monitored-object MAX_SUBSCRIPTION_PER_OBJECT), enforced in SetProperty’s subscribedRecipients case — the single choke point both AddListElement’s generic merge machinery and a plain WriteProperty funnel through, since value is already the complete post-operation list by the time SetProperty runs. Rejects with BACnetErrorClass::resources/BACnetErrorCode::noSpaceToAddListElement, reusing the exact error convention BACnetStackActiveCOVSubscriptions already uses for its own analogous per-object subscription cap. New test drives the real AddListElement path (not the test-only AddSubscriptionForTest bypass, which never reaches SetProperty’s cap check) to fill the list to exactly its capacity, then confirms one more distinct entry is rejected rather than silently growing the list.
  • rb a-20260908-62 (#1234, F03 hygiene mini-batch): 7 confirmed S3/S4 findings, one commit each.
    • CR-F03-006: BACnetPrimitiveBase::EncodeWithContext’s doc comment claimed it “First sets the context tag”; the const method only validates the object’s existing tag against the given tag number, it never sets one. Corrected to describe validate-then-encode.
    • CR-F03-009: BACnetTag::Decode’s 4-octet extended-length path shifted a promoted uint8_t (which promotes to a signed int) left by 24/16/8 before widening — shifting a byte with bit 7 set into bit 31 of a signed int is undefined behavior, not just implementation-defined. Widened each octet to uint32_t before shifting; no observable behavior change on any currently-supported target.
    • CR-F03-013: BACnetOptionalDateTime/BACnetOptionalDateTimePattern (.h/.cpp, 4 files) carried a truncated copyright/license header missing the Licensee/Publisher/Copyright/Contact/Reward/Unique-Identifiers sections every other file in the family has. Restored the full header block.
    • CR-F03-018: tests/TestBACnetPrimitiveDATE.cpp’s “Wildcard - Day” fixture comments (decode and encode tests) said 0x75 0C FF 02 but the actual encoded/expected byte is 0x07 (month 7), not 0x0C (12) — a comment-only typo, the test bytes and assertions were always correct. Fixed both occurrences to 0x75 07 FF 02.
    • CR-F03-021: deleted a fully dead, commented-out ToXml test block in tests/TestBACnetPrimitiveTIME.cpp referencing a ToXML method that no longer exists on this class.
    • CR-F03-033: BACnetPropertyValue::SetPropertyValue’s out-of-line definition documented its priority default argument as /*= 0*/, but the header’s actual default is 16. Corrected the comment to match.
    • CR-F03-040: BACnetObjectBase::DecodeWithContext/EncodeWithContext (the generic, widely-shared helper used by every BACnetObjectBase-derived optional field) carried leftover BACnetError-specific comments (“Decode the Error”, “Encode the error”, “Error decoding the error”) from whichever class this helper was originally extracted from. Reworded to generic “Decode/Encode the wrapped value” language.
  • rb a-20260908-63 (#1255, F26 hygiene mini-batch): BACnetLandingCall/BACnetLandingCallStatus/BACnetLandingDoor::operator= (CR-F26-005) each ignored their own Set* calls’ boolean return values before unconditionally marking the copy valid via SetValid(rhs.IsValid()) alone. Fixed by chaining each Set* call’s own return and gating SetValid() on the combined result AND rhs.IsValid(), matching the pattern already used elsewhere in the codebase. Since rhs is already valid, none of these setters can actually fail from a legitimately-valid source today (they’d need a fault-injected primitive-setter failure to observe a difference) — the fix is defensive, and the new tests are honest liveness/control tests confirming normal copy-assignment still works correctly, matching the same “HONEST LIMITATION” precedent this codebase already uses for un-fault-injectable branches.
    • Also fixed CR-F26-007: BACnetLandingCall::Decode’s floor-number decode-failure log said "Failed to decode networkNumber" (a copy-paste leftover) instead of "Failed to decode floorNumber".
    • CR-F26-006 (no dedicated test file for BACnetLandingDoor/BACnetLandingDoorStatus) was found already resolved by an earlier, unrelated batch — both test files exist with real (non-stub) coverage, split asymmetrically: TestBACnetLandingDoor.cpp covers Decode/Encode round-trip and IsEqual but has no Clone() test; TestBACnetLandingDoorStatus.cpp covers Clone() (including a MemTracker fault-injection case) but has no Encode/Decode round-trip or IsEqual test. Ticked as resolved given the aggregate coverage; a future batch could still add the missing half to each file if stricter per-file parity is wanted.
  • rb a-20260908-64 (#1256, CR-F27-005): BACnetColorOperation/BACnetColorOperationInProgress/BACnetColorTransition’s header comments fabricated an ASHRAE reserved-value-range annotation and a trailing ... extensibility marker the actual spec text does not carry for these three enumerations. Verified against _spec/spec/extracted/135-2024/clauses/21.6.txt: all three ENUMERATED definitions in the real spec have no ... and no reserved-range language — they are genuinely closed, non-extensible enumerations. Removed both fabricated elements from all three headers and replaced with an accurate closed-enumeration note. Docs-only, no behavior change.
  • rb a-20260908-65 (#1257, F28 hygiene mini-batch, 2 of 3 items): BACnetDailySchedule::operator= (CR-F28-010) ignored its copy loop’s Insert() return value, silently dropping an entry from the copy when its Clone() failed (e.g. under allocation exhaustion) rather than reflecting that in IsValid(). Fixed by composing validity from whether every Insert() call succeeded. BACnetTimeValue::operator= (CR-F28-011) could leave IsValid()==true with m_value==NULL when rhs.m_value’s own Clone() failed — unlike the converting constructor’s own already-fixed pattern (#748). Fixed by composing validity from (rhs.m_value == NULL) || (this->m_value != NULL), matching the constructor. Both new tests are honest liveness/control tests rather than genuine revert-reds — forcing the specific Clone() calls to fail would need a freshly-calibrated MemTracker::SetFailAfter count, a class of fault injection this session has previously found easy to mis-trace, and not warranted for a single S3 hygiene item each.
    • CR-F28-012 (no test coverage for BACnetTimeRangeSpecifier) is deferred to a follow-up batch — it needs a CASBACnetStackTests.vcxproj registration, and another concurrent session holds an active claim on that exact file.
  • rb a-20260908-66 (#1258, F29 hygiene mini-batch): BACnetPrescale::SetModuloDivide/Decode (CR-F29-011) previously accepted moduloDivide == 0 with no validation. Cl. 12.61’s own conversion algorithm uses modulo-divide as a repeated-subtraction divisor (“while the accumulator >= modulo-divide: … decrease the accumulator by modulo-divide”); a value of 0 would never reduce the accumulator, an infinite loop in that algorithm rather than a one-shot division-by-zero. Rejected in both the setter and Decode() (with BACnetRejectReason::parameterOutOfRange on the wire path). No current consumer of GetModuloDivide() exists in this codebase, so this is a defensive/latent fix, not a live crash fix.
    • Also removed CR-F29-012: a dead, namespace-scope, per-translation-unit-duplicated string-constant block in BACnetStatusFlags.h (BACNET_STATUS_FLAGS_NAME/CreateBACnetStatusFlagsEnumValueString()/BACnetStatusFlagsEnumNames) — the class extends BACnetPrimitiveBitSTRING, not BACnetPrimitiveENUM, so it never consumed this block; confirmed zero references anywhere in the codebase before removing.
    • CR-F29-013 and CR-F29-014 are both S4 informational findings with no code change recommended — posted corroborating-evidence comments on the target issues (#493 and #541 respectively) per each finding’s own recommendation.
    • Review-fix: SetPrescale’s SetMultiplier(...) && SetModuloDivide(...) ordering let the CR-F29-011 rejection introduce a newly-reachable partial-update bug — BACnetPrimitiveUINT::SetValue has no failure path, so SetMultiplier always succeeds and always commits first, leaving multiplier updated but moduloDivide untouched on a call the function itself reports as failed. Fixed by evaluating SetModuloDivide first, so a rejected call leaves both fields completely untouched.
  • rb a-20260908-67 (#1251, F21 hygiene mini-batch, 2 of 5 items): BACnetStackEventEnrollmentObject (CR-F21-005) embedded a raw-pointer-owning state core (BACnetStackAlarmAndEventObject) by value with no declared copy constructor/operator=, unlike its sibling BACnetStackNotificationClass, which already = deletes both for the identical shape under #781. Added the same = delete pair — confirmed no caller in this codebase copies/assigns an instance today (stored only as a pointer in BACnetDBDevice’s map, constructed only via new()).
    • Also fixed CR-F21-008: BACnetRecipient::operator= derived validity from rhs.IsValid() alone rather than from whether its own SetDevice/SetAddress call actually succeeded — the same structural gap already fixed 4 times this session on sibling CHOICE-wrapper classes. Fixed by composing validity from the actual setter return value. The pre-existing OperatorEquals test already exercises both branches with IsValid() assertions, serving as adequate liveness/control coverage.
    • CR-F21-007 (a diagnostic-warning judgment call), CR-F21-009 (a new test for 2 reject paths), and CR-F21-010 (low-priority test files for 4 thin enums) are deferred to a future batch.
  • rb a-20260908-68 (#1250, F20 hygiene mini-batch, 3 of 7 items): BACnetNotificationParametersChangeOfState (CR-F20-006) exported its public string-literal constant under the wrong, ChangeOfBitstring-named symbol (BACNET_NOTIFICATION_PARAMETERS_CHANGE_OF_BITSTRING_NAME, copy-pasted from the sibling class) — renamed to BACNET_NOTIFICATION_PARAMETERS_CHANGE_OF_STATE_NAME; the string value itself (“ChangeOfState”) is unchanged.
    • Also fixed CR-F20-010: 4 copy-pasted invalid-class-name log messages, one per named file — 3 in each Encode() (BACnetNotificationParametersSignedOutOfRange, UnsignedOutOfRange, ChangeOfLifeSafety all logged “Invalid BACnetNotificationParametersOutOfRange object”) corrected to name its own class, plus the 4th at BACnetNotificationParametersCommandFailure.cpp:73 — its constructor, not Encode() — which logged “…for BACnetNotificationParametersChangeOfValue” (copy-pasted from a different sibling file); corrected to name BACnetNotificationParametersCommandFailure.
    • Also fixed CR-F20-011: stale “Context 0” comment on the context-3 ErrorLimit encode in BACnetNotificationParametersFloatingLimit::Encode — corrected to “Context 3”, matching the Decode side’s own correct labeling.
    • CR-F20-007/008/009 (new dedicated test files) and CR-F20-012 (informational) are deferred to a future batch.
  • rb a-20260908-69 (#909, SECURITY): BACnetStack_SetServiceEnabled(deviceInstance, MAX_PROTOCOL_SERVICES_SUPPORTED, true) returned true and wrote std::vector<bool>::operator[] one past the last valid services-supported index (an off-by-one in both BACnetInterface::SetServiceEnabled’s service > MAX guard and BACnetDBDevice::SetServiceEnabled’s size() < service guard, both false exactly at service == MAX). Both guards now reject service >= MAX/service >= size(); the DB-layer rejection is now logged instead of silent.
  • rb a-20260908-69 (#942): BACnetChoiceSpanLength returned a primitive tag’s header + declared value length with no check that it fits inside the caller’s bufferLength — a truncated buffer could produce a span a caller trusts to read/copy/replay past its real buffer. Both the top-level primitive case and the constructed-value skip branch now require the declared length to fit inside what remains of bufferLength, returning 0 (malformed) otherwise.
  • rb a-20260908-71 (#906): BACnetDBDevice::AddObject had no 22-bit instance-range check at all, unlike BACnetInterface/BACnetDatabase’s own AddDevice/AddObject. Seven typed Add*Object paths (AddTrendLogObject etc.) called it directly, reaching neither checked layer, so an out-of-range instance silently truncated instead of being rejected. Moved the check down into AddObject so every path inherits it; also extended to AddChannelObject/AddGroupObject/AddScheduleObject, whose own side-table inserts had the identical gap.
  • rb a-20260908-71 (#913): BACnetDBDevice::IsConfirmedServiceSupported had no case for confirmedAuditNotification/auditLogQuery despite both having request-factory cases and declared supported-service bits, unconditionally rejecting them regardless of configuration. Added both cases under the same test-tool guard as the existing authRequest case.
  • rb a-20260908-72 (#912): SetConfirmedRequestAndTakeControl dereferenced its outer BACnetConfirmedServiceRequest** with no null check, crashing before the inner pointee’s own (already-correct) null guard ever ran. Fixed by rejecting a NULL outer pointer up front.
  • rb a-20260908-72 (#921): BACnetUnconfirmedRequestPDU::SetServiceRequestAndTakeControl dereferenced its parameter with no null check (the confirmed-request sibling already had this guard under CR-156). Separately, operator= dereferenced a default-constructed/Reset() rhs’s absent service request on ordinary assignment — #396’s own sweep had deliberately left this in place as “a separate defect with a separate fix”; this is that fix.
  • rb a-20260908-72 (#923): the confirmed-request retry loop decremented the retry counter even when the local send itself failed (CallbackSendMessage returned 0), exhausting configured retries on nothing but local socket/resource errors with no retry frame ever reaching the peer. Fixed by only decrementing the retry budget on a successful send.
  • rb a-20260908-73 (#920): maxSegmentsTransmittable == 0 meant “unspecified, no bound applied” in BACnetReadPropertyMultipleProcessor.cpp but “zero capacity, always overflow” in the segmented-response state machine — the same configuration value produced contradictory behavior depending on which response-generation path handled a request. The state machine now skips its own comparison when the value is 0, matching the interpretation BACnetReadPropertyMultipleProcessor already used.
  • rb a-20260908-73 (#928): BACnetComplexACKPDU::SetServiceAck/SetServiceAckAndTakeControl (and SetComplexAck, which calls the former) never cleared a previously-set raw/segmented memory buffer before installing a new typed service ACK — Encode() gives that buffer unconditional precedence, so a reused object would silently transmit the old buffer’s bytes instead of the newly-supplied ACK. Both setters now reset the buffer first, mirroring the existing CR-149 fix for the converse direction in the same file.
  • rb a-20260908-74 (#979): BACnetStack_AddRouterRoute returned true even when the supplied next-hop address exceeded BACNET_NPDU_MAX_DLEN — the over-long address was silently discarded and the route installed with no next hop (falling back to egress broadcast), with no signal to the caller that their explicit configuration was ignored. BACnetPhysicalRouter::LearnRoute now rejects an over-long next-hop address before installing a degraded route.
  • rb a-20260908-75 (#951, partial): projects/linux/Release/x64/makefile set CC := gcc, the lone outlier among the four Linux makefiles — Debug/x64, Release/x86, and Debug/x86 all already use g++. gcc’s link driver does not pull in the C++ runtime the way g++‘s does, so the resulting shared library could link with an incomplete C++ runtime reference set. Now matches the other three makefiles’ own established convention. The issue’s other two action items (ARM cross-toolchain target triples, a CI verification check) remain open.
  • rb a-20260908-76 (#1329, partial): six numeric-threshold event algorithms (BACnetSignedOutOfRangeEventAlgorithm, BACnetUnsignedOutOfRangeEventAlgorithm, BACnetUnsignedRangeEventAlgorithm, BACnetDoubleOutOfRangeEventAlgorithm, BACnetFloatingLimitEventAlgorithm, BACnetOufOfRangeEventAlgorithm) only treated a host callback’s decline as a real failure if the callback also set a non-success error code — a callback that legitimately declines a property write without touching the error code (a documented pattern) was silently ACKed anyway, at 11 call sites across the 6 files. Fixed to match #1041’s already-correct precedent (BACnetDBDevice.cpp’s maxManager/maxInfoFrames cases): the callback’s own false return alone now rejects the write. The same bug shape remains in ~11 other files (#1329’s own finding); left open for future batches.
  • rb a-20260908-77 (#1329, partial, continues #1560): eight ChangeOf*-family event algorithms (BACnetChangeOfBitstringEventAlgorithm, BACnetChangeOfCharacterstringEventAlgorithm, BACnetChangeOfDiscreteValueEventAlgorithm, BACnetChangeOfLifeSafetyEventAlgorithm, BACnetChangeOfStateEventAlgorithm, BACnetChangeOfStatusFlagsEventAlgorithm, BACnetChangeOfTimerEventAlgorithm, BACnetChangeOfValueEventAlgorithm) had the same errorCode != success callback-decline bypass as #1560’s six-file subset, at one timeDelay/timeDelayNormal call site each. Fixed the same way: the callback’s own false return alone now rejects the write. #1329’s remaining files (BACnetAccessEventEventAlgorithm, BACnetCommandFailureEventAlgorithm, BACnetOutOfRangeFaultAlgorithm) left open for a future batch.
  • rb a-20260908-78 (#1329, closes issue): the last 3 files of #1329’s original 17-file finding. BACnetAccessEventEventAlgorithm.cpp and BACnetCommandFailureEventAlgorithm.cpp had the same errorCode != success callback-decline bypass fixed in #1560/#1563, at one timeDelay/timeDelayNormal call site each. BACnetOutOfRangeFaultAlgorithm.cpp had the identical bug at 4 independent call sites, one per monitored-value datatype (Real, Double, SignedInteger, UnsignedInteger) — each branch has its own preamble check scoped to only its own callback type, so no dual-callback preamble trap applied. All 6 sites fixed the same way: the callback’s own false return alone now rejects the write. #1329 is now fully resolved.
  • rb a-20260908-79 (#919, closes issue): ci_scripts/footprint.sh overwrote source/CIBuildSettings.h once per measurement preset and “restored” it via git checkout -q -- source/CIBuildSettings.h, which resets to HEAD/the index, not to whatever bytes were in the working tree before the script ran — silently discarding a developer’s uncommitted local edits to that file, with no cleanup at all on a non-zero exit or an interrupt (set -u only, no trap). Fixed by capturing the file’s exact pre-run bytes to a private temp file and restoring them via a trap ... EXIT handler that fires on success, failure, and signals alike. Verified in isolation: a simulated local edit survives a full run and an interrupted (exit 1) run alike, where the old git checkout-based restore would have discarded it back to the committed content.
  • rb a-20260908-79 (#918): re-verified at the tip — both halves of this finding (the missing non-zero validation in SetSegmentationEnabled, and the DLL header documenting zero as accepted) were already fixed by a prior batch (#1015, commit d31791d5, predates this resolver session). No code change needed; closed with evidence.
  • rb a-20260908-80 (#925, closes issue): BACnetDataLink::SendOutgoingPacket and BACnetDataLinkIPv4::SendOutgoingPacket (both its DBTN-broadcast and unicast branches) treated any positive CallbackSendMessage return as a fully-sent frame, checking only sentBytes == 0. A host callback with partial-write semantics (returning fewer bytes than the encoded frame length) was reported as a successful send and, for the two non-broadcast sites, went on to register the message for confirmed-request retry tracking - silently corrupting the wire protocol with no error surfaced. Fixed by requiring sentBytes == outBufferLength for a successful handoff, rejecting short AND over-length reported counts alike. Two new tests (tests/TestBACnetDataLinkIpv4.cpp) drive BACnetDataLinkIPv4::ProcessOutgoingPacket (which calls the fixed SendOutgoingPacket) directly against a real BACnetDataLinkIPv4 instance, with the existing ProcessOutgoingPacketBVLLResultSuccess test as their exact-length control; the identical, mechanically-applied fix at the other two sites (the generic BACnetDataLink.cpp path, and the DBTN-broadcast branch in BACnetDataLinkIPv4.cpp) is not separately test-covered in this batch - disclosed, not silently dropped. While authoring the first draft of this test, the official build caught that the originally-chosen vehicle (BACnetInterface::SendReadBroadcastDistributionTable) does not actually route through the fixed code at all - it calls CallbackSendMessage directly with its own, separate, unfixed == 0 check. Filed as #1567 (5 similar BACnetInterface.cpp BVLL/BBMD-management functions with the identical bypass), not fixed here.
  • rb a-20260908-81 (#1568, closes issue): 7 CallbackSendMessage call sites in BACnetBBMD.cpp’s BDT/FDT forwarding paths (OriginalBroadcastNPDU’s BDT-forward and FDT-forward loops, ForwardedNPDU’s directed-subnet-send and FDT-forward loop, and DistributeBroadcastToNetwork’s unregistered-sender NAK-send, BDT-forward loop, and FDT-forward loop) had the identical sentBytes == 0 weak-check bypass fixed for the generic path in #925. Fixed the same way: require sentBytes == outBufferLength. Two new tests (tests/TestBACnetDataLinkIpv4.cpp) cover the OriginalBroadcastNPDU BDT-forward site directly - a short-write callback and its exact-length control - reusing the fixture’s existing ShortWriteSendMessage/OverLengthSendMessage mocks (already built for #925) and the same minimal BDT-setup pattern as the pre-existing ProcessOutgoingMessageBBMD_SameSubnetLoopSuppression_ReturnsTrueNotFalse test; a review-fix round after the first draft’s claim that this setup was too costly to justify turned out to be wrong (the reviewer found all the needed pieces were already directly reusable). The other 6 sites are not separately test-covered - the identical, mechanically-applied fix - and continue to pass the existing BTL-style BBMD test suite (tests/BACnetBBMDTests.cpp’s 14_2_2_*/14_7_* families) unchanged, which exercises them at real, full-length sends as a regression net (proving the tightened check doesn’t disturb the happy path, though not itself exercising the new rejection branch for those 6).
  • rb a-20260908-82 (#1567, closes issue): 5 CallbackSendMessage call sites in BACnetInterface.cpp (SendRegisterForeignDevice, SendReadBroadcastDistributionTable, SendWriteBroadcastDistributionTable, SendReadForeignDeviceTable, SendDeleteForeignDeviceTableEntry) had the identical weak-check bypass fixed for other files in #925/#1568, checked inline (== 0 directly in the if condition rather than a captured local) - a short write from a host callback was silently reported as a successful send. Fixed by capturing the return value and requiring it equal the encoded messageLength exactly, rejecting short and over-length reported counts alike. All 5 functions gained a discriminating short-write test and an over-length control (10 new tests total) - this is the exact function (SendReadBroadcastDistributionTable) whose independent, unfixed check caused batch a-20260908-80’s own test-vehicle mistake while fixing #925; it is now the fix target and its own tests genuinely exercise the fixed code.
  • rb a-20260908-83 (#1569, closes issue): 5 CallbackSendMessage call sites in BACnet/SC files (BACnetDataLinkSC_PacketProcessing.cpp’s SendOutgoingPacket; BACnetSCHubFunctionManager.cpp’s hub-function control-message and forwarded-packet sends; BACnetSCHubConnector_Outgoing.cpp’s outgoing-packet and ConnectRequest sends) had the same weak-check bypass fixed for other data links this session (#925/#1568/#1567). Confirmed the shared FPCallbackSendMessage interface has no offset/resume parameter on any data link, so there is no legitimate partial-write-then-retry contract for BACnet/SC’s WebSocket transport to preserve - the identical fix applies: require the callback’s reported byte count equal the encoded frame length exactly. Three new tests (tests/BACnetSC_Tests/TestBACnetDataLinkSC_PacketProcessing.cpp) drive BACnetDataLinkSC::SendOutgoingPacket directly against a real instance (a short-write test, an over-length control, and an exact-length control) - the other 4 sites are not separately test-covered, the identical, mechanically-applied fix, verified instead via the existing extensive BACnet/SC test suite continuing to pass unchanged. This closes the last of the 4 issues (#925/#1568/#1567/#1569) already filed from this session’s discovery - but a fresh repo-wide sweep by a reviewer of this batch found one more, previously-missed instance in source/BACnetStackSentConfirmedRequests.cpp’s retry-resend path, filed separately as #1573 (not fixed here - it interacts with the still-open #927’s own retry/timeout contract question).
  • rb a-20260908-84 (#1573, closes issue): BACnetStackSentConfirmedRequests::Loop’s retry-resend path computed sendSucceeded via CallbackSendMessage(...) != 0 - the same weak-check bypass fixed elsewhere this session (#925/#1568/#1567/#1569). A short (positive but incomplete) write from the host callback was silently reported as a successful resend and, per the existing #923 retry-budget protection, would have consumed the configured retry for an attempt that never actually left the host. Fixed by requiring the callback’s reported byte count equal GetMessageLength() exactly. New test LoopShortWriteResendDoesNotConsumeRetryBudget mirrors the existing LoopFailedSendDoesNotConsumeRetryBudget (#923) structure exactly, substituting a short-write mock for the hard-fail one - proving both that a short write is now correctly rejected and that the #923 retry-budget protection extends to it. This is the last of the 21 real instances of this bug shape found across the codebase this session (#925 discovery chain: #925, #1568, #1567, #1569, #1573).
  • rb a-20260908-85 (#1538, closes issue): BACnetAddress::operator= had no this == &rhs self-assignment guard. On self-assignment with an already-populated MAC address, rhs.GetMacAddress() returns a pointer into this->m_macAddress’s own backing storage, read after SetMacAddress had already begun mutating it via the underlying vector’s clear() - formal undefined behavior (harmless in practice on every real STL for a trivial uint8_t element type, dormant per the issue’s own severity assessment - no live call site triggers x = x on a BACnetAddress today). Added the guard, matching the established BACnetDailySchedule/BACnetTimeValue/BACnetHostNPort (#1107) pattern. Unlike the 95 classes the automated check-self-assignment-guards.js gate already catches (which all share a Reset()-then-copy shape), BACnetAddress::operator= copies straight from individual setters with no Reset() call - a different shape the gate’s detection heuristic doesn’t recognize, explaining why it wasn’t already caught mechanically.
  • rb a-20260908-86 (#1242, partial - CR-F11-012/013/014/015 addressed, CR-F11-016 answered without a fix): a low-severity hygiene mini-batch for review family F11 (WriteProperty/WritePropertyMultiple/WriteGroup/priority array).
    • CR-F11-012: BACnetPriorityValue.h’s ASN.1 header comment claimed a literal 135-2024 CHOICE transcription that matches no production in the spec. Corrected to describe what Decode()/Encode() actually implement - an internal generic 2-arm union (constructed-value/datetime) that collapses the spec’s individually-named primitive-type arms into one generic slot. Review-fix: the original comment also asserted a specific ~15-arm primitive-type list and count that could not be verified against the extracted spec text; reworded to a defensible comparison against a sibling CHOICE type (BACnetTimerStateChangeValue, cl. 12.57.20) instead of an unverifiable enumeration.
    • CR-F11-013: BACnetWritePropertyRequest::Decode’s datatype-lookup failure path returned 0 without setting an error reason, unlike every other failure branch in the same function. Set datatypeNotSupported. Review-fix: the original comment claimed this was GetPropertyValueDatatype’s “sole failure case (‘writing to a proprietary property’)” - adversarial review found the helper actually has 4 internal false-return paths, 2 of which (empty/truncated buffer, malformed inner tag) are reachable from this call site via malformed input, not just the proprietary-property case. The code change itself is still a net improvement (previously no error was ever set on this path); the comment now describes datatypeNotSupported as the closest general-purpose reason available given the helper’s undifferentiated bool-only return, rather than claiming a single precise cause. Redesigning the helper to propagate which failure occurred would touch all 4 of its callers - the same reason #1577/#1578 were filed separately (F03/F12) rather than fixed here.
    • CR-F11-014: BACnetWriteStatus had zero test coverage. Added a Constructor_Enumerated_WriteStatus test to TestBACnetPropertyStates.cpp, matching the existing Constructor_Enumerated_LightingInProgress/LightingOperation pattern (including asserting the concrete decoded type via GetName(), the exact discipline that pattern exists to enforce after a prior copy-paste defect, PR #213).
    • CR-F11-015: BACnetListOfWriteAccessSpecifications/BACnetWritePropertyMultipleRequest::Clone() silently dropped list elements (and still reported the clone valid) when a child Clone()/Insert() failed. Fixed both to fail atomically, matching the already-correct BACnetWriteAccessSpecification::Clone precedent. Review-fix: added a Clone_ChildAllocationFails_ReturnsNullNotPartial test to each of TestBACnetListOfWriteAccessSpecifications.cpp/TestBACnetWritePropertyMultipleRequest.cpp (via the shared MemTracker fault-injection harness) - the original commit fixed both memory-ownership paths with no new regression coverage for either.
    • CR-F11-016 (S4, operational observation, no code change): WriteGroup processing cost scaling with (Channel count × Control_Groups size) - answered on the issue itself; flagging-only per its own text, no fix implied without profiling data.
  • rb a-20260908-87 (#1577, #1578): 3 sibling files shared BACnetWritePropertyRequest::Decode’s CR-F11-013 gap (batch a-20260908-86) - a datatype-lookup failure path that returned 0 without setting an error reason on the CErrorContainer, unlike every other failure branch in the same function. Set datatypeNotSupported at all 3 sites, matching the sibling fix exactly (including its corrected rationale: GetPropertyValueDatatype has several internal failure causes, not one, but its bool-only return doesn’t distinguish which fired, so datatypeNotSupported is the closest general-purpose reason available).
    • source/BACnetPropertyValue.cpp (F03)
    • source/BACnetAddListElementRequest.cpp, source/BACnetRemoveListElementRequest.cpp (F12)
  • rb a-20260908-88 (#1239, partial - CR-F08-001/003/004/006/007/008 addressed, CR-F08-009 answered without a fix): a low-severity hygiene mini-batch for review family F08 (MS/TP data link), source/MSTP.c.
    • CR-F08-001: POLL_FOR_MASTER’s SendNextPFM transition fell through into DeclareSoleMaster’s re-evaluated condition (using the PollStation value SendNextPFM’s own body just mutated). Added the missing return;, matching every other terminal branch in the function. Review-fix: the original framing overstated this as a live, reachable bug (“declares sole master without waiting for a reply”) - SendNextPFM’s own body already unconditionally clears ReceivedInvalidFrame and resets the silence timer, so DeclareSoleMaster’s guard is unsatisfiable immediately afterward with or without the fix. The fallthrough is real but currently inert; the return; is kept as defensive consistency against a future change to that reset behavior, not as a fix for an exploitable defect today. Its regression test was corrected to not claim discrimination it doesn’t have (confirmed by review: reverting the fix and rebuilding leaves the test passing).
    • CR-F08-003: 5 copy-pasted “advance PollStation, skip self” blocks didn’t re-clamp PollStation against Nmax_master after the self-skip increment, letting it land one past the ring boundary. Re-clamped at all 5 sites. Confirmed by review: reverting one site and rebuilding makes its new test fail as expected.
    • CR-F08-004: 2 of POLL_FOR_MASTER’s Tusage_timeout comparisons used > instead of >=, contradicting their own inline comments and the other comparisons in the same function. Changed to >=. Confirmed by review: reverting one comparison and rebuilding makes its new test fail as expected.
    • CR-F08-006: send_DataCRC was appended via a raw memcpy of the uint16_t, silently assuming a little-endian host - the sibling Length-field bug (#183) was fixed this way already but this one wasn’t. Applied the same explicit-shift extraction. Honest limitation (per review): the new test passes identically against the pre-fix code on this little-endian CI host, since a uint16_t’s raw memory layout already matches the correct wire order there - it pins the correct byte order but can’t discriminate the two implementations without a big-endian build target. Same limitation as the existing CR-183 test it mirrors.
    • CR-F08-007: stale comment (MSTP_SetMaxMaster’s call site said “Max: 255”; the function clamps to 127). Fixed.
    • CR-F08-008: 4 <= 0 guards on unsigned uint32_t timing settings changed to == 0 for clarity.
    • CR-F08-009 (S4, no fix): Test_Request/Test_Response loopback origination has no entry point - answered on the issue itself, not required unless/until this engine is wired up and the loopback-test facility is wanted.
    • Deferred to a follow-up batch (need spec/architecture verification not done this batch): CR-F08-002 (extended-frame DataLength carve-out), CR-F08-005 (missing spec clause (d) on ReceivedUnwantedFrame), CR-F08-010 (broadcast defense-in-depth check).
  • rb a-20260908-89 (#1239, continued - CR-F08-005/010 addressed): follow-up to a-20260908-88, closing 2 of the 3 items deferred there for needing spec verification.
    • CR-F08-005: MSTP_MasterNodeStateMachine_Idle’s ReceivedUnwantedFrame condition (cl. 9.5.6.2) was missing clause (d) - a Reply To Poll For Master or Reply Postponed frame addressed to this station while it wasn’t actually expecting one (not in WAIT_FOR_REPLY) matched none of the function’s branches and fell through doing nothing, leaving ReceivedValidFrame stuck TRUE. Added the missing clause.
    • CR-F08-010: MSTP_MasterNodeStateMachine_UseToken’s SendCustomFrame branch trusted a queued higher-level frame’s own “expecting reply” bit even for a broadcast (destination == 255) frame, risking WAIT_FOR_REPLY for a reply no single peer is obligated to send. Added the destination == 255 override cl. 9.5.6.3’s SendNoWait condition specifies.
    • CR-F08-002 (extended-frame DataLength carve-out) remains deferred - needs the COBS/cl. 9.7.1 encoding rules worked out, a larger task than a mechanical fix.
  • rb a-20260908-90 (#1237, closes issue): a low-severity hygiene mini-batch for review family F06 (network layer: NPDU, routing, network messages, addresses), all 4 checklist items.
    • CR-F06-005: the finding’s own claim (“6 of 8 sibling router-message classes ship the #373 self-assignment guard but only 1 has a regression test”) turned out to be wrong for 3 of its 5 named files - BACnetIAmRouterToNetwork, BACnetRouterAvailableToNetwork, and BACnetRouterBusyToNetwork already have equivalent tests in tests/TestSelfAssignmentGuards.cpp (found only after writing duplicates and noticing they passed identically during this batch’s own build). Added SelfAssignment_IsSafeAndPreservesState only to the 2 classes genuinely missing coverage: BACnetInitializeRoutingTable and BACnetInitializeRoutingTableAck (the latter had no dedicated test file, added alongside its base class’s own test); left an explanatory comment in the other 3 files pointing at their real coverage.
    • CR-F06-006: BACnetWhatIsNetworkNumber::Decode/Encode validated nothing and never touched the error container. Decode now rejects buffer==NULL and bufferLength != 0 (this message carries no parameters per cl. 6.4.19), matching the sibling BACnetNetworkNumberIs::Decode’s own checks; Encode gets a symmetric buffer==NULL rejection. Review-fix: the fix shipped with no test, missed against the issue’s own plan (S3 code changes get a discriminating test with a control arm); new tests/TestBACnetWhatIsNetworkNumber.cpp (no prior dedicated test file existed) covers both rejection paths plus a control per function proving a genuinely valid call still succeeds with no error set.
    • CR-F06-007 (S4, no fix): open issue #495’s title is stale for BACnetEstablishConnectionToNetwork/BACnetDisconnectConnectionToNetwork - answered on this issue, suggesting the owner update #495’s title; no code change.
    • CR-F06-009: BACnetDBRouter::SetNetwork accepted the reserved local (0) and broadcast (65535) network numbers with no validation, unlike its siblings BACnetRoutingTableEntry::SetConnectedDnet/BACnetVirtualRouter::AddVirtualNetwork. Now bool-returning and rejects both reserved values; the default constructor sets m_network directly instead of through the now-validating setter. Confirmed via repo-wide grep that BACnetDBRouter has no call sites anywhere else, so the signature change is source-compatible. New tests/TestBACnetDBRouter.cpp (no prior dedicated test file existed).
  • rb a-20260908-91 (#1236, partial - CR-F05-002/011/015/016/017/019/020/022/023/026/027/033/034 addressed): a low-severity hygiene mini-batch for review family F05 (APDU layer), working a 13-item mechanical/low-risk subset of its 21-item checklist (review-fix: the original commit and board INTENT both undercounted this as “19-item” - the issue’s own checklist actually has 21 entries, and 13+8 deferred does fully reconcile against that correct total).
    • CR-F05-002: %u mistyped as &u in 3 log calls in BACnetConfirmedRequestPDU.cpp, losing the service-choice value from the diagnostic.
    • CR-F05-011: BACnetRejectPDU::Encode’s error log copy-pasted BACnetAbortPDU’s “Invalid abort pdu object” message.
    • CR-F05-015: confirmedAuditNotification/auditLogQuery were missing from BACnetConfirmedServiceChoice’s enum-to-string map.
    • CR-F05-016: BACnetConfirmedServiceChoice.h’s ASN.1 spec comment was never updated for its three most recent additions (32/33/34).
    • CR-F05-017: mismatched #endif comment on the subscribeCovPropertyMultiple case (said STACK_OPTION_SUBSCRIBE_COV_PROPERTY, missing _MULTIPLE).
    • CR-F05-019: unconfirmedAuditNotification (choice 12) was missing from BACnetUnconfirmedServiceChoice’s name table.
    • CR-F05-020: DecodeStillAcceptsTheHighestDefinedServiceChoice never checked Decode’s return value or IsValid(), only GetServiceChoice() - renamed/rescoped to state exactly what it verifies.
    • CR-F05-022: stale comment called the service-choice octet the “error choice” in BACnetUnconfirmedRequestPDU::Encode.
    • CR-F05-023: BACnetAbortReason.h/BACnetRejectReason.h included but unused in BACnetUnconfirmedRequestPDU.cpp.
    • CR-F05-026: BACnetStackRespondingStateMachineCurrentState::SetServices() dereferenced networkParameters with no NULL check, unlike its sibling mutators. New tests/TestBACnetStackRespondingStateMachineCurrentState.cpp (no prior dedicated test file existed).
    • CR-F05-027: SetSegmentationParameters’s initialSequenceNumber parameter (and the backing member) was uint32_t while every other accessor for the same field is uint8_t. Narrowed both.
    • CR-F05-033: TestBACnetError.cpp’s EncodeTest case 2 built the expected message but never compared it before the buffers were wiped - added the missing assertions; also fixed the case’s own comment (0x12 is 18 decimal, not 12).
    • CR-F05-034: BACnetErrorBase::IsEqual never compared m_errorChoice, so two objects with the same underlying error but a different choice were reported equal. New tests/TestBACnetErrorBase.cpp (no prior dedicated test file existed).
    • Deferred to a follow-up batch (each needs more investigation/judgment than a mechanical fix): CR-F05-006 (segmented reception unimplemented), CR-F05-012 (dead-code removal decision), CR-F05-018 (constructor failure-signaling design choice), CR-F05-021 (Decode dedup refactor), CR-F05-025 (Reset() cross-file invariant), CR-F05-028 (S4 operational note), CR-F05-040 (SetApduMemoryBuffer precondition), CR-F05-043 (buffer-size-for-SC reachability check).
  • rb a-20260908-92 (#1245, closes issue): a low-severity hygiene mini-batch for review family F14 (file access services), all 5 checklist items.
    • CR-F14-001: BACnetAtomicReadFileRequest/BACnetAtomicReadFileACK::operator= - including the #373 self-assignment guard - was untested (only CopyConstructor tests existed). Added an AssignmentOperator test plus a self-assignment test per class.
    • CR-F14-002: 5 sibling classes’ self-assignment guard was likewise untested. BACnetAtomicWriteFileRequest/BACnetAtomicWriteFileACK already had general AssignmentOperator coverage, so only needed a self-assignment test added; BACnetFileRecordAccessData had no operator= coverage at all, so got both; BACnetFileStreamAccessData/BACnetFileAccessMethodData have no dedicated test file but are already tested via tests/TestBACnetAtomicReadFileACK.cpp’s own tests for them, so a self-assignment test was added there rather than creating new files.
    • CR-F14-003: BACnetAtomicWriteFileRequest’s wire decode/encode was tested for stream access only. Added a record-access Decode/Encode round-trip test, built via the constructor+Insert API rather than hand-crafted wire bytes.
    • CR-F14-004: BACnetFileAccessMethodData/BACnetFileAccessMethodParameters::Encode each checked IsValid() twice in a row with no code path that could change validity in between. Removed the redundant second check in both.
    • CR-F14-005: BACnetFileAccessMethodData.h’s closing #endif comment named a different, non-existent header (__BACnetFileAccessData_h__ instead of the file’s own guard, __BACnetFileAccessMethodData_h__). Fixed.
  • rb a-20260908-93 (#1251, continued from a-20260908-67 - CR-F21-009/010 addressed, partial): continuing family F21’s hygiene mini-batch, closing 2 of the 3 items left after an earlier batch already closed CR-F21-005/008.
    • CR-F21-009: BACnetStackEventEnrollmentObject::SetFaultParametersProperty’s two explicit reject paths (“not a BACnetFaultParameter”, “no active CHOICE alternative”) had no unit test. Added one test per path, driven through the real WriteProperty dispatch, asserting invalidDataType and that no fault algorithm gets attached by the rejected write.
    • CR-F21-010: no dedicated test file existed for BACnetEventState, BACnetEventType, BACnetNotifyType (ENUM wrappers), or BACnetEventTransitionBits (bitstring wrapper). Added trivial construction/Clone/round-trip-encode coverage per the finding’s own “low priority… if convenient” framing, mirroring the established TestBACnetAuditLevel.cpp pattern.
    • CR-F21-007 remains deferred: it’s a diagnostic-warning design judgment call (“consider surfacing a warning”), not a mechanical fix.
  • rb a-20260908-94 (#1253, CR-F23 audit/auth family, all 4 items addressed): hygiene mini-batch for family F23.
    • CR-F23-013: the finding’s own file list was partly stale - tests/TestBACnetAuthRequestACK.cpp and tests/TestBACnetAuthenticationFactor.cpp already had substantive coverage. Added the two genuinely-missing files: tests/TestBACnetAuthenticationDecision.cpp, tests/TestBACnetAuthenticationStatus.cpp (trivial construction/Clone/GetName/round-trip-encode coverage, mirroring TestBACnetAuditLevel.cpp).
    • CR-F23-014: answered without a code change. BACnetAuthRequestACK::GetDatatype() already returns a unique, non-Invalid value (BACnetStackDatatypes::AuthRequestACK) - the finding’s claim that both BACnetAuthRequest and BACnetAuthRequestACK return Invalid is stale for the ACK class. BACnetAuthRequest itself is deliberately left returning Invalid, per the standing comment at source/BACnetStackDatatypes.h:484 and the regression test it names (TestBACnetRawConfirmedServiceRequest.cpp::IsEqual_RejectsADifferentClassThatAlsoReturnsInvalidDatatype), which depends on that exact sharing to exercise BACnetRawConfirmedServiceRequest’s typeid-based collision guard. No further action needed.
    • CR-F23-016: refutation confirmed by direct inspection - BACnetAuthenticationClient, BACnetAuthenticationFactor, and BACnetAuthenticationEvent all return unique, non-Invalid GetDatatype() values at this baseline. #949’s class list should drop these three.
    • CR-F23-017: refutation confirmed for the entire Authorization class family - BACnetAuthorizationConstraint, BACnetAuthorizationScope, BACnetAuthorizationScopeDescription, BACnetAuthorizationEvent, BACnetAuthorizationStatus, BACnetAuthorizationPolicy, and BACnetAccessToken all return unique, non-Invalid values. No action needed.
  • rb a-20260909-100 (#1246, CR-F16 alarm and event services family, all 8 items addressed): hygiene mini-batch closing family F16.
    • CR-F16-012: BACnetAcknowledgeAlarmRequest/BACnetAcknowledgeAlarmInfo::SetTimestamp now return the value’s own validity instead of unconditionally true. For BACnetAcknowledgeAlarmRequest, this activates an existing, already-correct caller check (its bulk setter already handles a false return - it was just previously unreachable).
    • CR-F16-013: added processor-level match/non-match tests for BACnetGetEnrollmentSummaryProcessor.cpp’s Priority Filter and Notification Class Filter. The finding’s own claim that Event State Filter also lacked coverage was stale - 3 existing tests already cover it (added via #1156, after this review’s baseline).
    • CR-F16-014: BACnetEventNotificationSubscription::SetRecipient now returns the recipient’s own validity instead of unconditionally true, matching the other three mutators. Activates an existing, already-correct caller check in CASBACnetStackTestToolDLL.cpp’s BuildEventNotificationSubscriptionList. The two in-class callers (BACnetEventNotificationSubscription.cpp’s own compact-string parser) don’t check the direct return value, but the same function unconditionally checks IsValid() right after, which SetRecipient also updates - an invalid recipient is still caught, just one level up.
    • CR-F16-015: BACnetConfirmedEventNotificationRequest/BACnetUnconfirmedEventNotificationRequest::GetMinimumMemoryCount() now forward to BACnetEventNotificationRequest::GetMinimumMemoryCount() instead of duplicating it identically.
    • CR-F16-016: corrected 3 files citing spec clause 13.8 (ConfirmedEventNotification, a different service) instead of 13.13 (LifeSafetyOperation), including two sub-clause references, verified against the extracted spec text.
    • CR-F16-017: added the missing VALUE_OUT_OF_RANGE row to BACnetLifeSafetyOperationProcessor.cpp’s in-code error table comment - already correctly handled and cited elsewhere in the same function, just missing from this summary table.
    • CR-F16-018: added tests/TestBACnetLifeSafetyOperation.cpp (no dedicated test file existed for this thin ENUM wrapper), including per-value name-string assertions via GetEnumValueString() per the finding’s own recommendation.
    • CR-F16-019: added copy-constructor, self-assignment (#373 guard), Reset(), and 9-arg Set() tests to tests/TestBACnetStackEventNotificationParameters.cpp.
  • rb a-20260909-101 (#1244, CR-F13 device/network management family, first 8 of 16 items addressed, partial): hygiene mini-batch continuing family F13. CR-F13-021 through CR-F13-028 remain deferred - too large for one batch.
    • CR-F13-013: BACnetIAmProcessor/BACnetIHaveProcessor::Process now check IsValid() on the decoded request, matching every sibling processor. Neither processor had a dedicated test file at all - added both.
    • CR-F13-014: removed 3 dead private helpers (GetMaxAPDULength/GetVendorIdentifier/GetSegmentationSupported) from BACnetWhoIsProcessor, never called from Process() or anywhere else.
    • CR-F13-015: added tests pinning the “only one limit present” rejection for Who-Is/Who-Has Decode() (already correctly handled in code, just untested), mirroring the existing Decode_ReversedRange_RejectsAsInvalid pattern with a both-present control arm.
    • CR-F13-016: BACnetDeviceAddressProxyTableEntry::SetComplete() now checks all three owned members’ validity instead of setting valid unconditionally, matching the sibling fix under #859.
    • CR-F13-017: added tests/TestBACnetEnableDisable.cpp and tests/TestBACnetRestartReason.cpp (no dedicated test file existed for either thin ENUM wrapper).
    • CR-F13-018: BACnetConfirmedPrivateTransferRequest/BACnetUnconfirmedPrivateTransferRequest::Reset() now chain BACnetObjectBase::Reset() (which also clears m_context), matching the ACK/Error siblings.
    • CR-F13-019: corrected BACnetPrivateTransferProcessor.h’s doc comment, which described a callback/hook mechanism that doesn’t exist in the implementation - verified against the .cpp, which has no hook at all (ProcessUnconfirmed is a no-op, ProcessConfirmed always denies).
    • CR-F13-020: added a direct unit test for BACnetPrivateTransferProcessor::ProcessConfirmed/ProcessUnconfirmed, pinning today’s documented always-deny/no-op behavior and NULL-parameter guards.
  • rb a-20260909-102 (#1244, CR-F13 device/network management family, remaining 8 of 16 items, completes the family): second half of family F13, continuing directly from a-20260909-101 (#1605).
    • CR-F13-021: added tests/TestBACnetDeviceStatus.cpp - no dedicated test file existed for this thin BACnetPrimitiveENUM wrapper.
    • CR-F13-022/023: removed a dead duplicate BACnetMessagePriorityEnum enum from BACnetTextMessage.h (the real member is m_messagePriority, a BACnetMessagePriority, never this local enum) and a duplicate m_messagePriority comparison in BACnetTextMessage::IsEqual (compared twice, m_message already covered separately). Both pure no-op cleanup - behavior unchanged, no new test needed.
    • CR-F13-024: un-gated tests/TestBACnetUTCTimeSynchronizationProcessor.cpp’s entire fixture class and its 5 tests, which were dead behind #ifdef ISSUE_421_UNIT_TEST_TIME_ZONE - a macro never defined anywhere in the build. This both proves CR-F13-003’s already-landed Daylight_Savings_Status fix (#1138) end-to-end at the Process() layer (rewrote ProcessWhenDaylightSavingsStatusIsTrue into a DST-off/DST-on control pair at identical date/time/offset) and surfaced a second, independent defect: the pre-existing Process test’s own hardcoded expected hour (13) was wrong - the correct value, per the same arithmetic the already-passing CalculateTimeUsingUTCNormal test uses for the same offset, is 12. Never caught because the whole fixture had never compiled or run. Fixed the assertion; the production CalculateTimeUsingUTC logic was already correct.
    • CR-F13-025: BACnetUTCTimeSynchronizationRequest::IsEqual now forwards to BACnetTimeSynchronizationRequest::IsEqual instead of duplicating it - m_datetime is inherited, not redeclared here. Added IsEqualComparesByValue since no IsEqual test previously existed for this class.
    • CR-F13-026: added a self-assignment guard to BACnetReinitializeDeviceRequest::operator=, matching the sibling BACnetDeviceCommunicationControlRequest::operator= and the codebase-wide (#373/#396) convention. Both members are copied member-wise with no intervening Reset(), so x = x was not actually unsafe here - added for consistency, not to fix an observed defect, so no new test.
    • CR-F13-027: removed stale “Context N” comments from BACnetAddressBinding::Decode/Encode - the type is a plain SEQUENCE of two application-tagged fields (per the struct comment already in the header), not context-tagged at all. Comment-only.
    • CR-F13-028: recorded, no fix. BACnetTimeSynchronizationProcessor/BACnetUTCTimeSynchronizationProcessor::Process fan out per-instance work across TrendLog/EventLog/TrendLogMultiple objects on every time-sync request with no debounce or rate limiting - confirmed the fan-out is real (verified against the Process() bodies) but whether a minimum-interval guard belongs in this processor, or is intentionally left to a caller/transport layer, is an architectural call for the owner.
  • rb a-20260909-103 (#1238, CR-F07 BACnet/IP family, first 8 of 15 items, partial): hygiene mini-batch opening family F07. CR-F07-013,014,016,017,018,021,022 remain deferred - too large for one batch.
    • CR-F07-002: already fixed by an earlier batch (#1309) - BACnetPacket::GetAttributes already reports a networkType of “SC” for BACnet/SC packets. Recorded, no action needed.
    • CR-F07-004: added 4 direct tests to tests/TestBACnetForwardedNPDU.cpp’s ForwardedNPDUNetworkLayerTests fixture covering BACnetDataLinkIPv4::ProcessBVLLForwardedNPDU’s J.2.5 originating-address substitution logic - the all-zero/limited-broadcast/zero-port rejection branches and the multicast pass-through case, none of which had a direct test before. Replaced the stale disabled test in tests/TestBACnetDataLinkIpv4.cpp (a defunct 3-argument signature under #629) with a comment pointing to this coverage instead of resurrecting a crash-prone duplicate. Reviewer-verified via an empirical revert-red build (temporarily disabling the guard) that all 4 tests genuinely discriminate.
    • CR-F07-005/006: BACnetDataLink::HandleProcessingErrors’s CErrorContainer::error case and BACnetDataLinkLayer::SetDeviceType’s DeviceType_Device case both relied on falling through into default (accidentally safe today since default is itself a no-op) - made explicit with a return false;/break;. The RETURN VALUE is unchanged for every input (reviewer-confirmed); the HandleProcessingErrors fallthrough path’s diagnostic log TEXT did change (a more specific message replaces a generic one) - no caller or test depends on the exact log string. No new test.
    • CR-F07-008: BACnetDeleteForeignDeviceTableEntry::GetIpAddress now returns true instead of the literal 1 (behavior-identical).
    • CR-F07-010: BACnetBVLL::GetElements/GetAttributes now guard m_bvllBase == NULL before dereferencing it, matching every sibling accessor in the file - extended the existing MethodsOnDefaultConstructedObjectDoNotCrash test (CR-057’s own precedent for this exact defect shape on sibling methods). Reviewer found this test (including CR-057’s own original assertions) lived inside an #ifndef STACK_OPTION_BVLL_BBMD block and never ran in this repo’s actual CI build config (STACK_OPTION_BVLL_BBMD is defined there) - moved the whole test to unconditional scope so it actually executes.
    • CR-F07-011: documented (comment-only) that BACnetBVLCResult::Decode’s Result Code passthrough with no bounds check against Annex J’s seven-value enumeration is deliberate.
    • CR-F07-012: upgraded from “recorded, no action needed” to fixed, after adversarial review disproved the original disposition. The original claim (integrator-only, via the public AddBDTEntry API, since Write-BDT’s wire service is unimplemented) missed a SEPARATE, genuinely wire-reachable path: cl. 12.56.38 requires a BBMD’s Broadcast_Distribution_Table Network Port property to be writable, and BACnetBusinessLogic::SetGeneratedBBMDPropertyValueToDataLink (reached from the ordinary WriteProperty/WritePropertyMultiple processors, gated only on IsBBMD()) replaces the whole table from a single incoming list with no bound - a real, remotely-triggerable unbounded-growth vector on any BBMD-configured device. Added STACK_OPTION_MAX_BROADCAST_DISTRIBUTION_TABLE_ENTRIES (mirroring #626’s existing FDT cap), set to 512 rather than the FDT’s 128 so the pre-existing #885 wrap-boundary regression test (which legitimately populates 300 entries) keeps passing. New cap+control test pair in tests/BACnetBBMDTests.cpp, mirroring #626’s own AddForeignDevice cap tests.
  • rb a-20260909-104 (#1238, CR-F07 BACnet/IP family, remaining 7 of 15 items, completes the family): second half of family F07, continuing directly from a-20260909-103 (#1611).
    • CR-F07-013: replaced the stale “ToDo: Add logic to write broadcast distribution table” comment in source/BACnetDataLinkIPv4.cpp::ProcessBVLLWriteBroadcastDistributionTable - the service is deliberately unimplemented (always NAK’d), not pending work. Documents the Annex J.4.4.2 vs. 135.1-2023 cl.12.3.3.x conformance conflict as a QUESTION FOR OWNER rather than settling it unilaterally.
    • CR-F07-014: corrected BACnetBBMD::ResetBroadcastDistributionTable()’s doc comment, which falsely claimed it disables BBMD mode - that only happens at the BACnetInterface::ResetBroadcastDistributionTable() caller (already documented correctly there), which calls this and separately calls SetDeviceType. This class’s own contract, true for both its actual callers (the other being SetGeneratedBBMDPropertyValueToDataLink’s mid-flow clear-then-rebuild), is just “clear the table.”
    • CR-F07-016: NOREF(callbacks) + documented why BACnetStackNetworkPortIpv4::SetNetworkPortProperty carries a parameter the base virtual signature doesn’t - it’s an overload (a using declaration brings the base 5-arg version into scope alongside this 6-arg one), not an override.
    • CR-F07-017: 6 new tests extending the existing BACnetStackNetworkPortIpv4Test group in tests/TestBACnetStackNetworkPortBase.cpp - SetNetworkPortProperty’s bacnetIpMode valid/out-of-range/wrong-datatype cases, its default-delegation path to the base class, and SetBacnetIpMode’s own valid/rejection paths directly. None had coverage before. Reviewer independently traced every caller of SetNetworkPortProperty repo-wide and confirmed this whole class-hierarchy method (base virtual + both IPv4/SC overrides) is currently dead code from BACnetDBDevice::SetNetworkPortProperty’s own perspective - that function has its own independent switch and never delegates into these class methods. The new tests are still worth having (real, currently-unreachable-from-the-wire code, exercised directly), just not proof of live wire behavior.
    • CR-F07-018: recorded, no fix (explicitly out of scope for these files per the finding’s own recommendation) - independently confirmed changesPending is hardcoded false in BACnetStackNetworkPortBase::GetNetworkPortProperty. Flagged for whoever next reviews BACnetDBDevice.cpp/BACnetDBPropertyProfile.cpp to confirm the callback escape hatch is adequately documented, or whether the stack should track pending-state itself by default.
    • CR-F07-021: BACnetFDTEntry::Decode now rejects timeToLive/remainingTimeToLive values above UINT16_MAX instead of letting GetTimeToLive()/GetRemainingTimeToLive() silently truncate them via a narrowing cast. Correction to this batch’s own first-draft claim: this IS wire-reachable, not just a defensive gap - bbmdForeignDeviceTable is a non-writable property, but BACnetWritePropertyRequest::Decode decodes the incoming value via DecodeByDatatype/ObjectFactory as part of parsing the request itself, before any writability check runs in the processor layer. An untrusted peer’s WriteProperty(networkPort, bbmdForeignDeviceTable, …) genuinely reaches BACnetFDTEntry::Decode() with attacker-controlled bytes, even though the write itself will subsequently be rejected as non-writable. Reviewer-caught; independently re-verified. 2 new hand-encoded wire-byte tests.
    • CR-F07-022: tests/TestBACnetFDTEntry.cpp’s XMLBACnetFDTEntry used APPLICATION tags instead of the CONTEXT tags Decode() actually requires, and never checked Decode()’s own return value - it silently failed, and the test’s own expected XML string already showed all-zero defaults, so it passed against a decode that never happened. Fixed to use the correct tags and assert the decode succeeds before checking the rendered XML shows the real values.
  • rb a-20260909-105 (#1582): MSTP_MasterNodeStateMachine_Idle’s ReceivedUnwantedFrame condition now implements clause (c) of cl. 9.5.6.2 (“FrameType has a value that indicates a standard or proprietary type that is not known to this node”) as a catch-all, not just the specific Reply/Reply Postponed case CR-F08-005 already covered under clause (d). Before the fix, a frame addressed to this station with an unrecognized vendor-proprietary FrameType (128-253) or one of the two named-but-undispatched COBS-extended types (32/33, MSTP_FRAME_TYPE_EXTENDED_DATA_EXPECTING_REPLY/NOT_EXPECTING_REPLY) - both deliberately left alone by MSTP_CheckHeader’s own CR-181 reserved-type guard, which only rejects the standard ASHRAE-reserved ranges upstream - matched none of the state machine’s dispatch branches and fell through doing nothing, leaving ReceivedValidFrame stuck TRUE. Scoped to DestinationAddress == ThisStation, matching the issue’s own reported scenario; a broadcast+unknown-type sibling gap was confirmed to share the identical shape during review and filed as its own follow-up (#1617) rather than folded into this fix. 3 new tests (COBS-extended type, unrecognized proprietary type, and a control proving a recognized type still takes its own dispatch branch and actually replies).
  • rb a-20260909-106 (#1617, sibling of #1582): MSTP_MasterNodeStateMachine_Idle’s ReceivedUnwantedFrame condition now also catches an unrecognized FrameType (vendor-proprietary 128-253, or COBS-extended 32/33) addressed as broadcast (DestinationAddress == 255), not just unicast to this station - the identical stuck-ReceivedValidFrame gap #1582 fixed for unicast, confirmed to reproduce for broadcast during #1582’s own review. 2 new tests mirroring #1582’s own pair, plus a new g_apduCallBackCount test-spy (the existing g_sendByteCallCount spy can’t discriminate the recognized-broadcast-type control case, since BroadcastDataNeedingReply calls APDUCallBack, not SendFrame).
  • rb a-20260909-107 (#1619, sibling of #1582/#1617): MSTP_MasterNodeStateMachine_Idle’s ReceivedUnwantedFrame condition now also rejects Poll For Master, Reply To Poll For Master, and Reply Postponed when sent as broadcast (DestinationAddress == 255) - added to clause (b)’s own broadcast-illegal FrameType list, alongside the existing Token/Test_Request pair. None of these three had a broadcast-legal dispatch path anywhere in this function, nor were they caught as unwanted at broadcast by any existing clause, leaving ReceivedValidFrame stuck TRUE the same way #1582/#1617 fixed for unrecognized types (a different bug class - these three are recognized, just never paired with dest==255 in any branch’s own condition). 3 new tests, one per FrameType.
  • rb a-20260909-111 (#1410): BACnetStackLifeSafetyObject::SetCoreProperty’s default case no longer unconditionally sets writeAccessDenied for a property it does not explicitly handle (only Mode/Tracking_Value are). It now returns false without setting an error, matching GetCoreProperty’s own identical contract - so BACnetDBDevice::SetGeneratedPropertyValue’s lifeSafetyPoint/lifeSafetyZone dispatch can fall through to the generic host-callback path. Previously this made Out_Of_Service (and any other host-opted-in property, e.g. Reliability) structurally unwritable via WriteProperty for Life Safety Point/Zone, even after SetPropertyWritable - the same mechanism every other object type in this stack uses.
  • Corrected a stale comment in BACnetDBDevice.cpp’s lifeSafetyPoint dispatch that documented the old (now-false) “every other property is read-only” claim.
  • 4 new discriminating tests (2 each in TestBACnetStackLifeSafetyZone.cpp and TestBACnetStackLifeSafetyPoint.cpp - the dispatch is a structurally separate/duplicated block per object type): a positive case (host-opted-in Out_Of_Service write reaches the host callback) and a control (a property never opted in is still rejected), both revert-red verified.
  • rb a-20260909-112 (#1562): BACnetChangeOfStateEventAlgorithm::SetProperty no longer requires CallbackSetPropertyReal to be registered. This class has no lowLimit/highLimit/deadband properties and never called that callback - a copy-paste artifact from an OutOfRange-family algorithm’s preamble (which genuinely needs both callbacks for those properties) rejected every timeDelay/timeDelayNormal write from a host that correctly implemented only CallbackSetPropertyUInt. 2 existing tests updated to no longer register the now-unneeded callback (exercising the fix directly), plus a new discriminating test for the sibling timeDelayNormal property.
  • rb a-20260909-113 (#1546): BACnetChoiceSpanLength (source/BACnetChoiceSpan.cpp) no longer truncates a declared value length to uint16_t before its own bounds checks. A BACnetTag’s 4-byte extended-length form can declare a value up to 2^32-1; a declared length whose low 16 bits alias a small in-range number (e.g. 0x1FFF0005, truncating to 0x0005) used to pass every check as if that small aliased value were the real declared length. Fixed at both sites (the top-level primitive case and the nested/constructed-value primitive-skip case) by rejecting any declared value above UINT16_MAX before the truncating cast - this function’s own uint16_t return type and bufferLength parameter mean it can never legitimately need to represent a longer span anyway.
  • Correctness-only per the issue’s own analysis: BACnetTag::Decode() already bounds-checks its own header reads independently (no out-of-bounds read), and BACnetChoiceSpanLength has no callers anywhere in source/ yet (confirmed by a repo-wide grep during review).
  • 5 new discriminating tests (2 sites x reject-the-aliased-value + accept-a-genuinely-small-value, plus a shared null/empty-buffer case already existed). One self-caught mistake along the way: the first version of the nested-site reject test had no matching closing tag, so it returned 0 via an unrelated code path regardless of whether the fix’s own check passed or failed - caught by locally reverting the fix and observing the test stayed green either way, fixed by adding the missing closing tag so a false-accept produces a genuine non-zero span.
  • rb a-20260909-114 (#1448): ci_scripts/check-header-standalone.sh is now committed executable (755), matching every other ci_scripts/*.sh file. The original issue misdiagnosed the cause as the script’s own --self-test path chmodding itself - it doesn’t. The real cause is pre-push.sh’s own unconditional chmod +x ci_scripts/*.sh (needed since several scripts, including this one, are invoked directly rather than via bash ci_scripts/...), which was a no-op for every other script (already 755) but a real mode flip on every run for this one (the only 644 script in the directory) - showing up as an unrelated “mode change” hunk in any diff staged afterward. Tooling-only change; no script logic modified.
  • rb a-20260909-115 (#1335): BACnetStackMemoryBuffer::Insert(const BACnetObjectBase*, bool) no longer treats a 0-byte encode with no error set as success. It used to take the “success” branch whenever bytesEncoded > 0 || !error.IsSet() - any Encode() override returning 0 without setting error (the same silent-failure shape #1093 fixed for BACnetStackUnknownAPDU::Encode) would silently insert zero bytes and still report success up the call chain. bytesEncoded > 0 is now the sole success condition; a 0-byte result falls through to the existing retry-then-fail path instead.
  • Every current caller of this overload passes a BACnetConfirmedServiceACK or a property-value BACnetObjectBase - none of which have a legitimate “successfully encoded to nothing” case, so hardening this trust boundary is safe without needing to audit every individual Encode() override (the issue’s own less-preferred alternative). One genuinely-0-byte-no-error case was found and traced during review (BACnetListOfReadAccessResults::Encode on an empty list, nested inside BACnetReadPropertyMultipleACK) - it is unreachable through this function because IsValid() (checked by Insert before Encode is ever called) is only ever set true after at least one result has been successfully inserted.
  • 3 new tests with a minimal mock BACnetObjectBase hierarchy: the exact bug reproduction (revert-red verified), a control confirming a genuine encode failure (error set) still fails exactly as before, and a control confirming a genuine encode success still succeeds with the real encoded bytes actually present.
  • rb a-20260909-117 (#1289): corrected _spec/spec-coverage/services.md’s AcknowledgeAlarm row, stale since #960/#1029. It still named the pre-#960 single BACnetStack_SendAcknowledgeAlarm export and claimed a test-tool-only BACnetStackTestTool_SendAcknowledgeAlarmWithTimeOfAck variant “also exists” - neither is true at the current tree (confirmed by repo-wide grep: the test-tool variant was removed by #1029, and #960 split the single export into three production exports, BACnetStack_SendAcknowledgeAlarmDateTime/...Unsigned/...Time, none test-tool-gated). Docs-only.
  • rb a-20260909-118 (#1341, R10 pattern - narrow-before-check): fixed 3 more sibling instances of #1207/#1208’s “wire-decoded uint32_t narrowed to a smaller type before range-checking it” shape.
    • BACnetDeviceCommunicationControlRequest::Decode stored the wire-decoded enableDisable value directly, bypassing SetEnableDisable’s own range check entirely (that only guards the separate programmatic SET path); GetEnableDisable then narrowed to uint8_t unchecked. Fixed by rejecting at Decode time, on the raw value, before any narrowing.
    • BACnetStackAlertEnrollmentObject::SetProperty’s notifyType case narrowed to uint8_t unchecked. notifyType is writable=false by default, so this is reached only when a host has explicitly opted it in via SetPropertyWritable - a real, if less common, configuration. Fixed the same way as #1207/#1208 (compare the full-width value against the last legal enumerator - a closed 3-value enumeration, no proprietary range - before narrowing).
    • BACnetStackNotificationForwarder::SetProperty’s reliability case (OOS-gated per cl. 12.51.7(b)) narrowed to uint16_t unchecked. Correction to this batch’s own commit message: reliability is not reachable generically the way AccessDoor’s OOS-gated properties are (notificationForwarder is absent from BACnetBusinessLogic::IsPropertyWritableIfOutOfService’s allowlist) - it requires the SAME SetPropertyWritable host opt-in as notifyType above, which the original commit message omitted stating explicitly. Once opted in, this class’s own Out_Of_Service check becomes the sole enforcement of cl. 12.51.7(b). BACnetReliability has a spec-defined vendor-proprietary range (cl. 23, values 64-65535), so the fix rejects only what the uint16_t storage type itself cannot hold (> UINT16_MAX), matching #1546’s BACnetChoiceSpanLength principle rather than #1207/#1208’s “last legal enumerator” one.
  • 6 new discriminating tests (2 per site: reject-the-wraparound + accept-a-legal/proprietary-value), all revert-red verified together.
  • rb a-20260909-119 (#1447): added MemTracker fault-injection test coverage for 2 of interface-batch-2’s (#1232) 4 untested allocation-failure fixes, and corrected/finalized docs/changelog.d/interface-batch-2.md’s own “Test coverage” claim for the other 2.
    • BACnetBusinessLogic::GetProperty’s GetPriorityArray* helpers (CR-F01b-016): a new test (AnalogOutputPriorityArraySingleIndex_InsertFails_ReportsFailure in tests/TestBusinessLogic.cpp) confirms an InsertObjectAndTakeOwnership OOM during priority-array construction reports failure rather than a silently short array.
    • BACnetBusinessLogic::GetProperty step 7’s *value = *storedValue; deep-copy (CR-F01b-013): a new test (ProprietaryProperty_StoredValueCloneFails_ReportsFailureNotInvalidSuccess) confirms a clone failure during the stored-value fallback reports failure rather than success with an invalid value. Uses a synthetic vendor-proprietary property (via the generic AddProprietaryProperty/AddValueToStoreInDB APIs) rather than a named real property, after TrendLog’s StopTime/LogInterval (bulk internal DateTime housekeeping consumes the injected OOM slot first) and ElevatorGroup’s GroupId (STACK_OPTION_OBJECT_TYPE_ELEVATOR_GROUP needs BACNET_PROTOCOL_REVISION >= 18, not enabled in this build config) both ruled out cleaner real-property targets.
    • CR-F01a-010 (SendSubscribeCOV/SendSubscribeCOVProperty’s SAFE_DELETE(serviceRequest) paths): traced instead of tested. Every underlying primitive setter reachable on this path (BACnetPrimitiveREAL/UINT/BOOL::SetValue, BACnetPropertyReference::SetPropertyReference) is unconditional-success POD assignment with no allocation - the setter-failure branch is unreachable via any legitimate value or MemTracker OOM, so this is genuinely dead defensive code, not an untested live path.
    • CR-F01b-012 (ProcessMessage’s confirmed-request outgoingPDU non-NULL-but-invalid leak): remains untested. Reaching it needs a full wire-level ConfirmedRequest round trip with an OOM landing in the narrow window that leaves the outgoing PDU built-but-invalid (not NULL, not complete) - judged too costly to calibrate blindly in this batch. Left for a future one.
  • rb a-20260909-120 (#1626): projects/linux/coverage/gcov_driver.cpp did not compile - 3 unrelated errors from stale BACnetInterface API usage, never revisited since the file’s original addition and never caught because run_coverage.sh is a manual/on-demand script, not wired into any CI job.
    • CbGetReal was missing the uint32_t* errorCode out-param RegisterCallbackGetPropertyReal now requires.
    • bacnet.ValueUpdated(...) renamed to bacnet.UpdateValue(...).
    • bacnet.EnableSegmentation(...) renamed to bacnet.SetSegmentationEnabled(...) (same argument order).
    • Also applied #1623’s two-step enable-then-writable/subscribable gate (SetPropertyByObjectTypeEnabled before each of the SetPropertyByObjectTypeSubscribable/SetPropertyByObjectTypeWritable calls), matching the sibling fix already applied in tests/BTL_Tests/BTLTests.cpp and tests/TestBACnetActiveCOVSubscriptionMemory.cpp.
    • Verified with bash projects/linux/coverage/run_coverage.sh: compiles, links, runs the driver, and aggregates coverage cleanly (exit 0).
  • rb a-20260909-121 (#1470): ci_scripts/adapter-binding-symbols.js’s --python mode counted a # comment as a live binding, so hand-adapter-coverage.sh reported “Python references 1” for an adapter (adapters/python/CASBACnetStackAdapter.py) that genuinely binds zero exports (a doc-only stub, per CR-187). The lone reference was BACnetStack_RegisterCallbackReceiveMessage inside a # comment describing the target ABI, not a wrong name needing to be caught.
    • stripComments() gained a 4th allowHashComments parameter, used only by --python, so a # comment is now stripped the same way ////* */ already were for the other languages.
    • Since a Python file genuinely binding nothing is now the correct answer for this specific stub, the extractor’s “refuse an empty result” safety guard (meant to catch a broken extractor for the OTHER modes) is exempted for --python only - it exits 0 and prints nothing instead of exit 2.
    • Added a --self-test mode (this script had none before) with a 5-case corpus: a name inside a # comment, inside a single/double-quoted string, inside a triple-quoted docstring (must NOT count), and a name in live code (must still be detected). Wired into pre-push.sh as a new gate ahead of the real hand-adapter-coverage.sh run.
    • hand-adapter-coverage.sh’s combined Python count (union of CASBACnetStackAdapterBindings.py + CASBACnetStackAdapter.py) is unaffected in total (230, unchanged) since the real bindings all live in the other file - only the stub’s own false-positive contribution is now correctly zero.
  • rb a-20260909-122 (#1653): tests/TestBACnetStackDebug.cpp’s BACnetDebugTests fixture assumed the process-wide debug callback (BACnetStackDebug.h’s DebugHelperCallbackPtr, set via APIDebugSetCallback/RegisterCallbackLogDebugMessage) was unregistered at the start of GetDebugMessage - a false assumption under --gtest_shuffle, since any earlier test in the same binary that registers a callback (several in this same file do, and never unregister) leaves it live for whichever test runs next. Fixed by adding SetUp()/TearDown() that explicitly reset the callback to NULL, rather than assuming a clean slate.
    • Revert-red verified against the issue’s own exact reproduction (--gtest_shuffle --gtest_random_seed=2): reproduces the reported failure with the fix disabled, passes with it restored.
    • Full unfiltered suite: 8106/8106 passed (including the usually-flaky #1374 entry, which is order/timing-dependent and not always red).
  • rb a-20260909-123 (#1482): adapters/csharp/CASBACnetStackAdapter.cs’s ResetCallbacks() nulled only 45 of the adapter’s 69 real callback registrations, despite its own comment claiming to reset “all the Callback functions”. The 24 missing ones (verified empirically against the current generated CASBACnetStackAdapterBindings.cs, not just the issue’s own list): GetPropertyObjectIdentifier/LightingCommand/AuthenticationFactorFormat, the matching 2 SetProperty* siblings, WriteGroupInhibitDelay, all 4 File Functions (Read/WriteFile, Read/WriteFileRecord), all 4 Backup/Restore Functions, LifeSafetyOperation, LogDebugMessage, all 7 BACnet/SC data-link callbacks, and RegisterHookTextMessage.
    • A caller relying on ResetCallbacks()’s own documented promise still had 24 live native callbacks pointing at potentially-collected managed delegates.
    • This is pre-existing in substance, not a regression: before #894 generated the full 69-delegate C# surface, the adapter could not register these 24 at all, so the gap was invisible.
    • Verified by diffing the real 69-export list (grep’d directly from CASBACnetStackAdapterBindings.cs) against ResetCallbacks()’s own call list: 45 present, 24 missing, matching the issue’s count exactly. After the fix: all 69 called, zero duplicates, confirmed by the same diff.
    • Compiled CASBACnetStackAdapter.cs + CASBACnetStackAdapterBindings.cs + PropertyBufferHelper.cs together via a throwaway dotnet build (0 warnings, 0 errors) - this codebase has no standing C# compile gate (pre-push.sh’s own comment: “No compiler runs here for any of them”).
    • node ci_scripts/generate-csharp-adapter.js --check and bash ci_scripts/hand-adapter-coverage.sh both still pass with no drift - ResetCallbacks() is hand-maintained, not generated, so this fix does not touch the generator.
    • Not pursued here (explicitly hedged as “Consider” in the issue, not required): generating this list from the model so it cannot drift again. Left as a possible follow-up.
  • rb a-20260909-124 (#1478): adapters/java/src/test/java/com/chipkin/bacnet/ had never once compiled - mvn -o test failed both because SimpleServerExampleTest.java called methods SimpleServerExample does not have, and because CallbackSignatureTest.java’s mock callback implementations were written against a pre-#974 version of the ICASBACnetStackLibrary FPCallbackGetProperty{Real,CharacterString,Enumerated} interfaces (missing the trailing Pointer errorCode parameter #974 added), plus a reference to a BACnetConstants.NETWORK_TYPE_BACNET_IP constant that was never actually added.
    • Deleted SimpleServerExampleTest.java: on inspection its assumptions (getVersionInfo(), a dynamic per-instance addAnalogInput(int, String, float), processMessages() returning boolean, double-initializeDevice throwing) do not match SimpleServerExample’s actual design (fixed setup via initializeDevice/startup/tick, values updated in place via updateAnalogInputPresentValue) - this is not a naming drift a rename could fix, it tests an object model that was never implemented. Matches the issue’s own concern: a test file that has never compiled “reads as coverage” it does not provide.
    • Fixed CallbackSignatureTest.java: added the missing Pointer errorCode parameter (with a mock Memory argument) to all 3 affected callback signatures; replaced the 2 uses of the never-added NETWORK_TYPE_BACNET_IP with the real NETWORK_TYPE_IP (BACnet/IP has no separate constant from generic IP in this stack - source/BACnetPacket.h’s NetworkType_IP already IS BACnet/IP); removed the resulting contradictory assertEquals(5, NETWORK_TYPE_IP) (the “5” was specific to the never-added constant, not a real value).
    • Updated the stale doc-comment cross-reference in CASBACnetStackAdapterTest.java and the “Business Logic Tests” section of adapters/java/README.md, both of which pointed at the now-deleted file.
    • Verified: mvn -o clean test - BUILD SUCCESS, 6/6 CallbackSignatureTest tests pass, CASBACnetStackAdapterTest’s 4 tests skip gracefully (no native library available in this environment, same as before this batch).
  • rb a-20260909-125 (#956): .github/workflows/release.yml’s “Create GitHub Release” step ran for both of the workflow’s triggers (a v*.*.* tag push, or manual workflow_dispatch) and always published the release under tag_name: "release-${{ github.ref_name }}" - a DIFFERENT tag from the one that actually triggered a real release (e.g. a v6.0.19 push published under release-v6.0.19, detaching the release from the tag a customer would follow, while the build/test/artifact-naming steps all correctly used the bare github.ref_name).
    • tag_name now uses the bare ${{ github.ref_name }}, matching the triggering tag exactly for a real release.
    • The step also gained an if: github.ref_type == 'tag' guard (the issue’s own recommended assertion). This is defense-in-depth, not the primary fix: the whole release job already only runs on github.event_name == 'push' && startsWith(github.ref, 'refs/tags/v') (a pre-existing, unrelated job-level condition), so a workflow_dispatch run never reached this step, or the zip steps before it, either before or after this fix - it only exercises the separate build job. The step-level check is a second line of defense should the job-level condition ever be loosened.
    • Not tested by execution: this only changes the release-publish path, which runs solely on a real v*.*.* tag push (or manual dispatch) - neither of which this batch can safely trigger itself (pushing a real version tag would create a live, public GitHub Release; the safety guidelines this session operates under treat that as a hard-to-reverse, human-gated action). Verified instead via: YAML re-parses cleanly (python -c "import yaml; yaml.safe_load(...)"), a repo-wide sweep found no other reference or documented rationale treating the release- prefix as intentional (the only other hit is a historical sprint-review doc recording this exact finding, left untouched as a point-in-time record), and github.ref_type/step-level if: are standard, well-documented GitHub Actions context fields and syntax. Should be spot-checked at the next real tagged release.
  • rb a-20260909-126 (#1668, #1669): 2 doc-only header cleanups, no ABI impact.
    • source/BACnetCallbackInterface.h (#1668): FPCallbackAuditNotificationReceived’s detailed doc block was misplaced above FPCallbackEventNotificationReceived’s typedef instead of its own - a merge-conflict recovery (commit 6a4848d38) inserted the recovered FPCallbackEventNotificationReceived typedef between the doc and the FPCallbackAuditNotificationReceived typedef it actually describes, leaving FPCallbackEventNotificationReceived with only a one-line stub. Restored FPCallbackEventNotificationReceived’s own original doc (recovered from before the conflict, commit 1abe6ccba on the sprint-101-eventnotification-receive branch), updated for the current parameter spelling (timeStampHundredthSecond, not the recovered text’s “Hundreth” typo) and for the useEventValues/encodedEventValues/encodedEventValuesLength trio Sprint 118 added after the original doc was written. Moved the detailed audit doc down to precede FPCallbackAuditNotificationReceived’s own typedef.
    • source/BACnetTestToolInterface.h + .cpp (#1669): deleted 2 orphaned Doxygen blocks in the header describing a test-tool AcknowledgeAlarm send surface (SendAcknowledgeAlarm/SendAcknowledgeAlarmWithTimeOfAck) that no longer has any method declaration following it - the surface was removed in #1029/#960. Found and fixed the same staleness in the sibling .cpp file (2 more orphaned comment blocks with no function definition following them) and 2 dangling cross-references (in the header and the .cpp) that described a still-existing method as “mirroring the … sibling pattern above” a method that no longer exists.
    • Verified: both headers syntax-check cleanly (g++ -fsyntax-only), and a full incremental rebuild + unfiltered suite run (8106/8106 passed) confirms no compile breakage from the reordering/deletions across the wider header-dependency graph.
  • rb a-20260909-127 (#955): ci_scripts/adapter-callback-signature-check.js’s --self-test evasion corpus (detects the historical six-vs-eight-parameter ReceiveMessage callback mismatch class, issue #584) was never invoked by any automated path - hand-adapter-coverage.sh (its only caller) ran only the live comparison, so a future regression in the comparison logic itself could silently turn the detector into a false green while the mechanism built specifically to catch that never ran to notice.
    • hand-adapter-coverage.sh now runs node ci_scripts/adapter-callback-signature-check.js --self-test first and fails closed on it (with an explicit message that the live comparison below it is not trustworthy until the self-test passes), before running the existing live comparison as a distinct second step.
    • Verified: the self-test corpus passes standalone (5/5 mismatches detected, 6/6 clean cases not flagged) and the full hand-adapter-coverage.sh gate still reports OK end-to-end.
    • Revert-red: temporarily made selfTest() return a simulated failure and confirmed hand-adapter-coverage.sh now correctly fails closed with the new message (previously this class of regression would have gone completely unnoticed, since --self-test was never invoked at all); restored and reconfirmed green.
    • Not pursued (explicitly hedged as “or” in the issue’s own recommendation, not required): a separate static invocation-check script asserting this wiring can’t be silently removed again. Left as a possible follow-up.
  • rb a-20260909-128 (#954): 8 ci_scripts gates’ own WHERE THIS RUNS/WHERE IT RUNS doc comments still said “no GitHub workflow invokes it” (citing pre-push.sh as the only caller), stale since .github/workflows/tests.yml’s fast-gates job (added closing #561/#586) started running all of them on every configured pull request and tag path. Updated all 8 to name the current coverage: check-msvs-test-registration.sh, hand-adapter-coverage.sh (and its child adapter-callback-signature-check.js), check-self-assignment-guards.js, node-adapter-argument-order.js, node-adapter-output-buffer-guards.js, check-todo-markers.js, check-contract-pins.js, check-md-links.js.
    • check-msvs-test-registration.sh kept its still-valid caveat: no workflow builds CASBACnetStackTests.vcxproj, so it can only prove the .vcxproj’s own file list matches tests/*.cpp on disk, not that the project compiles on Windows.
    • Also corrected AGENTS.md’s own matching cross-reference (“None of these is on the PR path… No GitHub workflow invokes any of them”), the exact stale claim the issue names as needing a coordinated fix alongside the 8 scripts.
    • Left ci_scripts/addon-compile.sh’s own “no GitHub workflow invokes it” note untouched - unlike the 8 above, its claim is still accurate today (confirmed by grep), and its own comment already correctly frames wiring it into tests.yml as a pending owner action, not something this fix’s scope covers.
    • This batch touches only doc comments in ci_scripts/*.sh/*.js and AGENTS.md - no .github/workflows/** file was edited (this task didn’t call for it, per AGENTS.md’s own “do not edit .github/workflows/** unless the task explicitly calls for it”).
    • Verified: all 7 JS files re-parse cleanly (node -c), both shell scripts pass bash -n, and every script’s own --self-test/real-gate run was re-executed and still passes (comment-only changes, no logic touched).
  • rb a-20260909-95 (#1248, CR-F18 fault algorithms family, remaining 5 items addressed): hygiene mini-batch continuing family F18 (6 of 11 items already resolved by earlier batches).
    • CR-F18-017: BACnetFaultParameterFaultLifeSafety::Decode now validates via AreAllParametersValid() like its sibling BACnetFaultParameterFaultExtended, instead of an unconditional SetValid(true). No new discriminating test added: BACnetSequenceOf<BACnetLifeSafetyState> and BACnetDeviceObjectPropertyReference both already set SetValid(true) unconditionally on any successful sub-decode with no content-range check, so there is currently no reachable input where the new check diverges from the old one - matching FaultExtended’s own test file, which also doesn’t test this exact scenario for the same structural reason. Verified via the 3 existing tests in tests/TestBACnetFaultParameterFaultLifeSafety.cpp continuing to pass.
    • CR-F18-018: added the missing 135-2024 clause citations (13.4.7 FAULT_OUT_OF_RANGE, 13.4.8 FAULT_LISTED, verified against _spec/spec/extracted/135-2024/clauses/13.4.txt) to source/BACnetOutOfRangeFaultAlgorithm.cpp’s and source/BACnetListedFaultAlgorithm.cpp’s Run() condition-list comments.
    • CR-F18-014: answered without a code change. Verified the spec-internal tension directly - cl. 13.4.2’s prose says fault values are BACnetOptionalCharacterString (permitting Null), but cl. 21.6’s ASN.1 grammar for fault-characterstring’s list-of-fault-values is a plain SEQUENCE OF CharacterString with no Null arm. Recorded for the owner per the finding’s own recommendation; no code change without a ruling, since conforming to the prose over the grammar would be a breaking wire-format change.
    • CR-F18-015: answered without a code change. Verified source/CASBACnetStackDLL.h exports only 4 of the 6 fault-algorithm setters (SetFaultOutOfRangeAlgorithmReal, SetFaultListedAlgorithm, SetFaultLifeSafetyAlgorithm, SetFaultStatusFlagsAlgorithm) - SetFaultStateAlgorithm/SetFaultCharacterstringAlgorithm are genuinely absent. Flagged for the owner per the finding’s own recommendation.
    • CR-F18-016: answered without a code change, with a correction. The underlying technical claim is confirmed - BACnetExtendedFaultAlgorithm is fully implemented (Run/CreateEventValues), factory-registered (source/BACnetFaultAlgorithm.cpp), and has substantial fixture-based test coverage (tests/TestBACnetExtendedFaultAlgorithm.cpp, TEST_F-based - not TEST-based, which an initial grep missed before a closer read). However, the finding’s “refines #475” framing is stale: #475’s current body (as of its last 2026-09-03 owner rulings, labeled needs-human-review) no longer contains a “FAULT_EXTENDED stub/partial” claim - it has since evolved into a much broader owner-decision item about promoting the Event Enrollment arming surface to the customer DLL. The technical observation stands; the cross-reference to #475 does not currently apply.
  • rb a-20260909-96 (#1250, CR-F20 notification parameters family, 3 of 4 remaining items addressed, partial): hygiene mini-batch continuing family F20 (2 of 6 total items already resolved by earlier batches).
    • CR-F20-008: the finding’s own claim was stale - tests/TestBACnetNotificationParametersChangeOfTimer.cpp already exists with 5 substantive tests, including exactly the recommended “three OPTIONAL fields’ absent-vs-malformed decode paths” coverage. No new work needed.
    • CR-F20-009: the finding’s own claim was partially stale - self-aliasing guards, IsEqual, and the Clone()-failure path for BACnetNotificationParametersExtendedParameter were already covered in tests/TestBACnetNotificationParametersExtended.cpp (SettingTheHeldPointerBackIsANoOp, DecodeRejectsAContextTagOtherThanPropertyValue, IsEqualDiscriminatesTheChoiceAlternative, AFailedDeepCopyIsNotValid). Only 3 of the 13 application-tagged CHOICE alternatives had round-trip coverage (Real, Unsigned, Null). Added tests/TestBACnetNotificationParametersExtendedParameter.cpp with the remaining 10 (Boolean, Integer, Double, OctetString, CharacterString, BitString, Enumerated, Date, Time, ObjectIdentifier) - the genuine gap.
    • CR-F20-012: flagged for the owner without a code change. Verified source/BACnetNotificationParametersOutOfRange.h is the only one of the six out-of-range sibling classes exposing a public SetNotificationParameters(...) bundling helper.
    • CR-F20-007 remains deferred: a dedicated test file for BACnetNotificationParametersChangeOfDiscreteValue/ChangeOfDiscreteValueNewValue covering all 10 CHOICE datatypes is a substantially larger undertaking, judged too large for this batch.
  • rb a-20260909-97 (#1247, CR-F17 event algorithms family, all 3 remaining items addressed, comment-only): hygiene mini-batch closing out family F17 (7 of 10 total items already resolved by earlier batches). No source or test changes - all 3 remaining items are S3/S4 comment-only or flag-for-owner.
    • CR-F17-013: flagged for the owner without a code change. Verified source/BACnetOufOfRangeEventAlgorithm.cpp (typo) exists alongside the correctly-spelled source/BACnetOutOfRangeEventAlgorithm.h, with no correctly-spelled .cpp present. Not fixed unilaterally per the finding’s own recommendation, given the rename risk (build system references, external tooling).
    • CR-F17-021: recorded without a code change. Verified source/BACnetChangeOfTimerEventAlgorithm.cpp never references Last_State_Change, Initial_Timeout, or Expiration_Time. Deferred pending CR-F17-004 (a higher-severity finding not part of this issue’s own checklist, filed separately per the review’s own severity-based split), per the finding’s own recommendation to address both together if Update_Time/transition tracking is added.
    • CR-F17-022: recorded without a code change. Confirmed BACnetOutOfRangeEventAlgorithm, BACnetDoubleOutOfRangeEventAlgorithm, BACnetSignedOutOfRangeEventAlgorithm, and BACnetUnsignedOutOfRangeEventAlgorithm all exist with a Run() method; the finding’s own detailed (a)-(h) transition-logic trace was accepted as-is (informational, recommendation “none”) rather than independently re-derived.
  • rb a-20260909-98 (#1241, CR-F10 ReadProperty/ReadPropertyMultiple/ReadRange family, all 6 items addressed): hygiene mini-batch closing family F10.
    • CR-F10-007: BACnetReadPropertyACK::Encode() now sets error.SetAbort(BACnetAbortReason::other) when m_propertyValue is NULL, instead of silently returning 0. Not fixed by rejecting NULL in SetPropertyValue/SetReadPropertyACK (the finding’s other suggested option): Clone() deliberately calls SetReadPropertyACK(..., NULL, ...) as an intermediate step before separately deep-cloning the property value, to avoid a double-free on a partial clone failure - rejecting NULL there would break every Clone() call.
    • CR-F10-008: source/BACnetReadPropertyMultipleProcessor.cpp no longer excludes Property_List from OPTIONAL-property expansion (135-2024 cl. 15.7 names only ALL/REQUIRED). No discriminating test added: every object-type block in source/BACnetDBPropertyProfile.cpp marks Property_List conformance-required, so GetOptionalPropertyList() never includes it regardless of this fix, and there’s no public API to override a conformance code for a test object.
    • CR-F10-010: BACnetReadRangeACK::SetItemCount() now rejects a count that doesn’t match m_itemData.GetSize(), mirroring Decode()’s existing self-consistency handling. SetEmptyResponse() reordered to clear m_itemData before calling SetItemCount(0) - calling it against stale, not-yet-cleared data (the #708 regression scenario) would otherwise reject 0 itself.
    • CR-F10-011: BACnetReadRangeRequest::GetElements() now renders the Range CHOICE (reference/referenceTime/count), mirroring the switch already used in GetMinimumMemoryCount(). Also fixed the function to return true on success - it unconditionally returned false before, regardless of whether rendering succeeded.
    • CR-F10-009: flagged for family F01 (owns source/BACnetBusinessLogic.cpp, python ci_scripts/review-file-issues.py family-of) without a code change - out of this family’s file ownership. Spot-checked and confirmed at least one genuine unset-error return false path in BACnetBusinessLogic::GetProperty (line ~1428, GetPropertyProfile failure). Not a full audit - the finding’s own recommendation explicitly punts the complete trace to whichever session works F01 (issue #1232, currently needs-human-review).
    • CR-F10-012: recorded without a code change (S4, “not urgent” per the finding’s own framing). Confirmed BACnetReadPropertyMultipleProcessor.cpp’s single ObjectExists check sits inside the per-property-reference loop rather than the outer per-read-access-specification loop.
  • rb a-20260909-99 (#1243, CR-F12 object management family, all 6 items addressed): hygiene mini-batch closing family F12.
    • CR-F12-002: BACnetCreateObjectError::GetElements() now returns true on success, matching every sibling render accessor in this family.
    • CR-F12-003: documented (docs-only) that the stack does not automatically invalidate cross-references to a deleted object; CallbackDeleteObject is where that host-side cleanup belongs.
    • CR-F12-004: flagged for the owner (docs-only) - BACnetAddListElementProcessor’s non-atomic-for-primitives gap (#459) is AddListElement-only; RemoveListElement instead refuses the whole request, an asymmetry between two spec-symmetric sibling services. No fix without a ruling on whether any future object model needs this.
    • CR-F12-005: added ASSERT_FALSE(device.ObjectExists(...)) control assertions to all 3 rollback tests in tests/TestBACnetCreateObjectProcessor.cpp (previously they asserted the CreateObject failure but never confirmed the atomicity guarantee they were implicitly testing for).
    • CR-F12-006: added the standard negative-path tests (NULL buffer, zero length, truncated buffer, invalid object) for BACnetDeleteObjectRequest::Decode/Encode in tests/TestBACnetDeleteObjectRequest.cpp.
    • CR-F12-007: added a test for BACnetDeleteObjectProcessor’s unregistered-callback success path (CallbackDeleteObject is optional; deletion must still succeed when it’s left NULL) in tests/TestBACnetDeleteObjectProcessor.cpp.
  • rb a-20260913-1 (#1837, #1835): owner-directed single batch covering every code-bug item from the #910 triage plus #1835 (C26).
    • C74: BACnetDBDevice::EnableFaultListedAlgorithm never enabled the monitored list (Fault_Signals for Lift/Escalator), so the FAULT_LISTED fault algorithm could never evaluate; Reliability could never transition to FAULTS_LISTED.
    • C60: BACnetDatabase::SetLiftHigherLowerDeck wrote Higher_Deck/Lower_Deck before validating/updating the Elevator Group’s Group_Members, so a later failure (or a group with no stored Group_Members, which is nothing to remove per cl. 12.58.7) was reported false after the database had already changed.
    • C20: a Backup_Failure_Timeout expiry delivered CallbackCompleteRestore(..., wasAborted=false) instead of true - 135-2024 cl. 19.1 treats a timeout as an abort.
    • C17: re-arming CHANGE_OF_STATE appended to the algorithm’s alarm-value list instead of replacing it on single-valued monitored properties (Binary Input/Value, Lift/Escalator), so the object could never return to NORMAL.
    • C16: CHANGE_OF_STATE alarm values were stored with a type that did not match the monitored value’s own type on 5 of 7 accepted object types (Access Door/Load Control/Access Zone always, plus the Bool variant on Binary Input/Value), so the algorithm could never fire on them.
    • #1835 C26: BACnetInterface::SendConfirmedTextMessage/SendUnconfirmedTextMessage discarded messageLength and built the outgoing message/messageClass strings via strlen(), which can read past a non-NUL-terminated caller buffer or silently truncate on an embedded NUL.
    • C22: internal doc for SetBACnetStackSetting’s IDs named a setting that does not exist and gave the real one (MaxActiveCOVSubscriptionsInAResponse) the wrong numeric ID.
    • C18: BACnetStackSchedule/RunSchedule did not write to List_Of_Object_Property_References on entering the schedule’s Effective_Period when the recalculated Present_Value happened to equal what it already held from before the period started (cl. 12.24.6 requires the write unconditionally on entry).
    • C13: BACnetInterface::Tick() reported didWork=false on a tick whose only work was sending COV-multiple notifications or committing a time-delayed alarm/event transition out of the pending queue - both are documented as “work” (CASBACnetStackDLL.h:338-342).
    • Frozen source/CASBACnetStackDLL.h doc text made stale by C16/C17/C18/C20/C22 is not edited here (owner/HUMAN work) - see the PR description for the specific line ranges.
    • Verified: ci_scripts/pre-push.sh full suite 8329/8329 tests passed; each item has its own ci_scripts/verify-fails-without.sh revert-red evidence (C18’s, a brand-new public accessor the script can’t cleanly revert, was verified by manually reverting the entry-detection line, confirming the new test fails, then restoring and reconfirming green).
  • rb a-20260913-3 (#1236, CR-F05-025): BACnetStackRespondingStateMachineCurrentState::Reset() left m_maxApduLength, m_segmentRetryCount, m_proposedWindowSize and m_actualWindowSize uncleared while clearing their sibling segmentation fields, relying on an undocumented cross-file invariant that the next transaction always re-sets them via SetSegmentationParameters() before reading them. Reset() now clears all 8 segmentation fields. Updated 3 pre-existing tests in tests/TestBACnetRespondingStateMachine.cpp that had asserted GetActualWindowSize()==1 immediately after the state machine returned to RespondingState_Idle (i.e. after Reset() ran) - these were enshrining the exact stale-field bug being fixed, inconsistent with every sibling field assertion in the same block already expecting 0 post-reset.
    • CR-F05-016 (ASN.1 spec comment for choices 32-34) re-verified at tip: already fixed by a-20260908-91; ticked on #1236, no new commit.
    • CR-F05-043 (BACnetStackUnknownAPDU buffer size vs BACnet/SC max) re-verified at tip and ticked as a non-issue: the finding’s premise doesn’t hold at the current tip - the SC-specific per-APDU constant (BACNET_MAX_APDU_LENGTH_SC = 1476) is smaller than the non-SC bound the class already uses (BACNET_NPDU_MAX_LENGTH = 1497), not larger; the larger SC constants the finding cited are router-hop/forwarding-frame bounds this class does not need.
    • Verified: ci_scripts/pre-push.sh full suite 8330/8330 tests passed.
  • rb a-20260913-5 (#1221, CR-F27-004): BACnetStackLightingOutput::ApplyLightingCommand silently ACKed RESTORE_ON/DEFAULT_ON/TOGGLE_RESTORE/TOGGLE_DEFAULT as no-ops, contrary to 135-2024 cl. 12.54.6’s “shall support all BACnetLightingOperations shown in Table 12-67” mandate (Table 12-67 lists all four as full, non-optional members). Implemented per Table 12-67:
    • RESTORE_ON / DEFAULT_ON: write Last_On_Value / Default_On_Value to the specified priority slot.
    • TOGGLE_RESTORE / TOGGLE_DEFAULT: write Last_On_Value/Default_On_Value when Present_Value is 0.0%, otherwise write 0.0%; a write at a priority weaker than the array’s current winner is accepted but does not modify the array, unless nothing is currently winning (no higher-priority command to protect).
    • Round-1 review caught that all four skipped syncing Tracking_Value (unlike every other instant write in this class) - fixed in a follow-up commit.
    • A sibling gap - the same four operations’ direct-Present_Value special-sentinel-value entry point (WriteDirectPresentValue, values -4.0..-7.0) still no-ops them - was found during this fix but is out of #1221’s own scope; filed as #1881.
    • Verified: ci_scripts/pre-push.sh full suite 8338/8338 tests passed.
  • rb a-20260914-1 (#1096, CR-F07-003, security): Forwarded-NPDU’s cl. J.2.5 “Originating Device” address-substitution logic in BACnetDataLinkIPv4 screened only all-zero, limited-broadcast and zero-UDP-port addresses before trusting an attacker-controlled field as the real peer identity (reply destination, next-hop MAC learning, per-source #427 quota bucket key). Per owner ruling (2026-09-13, “always follow the spec”), a multicast address (224.0.0.0/4) - never a device’s unicast address - is now excluded too, matching the existing limited-broadcast check’s rationale.
    • Directed/subnet broadcast, the ruling’s other named class, is NOT yet screened: detecting it needs this device’s own subnet mask for the receiving network port, which BACnetDataLinkIPv4 does not currently track anywhere. Filed as a follow-up, #1884.
    • Updated the one existing test that had pinned the pre-ruling “multicast is treated as usable” behavior; added a boundary control just below the multicast range.
    • Verified: ci_scripts/pre-push.sh full suite 8339/8339 tests passed.
  • rb a-20260914-11 (#1801): BACnetStackLoadControl‘s shedLevels (Shed_Levels, cl. 12.28.18) SetProperty rejected an index-0 element write with invalidArrayIndex, but accepted a whole-array write of any length. Per cl. 12.1.5.1/19.1’s index-0-as-resize convention and cl. 12.28.18’s size-parity requirement with Shed_Level_Descriptions (a read-only-via-BACnet, host-configured-only property), Shed_Levels cannot actually resize independently of a BACnet client’s own request: an index-0 write, and a whole-array write whose count doesn’t match Shed_Levels’ own current size, are both now invalidArraySize, mirroring the Weekly_Schedule precedent (#1797).
    • Verified: revert-red on both checks; the index-0 revert additionally exposed an unsigned-underflow segfault artifact of the specific revert technique (not a shipped hazard). An adversarial review caught a real regression in the first cut of this fix (comparing against Shed_Level_Descriptions’ size instead of Shed_Levels’ own, which would permanently reject every whole-array write for a host that never configures descriptions); corrected and re-verified. Full ci_scripts/pre-push.sh: 8352/8352 tests passed.
  • rb a-20260914-15 (#786): tests/TestBACnetHostAddress.cpp/TestBACnetHostNPort.cpp guarded on STACK_OPTION_OBJECT_TYPE_NETWORK_PORT alone, unlike BACnetHostAddress.h/.cpp‘s own 3-way gate (STACK_OPTION_OBJECT_TYPE_NETWORK_PORT || STACK_OPTION_AUTH_REQUEST || STACK_OPTION_DEVICE_AUTHORIZATION) - a build enabling only the latter two (e.g. STACK_OPTION_PROFILE_B_AS) compiled BACnetHostAddress/BACnetHostNPort in with zero direct unit coverage. Widened both test files’ guards to match; corrected two stale #endif comments naming only NETWORK_PORT.
    • Verified: build-configuration-only defect, no runtime behavior to revert-red - confirmed at the preprocessor level instead (g++ -E with only -DSTACK_OPTION_AUTH_REQUEST: old guard compiles the tests out, new guard compiles them in). Full ci_scripts/pre-push.sh under the standard full-build config: 8352/8352 tests passed.
  • rb a-20260914-17 (#1881): BACnetStackLightingOutput::WriteDirectPresentValue’s -4.0/-5.0/-6.0/-7.0 special values (cl. 12.54.4, Table 12-65: RESTORE_ON/DEFAULT_ON/TOGGLE_RESTORE/TOGGLE_DEFAULT) were still a recognized-and-no-op placeholder - the sibling Lighting_Command entry point for the exact same four operations was already fixed by #1221, but writing them directly to Present_Value got a Simple-ACK with no effect. Extracted #1221’s logic into a shared ApplyRestoreOrDefaultOrToggle helper and wired both entry points to it.
    • Verified: revert-red on 6 new/updated tests (5 new direct-Present_Value tests, 1 updated pre-existing test that pinned the old no-op). An adversarial review caught the refactor silently changing m_lastLightingCommand’s read-back contract for these four operations (now set unconditionally instead of #1221’s original success-only semantics); fixed and covered by a MemTracker fault-injection regression test. Full ci_scripts/pre-push.sh: 8358/8358 tests passed.
  • rb a-20260914-21 (#1785): Life Safety Point/Zone latching (cl. 12.15.4/12.16.4) was keyed to quiet rather than to event-NORMAL classification - any non-quiet Tracking_Value input latched Present_Value, and once latched, ANY further non-quiet input silently overwrote it without a RESET. Per owner ruling (2026-09-13), latching is now a property of the CURRENT Present_Value (is it a member of Alarm_Values/Life_Safety_Alarm_Values/Fault_Values?): while nothing is latched, every input - event-NORMAL or not - is copied through; once latched, no further input changes it until a RESET-family LifeSafetyOperation. Tracking_Value always tracks the raw input.
    • Verified: revert-red on 2 new discriminating tests (a later event-NORMAL non-quiet input no longer blocks a subsequent quiet transition; a latched value no longer gets silently overwritten by a different non-quiet input). Updated 8 existing tests across 3 files whose fixtures relied on either an empty Alarm_Values list or an unreset second latch attempt, both invalidated by the new rule. Full ci_scripts/pre-push.sh: 8360/8360 tests passed. Adversarial review: 0 defects.
  • rb a-20260914-23 (#1904, finding 1 of 5): BACnetEventAlgorithmRegistryTest.RegisterEventAlgorithm_RuntimeRegistration_IsServed leaked a runtime registration into BACnetEventAlgorithm’s process-wide static registry with no cleanup, failing its own first assertion on a second run in the same process (an observed order-dependent flake under --gtest_shuffle --gtest_repeat). Added BACnetEventAlgorithm::UnregisterEventAlgorithm (the RegisterEventAlgorithm mirror) and an RAII test-cleanup guard.
    • Verified: revert-red on the new UnregisterEventAlgorithm_RemovesRuntimeRegistration test; separately confirmed removing the RAII guard reproduces the exact original flake under --gtest_repeat=3. 15 shuffled repeats of the affected test file pass clean; full ci_scripts/pre-push.sh: 8361/8361 tests passed. Findings 2-5 of #1904 remain open for a future batch (separate files/root causes).
  • rb a-20260914-27 (#1904, finding 2 of 5): BACnetInterfaceVirtualDevicesTest.IAmBroadcast never reset its process-wide static iAmOffset counter (nor the iAmResponse/iAmMessageLength buffers it indexes) in SetUp(), so a second run of the test in the same binary started from whatever nonzero value the previous run left it at instead of 0 (an observed order-dependent flake under --gtest_shuffle --gtest_repeat). Added the three resets to SetUp().
    • Verified: reverting just the new reset lines and running IAmBroadcast alone with --gtest_repeat=3 reproduces the exact original failure signature from #1904’s own evidence (iAmOffset == 1 instead of 5, same assertion) on the third iteration. Restoring the fix: 15 shuffled repeats of the isolated test pass clean, and full ci_scripts/pre-push.sh: 8361/8361 tests passed. Findings 3-5 of #1904 remain open for future batches (separate files/root causes).
  • rb a-20260914-29 (#1904, finding 3 of 5): SubscribeCOV_BTL_Tests.9_10_1_9 never reset its process-wide static currentTime, currentTimeOffset, ai_presentValue, and ai_covIncrement in SetUp(), so a second run of the test in the same process started from whatever the previous run left them at instead of their original values, corrupting byte-for-byte comparisons against the test’s hardcoded expected COV notification messages (an observed order-dependent flake under --gtest_shuffle --gtest_repeat). Added the four resets to SetUp().
    • Verified: reverting just the new reset lines and running 9_10_1_9 alone with --gtest_repeat=3 reproduces the exact original failure signature from #1904’s own evidence (outMessage2[i] == 'A' (0x41) instead of sentBuffer[i] == '@' (0x40)) on the second iteration. Restoring the fix: 15 shuffled repeats of the isolated test pass clean, and full ci_scripts/pre-push.sh: 8361/8361 tests passed. Findings 4-5 of #1904 remain open for future batches (separate files/root causes).
  • rb a-20260914-3 (#1135, CR-F11-011): BACnetWriteGroupProcessor required a Channel object to list a WriteGroup’s requested group number in its OWN Control_Groups before matching that object’s Channel_Number - stricter than 135-2024 cl. 15.11, which makes group membership a DEVICE-level fact (any one Channel listing the group makes the whole device a member, and once established, every Channel whose Channel_Number matches a change-list entry is written, regardless of whether that specific Channel individually lists the group). Per owner ruling (2026-09-13, “always follow the spec”), membership is now checked once, device-wide, in a preliminary pass; the per-channel routing loop no longer re-checks Control_Groups.
    • Verified: ci_scripts/pre-push.sh full suite 8341/8341 tests passed.
  • rb a-20260914-31 (#1904, finding 4 of 5): BACnetDBDeviceTest.NetworkPortGetIpDnsServer_issue293 never cleared its process-wide static IPDNSServers vector in SetUp(), so a second run of the test in the same process accumulated the previous run’s (by then dangling) entries alongside the new ones, growing GetSize() to 4 instead of the expected 2 (an observed order-dependent flake under --gtest_shuffle --gtest_repeat). Added IPDNSServers.clear() to SetUp().
    • Verified: reverting just the new line and running the test alone with --gtest_repeat=3 reproduces the exact original failure signature from #1904’s own evidence (value.GetSize() == 4 instead of 2) on the second iteration, growing to 6 on the third (confirming pure accumulation). Restoring the fix: 15 shuffled repeats of the isolated test pass clean, and full ci_scripts/pre-push.sh: 8361/8361 tests passed. Finding 5 of #1904 remains open for a future batch.
  • rb a-20260914-33 (#1904, finding 5 of 5, final): BACnetReadRangeProcessorTest.ProcessTrendLogLogBufferNoRange never reset its process-wide static count in SetUp(), so a second run of the test in the same process started counting up from whatever the previous run left it at instead of 0.0f, producing wrong values in the returned trend log records (an observed order-dependent flake under --gtest_shuffle --gtest_repeat). Added count = 0.0f to SetUp(). This closes out #1904, whose remaining 4 findings were fixed in earlier batches (a-20260914-23/27/29/31).
    • Verified: reverting just the new line and running the test alone with --gtest_repeat=3 reproduces a failure at the exact same assertion line as #1904’s own evidence, consistent with the same per-run-pair accumulation pattern. Restoring the fix: 15 shuffled repeats of the isolated test pass clean, and full ci_scripts/pre-push.sh: 8361/8361 tests passed.
  • rb a-20260914-37 (#890, partial): BACnetAuthorizationStatus::Decode’s optional error[1] and error-source[2] fields had no guard at all against a present-but-malformed opening tag (the shared BACnetObjectBase::DecodeWithContext cannot itself distinguish “genuinely absent” from “present but malformed” - both return 0). A malformed opening tag for either field was silently accepted as though the field were simply absent. Added a caller-side tag-number peek before each DecodeWithContext call: if the field’s own tag number is present but decode still fails, reject as malformed rather than treating it as absent.
    • This is a partial fix - a broader audit found 24 more call sites across ~18 files with the same underlying defect shape; deferred to future batches given this area’s revert history (a prior attempt to fix the shared method itself broke ~20 unrelated CHOICE/SEQUENCE probing round-trips).
    • Verified: 2 new discriminating tests (forging a malformed opening tag via a byte-swap with the field’s own closing tag) fail without the fix and pass with it. Full ci_scripts/pre-push.sh: 8363/8363 tests passed.
  • rb a-20260914-39 (#890, continued): fixed 3 more of the 26 identified malformed-opening-tag call sites, using the tag-number-peek technique established in a-20260914-37 - BACnetSetpointReference::Decode (setpointReference[0]), BACnetAccessToken::Decode (notBefore[3]/notAfter[4]), and BACnetAuthorizationPolicy::Decode (notBefore[0]/notAfter[1]). All three previously relied only on an error.IsSet() check, which cannot catch a malformed OPENING tag specifically since the shared decode helper resets error on that path.
    • Verified: 2 new discriminating tests (SetpointReference, AuthorizationPolicy) fail without the fix and pass with it. BACnetAccessToken’s fix has no matching end-to-end test - its malformed-tag cascade lands on a mandatory field whose own failure path sets error regardless of this fix, making it structurally impossible to discriminate via a black-box Decode() test; disclosed and covered instead by full-regression-suite equivalence with the proven BACnetAuthorizationPolicy pattern. Full ci_scripts/pre-push.sh: 8365/8365 tests passed. 21 more sites remain for future batches.
  • rb a-20260914-41 (#890, continued): fixed all 6 malformed-opening-tag call sites in BACnetAuditNotification::Decode (sourceTimestamp[0], targetTimestamp[1], targetProperty[12], targetValue[14], currentValue[15], result[16]), using the established tag-number-peek technique. For targetProperty/targetValue/currentValue/result, the pre-fix behavior was worse than a delayed rejection: since no mandatory field follows any of them, a malformed opening tag caused the decode to silently return a truncated-but-“successful” result with IsValid() still reporting true, dropping the malformed field and everything after it with no error signaled.
    • Verified: 6 new discriminating tests (one per field) fail without the fix and pass with it. Full ci_scripts/pre-push.sh: 8371/8371 tests passed. 15 more sites remain across other files for future batches.
  • rb a-20260914-43 (#890, continued): fixed 4 more malformed-opening-tag call sites - BACnetConfirmedPrivateTransferACK::Decode (resultBlock[2], last field), BACnetConfirmedPrivateTransferRequest::Decode and BACnetUnconfirmedPrivateTransferRequest::Decode (serviceParameters[2], last field), and BACnetTextMessage::Decode (messageClass[1]). The three last-field sites had the “silent short decode” severity shape identified in a-20260914-41.
    • Verified: 3 new discriminating tests fail without the fix and pass with it. BACnetTextMessage’s fix has no matching end-to-end test - its cascade lands on a mandatory primitive field whose own failure sets error regardless of this fix, the same asymmetry documented in a-20260914-39; disclosed rather than shipping a non-discriminating test. Full ci_scripts/pre-push.sh: 8374/8374 tests passed. 11 more sites remain for future batches.
  • rb a-20260914-45 (#890, continued): fixed 4 more malformed-opening-tag call sites - BACnetAuthorizationEvent::Decode (client[2], token[3]), BACnetColorCommand::Decode (targetColor[1]), BACnetCOVNotificationMultipleRequest::Decode (timestamp[3]). targetColor had the “silent short decode” severity shape (no mandatory field follows) identified in a-20260914-41.
    • Verified: 3 new discriminating tests fail without the fix and pass with it. BACnetAuthorizationEvent’s client/token fixes have no matching end-to-end test - both cascade into a mandatory primitive field whose own failure sets error regardless of the fix, disclosed per the a-20260914-39 pattern. Full ci_scripts/pre-push.sh: 8376/8376 tests passed. 7 more sites remain for future batches.
  • rb a-20260914-47 (#890, final batch - completes the 26-site audit): fixed the remaining 5 malformed-opening-tag call sites - BACnetCreateObjectRequest::Decode (listOfInitialValues[1]), BACnetEventNotificationRequest::Decode (eventValues[12]), BACnetGetEnrollmentSummaryRequest::Decode (enrollmentFilter[1]), BACnetSCHubConnection::Decode (error[2]), BACnetSCHubFunctionConnection::Decode (error[6]). All 5 had the “silent short decode” severity shape identified in a-20260914-41.
    • Verified: 5 new discriminating tests fail without the fix and pass with it. One test’s own draft caught and fixed a self-inflicted false-positive hazard during this batch (a scan-from-offset-0 approach matched a coincidental byte inside a URI string rather than the intended field’s real tag). Full ci_scripts/pre-push.sh: 8381/8381 tests passed.
    • This completes #890’s originally-scoped 26-site audit. A follow-up audit pass (during this batch’s review) found 2 more sites of the identical defect shape in BACnetAccessRule.cpp, outside #890’s original scope - filed as a new issue for a future batch.
  • rb a-20260914-49 (#1932): BACnetAccessRule::Decode’s time-range[1] and location[3] optional fields had no guard against a present-but-malformed opening tag - both relied only on an error.IsSet() check, which cannot catch a malformed OPENING tag specifically since the shared decode helper resets error on that path. Added the tag-number-peek guard used throughout #890’s 26 sites to both.
    • Neither site has a matching end-to-end discriminating test: both cascades land on a mandatory primitive-typed field whose own failure sets error regardless of the fix, the same asymmetry documented for BACnetAccessToken; disclosed rather than shipping non-discriminating tests. Full ci_scripts/pre-push.sh: 8381/8381 tests passed.
  • rb a-20260914-5 (#1115, CR-F09-007): BACnet/SC hub connector’s reconnect-timeout backoff used a single shared timer field for both the primary and failover hub websockets, so the primary hub’s own backoff (armed by the primary’s failure) could incorrectly gate the immediate primary-to-failover crossover Annex AB.6.1 requires. Per owner ruling (2026-09-13, “option 1, per-hub reconnect timers” - a prior single-shared-timer attempt was reverted because it broke the crossover), the timer is now split into independent m_wsReconnectTimeoutTimerPrimary/m_wsReconnectTimeoutTimerFailover fields, routed by websocket identity; HandleTryFailover/HandleWaitFailover now gate on the failover hub’s own timer the same way the primary path already did, while the crossover itself stays immediate.
    • Verified: revert-red on the new test BACnetSCLoop_FailoverReconnectRetriesUsingConfiguredReconnectTimeoutIndependentlyOfPrimary (stubbing the new HandleTryFailover gate with if (false) fails it; restoring the gate passes it). ci_scripts/pre-push.sh full suite: 8342/8342 tests passed.
  • rb a-20260914-52 (#995): implemented clause 13.4.4’s no-overlap rule for LifeSafetyPoint/LifeSafetyZone objects - a FAULT_LIFE_SAFETY fault algorithm’s pFaultValues must share no value with its own object’s CHANGE_OF_LIFE_SAFETY intrinsic event algorithm’s pAlarmValues/pLifeSafetyAlarmValues; nothing previously enforced this. BACnetDBDevice::EnableFaultLifeSafetyAlgorithm and EnableIntrinsicChangeOfLifeSafetyAlgorithm now cross-validate against each other’s already-armed values, order-independently, before either algorithm object is created - a rejected call leaves no orphan algorithm behind.
    • Verified: 3 new tests (both rejection directions plus a disjoint-values control) pass; revert-red confirms both rejections are load-bearing. Full ci_scripts/pre-push.sh: 8384/8384 tests passed. This completes #995 - its other item (BUFFER_READY’s timeDelay) was already correctly documented as inert, needing no code change.
  • rb a-20260914-54 (#1053): added the 11 missing intrinsic-reporting/forwarding property cases (Member_Of, Event_Detection_Enable, Event_Enable, Acked_Transitions, Notify_Type, Event_Time_Stamps, Event_Message_Texts, Event_Message_Texts_Config, Reliability_Evaluation_Inhibit, Audit_Level, Auditable_Operations) to GetDefaultPropertyProfileForAuditLog - all 11 previously fell through to default: return false, so none of them could be read/written/reported through the profile mechanism. Datatypes/writability mirror the same properties on the Escalator/Accumulator/Audit Reporter siblings, per 135-2024 Table 12-83 (cl. 12.64).
  • rb a-20260914-54 (#1884): BACnetDataLinkIPv4 now tracks an optional local subnet mask (SetLocalSubnetMask, alongside the existing SetLocalAddress) and uses it to compute this device’s directed-broadcast address, screening it in the Forwarded-NPDU originating-device check the same way #1096 already screens multicast - closing the gap #1096’s owner ruling explicitly flagged as a follow-up. The screening only fires once both the local address and subnet mask are known; unset (the default), behavior is unchanged from before this issue.
    • Verified: 6 new tests (3 for #1053: the 11-property resolution, plus the existing required-property and a negative-control test; 3 for #1884: directed-broadcast rejection, a just-below-broadcast control, and an unknown-subnet control) pass; revert-red confirms both fixes are load-bearing. Full ci_scripts/pre-push.sh: 8389/8389 tests passed. Two-dimension single-reviewer pass: 0 defects.
  • rb a-20260914-56 (#1236): closed out the last 3 code items of the F05 (APDU layer) code-review hygiene checklist.
    • CR-F05-012: BACnetAbortReason/BACnetRejectReason’s instance surface (Clone/GetName/GetEnumValueString/round-trip encode) had zero direct call sites and zero tests. Kept for API-consistency with sibling enum-wrapper classes and given the same direct test coverage those siblings already have.
    • CR-F05-018: BACnetRawConfirmedServiceRequest’s (choice, payload) constructor silently dropped a payload SetServiceBytes rejected (over the length cap, or a NULL pointer paired with a non-zero length) with no way for the caller to detect it. The constructor now marks the object invalid in exactly that case, so IsValid() reports the failure.
    • CR-F05-021: BACnetUnconfirmedRequestPDU::Decode/DecodeUsingSupportedServices duplicated their entire header-parse block verbatim. Factored into a private DecodeHeaderAndServiceChoice helper; pure refactor, no behavior change.
    • CR-F05-006 confirmed independently tracked at #1683 (no code change here).
    • Verified: 2 new test files (AbortReason, RejectReason) plus 1 new test for the constructor fix; revert-red confirms both code fixes are load-bearing (and that the #1236 refactor’s shared helper genuinely shares code across both entry points, not just visually similar duplication). Full ci_scripts/pre-push.sh: 8402/8402 tests passed.
  • rb a-20260914-58 (#1840, #152 part A): added a whole-server-path peak-heap regression test for the segmented ReadProperty(object-list) response - the existing Issue820_* tests in tests/TestBACnetSegmentedObjectListTransmit.cpp only measured the segment-SEND half (RunAwaitResponseState, starting from an already-built ACK) and only RecordProperty()’d a number with no bound, so a regression that restored a full extra copy would have passed CI silently. tests/TestBACnetSegmentedObjectListPeakHeap.cpp measures the processor (BuildObjectList) + ACK-encode stage AND the segmented-transmit stage in one continuous MemTracker window, for 703 and 2000 objects, and asserts a hard peak-byte bound (12.5x the encoded APDU length +3000 bytes overhead, ~25-27% headroom over the measured ratios) plus an absolute check against the customer’s own 63,500-byte heap.
    • Test-only, no production code change.
    • Verified: the bound is load-bearing (an intentionally oversized extra buffer injected at the encode site makes it fail, while a small-property control test keeps passing); the issue’s own suggested revert-red technique (“revert #820 locally”) turned out to be a dead-code path for this specific request shape - disclosed and investigated rather than assumed, see the test file’s own header comment. Full ci_scripts/pre-push.sh: 8405/8405 tests passed.
  • rb a-20260914-60 (#1840, correction): fixed a scope-leak in the peak-heap regression test added by a-20260914-58 - the MemTracker measurement window included one-time device-population cost (AddObject x N) alongside the per-request ReadProperty processing cost it was meant to bound, inflating the measured peak roughly 4x (703 objects: 37021 measured vs. a corrected 8805 bytes). Population now runs before the window opens. The corrected ratio (~2.3-2.5x the encoded APDU length) matches #1841’s own independent prediction almost exactly. Tightened the bound from 12.5x+3000 to 2.8x+800 (same ~21-27% headroom philosophy) against the corrected numbers.
    • Test-only, no production code change.
    • Verified: re-ran the oversized-buffer revert-red against the new bound - both bound assertions fail, the control test stays green. Full ci_scripts/pre-push.sh: 8405/8405 tests passed.
  • rb a-20260914-62 (#1842, #152 part C “Crash 1”): re-ran the original MSVC Debug crash scenario (ReadProperty of a large Device object-list, segmented response) against the current tree under CRT debug-heap validation - never done since the original 2000-object access violation was reported. tests/TestBACnetObjectListProcessPath.cpp drives a FULL segmented exchange (every window ACKed, to completion at IDLE) for 2000 and 2500 objects, plus 703-object and 10-object unsegmented control arms, with _CRTDBG_ALLOC_MEM_DF | _CRTDBG_CHECK_ALWAYS_DF gated to _MSC_VER && _DEBUG.
    • Test-only, no production code change.
    • Result: built and ran under MSVC Debug|x64 (v143 toolset). All 4 tests passed, no heap corruption detected. Disclosed gaps: no Win32 (x86) build configuration currently exists anywhere in this repo’s MSVS projects - only Debug|x64 - so the ORIGINAL crash’s exact platform could not be tested; Application Verifier/PageHeap was not additionally run given the CRT debug-heap pass already took ~6.5 minutes. Full ci_scripts/pre-push.sh (Linux): 8409/8409 tests passed.
  • rb a-20260914-64 (#1599, scoped): fixed api.ts’s hand-maintained BACnetStackSetting copy, which had only 1 of the public header’s 4 entries despite its doc comment claiming to mirror CASBACnetStackDLL.h - a Node caller had no way to name the three Sprint 82 Device_Address_Binding heartbeat tunables (DABHeartbeatIntervalSeconds, DABRangeClusterTolerance, DABMaxMissesBeforeStale). Scoped to just this drift fix per the issue’s own triage note; its larger scope (generator-driven enum emission for every language binding) remains open.
    • Verified: whole-project tsc --noEmit clean, generate-node-adapter.js --check confirms no drift, other node-adapter gates unaffected. Full ci_scripts/pre-push.sh: 8409/8409 tests passed.
  • rb a-20260914-66 (#1849, #493 part B): BACnetWritePropertyMultipleProcessor now records Value_Source, matching the WriteProperty path - per cl. 19.5.1.3, WritePropertyMultiple must also capture the requester’s identity when it commands a stack-held object’s property, but the processor always passed NULL to BACnetBusinessLogic::SetProperty. Extracted the existing “build a BACnetValueSource from networkParameters” logic out of BACnetWritePropertyProcessor::Process into a shared static helper (BuildCommandSourceFromNetworkParameters) both processors call, avoiding a second copy; threaded networkParameters through BACnetWritePropertyMultipleProcessor::Process and its one caller.
    • Verified: 3 new tests (a real WritePropertyMultiple end-to-end capture, a NULL-networkParameters control, and a cross-processor equality check proving WriteProperty and WritePropertyMultiple build the identical BACnetValueSource). Revert-red confirms the fix is load-bearing. Full ci_scripts/pre-push.sh: 8412/8412 tests passed.
  • rb a-20260914-68 (#1848, #493 part A, cl. 19.5 DS-VSI-B): Access Door now wires up the Value_Source mechanism (Value_Source, Value_Source_Array, Last_Command_Time, Command_Time_Array), mirroring BACnetStackLightingOutput’s existing implementation - WritePresentValue now stashes the requester’s BACnetValueSource/timestamp, which WriteArraySlotValue/WriteArraySlotNull record via a new BACnetStackValueSourceState member. Scoped to Access Door only; Binary Lighting Output (also named in #1848) is deferred to a follow-up batch given its own WARN/blink-warn egress-timer complexity.
    • Factored the read-side derivation of these four properties out of BACnetDBDevice.cpp’s previously Lighting-Output-only inline block into a shared static helper, BACnetStackObjectHelpers::GetValueSourceProperty, now used by both object types (non-behavior-changing extraction, confirmed by an independent reviewer pass).
    • Design note: an initial draft mirrored Lighting Output’s WARN-egress arm-time snapshot for Access Door’s pulse-timer auto-relinquish, to attribute the eventual relinquish to whoever issued the pulse. Traced BACnetStackValueSourceState::ClearSlot (the function the pulse’s relinquish path calls) and found it takes no source/time argument at all and unconditionally clears both fields - so, unlike Lighting Output’s egress (which can resolve to either a real value write that consumes such a snapshot, or a relinquish that doesn’t), Access Door’s pulse can only ever resolve to a relinquish, making the snapshot dead code. Removed before writing a test for it.
    • Verified: 4 new tests (a real write end-to-end capture with relinquish clearing it back to none, a no-network-parameters control, a distinct-priority-doesn’t-disturb-other-slots control, and an initial-state control). Revert-red confirmed twice (before and after the snapshot-removal simplification) - identical results both times. Full ci_scripts/pre-push.sh: 8416/8416 tests passed. Reviewed by an independent subagent pass; one cosmetic stale-comment fix applied.
  • rb a-20260914-7 (#1067, CR-F03-030): BACnetPropertyStates’s cl. 21.6 CHOICE arms sc-connection-state [258]/sc-hub-connector-state [259] were only reachable as an opaque extended-value[63] primitive, unlike every other named arm. Owner-ruled (2026-09-13, “always follow the spec”, option (a)): Decode() now decomposes an extended-value sum whose tag is exactly 258/259 into the existing BACnetSCConnectionState/BACnetSCHubConnectorState typed wrappers; Encode() composes them back onto the wire.
    • Verified: revert-red on both the decompose and compose blocks (stubbed each with if (false) independently; the relevant new tests fail each way). Full ci_scripts/pre-push.sh: 8349/8349 tests passed.
  • rb a-20260914-7 (#1836, CR-F10-009): BACnetBusinessLogic::GetProperty’s GetPropertyProfile-fails branch (marked “should not be possible”) was the only parameter/existence check in the function that left errorContainer unset on return false, so a caller treating “false with no error” as a whole-request failure (rather than a per-property error) would lose an entire ReadPropertyMultiple response instead of reporting one property as an error. Now sets Error(property, unknownProperty) before returning, matching every sibling check.
    • Verified unreachable through the current BACnetDBDevice/BACnetStackObjectSetting/BACnetDBObject implementation (all three enabled/profile tiers share the same underlying storage, so “enabled” always implies a successful profile lookup at that tier); hardening only, no test added, scoped to the current implementation not a permanent guarantee.
  • rb a-20260914-70 (#1848, #493 part B, cl. 19.5 DS-VSI-B): Binary Lighting Output now wires up the Value_Source mechanism (Value_Source, Value_Source_Array, Last_Command_Time, Command_Time_Array), closing #1848 (the Access Door half was completed earlier in batch a-20260914-68, PR #1953). Adds m_valueSourceState/m_pendingCommandSource/m_pendingCommandTime, consumed by WriteArraySlotPV/WriteArraySlotNull - the same shape as Access Door’s simpler case.
    • UNLIKE Access Door (which has no egress-timer mechanism, so an analogous snapshot there was found to be dead code and removed), Binary Lighting Output has a real WARN_OFF/WARN_RELINQUISH blink-warn egress-timer (cl. 12.55.4/12.55.10.1) whose WARN_OFF path resolves via WriteArraySlotPV(priority, off) - a REAL value write that DOES consume an attribution. Ported BACnetStackLightingOutput’s existing m_egressCommandSource/m_egressCommandTime snapshot-and-restore pattern (originally added there after a two-round adversarial review) so the eventual egress resolution attributes to whoever ARMED it, not to whatever m_pendingCommandSource holds when the timer later expires or gets halted mid-write by an unrelated client’s own write.
    • Reuses the shared BACnetStackObjectHelpers::GetValueSourceProperty read-helper (added in the Access Door batch) for the read-side dispatch - no new read logic. BACnetDBPropertyProfile.cpp needed no change - the 4 properties were already declared there.
    • Verified: 9 new tests (3 direct-write unit tests, 2 egress-attribution tests mirroring Lighting Output’s own two-round-reviewed pair, 4 end-to-end WriteProperty tests). Revert-red confirmed: disabling the two capture hooks failed exactly the 6 source-dependent tests while the 3 controls stayed green. Full ci_scripts/pre-push.sh: 8425/8425 tests passed. Independent reviewer pass: all 8 scrutiny points confirmed, no findings.
  • rb a-20260914-72 (#1850, #493 part C, cl. 19.5.1.3): the device that commanded a priority may now directly rewrite that priority’s Value_Source, matching the spec’s write-back exception - previously every Value_Source write returned WRITE_ACCESS_DENIED unconditionally, since the property profile marks it non-writable on every commandable object with no carve-out for the commanding device.
    • Fixes a real design bug flagged in the issue: BACnetStackValueSourceState::IsOwnedBy (previously uncalled) compared the write requester against the DISPLAYED Value_Source, which the owner’s own write-back is allowed to change - so an owner’s second write-back would incorrectly self-deny after the first one changed the displayed value. Adds a separate m_commandingRequesterArray, set alongside the displayed array by RecordCommand, cleared by ClearSlot, and never touched by a direct write - IsOwnedBy now compares against this instead.
    • BACnetStackValueSourceState::WriteValueSourceDirect(priority, requester, newDisplayedValue) is the new mutation entry point: ownership-checked, mutates only the displayed value (Last_Command_Time unaffected per cl. 19.5.1.4), and rejects a never-commanded slot (which IsOwnedBy alone would allow, for a different reason - explicitly guarded, not delegated). Thin forwarding wrappers added to BACnetStackLightingOutput, BACnetStackBinaryLightingOutput, and BACnetStackAccessDoor.
    • BACnetBusinessLogic::SetValueSourceProperty intercepts a valueSource write in SetProperty’s existing writable-check special-case slot (before the generic denial), dispatching per object type; valueSourceArray stays unconditionally read-only (cl. 12.26.41), untouched by this change. An unspecified priority defaults to 16 (cl. 15.9.1.1.5), applied locally since Value_Source isn’t “commandable” in the sense BACnetWritePropertyProcessor’s own default-priority logic checks. writeAccessDenied is used uniformly for both the not-owner and never-commanded-slot rejections (the spec only says “an error” - disclosed choice).
    • Verified: 26 new tests (6 unit-level BACnetStackValueSourceState tests, 5 Lighting Output end-to-end WriteProperty/WritePropertyMultiple tests including a two-write ownership-retention test, 1 Access Door end-to-end variant). Revert-red confirmed: short-circuiting the interception failed exactly the 4 owner-write-must-succeed tests while every denial-expecting control stayed green. Full ci_scripts/pre-push.sh: 8437/8437 tests passed. Independent reviewer pass: all 8 scrutiny points confirmed, no findings.
  • rb a-20260914-74 (#1852, #493 part E, cl. 19.5 non-commandable objects): Life Safety Point and Life Safety Zone now capture and serve Value_Source for their non-commandable Mode property, and support the cl. 19.5.1.3 write-back exception - the device that wrote Mode may later rewrite Value_Source directly; every other device gets WRITE_ACCESS_DENIED.
    • Unlike every previously-wired object type (Lighting Output, Binary Lighting Output, Access Door - all commandable, 16-slot priority arrays), Mode has no priority array, so per cl. 19.5.1.4/.1.5 there is no Value_Source_Array/Last_Command_Time/Command_Time_Array at all for these two types - just a single Value_Source scalar. Reuses #1850’s requester-vs-displayed-value ownership model, adapted to one slot instead of sixteen: BACnetStackLifeSafetyObject (the shared base both Point and Zone derive from) gains m_valueSourceDisplayed/m_valueSourceRequester, a single-slot WriteValueSourceDirect, and a commandSource parameter threaded through SetCoreProperty’s Mode-write path.
    • The property profile for these two types already declared only valueSource (never the other three) before this batch, so no profile change was needed - a ReadProperty of Value_Source_Array/Last_Command_Time on either type already correctly returns unknownProperty.
    • Caught and fixed during implementation: BACnetValueSource’s default constructor leaves its choice at noChoice (via Reset()), not none (a separate state, only set by SetNone()) - every value-source field elsewhere in this codebase explicitly calls both; the new fields needed the same explicit SetNone() in the constructor to read none before any write, rather than relying on default construction.
    • Extends BACnetBusinessLogic::SetValueSourceProperty (built for #1850) with 2 more dispatch blocks for lifeSafetyPoint/lifeSafetyZone, omitting the priority argument the other 3 blocks pass (Mode has no priority concept).
    • Verified: 7 new tests (an unknownProperty control proving the no-array shape, a real Mode-write capture test, a no-network-parameters control, a two-write ownership-retention test, a non-owner-denied control, and a Zone variant). Revert-red confirmed: disabling the Mode-write capture failed exactly the 4 capture-dependent tests while every other control stayed green. Full ci_scripts/pre-push.sh: 8444/8444 tests passed. Independent reviewer pass: all 8 scrutiny points confirmed, no findings.
  • rb a-20260914-9 (#1230, CR-F29-009): Load Control’s Duty_Window was never reset on any SHED_INACTIVE-entering transition, contrary to cl. 12.28.13’s “reset by the device to this pre-agreed value” requirement (Shed_Duration’s matching cl. 12.28.12 requirement was already fixed in PR #1362). Owner-ruled (2026-09-13, “follow the spec”): added an internal m_dutyWindowPreAgreed (defaults to 0, no customer/test-tool header change), set on every TransitionToInactive() edge (CancelShed, FinishedSuccessfulShed, FinishedUnsuccessfulShed, forced Enable=FALSE) - a subsequent Start_Time-only re-shed with no Duty_Window write naturally carries the pre-agreed value.
    • Verified: revert-red (reverting the reset line fails all three updated/added test assertions). Full ci_scripts/pre-push.sh: 8349/8349 tests passed.
  • rb a-20260921-1 (#2225): BACnetInterface::Tick()’s ingress buffer (BACNET_INTERFACE_MAX_INPUT_BUFFER_LENGTH) now accepts a maximum-size, spec-conformant BACnet/SC frame. ANSI/ASHRAE 135 Annex AB requires a BACnet/SC device to relay BVLC-SC messages up to at least 1600 octets, but the constant was pinned at BACNET_NPDU_MAX_LENGTH (1497) — short even of the generic encoded-frame ceiling BACNET_ENCODED_FRAME_MAX_LENGTH (1529) by the data-link header overhead, let alone SC’s larger minimum. BACnetInterface::ProcessForPort()/Process() had their own, separately-pinned BACNET_ENCODED_FRAME_MAX_LENGTH ceiling checks, so bumping only the buffer would have left a 1530-1600 octet frame passing Tick() and then silently dropped one call later. BACNET_INTERFACE_MAX_INPUT_BUFFER_LENGTH is now 1600 octets when STACK_OPTION_DATA_LINK_LAYER_SC is compiled in (matching BACnetSCConstants::BACNET_SC_MINIMUM_BVLC_LENGTH) and BACNET_ENCODED_FRAME_MAX_LENGTH (1529) otherwise, and both ProcessForPort()’s and Process()’s ceiling checks now bound against it instead — revert-red: with the fix reverted, a 1590-octet Annex-AB-conformant Encapsulated-NPDU frame driven through Tick()’s real ProcessForPort dispatch logged "BACnetInterface::ProcessForPort() - a supplied length exceeds its buffer's declared capacity" and never reached HookIAm; with the fix restored the same frame is processed end-to-end and a 1601-octet frame (one octet past the new ceiling) is still correctly rejected.
  • rb a-20260921-1 (#2224 test-fixture follow-up): #2224’s production fix (commit 515b4f0db, same branch) made BACnet/SC egress correctly report the real Network Port object instance via GetNetworkPortInstanceForSend() instead of the old BACnetPacket::NetworkType_SC (2) transport-type byte. That broke 45 pre-existing tests that had hardcoded the old, buggy value as their expectation: 41 in tests/TestBACnetPhysicalRouterLivePath.cpp (BACnetPhysicalRouterLivePathTest, BACnetPhysicalRouterNoClockTest, BACnetSecureConnectRouterTest — via the file’s stale SC_EGRESS_NETWORK_PORT_INSTANCE constant, which was still pinned to NetworkType_SC) and 4 in tests/BACnetSC_Tests/TestBACnetSCHubConnector.cpp (asserting the captured networkPortInstance equalled NetworkType_SC for a fixture with no configured Network Port object, which now correctly reports the BACNET_UNKNOWN_DEVICE_INSTANCE sentinel instead). Also caught in the same sweep: tests/TestBACnetPhysicalRouterLivePath.cpp’s MSTP_EGRESS_NETWORK_PORT_INSTANCE constant, broken by the same #2224 commit converting BACnetDataLink::SendOutgoingPacket’s generic (MS/TP) path to GetNetworkPortInstanceForSend() — which for MS/TP’s un-overridden base implementation now returns BACNET_UNKNOWN_DEVICE_INSTANCE rather than the old NetworkType_MSTP (3) byte. Test-only: no production behaviour changed by this entry; every updated assertion now expects the value the router genuinely emits post-#2224, not a weakened check. TestBACnetPhysicalRouterLivePath.cpp additionally gained an IsScNetworkPortInstance() helper so EgressNpduOf/CountEgressToPeer/FirstEgressToPeer recognize either of BACnetSecureConnectRouterTest’s two real, now-DIFFERING SC port instances (2001 and 2003) as “an SC egress” while still telling them apart by peer connection string, as before. Revert-red: reverting just the two test files (scratch, not committed) while keeping the #2224/#2225 source fixes reproduced exactly the same 45 failures (41 + 4) the parent session’s bisection found; restoring the fix returned all 196 tests in the four affected fixtures to green, and the full ci_scripts/pre-push.sh gate is green at 8664/8664.
  • rb a-20260921-3 (#2214): BACnetDataLinkLayer::SendRoutedPacket’s MS/TP egress path now resolves the correct physical MS/TP data-link instance instead of always sending from instance 0. The switch (egressNetworkType) had a fully-worked-out NetworkType_IP case (#2037) — resolve the egress port’s own instance via FindIpv4DataLinkByRouterPort, and, when that fails, re-derive the port’s target network number from the physical router and re-run ResolveIpv4RouterPortBinding, REFUSING the send (rather than silently falling back to instance 0) if the resolved instance is unresolvable or IsRouterPortAmbiguous() — but MS/TP fell into a bare default: comment (“MS/TP is addressed by the connection string already set above”) that did nothing, and ProcessOutgoingPacket’s own NetworkType_MSTP case unconditionally called this->m_dataLinkMstp[0].ProcessOutgoingPacket(...), never even looking at its networkPortInstance parameter. #2153/#2154/#2155 had already built every piece of infrastructure this needed (the m_dataLinkMstp[] array, HasNetworkPort/TrackNetworkPort/GetNetworkPortObjectInstance, FindMstpDataLinkByRouterPort, FindMstpDataLinkByNetworkPortInstance, ResolveMstpRouterPortBinding/ApplyMstpRouterPortBinding) but nothing wired them into the two functions that actually send a routed packet. Fix: SendRoutedPacket gained a real case BACnetPacket::NetworkType_MSTP: mirroring the IP case’s structure and its ambiguity-refusal reasoning byte-for-byte (substituting the MS/TP equivalents #2155 added), explicitly NOT the simpler BACnet/SC case’s instance-0 fallback — SC’s own comment already documents that gap as a pre-existing limitation, not a pattern to extend. The IP case’s single ipv4RoutedNetworkPortInstance local (previously passed unconditionally to the final ProcessOutgoingPacket call regardless of which arm ran) is renamed routedNetworkPortInstance, since it now carries an MS/TP instance too. ProcessOutgoingPacket’s NetworkType_MSTP case now mirrors its own NetworkType_IP case: defaults to &this->m_dataLinkMstp[0], but resolves the real claimed instance via FindMstpDataLinkByNetworkPortInstance when networkPortInstance != BACNET_UNKNOWN_DEVICE_INSTANCE. Deliberately NOT the generic FindDataLinkByNetworkPortInstance the IP case uses for this same lookup: that shared helper’s own header comment records it does not search m_dataLinkMstp at all (“wiring MS/TP into that consumer path” was explicitly left to a later issue that never happened) — routing through it here would have silently resolved nothing and reproduced the exact bug this fix closes. FindMstpDataLinkByNetworkPortInstance (#2154’s dedicated MS/TP mirror of FindIpv4DataLinkByNetworkPortInstance) needed no such detour. Test added: tests/TestBACnetPhysicalRouterLivePath.cpp’s BACnetPhysicalRouterLivePathTest gained RoutedEgressOnASecondMstpInstance_ReachesThatInstanceNotInstanceZero (discriminating) and RoutedEgressOnASingleUnclaimedMstpPort_StillFallsBackToInstanceZero (control). MS/TP egress cannot be told apart by this file’s usual networkPortInstance-based FirstEgressOn/CountEgressOn machinery — BACnetDataLink::GetNetworkPortInstanceForSend’s base implementation, which BACnetDataLinkMSTP does not override, reports the same BACNET_UNKNOWN_DEVICE_INSTANCE sentinel for every MS/TP instance regardless of which one actually sent a frame (a separate, pre-existing gap this issue does not touch). The discriminating test instead claims two real MS/TP instances, binds each to its own router port, then Reset()s instance 0 (dropping its m_callbacks, among other service pointers, without touching instance 1’s) so instance 0 categorically cannot send anything — the pre-fix code’s unconditional m_dataLinkMstp[0].ProcessOutgoingPacket(...) call would fail under this sabotage no matter which network the message was routed to, while the fixed code resolves the egress port to instance 1 and succeeds. Revert-red: reverting just the source fix (scratch, not committed) and rerunning ci_scripts/dev-build.sh 'BACnetPhysicalRouterLivePathTest.*Mstp*' reproduced exactly the pre-fix symptom — BACnetDataLink.cpp(536) - Error: Callbacks does not exist bubbling up through BACnetDataLinkMSTP::ProcessOutgoingPacket and BACnetDataLinkLayer::SendRoutedPacket to BACnetNetworkLayer::ForwardNpduOutPort, and the new test failing with Value of: egress != __null / Actual: false / Expected: true; restoring the fix returned it to green, and the full ci_scripts/pre-push.sh gate is green: 8666/8666 tests passed, all gates passed.
  • rb a-20260921-4 (#2221): a ReadRange of a Trend Log, Trend Log Multiple, or Event Log’s Log_Buffer with the BY_POSITION range form never carried FirstSequenceNumber in the ACK, even though 135-2024 15.8.2 requires it whenever the referenced property is a Log_Buffer and the device supports sequence numbers (BY_SEQUENCE_NUMBER and BY_TIME already set it correctly). Fixed in BACnetStackTrendLog, BACnetStackTrendLogMultiple, and BACnetStackEventLog’s HandleReadRangeRequestByPosition.
  • Investigated the accompanying report that BY_SEQUENCE_NUMBER returns ItemCount 0 for every sequence number. Could not reproduce against this codebase: exhaustive tracing of HandleReadRangeRequestBySequenceNumber in all three classes, the existing 20+ BySequenceNumber unit tests, and a new test built from the exact live-device numbers in the report (Record_Count 60, Total_Record_Count 237, non-evenly-wrapped 60-record buffer, reference 178) all show correct, non-empty results. No fix applied for that half of the report; likely observed against firmware predating this repository’s existing Issue #602/#603 ReadRange fixes.
  • rb a-20260921-5 (#2201): DecodeAs(JSON) of a Group object’s Present_Value (135-2024 12.14.4, a BACnetLIST of ReadAccessResult - the same structure an RPM ack carries) lost every nested ObjectIdentifier/PropertyIdentifier to an empty UnknownAPDU, logging ObjectFactory() ... unknown datatype=[33]. BACnetAbstractSyntaxType::ObjectFactory had no case for ReadAccessResult, the datatype GetDefaultPropertyProfileForGroup wires to Present_Value; RPM’s own ack was unaffected because it builds ReadAccessResult objects directly and never goes through this factory. Added the missing case (mirrors the existing ReadAccessSpecification case).
  • rb a-20260921-7 (#2011, part A of #2006’s client-side segmented-send decomposition): implemented 135-2024 cl. 5.4.4.1’s CannotSend transition for an outgoing Confirmed-Request. This stack has no SendConfirmedSegmented code path (#2006/#2012, not implemented), so an oversized outgoing APDU previously failed silently - BACnetInterface::SendConfirmedRequest returned a bare false with no abort reason ever reaching the local application program (traced in the #2011 evidence comment; the outgoing-request encode-failure branch in BACnetPacket::Encode only logged “PANIC - should not get here”). BACnetInterface::SendConfirmedRequest/SendWriteProperty gained new, trailing, default-false destinationMaxSegmentsAcceptedIsKnown/destinationMaxSegmentsAccepted parameters (this stack has no automatic per-device cache of a destination’s Max_Segments_Accepted); when a caller supplies a known value and the encoded APDU cannot fit the send path’s real transmittable-length ceiling, a clean ABORT.indication (server=false, abort-reason=APDU_TOO_LONG) is now fired via the existing HookAbort callback instead of the previous silent failure. Every existing caller is unaffected (parameters default to “not known”, preserving prior behaviour exactly).
  • rb b-20260906-1 (#1282): BACnetDatabase::ReadTrendLogRecord no longer dereferences a NULL statusFlagsLength — a caller not interested in status flags (passing NULL for both statusFlags and statusFlagsLength, as BACnetInterface.ImportExportTrendLog does) segfaulted the whole process; revert-red: the filtered gtest run reproduces the identical make: *** [makefile:146: test-filter] Segmentation fault with the fix reverted, and passes with it restored.
  • rb b-20260906-10 (#1120): BACnetReadRangeRequest::SetRangeByPosition/SetRangeBySequenceNumber/SetRangeByDateTime now take count as int16_t, matching the public constructors — a negative Count no longer silently corrupts into a large positive value on the wire (135-2024 15.8.1.1.4.1.2).
  • rb b-20260906-10 (#1121): BACnetListOfReadAccessResults::Insert now rejects a NULL child, and Clone() fails atomically on any child clone failure instead of inserting NULL or reporting a truncated list as valid.
  • rb b-20260906-10 (#1122): BACnetListOfReadAccessSpecifications::Insert now rejects a NULL child, and operator= fails atomically on any child clone failure — the identical gap to #1121, fixed the same way.
  • rb b-20260906-10 (#1123): BACnetReadPropertyProcessor’s rpmAck out-of-memory branch now sets errorContainer.SetAbort(...) instead of leaving it unset, matching the two sibling OOM branches (#062, #681) already fixed in the same function.
  • rb b-20260906-11 (#1199): BACnetAuthRequestACK::Decode now clears validity before an early return on a null/zero-length buffer, instead of leaving a previously-decoded object’s stale IsValid()==true standing — matches #528’s fix for the sibling BACnetAuthRequest.
  • rb b-20260906-11 (#1200): BACnetAuthenticationClient::Decode — same fix, same shape.
  • rb b-20260906-11 (#1201): BACnetAuthenticationEvent::Decode — same fix, same shape.
  • rb b-20260906-11 (#1202): BACnetAuthenticationFactor::Decode — same fix, same shape.
  • rb b-20260906-11 (#1203): BACnetAuthorizationPolicy/BACnetAuthorizationScope/BACnetAuthorizationStatus’s copy paths now propagate a child Clone()/Insert() failure into the copy’s own validity instead of unconditionally reporting the source’s validity — matches #783’s established fix for the identical shape in BACnetAccessToken.
  • rb b-20260906-12 (#1056): BACnetAbstractSyntaxType::GetName() returned the misspelled literal "AbstractSyntaxtType" — corrected to "AbstractSyntaxType".
  • rb b-20260906-12 (#1058): BACnetPrimitiveBase::DecodeWithContext/EncodeWithContext now guarantee Reject(other)/Abort(other) is set when a derived Decode()/Encode() fails without setting an error itself, instead of returning 0 with no diagnostic. Revert-red: SilentFailurePrimitive-based tests fail on the pre-fix code.
  • rb b-20260906-12 (#1059): BACnetOptionalUnsigned::Decode now rejects a context-tagged buffer (Reject(invalidTag)) instead of silently misparsing it — these variants are application-tagged only.
  • rb b-20260906-12 (#1060): BACnetOptionalUnsigned::IsEqual no longer treats a Null value as equal to Unsigned(0) — both sides’ IsNull() are checked before comparing the underlying value.
  • rb b-20260906-12 (#1066): BACnetSequenceOf<T>::Decode and BACnetSequenceOfPropertyValue::Decode allocated each element with throwing new, making the following NULL check dead code — both now use new(std::nothrow). The BACnetSequenceOfPropertyValue test carries a documented limitation: BACnetPropertyValue::Decode()’s own adjacent nested allocations prevent MemTracker::SetFailAfter from isolating this specific line’s fix (see in-code comment); the BACnetSequenceOf<T> test does fully revert-red.
  • rb b-20260906-12 (#1068): BACnetDeviceObjectPropertyValue::SetComplete now requires deviceIdentifier/objectIdentifier/propertyIdentifier/propertyValue to all be valid before marking the object valid (matches #859’s fix for the sibling BACnetObjectPropertyValue). Revert-red confirmed.
  • rb b-20260906-13 (#1071): BACnetLogData.cpp’s timeChange string parse and DecodeDatatypeDataPairString’s datatype parse now use TryParse<T>() instead of the unchecked ToValue<T>() — malformed text (e.g. "3600junk", "1junk") now fails instead of being silently truncated. Completes #567/#619’s sweep for this file.
  • rb b-20260906-13 (#1069): SECURITY — a CharacterString with a non-UTF-8 declared encoding (or one that merely claims UTF_8 but is not actually well-formed) no longer forwards raw non-UTF-8 octets into JSON/XML output. Added ChipkinUtilities::SanitizeUtf8(), wired into JSONRenderer::WriteJsonString and XMLRenderer::WriteAttributeValue/WriteContent.
  • rb b-20260906-13 (#1070): SECURITY — XMLRenderer now strips XML 1.0-illegal C0 control characters (everything in 0x00-0x1F except tab/LF/CR) instead of forwarding them verbatim, which produced genuinely non-well-formed XML.
  • rb b-20260906-13 (#1235, CR-F04-004): ChipkinConvert::ToString(unsigned char*, length) now NULL-checks its buffer pointer, matching every sibling conversion function.
  • rb b-20260906-13 (#1235, CR-F04-005): removed dead, duplicate ChipkinUtilities::IsLeapYear/GetDaysInMonth (zero callers; BACnetDateUtils::IsLeapYear/LastDayOfMonth are the tested implementations).
  • rb b-20260906-13 (#1235, CR-F04-006): capped XMLRenderer::RenderElement/JSONRenderer::EmitNode recursion depth at 64 so a pathologically deep or cyclic IRenderable tree cannot exhaust the stack.
  • rb b-20260906-13 (#1235, CR-F04-008): removed dead ChipkinConvert::HexCharToInt (zero production callers; HexCharToIntChecked is used everywhere, and the unchecked form silently reintroduced #566’s exact bug for any future caller).
  • rb b-20260906-14 (#1127): WritePropertyMultiple now reports a Result(-) WritePropertyMultipleError (SERVICES/INVALID_TAG) instead of a bare Reject when a poisoned/out-of-range Priority is encountered after a prior write in the same request already succeeded — cl. 15.10.1.3.1/15.10.2.
  • rb b-20260906-14 (#1131): WritePropertyMultiple no longer silently drops the response (outgoingPDU left NULL) when a SetProperty failure after a successful prior write is Abort/Reject rather than Error-typed — now reports a Result(-) WritePropertyMultipleError (Abort maps to device/operationalProblem, Reject to services/invalidTag).
  • rb b-20260906-14 (#1133): BACnetWriteGroupRequest/BACnetGroupChannelValue::Decode() now validate the decoded writePriority/overridingPriority against 1..16 (cl. 15.11.1.1.2/15.11.1.1.3), closing an out-of-range-wire-value aliasing bug (e.g. 272 truncating to a legal 16).
  • rb b-20260906-14 (#1134): BACnetGroupChannelValue::Decode() now validates the decoded channel against 0..65535 (cl. 15.11.1.1.3), closing the same aliasing bug for Channel_Number matching.
  • rb b-20260906-14 (#1132): the cited defect (a mid-list decode failure in BACnetListOfWriteAccessSpecifications::Decode silently treated as success) no longer reproduces — already fixed under issue #1150 (commit c946c9d6). Left open for the owner to close as a duplicate/already-fixed finding.
  • rb b-20260906-15 (#1157): SECURITY — BACnetStackNotificationForwarder::m_pendingDeliveries now has a finite capacity (1000), rejecting new enqueues once at capacity instead of growing without bound on a burst of accepted event notifications.
  • rb b-20260906-15 (#1159): BACnetLifeSafetyOperationInfo::Decode now rejects the reserved LifeSafetyOperation range (10-63, cl. 21.2), mirroring BACnetLifeSafetyOperationRequest::Decode’s existing #840 check that this sibling class had been missed by.
  • rb b-20260906-15 (#1160): AcknowledgeAlarm now reports a Result(-) (SERVICES/serviceRequestDenied) instead of silently dropping the response entirely when CallbackAcknowledgeAlarm declines without setting an error code.
  • rb b-20260906-15 (#1162): BACnetGetEventInformationRequest::Decode now rejects a present-but-malformed lastReceivedObjectIdentifier instead of silently treating it as absent, confirming open issue #767.
  • rb b-20260906-16 (#1183): SECURITY — BACnetNotificationParameters (the CHOICE wrapper around every event-notification parameter variant) gained an explicit copy constructor and operator=; the implicit compiler-generated shallow copy aliased m_notificationParametersBase, so copying/assigning one instance and then destroying either led to a double-free/use-after-free of the other.
  • rb b-20260906-16 (#1184): BACnetNotificationParametersBufferReady::GetDatatype() and BACnetNotificationParametersFloatingLimit::GetDatatype() each returned a sibling’s datatype constant instead of their own, breaking the datatype gate check inside their IsEqual().
  • rb b-20260906-16 (#1185): BACnetNotificationParametersChangeOfValueNewValue::Decode now rejects a mistagged or opening/closing-tag encoding for the changed-bits [0]/changed-value [1] CHOICE alternatives instead of misrouting it into the primitive BitString/Real decoders, which silently misparsed it as valid content.
  • rb b-20260906-16 (#1186): BACnetNotificationParametersCommandFailure’s IsValid() no longer reports true after only Status_Flags is set — a new AreAllParametersValid() gate (matching every sibling in the family) requires Command_Value and Feedback_Value to also be set before the object is considered valid.
  • rb b-20260906-16 (#1349): BACnetNotificationParametersChangeOfLifeSafety::SetOperationExpected/Decode now reject the reserved LifeSafetyOperation range (10-63, cl. 21.2), mirroring the #840/#1159 fix already applied to BACnetLifeSafetyOperationRequest/BACnetLifeSafetyOperationInfo — a missed sibling filed from batch b-20260906-15’s review.
  • rb b-20260906-17 (#1111): BACnetSCConnectRequest/BACnetSCConnectAccept::SetMaximumBVLCLength/SetMaximumNPDULength now enforce symmetric, spec-defined bounds instead of a vacuous upper-bound-only (BVLC) or no-upper-bound (NPDU) check, matching BACnetSCHubConnector’s equivalent setters.
  • rb b-20260906-17 (#1112): BACnetSCAdvertisement::Decode now routes its four payload fields through the class’s own setters instead of assigning raw wire bytes directly, rejecting an out-of-spec HubConnectionStatus/AcceptDirectConnections value instead of storing it verbatim.
  • rb b-20260906-17 (#1114): BACnetStackNetworkPortSC::SetIssuerCertificateFiles now rejects vectors longer than 2 entries, matching the Issuer_Certificate_Files property’s fixed-size BACnetARRAY[2] definition (cl. 12.56.101).
  • rb b-20260906-17 (#1116): BACnetSCHeaderOption::MustUnderstand() is now documented as meaningful only for DestinationOptions entries; a new IsEverySegment() accessor reads the same header-marker bit under the name that applies to DataOptions entries.
  • rb b-20260906-17 (#1115): mirroring the primary-hub reconnect-timeout gate onto the failover-hub path regressed two existing tests that rely on an immediate cross-hub failover after a primary failure — m_wsReconnectTimeoutTimer is a single shared field, so gating both hubs identically blocks the very cross-over it should allow. Reverted before merge; posted as a question for the owner (needs a per-hub timer or an equivalent design decision).
  • rb b-20260906-2 (#907): BACnetInterface::SetBACnetSCCertificateFileObjects now discards its pending-certificate-changes transaction on every failure exit and logs via APIDebugLogError, so an unpaired BeginPendingCertificateChanges() cannot leave m_scChangesPending stuck true (the failure paths are currently unreachable — all five setters they guard return true unconditionally today — so this is defense-in-depth, not a currently-observable fix). The success path deliberately stays pending, matching the existing multi-call staged-transaction contract SCNetworkPortProperties_CertificateReferenceDiscardRestoresActiveValues already pins.
  • rb b-20260906-2 (#986): BACnetBusinessLogic::SendExternalWriteProperty and BACnetDBDevice::WriteValueToPropertyReferences gain a Null datatype case, so a Schedule’s Present_Value going Null (cl. 12.24.4) can relinquish a remote/external WriteProperty target instead of being rejected as an unsupported datatype — revert-red: the new test fails (“unsupported datatype”, no message sent) with the Null case removed, and passes with it restored.
  • rb b-20260906-3 (#1034): BACnetDBDevice::GetNetworkPortObjectInstanceFromNetworkParameters gains a NetworkType_SC case, so a multi-network-port device that includes a BACnet/SC port can resolve NetworkPort,4194303 (cl. 15.5.2) for a request arriving over SC instead of aborting.
  • rb b-20260906-3 (#1035): EnableNetworkPortEthernetProperties/MSTPProperties/VirtualProperties/SecureConnectProperties now validate networkNumber/networkNumberQuality the same way EnableNetworkPortIPv4Properties already does, instead of silently substituting a default when the constructor rejects an invalid value internally.
  • rb b-20260906-3 (#1036): EnableLiftOrEscalatorProperties no longer dereferences a NULL groupMembers value on an allocation failure while adding a lift/escalator to an elevator group.
  • rb b-20260906-4 (#1103): BACnetPhysicalRouter::AllowReject (the #471 reject-rate limiter) now keys its per-source bucket on the sender’s identity prefix rather than the full BACnet/IP connection string (IP+UDP port), matching #427’s fix for the sibling held-message quota — a host could previously claim a fresh 4-token burst per source UDP port.
  • rb b-20260906-4 (#1104): a virtual-router-only device’s cl. 6.6.3.1 Reject-Message-To-Network is now actually sent — it was fully built and marked valid but the error state was never surfaced to the caller, so every caller silently discarded it as an internal error.
  • rb b-20260906-4 (#1105): the same reject now routes to a routed originator’s SNET/SADR (when the rejected NPDU already carries one) instead of always answering the immediate ingress peer, matching the physical-router path’s existing routing logic.
  • rb b-20260906-4 (#1107): BACnetHostNPort::operator= gains a self-assignment guard, matching the #396 pattern already used by its sibling BACnetHostAddress::operator=.
  • rb b-20260906-5 (#1100): BACnetBDTEntry::Decode now rejects a present-but-undersized broadcast-mask instead of accepting it; operator=/Clone() always copied a fixed 4 bytes out of the decoded octet string, a heap over-read if the wire supplied fewer (a genuinely absent mask, legal for BACnet/IPv6 entries, still decodes fine).
  • rb b-20260906-5 (#1101): BACnetFDTEntry::Decode now rejects an ipAddress whose length is neither of the two spec-legal lengths (6-octet B/IP, 18-octet B/IPv6); operator=/Clone()/SetIpAddress now use the address’s actual length instead of a hardcoded 6, which previously both truncated legitimate 18-byte IPv6 entries and over-read anything decoded shorter.
  • rb b-20260906-5 (#1102): Network Port BBMD_Broadcast_Distribution_Table is now writable, matching cl. 12.56.38 (“required to be present and writable” in BBMD mode) — only this property had deviated from the spec; the sibling BBMD_Foreign_Device_Table (cl. 12.56.40, read-only) was already correct.
  • rb b-20260906-5 (#1303): fixed an #include filename-casing mismatch in TestBACnetInterface_BACnetSC.cpp that only failed to compile on a case-sensitive filesystem.
  • rb b-20260906-6 (#1126): BACnetPriorityValue/BACnetPriorityArray::Reset() now clear the inherited valid flag; previously a Reset() object could stay “valid” with a NULL m_priorityValue, crashing Encode().
  • rb b-20260906-6 (#1128): BACnetPriorityValue::IsEqual/GetElements no longer dereference a NULL m_priorityValue — the documented-normal state for a never-written priority slot.
  • rb b-20260906-6 (#1129): BACnetPriorityValue/BACnetPriorityArray::operator=/copy-construction no longer dereference a possibly-NULL source priority value.
  • rb b-20260906-6 (#1130): BACnetPriorityFilter::DecodeWithContext now honors its own optional parameter, matching BACnetObjectBase::DecodeWithContext’s convention — an absent priority-filter no longer leaves a rejected error state standing on an otherwise-successful decode.
  • rb b-20260906-7 (#1109): BACnetSCBVLLValidator::IsResponseBvllFunction no longer misclassifies Advertisement as a response message (135-2024 Annex AB.3.1.2/AB.3.1.3) — a malformed unicast Advertisement is now NAKed instead of silently dropped.
  • rb b-20260906-7 (#1110): BACnetSCHubFunctionManager::UpsertPeer now checks the known-Device-UUID case before the VMAC-collision check, matching 135-2024 Annex AB.6.2.3’s precedence — an ordinary same-VMAC reconnect no longer gets spuriously NAK’d with NODE_DUPLICATE_VMAC.
  • rb b-20260906-7 (#1113): BACnetSCSentMessageTracker::Track now overwrites (freeing the prior owned pointer) on a messageId collision instead of silently dropping and leaking the new message.
  • rb b-20260906-7 (#1117): BACnetSCCommon::GenerateRandomVMAC now seeds its PRNG once from a high-resolution clock, instead of relying on libc’s default (deterministic) rand() seed.
  • rb b-20260906-8 (#1153): BACnetAcknowledgeAlarmProcessor::Process now emits the ACK_NOTIFICATION’s freshly-generated timestamp instead of overwriting it with the original event’s timestamp (135-2024 cl. 13.5.2) — the prior fix’s own cited spec text did not actually support the behavior it justified.
  • rb b-20260906-8 (#1154): BACnetGetEventInformationProcessor now resumes pagination at the LastReceivedObjectIdentifier marker’s successor when that object has been deleted, and correctly returns UNKNOWN_OBJECT when no successor exists in either the intrinsic or EventEnrollment map (135-2024 cl. 13.12.1.1.1), instead of silently truncating or returning a successful empty response.
  • rb b-20260906-8 (#1156): BACnetGetEnrollmentSummaryProcessor’s Event State Filter OFFNORMAL now requires an exact Event_State match (135-2024 cl. 13.11.1.1.3) instead of also matching highLimit/lowLimit/lifeSafetyAlarm.
  • rb b-20260906-9 (#1141): BACnetReinitializeDeviceRequest::Decode now distinguishes an absent optional Password from a present-but-malformed one (135-2024 cl. 16.4.1.1.2), instead of silently coercing malformed-but-present into absent — matching #847’s fix for the sibling DCC Request.
  • rb b-20260906-9 (#1142): DABTable::Acquire() now caps table size (CAS_BACNET_DAB_MAX_ENTRIES = 4096) instead of growing without bound from a remotely-writable NotificationClass Recipient_List write.
  • rb b-20260906-9 (#1143): BACnetDeviceAddressProxyTableEntry’s three owned data members are now private, consistent with every sibling class in the family.
  • rb b-20260906-9 (#1145): BACnetWhoHasRequest::IsEqual/GetElements no longer dereference the mandatory object CHOICE (m_object) unconditionally — comparing two default-constructed or decode-rejected requests used to segfault.
  • rb b-20260906-9 (#1146): BACnetConfirmedPrivateTransferRequest/BACnetConfirmedPrivateTransferACK/BACnetUnconfirmedPrivateTransferRequest’s Encode() now check the Service Number encode’s return value instead of silently omitting it from the wire on an undersized buffer while reporting a successful encode.
  • rb b-20260907-1 (#1371): BACnetSCAdvertisement::SetMaximumBVLCLength/SetMaximumNPDULength now enforce the same symmetric, spec-defined bounds as BACnetSCConnectRequest/BACnetSCConnectAccept (#1111), fixed elsewhere in the previous batch.
  • rb b-20260907-1 (#1398): BACnetSCDirectConnection::SetMaximumBVLCLength/SetMaximumNPDULength now enforce SC-specific bounds (previously delegated entirely to a generic unsigned-integer range with no bounds at all); Decode now re-validates both fields after decode instead of bypassing the setters.
  • rb b-20260907-1 (#1370): BACnetSCConnectRequest/BACnetSCConnectAccept::Decode now route the Maximum-BVLC/NPDU-Length fields through the class’s own (now-bounded) setters instead of assigning raw wire bytes directly.
  • rb b-20260907-1 (#1118): Connect-Request/Connect-Accept/Disconnect-Request/Disconnect-ACK/Heartbeat-Request/Heartbeat-ACK messages carrying an Originating or Destination Virtual Address are now rejected across all three BVLC ingress paths (hub-function, direct-connect, and hub-connector-client), generalizing a check that previously only covered Disconnect-Request.
  • rb b-20260907-1 (#1119): no rate limiting exists on the hub-function forwarding path. The finding’s own recommendation asks for an owner decision on the rate-limiting design (layer, budget shape, and enforcement policy) before implementation; posted as a question for the owner rather than implementing an unguided security control.
  • rb b-20260907-10 (#1233 CR-F02c-009): BACnetObjectSelector’s CHOICE mutators (SetNone/SetObject/SetObjectType) now reset all three arms before setting the chosen one, mirroring already-fixed issue #598’s pattern in the sibling BACnetObjectSpecifier. GetMinimumMemoryCount() now dispatches on m_choice instead of taking an unconditional max() of both arms.
  • rb b-20260907-12 (#1233 CR-F02c-005): SetValueAndTakeControl’s “take ownership only on success” contract was violated by 16 callers across source/BACnetDBDevice.cpp, source/BACnetDatabase.cpp and source/BACnetBusinessLogic.cpp - a failed store leaked the fully-constructed value, several with no named pointer to free at all (constructed inline as the call argument). All 16 now free correctly on failure, matching the pattern this contract’s already-correct callers (AddFileObject, BACnetDataLinkSC.cpp, BACnetDatabase.cpp’s energyMeterRef site) already use.
  • rb b-20260907-13 (#1233 CR-F02b-013): GetDefaultPropertyProfileForAccessDoor’s Current_Command_Priority case is now guarded by #if BACNET_PROTOCOL_REVISION > 16, matching the already-guarded sibling insertion in GetRequiredPropertiesForAccessDoor. No currently-observable behavior change (the codebase’s default BACNET_PROTOCOL_REVISION is 24, and nothing in this codebase’s build configuration overrides it).
  • rb b-20260907-2 (#1240 CR-F09-013): BACnetSCHubConnector::ProcessConnectRequest now sends a BVLC-Result NAK (optionalFunctionalityNotSupported) for a spurious Connect-Request instead of logging and silently dropping it.
  • rb b-20260907-2 (#1240 CR-F09-014): BACnetSCCommon::GenerateRandomVMAC now excludes the two protocol-reserved VMAC values (all-0x00, all-0xFF), resampling via a new IsReservedVMAC predicate rather than handing either out.
  • rb b-20260907-2 (#1240 CR-F09-015): BACnetStackNetworkPortSC::SetSCHubFunctionAcceptUris/SetSCDirectConnectAcceptUris now validate each entry’s wss:// scheme, matching the existing SetSCPrimaryHubUri/SetSCFailoverHubUri behavior.
  • rb b-20260907-2 (#1240 CR-F09-016): BACnetSCConnectAccept::SetConnectRequest renamed to SetConnectAccept (was a copy-pasted name from the unrelated BACnetSCConnectRequest class); mechanical, no behavior change.
  • rb b-20260907-2 (#1240 CR-F09-024): removed a duplicate #include "BACnetDataLinkSC.h" in BACnetDataLinkSC_HubFunction.cpp.
  • rb b-20260907-4 (#1378): BACnetLogDatum::SetAnyValue no longer reports IsValid()==true when the nested m_logDatum assignment fails under allocation exhaustion (SetValid(true) unconditional -> SetValid(this->m_logDatum.IsValid())), mirroring #1194’s fix to this class’s own operator=.
  • rb b-20260907-4 (#1252 CR-F22-014): BACnetEventLogDatum::SetNotification’s Clone()-failure path now resets m_choiceTag to noChoice and calls SetValid(false), instead of leaving a stale m_choiceTag==notification from an earlier successful call while m_notification is NULL.
  • rb b-20260907-4 (#1252 CR-F22-015): BACnetEventLogDatum::SetLogStatus now tracks the supplied BACnetLogStatus’s own validity instead of reporting valid unconditionally.
  • rb b-20260907-5 (#1456): BACnetSingleLogData::SetAnyValue no longer reports IsValid()==true when the nested m_logData assignment fails under allocation exhaustion. Mirrors #1378’s identical fix to the unrelated sibling class BACnetLogDatum::SetAnyValue (batch b-20260907-4).
  • rb b-20260907-6 (#1415): EnableNetworkPortEthernetProperties now enforces cl. 12.56.11’s PHYSICAL/Reference_Port constraint, mirroring #1391’s fix to the sibling functions EnableNetworkPortVirtualProperties/SecureConnectProperties.
  • rb b-20260907-6 (#1233 CR-F02a-012): EnableNetworkPortMSTPProperties/EnableNetworkPortVirtualProperties no longer leak the constructed Network Port on 3 error paths (found: one more than the finding’s own “two” count named — a second, separate branch inside MSTP).
  • rb b-20260907-6 (#1233 CR-F02a-013): AddProprietaryProperty now propagates its own BACnetDBObject call’s result instead of discarding it. No currently observable behavior change (the callee cannot yet return false); a forward-looking consistency fix.
  • rb b-20260907-7 (#1233 CR-F02a-014): AddLightingOutputObject/AddLightingOutputObjectForTest now validate relinquishDefault against Present_Value’s own spec-documented range (0.0% to 100.0%, rejecting NaN/Inf too), instead of accepting any value.
  • rb b-20260907-7 (#1233 CR-F02a-016): the Network_Number write path now reports priority 0 to the host callback (Network_Number is not commandable), matching every sibling CallbackSetPropertyUInt call site in the same file instead of an unexplained 16.
  • rb b-20260907-7 (#1233 CR-F02a-019): fixed two copy-pasted "maxMaster" debug-log strings in the maxInfoFrames write path to say "maxInfoFrames".
  • rb b-20260907-8 (#1233 CR-F02a-017): documented the latent, currently-unreachable static_cast<BACnetStackNetworkPortIpv4*> assumption at both cast sites - verified no ipv6 network-port construction path exists anywhere in the codebase, so the cast is safe today but would need a real type check if one is ever added.
  • rb b-20260907-8 (#1233 CR-F02a-018): re-verified at the tip - EnableIntrinsicChangeOfBitstringAlgorithm/its 64-bit sibling already reject (not clamp) an out-of-range bitstringLength, fixed by earlier unrelated issue #994. Ticked not-applicable with an evidence comment rather than re-fixing.
  • rb b-20260907-8 (#1233 CR-F02c-013): documented why BACnetDBObject’s 3-argument constructor intentionally discards objectType/objectInstance via NOREF - every caller already tracks identity itself, either via a map keyed by objectIdentifier or (for BACnetDBDevice) a separately-tracked field.
  • rb b-20260907-9 (#1233 CR-F02c-004): BACnetDBObject::AddProprietaryProperty now returns false instead of silently discarding a duplicate property-identifier registration and reporting success. Since CR-F02a-013 (PR #1466) already propagates this return value up through BACnetDBDevice/BACnetDatabase, this is a real, observable behavior change up the whole call chain.
  • rb b-20260907-9 (#1233 CR-F02c-006): BACnetStackObjectSetting::GetOptionalPropertyList/GetRequiredPropertyList now skip a property with no default profile for the queried object type and keep going, instead of aborting the whole enumeration - matching the sibling BACnetDBObject method’s already-correct behavior. Has a real, current effect via BACnetDBDevice::GetPropertyList and the property-reference expansion path in BACnetReadPropertyMultipleProcessor.
  • rb b-20260907-9 (#1233 CR-F02c-015): BACnetStackObjectSetting::GetPropertyWritable/GetPropertySubscribable rewritten from a redundant enabled-check plus linear scan to a single .find() - behavior-preserving, matching the sibling TryGetProperty* functions.
  • rb b-20260908-28 (#1332, partial): BACnetStackAuditReporter::GetAuditReporterProperty (all 3 branches), BACnetStackEventLog::GetEventLogProperty, BACnetStackTrendLogBase::GetTrendLogProperty, and — found by adversarial review as the same live vector, not a separate concern — its two derived overrides BACnetStackTrendLog::GetTrendLogProperty and BACnetStackTrendLogMultiple::GetTrendLogProperty no longer report success when InsertObjectAndTakeOwnership fails under memory pressure, and consistently set errorContainer on failure — revert-red: all fault-injection tests failed with their respective fix reverted (ci_scripts/verify-fails-without.sh).
  • rb b-20260908-28 (#1534): TestBACnetStackTrendLog.cpp’s SetLastCovValueAndCloneFreesThePreviousSnapshot leaked-block bound widened from 4 to 6 to tolerate MSVC’s debug-CRT allocation count observed in #1534’s own MSVS run; the issue’s second item (a flaky BACnetTimeValue test) needed no change, already fixed by #1279/PR #1658.
  • rb b-20260908-28 (#1491): CASBACnetStackTestsMemory.cpp ported off the removed v5 fp* API to the current BACnetStack_* names, and a masked BACnetPropertyIdentifier ambiguous-symbol error fixed alongside it — compile-verified clean with cl.exe.
  • rb b-20260909-29 (#1332, partial): BACnetBusinessLogic::BuildNetworkPortBbmdBroadcastDistributionTable/BuildNetworkPortBbmdForeignDeviceTable no longer report success when a per-entry InsertObjectAndTakeOwnership fails under memory pressure — revert-red: 2 fault-injection tests failed with the fix reverted (ci_scripts/verify-fails-without.sh).
  • rb b-20260909-30 (#1332, partial): BACnetBusinessLogic::UseCallbackGetElevatorGroupProperty’s landingCalls/landingCallControl cases no longer report success when constructing/inserting a BACnetLandingCallStatus fails under memory pressure — revert-red: 2 fault-injection tests failed with the fix reverted (ci_scripts/verify-fails-without.sh).
  • rb b-20260909-30 (#1380, partial): BACnetDBDevice::UpdateObjectIdentifier now also migrates m_alarmAndEventObjects/m_pendingAlarmAndEventObjects, the two highest-impact of the ~26 more specialized identity-keyed maps beyond #1040’s original 4 — revert-red confirmed for the pending-queue map via ci_scripts/verify-fails-without.sh.
  • rb b-20260909-31 (#980): BACnetStack_AddElevatorGroupObject no longer silently creates a Positive Integer Value object for machineRoomId if one doesn’t already exist — it now requires the object to exist and rejects otherwise, mirroring AddLiftOrEscalatorObject’s own precedent for elevatorGroupInstance — revert-red: the new discriminating test failed with the fix reverted (ci_scripts/verify-fails-without.sh).
  • rb b-20260909-32 (#1332): CheckForChangeOfValue’s covIncrement insert no longer discards InsertObjectAndTakeOwnership’s return value — an allocation failure now correctly leaves useCOVIncrement false instead of proceeding as if the increment were usable. Sibling of #787/#1025/#1232; no dedicated test (verified NOT gated by the 5 existing CheckForChangeOfValue tests via ci_scripts/verify-fails-without.sh, matching established precedent for this bug class).
  • rb b-20260909-32 (#1380): UpdateObjectIdentifier now migrates m_trendLogs, m_eventLogs, and m_auditLogs on rename, same shape as the maps already migrated for #1040/#1380 in earlier batches — revert-red: the new AuditLogSurvivesRename test failed with the fix reverted (ci_scripts/verify-fails-without.sh). Also fixed a second, independent bug found while writing that test: IsObjectTypeCompiledAndEnabled had no case at all for BACnetObjectType::auditLog, so an audit log rename was rejected unconditionally regardless of build configuration.
  • rb b-20260909-33 (#916): RunSegmentedResponseState now checks BACnetSegmentACKPDU::IsNegativeACK() before advancing its send window — an in-window negative Segment-ACK previously advanced the window and discarded the reported-bad segments exactly like a positive ACK, instead of retransmitting them. A negative ACK now retransmits the currently outstanding segments and counts against the same retry limit as a plain segment timeout, mirroring that existing logic; retries exhausted returns to Idle the same way a final timeout does. Revert-red: the new discriminating test failed with the fix reverted (ci_scripts/verify-fails-without.sh).
  • rb b-20260909-34 (#951): projects/linux/Release/arm7/makefile and projects/linux/Release/arm64/makefile now cross-compile with the real ARMv7 (armhf) and AArch64 toolchains (arm-linux-gnueabihf-g++ / aarch64-linux-gnu-g++, both overridable via CC=) instead of the host’s own g++ with no ISA selector — on an x86-64 host both previously built and mislabeled an ordinary x86-64 .so as arm7/arm64. projects/linux/build.sh now builds these two targets when the cross-toolchain is available and prints an explicit SKIPPED message (naming the apt package to install) otherwise, rather than never invoking them at all. Verified via file on the resulting .so for each target (ELF 32-bit ... ARM, EABI5 and ELF 64-bit ... ARM aarch64 respectively). The x64 gcc/g++ half of this issue was already fixed in an earlier batch (PR #1559).
  • rb b-20260910-35 (#1734): TrendLogMultiple’s Log_Interval default property profile is now writable (135-2024 cl. 12.30.13 — “shall be writable if Logging_Type has the value POLLED”), matching TrendLog’s own unconditional Log_Interval default. BACnetStackTrendLogMultiple::SetTrendLogProperty already fully implements writing it (rejecting only while Logging_Type is TRIGGERED), so the previous writable=false default caused BACnetDBDevice::IsPropertyWritable to under-report writability for a property the object’s own code already accepted. Revert-red: the updated TrendLogMultiple_LogIntervalMatchesSpec_WritableInProfile test failed with the fix reverted (ci_scripts/verify-fails-without.sh).
  • rb bs-20260922-1 (#2226): the BACnetStack_RegisterCallbackSendMessageForPort doc comment said the callback’s return is “A FLAG, NOT A COUNT” and “never compared with messageLength”. Every send site has required the return to equal messageLength since #925/#1567-#1573, so a host that followed the header had every send treated as failed. The header now documents the exact-length contract. Pinned on the IPv4 and BACnet/SC paths: a callback returning 1 fails the send, and an exact-length return succeeds.
  • rb bs-20260922-1 (#1484): FPCallbackGetPropertyReal’s parameter docs called its float* value “a signed integer” (copied from the SignedInteger callback); it now says real (32-bit IEEE-754 float). No declaration change.
  • rb bs-20260922-2 (#899): behaviour change for integrators — BACnetStack_AddScheduleObject now returns false (and logs “Schedule object instance [N] does not exist. Add it first with BACnetStack_AddObject(deviceInstance, 17, N).”) when the Schedule object was never added with BACnetStack_AddObject; before, it returned true and the Schedule served nothing. The header’s “existence is NOT currently verified” note is removed (IFC-042).
  • rb bs-20260922-2 (#1125): ReadRange of a Trend Log / Trend Log Multiple / Event Log Log_Buffer with Reference Index 0 (by position) or Reference Sequence Number 0 (by sequence number) now returns an empty Result(+) (Item Count 0, Result Flags all false, no First Sequence Number) as 135-2024 cl. 15.8.1.1.4.1.1 / 15.8.1.1.4.2.1 and 135.1-2023 9.21.2.4 require, instead of Result(-) SERVICES/VALUE_OUT_OF_RANGE.
  • rb bs-20260922-2 (#541): Program object Program_Change follows 135-2024 Figure 12-3 as drawn (read from the licensed PDF): every request is valid in every Program_State (only the READY gate returns Result(-)); a READY write is an accepted no-op; the no-host auto-complete settles to the figure’s state from the current state (e.g. LOAD from IDLE now ends RUNNING, HALT from IDLE stays IDLE). The previous table was reconstructed from OCR-scrambled text and rejected 22 of 36 valid pairs (IFC-043).
  • rb bs-20260922-2 (#2291): a physical BACnet router (two or more AddRouterPort ports) now makes Network_Number writable on its Network Port objects (135-2024 cl. 12.56.13, “shall be writable in routers”), whichever order ports and Network Port objects are configured in; virtual devices are unaffected.
  • rb bs-20260922-3 (#2318): a virtual router (BACnetStack_AddVirtualNetwork) now actually makes Network_Number writable on its Network Port objects (135-2024 cl. 12.56.13, “shall be writable in routers”), both the existing ports and the virtual network’s own port, whichever order they are added in. Before, AddVirtualNetwork set only the object-type-level flags, which each Network Port’s per-instance setting shadows, so neither port became writable.
  • rb bs-20260922-3 (#1481): breaking change for Java integrators. The Java (JNA) bindings now map C bool to byte instead of boolean. JNA marshals a Java boolean as a 32-bit int while C’s bool is one byte, so the old mapping was correct only by reading the low byte on little-endian targets. Pass (byte) (flag ? 1 : 0), test a result with != 0, and return (byte) 1 / (byte) 0 from a callback. SimpleServerExample also moved to the ...ForPort send/receive callbacks; it did not compile against the current bindings before. New constant BACnetConstants.NETWORK_PORT_DEFAULT (4194303, BACNET_NETWORK_PORT_DEFAULT).
  • rb bs-20260922-4 (#2322): a WriteProperty of a Network Port object’s Network_Number now refuses values above 65534 with PROPERTY / VALUE_OUT_OF_RANGE (135-2024 cl. 12.56.13: “The range for this property shall be 0 .. 65534”). The stored Network_Number and Network_Number_Quality now change only after the host’s CallbackSetPropertyUInt accepts the write. Before, both were stored first, so a write the host declined (Result(-)), or that had no callback to go to (Abort), still changed them. This became reachable when #2291/#2318 made Network_Number writable on routers. Integrator note: during the CallbackSetPropertyUInt call the port now still holds the old number, so a host that re-read the port inside the callback to learn the new value must use the callback’s value argument instead. The stack does not change the virtual router’s routing on this write; the host still does that with BACnetStack_UpdateVirtualNetworkNumber. Changes_Pending and effect at ReinitializeDevice are tracked separately in #2326.
  • rb c-20260908-1 (#1016): ci_scripts/check-header-standalone.sh now proves a specific export is reachable from the customer header, not merely that the header parses. Its isolated C99 and C++17 consumers reference both halves of the Trend Log backup/restore pair, so demoting either is a build failure. Verified discriminating rather than assumed: with the declaration deleted the gate fails as C99 ('BACnetStack_InsertTrendLogRecord' undeclared) and as C++17 (was not declared in this scope). The comment states the gate’s own limit — it compiles -c and never links, so it witnesses the declaration, not a definition.
  • rb c-20260908-1 (#1016): ci_scripts/node-adapter-parity-allowlist.txt drops test-tool:InsertTrendLogRecord, which the promotion made stale; the gate flagged it itself.
  • rb c-20260908-1 (#1016): stale hard-coded export counts removed from hand-written prose in ci_scripts/check-header-standalone.sh (5) and ci_scripts/generate-{csharp,java,python,rust}-adapter.js (7). Removed rather than updated 229 → 230, because a count in a comment rots at the next export change.
  • rb c-20260908-1 (#1016 / #1532): the Node binding of this export is not complete, and #1016 stays open because of it. BACnetStack_InsertTrendLogRecord’s napi registration (interface.cc:293) and wrapper (interface_objects.cc:944) are still inside #if defined(BACNET_STACK_TESTTOOL), while the regenerated adapter-exports.ts binds the name unconditionally and EXPECTED_PRODUCTION_ADDON_EXPORTS omits it — so a production Node addon build exposes the export as undefined and fails at call time rather than at load. Both of this batch’s reviewers found it independently. Those two files are held by another session’s active work, so it is filed as #1532 with an ASK on the coordination board rather than edited here. C, C++, C#, Java, Python and Rust consumers are unaffected. #1532 also records the reason no gate caught it: nothing re-derives napi gating from the header that declares an export, so the mismatch is silent in both directions (#450 was the opposite direction).
  • rb c-20260908-2 (#1012): BACnetDBDevice’s default constructor never initialised m_stackSettings, so every != NULL guard on it was testing an indeterminate pointer — undefined behaviour that usually takes the true branch and dereferences garbage. Pre-existing; fixed here because this change put that pointer on a new read path.
  • rb c-20260908-2 (#1012): BACnetDBDevice::Reset() restores the DAB tunables to their boot defaults via m_dab.Clear(); the per-device setting overrides are now cleared with them, carrying the repository’s // RESET: tag. Without it the device would report an override the heartbeat engine was no longer using — the same divergence Sprint 261 item 25 fixed, one layer up.
  • rb c-20260908-2 (#1012): the v5→v6 breaking-changes documents record this signature change, with the behaviour-preserving migration. They previously documented only the old two-argument getter, so the one document a migrating customer reads omitted an ABI break.
  • rb c-20260908-3 (#1532, completing #1016): BACnetStack_InsertTrendLogRecord now works in a production Node addon build. #1016 promoted it to the customer header, but its napi registration and wrapper stayed inside #if defined(BACNET_STACK_TESTTOOL) while the generated adapter-exports.ts bound the name unconditionally and EXPECTED_PRODUCTION_ADDON_EXPORTS omitted it — so a production addon exposed the export as undefined and failed at call time, which LoadBACnetFunctions() could not catch. BACnetStack_InsertEventLogRecord stays gated: it is still test-tool-only, because there is no public ReadEventLogRecord and so no backup-without-restore asymmetry to fix.
  • rb c-20260908-3 (#1532): ci_scripts/node-adapter-coverage.sh now derives the napi BACNET_STACK_TESTTOOL gating from the header that DECLARES each export, in both directions — a customer-header export that is gated fails (#1532), and a test-tool-only export that is ungated fails (#450). This is the mechanism half of the issue: everything the gate previously compared derived from interface.cc, so when a promotion left the gating stale both sides agreed with each other on a broken tree and nothing dissented. Proven revert-red in both directions, in the state that actually reproduces the defect — interface.cc and the manifest agreeing while only the header disagrees.
  • rb c-20260908-3 (#1532): adapters/node/.../api.ts no longer tells Node customers InsertTrendLogRecord is TEST-TOOL BUILD ONLY. That is the file a Node customer reads, and it contradicted the customer header #1016 wrote — the same denial #1016 removed from the v5→v6 migration document.
  • rb c-20260908-3: api.ts documented a Trend Log Error datum as "1,31" (comma). BACnetLogDatum’s parser splits on - and rejects anything else, the round-trip emits -, and the customer header says HYPHEN explicitly — a Node caller following the JSDoc got a bare false. Pre-existing, but it became shipped customer-facing documentation the moment this batch un-gated the export.
  • rb c-20260908-3: stale hard-coded gated-export count removed from binding.gyp (said 59, is 133).
  • rb c-20260908-5 (#1013): the array index now reaches the encoded notification, not just the property read. Decoding it and passing it only to GetProperty leaves clause 21.3.2’s property-array-index [1] off the wire, so a receiver sees the array property with a single-element value and no index — a claim about the whole array.
  • rb c-20260908-5 (#1013): the packed-buffer length guard is 64-bit. propertyCount * 9 in 32-bit arithmetic wraps — propertyCount = 477218589 yields 5, so a 9-byte buffer passed and the loop then walked 477 million elements off the end. The napi wrapper had the identical wrap, which defeated its own #392 buffer guard. Both fixed, with a regression test using that exact count.
  • rb c-20260908-5 (#1527): this export’s napi wrapper is bespoke and on ci_scripts/napi-wrapper-bespoke-allowlist.txt, and allowlisting exempts a wrapper from the byte-identical comparison entirely — so generate-napi-wrappers --gate reported OK while the wrapper was still calling the pre-#1013 signature. For the 71 allowlisted wrappers there is no signature check of any kind; only the C++ compile catches a mismatch. Reported on #1527 with a suggested split: exempt bespoke bodies, never their call shape.
  • rb c-20260908-8 (#1593): ci_scripts/check-property-identifier-enum.js gates the published enum against the internal one it is generated from, wired into pre-push.sh. Publishing 522 entries creates a second copy of a table, and this repository has repeatedly shipped that failure — two representations of one truth with nothing comparing them (#1532, #1596, #1013). The header’s other published enum keeps its copies in sync with a comment, and its hand-written Node copy has already drifted to 1 of 4 entries (#1599).
  • rb c-20260908-8 (#1593): the gate’s --self-test has five arms — a wrong value, a missing entry, a public-only entry, an enumerator with no explicit value, and the aligned state passing — so it cannot be satisfied by a check that is always red or always green.
  • rb c-20260908-8 (#1593): the new gate runs in ci_scripts/pre-push.sh but not in .github/workflows/tests.yml, where every sibling gate is also wired. A push that bypasses the hook would land drift unchecked. .github/** is owner-only, so this is a two-line follow-up for the owner rather than something this batch could do.
  • rb d-20260910-32 (#1473): BACnetStack_RegisterCallbackGetSequenceLiftLandingDoorStatus’s direction out-parameter renamed to doorStatus — a copy-paste artifact from a neighboring callback. Not an ABI break (parameter names aren’t part of the C ABI); no action required by existing integrations. See docs/interface/IFC-024.md.
  • rb d-20260910-34 (#1014): BACnetStack_SendIAmRouterToNetwork no longer fails for a plain (physical) BACnet router configured via BACnetStack_SetRouterEnabled/BACnetStack_AddRouterPort - it previously always returned false with “BACnet Router logic not yet implemented” for that case.
  • rb d-20260910-36 (#1009): Align_Intervals/Interval_Offset on TrendLog/TrendLogMultiple were stored nowhere and never consulted by polled-mode scheduling - writing them had no observable effect. They now round-trip via ReadProperty/WriteProperty and ReadyToLog’s polled cadence honors Align_Intervals (aligning to real-time boundaries offset by Interval_Offset instead of free-running from the last poll). Log_Interval’s hundredths-of-a-second resolution (cl. 12.25.9) is also no longer truncated to whole seconds by the scheduling math.
  • rb d-20260910-43 (#822/#556, WP-5 pkg 1a): BACnetStack_SetBBMD’s networkPortInstance parameter was accepted but silently ignored - the device’s BBMD-enabled flag was always set on the first BACnet/IP data link instance regardless of which Network Port object instance was named. It now targets the correct instance.
  • rb d-20260910-49 (#822/#556, WP-5 pkg 3a): the Device_Address_Binding-driven Who-Is heartbeat chase (#1021/#1328, Notification Class device-identifier recipients) and DAB-resolved external property writes (e.g. a Channel/Schedule object targeting a device by Device Identifier) now correctly resolve the real Network Port instance a peer’s I-Am was heard on, instead of feeding a raw transport-type byte into the widened networkPortInstance parameter above - a regression that would otherwise have silently broken both features for any deployment whose Network Port instances are not coincidentally numbered 0/1/2.
  • rb d-20260910-55 (#1751, #822/#556): BACnetDataLinkIPv4::SendOutgoingPacket’s two internally-generated BVLL reply call sites (foreign-device DistributeBroadcastToNetwork, and the general unicast/broadcast send) now report the real claimed networkPortInstance to CallbackSendMessageForPort, instead of always reporting 0 (BACnetPacket::NetworkType_IP). A host on a multi-homed BACnet/IP device (more than one claimed Network Port object) can now tell which instance an auto-generated Ack/Nak/Result reply (for example to ReadBroadcastDistributionTable, RegisterForeignDevice) actually came out on. BACnet/SC and MS/TP egress are unaffected by this fix - those send sites still report the old transport-type byte, tracked separately.
  • rb e-20260910-1 (#1767): BACnetReadPropertyMultipleProcessor::EstimateResponseLength no longer applies its worst-case 1000-element array pricing to a whole-array-read of a property that doesn’t exist (or isn’t enabled) on the target instance — revert-red: a whole-array read of a nonexistent device instance estimated identically to the same read of a real device (both >1000 bytes) instead of the expected small framing-only estimate.
  • rb e-20260910-1 (#1769): BACnetEventNotificationRequest::SetEventNotificationRequest no longer encodes AckRequired for an ACK_NOTIFICATION (notifyType=ackNotification), per 135-2024 cl. 13.8.1.1.10/13.9.1.1.10 — revert-red: all 24 strengthened ackNotification assertions in tests/BTL_Tests/9_1_AcknowledgeAlarmServiceExecutionTests.cpp fail (IsValid() false, UseAckRequired() true) with the fix reverted.
  • rb e-20260910-1 (#1768): BACnetStackChannel::SetProperty’s executionDelay case now honours the array index (index 0 rejected writeAccessDenied, 1..N replaces that element, >N is invalidArrayIndex) instead of always replacing the whole array — revert-red: three new tests in tests/TestBACnetStackChannel.cpp, one per failure scenario from the issue, each fails with the fix reverted.
  • rb e-20260910-2 (#914): BACnetComplexAckProcessor now dispatches an inbound AuditLogQuery-ACK through a new BACnetAuditLogQueryAckProcessor instead of silently dropping it in the default: branch — revert-red: AuditLogQueryAck_MatchedInvokeId_DoesNotCrash in tests/TestBACnetComplexAckProcessor.cpp proves the switch dispatches to the new processor rather than falling through.
  • rb e-20260910-3 (#1596): generate-napi-wrappers.js --gate now checks an allowlisted (bespoke) napi wrapper’s call shape — which info[] slot each parameter is read from, the C export called and the order/identity of its resolved arguments, and the return kind — instead of skipping comparison entirely once a wrapper is allowlisted. Previously the class of defect #1013 shipped undetected (a parameter list widened from 11 to 15, the hand-written wrapper still calling the old signature) on any allowlisted wrapper. New --self-test (wired into pre-push.sh) proves the mutation-detection with three synthetic cases. A residual gap (a defect living entirely inside a Validate* call, the #1464 shape) is disclosed in code and filed as #1777, not fixed here.
  • rb e-20260910-4 (#1777): generate-napi-wrappers.js --gate now also catches a Validate* check on an allowlisted wrapper that resolves a different info[] slot than the header describes — the #1464 “swapped length/max-length pair” shape isolated entirely inside a check call, which #1596’s own call-shape comparison can’t see. Keys checks by every leading string-literal argument (not just the check name, and not just the first literal) so repeated same-named checks for different fields — including the two-literal ValidateBACnetDateField family — are disambiguated correctly rather than collapsing to whichever occurrence was written last. Two rounds of review found and fixed two further collision shapes before merge; a third, capped review round confirmed (via exhaustive enumeration of every real Validate* call site) no remaining gap.
  • rb e-20260910-5 (#1782): a whole-array WriteProperty whose length disagrees with a fixed-size (non-resizable) array now returns INVALID_ARRAY_SIZE (135-2024 cl. 15.9.1.3.1/15.10.1.3.1’s own error table), not INVALID_ARRAY_INDEX (an out-of-range element index) or WRITE_ACCESS_DENIED (property not writable at all). Corrects the author’s own earlier work from #1768 (PR #1774) and #967, found live by the CAS BTL conformance test tool: BACnetBusinessLogic.cpp’s shared SetPrimitiveArrayUsingCallbacks not-resizable/length-mismatch branch, and BACnetStackChannel.cpp’s Execution_Delay index-0 rejection and whole-array length-mismatch rejection. Swept the class - BACnetStackNotificationForwarder.cpp’s Port_Filter and BACnetStackGroup.cpp’s List_Of_Group_Members were deliberately left unchanged (unconditional write prohibition and genuine resizable BACnetLIST, respectively - neither matches this defect class). A related, out-of-scope defect in BACnetStackSchedule.cpp’s weeklySchedule found during review is filed separately as #1797 rather than folded into this fix.
  • rb e-20260910-8 (#1797): Schedule object’s weeklySchedule (a fixed 7-element array, one per weekday) now correctly rejects a resize-shaped index-0 write and a whole-array write of the wrong element count with INVALID_ARRAY_SIZE (135-2024 cl. 15.9.1.3.1/19.1), the same fix class #1782 applied elsewhere. Also fixes a silent-truncation bug: writing MORE than 7 days to the whole array used to be silently accepted and truncated to the first 7 with no error at all; it is now correctly rejected.
  • rb e-20260911-11 (#1332 Group 6): BACnetStackNetworkPortSC/BACnetStackNetworkPortBase’s GetNetworkPortProperty now checks the return value of every InsertObjectAndTakeOwnership/InsertEncodedObjectAndCleanUpMemory call instead of discarding it and unconditionally reporting success — a mid-build allocation failure used to silently return a successful GetProperty with an empty or invalid value. Array-shaped properties (hub/direct-connect connection status lists, certificate file arrays, failed-connection-request lists, etc.) no longer silently truncate on a mid-loop allocation failure either.
  • rb e-20260911-14 (#1380): BACnetDBDevice::UpdateObjectIdentifier now migrates all 22 remaining specialized identity-keyed maps (network ports, trend log multiples, authorization policies/caches, notification classes, channels, groups, schedules, lighting outputs, binary lighting outputs, timers, averagings, load controls, life safety points/zones, access doors/credentials/users/rights/zones, credential data inputs, access points) that were previously left un-migrated on an object rename, orphaning their stack-held state under the old identifier.
  • rb e-20260911-16 (#1251, CR-F21-007): BACnetDestination::SetFromTime/SetToTime now warn immediately when the resulting From/To window is inverted (always empty), mirroring the diagnostic IsValidDayAndTime() already logs at evaluation time. Diagnostic only - no change to setter return values or object validity.
  • rb e-20260911-17 (#1249, CR-F19-008): BACnetEventParameterChangeOfDiscreteValue::Decode no longer calls Reset() a second time when the statusFlags field fails to decode, matching every sibling BACnetEventParameterChangeOf* class - an already-decoded newValue now survives a subsequent statusFlags decode failure instead of being wiped, consistent with the object’s IsValid() state (which was, and remains, unaffected).
  • rb e-20260911-19 (#1234, CR-F03-014/016/020): BACnetOptionalDateTime/BACnetOptionalDateTimePattern::Encode now abort on a corrupted/unset choice instead of silently encoding Null, matching all 13 BACnetOptional* siblings’ switch/default pattern; their GetMinimumMemoryCount() now accounts for the Null alternative via the sibling family’s max(n,v) pattern; and BACnetPrimitiveREAL/DATE/TIME‘s Encode buffer-capacity checks now use GetMinimumMemoryCount() for consistency with BACnetPrimitiveDOUBLE (issue #565). All three changes are behaviorally inert for any state reachable through these classes’ own public API today.
  • rb e-20260911-20 (#1234, CR-F03-031): BACnetPropertyStates::GetElements() now correctly renders vendor-proprietary (tag 64-254) property states instead of reporting them as invalid.
  • rb e-20260911-21 (#1234, CR-F03-041): BACnetObjectPropertyValue/BACnetDeviceObjectPropertyValue::GetMinimumMemoryCount() no longer overcount an unset OPTIONAL field’s memory requirement, matching the sibling BACnetDeviceObjectPropertyReference’s existing pattern.
  • rb e-20260911-25 (#1816): BACnetActionCommand::GetMinimumMemoryCount() no longer overcounts an unset OPTIONAL field’s memory requirement, matching the fix already applied to BACnetObjectPropertyValue/BACnetDeviceObjectPropertyValue/BACnetDeviceObjectPropertyReference. 1 of 11 classes named in #1816; 10 remain.
  • rb e-20260911-26 (#1816): BACnetColorCommand/BACnetLightingCommand::GetMinimumMemoryCount() no longer overcount unset OPTIONAL fields, matching the fix already applied to BACnetActionCommand and others. 3 of 11 classes named in #1816; 8 remain.
  • rb e-20260911-27 (#1816): BACnetAuthenticationFactorFormat/BACnetPropertyAccessResult::GetMinimumMemoryCount() no longer overcount unset OPTIONAL fields, matching the fix already applied to 3 other classes. BACnetAuthenticationFactorFormat also gained its first-ever dedicated test coverage. 5 of 11 classes named in #1816; 6 remain.
  • rb e-20260911-28 (#1816): BACnetAuthenticationEvent::GetMinimumMemoryCount() and BACnetAuthorizationPolicy::GetMinimumMemoryCount() no longer add their OPTIONAL fields (decision-details; not-before/not-after) unconditionally — each is now gated behind its own Has*() accessor, matching the pattern already applied to BACnetActionCommand/BACnetColorCommand/BACnetLightingCommand/BACnetAuthenticationFactorFormat/BACnetPropertyAccessResult. 7 of 12 #1816 classes now fixed.
  • rb e-20260911-29 (#1816): BACnetAccessRule::GetMinimumMemoryCount() no longer adds its OPTIONAL fields (time-range, location) unconditionally — each is now gated behind its own Has*() accessor. 8 of 12 #1816 classes now fixed.
  • rb e-20260911-30 (#1816): BACnetAccessToken::GetMinimumMemoryCount() no longer adds its OPTIONAL fields (not-before, not-after) unconditionally — each is now gated behind its own Has*() accessor. 9 of 12 #1816 classes now fixed.
  • rb e-20260911-31 (#1816): BACnetAuthorizationEvent::GetMinimumMemoryCount() no longer adds its OPTIONAL fields (client, token, decision-details) unconditionally — each is now gated behind its own Has*() accessor. 10 of 12 #1816 classes now fixed.
  • rb e-20260911-32 (#1816): BACnetAuthorizationStatus::GetMinimumMemoryCount() no longer adds its 7 OPTIONAL fields unconditionally — each is now gated behind its own Has*() accessor. Also corrected a separate, pre-existing wrap-attribution bug (posture/error-details, both primitives, were incorrectly credited with a tag wrap they never actually get). 11 of 12 #1816 classes now fixed.
  • rb e-20260911-33 (#1816): BACnetSCHubFunctionConnection::GetMinimumMemoryCount() no longer adds its OPTIONAL fields (error, error-details) unconditionally — each is now gated behind its own Has*() accessor. This was the 12th and final class named in #1816; the issue is now fully resolved.
  • rb e-20260911-37 (#1786): a Zone-addressed LifeSafetyOperation (e.g. RESET) now applies depth-first — members are reset before the Zone itself, so its own roll-up recompute no longer sees stale member state. This eliminates a spurious NORMAL→OFFNORMAL→NORMAL notification oscillation for what should be a single clean transition.
  • rb e-20260911-38 (#1784): SubscribeCOVProperty now rejects a request with exactly one of Lifetime/Issue Confirmed Notifications present (Reject(missingRequiredParameter)) — previously this created a spec-forbidden indefinite subscription. A pre-existing test fixture with a hand-encoded message that itself violated this rule was corrected.
  • rb e-20260911-39 (#1483): Rust adapter RegisterCallbackX/RegisterHookX functions now take Option<FPCallbackX> — None clears a callback, matching C#/Java/Python and source/CASBACnetStackDLL.h’s documented NULL contract. Added adapters/rust/Cargo.toml pinning libloading/once_cell; actually verified with cargo check and the previously-never-run pack-helper self-test, both clean.
  • rb e-20260914-25 (#1406): BACnetPrimitiveENUM::SetValue is now virtual, so BACnetPropertyStates::SetEnumeratedValue’s generic construction path (which only ever holds a base-typed BACnetPrimitiveENUM*) actually reaches BACnetLockStatus/BACnetDoorSecuredStatus/BACnetDoorValue/BACnetAccessPassbackMode’s own closed-ENUMERATED domain checks (added by #1213), instead of always resolving to the unchecked base implementation. BACnetPrimitiveENUM::operator= no longer re-validates an assignment the derived SetValue() refused.
  • rb e-20260914-28 (#1843, part A of #1683): BACnetConfirmedRequestPDU::Decode/DecodeUsingSupportedServices now store a segmented Confirmed-Request’s raw payload bytes in a new m_memoryBuffer instead of trying to decode the service request immediately from a single segment’s slice (mirrors the client-side BACnetComplexACKPDU #1091/#1095 fix). Added GetServiceRequestFromMemoryBufferAndTakeControl() and a small per-transaction reassembly accumulator (AppendReceivedSegmentBytes/GetReceivedSegmentedRequestByteCount/TakeAssembledReceivedServiceRequest) on BACnetStackRespondingStateMachineCurrentState, gated STACK_OPTION_SEGMENTATION, enforcing a new STACK_OPTION_MAX_RECEIVED_SEGMENTED_REQUEST_SIZE byte cap before growing. No state-machine behavior change yet — RunIdleState still aborts every segmented request (see #1844).
  • rb e-20260914-30 (#1844, part B of #1683): BACnetRespondingStateMachine::RunIdleState/RunSegmentedRequestState implement the in-order SEGMENTED_REQUEST receive path (cl. 5.4.5.1/5.4.5.2) - a segmentedReceive/segmentedBoth device now accepts a segmented Confirmed-Request, sends SegmentACK(server=TRUE) per window, reassembles the request via #1843’s accumulator, and dispatches it through BACnetBusinessLogic::ProcessMessage on the final segment, entering AWAIT_RESPONSE exactly like an unsegmented request (including a segmented ComplexACK reply). Duplicate/out-of-order/timeout handling stays today’s abort behavior (#1845/#1846). Adds a narrow, BACNET_STACK_TESTTOOL-only internal test seam (BACnetDBDevice::SetSegmentationEnabledForTesting) so tests can enable the receive path before #1847 lifts SetSegmentationEnabled’s public restriction. Also fixes a related gap found in review: BACnetConfirmedRequestPDU::SetServiceRequest/SetServiceRequestAndTakeControl now clear m_memoryBuffer (mirrors BACnetComplexACKPDU’s existing fix) so a freshly-attached service request is never shadowed by stale raw segment bytes.
  • rb e-20260915-1 (#1845, part C of #1683): BACnetRespondingStateMachine::RunSegmentedRequestState implements the remaining SEGMENTED_REQUEST error transitions (cl. 5.4.5.2) - DuplicateInWindow/Ndup-based duplicate-segment handling (silently dropped while under the limit, negative SegmentACK once exceeded), out-of-order segment recovery (negative SegmentACK at LastSequenceNumber), an Abort from the peer (silent, no response), an unsegmented Confirmed-Request or a SegmentACK(server=FALSE) arriving mid-reassembly (Abort(INVALID_APDU_IN_THIS_STATE)), and inconsistent “every segment” header attributes across segments (Abort(INCONSISTENT_ATTRIBUTES), falling back to other on this build’s pre-rev-25 BACNET_PROTOCOL_REVISION). Adds DuplicateInWindow and ReceivedSegmentAttributesConsistent helpers plus a DuplicateCount counter on BACnetStackRespondingStateMachineCurrentState. Fixes a stale pre-2024 spec comment (InitialSequenceNumber+1) along the way. Timeout handling stays #1846’s job.
  • rb e-20260915-12 (#643): an address-form Notification Class recipient whose 6-octet MAC is actually a BACnet/SC VMAC is no longer silently misresolved as a BACnet/IP connection string - BACnetNetworkParameters::SetClientNetworkParameters now searches the sending device’s own Network Port objects for one whose network number matches the destination’s, and trusts that port’s network type (BACnet/SC or BACnet/IP) instead of always assuming IPv4 for any 6-octet MAC. When multiple ports share an unconfigured/default network number, the match is ranked by Network_Number_Quality rather than by object-instance order.
  • rb e-20260915-12 (#643): the BACnet/SC broadcast-VMAC convention (all 6 octets 0xFF) is now used for the SC-resolved broadcast test, instead of the IPv4-shaped 4-octet-prefix test previously applied to every 6-octet MAC; IsRecipientAddressBroadcast’s confirmed-notification-to-broadcast refusal (#385) is updated to match, so it no longer disagrees with the network type SetClientNetworkParameters just resolved.
  • rb e-20260915-15 (#1085): two stack-originated errors used the wrong error-class for their error-code, per 135-2024 cl. 18 - BACnetCreateObjectProcessor paired NO_SPACE_FOR_OBJECT with OBJECT instead of RESOURCES (cl. 18.4); BACnetStackProgram’s READY-gate and Figure-12-3 rejections paired INVALID_OPERATION_IN_THIS_STATE with PROPERTY instead of OBJECT (cl. 18.2, easily confused with the similarly-named PROPERTY code INVALID_VALUE_IN_THIS_STATE).
  • rb e-20260915-15 (#1085): BACnetError::SetError now runs a debug-build-only diagnostic check (no behavior change in Release, never rejects) confirming a stack-originated error-class/error-code pairing is legal per cl. 18, covering the device/object/property/resources/security classes. Ingress (Decode/SetErrorClass/SetErrorCode) is documented as intentionally tolerant of any pairing, per cl. 23’s vendor/future-revision extensibility.
  • rb e-20260915-19 (#2025): extended #1085’s debug-build error-class/error-code legality check to all 8 BACnetErrorClass values (previously 5 of 8; services/communication/vt were left uncovered). The completed check found and fixed 3 real illegal pairings: BACnetSCDirectConnectManager’s duplicate-VMAC NAK used resources/NODE_DUPLICATE_VMAC instead of communication/NODE_DUPLICATE_VMAC (cl. 18.7); BACnetHealth’s “no error” reset default, used in two places, paired object/SUCCESS instead of security/SUCCESS (cl. 18.5) - only surfaced once coverage reached the classes this pairing touches.
  • rb e-20260915-19 (#2025): resolved #1085’s own originally-disclosed uncertain finding (property/INVALID_CONFIGURATION_DATA, used by BACnetReinitializeDeviceProcessor) - confirmed correct, not a bug, per cl. 16.4.1.3.1’s own ReinitializeDevice Error Type table, which explicitly specifies this exact pairing for network-port validation failures.
  • rb e-20260915-24 (#2038): Network Port’s Routing_Table property (428) could not be read in a customer-facing (non-BACNET_STACK_TESTTOOL) build - an empty routing table was never marked valid (InsertObjectAndTakeOwnership, which marks the value valid as a side effect, is never called for a zero-entry table), so BACnetObjectBase::EncodeWithContext refused to encode it and the whole ReadProperty aborted. Added the same SetValid(true) guard for the empty-list case that every sibling list property already has (BACnetStackNetworkPortSC.cpp’s hubFunctionConnectionStatus and others). A separate, larger gap - BACnetPhysicalRouter’s configured ports/routes are never wired into the routing table for non-SC routers, so it now reads back valid-but-empty rather than reflecting real router state - is filed separately as #2041.
  • rb e-20260915-27 (#2045): fixed a continuous internal error-log flood (“Failed to set the date”/“Failed to set the time”) triggered by BACnetStack_AddEventLogObject and any other caller that constructs or resizes a ChipkinCircularBuffer<T> whose record type carries a BACnetDateTime member (also affects Trend Log, Trend Log Multiple, and Audit Log, which share the same template). Root cause: the buffer’s constructor and Resize() copy-constructed every slot from a single shared, deliberately-invalid default record via std::vector<T>(n, T()), and Resize()’s own final buffer swap re-copy-constructed every slot a second time via a std::vector copy-assignment - each copy fired BACnetDateTime::operator=()’s own error log for its invalid source. Fixed by value-initializing each slot independently (std::vector<T>(n)) and move-assigning the resized buffer instead of copying it. No functional/data change; purely eliminates the log noise.
  • rb e-20260915-28 (#2033): STACK_OPTION_DM_LM_LIST_MANIPULATION (AddListElement/RemoveListElement, cl. 15.1-15.2) was missing from the STACK_OPTION_ENABLE_ALL_BIBBS group STACK_OPTION_TARGET_FULL pulls in, so a customer-facing (non-test-tool) STACK_OPTION_TARGET_FULL build never got the feature even though that preset’s own doc comment promises “ALL BIBBs” - previously only BACNET_STACK_TESTTOOL builds and the standalone STACK_OPTION_PROFILE_B_RTR preset turned it on. One-line preset-config fix; the feature’s implementation was already complete and already exercised by those two other build shapes.
  • rb e-20260915-3 (#1846): BACnetRespondingStateMachine::Loop() now revisits RespondingState_SegmentedRequest and enforces cl. 5.4.5.2’s Timeout transition (4x APDU_Segment_Timeout) - a peer that sends segment 0 of a segmented request and then goes silent no longer pins the reassembly and its accumulator’s heap forever.
  • rb e-20260915-3 (#1846): a device with no CallbackGetSystemTime registered now refuses to enter SEGMENTED_REQUEST (Abort segmentationNotSupported) instead of holding state that could never time out, mirroring the existing refusal on the SEND direction (#380).
  • rb e-20260915-3 (#1846): a received segment that would exceed the device’s Max_Segments_Accepted or the existing byte cap now aborts with BUFFER_OVERFLOW (was the generic OTHER), matching cl. 5.4’s client-side NewSegmentReceived_NoSpace analogy.
  • rb e-20260915-30 (#2041): a physical router’s Routing_Table (property 428) was only ever populated - and only ever ENABLED - on the one Network Port object a BACnetDataLinkSC instance happened to be tracking. A plain physical router with no BACnet/SC configured (e.g. IPv4/MS-TP only) always answered unknownProperty for Routing_Table on every Network Port object, regardless of how many ports/routes were actually configured. Fixed by pushing the router’s current route table into every Network Port object on every device, every Tick(), enabling/disabling the property to match whether a physical router is currently enabled - mirroring the existing BACnet/SC sync’s own enable/disable pattern rather than only its stored-value assignment.
  • rb e-20260915-4 (#2003): RunIdleState and RunSegmentedRequestState (BACnetRespondingStateMachine) now guard against a NULL outgoingPacket before building a response into it, closing a latent null-pointer dereference on the BACnetDataLink::RunRespondingStateMachine call shape (a real incoming packet with nowhere yet to put a response) - mirrors the existing guard RunAwaitResponseState already had for the same shape.
  • rb e-20260915-6 (#2007): the client’s received-segmented-ComplexACK reassembly (BACnetStackReceivedSegmentedResponses) now sends a negative SegmentACK naming LastSequenceNumber (cl. 5.4.4.4 SegmentReceivedOutOfOrder) for an out-of-order segment, instead of silently dropping it with no feedback to the peer - the peer previously only recovered via its own segment-ack-timeout.
  • rb e-20260915-6 (#2007): added DuplicateInWindow (cl. 5.4.2.2) to the client-side reassembly, mirroring the server-side implementation (#1845), for correctness once window sizes greater than 1 are supported.
  • rb e-20260916-13 (#1651): BACnetReinitializeDeviceProcessor::Process now re-validates reinitializedStateOfDevice (a closed 0..7 enumeration, cl. 16.4.2) before it reaches CallbackReinitializeDevice or HandleReinitializedStateOfDevice, rejecting an out-of-range value with BACnetRejectReason::parameterOutOfRange, per the owner’s ruling that the stack — not the host callback — is the admissibility authority for it. BACnetReinitializeDeviceRequest::Decode already rejects an out-of-range wire value (#702), so this is defence-in-depth against a caller mishandling a failed decode, not a live gap in the wire path.
  • rb e-20260916-15 (#2073): RemoveListElement on a Notification Class object’s Recipient_List was unsupported — the stack-store remove helper had no arm for it, so a customer build answered Error(DEVICE, OPERATIONAL_PROBLEM) and a test-tool build could silently SimpleACK a removal that never touched the stack-held list. Added BACnetStackNotificationClass::RemoveProperty (element-wise removal, cl. 15.2.1.3.1 atomicity, full structural BACnetDestination::IsEqual matching), a new arm in BACnetDBDevice::RemoveGeneratedPropertyValueElement with the #1328 DAB Release() bookkeeping extended to per-element removal, and a defence-in-depth guard in the host-served fallback path so a genuinely stack-backed Recipient_List can never be silently rewritten through the host. AddListElement on the same property was already wired end-to-end but had zero test coverage — added.
  • rb e-20260916-17 (#2076): Notification Class Recipient_List’s DAB Acquire/Release bookkeeping (the whole-list WriteProperty path and the per-element RemoveListElement path) used a presence-based old-vs-new diff, but DABTable is refcounted — two Destination entries referencing the same device instance with different Process Identifiers each hold their own reference, and dropping only one of them never triggered Release() since the instance was still “present” via the other recipient, permanently leaking a stale DAB entry once the second one was later removed too. Both call sites now share a single, device-instance-COUNT-aware reconciliation helper that Acquires/Releases exactly the occurrence-count delta.
  • rb e-20260916-19 (#2069): the server-side NewSegmentReceived_InconsistentAttributes transition (cl. 5.4) only compared a fixed set of header fields between segments of an incoming segmented Confirmed-Request (invoke-id, service choice, max-segments-accepted, max-apdu-length-accepted, segmented-response-accepted) — it never compared SC cl. 5.1.3 data_attributes, so a client sending segments with inconsistent attributes went undetected. Threaded BACnetDataLink::ProcessAPDU’s existing (#2017) incomingDataOptions one hop further into the responding state machine, and reused #2018’s shared comparison helper (BACnetStackReceivedSegmentedResponses::AreDataAttributesConsistentForLaterSegment, unmodified) to close the server-side mirror of that gap.
  • rb e-20260916-22 (#741, step 1): many hand-written BVLL test fixtures across tests/ had a declared BVLC length that didn’t match their own actual byte count — a stale/miscalculated value with no explanatory intent, distinct from the codebase’s genuinely-intentional *_WithPadding tests that deliberately exercise Annex J’s tolerance for a shorter declared length. Corrected 18 such fixtures across 5 files. The remaining local-NPDU declared-length clamp itself (previously reverted twice, exactly because of these stale fixtures) is separate, later work — not re-landed here.
  • rb e-20260916-24 (#2086): WriteProperty to a DateTime Value or DateTime Pattern Value object’s Present_Value failed to decode past the Date field. GetPropertyValueDatatype’s presentValue override (intended for properties like AnalogOutput’s Present_Value that can be NULL or Real, disambiguated only by the wire’s own application tag) fired unconditionally, discarding the property profile’s own BACnetStackDatatypes::DateTime datatype and replacing it with just the first application tag’s number (Date’s tag, 10) — losing the fact that a Time application tag follows. The override now excludes the case where the profile already resolved to BACnetStackDatatypes::DateTime.
  • rb e-20260916-26 (#2085): BACnetFaultParameterFaultOutOfRangeValue (the real/unsigned/double/integer CHOICE used for BACnetFaultParameter’s fault-out-of-range min-normal-value/max-normal-value, ANSI/ASHRAE 135-2024 clause 21) encoded and decoded each alternative with a context tag from its own internal [0]-[3] CHOICE numbering. The spec leaves these alternatives untagged — each carries its own application tag (Real=4, Unsigned=2, Double=5, Integer=3) inside the enclosing [0]/[1] constructed tags. As a result the stack rejected a conformant Fault_Parameters value and produced one a conformant peer couldn’t decode. Fixed by selecting the decode branch from the wire’s application tag and encoding each alternative with its own application tag.
  • rb e-20260916-28 (#2087): the Node adapter’s RegisterCallbackReinitializeDevice TypeScript declaration (api.ts) declared a 5-argument handler including passwordLength: number, but the native addon calls the handler with only 4 arguments — the password Buffer’s own length already conveys it. A handler written to the stale declaration received the errorCode Buffer where it expected passwordLength, and undefined for errorCode, throwing inside Tick() on its first write to the error code. Also fixed a related null-pointer risk in the same handler: Napi::Buffer<char>::Copy() was called with a null password pointer for a ReinitializeDevice request carrying no password.
  • rb e-20260916-31 (#2098): the Node adapter’s RegisterCallbackAcknowledgeAlarm TypeScript declaration (api.ts) declared an acknowledgmentSourceLength: number argument the native addon never passes — the acknowledgmentSource Buffer’s own length already conveys it, the same bug class fixed for RegisterCallbackReinitializeDevice in #2087. A handler written to the stale 29-argument declaration would receive acknowledgmentSourceEncoding’s value where it expects acknowledgmentSourceLength, with every subsequent parameter shifted one slot early.
  • rb e-20260916-33 (#2097): the Node adapter’s RegisterCallbackDeviceCommunicationControlWrapped (interface_callbacks.cc) called Napi::Buffer<char>::Copy() with a NULL password pointer when a DeviceCommunicationControl request carried no password — Napi::Buffer<char>::Copy() with a null source is not documented as safe by node-addon-api, even at length 0. Same fix as #2087: an empty buffer is now built directly for the no-password case instead of copying from NULL.
  • rb e-20260916-35 (#2105): the Node adapter’s RegisterCallbackLifeSafetyOperationWrapped (interface_callbacks.cc) called Napi::Buffer<char>::Copy() with a NULL requestingSource pointer when a LifeSafetyOperation request carried no requesting source — the same bug pattern already fixed for #2087 and #2097. An empty buffer is now built directly for that case instead of copying from NULL.
  • rb e-20260916-37 (#2095): retransmitted confirmed requests and HookTimeout reported networkPortInstance 4194303 (BACNET_UNKNOWN_DEVICE_INSTANCE) instead of the original send’s Network Port instance, because SentConfirmedRequestKey was never given one — BACnetDataLink::HandleConfirmedRequest, BACnetDatabase::AddSentConfirmedRequest, and BACnetStackSentConfirmedRequests::InsertRequest had no way to thread it through. An application routing frames by Network Port instance couldn’t tell which data link a retransmission belonged to. Fixed by threading GetNetworkPortInstanceForSend()’s resolved instance from the original send through to the stored key, so every retry and timeout now reports the real instance.
  • rb e-20260916-39 (#2094): BACnetGroupChannelValue (one entry of a WriteGroup change list) encoded and decoded fields in the wrong order and wrapped its value in an extra context tag. ANSI/ASHRAE 135-2024 clause 21 has channel [0], overridingPriority [1] OPTIONAL, then value (a BACnetChannelValue CHOICE) carrying its own application tag or its own constructed alternative’s [0]/[1]/[2] tag — no enclosing wrapper. The stack’s extra [1] wrapper around value (with overridingPriority moved to [2] after it) made a WriteGroup the stack sends unreadable by a conformant device, and the wrapper was ambiguous with the CHOICE’s own xy-color [1] tag. Fixed by reordering the fields and selecting/encoding the BACnetChannelValue alternative by its own tag instead of wrapping it.
  • rb e-20260916-41 (#900, part 1): every intrinsic/fault algorithm setter in BACnetDBDevice.cpp (28 call sites) logged the identical, unhelpful "alarmAndEventObject does not exist. Please create one first using SetAlarmsAndEventsForObjectEnabled" when the object’s alarm/event configuration wasn’t set up yet — naming an internal type instead of the actual BACnetStack_Set* export a customer called, or which object failed. Each occurrence now names its own function’s real customer-facing export and the failing object’s type/instance.
  • rb e-20260916-43 (#741, step 2 of 2): BACnetDataLinkIPv4::ProcessIncomingPacket handed local NPDU processing the whole remaining UDP datagram instead of clamping it to the BVLC-declared length, while the BBMD forward paths elsewhere in the same file already used the declared-length-clamped copy. Trailing bytes past a datagram’s declared BVLC Length (a legal Annex J.2 padding tolerance) could be parsed as if they belonged to the message being processed locally — for example, a WhoIs followed by trailing bytes shaped like a device-instance-range would have that range applied even though it was never part of the declared message. Local processing now uses the same declared-length-clamped payload the forward paths already use. (Step 1, correcting hand-written test fixtures whose declared BVLC length didn’t match their own byte count — the reason this fix was twice reverted — landed separately as e-20260916-22/#2088.)
  • rb e-20260916-47 (#2126): fixed Command_Time_Array reads aborting (Abort(other)) on Lighting Output / Binary Lighting Output objects whenever any priority slot had never been commanded (or had been relinquished). BACnetStackObjectHelpers::GetValueSourceProperty’s commandTimeArray case copied a never-commanded slot’s live BACnetTimeStamp element straight into the output array — that element is non-NULL but Reset()’d/invalid (BACnetStackValueSourceState’s constructor/ClearSlot only call Reset() on m_commandTimeArray[i], with no default-value set afterward) — and encoding the invalid element later hit BACnetTimeStamp::Encode’s own IsValid() guard, aborting the whole request. Now substitutes the same BACnetTimeStamp((time_t)0) default already used for the NULL-pointer case whenever the live element is invalid. Also fixed BACnetTimeStamp::Encode’s copy-paste error string (said BACnetHostAddress, now says BACnetTimeStamp) per the issue’s own suggestion. Investigated the issue’s other guess — that Value_Source_Array shares the same bug — and found it does not: m_valueSourceArray[i] always gets an explicit SetNone() right after Reset(), so it’s never left invalid; left that code path unchanged (confirmed via a revert-red test that correctly did not fail without the fix). Round-1 adversarial review (PR #2130) found the same bug was also reachable through the singular Last_Command_Time case (GetValueSourceProperty’s lastCommandTime branch): RecordCommand’s own none-choice path (a real value written with no known source, e.g. a WriteProperty processed with no network parameters) routes through ClearSlot, leaving m_commandTimeArray[index] invalid at a priority the object’s own priority array still reports as winning. Fixed the same way, with its own discriminating test.
  • rb e-20260916-53 (#2112): fixed the Rust adapter’s generated load_library() (ci_scripts/generate-rust-adapter.js, regenerating adapters/rust/cas_bacnet_stack_adapters.rs), which hardcoded a lookup for the Debug-named library at a cwd-relative path — dead-on-arrival for any real consumer, since every other adapter/example in the series ships Release only. Now defaults to the Release-named library (matching the sibling adapters’ convention) and reads a CAS_BACNET_STACK_LIBRARY_PATH environment variable to let a consumer override with a full path (including extension) to wherever they actually placed the library, per the issue’s own suggested fix. This file is not compiled by any gate in this repository (disclosed in its own header comment since #1483) and this environment has no Rust toolchain to compile-check it against — this change is unverified-by-compiler like every other change to this file has been since #1483’s one-time manual cargo check. The issue’s separate “no CI gate for Rust” ask is not addressed by this batch (needs a Rust toolchain this environment doesn’t have) and remains open on the issue as a disclosed gap.
  • rb e-20260916-7 (#1599): the Python adapter’s hand-maintained bacnet_propertyIdentifier dict (adapters/python/CASBACnetStackAdapter.py) was missing 72 of 522 entries from source/CASBACnetStackDLL.h’s enum BACnetPropertyIdentifier - mostly Protocol Revision 24-30 era additions plus the vendor-proprietary range (>= 4194304) - with zero value mismatches on the entries present. Added the 72 missing entries. A new self-test, adapters/python/CASBACnetStackAdapter_selftest.py, independently re-parses the header and diffs it against the dict, so a future drift is caught rather than shipping silently a second time.
  • rb e-20260916-9 (#1841, part B of #152): responding to ReadProperty for a list-shaped property (e.g. Object_List) double-encoded the property’s value: BACnetDBDevice::BuildObjectList already fully encodes it into its own memory buffer, then BACnetComplexACKPDU::InsertEncodedServiceAndCleanUpMemory re-encoded the whole ACK (value included) into a second, separate buffer, so both full-size copies were alive at once at peak - the direct cause of a customer’s segmented-ComplexACK heap exhaustion on a 63,500-byte device (#152). Added BACnetComplexACKPDU::InsertReadPropertyACKAndCleanUpMemory, which instead adopts the already-encoded value buffer in place (prepending the fixed prologue and appending the closing tag) when the value was pre-encoded this way, falling back to the original generic re-encode path for scalar/non-list properties whose value is still a live object graph. Measured on a 703/2000-object synthetic device: whole-path peak heap during processor+encode dropped from 7568→6449 bytes (703 objects) and 22808→21041 bytes (2000 objects). As a side effect, this also fixes MemoryAudit_Issue152 (tests/TestMemoryLeakRegressions.cpp), a 100-object peak-heap assertion that had been failing - and disclosed as an unrelated pre-existing failure in every batch’s test summary this session - because it was hitting the exact same double-copy, just at a smaller scale.
  • rb e-20260917-77 (#2175, severity:high): fixed BACnetReadPropertyMultipleProcessor::PrepareMultipleReadAccessResults() aborting the ENTIRE ReadPropertyMultiple response (a device-level Abort-PDU) when a single property in an ALL/REQUIRED/OPTIONAL expansion fails with an unclassified error, instead of embedding a per-property Error and continuing per cl. 15.7. PrepareReadAccessResult() already turns a classified GetProperty() failure into its own embedded propertyAccessError and returns true; it only returns false for an unexpected/unclassified failure (out of memory, or a property read path that never reports a real BACnet error). The loop’s own return false; on that case discarded every other property in the same expansion, not just the one that failed. Fixed by inserting a generic PROPERTY/OTHER propertyAccessError for that one property and continue-ing the loop instead. A concrete, still-reachable trigger: BACnetBusinessLogic::GetValueUsingCallbacks()’s relevantCallbackDeclined branch leaves the CErrorContainer completely unset when a registered CallbackGetPropertyConstructed callback declines a LIST-datatype property without setting an error code — reproduced end-to-end via Device_Address_Binding in the new test. Root cause #2 from the issue (why Device.Description specifically fails against the reporter’s own example app) needs debugging that app’s own callback implementation, which isn’t in this repo — explicitly left out of scope, per the issue’s own framing.
  • rb e-20260917-79 (#2163): BACnetBusinessLogic::UseCallbackGetList()’s default: case (any LIST-datatype property that isn’t Enumerated — e.g. Device_Address_Binding) unconditionally logged Callback logic for objectType=[%u], propertyIdentifier=[%u] not yet implemented. whenever no CallbackGetPropertyConstructed was registered, even though this is the fully-expected “nothing registered” case: the caller (GetValueUsingCallbacks’s own caller, via its listCallbackWasRegistered check) unconditionally falls back to a valid, correct empty list for exactly this condition. The FYI misrepresented a spec-correct response as an unimplemented gap. Same class as #2050/#2045 (noisy internal logging co-occurring with correct protocol behavior). Removed the log line; the genuinely interesting case — a registered callback that declines — already gets its own diagnostic via outDeclined/outErrorCode. Added DeviceAddressBindingRead_DoesNotLogNotYetImplementedFyi (captures stdout, since APIDebugLogInfo/Error fall back to printf() with no debug callback registered) confirming the FYI is gone while the empty-list behavior is unchanged.
  • rb e-20260917-81 (#2164, severity:high): BACnetDBDevice::BuildArrayProperty() explicitly called errorContainer->SetAbort(BACnetAbortReason::other) whenever a registered CallbackGetPropertyUInt declined to report an array property’s size — the normal, expected shape for an optional BACnetARRAY property (e.g. Network Port’s IP_DNS_Server) the application never implemented, matching every other “not implemented” fallback in this same file that answers with a per-property Error. Aborting the whole APDU exchange over one unimplemented optional property meant a client’s ReadPropertyMultiple/refresh could lose an entire batch of unrelated reads, not just this one. Fixed by calling SetError(BACnetErrorClass::property, BACnetErrorCode::unknownProperty) instead, matching this file’s own established convention — confirmed via BACnetReadPropertyProcessor::Process’s own if (!errorContainer.IsSet()) SetAbort(...) guard: since the error container is now already set, it correctly skips the abort escalation. Deliberately left the sibling CallbackGetPropertyUInt == NULL branch (a few lines earlier, when the callback isn’t registered at all) untouched — a broader, unverified condition distinct from what this issue actually reported and reproduced. Added ReadUnimplementedArrayProperty_ReportsErrorNotAbort, reproducing the exact log signature from the issue (Network Port / IP_DNS_Server) and confirming the response is now Error(PROPERTY, unknownProperty), not Abort.
  • rb e-20260917-83 (#2165): BACnetPacket’s 4 Decode* variants (DecodeAPDU, DecodeAPDUUsingSupportedServices, Decode, DecodeUsingSupportedServices) and BACnetNetworkLayer::ProcessNPDU() each logged a generic “Unable to decode the APDU”/“Failed to decode the apdu” whenever their inner decode call returned 0 — even though the inner decoder (BACnetConfirmedRequestPDU::Decode()/DecodeUsingSupportedServices()) already distinguishes a genuine decode failure from a successfully-parsed request for a service the device doesn’t support, setting error to Reject(unrecognizedService) and logging its own precise “Services is not supported service=[N]” message in that case. A routine, correctly-handled rejection (e.g. SubscribeCOV on a device that only implements DS-RP-B) was therefore logged identically to an actual malformed/corrupt APDU. Same class as #2050/#2045/#2163. Fixed by skipping the generic outer message whenever error.IsSet() — the inner call already logged something more specific. Fixed all 4 occurrences of the identical pattern in BACnetPacket.cpp (not just the one the issue explicitly named) for consistency, since they’re the same code shape calling the same inner decoders. Added DecodeUsingSupportedServicesUnsupportedServiceReportsRejectNotGenericDecodeFailure (captures stdout) confirming the generic message is gone while the inner “Services is not supported” diagnostic and the correct Reject(unrecognizedService) classification are both unchanged.
  • rb e-20260918-85 (#2151): BACnetDBDevice::SetGeneratedPropertyValue’s accessPoint branch no longer answers Error(object: unknown-object) for a WriteProperty when the internal Access Point engine was never provisioned (no public DLL entry point creates it) even though the object itself exists — falls through to the generic host-callback path instead, so a registered CallbackSetPropertyEnum is reached or the correct writeAccessDenied is returned.
  • rb e-20260918-85 (#2178): BACnetDailySchedule::Reset() now marks itself valid (an empty day is legal per 135-2024 12.24.6) instead of leaving IsValid() permanently false, which previously poisoned an entire Weekly_Schedule read and produced Abort(other) instead of the value; Decode()’s own failure path still explicitly re-poisons validity so malformed wire data is correctly reported invalid.
  • rb e-20260918-87 (#2187): BACnetDBDevice::SetGeneratedPropertyValue/GetGeneratedPropertyValue no longer answer Error(object: unknown-object) for accessCredential, credentialDataInput, timer, averaging, loadControl, lifeSafetyPoint, lifeSafetyZone, and program when the internal engine was never provisioned (no public DLL entry point creates it) even though the object itself exists — falls through to the generic host-callback/property-profile path instead, matching the already-fixed accessPoint (#2151). The Read-side fix additionally stops blocking generic properties (e.g. objectName) that need no engine at all.
  • rb e-20260918-89 (#2172): DecodeAs(JSON) no longer returns an empty UnknownAPDU for a BACnetColorCommand written to Color_Command — the property was profiled with datatype Any instead of ColorCommand, routing decode through the generic AbstractSyntaxType path instead of BACnetColorCommand’s own decoder.
  • rb e-20260918-91 (#2194): TestBACnetInterfaceTickCallbackBounds.TickRejectsOversizedMessageLength no longer leaks a registered debug callback (RegisterCallbackLogDebugMessage) into later tests in the same suite run — the actual root cause of two previously-worked-around CaptureStdout() false negatives this session (#2165’s TestBACnetPacket.cpp, and TestBACnetEventNotificationConfirmedBroadcastRefused.cpp). Its fixture now resets the callback in TearDown(), matching TestBACnetStackDebug.cpp’s own precedent.
  • rb e-20260918-93 (#2156): ci_scripts/review-file-issues.py’s family-of command now resolves adapters/** (F30) and ci_scripts/**/projects/** (F31) to real families instead of the F99 catch-all that was supposed to stay empty but silently didn’t — the resolver runbook’s own claim-time family auto-labeling and soft family lock now work for these paths.
  • rb e-20260919-103 (#2212): adding an MS/TP Network Port object now automatically claims an MS/TP data link instance for it, mirroring BACnet/IP’s ClaimIpv4DataLinkForNewNetworkPortObject wiring into AddNetworkPortObject/AddNetworkPortObjectWithNetworkNumber. Follow-up to #2153/#2154/#2155: ClaimMstpDataLinkForNetworkPort (#2154) existed but nothing called it from the object-creation path, so #2155’s own router-port-binding tests had to call it directly to exercise anything at all — revert-red: with the new wiring removed, AddNetworkPortObjectWithNetworkNumber/AddNetworkPortObject for an MS/TP Network Port object left FindMstpDataLinkByNetworkPort/FindMstpDataLinkByNetworkPortInstance returning NULL (Value of: claimed != 0 / Actual: false / Expected: true), while the IPv4 control arm was unaffected.
  • rb e-20260919-105 (#2213): BACnetDataLinkLayer::RebindRouterPorts now also re-derives BACnet MS/TP router-port bindings, mirroring the existing BACnet/SC and BACnet/IP two-phase resolve-then-apply blocks with ResolveMstpRouterPortBinding/ApplyMstpRouterPortBinding (#2155). Follow-up to #2155/#2212: the MS/TP resolve/apply pair existed but was never wired into RebindRouterPorts itself, so a router-port configuration change at runtime (e.g. a Network_Number rewrite, picked up every router tick via BACnetNetworkLayer::LoopRouter) re-derived SC and IPv4 bindings but left an existing MS/TP router-port binding stale/unrefreshed — revert-red: with the MS/TP block removed, a router port whose network number newly matched a different MS/TP data link instance stayed bound to its original instance forever (GetRouterPortId() stuck at 2 instead of moving to 1), while a control test with a stable configuration was unaffected.
  • rb e-20260919-97 (#2202): DecodeAs(JSON) no longer discards the whole packet when a BACnetObjectType enumerated value is color (63) or colorTemperature (64) — EnumerationFactory() was missing these two cases despite the enum values being otherwise fully supported. Additionally, an unrecognized enumerated value now degrades to an UnknownAPDU for just that one member instead of failing the entire packet decode.
  • rb e-20260919-97 (#2203): Current_Health and SC_Primary_Hub_Connection_Status now render their decoded members in DecodeAs(JSON) output — both classes decoded correctly but had no GetElements() implementation, so JSON rendering was always empty. Color’s Present_Value/Tracking_Value now decode both members of the underlying BACnetxyColor (x and y) instead of only the first — the property was mis-registered with datatype Any instead of xyColor.
  • rb e-20260922-114 (#2249): BACnetStackRespondingStateMachineCurrentState::InWindow used ((seqA - seqB) % 256) < m_actualWindowSize - seqA/seqB are uint8_t, so the subtraction promotes to int, and when seqA < seqB the result is negative; C++’s % preserves the dividend’s sign (truncated division), so a wrapped-around difference stays negative instead of wrapping into [0,255], always satisfying < m_actualWindowSize for any positive window size and misclassifying a wrapped-around SegmentACK sequence number’s in-window status. Changed to (uint8_t)(seqA - seqB) < this->m_actualWindowSize, matching the already-correct sibling DuplicateInWindow (RB185, docs/runbooks/runbook-resolver-lessons.md) and the client-side SentConfirmedRequestValue::InWindow (#2013) - revert-red: with the pre-fix code restored, state.InWindow(2, 250) (window size 8) returned true, the actual observed terminal output was Actual: true / Expected: false, against the corrected code’s false.
  • rb e-20260922-119 (#2251): bumped the Node adapter’s node-gyp devDependency ^11.5.0 -> ^12.4.0 - node-gyp 11’s find-visualstudio.js version-mapping table only knows Visual Studio 2019/2022, so a machine with Visual Studio 2026 (major version 18) reads as “unknown version undefined” and npm run build:cpp fails at configure with no addon produced; node-gyp 12.4.0 (the latest 12.x) accepts the same VS 2026 installation and compiles the same source unchanged. node-gyp 13 was not used: it requires a newer Node than this package’s engines range supports and fails at configure. Deliberately did not narrow engines.node to match node-gyp 12’s tighter ^20.17.0 || >=22.9.0 floor - README.md documents Node 18.20.8 as a verified native-addon-build target, so narrowing would silently contradict a standing, disclosed support claim; the resulting gap (building from source on Node 18.x, 20.0-20.16 or 22.0-22.8 now fails where it previously worked; CI and published-package consumers are unaffected) is disclosed in a //engines-node-gyp-12-note field next to engines in package.json for the owner to resolve deliberately.
  • rb e-20260922-121 (#2248): 135-2024 cl. 5.4.4.2 SEGMENTED_REQUEST(client) UnexpectedPDU_Received - a SimpleACK-PDU, non-segmented ComplexACK-PDU, or Error-PDU arriving for a request this device is still sending segments of (SentConfirmedRequests entry IsSegmenting()==true) now sends a BACnet-Abort-PDU (abort-reason=invalidApduInThisState), removes the transaction, and fires HookAbort, instead of being delivered as if the send had already completed. Added at the three points that previously assumed “any SimpleACK/ComplexACK/Error means AWAIT_CONFIRMATION” (two in BACnetDataLink::ProcessAPDU, one new case BACnetPDU::error: there) via a single peek-only helper, BACnetDataLink::HandleUnexpectedPDUForSegmentingClient; the far more common ordinary (non-segmented) path is untouched - proven by both a dedicated control-arm test and the unmodified full suite. Local-application SendAbort (the other transition #2248 named) was investigated and found to need a new public API this codebase does not yet expose for aborting an outstanding client-sent request (segmented or not); left unimplemented rather than inventing new surface on CASBACnetStackDLL.h/.cpp under this batch’s own authority - revert-red: the three new discriminating tests fail (hookAbortFireCount stays 0; the PDU is delivered as an ordinary SimpleACK/Error and the ComplexACK reaches its normal ack processing) with the guard’s own body forced to return false.
  • rb e-20260922-123 (#2014): 135-2024 cl. 5.4.4.3 TimeoutSegmented - the final piece of #2006’s segmented-send decomposition (part D, after #2011/#2012/#2013/#2248). Resolved the issue’s own flagged ambiguity by re-reading cl. 5.4.4 as a whole against #2011’s landed WouldRequireCannotSendAbort preflight: an initially-oversized request can never reach AWAIT_CONFIRMATION unsegmented in the first place (SendConfirmedUnsegmented’s own cl. 5.4.4.1 precondition already guarantees length <= maximum-transmittable-length for anything it ever sends, independent of CannotSend); TimeoutSegmented is really about a request that WAS sent segmented, completed (FinalACK_Received, cl. 5.4.4.2), and is now sitting in AWAIT_CONFIRMATION awaiting the final reply, whose RequestTimer then expires because that reply never arrives. BACnetStackSentConfirmedRequests::Loop() now recognizes this case via SentConfirmedRequestValue::GetOutgoingPacket() != NULL (populated only by SetSegmentationState, called only from SendConfirmedSegmented, whose own precondition IS length > maximum-transmittable-length, and never cleared by StopSegmentingAndStartRequestTimer’s FinalACK_Received handling) and, on a retry-eligible timeout, rebuilds and resends the first segment (sequence zero) via the existing BACnetPacket::CreateSegment/segment-encode machinery, re-entering SEGMENTED_REQUEST (SentConfirmedRequestValue::RestartSegmentedRetry) instead of retransmitting the whole oversized APDU as one raw unsegmented frame. An ordinary (never-segmented) request’s TimeoutUnsegmented retry is unaffected (GetOutgoingPacket() stays NULL for its whole life). SentConfirmedRequestValue::SetSegmentationState gained a new trailing proposedWindowSize parameter (threaded through BACnetStackSentConfirmedRequests/BACnetDatabase/BACnetInterface::SendConfirmedSegmented) so the retry reuses the transaction’s original ProposedWindowSize - cl. 5.4.4.3’s own text does not re-set it, unlike SendConfirmedSegmented’s. New tests: tests/TestBACnetInterface_TimeoutSegmented.cpp - a discriminating test driving a segmented send to FinalACK_Received completion, expiring RequestTimer, and proving the retry is segmented (not raw) and the transaction is genuinely alive again (a SegmentACK for the retried segment 0 drives the next segment), plus a control arm proving an ordinary unsegmented request’s TimeoutUnsegmented retry stays byte-for-byte unaffected. Revert-red verified: with the source fix reverted, the discriminating test’s liveness assertion fails for real (the pre-fix retry resends segment 0’s stored bytes, which happen to still look “segmented” on the wire, but never resets IsSegmenting()/the segmentation bookkeeping, so a SegmentACK for it is silently ignored exactly like an ordinary stray-SegmentACK - no further segment is ever sent); restoring the fix and rerunning the full suite (8691 tests) reconfirms green, only the pre-existing MemoryAudit_Issue152.ObjectListEncode_PeakHeapBaseline failure.
  • rb e-20260922-132 (#2274): two code comments (source/BACnetStackSentConfirmedRequests.cpp, tests/TestBACnetInterface_TimeoutSegmented.cpp) that cited a lesson file deleted by CONSOLIDATE e-20260922-consolidate13 now cite its folded entry, RB361 in docs/runbooks/runbook-resolver-lessons.md; check-md-links.js was red on the first at the issues/runbook tip. Comment-only.
  • rb e-20260922-133 (#2275): the Node addon’s SendReadPropertyWithLength checked the packed propertyArrays Buffer against the element count only, then passed the caller’s propertyArraysLength to the DLL without comparing it to the Buffer, so a small Buffer with a large length made the DLL read past its end (#392 class). The packed-element-buffer role in ci_scripts/adapter-roles.json now takes an optional lengthParameter (checked by adapter-roles-verify.js), and the generator emits ValidateBufferLength on it before holding the count against that length; it is set on all four exports of this shape. That was the only difference from the hand-written SendReadProperty, SendWriteProperty and SendCreateObject, so all three are now generated (bespoke allowlist 57 -> 54); the two write-side wrappers no longer narrow propertyArraysCount to 8 bits or propertyArraysLength to 16 bits. The test-tool SendConfirmedEventNotificationWithOutOfRangeValues also gains the missing sentInvokeId output-buffer check (#391 class). Revert-red: node-adapter-output-buffer-guards: 1 JS buffer(s) read through with an unchecked caller-supplied length: ... SendReadPropertyWithLengthWrapped.
  • rb e-20260922-133 (#2278): ci_scripts/check-node-api-returns.js now checks the return types of the 20 arrow members of api.ts’s export const api = { ... } object against the napi kind their wrapper returns. It read only column-0 function heads, and its head-count cross-check used the same regex, so it could not see what it missed. The member parser is shared with check-node-api-params.js (ci_scripts/lib/api-ts-members.js), and the count covers both shapes (383 = 363 functions + 20 members). 17 members are checked and match; the 3 that return a result object are pinned in node-api-return-allowlist.txt. No api.ts declaration was wrong. The gate stays deferred under #1641 and was run with the deferral lifted. Revert-red: check-node-api-returns (self-test 4/5): FAIL - an arrow member declared number over a Boolean-returning wrapper was NOT caught.
  • rb e-20260922-133 (#1599): the Node adapter’s BACnetStackSetting and BACnetDecodeType are no longer hand copies in api.ts - they are imported from the generated adapter-enums.ts, with the same names, members and values (no behaviour change; the hand copy’s missing Device_Address_Binding entries were already filled in by #2002). A generated copy cannot fall behind the header again.
  • rb e-20260922-134 (#2281): the Node addon’s SendCreateObject now rejects a propertyArrays whose declared propertyArraysLength is shorter than 13 bytes per declared element. The guard used 7 bytes per element, but since #961 the minimum element is 13 bytes plus the value (source/CASBACnetStackDLL.h; BACnetStackCreateObjectAsync::MIN_ARRAY_SIZE = 13: flags 1 + propertyIdentifier 4 + propertyArrayIndex 4 + dataType 1 + priority 1 + valueLength 2, and the value may be empty). The DLL already rejected such a buffer, so this is a stricter early error rather than a memory-safety fix. For example, count 2 with length 20 used to pass the addon check (20/7 = 2) and is now refused (20/13 = 1). bytesPerElement for the role in ci_scripts/adapter-roles.json is now 13, and the wrapper has been regenerated. The refusal of count 2 / length 20 follows by construction of ValidatePackedElementBuffer (the addon compiles with the #1641 deferral lifted: ADDON_COMPILE_STRICT=1 addon-compile.sh 12/12), but no runtime test exercises it. Revert-red, run with the #1641 deferral lifted (role at 13, wrapper at 7): generate-napi-wrappers: FAIL - 1 generated wrapper(s) differ from what the generator emits now: SendCreateObject.
  • rb e-20260922-135 (#2285): node ci_scripts/generate-napi-wrappers.js --write now rewrites every napi wrapper carrying the // GENERATED by marker to the generator’s current output, whether or not its current body still matches. Before, it rewrote only wrappers whose behaviour signature still matched, so a role change in ci_scripts/adapter-roles.json that altered a generated wrapper’s checks reported “nothing to do” and left the old check in place until someone edited the generated code by hand (e-20260922-133 #2275, e-20260922-134 #2281). A marked wrapper whose export is on the bespoke allowlist, or that the generator cannot produce, is now reported and left as it is - even when its body still matches - while the run’s other rewrites are applied, and --write exits 1. Unmarked hand-written wrappers are still never overwritten. A new --self-test arm runs --write against a scratch copy with a role removed from a generated wrapper. It checks that the wrapper is refreshed, that no other wrapper changes, that a second run does nothing, and that a marked allowlisted wrapper is refused and left byte-for-byte as it is, both when its signature differs and when it still matches. The scratch-copy override variables are honoured only in the self-test’s own child process, so a stray shell variable cannot redirect --gate or --write.
  • rb f-20260914-1 (#1240, CR-F09-012): BACnetSCHubConnector::ProcessAdvertisement now records an incoming Advertisement’s Hub Connection Status, Accept Direct Connections, Maximum BVLC Length and Maximum NPDU Length against the peer websocket (135-2024 AB.3.2), instead of logging and ignoring the message. BACnetSCWebsocket gains a new SetAdvertisedPeerStatus mutator and two new peer-status accessors.
  • rb f-20260914-2 (#1139, CR-F13-004): BACnetYouAreProcessor::Process now reads and validates the Device MAC Address half of an incoming You-Are (135-2024 16.11.4). MAC-length validity gates acceptance of the whole request (6 octets for BACnet/IP, 1 octet 0-127 for MS/TP); BACnet/SC (VMAC) ports ignore the MAC entirely (H.7.2); the existing identity-match gate now applies to both assignment forms; an I-Am is sent after an accepted assignment. Actually changing the device’s MAC address remains deferred to #1839.
  • rb f-20260917-1 (#2096): BACnetBusinessLogic::GetProperty no longer aborts an unrelated, required, LIST-typed property read just because any CallbackGetPropertyConstructed was registered somewhere in the device. UseCallbackGetList’s constructed-BLOB fallback now propagates the callback’s actual decline/error-code signal instead of discarding it, so a per-property decline surfaces as a clean error rather than an unset-error failure that escalated to Abort.
  • interface-batch-2 (#1140, SECURITY): a DeviceCommunicationControl password decoded off the wire is now rejected when it exceeds the 20-character limit of cl. 16.1.1.1.3. SetPassword() enforced that cap, but the decode path wrote the password directly and never went through it, so an oversized password reached CallbackDeviceCommunicationControl — whose passwordLength is a uint8_t. The cap is character-aware: the stack stores raw encoded octets (#580), and cl. 20.2.9 permits multi-octet charsets, so the bound is the most octets 20 characters can occupy in the string’s declared encoding (40 for UCS-2, 80 for UCS-4/UTF-8, 20 for single-octet charsets). A spec-legal 20-character multi-byte password is still accepted.
  • interface-batch-2 (#938): the inbound audit-notification callback now receives the target-property reference (identifier + optional array index), the optional target value and current value as application-tagged BLOBs, and the optional result’s error class/code. Previously only a bare useTargetProperty boolean reached the callback: a consumer could learn that a target property existed but not which one, and the three value-bearing optionals the decoder had already parsed were dropped. FPCallbackAuditNotificationReceived and BACnetStack_RegisterCallbackAuditNotificationReceived gain the new parameters — test-tool-only (the export lives in CASBACnetStackTestToolDLL.h), not part of the frozen production ABI.
  • interface-batch-2 (#1232, CR-F01b-018): IsPacketBroadcasted logged a NULL networkParameters and then dereferenced it — the missing return false; is added.
  • interface-batch-2 (#1232, CR-F01b-012): a confirmed-request outgoingPDU that is non-NULL but invalid is now SAFE_DELETEd instead of leaked.
  • interface-batch-2 (#1232, CR-F01b-013): GetProperty now checks that its stored-value copy actually succeeded, answering outOfResources instead of reporting success with an invalid value.
  • interface-batch-2 (#1232, CR-F01b-014): ElevatorGroup’s landingCallControl read fell through implicitly into default: on callback failure. The case now handles its own failure explicitly and sets outRelevantCallbackDeclined like the sibling landingCalls case. The return value is unchanged, and the flag has no observable effect for this property today (GetProperty only consults it for list-shaped properties, which landingCallControl is not) — the fix is the explicit control flow.
  • interface-batch-2 (#1232, CR-F01b-016): the ten GetPriorityArray* siblings now check InsertObjectAndTakeOwnership’s return value, as GetPriorityArrayReal already did — a failed insert is reported instead of silently yielding a short array.
  • interface-batch-2 (#1232, CR-F01a-009): AddNetworkPortObject/AddNetworkPortObjectWithNetworkNumber now guard their BACnet/SC claim-failure rollback on whether the object pre-existed, adopting AddDevice’s own pattern — the add steps are idempotent, so an unguarded delete could destroy an object an earlier successful call created.
  • interface-batch-2 (#1232, CR-F01a-010): SendSubscribeCOV/SendSubscribeCOVProperty no longer leak serviceRequest when a post-construction setter fails.
  • interface-batch-2 (#1232, CR-F01c-010): BACnetStack_SendNetworkNumberIs validates networkNumberQuality’s upper bound before the enum cast.
  • interface-batch-2 (#1232, CR-F01c-016): BACnetTestToolInterface::RegisterCallbackProcessAuthRequest takes FPCallbackProcessAuthRequest directly instead of a void*-shaped signature it reinterpret_cast into the typed pointer the processor actually calls it through — that cast was undefined behavior. Test-tool-only; a host callback must now use the typed signature.
  • rb napi-20260908-1 (#1509): BACnetStack_SetBACnetSCWebSocketStatus’s Node wrapper returns its result. The export is declared bool and documented “True if a websocket or peer matching uri was found and updated, otherwise false”; the wrapper discarded it and returned undefined, so a JavaScript caller could not distinguish success from any documented failure - no matching uri, no BACnet/SC data link, or the stack built without the option. Customer-visible: the function now returns a boolean where it returned undefined.
  • rb napi-20260908-1 (#1509): five more Node wrappers hand JavaScript the type their export declares. AddVirtualNetwork is declared bool and returned a Number; ReadTrendLogRecord, ReadTrendLogMultipleRecord, GetBDTEntry and GetFDTEntry are declared uint32_t and returned a Boolean. Customer-visible: AddVirtualNetwork now returns true/false where it returned 1/0, and the other four return the byte count their \return documentation describes where they returned true/false. All six are generated from the header now rather than hand-written, so the disagreement cannot recur.
  • rb napi-20260908-1 (#1509): AddVirtualNetwork’s Node wrapper reads its network argument as uint16_t, the type the export declares, instead of uint32_t. The value was truncated implicitly at the call, so no behaviour changes; the declared type is now correct at the point of the read.
  • rb napi-20260908-1 (#1509): the TypeScript facade adapters/node/@chipkin/cas-bacnet-stack/src/api.ts declares the corrected return types. ReadTrendLogRecord, ReadTrendLogMultipleRecord, GetBDTEntry and GetFDTEntry are number, not boolean, and SetBACnetSCWebSocketStatus is boolean, not void. Without this a TypeScript caller writing const ok: boolean = GetBDTEntry(...) would compile against the old declaration and then compare a byte count against true — a successful read handled as a failure, with tsc unable to see it. The @return prose that described the old boolean contract is corrected in the same place.
  • rb napi-20260908-2 (#1527): CheckParametersCountAndType reports an unrecognised type code as a Napi::TypeError instead of terminating the process. It looked the code up with typeMap.at() before the switch, so std::out_of_range was thrown for exactly the input the default: “Unknown data type” branch existed to handle — making that branch unreachable and turning a one-line mistake (adding a code to the enum and forgetting the message map) into an uncaught C++ exception crossing the addon boundary.
  • rb napi-20260909-2 (#1600): the self-test’s real-tree tripwire compares checksums taken across the run rather than asking git diff HEAD, and runs from the EXIT trap. The old form could not distinguish “a corpus damaged the tree” from “the developer has uncommitted edits”, so anyone mid-edit on interface.cc or either header got a red gate with a message accusing the corpora; it also listed five paths while the corpora write to eight, and sat after the corpora, so a fail() partway through skipped it entirely.
  • rb napi-20260909-2 (#1600): an unrecognised argument exits 2 instead of falling through to the ordinary gate. --selftest (no hyphen) previously printed the ordinary OK line and exited 0, so a typo in the pre-push.sh wiring would report node adapter coverage (self-test): OK with no self-test having run.
  • sprint-198 (#901): BACnetInterface::SetSchedulePresentValue now takes a leading deviceInstance and resolves the target device by instance instead of BACnetDatabase::GetDevice()’s no-argument “first device in the list” fallback — the same defect class #959 already fixed for AddChannelObject/AddGroupObject/AddScheduleObject and their siblings. No DLL export exists for this method today (zero callers found anywhere in source/ or tests/), so this is not itself a customer-facing ABI change.
  • rb tool-20260910-1 (#1641): the hand-written Node addon compiles against the issues/runbook headers again. The per-translation-unit count was 30 errors — 20 in interface_send.cc, 10 in interface_callbacks.cc, 3 in interface_objects.cc. A plain node-gyp build showed only the first file’s, because MSBuild stops the project at the first failing unit.
  • rb tool-20260910-1 (#1641): 31 hand-written napi send wrappers read the data-link selector as uint8_t networkType, several through an explicit (uint8_t) cast. They compiled against the widened uint32_t networkPortInstance C signature and silently truncated it: any instance above 255, including BACNET_NETWORK_PORT_DEFAULT, reached the stack wrapped. They now read a uint32_t.
  • rb tool-20260910-1 (#1641): the RegisterCallbackReceiveMessageForPort trampoline passed JS a 1-byte buffer and copied that byte into *networkPortInstance. It now carries a 4-byte uint32, seeded with BACNET_NETWORK_PORT_DEFAULT.
  • rb tool-20260910-1 (#1641): every GetProperty*/SetProperty* trampoline copied an unseeded Napi::Buffer::New into *errorCode, whatever the JS callback returned. A JS getter that declined without writing it (every getter written before #974) handed the stack heap garbage. That defeats the decline-with-errorCode-unset fall-through to the fabrication path for required properties. The buffer is now seeded from the stack’s preset before the call.
  • rb tool-20260910-1 (#1641, #406): IFC-004’s <name>Minus1900 rename left 15 bacnet-date-field year roles in ci_scripts/adapter-roles.json (13 exports) keyed on the old parameter names. So a wrapper regenerated from today’s headers silently dropped its year guard: JS 2026 became wire year 234 (= 2134). The roles are re-keyed, and the 5 affected GENERATED wrappers were regenerated. Revert-red: node-adapter-selftest.mjs date-field-range-guards failed with SendTimeSynchronization(year 2026): expected a throw, got none.
  • rb tool-20260910-1 (#1641, #391/#392): four unguarded Node addon buffer sites closed, found by lifting the #1641 deferral locally and running node-adapter-output-buffer-guards.js. adapter-roles.json is keyed by parameter name, so generated wrappers carry no guard for a parameter the table never learned.
    • SendReadProperty: IFC-017 added propertyArraysLength, but the wrapper still checked only count × 15 while the DLL reads propertyArraysLength bytes. Present at dc51cb0a. It now uses SendWriteProperty’s two-guard bespoke shape.
    • SendIAmRouterToNetwork: IFC-025 added networkNumbers with no role, so a short buffer and a large count was an out-of-bounds read. Present at dc51cb0a. It now has an input-buffer role.
    • SendGetAlarmSummary and SendGetEnrollmentSummary: IFC-006 added a leading sentInvokeId out-buffer with no role, so the invoke id was written through an unchecked buffer. Both now have output-buffer roles. With the deferral lifted, generate-napi-wrappers.js --gate, node-adapter-output-buffer-guards.js, adapter-roles-verify.js, node-adapter-coverage.sh, node-adapter-argument-order.js and check-node-api-returns.js all pass.
  • rb tool-20260910-3 (#1767 follow-up): the ReadPropertyMultiple preflight (EstimateResponseLength) prices a whole-array read of a present Priority_Array at its fixed 16 elements (cl. 19.2) instead of the unknown-size 1000. At 1000 elements a commandable object’s Priority_Array cost 7008 bytes, so a non-segmenting device with a 1476-octet max APDU aborted the RPM as segmentationNotSupported. #1767’s fix (PR #1774) only covered an absent object or property. Revert-red: EstimateResponseLength_PriorityArrayWholeRead_PricedAtSixteenElements (estimate 7020) and Process_PriorityArrayOfCommandableObject_DoesNotAbort (abort reason 4) fail on 546803c8; a third added case, ..._OfPropertyNotOnInstance_DoesNotApplyArrayPricing, already passed there and pins #1767’s own behaviour for an existing object.
  • rb 1487-swap (#1487): the wrapper gate found the CASBACnetTime drift on the first run after rebasing onto #933 — InsertTrendLogMultipleRecord “no longer matches what the model and role table describe” — which is the drift class it exists to catch, reported the same day the header changed rather than at the next integration. The other twelve sites are in wrappers the generator does not own, and were fixed by hand.
  • rb 1487-generate (#1487): the role table now records which BACnet date field a bacnet-date-field role refers to (BACNET_DATE_FIELD_YEAR, _MONTH, …) — all 60 carry it — and the literal required extent of an output buffer where it is not a parameter (ValidateOutputBufferLength(info, "sentInvokeId", …, 1)). Without both, a generator can record that a check exists but cannot emit it: a month is 1..12 or 255 and a weekday 1..7 or 255, so a date check without its kind is a check in name only.
  • rb 1487-generate (#1487): the comparison is now a gate — generate-napi-wrappers.js --gate, wired into pre-push.sh. The 111 wrappers that match are held to the model and role table, so one cannot quietly stop agreeing with source/CASBACnetStackDLL.h; the 34 bespoke ones are allowlisted in ci_scripts/napi-wrapper-bespoke-allowlist.txt, and that list may only shrink. This is the check node-adapter-argument-order.js says it cannot do — “catching that needs the C signature compared against the variable names — a different and much larger job” — and the typed model turned it into a data comparison. It replaces no wrapper: the hand-written code keeps its DebugLogger prose and per-export comments and simply may not drift. Revert-red, both directions: pointing one wrapper’s deviceInstance at info[3] instead of info[0] (#390’s exact defect class) makes it exit 1 naming that wrapper, and a stale allowlist entry fails too, because an exemption that no longer applies hides a regression.
  • rb 1487-swap (#1487): the three comments that justified a guard’s position by pointing at “the DebugLogger below” now state the rule instead of the vanished example — guard at the point of first use, not immediately above the C call — and keep their #391 / #395 references, because the history is why the rule exists.
  • rb 1487-swap (#1487): supersedes the “It replaces no wrapper: the hand-written code keeps its DebugLogger prose” note in the 1487-generate entry above. The prose was the reason to keep the wrappers hand-written; the owner ruled the trade acceptable, so it went.
  • rb 1487-next (#1487): the extractor resolves a third indirection — an alias taken off a Napi::Buffer (char* dataAsString = dataAsStringBuffer.Data();), where the call passes the alias and the validation names the buffer. Without it dataAsString and recipientMacAddress looked unvalidated when they are validated.
  • rb 1487-next (#1487): role coverage of the byte/char pointer parameters on exports the addon actually binds reaches 141 of 141. 274 entries: 160 input-buffer, 60 BACnet date field, 39 output-buffer, 5 packed-element, 4 derived-length, 3 guarded-optional, 3 scalar-out. Verifier reports 0 problems. This completes the input a generated napi layer needs; nothing is generated yet.
  • rb 1487-roles-decide (#1487): ci_scripts/adapter-roles.json goes from 254 to 257 entries with zero verifier problems. void* buffer coverage rises from 2 of 6 to 5 of 6, so the list of parameters still needing a human ruling is 13, not the 16 reported when the four refusals were outstanding. Byte/char pointer coverage is unchanged at 129 of 141 (91%) on exports the addon actually binds — the recovered entries are ones that were previously refused outright, not previously wrong.
  • rb 1487-swap (#1487): 94 of the Node addon’s 145 non-callback napi wrappers are now GENERATED — written into adapters/node/…/interface*.cc by node ci_scripts/generate-napi-wrappers.js --write, from source/CASBACnetStackDLL.h plus ci_scripts/adapter-roles.json, and held there byte-for-byte by --gate in pre-push.sh. Each carries a // GENERATED by … marker. This is the swap the previous entry deliberately did not do: the wrappers were the last hand-written binding surface, and the defect classes they shipped — #390 (wrong info[] slot), #1464 (a swapped length/max-length pair, a reordered argument list), #1019 (missing parameters) — are errors a generator structurally cannot make.
  • rb 1487-swap (#1487): the generated buffer’s element type now comes from the C pointee type (uint16_t* → Napi::Buffer<uint16_t>), and a pointer-to-pointer parameter is refused outright rather than guessed at. The behaviour signature cannot catch an error here — it records “read slot3 as Buffer” and is deliberately blind to T — so a wrapper could match perfectly and still be a type error the moment it was written out.
  • rb 1487-testtool-gate (#1508): ci_scripts/generate-napi-wrappers.js compares the napi wrappers of source/CASBACnetStackTestToolDLL.h as well as source/CASBACnetStackDLL.h. The test-tool header’s 179 exports were previously outside the comparison entirely - neither generated nor allowlisted - which is why the #933 timestamp change was caught on a customer export and missed on two test-tool ones. 266 non-callback wrappers are now considered (was 145), 111 are generated and held byte-for-byte (was 94), and 155 are allowlisted with a reason each (was 51). Three wrappers left the allowlist and are generated now. Generated-code and tooling change only; no export’s behaviour changes.
  • rb 1487-testtool-gate (#1508): ci_scripts/adapter-roles-extract.js’s coverage report walks both surfaces. Role recovery already did; only the report of what is left unruled did not, so 74 test-tool buffer parameters with no recovered role went unlisted. Byte/char pointer coverage reads 193/279 (69%) where the customer-only report read 141/153 (92%).
  • 604-build-matrix (#604): the shipped Windows binary set is now one file — CASBACnetStack_x64_Release.dll. The x64 Debug DLL is no longer built, and Win32 is a source-embed target only: the DebugDll|Win32, ReleaseDll|Win32, DebugLib|Win32 and ReleaseLib|Win32 configurations are removed from projects/msvs/CASBACnetStack/CASBACnetStack.vcxproj, along with DebugDll|x64. Customers who embed the source tree — the large majority — are unaffected.
  • 604-build-matrix (#604): the x64 static library (CASBACnetStack_x64_Release.lib) can now actually be built, opt-in: release.yml gains a build_static_lib workflow_dispatch input, default false. A tagged release builds the DLL only. Note what this does and does not give you: the input is settable only on a manual workflow_dispatch run, and the packaging job is gated on a tag push, so the library is produced as a workflow-run artifact and never enters a release zip. Owner ruling, 2026-09-07: accepted as-is, on the condition that the limitation is documented — which this bullet is. If a customer-downloadable static library is wanted later, release.yml’s packaging job would have to accept a workflow_dispatch run as well as a tag push.
  • 604-build-matrix (#604): DllExport now emits __declspec(dllexport) only when CAS_BACNET_STACK_DLL is defined — that is, only while source/CASBACnetStackDLL.h is being compiled into the stack’s own DLL. It was previously unconditional for every Windows C++ consumer, so anyone embedding source/ directly (the common case) or linking the static .lib had their own binary re-export all 229 stack symbols. There is deliberately no dllimport branch: DLL consumers never reach this header, because adapters/cpp/CASBACnetStackAdapter.h routes them to the generated function-pointer header bound at run time by LoadBACnetFunctions(), and no import library is produced or shipped. The macro reused is the one that already guarded DllMain, rather than a second parallel one.
  • 604-build-matrix (#604): ReleaseDll|x64 — the only DLL configuration — now defines CAS_BACNET_STACK_DLL. Previously only the deleted DebugDll|Win32 did, so DllMain was compiled into nothing that shipped.
  • 604-build-matrix (#604): source/CASBACnetStackTestToolDLL.h’s two export macros (BACnetTestToolDllExport for the BACnetTestToolInterface C++ class, BACnetTestToolDllExportC for the flat extern "C" exports) are gated on CAS_BACNET_STACK_DLL the same way, making good on their own “mirrors DllExport” comment. The test-tool surface is built by exactly one configuration — DebugTestTool|x64, a static library — so in the MSVS projects the unconditional __declspec(dllexport) was wrong everywhere it was reachable: it made the linking test executable re-export every BACnetStackTestTool_* symbol as its own. (It was also reachable from the Node addon, whose binding.gyp defines BACNET_STACK_TESTTOOL by default; harmless there, since N-API registration does not use dllexport.) Writing the condition identically also preserves the header’s documented promotion path — an export moved into a DLL build picks up dllexport automatically, and nothing else does.
  • 604-build-matrix (#604): the MSVS solution is x64-only. BuildCASBACnetStack.sln drops from 18 solution configurations to 4 — Debug|x64 (builds and runs the tests, with the stack as the BACNET_STACK_TESTTOOL static library), DebugLib|x64, ReleaseDll|x64 and ReleaseLib|x64 — each mapping to a configuration that actually exists.
  • 604-build-matrix (#604): CASBACnetStackTests, CASBACnetStackTestsMemory and the stack’s DebugTestTool configuration move from Debug|Win32 to Debug|x64. The test-tool library is now CASBACnetStack_x64_DebugTestTool.lib; test reports and coverage output pick up _x64_ in place of _Win32_.
  • 604-build-matrix: CASBACnetStackTestsMemory builds as C++17. It had no <LanguageStandard> at all, so it was on MSVC’s C++14 default while the stack and CASBACnetStackTests were both stdcpp17. All three projects already pinned the current v145 toolset.
  • rb 894-adapter-generation (#894): ci_scripts/generate-cpp-adapter.js and ci_scripts/generate-node-adapter.js now read the shared model instead of each carrying its own header parse. Output is byte-identical — both --check runs pass and no file under adapters/ changed — which is the proof that the model is rich enough for the two consumers that already work, before anything new depends on it. Every guard the two generators carried (count-assert the export lines, reject a duplicate symbol, die on an unreadable declaration) is kept in the model; generate-node-adapter.js still reads the adapter’s own sources directly for the napi registrations, the BACNET_STACK_TESTTOOL gate walk, api.ts and property-arrays.ts, which describe the adapter rather than the C surface.
  • rb 894-adapter-generation (#894): adapter naming is now uniform across all seven languages — the full C export name (BACnetStack_AddDevice) and callback types named after the C typedef (FPCallbackReceiveMessage). Owner decision 2026-09-07: adapter signatures may change in order to become uniform. C# moved 118 short-named methods onto the full name; ResetCallbacks() and SimpleServerExample.java were updated to match.
  • rb 894-adapter-generation (#894): ci_scripts/hand-adapter-coverage-allowlist.txt drops from 252 live entries to 4. The 4 remaining are the C# Async region’s dead bindings, left rather than deleted because rewiring its helpers onto the v6 replacements is #1019. The drift --check gates replace the allowlist and are strictly stronger: it could only assert that the set of unbound names had not grown, --check asserts the file is byte-identical to what the current header produces.
  • rb 894-adapter-generation (#894): ci_scripts/adapter-callback-signature-check.js retargeted at the generated declarations and extended to Rust (previously excluded as a known-bad shape) — 5 languages checked, up from 4.
  • 933-cas-bacnet-time (#933): absolute times now cross the public ABI as CASBACnetTime, a typedef int64_t that is the same width on every platform. time_t no longer appears in any exported signature. Converted: BACnetStack_ReadTrendLogRecord, BACnetStack_InsertTrendLogMultipleRecord, BACnetStack_ReadTrendLogMultipleRecord, BACnetStack_RegisterCallbackGetSystemTime and BACnetStack_RegisterCallbackValidateBACnetSCOperationalCertificate in source/CASBACnetStackDLL.h, the two test-tool exports BACnetStack_InsertTrendLogRecord/BACnetStack_InsertEventLogRecord, and the two matching callback typedefs in source/BACnetCallbackInterface.h.
  • 933-cas-bacnet-time (#933): 32-bit builds are now possible. source/CASBACnetStackDLL.cpp’s cas_time_t_is_64bit assert — typedef char cas_time_t_is_64bit[sizeof(time_t) == 8 ? 1 : -1];, issue #606’s way of protecting the width-bound language adapters — was a hard compile error wherever time_t was 4 bytes. Fixing the width in the ABI removes both the assert and the reason for it. Removing the assert without fixing the width would have been worse than leaving it: a 32-bit library would pass 4 bytes where the C#, Java, Python and Rust bindings read 8, which is silent corruption at the boundary rather than a loud failure.
  • 933-cas-bacnet-time (#933): this is not an ABI break for anything that ships. time_t is already 8 bytes on 64-bit MSVC (__int64) and 64-bit Linux (long), so on every shipped target int64_t is byte-identical to what the ABI carried before. Regenerating all six adapters changed only type names and doc comments — not one binding width: Python still c_int64 (5 sites), Rust i64 (8), C# long (8), Java long (6), and no line in the adapters/ diff mentions a width. The DLL still exports exactly 229 symbols.
  • 933-cas-bacnet-time (#933): ci_scripts/check-contract-pins.js’s PINNED_TIME_T_COUNT = 6 is replaced by a rule, not re-counted: no exported signature in the frozen header may name time_t. A rule needs no maintenance when a time-carrying export is added, states the actual contract instead of a number that happened to be true on one day, and fails loudly if time_t creeps back. Its --self-test corpus now asserts both directions — a spliced-in DllExport naming time_t is flagged, and a comment mentioning time_t is not, which matters because the header now carries several comments explaining why it is absent.
  • 933-cas-bacnet-time (#933): the six adapter generators learned the new type. They refused to guess — each died with “no C#/Java/ctypes/Rust type for C return type CASBACnetTime” rather than emitting something plausible, which is the strictness property #894’s decision doc argued for.
  • rb a-20260907-27 (#1249, CR-F19-005): GetType() in 6 BACnetEventParameter* classes (ChangeOfBitstring, ChangeOfState, ChangeOfValue, CommandFailure, ChangeOfDiscreteValue, ChangeOfTimer) now returns the named BACnetEventParameter CHOICE-tag enum instead of a hardcoded integer literal, matching the convention already used by their sibling classes. Style/consistency only — every literal was numerically correct, no behavior change. ChangeOfDiscreteValue.cpp’s comment also misnamed the enum family (BACnetEventType instead of BACnetEventParameter); the comment is now redundant with the self-documenting named return and was removed.
  • rb a-20260907-29 (#1248, CR-F18-011): documents BACnetFaultParameterNone::Decode/Encode as intentionally dead/defensive code — the owning BACnetFaultParameter wrapper handles the none [0] CHOICE arm entirely inline (a primitive context-tagged NULL) and never calls this class’s Decode/Encode methods. (The class itself is still constructed on a live path, via BACnetFaultAlgorithm::BuildFaultParameters, but that caller only reads GetType().) Also documents the class’s ambiguous 0-return-means-both-success-and-failure shape as a known, currently-harmless characteristic. Docs-only, no behavior change.
  • rb a-20260907-34 (#1248, CR-F18-009): adds the spec clause citation (135-2024 cl. 13.4.2/13.4.5/13.4.6) to the class doc comments of BACnetCharacterstringFaultAlgorithm, BACnetStateFaultAlgorithm, and BACnetStatusFlagsFaultAlgorithm, matching the codebase’s dominant “135-2024 cl. X.Y” citation convention used elsewhere. Docs-only, no behavior change.
  • rb a-20260907-35 (#1429, CR-F18-009 follow-up): adds the spec clause citation (135-2024 cl. 13.4.4/13.4.7/13.4.8) to the class doc comments of BACnetLifeSafetyFaultAlgorithm, BACnetOutOfRangeFaultAlgorithm, and BACnetListedFaultAlgorithm. BACnetLifeSafetyFaultAlgorithm.h’s own doc comment also incorrectly named itself “the FaultState fault algorithm” (a copy-paste artifact from its sibling); corrected to “FaultLifeSafety” alongside the citation. Docs-only, no behavior change.
  • rb a-20260909-108 (#976, owner ruling): BACnetDBDevice::SetPropertyWritable/SetPropertySubscribable no longer implicitly enable a property before setting its flag - the owner ruled this is not intended, settling a design question an earlier session (b-20260907-13) correctly deferred rather than guessed at. Both now require the property already be enabled (per-instance, per-objectType, or required-by-default, via the existing IsPropertyEnabled resolution) and return false with an explicit error log otherwise. The old implicit-enable had a live, customer-visible side effect: calling SetPropertyWritable(..., false) (“make it read-only”) on a property never otherwise enabled silently ADDED it to Property_List and the RPM ALL response - the opposite of the caller’s apparent intent.
  • Also fixes the override-entry seeding gap the same issue reported: when the property IS enabled but this is the first per-instance touch, BACnetDBObject::SetPropertyEnabled now accepts an optional seed profile, used instead of the raw default profile when materializing the new per-instance entry. The device-level wrappers resolve this seed from any existing per-objectType override before creating the entry, so setting Writable no longer silently resets an existing Subscribable override (and vice versa) - the issue’s second reported symptom.
  • Flag-preservation across a disable/re-enable cycle (the issue’s third item) is documented rather than changed - still resets, a larger change (a distinct “disabled” representation instead of removing the override entry outright) left for its own decision.
  • The new gate surfaced 3 internal call sites that relied on the old implicit-enable to both enable an optional property and set its flag in one call, all caught by the full test suite: AddBinaryLightingOutputObject’s Elapsed_Active_Time/Strike_Count setup, AddScheduleObject’s Weekly_Schedule/Schedule_Default setup, and a stale SetPropertyWritable(backupFailureTimeout, false) call in SetBackupAndRestoreEnabledProperties’s disable branch that ran immediately after disabling the same property - under the old behavior this silently RE-enabled it, undoing the disable one line above, a pre-existing bug the implicit-enable convenience was masking. Fixed by adding the explicit SetPropertyEnabled calls the new contract requires (first two) and removing the now-redundant call (third, since disabling already fully removes the property).
  • Adversarial review of this change (justified given its blast radius across widely-used internal functions) found 2 more call sites the green test suite did not catch, via an exhaustive sweep of every internal SetPropertyWritable/SetPropertySubscribable caller: AddTimerObject’s Default_Timeout and AddLightingOutputObject’s Transition/Min_Actual_Value/Max_Actual_Value - none of these object-type/property combinations had prior test coverage. Fixed the same way (explicit SetPropertyEnabled added before the existing SetPropertyWritable call), with new regression tests added for both.
  • Also corrected an affirmative false claim the old implicit-enable behavior left behind in the frozen CASBACnetStackDLL.h: BACnetStack_SetPropertySubscribable’s doc comment asserted a “SIDE EFFECT: this ENABLES the property” that this issue’s fix removes.
  • 8 new tests: the gate (property not yet enabled -> false, Property_List unchanged) for both setters, the seeding fix (objectType-scope override survives materializing a per-instance entry) for both setters, a new assertion on the existing ConfigurationFiles_Disable_EmptiesTheList test pinning that Backup_Failure_Timeout stays genuinely disabled, and regression tests for the 2 review-fix call sites (AddTimerObject’s Default_Timeout, AddLightingOutputObject’s Transition/Min_Actual_Value/Max_Actual_Value).
  • Found and filed separately rather than fixed here: BACnetStackObjectSetting::SetPropertyByObjectTypeWritable/SetPropertyByObjectTypeSubscribable have the identical implicit-enable bug at the objectType scope, in a different class this issue never named (#1623).
  • rb a-20260909-109 (#1623, parallel to #976): BACnetStackObjectSetting::SetPropertyByObjectTypeWritable/SetPropertyByObjectTypeSubscribable no longer implicitly enable a property before setting its flag - the identical owner-ruled fix #976 applied at the per-instance scope, now applied one scope up at the objectType-template level. Both now require the property already be enabled via SetPropertyByObjectTypeEnabled, returning false with an explicit error log otherwise.
  • One internal call site relied on the old implicit-enable and is now fixed with an explicit SetPropertyByObjectTypeEnabled call: BACnetInterface’s virtual-router setup, which makes a network port’s Network_Number writable when the device becomes a router.
  • A full test run surfaced 5 more call sites relying on the old behavior (3 in TestBusinessLogic.cpp, 2 in the TrendLogMultiple BTL compliance tests), all fixed the same way. A further exhaustive repo-wide sweep (not just trusting the green suite - see #976’s own lesson) found 4 more test call sites that were silently no-op’d by the new gate with no assertion catching it (BTLTests.cpp, TestBACnetActiveCOVSubscriptionMemory.cpp, TestTrendLogs.cpp x2), fixed proactively.
  • 2 new discriminating tests added to TestBACnetStackObjectSettings.cpp covering the gate itself.
  • Adversarial review’s own independent sweep found 2 more callers outside source//tests/ (so outside this batch’s own grep pattern): projects/msvs/CASBACnetStackTestsMemory/CASBACnetStackTestsMemory.cpp and projects/linux/coverage/gcov_driver.cpp. Both were verified by attempting to compile them directly rather than trusting the finding at face value - both already fail to compile for reasons unrelated to this batch (stale API usage predating #1623), so neither is a live regression here. The first is already tracked as #1491; the second is newly filed as #1626, with the one-line-each fix this investigation already worked out included for whoever restores the file to a compiling state.
  • rb a-20260914-101 (#1872 continuation, part B of #1592): packed 6 of BACnetDBDevice’s remaining methods - AddObjectPropertyReferenceToChannel, AddScheduleObjectPropertyReference, AddTimerObjectPropertyReference (pair-form overload kept as a one-line forwarder, see below), AddLogDeviceObjectPropertyToTrendLogMultiple, EnableTrendLogProperties, and EnableLiftOrEscalatorProperties - from (objectType, objectInstance) PAIR parameters into the strong BACnetObjectIdentifier32 type.
    • Deviation from the parent issue’s framing, verified by reading the actual current declarations (not assumed): the parent issue’s INTENT called these “dual-identifier” methods expecting two identifiers each (the object itself plus the referenced object). Reading source/BACnetDBDevice.h’s current signatures showed only ONE real (objectType, objectInstance) pair per method: AddObjectPropertyReferenceToChannel/AddScheduleObjectPropertyReference/ AddTimerObjectPropertyReference/AddLogDeviceObjectPropertyToTrendLogMultiple/ EnableTrendLogProperties each take the CHANNEL/SCHEDULE/TIMER/TREND-LOG object’s own identity as a bare objectInstance with no explicit type parameter (the object type is implicit/fixed by the method’s own #ifdef STACK_OPTION_OBJECT_TYPE_* guard) - only the REFERENCED object (refObjectType/refObjectInstance or logObjectType/logObjectInstance) is a real pair, and that is what each converts. EnableLiftOrEscalatorProperties is the one method here that DOES convert its own object’s (objectType, objectInstance) pair, because it is shared between two object types (lift/escalator) and so must carry the type explicitly. The refDeviceInstance/ logDeviceInstance parameters some of these carry stay bare uint32_t - a device identifier’s type is always implicitly device, so there is no pair there to convert either.
    • Each body now unpacks the type/instance locals from the packed identifier parameter before calling into the OTHER class that owns the actual reference storage and has not itself been converted (#1592 scope is BACnetDBDevice only this batch): BACnetStackChannel:: InsertObjectPropertyReference, BACnetStackSchedule::InsertObjectPropertyReference, BACnetStackTimer::InsertObjectPropertyReference, BACnetStackTrendLogMultiple:: InsertLogDeviceObjectProperty, and BACnetStackTrendLog::SetLogDeviceObjectProperty all still take the pair, so no pass-through is possible into them. EnableLiftOrEscalatorProperties similarly unpacks locally before calling this->m_storedDBValues.SetValueAndTakeControl and this->ObjectExists (the object-store trio, deliberately deferred to its own batch per this batch’s own INTENT comment on #1273) which also still take the pair.
    • AddTimerObjectPropertyReference’s OLD pair-form overload is kept as a thin forwarder (this->AddTimerObjectPropertyReference(objectInstance, refDeviceInstance, BACnetObjectIdentifier32::FromRaw(BACnetPrimitiveObjectIdentifier::GenerateObjectIdentifier(refObjectType, refObjectInstance)), refPropertyIdentifier, refUseArrayIndex, refArrayIndex)) because source/BACnetTestToolInterface.cpp (session D, ban-listed/read-only for this issue) still calls that exact pair-form overload directly from its own AddTimerObjectPropertyReference - confirmed by grep, no other caller uses it. This is below the true object-identifier boundary (the pair there already came from an already-valid BACnetObjectIdentifier32 that BACnetTestToolInterface itself unpacked), so the forwarder repacks via FromRaw/GenerateObjectIdentifier rather than TryMake, matching every other internal repack in this file - TryMake stays reserved for the two DLL boundary files.
    • No pair overload was kept for the other 5 methods: grep confirms none of CASBACnetStackDLL.cpp/ .h, CASBACnetStackTestToolDLL.cpp/.h, BACnetTestToolInterface.h/.cpp, or BACnetCallbackInterface.h (the session-D files) call any of them on BACnetDBDevice directly.
    • Pass-through cleanup (verified against the diff via grep, not just claimed):
      • source/BACnetInterface.cpp’s AddObjectPropertyReferenceToChannel/ AddScheduleObjectPropertyReference already receive a BACnetObjectIdentifier32 refObjectIdentifier parameter; their calls into BACnetDBDevice’s now-converted methods pass that parameter straight through instead of unpacking to a pair and repacking - the two now-dead local unpack lines in each were removed (their #else/not-compiled branch’s NOREF now references refObjectIdentifier directly instead of the two locals).
      • source/BACnetDatabase.cpp’s AddTrendLogObject/AddLoggedObjectToTrendLogMultiple/ AddLiftOrEscalatorObject already receive a BACnetObjectIdentifier32 logObjectIdentifier/ objectIdentifier parameter; their calls into EnableTrendLogProperties/ AddLogDeviceObjectPropertyToTrendLogMultiple/EnableLiftOrEscalatorProperties now pass that parameter straight through. AddTrendLogObject’s and AddLoggedObjectToTrendLogMultiple’s two now-dead local unpack lines (only ever used at that one call site) were removed; AddLiftOrEscalatorObject’s objectType/objectInstance locals stay, since they are still used by that function’s own unconverted ObjectExists/AddObject/DeleteObject calls - only the EnableLiftOrEscalatorProperties call itself was switched to pass objectIdentifier directly.
    • Tests switched to the packed form (constructing the identifier via BACnetObjectIdentifier32::FromRaw(BACnetPrimitiveObjectIdentifier::GenerateObjectIdentifier(...)), matching a-20260914-97’s AddAveragingObject precedent even for the method that kept a pair forwarder): tests/TestBACnetDBDevice.cpp (22 call sites across EnableTrendLogProperties/ AddLogDeviceObjectPropertyToTrendLogMultiple/EnableLiftOrEscalatorProperties), tests/TestBACnetReadRangeProcessor.cpp, tests/TestBACnetStackSchedule.cpp, tests/TestBACnetStackTimer.cpp (converted to the packed form even though the pair-form forwarder still exists, to exercise the new API), and tests/TestBusinessLogic.cpp (5 call sites).
    • ci_scripts/object-identifier-boundary-allowlist.txt: BACnetDBDevice.h lowered from 66 to 61 - the 5 fully-converted methods each drop one pair-declaration match; AddTimerObjectPropertyReference contributes no net change because its kept pair-form forwarder still declares the pair (verified by hand-running the gate’s own regex against the header: 61, matching the lowered ceiling exactly).
    • Verification: ci_scripts/check-object-identifier-boundary.sh --self-test (8/8) and the real gate both green with the lowered ceiling. Control arm (gate): re-adding the pair form of AddObjectPropertyReferenceToChannel’s declaration (restoring const uint16_t refObjectType, const uint32_t refObjectInstance in place of the packed parameter) made the gate fail red (BACnetDBDevice.h has 62 pair-parameter declaration(s), allowlist ceiling is 61); reverted. Control arm (type): replacing a converted EnableLiftOrEscalatorProperties call’s identifier argument with a bare 1u in source/BACnetDatabase.cpp failed to compile (cannot convert 'unsigned int' to 'CASBACnetStack::BACnetObjectIdentifier32'); reverted. This batch’s own touched source files (BACnetDBDevice.cpp, BACnetDatabase.cpp, BACnetInterface.cpp) compile cleanly standalone (g++ -fsyntax-only) via WSL under both default/TESTTOOL and NO_TESTTOOL=1. Full WSL g++ suite green under default/TESTTOOL: 8504/8504 tests passed, 902 suites. NO_TESTTOOL=1 hits only the same pre-existing, unrelated gap already documented by a-20260914-91/93/95/97 (TestBACnetComplexAckProcessorWireLevel.cpp’s unguarded BACnetTestToolInterface construction, not SetSegmentationEnabledForTesting - confirmed NOT a repeat of that separately-hotfixed regression).
  • rb a-20260914-103 (#1875, part E of #1592): gave all 25 codec/service-request classes named in the issue a packed BACnetObjectIdentifier32 object-identifier accessor pair, converting every internal caller and deleting the old (objectType, objectInstance) PAIR-form SetObjectIdentifier overload wherever no caller remains.
    • Naming convention chosen (same name across all 25, per the issue’s own requirement): bool SetObjectIdentifier(const BACnetObjectIdentifier32&) as a new overload of the existing setter name, and BACnetObjectIdentifier32 GetObjectIdentifier32() const as a new getter alongside each class’s existing GetObjectIdentifier() (which - verified per-class by reading every one of the 25 headers/.cpp files, not assumed - already returns BACnetPrimitiveObjectIdentifier (by const ref, or by pointer for BACnetWhoHasRequest’s optional CHOICE) in all 25 cases, so no getter conversion was in scope; only the new GetObjectIdentifier32() convenience wrapper was added, implemented as this->m_objectIdentifier.GetIdentifier32()).
    • Verified-per-class table (all 25 held a BACnetPrimitiveObjectIdentifier member and a pair-form SetObjectIdentifier(const uint16_t, const uint32_t) at this batch’s tip): BACnetActionCommand, BACnetAlarmSummary, BACnetAuditLogQueryParameters, BACnetCovSubscriptionSpecification (member is m_monitoredObjectIdentifier), BACnetCreateObjectACK, BACnetDeleteObjectRequest, BACnetDeviceObjectPropertyReference, BACnetDeviceObjectPropertyValue, BACnetDeviceObjectReference, BACnetEnrollmentSummary, BACnetEventSummary, BACnetIHaveRequest, BACnetLifeSafetyOperationRequest, BACnetObjectPropertyValue, BACnetObjectSpecifier, BACnetPropertyAccessResult, BACnetReadAccessResult, BACnetReadAccessSpecification, BACnetReadPropertyACK, BACnetReadPropertyRequest, BACnetReadRangeACK, BACnetReadRangeRequest, BACnetWhoHasRequest (m_object is a BACnetPrimitiveBase* CHOICE pointer, GetObjectIdentifier() returns NULL when object-name is the active CHOICE), BACnetWriteAccessSpecification, BACnetWritePropertyRequest. All 25 converted in full this batch (no split needed).
    • Each class’s packed setter became the real implementation (unpacking ObjectType()/ObjectInstance() locally before calling the class’s own BACnetPrimitiveObjectIdentifier::SetValue(type, instance) - itself exempt from the boundary, per plan). Every internal caller (constructors, Clone(), copy assignment) that used to call the pair-form setter now calls the packed setter, repacking via BACnetObjectIdentifier32::FromRaw(BACnetPrimitiveObjectIdentifier::GenerateObjectIdentifier(...)) where it only has locals in scope (e.g. every converted constructor), or via a peer accessor (rhs.GetObjectIdentifier32(), this->m_objectIdentifier.GetIdentifier32()) where one is already available - never TryMake, which stays boundary-only. One caller outside the 25 classes needed the same repacking-forwarding fix: BACnetCreateObjectRequest::SetObjectSpecifierObjectIdentifier calls into the now-converted BACnetObjectSpecifier::SetObjectIdentifier.
    • Bug caught and fixed by the standalone g++ -fsyntax-only compile pass before committing: BACnetPropertyAccessResult’s single-line setter body referenced the bare enum constant objectIdentifier (its own context-tag number) via SetContextTagNumber(objectIdentifier); naming the new parameter objectIdentifier (matching every other converted class) would have silently shadowed that enum inside the method body. Renamed this one class’s parameter to identifier to keep the enum reference intact - the parameter name is otherwise objectIdentifier everywhere else.
    • Pair overload kept as a forwarder in exactly one class: BACnetAuditLogQueryParameters - grep confirms source/BACnetTestToolInterface.cpp (session D, ban-listed/read-only for this issue) still calls params.SetObjectIdentifier(queryObjectType, queryObjectInstance) directly. Its forwarder body is return this->SetObjectIdentifier(BACnetObjectIdentifier32::FromRaw(BACnetPrimitiveObjectIdentifier::GenerateObjectIdentifier(objectType, objectInstance)));. #1876 deletes it once that session converts. No other kept forwarders: grep confirms no BACnetBusinessLogic/BACnetDBDevice caller uses any of the other 24 classes’ pair setter, and no other session-D file calls any of them either.
    • BACnetWhoHasRequest’s GetObjectIdentifier32() returns the default identifier (type 0, instance 0) when the object-name CHOICE is active or nothing has been set yet, matching GetObjectIdentifier()’s own NULL-on-absence convention as closely as a by-value return can - a dedicated test (WhoHasRequest_ObjectNameChoiceReturnsDefaultIdentifier) exercises this so the getter can never dereference a NULL m_object.
    • Tests switched from the deleted pair-form setter to the packed form (via BACnetObjectIdentifier32::FromRaw(BACnetPrimitiveObjectIdentifier::GenerateObjectIdentifier(...)), matching this window’s established internal-repack idiom): tests/TestBACnetActionCommand.cpp (10 sites), tests/TestBACnetActionList.cpp, tests/TestBACnetComplexAckProcessor.cpp (3 sites), tests/TestBACnetDeleteObjectProcessor.cpp (5 sites), tests/TestBACnetNotificationParametersExtended.cpp (3 sites), tests/TestBACnetObjectPropertyValue.cpp (15 sites), tests/TestBACnetObjectSpecifier.cpp (5 sites), tests/TestBACnetPropertyAccessResult.cpp (5 sites), tests/TestBACnetPropertyAccessResultPolicy.cpp (2 sites), tests/TestBACnetReadAccessSpecification.cpp, tests/TestBACnetReadRangeProcessor.cpp, tests/TestBACnetStackAccessZone.cpp, tests/TestBACnetUnconfirmedRequestPDU.cpp, tests/TestBACnetWhoHasRequest.cpp, tests/TestBACnetWriteAccessSpecification.cpp, tests/TestBusinessLogic.cpp (2 sites), tests/TestGetElementsCompleteness.cpp (4 sites). tests/TestBACnetAuditLogQuery.cpp’s 2 pair-form call sites were left as-is, since BACnetAuditLogQueryParameters kept its pair-form forwarder.
    • ci_scripts/object-identifier-boundary-allowlist.txt: lowered by exactly 1 for each of the 24 fully converted classes’ headers (BACnetActionCommand.h 2→1, BACnetAlarmSummary.h 3→2, BACnetCovSubscriptionSpecification.h 2→1, BACnetCreateObjectACK.h 2→1, BACnetDeleteObjectRequest.h 2→1, BACnetDeviceObjectPropertyReference.h 3→2, BACnetDeviceObjectPropertyValue.h 2→1, BACnetDeviceObjectReference.h 3→2, BACnetEnrollmentSummary.h 3→2, BACnetIHaveRequest.h 3→2, BACnetLifeSafetyOperationRequest.h 3→2, BACnetObjectSpecifier.h 2→1, BACnetPropertyAccessResult.h 2→1, BACnetReadAccessResult.h 2→1, BACnetReadAccessSpecification.h 2→1, BACnetReadRangeACK.h 3→2, BACnetReadRangeRequest.h 5→4, BACnetWhoHasRequest.h 3→2, BACnetWriteAccessSpecification.h 2→1) and deleted outright for the 5 that reached 0 (BACnetEventSummary.h, BACnetObjectPropertyValue.h, BACnetReadPropertyACK.h, BACnetReadPropertyRequest.h, BACnetWritePropertyRequest.h). BACnetAuditLogQueryParameters.h stays at 1 unchanged (its kept forwarder still declares the pair).
    • Mandatory wire-identity test added: tests/TestObjectIdentifier32WireIdentity.cpp, covering BACnetReadPropertyRequest, BACnetWhoHasRequest, BACnetReadRangeRequest, and BACnetDeviceObjectPropertyReference (the 4 the issue names). For each: encodes the same (1023, 4194303) value via the new packed setter and via the still-public pair-shaped constructor (these 4 classes’ constructors were not converted, only Set/GetObjectIdentifier - the constructors stay pair-shaped per the issue’s own scope) and asserts byte-identical output; decodes and asserts GetObjectIdentifier32() round-trips both (1023, 4194303) and (512, 1); and a negative control encodes a deliberately different (4, 99) value and asserts its bytes differ from the first, so the equality assertion cannot pass vacuously. All 9 tests pass.
    • Verification: ci_scripts/check-object-identifier-boundary.sh --self-test (8/8) and the real gate both green with the lowered/deleted allowlist entries. Control arm (gate): re-adding a pair-form SetObjectIdentifier(const uint16_t, const uint32_t) overload to BACnetReadPropertyRequest.h made the gate fail red (BACnetReadPropertyRequest.h has 1 pair-parameter declaration(s), allowlist ceiling is 0); reverted. Control arm (type): replacing a converted SetReadPropertyRequest call’s identifier argument with a bare objectInstance in source/BACnetReadPropertyRequest.cpp failed to compile (cannot convert 'const uint32_t' to 'const CASBACnetStack::BACnetObjectIdentifier32&'); reverted. Every touched source/*.cpp and tests/*.cpp file compiles cleanly standalone (g++ -fsyntax-only) via WSL. Full WSL g++ suite green under default/TESTTOOL: 8513/8513 tests passed, 903 suites. NO_TESTTOOL=1 hits only the same pre-existing, unrelated TestBACnetComplexAckProcessorWireLevel.cpp link gap already confirmed unrelated by 5 prior batches this window (BACnetTestToolInterface incomplete type under NO_TESTTOOL) - every other file, including all files this batch touched, compiles clean under NO_TESTTOOL=1 too.
  • rb a-20260914-105 (#1872 continuation, part B of #1592): converted 3 of the 4 remaining BACnetDBDevice object-store trio methods - ObjectExists, DeleteObject, UpdateObjectIdentifier
    • to the packed BACnetObjectIdentifier32 type, leaving AddObject for its own dedicated future batch. 40/42 methods now converted for this issue overall.
    • Measurement that drove the scope decision: raw call-site counts at this batch’s tip - AddObject 862, ObjectExists 131, DeleteObject 31, UpdateObjectIdentifier 18. After excluding non-BACnetDBDevice receivers found by reading each call site (BACnetDatabase::ObjectExists/ DeleteObject and BACnetInterface::ObjectExists, already packed by #1871; g_bacnet.ObjectExists in the ban-listed CASBACnetStackDLL.cpp), the real BACnetDBDevice::ObjectExists/DeleteObject/ UpdateObjectIdentifier fan-out was ~33 internal (this->ObjectExists) + ~28 other source/ call sites + 61 tests/ call sites - large but hand-tractable in one batch with a verified scripted rewrite for the highly-repetitive equality patterns. AddObject’s 862 sites (mostly tests/ fixture setup like device.AddObject(BACnetObjectType::timer, 600)) remain out of scope, per the issue’s own standing permission to split this group further - the same call this batch’s 4 predecessors made for smaller groups.
    • source/BACnetDBDevice.h/.cpp: ObjectExists(const BACnetObjectIdentifier32), DeleteObject(const BACnetObjectIdentifier32, const bool updateDatabaseRevision = true), UpdateObjectIdentifier(const BACnetObjectIdentifier32 oldObjectIdentifier, const BACnetObjectIdentifier32 newObjectIdentifier). Each body unpacks ObjectType()/ObjectInstance() locally only where still needed to call into not-yet-converted internals (EraseFromEngineMaps, m_storedDBValues.DeleteObjectValues, BACnetBusinessLogic::IsObjectTypeCompiledAndEnabled, the ~30 specialized instance-keyed maps UpdateObjectIdentifier migrates on rename); the raw-word map operations (m_objectList2, m_objectPropertyOverrides, keyed by the internal objectIdentifier32 = uint32_t typedef) now use .Raw() directly instead of re-deriving the packed word via GenerateObjectIdentifier. UpdateObjectIdentifier’s pre-existing raw local variables (previously also named oldObjectIdentifier/newObjectIdentifier) were renamed to oldObjectIdentifierRaw/ newObjectIdentifierRaw to avoid shadowing the new strong-typed parameters of the same name.
    • #906 rejection verified unchanged: AddObject (out of scope this batch, untouched) still holds the 4194303-instance rejection exactly where it was; DeleteObject/ObjectExists/ UpdateObjectIdentifier never touched that logic - confirmed by reading AddObject’s body before and after this batch’s edits (byte-identical, since the diff never enters that function).
    • Callers switched outside the 4 ban-listed session-D files, all via this window’s established call-site idiom BACnetObjectIdentifier32::FromRaw(BACnetPrimitiveObjectIdentifier::GenerateObjectIdentifier(type, instance)) (417 existing occurrences of this exact idiom already in the tree before this batch, added by prior sessions converting other methods - confirmed by grep, not assumed): BACnetAcknowledgeAlarmProcessor.cpp, BACnetAtomicReadFileProcessor.cpp, BACnetAtomicWriteFileProcessor.cpp, BACnetBusinessLogic.cpp (4 sites, including one unpacking an existing objectIdentifier->GetObjectType()/GetObjectInstance() pair), BACnetCreateObjectProcessor.cpp (2 sites), BACnetDatabase.cpp (ObjectExists/DeleteObject now pass their own already-packed objectIdentifier parameter straight through instead of unpacking-then-repacking; 4 devicePtr->ObjectExists + 18 devicePtr->DeleteObject call sites converted, plus UpdateDeviceInstance’s devicePtr->UpdateObjectIdentifier call), BACnetDeleteObjectProcessor.cpp (2 sites), BACnetInterface.cpp (3 sites, Secure Connect certificate-file existence checks), BACnetReadPropertyMultipleProcessor.cpp (2 sites), BACnetSubscribeCOVProcessorHelpers.cpp, BACnetSubscribeCOVPropertyMultipleProcessor.cpp, BACnetWhoHasProcessor.cpp.
    • Scripted rewrite used for the mechanical call-site conversions (both source/ and tests/): a Python script scanning for each exact call prefix (e.g. device->ObjectExists(, devicePtr->DeleteObject(, this->UpdateObjectIdentifier(), parsing the parenthesized argument list with balanced-paren/top-level-comma splitting (so nested casts like (uint16_t)BACnetObjectType::calendar and nested calls like objectIdentifier->GetObjectType() are not mis-split), skipping any match on a //-commented line, and requiring an exact argument count (2 for ObjectExists/DeleteObject, 4 for UpdateObjectIdentifier) before rewriting - a call that didn’t match the expected shape was left untouched rather than guessed at. UpdateObjectIdentifier’s 4-argument sites became two identifier expressions, not one. Applied per-file, one call-prefix at a time, so a differently-shaped receiver on the same method name (db.ObjectExists/BACnetDatabase::ObjectExists, already-packed) was never touched.
    • Spot-check coverage: every rewritten line was re-grepped and read back in full (not sampled) - 28 this->ObjectExists sites and 1 this->UpdateObjectIdentifier site in BACnetDBDevice.cpp; 4 in BACnetBusinessLogic.cpp; 4 (ObjectExists) + 18 (DeleteObject) in BACnetDatabase.cpp; 3 in BACnetInterface.cpp; 2 in BACnetReadPropertyMultipleProcessor.cpp, 1 each in BACnetSubscribeCOVProcessorHelpers.cpp, BACnetSubscribeCOVPropertyMultipleProcessor.cpp, BACnetWhoHasProcessor.cpp; and across tests/: 61 ObjectExists sites (TestBACnetAtomicFileRecordAccess.cpp, TestBACnetCreateObjectProcessor.cpp x6, TestBACnetDatabase.cpp x27, TestBACnetDBDevice.cpp x13, TestBACnetDeleteObjectProcessor.cpp x8, TestBACnetStackAccessDoor.cpp x2, TestBACnetStackEventEnrollmentObject.cpp x3, TestBACnetStackFoundations106.cpp), 4 DeleteObject sites (TestBACnetAuditLogQuery.cpp, TestBACnetDBDevice.cpp, TestBACnetStackAccessDoor.cpp, TestBACnetStackAlertEnrollmentObject.cpp), and 16 UpdateObjectIdentifier sites (TestBACnetDBDevice.cpp). Checked for: correct argument order preserved (no swap), no double-wrapping of an already-BACnetObjectIdentifier32 argument, db.ObjectExists/ BACnetDatabase::ObjectExists/BACnetInterface::ObjectExists/g_bacnet.ObjectExists (different receivers, already packed or ban-listed) left untouched, and commented-out call sites (one, in BACnetDBDevice.cpp) left as-is.
    • ci_scripts/object-identifier-boundary-allowlist.txt: BACnetDBDevice.h lowered from 61 to 58 (one per converted declaration), re-measured directly against the gate’s own regex rather than assumed.
    • Required regression test added (the original #1872 issue’s own ask, not previously present): tests/TestBACnetDBDevice.cpp’s UpdateObjectIdentifier_TimerRename_OldGoneNewCarriesStoredValue - renames a Timer object (timer, 600) to (timer, 601) through the newly-packed UpdateObjectIdentifier, asserts the old identifier no longer exists and GetTimer(600) is NULL, and asserts the new identifier exists with GetTimer(601)->GetDefaultTimeoutMs() still carrying the 12345 value stored before the rename (proving m_timers, one of UpdateObjectIdentifier’s specialized instance-keyed maps, migrated correctly). No new test file, so no vcxproj registration needed.
    • No pair forwarders were needed for any of the 3 converted methods: grep confirms no session-D file (CASBACnetStackDLL.cpp/.h, BACnetCallbackInterface.h, CASBACnetStackTestToolDLL.cpp/.h, BACnetTestToolInterface.cpp/.h) calls BACnetDBDevice::ObjectExists/DeleteObject/ UpdateObjectIdentifier directly with the old pair form.
    • Verification: ci_scripts/check-object-identifier-boundary.sh --self-test (8/8) and the real gate both green with the lowered allowlist (58). Control arm (gate): re-adding a pair (const uint16_t objectType, const uint32_t objectInstance) to ObjectExists’s declaration made the gate fail red (BACnetDBDevice.h has 59 pair-parameter declaration(s), allowlist ceiling is 58); reverted. Control arm (type): replacing UpdateObjectIdentifier’s converted this->ObjectExists(newObjectIdentifier) call with this->ObjectExists(newObjectInstance) (a bare uint32_t) failed to compile (cannot convert 'const uint32_t' ... to 'CASBACnetStack::BACnetObjectIdentifier32'); reverted. Every touched source/*.cpp file (13 files) compiles cleanly standalone (g++ -fsyntax-only) under both default/BACNET_STACK_TESTTOOL and NO_TESTTOOL=1 via WSL. Full WSL g++ suite run TWICE under default/TESTTOOL, both green and identical: 8514/8514 tests passed, 903 suites (the total reflects this batch’s own new regression test plus every intervening batch’s additions since a-20260914-101’s own 8504/902 count). NO_TESTTOOL=1 could not be fully linked/run at this tip - confirmed BEFORE this batch’s changes too (via git stash + rebuild), i.e. entirely pre-existing and unrelated to this batch: beyond the previously-documented TestBACnetComplexAckProcessorWireLevel.cpp/TestBACnetInterface_Hooks.cpp BACnetTestToolInterface-incomplete-type gap, TestBACnetInterface.cpp hits the same gap, and TestBACnetOptionalPriorityFilter.cpp/TestSelfAssignmentGuards.cpp fail on unrelated missing symbols (BACnetPriorityFilter, BACnetPrimitiveBitSTRING::TestSetBitStringFromSelf) - identical error set with and without this batch’s diff applied. No known-flaky test (BACnetInterfaceWritePropertyAsyncTest.SendWriteProperty_StateTextCharacterStringArray) failure was observed in either TESTTOOL run.
    • Deviation from the runbook’s suggested WSL-clone branch workflow: this session ran git branch/checkout commands against the /mnt/c/... WSL mount of THIS SAME checkout (not a separate WSL clone), which briefly switched the working branch to issues/runbook and deleted the local rb/a-20260914-105 branch pointer. No commits or working-tree edits were lost - rb/a-20260914-105 was recreated at the same commit (0455c995c, identical to issues/runbook, confirmed via git reflog before recreating) and the working tree’s uncommitted changes were never touched by the branch operations. Recorded here for the parent session’s awareness; no code or test impact.
  • rb a-20260914-91 (#1871, part A of #1592): packed BACnetDatabase’s and BACnetDBObject’s public (objectType, objectInstance) PAIR parameters into the strong BACnetObjectIdentifier32 type (source/BACnetObjectIdentifier32.h, from #1762) - the second package of the internal half of the #1592 object-identifier boundary conversion (session D’s BACnetInterface/test-tool DLL work is the first half; BACnetDatabase/BACnetDBObject is the layer directly below it).
    • source/BACnetDatabase.h/.cpp: all 51 pair-taking method declarations converted to take a packed BACnetObjectIdentifier32 (or, for the handful with a second identifier - AddTrendLogObject, AddLoggedObjectToTrendLogMultiple’s log-object identifier; SetLiftOrEscalatorEnergyMeterRef’s energy-meter-ref identifier; EnableIntrinsicAccessEventAlgorithm’s access-credential identifier; the AddEventEnrollmentObject* family’s/SetEventEnrollmentAlgorithmPropertyReference’s monitored/ referenced-object identifier - two packed parameters). Each method body unpacks the identifier into local objectType/objectInstance at the top via .ObjectType()/.ObjectInstance() and forwards those locals unchanged into BACnetDBDevice (whose own conversion is the separate, later #1872/#1873 - BACnetDBDevice.h keeps its own unrelated allowlist entry at 96, untouched here).
    • source/BACnetDBObject.h/.cpp: the constructor now takes BACnetObjectIdentifier32 instead of the pair (the parameter was already unused/NOREF’d in the body, so this is a pure signature change).
    • 12 pair-form overloads were kept as one-line forwarders (FromRaw(GenerateObjectIdentifier(...)), not TryMake - these are internal repacking, not a boundary revalidation) because BACnetTestToolInterface.cpp (session D, not yet converted; ban-listed for this batch) still calls them with a raw pair: EnableIntrinsicAccessEventAlgorithm, SetAccessEventContext, AddEventEnrollmentObjectWithOutOfRangeAlgorithm, AddEventEnrollmentObjectWithChangeOfStateAlgorithm, AddEventEnrollmentObjectWithChangeOfValueAlgorithm, AddEventEnrollmentObjectWithExtendedAlgorithm, AddEventEnrollmentObject, SetFaultStateAlgorithm, SetEventEnrollmentAlgorithmPropertyReference, SetEventEnrollmentFaultAlgorithmPropertyReference, SetFaultCharacterstringAlgorithm, SetFaultExtendedAlgorithm. #1876 deletes these forwarders once that session converts. Every other pair overload was deleted outright rather than left as dead code.
    • Callers updated: source/BACnetInterface.cpp (call sites into BACnetDatabase only - passes its own already-packed identifier parameter through directly instead of unpacking-then-repacking; two of its own methods, SetTrendLogStartStopTime and SetLiftOrEscalatorEnergyMeterRef, still take a raw pair themselves - out of scope, a separate BACnetInterface.h package - so their call into BACnetDatabase packs via FromRaw(GenerateObjectIdentifier(...)) at the call site instead) and five direct BACnetDBObject construction sites plus two base-class constructor chains in source/BACnetDBDevice.cpp (also out of scope for its own, later pair conversion; packs via FromRaw from the already-computed local identifier).
    • Tests updated to call the packed signatures: tests/TestBACnetDatabase.cpp, tests/TestBACnetNetworkLayer_NetworkNumberIs.cpp, tests/TestTrendLogs.cpp, tests/TestBACnetInterface_VirtualDevices.cpp, tests/TestBACnetDBObject.cpp. A new test, BACnetDatabase.PackedObjectIdentifierCorners_ObjectExistsAndPropertyEnabledRoundTrip, drives AddObject/ObjectExists/SetPropertyEnabled at the three corners called out in the issue - (0,0), (1023, 4194303) (BACnet’s wildcard/“no object” instance, cl. 12 - accepted by the type, but still rejected by BACnetDatabase::AddObject’s own separate, pre-existing 22-bit range check, unrelated to and unchanged by this conversion), and (512, 1) (the #562 signed-shift corner) - comparing the packed API’s own behavior against BACnetDBDevice’s still-pair ObjectExists/IsPropertyEnabled for the same identifier.
    • ci_scripts/object-identifier-boundary-allowlist.txt: BACnetDBObject.h’s line removed (0 pair declarations left); BACnetDatabase.h lowered from 51 to 12 (exactly the forwarder count above).
    • Verification: ci_scripts/check-object-identifier-boundary.sh --self-test (8/8) and the real gate both green. Full WSL g++ test suite green under default/TESTTOOL (8494 tests). Under NO_TESTTOOL=1, the 4 changed source files compile cleanly (g++ -fsyntax-only) but the full test binary does not link - confirmed pre-existing and unrelated to this batch (≈20+ test files unconditionally construct BACnetTestToolInterface with no #ifdef BACNET_STACK_TESTTOOL guard, e.g. TestBACnetComplexAckProcessorWireLevel.cpp, TestBACnetInterface_Hooks.cpp; this commit touches neither file, and independently re-verified by review).
  • rb a-20260914-93 (#1872, part B of #1592) - partial, 23 of 42 methods: packed a subset of BACnetDBDevice’s public (objectType, objectInstance) PAIR parameters into the strong BACnetObjectIdentifier32 type (source/BACnetObjectIdentifier32.h, from #1762/#1871).
    • Scope decision. The issue’s own suggested first slice (the object-store trio AddObject/DeleteObject/ObjectExists plus IsObjectDeletable/IsObjectSubscribable/ UpdateObjectIdentifier) was measured before converting and found to have roughly 1,023 call sites combined (861 for AddObject alone, the great majority in tests/, which call it to build test fixtures) - far beyond what this session could convert and verify safely. This batch instead converts a different, smaller, self-consistent slice: the property-metadata query/build family, whose combined call-site count is ~123. AddObject, DeleteObject, ObjectExists, and UpdateObjectIdentifier are left for a follow-up (see below).
    • Methods converted (23): GetObjectOverrides, GetPropertyList, GetRequiredPropertyList, GetOptionalPropertyList, GetPropertyProfile, GetPropertyArrayLength, IsPropertyAnArray, IsPropertyWritable, IsPropertySubscribable, IsObjectSubscribable, IsObjectDeletable, GetStoredDBValue, SetStoredDBValue, AddValueToStoreInDB, GetDefaultPropertyValue, GetGeneratedPropertyValue, BuildPropertyList, BuildArrayProperty, BuildCommandablePresentValue, BuildCurrentCommandPriority, BuildLiftOrEscalatorEnergyMeter, IsConfigurationFile, GetTrendLogsByObject. Each body unpacks the identifier into local objectType/objectInstance at the top via .ObjectType()/.ObjectInstance() and forwards those locals unchanged, matching #1871’s convention exactly; no pair overload was kept as a forwarder for any of these 23 (none is called from a session-D file).
    • Methods NOT converted in this batch, left for #1872’s follow-up: AddObject, DeleteObject, ObjectExists, UpdateObjectIdentifier (the object-store trio + rename - very large call-site fan-out, mostly in tests/), SetPropertyEnabled/SetPropertyWritable/SetPropertySubscribable (378/115/73 call sites respectively - the mutator siblings of the three query methods converted here), AddProprietaryProperty (excluded because its declared parameter count overlaps BACnetDBObject::AddProprietaryProperty’s own optional-argument range, making a scripted, exact-arg-count call-site rewrite ambiguous between the two classes), AddGroupMember/ RemoveGroupMember/AddAveragingObject/GetSubscriptions/ GetSubscriptionsForObjectSkippingSpecificProperty (each shares its name with an unrelated method on another class - BACnetInterface, BACnetStackGroup, BACnetTestToolInterface, BACnetStackActiveCOVSubscriptions - and needs call-site-by-call-site review rather than a bulk rewrite), and the four dual-object-reference methods plus EnableTrendLogProperties/EnableLiftOrEscalatorProperties (AddObjectPropertyReferenceToChannel, AddScheduleObjectPropertyReference, AddTimerObjectPropertyReference, AddLogDeviceObjectPropertyToTrendLogMultiple).
    • Correction (post-review): this fragment originally claimed the BACnetDatabase.cpp call sites #1871 left unpacking-then-repacking, and the internal BACnetDBDevice.cpp callers of one converted method from another (GetObjectOverrides’s callers; GetGeneratedPropertyValue’s ~15 internal calls into BuildPropertyList/BuildArrayProperty/BuildCommandablePresentValue/ BuildCurrentCommandPriority/BuildLiftOrEscalatorEnergyMeter/GetPropertyProfile/GetPropertyList/ GetRequiredPropertyList/GetOptionalPropertyList/GetStoredDBValue), now “pass the packed identifier straight through.” An independent review found this false: every one of those call sites still unpacks the in-scope BACnetObjectIdentifier32 into objectType/objectInstance locals and then repacks a NEW identifier via `BACnetObjectIdentifier32::FromRaw(BACnetPrimitiveObjectIdentifier::GenerateObjectIdentifier(objectType, objectInstance))for the downstream call, rather than passing the already-in-scope identifier directly.GenerateObjectIdentifierreconstructs the identical bit pattern, so this is functionally harmless (no test can distinguish it, and it is not a boundary revalidation - noTryMakeis involved) - but it is real, unnecessary pack/unpack/repack churn this batch introduced and did not actually remove as claimed, left as a disclosed follow-up cleanup (mechanical: replace eachFromRaw(GenerateObjectIdentifier(objectType, objectInstance))` expression with the already-in-scope identifier parameter, once each site’s exact parameter name is confirmed) rather than risk a blanket, unverified rewrite across ~30 call sites in a 10,000+ line file at this stage.
    • Tests switched to the packed form across tests/TestBACnetDBDevice.cpp, tests/TestBACnetDBDeviceGetPropertyArrayLength.cpp, tests/TestBACnetDBPropertyProfileRequiredListAudit.cpp, tests/TestBACnetDatabase.cpp, tests/TestBACnetInterface_VirtualDevices.cpp, tests/TestBACnetStackEventEnrollmentObject.cpp, tests/TestBACnetStackLightingOutput.cpp, tests/TestBusinessLogic.cpp, tests/TestBACnetAtomicFileRecordAccess.cpp, tests/TestBACnetCreateObjectProcessor.cpp, tests/BACnetSC_Tests/TestBACnetInterface_BACnetSC.cpp, and tests/Helper_Objects/P0CreationPathComparator.h/.cpp. No UpdateObjectIdentifier regression test was added because that method was not converted in this batch.
    • ci_scripts/object-identifier-boundary-allowlist.txt: BACnetDBDevice.h lowered from 96 to 73 (exactly the 23 declarations converted and deleted - none kept as a forwarder).
    • Verification: ci_scripts/check-object-identifier-boundary.sh --self-test (8/8) and the real gate both green with the lowered ceiling. Control arm (gate): re-adding a pair parameter to IsObjectDeletable’s declaration made the gate fail red (BACnetDBDevice.h has 74 pair-parameter declaration(s), allowlist ceiling is 73); reverted. Control arm (type): replacing a converted GetOptionalPropertyList call’s identifier argument with a bare 1u failed to compile (cannot convert 'unsigned int' to 'CASBACnetStack::BACnetObjectIdentifier32'); reverted. Full WSL g++ suite green under default/TESTTOOL (8503/8503 tests). Under NO_TESTTOOL=1, the build fails only on the same pre-existing, unrelated gap #1871 already documented (TestBACnetComplexAckProcessorWireLevel.cpp unconditionally constructs BACnetTestToolInterface with no #ifdef BACNET_STACK_TESTTOOL guard); the four touched source files (BACnetDBDevice.cpp, BACnetDatabase.cpp, BACnetDBObject.cpp, BACnetInterface.cpp) compile cleanly standalone (g++ -fsyntax-only) under both configurations.
  • rb a-20260914-95 (#1872 continuation, part B of #1592): packed BACnetDBDevice’s SetPropertyEnabled/SetPropertyWritable/SetPropertySubscribable - the direct mutator siblings of IsPropertyEnabled/IsPropertyWritable/IsPropertySubscribable (the latter two converted by a-20260914-93) - from (objectType, objectInstance) PAIR parameters into the strong BACnetObjectIdentifier32 type. All 3 methods converted in full (measured call-site counts at this tip: 389/129/82 respectively across source/ and tests/, in line with the prior batch’s estimate).
    • Each method’s body now takes const BACnetObjectIdentifier32 objectIdentifier and unpacks objectType/objectInstance locals at the top via .ObjectType()/.ObjectInstance(), matching #1871’s and a-20260914-93’s convention. The internal per-instance override map (m_objectPropertyOverrides, keyed by the raw packed word) now reads the key via objectIdentifier.Raw() instead of repacking with GenerateObjectIdentifier, and the BACnetDBObject constructed for a new override entry is built directly from the already-packed objectIdentifier parameter (BACnetDBObject object(objectIdentifier)) instead of a FromRaw(GenerateObjectIdentifier(...)) repack.
    • Pass-through cleanup (verified against the diff, not just claimed - see a-20260914-93’s post-review correction for why this matters): BACnetDatabase.cpp’s three SetPropertyEnabled/SetPropertyWritable/SetPropertySubscribable forwarders each already receive a BACnetObjectIdentifier32 objectIdentifier parameter; their calls into BACnetDBDevice’s now-converted methods pass that parameter straight through (devicePtr->SetPropertyEnabled(objectIdentifier, propertyIdentifier, enabled)) instead of unpacking it into locals and repacking - the two dead objectType/objectInstance unpack lines in each forwarder are removed since nothing else in those bodies used them. SetLiftOrEscalatorEnergyMeterRef (BACnetDatabase.cpp) has the same objectIdentifier parameter in scope for other uses in the function; its one call into SetPropertyEnabled was changed to reuse it directly instead of repacking. Confirmed by grep after the edit: objectIdentifier, BACnetPropertyIdentifier::energyMeterRef and the three objectIdentifier, propertyIdentifier call sites are the only argument-lists at those 4 sites now - no FromRaw(GenerateObjectIdentifier remains at them. Elsewhere - the two SetLiftHigherLowerDeck calls in BACnetDatabase.cpp, the 16 call sites in BACnetDataLinkSC.cpp/BACnetDataLinkSC_NetworkPort.cpp, and the 279 internal this->SetProperty{Enabled,Writable} calls inside BACnetDBDevice.cpp itself (spread across 49 helper functions - object-creation helpers like AddLightingOutputObject/AddFileObject/ AddTimerObject/AddLifeSafetyPointObject, network-port/backup/trend-log property setup like EnableNetworkPortSecureConnectProperties/EnableNetworkPortIPv4Properties/ SetBackupAndRestoreEnabledProperties/EnableTrendLogProperties, and the alarm/event intrinsic algorithm enablers like EnableIntrinsicOutOfRangeAlgorithm/SetAlarmsAndEventsForObjectEnabled) - no identifier variable of the same pair is in scope under the converted parameter’s name at any of these call sites: each caller still only has a literal BACnetObjectType::x (or a raw objectType parameter) plus a bare instance value/parameter, because none of these 49 functions has itself been converted to take a BACnetObjectIdentifier32 yet. Those call sites therefore legitimately keep the BACnetObjectIdentifier32::FromRaw(BACnetPrimitiveObjectIdentifier::GenerateObjectIdentifier(...)) repack; there is no pass-through to do until (if ever) their enclosing functions are themselves converted.
    • No pair overload was kept as a forwarder for any of the 3 methods: neither CASBACnetStackTestToolDLL.cpp/.h, BACnetTestToolInterface.h/.cpp, nor BACnetCallbackInterface.h (the session-D files) call BACnetDBDevice’s SetPropertyEnabled/ SetPropertyWritable/SetPropertySubscribable directly - confirmed by grep, none matched.
    • Tests switched to the packed form: tests/TestBACnetDBDevice.cpp, tests/TestBACnetDBDeviceStackSettings.cpp, tests/TestBACnetReadPropertyMultipleProcessor.cpp, tests/TestBACnetStackEventEnrollmentObject.cpp, tests/TestBACnetStackLifeSafetyPoint.cpp, tests/TestBACnetStackLifeSafetyZone.cpp, tests/TestBACnetStackTimer.cpp, tests/TestBACnetSubscribeCOVProcessor.cpp, tests/TestBACnetSubscribeCOVProcessorHelper.cpp, tests/TestBACnetSubscribeCOVPropertyMultipleProcessor.cpp, tests/TestBACnetSubscribeCOVPropertyProcessor.cpp, tests/TestBACnetSubscribedRecipientsMatchRule.cpp, tests/TestBACnetVirtualNetworkPortNetworkNumberWrite.cpp, tests/TestBACnetWriteGroupProcessor.cpp, tests/TestBACnetWritePropertyMultipleProcessor.cpp, tests/TestBACnetWritePropertyProcessor.cpp, tests/TestBACnetYouAreProcessor.cpp, tests/TestBusinessLogic.cpp, tests/TestBACnetBusinessLogicAddListElement.cpp, tests/TestBACnetBusinessLogicRemoveListElement.cpp, tests/TestBACnetChangeListErrorReporting.cpp, tests/TestP0GenericVsTyped_LightingTimerProgramLoadAveraging.cpp. Test fixtures using the already-packed BACnetInterface/BACnetDatabase layer (bacnet./db. receivers, which take (deviceInstance, objectIdentifier, propertyIdentifier, ...) and were already converted by earlier batches) and BACnetDBObject’s own unrelated 3/4-arg SetPropertyEnabled/SetPropertyWritable/SetPropertySubscribable (object. receiver) were left untouched - neither is BACnetDBDevice’s method.
    • ci_scripts/object-identifier-boundary-allowlist.txt: BACnetDBDevice.h lowered from 73 to 70 (exactly the 3 declarations converted).
    • Verification: ci_scripts/check-object-identifier-boundary.sh --self-test (8/8) and the real gate both green with the lowered ceiling. Control arm (gate): re-adding the pair (const uint16_t objectType, const uint32_t objectInstance) ahead of SetPropertyEnabled’s objectIdentifier parameter made the gate fail red (BACnetDBDevice.h has 71 pair-parameter declaration(s), allowlist ceiling is 70); reverted. Control arm (type): replacing a converted SetPropertyEnabled call’s identifier argument with a bare 1u in tests/TestBACnetDBDevice.cpp failed to compile (cannot convert 'unsigned int' to 'CASBACnetStack::BACnetObjectIdentifier32'); reverted. Full WSL g++ suite green under default/TESTTOOL (8503/8503 tests, 902 suites). Under NO_TESTTOOL=1 the build fails only on the same pre-existing, unrelated gap already documented by a-20260914-91/93 (TestBACnetComplexAckProcessorWireLevel.cpp unconditionally constructs BACnetTestToolInterface with no #ifdef BACNET_STACK_TESTTOOL guard); the four touched source files (BACnetDBDevice.cpp, BACnetDatabase.cpp, BACnetDataLinkSC.cpp, BACnetDataLinkSC_NetworkPort.cpp) compile cleanly standalone (g++ -fsyntax-only) under both configurations.
  • rb a-20260914-97 (#1872 continuation, part B of #1592): packed 4 of BACnetDBDevice’s remaining name-collision methods - AddGroupMember, RemoveGroupMember, GetSubscriptions, GetSubscriptionsForObjectSkippingSpecificProperty - in full, from (objectType, objectInstance) PAIR parameters into the strong BACnetObjectIdentifier32 type, plus AddAveragingObject with its pair-form overload kept as a one-line forwarder (see below). These 5 were deliberately deferred by an earlier batch because each name-collides with an unrelated, differently-shaped method on ANOTHER class (BACnetInterface, BACnetStackGroup, BACnetStackActiveCOVSubscriptions, BACnetTestToolInterface) - every call site was checked by receiver type, not just method name, to avoid converting the wrong class’s method.
    • AddGroupMember/RemoveGroupMember: the pair being converted is (memberObjectType, memberObjectInstance) - the GROUP MEMBER’s identifier, not the Group object’s own objectInstance (which stays a bare uint32_t, unrelated to this issue). Each body now unpacks memberObjectType/memberObjectInstance locals from memberObjectIdentifier before calling BACnetStackGroup::InsertGroupMember/RemoveGroupMember - a DIFFERENT class’s same-named method that has not itself been converted and still takes the pair, so no pass-through is possible there.
    • GetSubscriptions/GetSubscriptionsForObjectSkippingSpecificProperty: the pair being converted is (objectType, objectInstance), unpacked locally before calling BACnetStackActiveCOVSubscriptions::GetSubscriptions/GetSubscriptionsForObjectSkippingSpecificProperty
      • same names, different (unconverted) class - which still takes the pair, so no pass-through is possible there either.
    • AddAveragingObject: the pair being converted is (refObjectType, refObjectInstance) - the monitored/referenced object, not the Averaging object’s own objectInstance. Its OLD pair-form overload is kept as a thin forwarder (this->AddAveragingObject(objectInstance, BACnetObjectIdentifier32::FromRaw(BACnetPrimitiveObjectIdentifier::GenerateObjectIdentifier((refObjectType), (refObjectInstance))), refPropertyIdentifier, ...)) because BACnetTestToolInterface.cpp (session D, ban-listed/read-only for this issue) still calls that exact pair-form overload directly from its own AddAveragingObject - confirmed by grep, no other caller uses it. This is below the true object-identifier boundary (the pair there already came from an already-valid BACnetObjectIdentifier32 BACnetTestToolInterface itself unpacked), so the forwarder repacks via FromRaw/GenerateObjectIdentifier rather than TryMake, matching every other internal repack in this file - TryMake stays reserved for the two DLL boundary files.
    • Pass-through cleanup (verified against the diff via grep, not just claimed - see a-20260914-93’s post-review correction for why this matters): BACnetInterface::AddGroupMember/ RemoveGroupMember already receive a BACnetObjectIdentifier32 memberObjectIdentifier parameter; their calls into BACnetDBDevice’s now-converted methods (device->AddGroupMember(groupObjectInstance, memberObjectIdentifier, propertyIdentifier, useArrayIndex, arrayIndex) and device->RemoveGroupMember(groupObjectInstance, memberObjectIdentifier)) pass that parameter straight through instead of unpacking to a pair and repacking - the two now-dead local unpack lines in each were removed (their only other use was the #else/not-compiled branch’s NOREF, which now references memberObjectIdentifier directly). source/BACnetBusinessLogic.cpp’s two callers of GetSubscriptions/ GetSubscriptionsForObjectSkippingSpecificProperty (inside UpdateCOVSubscriptions) have no BACnetObjectIdentifier32 in scope under any name (that function’s own objectType/ objectInstance parameters have not themselves been converted), so those two call sites legitimately repack via FromRaw/GenerateObjectIdentifier rather than passing anything through.
    • No pair overload was kept for AddGroupMember/RemoveGroupMember/GetSubscriptions/ GetSubscriptionsForObjectSkippingSpecificProperty: grep confirms none of CASBACnetStackDLL.cpp/.h, CASBACnetStackTestToolDLL.cpp/.h, BACnetTestToolInterface.h/ .cpp, or BACnetCallbackInterface.h (the session-D files) call any of these 4 methods on BACnetDBDevice directly - CASBACnetStackDLL.cpp’s own AddGroupMember/RemoveGroupMember exports call BACnetInterface’s (already-packed) versions, not BACnetDBDevice’s, and are untouched.
    • Tests switched to the packed form: tests/TestBACnetBusinessLogicRemoveListElement.cpp, tests/TestBACnetStackGroupPresentValue.cpp, tests/TestBACnetStackAveraging.cpp, tests/TestP0GenericVsTyped_LightingTimerProgramLoadAveraging.cpp, tests/TestBACnetSubscribeCOVProcessorHelper.cpp. tests/TestBACnetStackGroup.cpp’s own BACnetStackGroup::RemoveGroupMember/InsertGroupMember calls (group. receiver, the unrelated other-class method) were left untouched, as was BACnetStackActiveCOVSubscriptions::GetSubscriptions/GetSubscriptionsForObjectSkippingSpecificProperty (no test calls that class’s version directly).
    • ci_scripts/object-identifier-boundary-allowlist.txt: BACnetDBDevice.h lowered from 70 to 66 - the 4 fully-converted methods each drop one pair-declaration match; AddAveragingObject contributes no net change to the header’s count because its kept pair-form forwarder still declares the pair (verified by hand-counting the gate’s own regex against the header: 66, matching the lowered ceiling exactly).
    • Verification: ci_scripts/check-object-identifier-boundary.sh --self-test (8/8) and the real gate both green with the lowered ceiling. Control arm (gate): re-adding the pair form of AddGroupMember’s declaration (restoring const uint16_t memberObjectType, const uint32_t memberObjectInstance in place of the packed parameter) made the gate fail red (BACnetDBDevice.h has 67 pair-parameter declaration(s), allowlist ceiling is 66); reverted. Control arm (type): replacing a converted GetSubscriptions call’s identifier argument with a bare 1u in tests/TestBACnetSubscribeCOVProcessorHelper.cpp failed to compile (cannot convert 'unsigned int' to 'CASBACnetStack::BACnetObjectIdentifier32'); reverted. This batch’s own touched source files (BACnetDBDevice.cpp, BACnetInterface.cpp, BACnetBusinessLogic.cpp) compile cleanly standalone (g++ -fsyntax-only) via WSL under both default/TESTTOOL and NO_TESTTOOL=1.
    • Pre-existing, unrelated defect found during verification (NOT introduced by this batch - confirmed via git stash against the unmodified tip): the WSL default/TESTTOOL full-suite build fails to compile at this batch’s base commit already, in BACnetDBDevice::SetSegmentationEnabledForTesting (#if defined(BACNET_STACK_TESTTOOL), unrelated to any of this batch’s 5 methods) - 2 calls there still use the old pair-form SetPropertyEnabled removed with no forwarder by a-20260914-95. This blocked getting a from-scratch full-suite pass/ fail count for this batch; a scratch-only, not-committed local patch to those 2 lines was used solely to unblock a full build/run for confidence (then discarded, not part of this commit), and a follow-up fix has been flagged separately. With that scratch patch applied, the full WSL suite built and ran to completion under default/TESTTOOL: 8504/8504 tests passed, 902 suites. NO_TESTTOOL=1 was not similarly blocked (SetSegmentationEnabledForTesting compiles out entirely under NO_TESTTOOL) and only hits the same pre-existing, unrelated gap already documented by a-20260914-91/93 (TestBACnetComplexAckProcessorWireLevel.cpp’s unguarded BACnetTestToolInterface construction) - this batch’s own touched source files compile cleanly standalone (g++ -fsyntax-only) under NO_TESTTOOL=1.
  • rb b-20260906-13 (#1235, CR-F04-007): documented ChipkinUtilities::GetBit/SetBit’s bit<8 precondition (docs-only, S4 observation).
  • rb b-20260906-15 (#1158): documented (docs-only, no behavior change) that LifeSafetyOperation current-state-validity checking (cl. 13.13.2) is entirely delegated to the host CallbackLifeSafetyOperation — a default build with no callback registered always accepts the operation regardless of the target object’s current state.
  • rb b-20260907-11 (#1233 CR-F02c-011): BACnetServicesSupported’s STACK_OPTION_STRING_LITERALS name table moved from header/namespace scope (a private, unused, per-translation-unit copy) to class-static members declared in the header and defined once in a new source/BACnetServicesSupported.cpp, matching the pattern this file’s own cluster-mate BACnetPropertyIdentifier already uses. No behavior change.
  • rb b-20260909-29 (#1660): renamed acknowledgementFilter/m_acknowledgementFilter/GetAcknowledgementFilter/SetAcknowledgementFilter to the spec spelling (acknowledgmentFilter, no middle “e”) across BACnetGetEnrollmentSummaryRequest’s internal identifiers — internal-only, not a customer-facing interface change.
  • rb b-20260909-31 (#993): added missing regression coverage for a fix already present at tip — EnableFaultOutOfRangeAlgorithmReal rejecting non-REAL object types, and OutOfRangeValue::operator< refusing mismatched-datatype comparisons — no production code changed.
  • rb bs-20260922-1 (#2251): the npm package’s supported Node.js range is now >=22.9.0 <25 (was >=18.20.8 <=24.x). Node 18 and 20 are end of life, and the node-gyp 12 needed to build with Visual Studio 2026 requires ^20.17.0 || >=22.9.0. README now states that installing compiles the native addon locally (the published package has no prebuilt binary), and which npm versions can build with Visual Studio 2026 (measured).
  • rb bs-20260922-2 (#2311): BACnet/SC manual §19 and Appendix D.3 give the correct BACnetStack_SetBACnetSCWebSocketStatus values (Connecting 1, Connected 2, Disconnected 3, Error 4; previously Disconnected was shown as 1) and when to use Error (with the RFC 6455 close code) versus Disconnected.
  • rb bs-20260922-5 (#2329): breaking for Node 22 consumers. The npm package now requires Node 24.21.x (engines >=24.21.0 <25, owner ruling on #2329; this supersedes #2251’s >=22.9.0 <25) and is built with it: .nvmrc, the publish workflow, @types/node 24, packageManager npm@11, and the devcontainer (Node 24.21.0 from the checksum-verified nodejs.org tarball, replacing Ubuntu’s Node 18).
  • rb bs-20260922-5 (#2329): source builds of the Node addon compile in parallel. The package’s install script and npm run build:cpp / build:cpp:fast run node-gyp rebuild -j max; without -j, node-gyp compiles on one core on Linux and macOS. Measured on a 12-core machine (WSL, Node 24.21.0): 525 s serial → 86-88 s. Windows was already parallel through Directory.Build.props (#2315).
  • rb bs-20260922-5 (#2329): publish_npm.yml compiles the default addon once per platform, in parallel, instead of three times serially. The last of the three (npm run build) produced a build/ directory that was never shipped. The redundant RTTI environment flags were removed (binding.gyp already drops -fno-rtti). A publish input on manual dispatch allows a dry run.
  • rb c-20260908-1 (#1016): BACnetStack_InsertEventLogRecord stays test-tool-only, and the rationale in source/CASBACnetStackTestToolDLL.h/.cpp is rewritten to say why rather than left contradicting the customer header. Sprint 261 (5b59c97a0) moved both inserts out on a family-consistency argument — InsertAuditLogRecord and InsertNotificationLogRecord were already test-tool-only — but that rule was not applied to the whole family: InsertTrendLogMultipleRecord stayed in the customer header in the same commit and still ships there. The invariant the shipped surface actually follows is the read/write pair: if the Read half is public, the Insert half is public. Trend Log Multiple ships both, Event Log ships neither, and only plain Trend Log was split — so this restores a rule rather than overturning one.
  • rb c-20260908-1 (#1016): docs/CAS BACnet Stack - Breaking Changes from version 5 to version 6.md no longer tells customers BACnetStack_InsertTrendLogRecord has moved to the test-tool interface. That document is the single source of truth for v5→v6 migration, so a customer checking whether a restore path exists was reading the exact denial this issue exists to remove. It now carries a “moved, then RESTORED” row, which also tells anyone who wrote a workaround for the v6 removal that they can drop it.
  • rb c-20260908-2 (#1012): ABI BREAK — BACnetStack_SetBACnetStackSetting and BACnetStack_GetBACnetStackSetting take a leading deviceInstance. Stack settings are now addressed per device. Migration is mechanical and preserves the old behaviour exactly: pass BACNET_STACK_SETTING_ALL_DEVICES (4194303) as the new first argument — the old two-argument form applied process-wide, and that constant addresses precisely that, the process default. Pass a real device instance instead to override the default for one device. 4194303 cannot collide with a real device: BACnetStack_AddDevice already rejects it as the BACnet reserved instance.
  • rb c-20260908-2 (#1012): settings resolve in two layers — an explicit per-device value always wins, otherwise the process default, including for devices added after the default was set. Writing the default never disturbs a device that has set its own value, and a device may deliberately set BACNET_STACK_SETTING_DISABLED to opt out of a cap the default enables. There is currently no way to remove an override once set; the header says so.
  • rb c-20260908-2 (#1012): the Device_Address_Binding tunables are configurable per device for the first time. They previously routed through BACnetDatabase::GetDevice() with no argument — the main device, hardcoded — so they could not be set for any other device at all. That was unreachable rather than relied upon, since there was no parameter to pass.
  • rb c-20260908-2 (#1012): BACnetStack_SetBACnetStackSetting can now be called before any device is added, to establish the default. It previously failed for the DAB tunables until a device existed.
  • rb c-20260908-5 (#1013): ABI BREAK — BACnetStack_SendUnsubscribedUnconfirmedCOVNotification no longer hard-wires three of clause 21.3.2’s five required fields. subscriberProcessIdentifier and timeRemaining were both fixed at 0 and are now parameters; the single propertyIdentifier becomes a packed property list, so an unsubscribed notification can finally carry the Present_Value and Status_Flags pair clause 13.1 expects, with array indices. Migration is mechanical and behaviour-preserving: pass 0 for both new scalars and a one-element list.
  • rb c-20260908-5 (#1013): the property list is propertyCount fixed 9-byte little-endian elements — uint8 flags, uint32 propertyId, uint32 arrayIndex, flags bit0 = usePropertyArrayIndex — the same shape convention BACnetStack_SendSubscribeCOVPropertyMultiple’s references buffer uses. propertiesLength is validated against the count before any read, so a short buffer is refused rather than over-read.
  • rb c-20260908-5 (#1013): values are read from the announcing device’s own database, not supplied by the caller — the issue’s open action item 4, decided and documented in the header. A gateway announcing for a device it proxies models that device as a virtual device (AddVirtualNetwork + AddDeviceToVirtualNetwork) and announces from it, at which point the values are local by construction.
  • rb c-20260908-8 (#1593): the 50 propertyIdentifier parameters deliberately stay uint32_t. #123-B originally proposed typing them as the enum; the header’s own precedent says otherwise twice — BACnetStackSetting and BACnetDecodeType both publish an enum and keep a plain-integer parameter. In C99 an enum’s underlying type is implementation-defined, and this header compiles as C99, so typing a parameter as an enum would put a compiler’s choice of width and signedness into a frozen ABI for a compile-time nicety.
  • rb c-20260908-8 (#1593): eleven doc blocks referred customers to #CASBACnetStack::BACnetPropertyIdentifier — the internal C++ class they cannot see — and now point at the published enum. Those doc comments are emitted into the generated C#, Java, Python and Rust bindings, so this reaches those customers too.
  • rb d-20260908-3 (WP-1 part 1, interface conventions sweep — IFC-001, IFC-002, IFC-003): every function input on the customer and test-tool surfaces is now const; acknowledgement → acknowledgment and hundreth… → hundredths… (the spec’s own spelling) across the customer flat exports and host callback typedefs. Parameter names and qualifiers only — no ABI impact, no caller or callback implementation needs to change. See docs/interface/IFC-001.md, IFC-002.md, IFC-003.md for the full export lists.
  • rb d-20260908-5 (WP-1 part 2, interface conventions sweep — IFC-004): renamed every wire-year parameter (the BACnet cl. 20.2.12 “year minus 1900” wire octet) to <name>Minus1900/yearMinus1900 — 10 base names across 36 customer, test-tool, and callback declarations. Removes a naming collision the same word had with unrelated full-calendar-year parameters (e.g. SetScheduleEffectivePeriod’s startYear, untouched). Parameter names only — no ABI impact, no caller or callback implementation needs to change. See docs/interface/IFC-004.md.
  • rb d-20260908-7 (WP-1 part 3, interface conventions sweep — IFC-005): documented the const-on-every-input and spec-verbiage rules explicitly in source/CASBACnetStackDLL.h’s API Conventions block (a comment-only addition, no declaration changed) — completing WP-1 of the interface-freeze runbook.
  • rb d-20260909-1 (WP-2 pkg 1, interface — IFC-006): converted the test-tool int/-1 invoke-id return shape on BACnetStackTestTool_SendGetAlarmSummary and BACnetStackTestTool_SendGetEnrollmentSummary to bool + a leading uint8_t* sentInvokeId out-parameter, matching the customer surface’s confirmed-send shape (#1637 items 1/3/4). Breaking change for any existing caller of these two test-tool exports (compile error, not silent) — see IFC-006 for the migration.
  • rb d-20260909-11 (WP-3 pkg 1, interface — IFC-011): extended source/CASBACnetStackDLL.h’s existing “Byte order” section (added the previous day) to also name BACnetStack_SendUnsubscribedUnconfirmedCOVNotification’s buffer among the host-order little-endian packed buffers — the one export that section’s original list omitted. Comment-only, no declaration changed. (This entry’s first draft duplicated the existing section instead of extending it; corrected before merge — see IFC-011.)
  • rb d-20260909-3 (WP-2 pkg 2, interface — IFC-007): converted the test-tool int/-1 invoke-id return shape on the AddListElement/RemoveListElement family (6 flat exports: AddListElement, RemoveListElement, AddListElementReadAccessSpecifications, RemoveListElementReadAccessSpecifications, AddListElementEventNotificationSubscriptions, RemoveListElementEventNotificationSubscriptions, plus their 4 underlying BACnetTestToolInterface C++ overloads) to bool + a leading uint8_t* sentInvokeId out-parameter, matching the customer surface’s confirmed-send shape (#1637 items 1/3/4). Breaking change for any existing caller (compile error, not silent) — see IFC-007 for the migration.
  • rb d-20260909-5 (WP-2 pkg 3, final — IFC-008): converted the test-tool int/-1 invoke-id return shape on the SendConfirmedEventNotification* family (SendConfirmedEventNotification, WithTimestampForm, WithEventValues, WithOutOfRangeValues) to bool + a leading uint8_t* sentInvokeId out-parameter, matching the customer surface’s confirmed-send shape (#1637 items 1/3/4). This completes WP-2’s return-shape conversion — all 12 targeted exports are now converted (see IFC-006, IFC-007, IFC-008). Breaking change for any existing caller (compile error, not silent).
  • rb d-20260909-7 (WP-2 pkg 4, interface — IFC-009): reordered the two COV notification identifier parameters (initiatingDeviceInstance now before subscriberProcessIdentifier) on 4 test-tool exports (SendConfirmedCOVNotification, SendUnconfirmedCOVNotification, SendConfirmedCOVNotificationMultiple, SendUnconfirmedCOVNotificationMultiple) to match the customer surface’s existing order (#1637 item 3). Silent breaking change, not a compile error — both parameters are uint32_t, so an un-updated caller compiles but binds the values to the wrong parameter. See IFC-009 for the migration.
  • rb d-20260909-9 (WP-2 pkg 5, final — IFC-010): narrowed vendorId from uint32_t to uint16_t on 3 test-tool exports (AddEventEnrollmentObjectWithExtendedAlgorithm, SetEventEnrollmentFaultExtendedParameters, SendAlert); changed objectName on SendIHave from const uint8_t*/uint16_t length to const char*/uint32_t length; narrowed portId from uint32_t to uint8_t on SetNotificationForwarderPortFilter (#1637 item 4). This completes WP-2 in full (all of #1637 items 1/3/4, IFC-006 through IFC-010). Breaking change for existing callers — see IFC-010 for the migration and a corrected export count against the freeze review’s original tally.
  • rb d-20260910-1 (WP-3 pkg 2, interface — IFC-012): widened BACnetStack_SendWriteGroup’s changeListCount (uint8_t→uint32_t) and changeListLength (uint16_t→uint32_t), removing the previous 255-element / 65535-byte ceiling on a WriteGroup change list (#1594 item 2, ruled 2026-09-08). Also fixed a real overflow bug the widening would otherwise have introduced in the internal per-element parsing loop. All six generated language adapters regenerated. Purely additive for existing callers (integer widening is always implicitly safe) — no caller update required.
  • rb d-20260910-11 (WP-3 pkg 7, interface — IFC-017): BACnetStack_SendReadProperty gains a propertyArraysLength parameter and its propertyArraysCount widens to uint32_t (#1594 item 2, ruled 2026-09-08), reopening the ABI freeze issue #992 previously imposed on this export. This is a source-compatibility break: existing C/C++ callers must add one more argument. BACnetStack_SendReadPropertyWithLength (the sibling export created because of that freeze) now has an identical signature to SendReadProperty; both continue to exist side by side. All six language adapters regenerated, including a real fix (not just a documented gap) to the Node addon’s generated wrapper and its hand-maintained TypeScript layer. This completes #1594 item 2 in full - all 9 packed-buffer exports now reach uint32_t count / uint32_t length.
  • rb d-20260910-13 (WP-3 pkg 8, interface — IFC-018): added explicit byte-offset field diagrams for the 4 test-tool COV notification exports’ packed buffers (per-value element and the *Multiple forms’ object-grouping header), closing the runbook’s “add layouts for the two COV property-value buffers that have none” item in full (the other half, SendUnsubscribedUnconfirmedCOVNotification, was already documented). Comment-only, no signature or behavior change.
  • rb d-20260910-16 (WP-3 pkg 9, interface — IFC-019): added worked byte examples for the 4 packed-buffer exports whose value-encoding is stable regardless of the pending #1594 value-encoding ruling (SendReadProperty, SendReadPropertyWithLength, SendSubscribeCOVPropertyMultiple, SendUnsubscribedUnconfirmedCOVNotification) - all verified against actual BACnet enum values and IEEE-754 bit layout, not hand-typed. Also removed a stale doc paragraph on SendSubscribeCOVPropertyMultiple describing its pre-#902 int-return behavior, which contradicted its own current bool+out-param signature. Comment-only, no signature or behavior change.
  • rb d-20260910-18 (WP-4 pkg 1, internal - IFC-020): Sprint 199 Phase 1, the resolution layer. BACnetDataLinkIPv4 gains a bound local address (new state, recorded when the Network Port claims it); BACnetDataLinkLayer::FindDataLinkByNetworkPortInstance/ResolveDataLinkForNetworkPortInstance add the reverse map (port instance alone -> data link) and the sentinel resolution the eventual networkPortInstance ABI parameter will need. Internal only - not wired into packet processing yet (Phase 2). One test-tool export (BACnetStackTestTool_SetNetworkPortIPv4DataLinkInstance) widened to carry the address optionally; no customer-facing change.
  • rb d-20260910-20 (WP-4 pkg 2, internal): Sprint 199 Phase 2, internal plumbing. BACnetDataLinkLayer::ProcessIncomingPacket stops being hardwired to IPv4 instance 0 - it now resolves the right claimed instance via a new FindIpv4DataLinkByLocalAddress (the IPv4 mirror of the existing SC local-URI lookup), matching the destination or source connection string against each claimed instance’s bound local address (Sprint 199 Phase 1). ProcessOutgoingPacket gains the same optional networkPortInstance parameter for an explicit override; no real caller passes one yet, so outgoing behavior is unchanged. New gate test tests/TestBACnetDataLinkIpv4_PerPort.cpp proves two Network Port objects on distinct local addresses are routed to the correct instance. No customer or test-tool surface touched - internal only.
  • rb d-20260910-26 (#1594): BACnetStack_SendWriteGroup’s change-list value bytes now carry the value’s RAW BINARY wire form instead of ASCII/UTF-8 text — a hard cutover, not a dual-supported option. Existing callers must re-encode; see CASBACnetStackDLL.h’s VALUE ENCODING block on the export and docs/interface/IFC-021.md for the full per-type byte layout. First of 7 packed-buffer exports converting under this ruling — SendWriteProperty, SendCreateObject, and the 4 test-tool COV notification exports are unaffected and remain on the text form for now.
  • rb d-20260910-28 (#1594): BACnetStack_SendCreateObject’s initial-property-value bytes now carry the value’s RAW BINARY wire form instead of ASCII/UTF-8 text — a hard cutover, not a dual-supported option. Existing callers must re-encode; see CASBACnetStackDLL.h’s VALUE ENCODING block on the export and docs/interface/IFC-022.md for the full per-type byte layout. Second of 7 packed-buffer exports converting under this ruling — SendWriteProperty and the 4 test-tool COV notification exports are unaffected and remain on the text form for now.
  • rb d-20260910-3 (WP-3 pkg 3, interface — IFC-013): widened BACnetStack_SendReadPropertyWithLength’s propertyArraysCount (uint8_t→uint32_t) and propertyArraysLength (uint16_t→uint32_t), removing the previous 255-element / 65535-byte ceiling (#1594 item 2, ruled 2026-09-08). Also fixed a real loop-accumulator overflow bug the widening would otherwise have introduced. All six generated language adapters regenerated. Purely additive for existing callers — no caller update required. BACnetStack_SendReadProperty’s own frozen ABI (#992) is untouched.
  • rb d-20260910-30 (#1594): the 4 test-tool COV notification exports (BACnetStackTestTool_SendConfirmedCOVNotification, ..._SendUnconfirmedCOVNotification, and their *Multiple variants) now carry the value’s RAW BINARY wire form instead of ASCII/UTF-8 text — a hard cutover, not a dual-supported option. Existing callers must re-encode; see the VALUE ENCODING block above these exports in CASBACnetStackTestToolDLL.h and docs/interface/IFC-023.md for the full per-type byte layout. Buffers 3-6 of 7 converting under this ruling — only SendWriteProperty remains unconverted.
  • rb d-20260910-34 (#1014): BACnetStack_SendIAmRouterToNetwork gains a caller-suppliable network-number-list parameter per cl. 6.4.2 (networkNumbers, networkNumberCount) - passing NULL, 0 preserves today’s “announce my full routing table” behavior exactly. A source-compatibility break for existing callers (the parameter count changed); see docs/interface/IFC-025.md.
  • rb d-20260910-45 (#822/#556, WP-5 pkg 1b): BACnetStack_RegisterCallbackReceiveMessage/BACnetStack_RegisterCallbackSendMessage (and their FPCallbackReceiveMessage/FPCallbackSendMessage typedefs) are renamed to BACnetStack_RegisterCallbackReceiveMessageForPort/BACnetStack_RegisterCallbackSendMessageForPort (FPCallbackReceiveMessageForPort/FPCallbackSendMessageForPort). The old names are removed, not deprecated. The port-identifying parameter widens from uint8_t networkType (one of 3 enum values) to uint32_t networkPortInstance (a Network Port object instance, or the new BACNET_NETWORK_PORT_DEFAULT sentinel). A host must update its callback signatures; existing dispatch behaviour is unchanged except that a receive callback which never reports a claimed instance now has its message silently rejected on any build with more than one data-link transport compiled in, rather than being implicitly treated as BACnet/IP.
  • rb d-20260910-47 (#822/#556, WP-5 pkg 2): the 8 customer BACnetStack_RegisterHook* exports, 3 test-tool BACnetStackTestTool_RegisterHook* exports, and 2 test-tool BACnetStack_RegisterCallback{Audit,Event}NotificationReceived exports keep their names, but each callback’s uint8_t networkType parameter widens to uint32_t networkPortInstance, now reporting a Network Port object instance (or BACNET_NETWORK_PORT_DEFAULT) instead of the packet’s transport type. A host must update its callback signatures; a host whose hook compares the reported value against the old BACnetPacket::NetworkType_* constants will need to compare against a claimed Network Port instance instead once it has claimed one via AddNetworkPortObject.
  • rb d-20260910-49 (#822/#556, WP-5 pkg 3a): all 41 customer client-send exports (BACnetStack_SendIAmRouterToNetwork, BACnetStack_SendWhoIs, BACnetStack_SendReadProperty, BACnetStack_SendWriteProperty, BACnetStack_SendSubscribeCOV, and 36 others) keep their names, but each one’s uint8_t networkType parameter widens to uint32_t networkPortInstance, and the call now resolves and sends over the Network Port object that instance identifies instead of selecting a transport by a small integer. A host that already claims Network Port objects for every transport it uses (via AddNetworkPortObject) and passes a real claimed instance to these calls sees no functional change beyond recompiling. A host that still passes a raw BACnetPacket::NetworkType_* literal (0/1/2) without ever claiming a Network Port object will have every one of these calls start failing loudly instead of silently succeeding - call BACnetStack_AddNetworkPortObject for each transport in use and pass its instance to these exports instead.
  • rb d-20260910-5 (WP-3 pkg 4, interface — IFC-014): widened BACnetStack_SendCreateObject’s propertyArraysCount (uint8_t→uint32_t) and propertyArraysLength (uint16_t→uint32_t), removing the previous 255-element / 65535-byte ceiling (#1594 item 2, ruled 2026-09-08). Also fixed a loop-accumulator overflow and a missing bounds-check gap in the same per-element parsing loop. All six generated language adapters regenerated. Purely additive for existing callers — no caller update required.
  • rb d-20260910-51 (#822/#556, WP-5 pkg 3b, test-tool only - BACNET_STACK_TESTTOOL): all 36 test-tool client-send exports (BACnetStackTestTool_SendIHave, BACnetStackTestTool_SendAuditLogQuery, BACnetStackTestTool_SendUnconfirmedCOVNotification, and 33 others) keep their names, but each one’s uint8_t networkType parameter widens to uint32_t networkPortInstance, matching pkg 3a’s customer-side change (rb d-20260910-49). A caller that already claims Network Port objects and passes a real claimed instance sees no functional change beyond recompiling; a caller still passing a raw BACnetPacket::NetworkType_* literal (0/1/2) without ever calling BACnetStack_AddNetworkPortObject will have every one of these calls start failing loudly instead of silently succeeding.
  • BACnetStackTestTool_SendUnconfirmedCOVNotification/...Multiple: the broadcast parameter moved from last to immediately after networkPortInstance, matching every other Send export’s addressing-block shape (#1637 §2). A source-level break for any caller passing arguments positionally.
  • BACnetStackTestTool_SendAuditLogQuery/BACnetStackTestTool_SendGetEnrollmentSummary: the connection-string/network-port-instance/destination addressing block moved from the middle of the parameter list to the end (#1637 §2). The parameters that used to follow it no longer have default values, since C++ does not allow a default-valued parameter to precede a required one - callers relying on those defaults must now supply them explicitly.
  • rb d-20260910-53 (#822/#556, WP-5 pkg 4 - closeout): BACnetStack_SendRegisterForeignDevice, BACnetStack_SendReadBroadcastDistributionTable, BACnetStack_SendReadForeignDeviceTable, BACnetStack_AddBDTEntry, BACnetStack_GetBDTEntry, and BACnetStack_GetFDTEntry each gain a new leading uint32_t networkPortInstance parameter, and now fail loudly instead of silently defaulting to IPv4 Network Port instance 0 when a caller hasn’t claimed that instance via BACnetStack_AddNetworkPortObject. This is a source-level break: existing callers must add the new argument, not just recompile.
  • BACnetStackTestTool_SendWriteBroadcastDistributionTable/BACnetStackTestTool_SendDeleteForeignDeviceTableEntry (test-tool only, BACNET_STACK_TESTTOOL) gain the identical new leading networkPortInstance parameter.
  • BACnetStack_DecodeAs’s networkType parameter is renamed packetNetworkType (no type change) - a documented, deliberate exception to WP-5’s networkPortInstance conversion, since a pure decoder may be handed bytes captured from a transport this device has no Network Port object for.
  • BACnetStack_AddRouterPort is unchanged - documented as WP-5’s second deliberate exception; a router port is a routing-layer concept distinct from a Network Port object (#304).
  • rb d-20260910-7 (WP-3 pkg 5, interface — IFC-015): widened BACnetStack_SendWriteProperty’s propertyArraysCount (uint8_t→uint32_t) and propertyArraysLength (uint16_t→uint32_t), removing the previous 255-element / 65535-byte ceiling (#1594 item 2, ruled 2026-09-08) - the last of the three FromBytes-cascade exports (alongside SendReadPropertyWithLength/IFC-013 and SendCreateObject/IFC-014). Also fixed a loop-accumulator overflow and a missing bounds-check gap in the same per-element parsing loop, flagged but deferred in both prior entries. All six generated language adapters regenerated. Purely additive for existing callers — no caller update required.
  • rb d-20260910-9 (WP-3 pkg 6, interface — IFC-016): widened the 4 test-tool COV notification exports’ count/length parameters (uint8_t/uint16_t→uint32_t/uint32_t) - BACnetStackTestTool_SendConfirmedCOVNotification, SendUnconfirmedCOVNotification, SendConfirmedCOVNotificationMultiple, SendUnconfirmedCOVNotificationMultiple (#1594 item 2, ruled 2026-09-08). Also fixed two missing bounds-check gaps in the shared per-value parsing loops. No generated adapter touches the test-tool surface, so no adapter regeneration needed. Purely additive for existing callers — no caller update required. This completes all count/length widenings from #1594 item 2 except SendReadProperty (gains a parameter, own isolated package per #992).
  • rb d-20260911-13 (#1592): BACnetTestToolInterface’s SendAddListElement (both overloads) and SendRemoveListElement (both overloads) now take a single BACnetObjectIdentifier32 parameter instead of a separate (objectType, objectInstance) pair - a source-level break for source/static-lib consumers of the C++ class only. The flat C test-tool exports (CASBACnetStackTestToolDLL.h) are completely unaffected. See docs/interface/IFC-037.md.
  • rb d-20260911-15 (#1592): BACnetTestToolInterface’s SetIntrinsicAccessEventAlgorithm (primary (objectType, objectInstance) pair only) and SetAccessEventContext now take a single BACnetObjectIdentifier32 parameter instead of a separate (objectType, objectInstance) pair - a source-level break for source/static-lib consumers of the C++ class only. The flat C test-tool exports (CASBACnetStackTestToolDLL.h) are completely unaffected. See docs/interface/IFC-038.md.
  • rb d-20260911-2 (#1592): BACnetTestToolInterface’s SetFaultStateAlgorithm, AddTimerObjectPropertyReference, AddAveragingObject, SendIHave, InsertNotificationLogRecord, and InsertNotificationLogRecordWithEventValues now take a single BACnetObjectIdentifier32 parameter instead of a separate (objectType, objectInstance) pair - a source-level break for source/static-lib consumers of the C++ class only. The flat C test-tool exports (CASBACnetStackTestToolDLL.h) are completely unaffected. See docs/interface/IFC-033.md.
  • rb d-20260911-3 (#1592): BACnetTestToolInterface’s AddEventEnrollmentObjectWithOutOfRangeAlgorithm, AddEventEnrollmentObjectWithChangeOfStateAlgorithm, AddEventEnrollmentObjectWithChangeOfValueAlgorithm, AddEventEnrollmentObjectWithExtendedAlgorithm, AddEventEnrollmentObject, SetEventEnrollmentAlgorithmPropertyReference, SetEventEnrollmentFaultAlgorithmPropertyReference, SetFaultCharacterstringAlgorithm, SetFaultExtendedAlgorithm, SendAlert, SetLifeSafetyAcceptedModes, SetLifeSafetyAlarmValues, SetLifeSafetyFaultValues, SetLifeSafetyLifeSafetyAlarmValues, and SetLifeSafetyTrackingValue now take a single BACnetObjectIdentifier32 parameter instead of a separate (objectType, objectInstance) pair - a source-level break for source/static-lib consumers of the C++ class only. The flat C test-tool exports (CASBACnetStackTestToolDLL.h) are completely unaffected. See docs/interface/IFC-034.md.
  • rb d-20260911-5 (#1592): BACnetTestToolInterface’s SendConfirmedEventNotification, SendUnconfirmedEventNotification, SendConfirmedEventNotificationWithTimestampForm, SendConfirmedEventNotificationWithEventValues, SendUnconfirmedEventNotificationWithEventValues, SendConfirmedEventNotificationWithOutOfRangeValues, and SendUnconfirmedEventNotificationWithOutOfRangeValues now take a single BACnetObjectIdentifier32 parameter instead of a separate (objectType, objectInstance) pair - a source-level break for source/static-lib consumers of the C++ class only. The flat C test-tool exports (CASBACnetStackTestToolDLL.h) are completely unaffected. See docs/interface/IFC-035.md.
  • rb d-20260911-9 (#1592): BACnetTestToolInterface’s SendConfirmedCOVNotification and SendUnconfirmedCOVNotification now take a single BACnetObjectIdentifier32 parameter instead of a separate (objectType, objectInstance) pair - a source-level break for source/static-lib consumers of the C++ class only. The flat C test-tool exports (CASBACnetStackTestToolDLL.h) are completely unaffected. See docs/interface/IFC-036.md.
  • rb e-20260911-34 (#1234, CR-F03-010): BACnetTag::IsContextTag(uint32_t) renamed to IsContextTagWithValue — the old name, sitting next to IsContextTagWithTagNumber, was confusable with checking a tag number when it actually checks the tag’s value/class. Pure rename, no behavior change.
  • rb e-20260911-35 (#1234, CR-F03-019): BACnetPrimitiveREAL/BACnetPrimitiveDOUBLE’s duplicated IEEE-754 byte-reversal logic is now shared via two new CEndianness helpers (DecodeFixedWidthFromWire/EncodeFixedWidthToWire). Pure refactor, no behavior change.
  • rb e-20260915-25 (#1858): raised BACNET_PROTOCOL_REVISION’s default from 24 to 26, now that every addendum through Revision 26 (cf/#1854, ch/#1855, ck/#1856, cn - already satisfied, cq/cs/#1857) is confirmed satisfied or not applicable. Every >= 25 guard (the inconsistentAttributes abort reason, 4 call sites) now activates by default; every existing >= 24/>= 22/< 22 guard resolves identically to before, since no object-type or service-bit gate in the tree distinguishes 24 from 26. Also corrected a stale STACK_OPTION_MAX_BIT_STRING_SIZE compile-time floor (63, a Revision-22-era value) to 65, matching BACnetObjectType::MAX_BACNET_OBJECT_TYPE (colorTemperature = 64, needing bit offsets 0-64) - already stale at Revision 24, not just newly so at 26. -DBACNET_PROTOCOL_REVISION=24 (or any other value) still overrides the default for a build that must claim an earlier, still-fully-supported revision.
  • rb e-20260916-49 (#2127): extended ci_scripts/node-adapter-argument-count.js’s coverage from 54 to 58 of the ~64 RegisterCallback* pairs. Closes 2 of the gap via an explicit name alias for the ForPort-suffix mismatch between api.ts and the addon’s own <X>Wrapped function (RegisterCallback{Receive,Send}MessageForPort), and 2 more by extending the declaration parser to also match the object-property arrow-function declaration style with a fixed arity (RegisterCallbackInvokeIdGenerator, RegisterCallbackFSMTransition). The remaining 6 are now explicitly reported rather than silently absent: 2 are genuinely variadic (rest-parameter) handlers with no fixed arity to compare (RegisterCallback{Audit,Event}NotificationReceived, printed as “variadic, not checked”), and 4 are file-callback pairs whose addon side invokes a persistent Napi::FunctionReference from a separate static C-callback function rather than <X>Wrapped’s own it->second.Call(...) (RegisterCallback{Read,Write}File{,Record}, printed as “known unchecked” — hand-verified correct, teaching the parser to follow that second indirection would be disproportionate for 4 pairs). Validated the two newly-covered paths against real injected mismatches (reverted before commit) to confirm they actually discriminate, not just parse cleanly.
  • rb e-20260916-51 (#2114): documented the Windows MSVC native-DLL build path (projects/msvs/BuildCASBACnetStack.sln) in BUILD.md, which previously covered only the Linux/gtest and Node addon build paths with no mention of building on Windows at all. CASBACnetStack.vcxproj, CASBACnetStackTests.vcxproj, and CASBACnetStackTestsMemory.vcxproj all hardcode PlatformToolset=v145 (VS2026/v18’s toolset) — not settled by the issue itself as intentional-vs-stale, but the strongest available evidence (release.yml’s own Windows CI job, deliberately pinned to windows-2025 because that image ships v145 natively) points to a real migration, not an accident. The failure this documents (MSB8020) is about which MSBuild.exe binary you invoke, not whether v145 exists anywhere on the machine: a VS2022-generation MSBuild.exe fails even when a separate, newer VS install with v145 is also present (reproduced on this exact machine, which has both). Documents the /p:PlatformToolset=v143 command-line override instead of retargeting the .vcxproj/.sln files, matching what the 4 B-SS language-port example builds already had to add. Round-1 review (two independent findings, both corrected) plus a round-2 finding on the round-1 fix itself (also corrected): the first draft (a) mischaracterized this as a live CI problem (citing windows-2022) when current CI runs on windows-2025/v145 natively; (b) stated “v145 is intentional” and an unverifiable claim about this session’s own MSBuild history with more confidence than either was supported by; the round-1 fix’s own replacement claim (“this machine has only v143”) was itself checked and found false — the machine has both toolsets via separate VS installs, and the real, reproduced failure mode is specifically about the older MSBuild binary not discovering the newer toolset.
  • rb e-20260916-59 (#1872, part B of #1592, continuation): converted BACnetDBDevice::AddProprietaryProperty’s remaining (objectType, objectInstance) pair parameter to a packed BACnetObjectIdentifier32, matching the established convention of every already-converted sibling method in this file. Updated the one production call site (BACnetDatabase::SetProprietaryProperty, which already held a packed identifier and previously unpacked/repacked it pointlessly) and 6 test call sites. Lowered the object-identifier-boundary-allowlist.txt entry for BACnetDBDevice.h from 58 to 57. This closes the object-store trio’s last remaining sibling method besides AddObject itself (~862 call sites, left for its own dedicated future batch per the prior batch’s own assessment) and AddProprietaryProperty — now both done. Only AddObject’s ~862 call sites remain open on #1872.
  • rb e-20260916-61 (#1873, part C of #1592, first slice): converted BACnetDBDevice::EraseFromEngineMaps, AddAlarmAndEventToPendingQueue, and RemoveAlarmAndEventFromPendingQueue’s (objectType, objectInstance) pair parameters to a packed BACnetObjectIdentifier32, matching the established convention from #1872. Updated all real call sites (1 in BACnetDBDevice.cpp already holding a packed local, 2 in BACnetBusinessLogic.cpp constructing one at the call site, 1 test) and lowered the object-identifier-boundary-allowlist.txt entry for BACnetDBDevice.h from 57 to 54. This is a first, deliberately small slice of #1873’s much larger scope (~40 methods total, per the issue’s own exhaustive list) — the remaining ~37 methods (22 EnableIntrinsic*Algorithm variants, 7 EnableFault*Algorithm variants, and the larger alarm/event/life-safety accessors) are left open for dedicated future batches, matching #1872’s own successful incremental pattern.
  • rb e-20260916-63 (#1873, part C of #1592, second slice): converted BACnetDBDevice::GetLifeSafetyObject’s (objectType, objectInstance) pair parameter to a packed BACnetObjectIdentifier32. Leaf-first conversion: all 10 real call sites across 8 calling methods (SetLifeSafetyAcceptedModes, SetLifeSafetyAlarmValues, SetLifeSafetyFaultValues, SetLifeSafetyLifeSafetyAlarmValues, SetLifeSafetyTrackingValue, ApplyLifeSafetyOperationToOneObject, RecomputeLifeSafetyZoneRollUp‘s zone-member lookup, and FeedLifeSafetyIntrinsicAndReport which calls it twice, plus 1 test) are themselves still-unconverted methods on #1873’s remaining list — each call site now constructs a packed identifier from its own still-scalar parameters, following the established FromRaw(GenerateObjectIdentifier(...)) idiom, without needing to convert the calling methods’ own signatures this batch. Lowered the object-identifier-boundary-allowlist.txt entry for BACnetDBDevice.h from 54 to 53. Remaining scope on #1873 is unchanged besides this one method — FeedLifeSafetyIntrinsicAndReport itself remains on the unconverted list despite 2 of its internal call sites now using the packed form.
  • rb e-20260916-65 (#1873, part C of #1592, third slice): converted BACnetDBDevice::EnsureFaultAlgorithm’s (objectType, objectInstance) pair parameter to a packed BACnetObjectIdentifier32. Leaf-first, same pattern as e-20260916-63: all 7 real call sites (the EnableFault*Algorithm family — faultOutOfRange, faultListed, faultStatusFlags, faultLifeSafety, faultState, faultCharacterstring, faultExtended) are themselves still-unconverted methods on #1873’s remaining list — each now constructs a packed identifier at the call site from its own still-scalar parameters. EnsureFaultAlgorithm’s own body already packed objectType/objectInstance inline twice (via the established FromRaw(GenerateObjectIdentifier(...)) idiom) to call SetPropertyEnabled; this conversion simplifies those two call sites to use the parameter directly. Lowered the object-identifier-boundary-allowlist.txt entry for BACnetDBDevice.h from 53 to 52. No test file needed changes — no test calls EnsureFaultAlgorithm directly; the existing behavioral test suite (the EnableFault*Algorithm/event-enrollment/life-safety fault families) already exercises this function through its 7 real callers and continues to pass unchanged, which is the meaningful verification for a pure signature/plumbing change with pre-existing coverage. Remaining scope on #1873 is unchanged besides this one method.
  • rb e-20260916-67 (#1873, part C of #1592, fourth slice): converted BACnetDBDevice::EnableFaultExtendedAlgorithm’s (objectType, objectInstance) pair parameter to a packed BACnetObjectIdentifier32. Only 3 real touch points: (1) its own internal call to the already-converted EnsureFaultAlgorithm, which previously repacked objectType/objectInstance via FromRaw(GenerateObjectIdentifier(...)) and now passes the parameter through directly; (2) BACnetDatabase::SetFaultExtendedAlgorithm (source/BACnetDatabase.cpp), a “caller-already-packed” case — it already held a packed objectIdentifier local (unpacked only for its own error-log formatting) and previously unpacked-then-repacked needlessly just to call this leaf; now passes objectIdentifier straight through; (3) one test call site in tests/TestBACnetStackEventEnrollmentObject.cpp, which constructs a packed identifier via the established FromRaw(GenerateObjectIdentifier(...)) idiom since it only had scalar literals available. Lowered object-identifier-boundary-allowlist.txt’s BACnetDBDevice.h entry from 52 to 51. Revert-red (stashing just the signature-owning header/impl/allowlist while BACnetDatabase.cpp and the test still call the new signature) reproduced exactly 1 compile error at BACnetDatabase.cpp:2096 (a type mismatch), confirming the change is load-bearing. Full suite: 8609/8610 passed (only the known pre-existing MemoryAudit_Issue152 failure). Remaining scope on #1873 is unchanged besides this one method.
  • rb e-20260916-69 (#1873, part C of #1592, fifth slice): converted BACnetDBDevice::EnableFaultStatusFlagsAlgorithm’s (objectType, objectInstance) pair parameter to a packed BACnetObjectIdentifier32. Same “caller-already-packed” shape as e-20260916-67: only 2 real touch points — its own internal call to the already-converted EnsureFaultAlgorithm (previously repacking objectType/objectInstance, now passes the parameter through directly), and BACnetDatabase::EnableFaultStatusFlagsAlgorithm (source/BACnetDatabase.cpp), which already held a packed objectIdentifier local (unpacked only for its own error-log formatting) and previously unpacked-then-repacked needlessly just to call this leaf; now passes objectIdentifier straight through. No test calls this leaf directly (BACnetInterface::EnableFaultStatusFlagsAlgorithm forwards to the BACnetDatabase-level overload, not this one — untouched). Lowered object-identifier-boundary-allowlist.txt’s BACnetDBDevice.h entry from 51 to 50. Revert-red (stashing just the signature-owning header/impl/allowlist while BACnetDatabase.cpp still calls the new signature) reproduced exactly 1 compile error at BACnetDatabase.cpp:1833 (a type mismatch), confirming the change is load-bearing. Full suite: 8609/8610 passed (only the known pre-existing MemoryAudit_Issue152 failure). Remaining scope on #1873 is unchanged besides this one method.
  • rb e-20260916-71 (#1873, part C of #1592, sixth slice): converted BACnetDBDevice::EnableFaultListedAlgorithm’s (objectType, objectInstance) pair parameter to a packed BACnetObjectIdentifier32. Same “caller-already-packed” shape as e-20260916-67/69: BACnetDatabase::EnableFaultListedAlgorithm (source/BACnetDatabase.cpp) already held a packed objectIdentifier local (unpacked only for its own error-log formatting) and was unpacking-then-repacking needlessly just to call this leaf; now passes objectIdentifier straight through. The leaf’s own body was already inline-packing the identifier twice internally (for its EnsureFaultAlgorithm and SetPropertyEnabled calls) — both now use the parameter directly. Two test call sites also converted (tests/TestBACnetDBDevice.cpp, tests/TestBACnetStackEventEnrollmentObject.cpp), each constructing a packed identifier via the established FromRaw(GenerateObjectIdentifier(...)) idiom since neither had a packed value nearby. BACnetInterface::EnableFaultListedAlgorithm (line 2536) forwards to the unrelated BACnetDatabase-level overload and was correctly left untouched. Lowered object-identifier-boundary-allowlist.txt’s BACnetDBDevice.h entry from 50 to 49. Revert-red (stashing just the signature-owning header/impl/allowlist while BACnetDatabase.cpp and both test files still call the new signature) reproduced a compile error at BACnetDatabase.cpp:1814 (a type mismatch), confirming the change is load-bearing. Full suite: 8609/8610 passed (only the known pre-existing MemoryAudit_Issue152 failure). Remaining scope on #1873 is unchanged besides this one method.
  • rb e-20260916-73 (#1873, part C of #1592, seventh slice): converted BACnetDBDevice::EnableFaultLifeSafetyAlgorithm’s (objectType, objectInstance) pair parameter to a packed BACnetObjectIdentifier32. Fourth caller-already-packed sibling in a row (after e-20260916-67/69/71): BACnetDatabase::EnableFaultLifeSafetyAlgorithm (source/BACnetDatabase.cpp) already held a packed objectIdentifier local (unpacked only for its own error-log formatting) and was unpacking-then-repacking needlessly just to call this leaf; now passes objectIdentifier straight through. The leaf’s own single internal call to the already-converted EnsureFaultAlgorithm simplified from repack to pass-through. Five test call sites converted (1 in tests/TestBACnetStackEventEnrollmentObject.cpp, 4 in tests/TestBACnetStackLifeSafetyPoint.cpp — 3 of which shared an identical call-expression substring on POINT_INSTANCE, converted via one replace_all), each constructing a packed identifier via the established FromRaw(GenerateObjectIdentifier(...)) idiom. BACnetInterface::EnableFaultLifeSafetyAlgorithm (line 2553) forwards to the unrelated BACnetDatabase-level overload and was correctly left untouched. Lowered object-identifier-boundary-allowlist.txt’s BACnetDBDevice.h entry from 49 to 48. Revert-red (stashing just the signature-owning header/impl/allowlist while BACnetDatabase.cpp and both test files still call the new signature) reproduced a compile error at BACnetDatabase.cpp:1852 (a type mismatch), confirming the change is load-bearing. Full suite: 8609/8610 passed (only the known pre-existing MemoryAudit_Issue152 failure). Remaining scope on #1873 is unchanged besides this one method.
  • rb e-20260916-75 (#1873, part C of #1592, eighth slice): converted BACnetDBDevice::EnableFaultStateAlgorithm’s (objectType, objectInstance) pair parameter to a packed BACnetObjectIdentifier32. Fifth caller-already-packed sibling in this family (after e-20260916-67/69/71/73): BACnetDatabase::SetFaultStateAlgorithm (source/BACnetDatabase.cpp) already held a packed objectIdentifier local (unpacked only for its own error-log formatting) and was unpacking-then-repacking needlessly just to call this leaf; now passes objectIdentifier straight through. The leaf’s own single internal call to the already-converted EnsureFaultAlgorithm simplified from repack to pass-through. Seven test call sites converted in tests/TestBACnetStackEventEnrollmentObject.cpp (a mix of single-line and multi-line calls; 2 pairs shared identical text and were converted via replace_all, the rest hand-converted), each constructing a packed identifier via the established FromRaw(GenerateObjectIdentifier(...)) idiom. Lowered object-identifier-boundary-allowlist.txt’s BACnetDBDevice.h entry from 48 to 47. Revert-red (stashing just the signature-owning header/impl/allowlist while BACnetDatabase.cpp and the test file still call the new signature) reproduced a compile error at BACnetDatabase.cpp:1993 (a type mismatch), confirming the change is load-bearing. Full suite: 8609/8610 passed (only the known pre-existing MemoryAudit_Issue152 failure). Remaining scope on #1873 is unchanged besides this one method.
  • rb e-20260918-89 (#2111): adapters/golang/CASBACnetStackAdapter.go updated to compile against the current DLL header — moved to the *ForPort callback registrations, removed a type-erasing cast that bypassed cgo signature checking on 5 registrations, retyped the system-time trampoline to CASBACnetTime, added the 4 missing Get*Property trampolines (Bool/Enumerated/UnsignedInteger/OctetString), and switched the hardcoded library reference from Debug to Release.
  • rb e-20260918-95 (#2153): mirrored BACnet/IP’s per-instance data-link array/count/accessor shape onto MS/TP (STACK_OPTION_MAX_DATA_LINK_INSTANCES_MSTP), a pure structural change with no observable behavior difference — GetMstpDataLink() (no-arg) still returns instance 0 unconditionally. Prepares the ground for #2154 (Network Port tracking) and #2155 (router-port binding).
  • rb e-20260922-132 (#1869, part D of #1487): the napi generator now packs a JavaScript Array into the flat C array an export takes. ci_scripts/adapter-roles.json gains an array role (C element type, the governing count parameter, an optional max-count clamp; 16 exports), adapter-roles-verify.js holds each one to a real, count-named integer parameter of the same export and to the header’s element type, and generate-napi-wrappers.js emits the build through a new ValidateArrayElements helper (interface_common.h). 14 test-tool wrappers (SetAccessDoorMembers, SetLifeSafety*Values, SetLoadControlShedLevels, …) are now generated; the bespoke allowlist shrinks by 14, from 71 to 57 entries. Behaviour change for those 14, all test-tool-only: the element count is still the first array’s length, but a parallel array of a different length, a non-Boolean in a bool array, or an element that is not an integer in the C element type’s range (a negative value, a fraction, 65536 for a uint16_t) now throws a TypeError instead of being wrapped, truncated or read as 0. SendAuthRequest (fixed stack arrays with rejecting clamps), SetAccessCredentialAuthenticationFactors (an array of Buffers), AddEventEnrollmentObjectWithChangeOfValueAlgorithm and SetFaultStateAlgorithm stay hand-written, each with its specific reason in the allowlist. ValidateArrayElements leaves an exception an element getter throws pending rather than throwing a TypeError over it (which aborts the process under NAPI_DISABLE_CPP_EXCEPTIONS). adapter-roles-verify.js, generate-napi-wrappers.js --gate and addon-compile.sh are deferred under #1641: they were run and passed with the deferral lifted, and enforce this change again when it lifts.
  • rb e-20260922-135 (#2290): the Node api.ts facade now gives every function parameter that takes the BACnet wire year (cl. 20.2.12: the year minus 1900) its honest ...Minus1900 name, in both the declaration and the forwarding call (callback-signature parameter names are tracked separately in #2300), and thirteen addon wrappers now reject an out-of-range date octet themselves. Renamed public TypeScript parameters (34 in 30 functions; positional callers are unaffected, only the names shown by editors and .d.ts output change; the JSDoc for each now says “Years since 1900 (e.g. 126 for 2026)”): year -> yearMinus1900 in SetAccessCredentialActivationTime, SetAccessCredentialExpirationTime, UseAccessCredential, SetAccessPointLockout, EvaluateAccessRights, AccessPointAuthenticateFactor, AccessPointRequestAccess, AccessZoneEnter, AccessZoneExit, IsAccessZonePassbackViolation, SendTimeSynchronization, SendUTCTimeSynchronization; startYear/stopYear -> startYearMinus1900/stopYearMinus1900 in SetTrendLogStartStopTime, SetEventLogStartStopTime; referenceYear -> referenceYearMinus1900 in SendReadRangeWithTime; timestampYear -> timestampYearMinus1900 in SendConfirmedEventNotification, SendConfirmedEventNotificationWithTimestampForm, SendConfirmedEventNotificationWithEventValues, SendConfirmedEventNotificationWithOutOfRangeValues, SendUnconfirmedEventNotification, SendUnconfirmedEventNotificationWithEventValues, SendUnconfirmedEventNotificationWithOutOfRangeValues, InsertNotificationLogRecord, InsertNotificationLogRecordWithEventValues, SendUnconfirmedAuditNotification, SendConfirmedAuditNotification; eventTimeStampYear -> eventTimeStampYearMinus1900 in SendAcknowledgeAlarm, SendAcknowledgeAlarmWithTimeOfAck; timeOfAcknowledgmentYear -> timeOfAcknowledgmentYearMinus1900 in SendAcknowledgeAlarm, SendAcknowledgeAlarmWithTimeOfAck, SendAcknowledgeAlarmUnsigned, SendAcknowledgeAlarmTime. New exceptions (behaviour change): previously an out-of-range date octet passed to these thirteen wrappers - for example a 4-digit year, which the uint8_t cast aliased mod 256 (a JS 2026 became 234, a legal wire year meaning 2134, the #406 trap), or a month or weekday of 0 - reached the stack, which rejected it by returning false (or {success:false}) where it could tell at all. Now these wrappers validate year, month, day and weekday at full width with ValidateBACnetDateField and throw a RangeError before the call, as the other date-carrying wrappers already did; scripts that asserted on false for such input must catch the exception instead. The thirteen are the generated SetAccessCredentialActivationTime, SetAccessCredentialExpirationTime, UseAccessCredential and SetAccessPointLockout (new bacnet-date-field roles in ci_scripts/adapter-roles.json, regenerated by --write) and the hand-written EvaluateAccessRights, AccessPointAuthenticateFactor, AccessPointRequestAccess, AccessZoneEnter/AccessZoneExit/IsAccessZonePassbackViolation (through their shared unpacking helper), InsertNotificationLogRecordWithEventValues, SendUnconfirmedAuditNotification and SendConfirmedAuditNotification. Roles are also recorded for the hand-written exports whose JS argument layout matches the C one, so the future regeneration keeps the check. The eight hand-written wrappers that already validated a year now name the year argument by its new api.ts name in their error message (for example timestampYearMinus1900). ci_scripts/node-api-param-shape-allowlist.txt gains AccessZoneEnter, AccessZoneExit and IsAccessZonePassbackViolation (minus1900@6), because their wrapper now names the slot yearMinus1900. The addon compiles (ADDON_COMPILE_STRICT=1 addon-compile.sh 12/12, with the #1641 deferral lifted locally for the run), but no runtime test exercises the new rejections.
  • rb e-20260922-136 (#2300): the Node api.ts facade’s host-callback signatures now name every year argument the stack passes as the BACnet wire year (cl. 20.2.12: the year minus 1900) with an honest ...Minus1900 name, with JSDoc saying “years since 1900, e.g. 126 for 2026”. This changes type-level names only; handlers are called positionally, so existing handlers are unaffected. Renamed public TypeScript callback parameters (5 in 4 registrations): year -> yearMinus1900 in the handler types of RegisterCallbackGetPropertyDate (a 1-byte out-Buffer), RegisterCallbackSetPropertyDate and RegisterCallbackSetSystemTime; eventTimeStampYear -> eventTimeStampYearMinus1900 and timeOfAcknowledgmentYear -> timeOfAcknowledgmentYearMinus1900 in RegisterCallbackAcknowledgeAlarm. SetScheduleEffectivePeriod’s startYear/endYear are not renamed, because the C header documents them as full 4-digit years (uint16_t). No gate compares callback parameter names (#2310); verified by diff and tsc --noEmit.
  • rb e-20260922-136 (#2288): ci_scripts/generate-node-api-params.js now derives the element type of an array parameter in api.ts (number[], boolean[], string[]). It reads the type from the napi wrapper’s per-element .Get(i).As<Napi::X>() reads or, for a ValidateArrayElements hand-off, from the array role in ci_scripts/adapter-roles.json. This byte-checks 23 more api.ts parameter lists (all already identical, so no api.ts line changed; 234 -> 257), and ci_scripts/node-api-param-shape-allowlist.txt shrinks from 141 to 118 entries (3 more narrowed to their remaining reason). --write fills a derivable array stub, and treats a changed element type as a signature difference for a person to resolve, not as spelling. #2288 stays open: callback, array-of-object/Buffer and other entries remain (see the issue).
  • rb e-20260923-137 (#2315): the Windows node addon build (npm install / npm run build:cpp) now compiles in parallel. A new Directory.Build.props at the package root (shipped in the npm package via package.json files) sets MSBuild’s UseMultiToolTask and EnforceProcessCountAcrossBuilds for the project node-gyp generates into build/. gyp gives each source its own object file name, so MSBuild ran one cl.exe per file, one at a time, and /MP had no effect. Measured on a 4-CPU Windows 10 box (VS 2026, Node 20.20.2): a clean node-gyp rebuild took 1303 s with 1 cl.exe at a time before, and 473 s with 3-4 at a time after. A 12-CPU box measured 17-18 min before and 175 s after, with 8-13 at a time. To turn it off, set UseMultiToolTask=false in the environment. The binding.gyp comment now explains why /MP gave no gain. The new file chains to any Directory.Build.props above it, so a consumer’s own repo-level props still reach the addon build (verified with a scratch parent file and a control run on the unchained version, which lost it). Verified from the repo checkout; npm pack lists the file, but a timed install from the packed tarball was not run.
  • rb e-20260923-137 (#2315): ci_scripts/test.sh, the CI unit-test gate, now runs make -j"$(nproc)" instead of plain make, as ci_scripts/dev-build.sh already did (JOBS=N overrides this). The gate should get faster on the 4-vCPU runner, but this was not measured here because the box has no Linux/WSL.
  • rb e-20260923-137 (#2315): /MP (MultiProcessorCompilation) is now on in projects/msvs/CASBACnetStack/CASBACnetStack.vcxproj (all four configurations) and in CASBACnetStackTests.vcxproj, which used to set it to false with no stated reason. Measured on the same 4-CPU box (VS 2022 BuildTools): a clean Debug|x64 CASBACnetStackTests build took 775 s, down from 32-37 min in earlier single-cl.exe builds (not an A/B on the same tree), and the suite ran 8710 tests with 8709 passing, the one failure being the pre-existing MSVC-only MemoryAudit_Issue152.ObjectListEncode_PeakHeapBaseline. Only the DebugTestTool and ReleaseDll configurations of CASBACnetStack.vcxproj were built with /MP; DebugLib and ReleaseLib were not. The Linux release library step in release.yml still runs a plain make. That change is left to the owner because it is a .github/ edit.
  • interface-batch-2 (#1028): the zero-parameter placeholder typedefs FPCallbackGetForeignDeviceTable and FPCallbackGetBroadcastDistributionTable are removed. They took no arguments, so they could never have received a decoded table, and nothing ever invoked them; the working replacements are FPHookReadBroadcastDistributionTableAck/FPHookReadForeignDeviceTableAck (#804).
  • interface-batch-2 (#1232, CR-F01b-022): an #if 0-disabled second IsPropertyCommandable overload (~85 lines duplicating the cl. 19.2.1.1 object-type table) is removed; its declaration had been commented out for as long as the body was disabled.
  • interface-batch-2 (#1232, CR-F01c-018): four DLL export wrappers (BACnetStack_SendReadProperty, BACnetStack_SendReadPropertyWithLength, BACnetStack_SendWriteProperty, BACnetStack_SendCreateObject) no longer strip const from propertyArrays — the receiving BACnetInterface methods already take const uint8_t*. The finding said three; there were four.
  • interface-batch-2 (#1232, CR-F01c-019): the two “re-compile with BACNET_STACK_TESTTOOL” diagnostics in a file already gated on that macro now name the macros that actually gate them.
  • source/CASBACnetStackTestToolDLL.h and source/BACnetTestToolInterface.h now open with a conventions note naming the API Conventions block in source/CASBACnetStackDLL.h as normative for the test-tool surface too (owner ruling, 2026-09-08, #1637), with the promotion rationale and the conventions that surface breaks most often. Comment-only; no declaration changed.
  • rb napi-20260908-1 (#1518): ci_scripts/generate-napi-wrappers.js compares every argument of the Validate* family, not the first three. The old matcher fitted ValidateBufferLength(info, "name", buf.Length(), len) and nothing else in the family: on ValidateBACnetDateField(info, "<export>", "<field>", info[n], <KIND>) it captured the field name and info[n], which reduced to expr:info for every index, so every date check in the addon produced the identical comparison string and the eight in one wrapper were eight copies of it. The slot index and the field kind were both invisible, and the trailing arguments of ValidateOutputBufferLength (the required element count, #391’s class) and ValidatePackedElementBuffer (bytesPerElement, fixedStride, #447’s class) were never captured. Arguments are split by balancing parentheses rather than by a comma regex, because every operand in this family is a call. Tooling only; no wrapper’s behaviour changes and the generated set is unmoved at 117.
  • rb napi-20260908-1 (#1509): 95 Node wrappers reject a bad call by throwing a Napi::TypeError instead of returning undefined silently. Customer-visible. Each hand-rolled its argument check as if (info.Length() < N || !info[0].IsNumber() || …) return info.Env().Undefined(); instead of calling CheckParametersCountAndType. The two are equivalent in what they accept and differ only in what they do on rejection, so a JavaScript caller passing correct arguments sees no change; one passing wrong types now gets an exception where it previously got undefined and a silently skipped call. 295 of the addon’s 348 wrappers call CheckParametersCountAndType, where 200 did; 41 still check their own arguments.
  • rb napi-20260908-1 (#1509): 76 of those 95 differed from what the generator emits only in that guard, so they adopted the generated form and left ci_scripts/napi-wrapper-bespoke-allowlist.txt. Generated wrappers go from 111 to 193 of 266; the allowlist from 155 to 73.
  • rb napi-20260908-2 (#1527): CheckParametersCountAndType gains two argument kinds it could not express — DATA_TYPE_ARRAY and DATA_TYPE_BUFFER_OR_NULL (an optional buffer, where null means “not supplied” rather than “wrong type”). 30 more Node wrappers now reject a bad call by throwing a Napi::TypeError instead of returning undefined silently, finishing what #1509’s ruling started: 325 of the addon’s 348 wrappers use the shared checker, where 200 did before #1509 and 295 after it. Each converted wrapper’s type list is a transcription of the hand-rolled check it replaced, so what the wrapper accepts is unchanged.
  • rb napi-20260909-2 (#1600): ci_scripts/node-adapter-coverage.sh --self-test grows from 2 corpora to 15, and a 20-mutation battery against the script finds no survivors. Most of the new corpora exist for the parsers rather than the comparisons — GATE_AWK’s depth counter and its #else/#elif rule, and each header’s ^[[:space:]]* anchor and comment stripper. None of those had coverage from any direction, because the real interface.cc holds a single unnested #if/#endif pair with no #else and no registration after it, and neither header has a commented-out or a mid-line declaration; the corpora stage shapes the tree does not contain. This supersedes the note in napi-20260909-1 that the self-test covered two of the script’s eight checks.
  • rb napi-20260909-2 (#1600): the gate’s OK line carries each header parser’s own declaration total. Every other number on it is an intersection or a difference, so a parser that under-matched — a narrowed anchor, a stripper eating too much — only shrank a set that was subtracted and no count moved; the test-tool parser could be blinded to 31 of its 178 declarations with every corpus green.
  • sprint-198 (#895): BACnetStack_GetBDTEntry returns uint32_t (the number of bytes written to ipAddress, 0 on failure) instead of bool, adopting the same out-buffer convention BACnetStack_GetInfo already uses. ABI break.
  • sprint-198 (#896): BACnetStack_GetFDTEntry — same change as #895 (returns bytes written to ipAddress, 0 on failure). Also documents timeToLive/timeRemaining as seconds. ABI break.
  • sprint-198 (#897): BACnetStack_ReadTrendLogRecord returns uint32_t (bytes written to datumAsString, matching datumLength, 0 on failure) instead of bool. The datumType doc now inlines the format table directly (0=Log Status..9=Time Change) instead of pointing at BACnetStack_InsertTrendLogRecord, which is test-tool-only and unreachable from this header. ABI break.
  • sprint-198 (#898): BACnetStack_ReadTrendLogMultipleRecord — same return-shape change as #897 (bytes written to dataAsString, matching dataLength, 0 on failure). ABI break.
  • sprint-198 (#902): BACnetStack_SendGetEventInformation, BACnetStack_SendAcknowledgeAlarmDateTime/Unsigned/Time, and BACnetStack_SendSubscribeCOVPropertyMultiple move from int (the sent invoke-id, or -1 on failure) to bool + a new leading uint8_t* sentInvokeId out-parameter, matching every other confirmed-send export in the header (e.g. BACnetStack_SendSubscribeCOV) — the old shape could not represent invoke-id 255 distinctly from an error. ABI break.
  • rb tool-20260910-1 (#1641, #963): AddScheduleExceptionEventWithCalendarReference (IFC-032) gets its napi twin and a typed api.ts function. The wrapper is bespoke, like its WithCalendarEntry sibling, and returns { success, exceptionIndex }. With it, node-adapter-parity.sh has no unlisted divergence left, so the whole deferred Node gate family passes with the deferral lifted locally.
  • rb tool-20260910-1 (#1641, #822/#556): the napi exports RegisterCallbackReceiveMessage/RegisterCallbackSendMessage are renamed ...ForPort to match the C exports, so generate-node-adapter.js keeps them on the named ESM surface. The 8 RegisterHook* trampolines and the audit/event notification receivers take uint32 networkPortInstance (IFC-028).
  • rb tool-20260910-1 (#1641): 57 GENERATED napi wrappers regenerated from today’s headers. generate-napi-wrappers.js --write refuses these by design, because their behaviour signature changed along with the header. 12 bespoke test-tool wrappers aligned by hand:
    • SendUnconfirmedCOVNotification and ...Multiple: IFC-009 parameter order, IFC-016 widths, and broadcast is now a required argument after the port.
    • The 6 Add/RemoveListElement variants and the 3 SendConfirmedEventNotification forms: IFC-007/008 leading sentInvokeId and a bool return.
    • SendAuditLogQuery: the full header shape, 24 arguments and a bool return instead of {success, invokeId}.
  • rb tool-20260910-1 (#1641): api.ts follows the rebuilt napi layer:
    • networkPortInstance on every Send*, hook and message callback. DecodeAs keeps its packet transport type, and AddNetworkPortObject keeps its BACnetNetworkType.
    • Typed GetProperty* callbacks take their errorCode buffer.
    • The spellings hundredth and acknowledgment.
    • The 22 reshaped wrappers above. adapter-exports/adapter-manifest/index are regenerated (generate-node-adapter.js --check clean). node-adapter-selftest.mjs’s event-notification control uses the IFC-008 shape. All 8 legs OK, and generate-napi-wrappers.js matches 197/266. This is not WP-9: deferred-gates.txt is untouched.
  • rb tool-20260910-1 (#1641, #479): the Node addon binds the stack-owned Timer engine. Before this a Node host could not create a Timer engine at all. New napi exports and typed api.ts functions: AddTimerObject, SetTimerStateChangeValueNoValue, SetTimerStateChangeValue, AddTimerObjectPropertyReference and SetTimerPriorityForWriting. Every Timer duration is milliseconds. SetTimerStateChangeValue takes the (valueDatatype, valueAsUnsigned, valueAsReal) triple, not a packed buffer.
  • rb tool-20260910-1 (#1641, #480/#481): the two unbound lighting engine constructors are bound too, AddLightingOutputObject and AddBinaryLightingOutputObject. Every test-tool engine constructor now has a napi twin.
  • rb tool-20260910-1 (#1641, #476): RegisterCallbackProgramChangeRequested binds the Program_Change host seam (cl. 12.22.5). The JS callback returns true to settle the request later through SetProgramState. false, a non-boolean, or null (unregister) keeps the stack’s synchronous auto-complete.
  • rb 1464-addon-compile (#1464): ci_scripts/addon-compile.sh — -fsyntax-only over the six napi translation units in both binding.gyp configurations (BACNET_STACK_TESTTOOL and CAS_BACNET_PRODUCTION_BUILD), 12 compiles in a few seconds, no link and no node-gyp. Wired into pre-push.sh. Skips with exit 0 when node-addon-api or the node headers are absent, so it does not block a developer who has not run npm install. node-gyp clears build/ between configurations, so only one is ever built at a time and a production-only break was previously invisible whenever the test-tool build was the one checked.
  • rb 1487-generate (#1487): ci_scripts/generate-napi-wrappers.js — generates a napi wrapper from the typed model plus the parameter-role table, and proves it by comparing against the hand-written one rather than by replacing it. Both sides are reduced to a behaviour signature — which info[] slot each call argument comes from, the argument order, the napi kind each slot is read as, and which slot is bounds-checked against which — and compared. 111 of 145 non-callback wrappers (77%) reproduce the hand-written behaviour exactly. Nothing is replaced yet; a wrapper whose signature matches is one generation can take over safely, and a wrapper that differs is adjudicated rather than resolved in the generator’s favour, because #1464 established the hand-written side is not automatically right.
  • rb 1487-generate (#1487): the safety-critical direction is clean — there is not one export where the generator emits a bounds check the hand-written wrapper lacks. All 34 remaining differences are the wrapper doing more than the model plus roles describes (a loop, a std::vector build, a conditional argument, a std::string construction), never less. That is the result that matters: whatever else the napi layer is doing, it is not missing a check the role table says it needs.
  • rb 1487-napi-roles (#1487): ci_scripts/adapter-roles.json and ci_scripts/adapter-roles-extract.js — the per-parameter roles a generated napi layer needs and the C headers cannot state: which length parameter governs which buffer, which pointer is an output, which parameter is a BACnet date field. 254 role entries across 78 exports, recovered from the hand-written wrappers rather than authored: every ValidateBufferLength(info, "connectionString", connectionString.Length(), connectionStringLength) call site is the pairing, already shipped and reviewed. Coverage is 129 of 141 (91%) of the byte/char pointer parameters on exports the addon actually binds; the remaining 12, plus 4 void* buffers whose roles were refused (see below), are recognisable safe patterns or need a ruling; they (a derived length from std::string::size(), a use*-guarded optional buffer, a scalar &sentInvokeId out-parameter) that need naming as roles rather than new decisions.
  • rb 1487-napi-roles (#1487): the extraction maps through each wrapper’s actual call site — arguments matched positionally against the model’s parameters — rather than by naming convention, because the convention does not hold: allowedScopeBits is governed by scopeBitCount, one site passes (size_t)length, and SendWhoHasWithObjectName validates a local named objectNameBuffer before passing a std::string built from it. An earlier by-name probe reported 94% coverage; the honest per-export figure is 91%, and the difference was the probe treating a parameter name as meaning the same thing in every export.
  • rb 1487-napi-roles (#1487): the extractor refuses a buffer role that does not land on a pointer. Four entries had attributed a buffer role to the LENGTH parameter beside it, one of them (SendWriteProperty.propertyArraysLength) naming itself as its own length. A role table asserting a scalar is a buffer would generate a bounds check on the wrong argument, which is worse than generating none — so it is refused and reported, not recorded. Found by an independent verifier written against the emitted JSON rather than against the extractor, so a bug in the extractor could not validate itself.
  • rb 1487-napi-roles (#1487): honest remaining count — 16 parameters need a ruling, not 12: the 12 byte/char patterns plus 4 void* buffers (propertyArrays x3, changeList) whose roles were the ones refused above. 6 void* buffer parameters exist; 2 have roles.
  • rb 1487-next (#1487): ci_scripts/adapter-roles-manual.json — the 10 parameter roles the wrappers do not state, each with the evidence for its ruling, kept in a separate file from the 264 recovered ones so a reader can always tell a recovered role from a decided one. All 10 are shapes that need no caller-supplied length to check and where emitting a bounds check would be wrong, not merely unnecessary: derived-length (the wrapper computes the length from the JS value — modelName.c_str(), (uint32_t)modelName.size()), guarded-optional (useDeviceMACAddress ? deviceMACAddress.Data() : NULL, pointer and length under one guard), and scalar-out (&sentInvokeId, a pointer to one scalar, not a buffer). A manual entry never overwrites a recovered one — if the wrappers say something, the wrappers win.
  • rb 1487-next (#1487): ci_scripts/adapter-roles-verify.js, wired into pre-push.sh. It checks the emitted table independently of the extractor that produced it, so a bug in the extractor cannot validate itself — which is exactly how four entries recording a buffer role on the length parameter were caught. Revert-red: pointing one lengthParameter at a name that is not a parameter of that export makes it exit 1 naming the entry.
  • rb 1487-swap (#1487): ci_scripts/napi-wrapper-notes.json — per-export prose, emitted above the wrapper it belongs to. Replacing a hand-written wrapper destroys its comments, and 228 comment lines in these files were not boilerplate: why GetFDTEntry‘s required extent is 1 and not 2, that AddDeviceToVirtualNetwork used to read the device instance into network as well, what BACnetInterface::GetBDTEntry does with a declared length. Ten exports’ worth was recovered into this file, and the per-check-kind rationale (#392, #391, #447, #406 — the same paragraph repeated 69 times by hand for #392 alone) moved into the generator, which owns it. Verified: zero substantive comment lines present before the swap are absent after it.
  • rb 894-adapter-generation (#894): ci_scripts/adapter-model.js — one strict, typed model of the C export surface, extracted from source/CASBACnetStackDLL.h, source/CASBACnetStackTestToolDLL.h, source/BACnetCallbackInterface.h and source/version.h. It holds, per export, the return type, the ordered parameters with their C types and const/pointer qualifiers, the declaring header (which is what decides production vs test-tool surface — 19 BACnetStack_* symbols are declared in the test-tool header), the section banner, and the Doxygen block; 408 exports, 2,665 parameters, 86 callback typedefs. It asserts that re-rendering every parsed declaration reproduces the header text byte for byte, which is a stronger guarantee than the name-and-opaque-string parses it replaces could make.
  • rb 894-adapter-generation (#894): a new adapter model (self-test) gate in ci_scripts/pre-push.sh — a fixture corpus that pins the parser against shapes the headers do not contain today, including #1019 (a parameter added) and #1464 (a return type changed) as type comparisons. Revert-red: without the qualifier-is-not-a-type guard the corpus reports ACCEPTED a parameter it must reject: "const uint32_t" and exits 2.
  • rb 894-adapter-generation (#894): four new adapter generators — ci_scripts/generate-csharp-adapter.js, generate-java-adapter.js, generate-python-adapter.js and generate-rust-adapter.js — all reading the same typed model as the C++ and Node generators, each with a --check drift gate wired into pre-push.sh. C# and Java bindings now cover all 229 customer exports (they covered 110 and 135, plus 19 and 20 bindings to symbols the library does not export); Python goes from 0 exports bound to 229; Rust from 17 to 229. Every export’s Doxygen block is carried into the target’s own idiom — C# XML doc, Javadoc, Google-style Python docstrings, rustdoc — including the per-parameter callback documentation, which lives in BACnetCallbackInterface.h rather than on the registration export.
  • rb 894-adapter-generation (#894): ci_scripts/adapter-emit.js, the shared write-or---check step, so what --check means is one definition rather than six.
  • rb 894-adapter-generation (#894), from code review: all four new generators now assert that two function-pointer parameters normalising to the same callback type name declare the same signature. The map was first-wins, so a future header carrying both void(*CallbackFoo)(uint32_t) and bool(*FPCallbackFoo)(uint32_t, uint8_t) would have bound the second registration with the first’s type - #584’s failure mode reintroduced through the back door, in the change whose purpose is to make it impossible. Revert-red: the guard fires on a synthetic one-parameter-short clash in all four generators and stays silent when the signatures agree.
  • rb 894-adapter-generation (#894), from code review: hand-adapter-coverage.sh’s variadic extract() now fails on an empty file list. The single-path form could not silently pass; the rewrite could, and for Python/Go/Rust - which have no report_missing - it would have exited 0 reporting “references 0”.
  • rb a-20260907-24 (#1249, CR-F19-002): dedicated Decode() round-trip test coverage for the 6 out-of-range BACnetEventParameter* wire classes (OutOfRange, DoubleOutOfRange, SignedOutOfRange, UnsignedOutOfRange, UnsignedRange, FloatingLimit) — previously exercised only indirectly (accessors and freshly-built Encode(), never Decode()), with three of the six appearing in no test file at all.
  • rb a-20260907-25 (#1249, CR-F19-003): dedicated Decode() round-trip test coverage for the 7 change-of-value BACnetEventParameter* wire classes (ChangeOfBitstring, ChangeOfCharacterstring, ChangeOfDiscreteValue, ChangeOfState, ChangeOfStatusFlags, ChangeOfValue, ChangeOfValueCovCriteria) — previously exercised only indirectly (Encode() on empty-list objects and equality checks), never Decode(), one dedicated test file per class.
  • rb a-20260907-26 (#1249, CR-F19-004): dedicated test coverage for the 5 specialized BACnetEventParameter* wire classes (ChangeOfTimer, ChangeOfLifeSafety, CommandFailure, AccessEvent, BufferReady) — CommandFailure previously had zero test-suite presence of any kind; the other four were exercised only indirectly (Encode() on freshly-built objects), never Decode().
  • rb a-20260907-28 (#1249, CR-F19-006): dedicated test coverage for BACnetEventParameterNone, calling its Decode()/Encode() directly rather than through the BACnetEventParameter wrapper (which special-cases none [20] inline and never reaches this class) — pins the class’s own ambiguous 0-return-means-both-success-and-failure shape as documented behavior, not a change.
  • rb a-20260907-30 (#1248, CR-F18-012): dedicated test coverage for BACnetFaultParameterFaultExtended, BACnetFaultParameterFaultLifeSafety, and BACnetFaultParameterNone — none of the three had a dedicated test file before this batch. FaultExtended’s test mirrors the sibling TestBACnetEventParameterExtended.cpp (the two productions share the same element type), including a hand-encoded wire-byte pair rather than relying only on Encode-then-Decode self-consistency. FaultParameterNone’s test calls Decode/Encode directly, bypassing the BACnetFaultParameter wrapper, giving CR-F18-011’s documented dead code its direct wire-level coverage.
  • rb a-20260907-32 (#1248, CR-F18-013): dedicated test coverage for BACnetFaultParameterFaultOutOfRange, BACnetFaultParameterFaultOutOfRangeValue, and BACnetFaultParameterFaultListed — the min/max-CHOICE-arm selection logic backing CR-F18-002/006 previously had zero direct unit-test coverage. FaultOutOfRangeValue’s own 4-way raw-CHOICE dispatch (real/unsigned/double/integer) mirrors the proven TestBACnetEventParameterChangeOfValueCovCriteria.cpp pattern from the F19 sibling family.
  • rb a-20260907-33 (#1248, CR-F18-010): dedicated test coverage for BACnetFaultParameterFaultCharacterstring, BACnetFaultParameterFaultState, and BACnetFaultParameterFaultStatusFlags. FaultCharacterstring/FaultState get hand-encoded empty-list wire-byte tests (Encode/Decode independently verified against a hand-derived byte sequence, not just self-consistency); FaultStatusFlags (a mandatory nested SEQUENCE with no legal empty form) uses the standard round-trip pattern.
  • rb a-20260907-36 (#1248, CR-F18-008 partial): dedicated test coverage for BACnetFaultType (the enum wrapper) and BACnetFaultParameterBase (whose only testable surface is its static 8-way factory dispatch, since every other member is pure virtual). BACnetFaultAlgorithm, the third and largest shared base class named by this finding, is deferred to a separate future batch given its size.
  • rb a-20260907-38 (#1248, CR-F18-008, completing the a-20260907-36 partial): dedicated test coverage for BACnetFaultAlgorithm’s own base-class behavior — the accessor pair, the refusal/no-op stubs a concrete subclass overrides, the genuinely shared SetStatusFlags/ChangeReliability logic, and the RegisterFaultAlgorithm/FaultAlgorithmFactoryByFaultType registry. FaultAlgorithmFactoryByObjectType, IsFaultTypeSupportedForObjectType, and IsObjectPropertyMonitoredByIntrinsicFault (large object-type mapping tables) remain deferred to a separate future batch.
  • rb a-20260907-39 (#1247, CR-F17-018): dedicated test coverage for BACnetEventAlgorithm, the shared base class every concrete event algorithm inherits from — the full (newState, m_previousState) ChangeState transition matrix, Reset(), SetCurrentState, the base GetProperty/SetProperty refusal behavior, and the other no-op stubs/accessors, per the finding’s own recommendation. Deliberately does not re-test the RegisterEventAlgorithm/EventAlgorithmFactoryByEventType registry (already covered by tests/TestBACnetEventAlgorithmRegistry.cpp) or the static object-type-mapping helpers (already covered by tests/TestBACnetChangeOfStateEventAlgorithm.cpp and tests/TestBACnetStackEventEnrollmentObject.cpp).
  • rb a-20260907-42 (#1434): representative-per-group parameterized test coverage for BACnetFaultAlgorithm’s 4 static object-type mapping helpers (FaultAlgorithmFactoryByObjectType, IsFaultTypeSupportedForObjectType, IsObjectPropertyMonitoredByIntrinsicFault, GetPropertyMonitoredByIntrinsicFaultForObject), deferred from CR-F18-008 (a-20260907-36/-38). Covers one representative BACnetObjectType per meaningful group (6 of the 7 non-none fault types reachable through this object-type mapping — faultExtended has no BACnetObjectType mapped to it at all, so no case for it exists here — plus the “uses a fault algorithm but detects none intrinsically” group, the “rejects reliability-evaluation entirely” group, and an invalid/unrecognized objectType) rather than all ~50 individual cases, per the issue’s own recommendation. The property-mapping test checks IsObjectPropertyMonitoredByIntrinsicFault and GetPropertyMonitoredByIntrinsicFaultForObject together per group specifically because they must agree by construction — the exact invariant the lift/escalator bug above violated.
  • rb a-20260907-45 (#1247, CR-F17-019): extends tests/TestBACnetStackAlarmAndEventObject.cpp (which already existed from an earlier batch) with the coverage the finding asks for that it didn’t yet have — HandleToFault (mirroring the file’s own existing Event_Enable-false/true pair for HandleToOffNormal), the remaining HandleToNormal variants (FromFault, invalid-stateChange), Reset(), and CheckAndRunAlgorithms (parameter validation, missing-GetSystemTime-callback, Event_Detection_Enable-false forcing both algorithms to their baseline unconditionally, one representative full pass through each of the fault and event dispatch paths using BACnetLifeSafetyFaultAlgorithm/BACnetChangeOfStateEventAlgorithm as minimal concrete algorithms, and both inhibit-flag gates). Test-only.
  • rb a-20260909-116 (#1339): added the missing discriminating test for #1073’s uint8_t requiredSegments wraparound in BACnetRespondingStateMachine::RunAwaitResponseState - the existing test in the same file for the related-but-different #922 remainder-divisor bug doesn’t use large enough numbers to trigger the wraparound specifically. The new test constructs an 11520-byte synthetic APDU (a raw buffer, content-agnostic - only its length matters) at the minimum negotiated max-APDU-length, giving a correctly-computed requiredSegments of exactly 256 - a value the pre-#1073 uint8_t-typed local would have truncated to 0. Revert-red verified: temporarily narrowing the (already-fixed) local back to uint8_t reproduces the wraparound and the new test goes red.
  • Review-fix: the test’s maxSegmentsTransmittable was initially copy-pasted from a sibling test as a nonzero value (10), which independently satisfied the abort condition’s other comparison and confounded the isolation the test’s name/comments claim. Corrected to 0 (unspecified/no cap), so only the intended maxSegmentsAcceptedFromPeer(255) boundary drives the result.
  • rb a-20260914-76 (#1859, PlugFest P0 part A of #872): characterization sweep comparing the generic production BACnetStack_AddObject creation path against each type’s test-tool typed constructor, for 8 access-control/life-safety object types (Access Credential, Access Door, Access Point, Access Rights, Access Zone, Credential Data Input, Life Safety Point, Life Safety Zone). Test-only - no source/ changes.
    • Adds a reusable comparator (tests/Helper_Objects/P0CreationPathComparator.h/.cpp) comparing enabled-property presence (BACnetDBDevice::GetPropertyList/IsPropertyEnabled) and value (BACnetBusinessLogic::GetProperty + BACnetAbstractSyntaxType::IsEqual) between the two arms - a deliberate implementation deviation from the issue’s own suggested wire-loopback method, disclosed in the PR, since both approaches observe the same thing without needing a second wire-framing harness.
    • Finding: contra the parent issue’s own prior prediction (#893: “the expected result is equivalent”), none of the 8 types are equivalent. BACnetStack_AddObject never allocates the underlying C++ engine object (e.g. BACnetStackAccessDoor) that only the typed constructor creates, so a generically-created object of any of these 8 types has no engine to serve its own required properties - they fall through to an unregistered host-callback path and read as errors/absent. Access Door’s own behavior probe confirms the practical consequence: a generic Access Door cannot even be commanded (Present_Value write fails outright).
    • Two mandatory controls (null: same-arm-twice reports zero delta; sensitivity: a single perturbed property is reported as exactly that property) both pass, both independently verified to fail when their premise is broken.
    • Evidence and the full 8-row delta table: docs/reviews/2026-09-p0-generic-vs-typed.md (also documents two non-blocking comparator limitations an independent reviewer pass found, for #1860/#1861’s future reuse - neither affects these 8 results).
    • Verified: 10 new tests (8 per-type + 2 controls), all green. make NO_TESTTOOL=1 confirmed to still build (the new test file compiles cleanly with BACNET_STACK_TESTTOOL undefined, isolated via direct single-file compilation given an unrelated pre-existing NO_TESTTOOL build gap elsewhere in the suite). Full ci_scripts/pre-push.sh: 8454/8454 tests passed. Independent reviewer pass: all 8 scrutiny points addressed, load-bearing “not equivalent” finding independently re-derived from source and confirmed genuine (not a test-fixture artifact).
  • rb a-20260914-78 (#1860, PlugFest P0 part B of #872): characterization sweep continuing #1859’s work, comparing generic BACnetStack_AddObject against each type’s test-tool typed constructor for 6 more object types (Lighting Output, Binary Lighting Output, Timer, Program, Load Control, Averaging). Test-only - no source/ changes; reuses #1859’s P0CreationPathComparator unchanged, per the issue’s own instruction not to fork it.
    • Finding: none of these 6 types are equivalent either - same root cause as #1859’s own 8 types (AddObject never allocates the underlying engine object). Notably, this directly contradicts #893’s more specific, narrower prediction that Program and Load Control specifically WOULD be equivalent (their typed constructors take no extra configuration arguments, unlike the other 4 types in this family) - the root cause applies identically regardless of whether the typed constructor takes config arguments or not. Across both #1859 and #1860, 14 of 14 measured types are non-equivalent.
    • Both mandatory controls (null, sensitivity) re-proven against a type from this family (Timer), demonstrating the harness generalizes beyond the Access/LifeSafety family it was originally built against.
    • Two fixture gaps caught and fixed during development: Timer’s WriteProperty path needs CallbackGetSystemTime registered (to stamp its own internal state), and Timer’s priorityForWriting is an optional property AddTimerObject doesn’t auto-enable, so the sensitivity control needed an explicit SetPropertyEnabled call before perturbing it.
    • 6 new rows and their evidence appended to docs/reviews/2026-09-p0-generic-vs-typed.md.
    • Verified: 8 new tests (6 per-type + 2 controls), all green; 18 total across both P0 test files. make NO_TESTTOOL=1 confirmed for the new file (isolated single-file compile). Full ci_scripts/pre-push.sh: 8462/8462 tests passed. Independent reviewer pass: all 8 scrutiny points confirmed, including independently re-deriving the Program/Load Control root cause from source (both types’ engine-object maps are populated only by their typed constructors, never by generic AddObject) - not a fixture artifact.
  • rb a-20260914-80 (#1861, PlugFest P0 part C of #872, FINAL child): characterization sweep completing #1859/#1860’s work, comparing generic BACnetStack_AddObject against each type’s test-tool typed constructor for the event/reporting family - Event Enrollment (5 typed variants), Alert Enrollment, Notification Forwarder, Audit Log, Notification Log. Test-only - no source/ changes; reuses #1859’s P0CreationPathComparator unchanged.
    • Disclosed scope decision: the issue’s own method section asks for wire-level behavior probes (event notification leaving on loopback, notification forwarding to a recipient, an audit record appending). Building real notification dispatch/loopback capture for 4 mechanisms was judged substantially bigger and more novel than #1859/#1860’s WriteProperty-shaped probes. Scoped down to engine-presence checks (generic arm’s accessor returns NULL, typed arm’s doesn’t) plus, for Event Enrollment specifically, a real CheckAndRunAlgorithms trigger showing the algorithm genuinely evaluates and transitions Event_State - disclosed on the coordination board and in the test file’s own header, with a reviewer-prompted follow-up softening the “evidence-equivalent” framing to avoid overstating what an engine-presence check proves versus true wire-level behavioral evidence.
    • Finding: 22 of 23 types/variants measured across all 3 P0 children show a real delta. Audit Log is the sweep’s one exception - its property-level comparison reports equivalent, though the underlying engine-object gap is confirmed just as real as every other type (GetAuditLog on the generic arm still returns NULL). Traced the precise cause: ReadProperty over Audit Log’s own properties isn’t wired to the engine at all yet (per BACnetDBDevice.cpp’s own Sprint 108 Phase 7 comment - cl. 13.13.6’s AuditLogQueryProcessor is the real read path, not ReadProperty), so neither arm’s property reads exercise the engine, making the gap invisible to a property-comparison-only sweep.
    • Doc now carries a full closing summary across all 23 types/variants measured across the entire 3-part PlugFest P0 sweep - equivalent=1, genericMissingProperties=10, genericValueMismatch=12, genericRejected=0 - the evidence #872’s parent rulings were waiting on.
    • Verified: 11 new tests (5 Event Enrollment variants + Alert Enrollment + Notification Forwarder + Audit Log + Notification Log + 2 controls), all green; 29 total across all 3 P0 test files. make NO_TESTTOOL=1 confirmed for the new file. Full ci_scripts/pre-push.sh: 8473/8473 tests passed. Independent reviewer pass: confirmed the Audit Log exception is genuine (not a test bug) and independently traced its precise root cause; flagged the probe-scope framing as overstated, fixed in the same batch.
  • rb a-20260914-82 (#1857, part D of #869): applicability finding for rev 26 addenda cq (Annex Z JSON “short form” for Array/List/SequenceOf, plus the primitive short form) and cs (Annex AA.2 Certificate Authority requirements interchange zip file format). Documentation only - no source change.
    • Verdict for both: not applicable to this stack. cq needs a JSON-representation producer/consumer role (BACnet/WS, CSML) the stack doesn’t have - the only JSON-related code found is JSONRenderer.h/.cpp, a debug decode-tree pretty-printer (mirrors XMLRenderer), not an Annex Z wire-representation implementation. cs needs a CA-tooling/multi-device-CSR-bundling role - no zip file format code exists anywhere; the only CSR-related code is the pre-existing, narrower generateCsrFile Network_Port_Command (single local CSR generation, unrelated to AA.2’s own bundling format).
    • Method validated with a control: the same grep-based search correctly finds the real AA.1 TimeSeries implementation, confirming the method surfaces genuine implementations rather than failing to find anything by construction.
    • Finding recorded in _spec/spec-coverage/00-index.md; owner confirmation of the “not applicable” verdict requested on the issue before #1858 flips BACNET_PROTOCOL_REVISION to 26.
    • #1856 (part C, the ck ASN.1-codec-coverage audit) deliberately NOT claimed this batch - clause 21 has 200+ productions (204 in 21.6.txt alone), materially bigger than #1857’s scope-finding shape; deferred to its own dedicated batch.
  • rb a-20260914-84 (#1864, part of #1312): bind 6 existing C exports into the Node addon (adapters/node/@chipkin/cas-bacnet-stack/src/), all generated wrappers (matched byte-for-byte against ci_scripts/adapter-model.js + ci_scripts/adapter-roles.json via node ci_scripts/generate-napi-wrappers.js --write - no bespoke-allowlist additions needed):
    • BACnetStack_SendAcknowledgeAlarmUnsigned / BACnetStack_SendAcknowledgeAlarmTime (source/CASBACnetStackDLL.h) - the sequence-number [1] Unsigned and time [0] Time BACnetTimeStamp CHOICE-arm siblings of the already-bound SendAcknowledgeAlarm (datetime [2] arm), per #960. Exposed 1:1 with their C signature (sentInvokeId as an explicit out-Buffer, boolean return) rather than the older SendAcknowledgeAlarm/SendAcknowledgeAlarmWithTimeOfAck convention of translating the invoke-id back into a JS number.
    • BACnetStack_SendReadPropertyWithLength (source/CASBACnetStackDLL.h) - the #992 sibling of SendReadProperty, now identical in signature to it since #1594 reopened SendReadProperty’s ABI freeze; both continue to exist side by side.
    • BACnetStack_SetSegmentationEnabled (source/CASBACnetStackDLL.h).
    • BACnetStackTestTool_SendConfirmedEventNotificationWithOutOfRangeValues / BACnetStackTestTool_SendUnconfirmedEventNotificationWithOutOfRangeValues (source/CASBACnetStackTestToolDLL.h) - the outOfRange [5] eventValues sibling (issue #506) of the already-bound changeOfState [1] ...WithEventValues pair; registered inside the BACNET_STACK_TESTTOOL gate alongside their siblings.
    • BACnetStackTestTool_SetInvokeIdSeed is intentionally NOT bound: deprecated per #442, superseded by the already-bound RegisterCallbackInvokeIdGenerator (#84), which supersedes a seeded counter with per-request invoke-id control. Noted in api.ts’s RegisterCallbackInvokeIdGenerator doc comment.
    • ci_scripts/adapter-roles.json gained 6 new entries (input-buffer / output-buffer / bacnet-date-field / packed-element-buffer roles) modeled on the closest existing sibling for each export.
    • Pre-existing #1641 gate-skip state confirmed unchanged: generate-napi-wrappers.js --gate, adapter-roles-verify.js, check-node-api-returns.js, node-adapter-argument-order.js and node-adapter-coverage.sh all report SKIPPED - deferred per #1641 (removed once the hand-written Node addon is regenerated against the final interface, WP-9) rather than running; addon-compile.sh likewise reports the same deferred skip, so no native build was attempted this batch. generate-node-adapter.js --check (not deferred) passes after regenerating adapter-manifest.ts / adapter-version.ts / adapter-exports.ts / index.ts.
  • rb a-20260914-86 (#1862, part A of #1312): bind 5 existing test-tool-only C exports into the Node addon (adapters/node/@chipkin/cas-bacnet-stack/src/), all generated wrappers (matched byte-for-byte against ci_scripts/adapter-model.js + ci_scripts/adapter-roles.json, derived via node ci_scripts/generate-napi-wrappers.js --emit - no bespoke-allowlist additions needed):
    • BACnetStackTestTool_SendIHave (source/CASBACnetStackTestToolDLL.h) - the I-Have unconfirmed request (issue #93), the ANSWER half of the Who-Has / I-Have pair. objectName is MANDATORY per cl. 16.9 (the I-Have-Request SEQUENCE has no OPTIONAL members for it) - bound as a plain input-buffer, matching the codebase’s existing convention that the native side, not the JS binding, rejects a NULL/zero-length mandatory buffer; no bespoke mandatory-buffer check exists elsewhere in this addon to follow, so none was invented.
    • BACnetStackTestTool_SendUnknownNetworkLayerMessage (issue #94) - emits a network-layer message with a caller-chosen message type octet and opaque payload. messageType is deliberately NOT validated for meaning (unknown/reserved/vendor types are the point); messageData may be a zero-length buffer.
    • BACnetStackTestTool_SendConfirmedRequestRaw (issue #81) - a confirmed request with a caller-supplied service-choice octet and pre-encoded payload, routed through BACnetInterface::SendConfirmedRequest for invoke-id/segmentation/retry handling (hence sentInvokeId as an explicit out-Buffer, bound like the SendAcknowledgeAlarmUnsigned/SendAcknowledgeAlarmTime siblings from a-20260914-84). serviceChoice is deliberately NOT validated for meaning. The service-choice-3 (getAlarmSummary) payload-drop quirk is carried verbatim from the header into the api.ts doc comment.
    • BACnetStackTestTool_SendUnconfirmedPrivateTransfer / BACnetStackTestTool_SendConfirmedPrivateTransfer (Sprint 57 B8) - Private Transfer TX, unconfirmed (choice 4) and confirmed (choice 18, sentInvokeId out-Buffer). Guarded at the C level by #ifdef STACK_OPTION_PRIVATE_TRANSFER; confirmed against source/CASBACnetStackOptions.h that the option is enabled by default with no STACK_OPTION_DISABLE_PRIVATE_TRANSFER defined anywhere in this addon’s build, so both exports reach a BACNET_STACK_TESTTOOL addon normally with no extra guard needed in the Node adapter.
    • ci_scripts/adapter-roles.json gained 5 new entries (input-buffer / output-buffer roles only - none of the 5 has a date field or packed-element buffer).
    • api.ts gained two new small helpers, checkUint8Width/checkUint16Width, and each of the 5 wrappers throws RangeError for its narrowed-width (uint8_t/uint16_t) scalar parameters (serviceChoice, vendorId/vendorIdentifier, messageType, objectType, connectionStringLength, destinationAddressLength, destinationNetwork, messageDataLength) per this issue’s acceptance criteria. No prior wrapper in api.ts range-checked a narrowed-width scalar on the JS side (the napi layer’s Validate* calls only cover buffer lengths and BACnet date octets), so this is a new, narrowly-scoped addition rather than an existing convention being followed; uint32 parameters (deviceObjectInstance, objectInstance, networkPortInstance, serviceNumber, and the *Length buffer-length parameters already covered by the napi layer) are left unchecked since no existing BACnet-instance-range convention exists elsewhere in api.ts to match.
    • Pre-existing #1641 gate-skip state confirmed unchanged: generate-napi-wrappers.js --gate, adapter-roles-verify.js, check-node-api-returns.js, node-adapter-argument-order.js and node-adapter-coverage.sh all report SKIPPED - deferred per #1641 rather than running; addon-compile.sh likewise reports the same deferred skip, so no native build was attempted this batch. generate-napi-wrappers.js --self-test and node-adapter-coverage.sh --self-test are both LIVE and pass (9/9 and 15/15 respectively). generate-node-adapter.js --check (not deferred) passes after regenerating adapter-manifest.ts / adapter-version.ts / adapter-exports.ts / index.ts.
  • rb a-20260914-88 (#1863, part B of #1312): bind 10 existing test-tool-only sends/builders + 3 hand-written receive-side callback hooks into the Node addon (adapters/node/@chipkin/cas-bacnet-stack/src/) - closing the gap where JavaScript could not see any router-control network message and Read-BDT / Read-FDT acks never reached the caller:
    • Network-layer (NPDU) router-control sends (ASHRAE 135-2024 cl. 6.4): BACnetStackTestTool_SendICouldBeRouterToNetwork (issue #95), SendRejectMessageToNetwork (#73), SendRouterBusyToNetwork (#78) and SendRouterAvailableToNetwork (#96). All are generated wrappers, matched byte-for-byte against ci_scripts/adapter-model.js + the pre-existing ci_scripts/adapter-roles.json entries (node ci_scripts/generate-napi-wrappers.js --emit) - no bespoke-allowlist additions needed. SendRouterBusyToNetwork/SendRouterAvailableToNetwork’s dnets/dnetCount (a uint16_t* array) is expressed as a plain input-buffer role like every other buffer/length pair - the generator infers the Napi::Buffer element type from the C parameter’s own pointer type, so a uint16_t* array needs no special role shape.
    • SendInitializeRoutingTable (cl. 6.4.7, issue’s own doc: “the flat boundary cannot carry a BACnetRoutingTableEntry”) - three PARALLEL ARRAYS (connectedDnets, portIds, portInfoLengths, each portsCount roled elements) plus one concatenated portInfo blob (roled against portInfoTotalLength). Also a plain generated wrapper - each parallel array’s role simply names its own length parameter, which the generator already supports.
    • RegisterHookInitializeRoutingTableAck (cl. 6.4.8, #363) - hand-written in interface_callbacks.cc following RegisterHookIHaveWrapped’s structure (the generator skips function-pointer exports). Carries the same parallel-array shape as the send above, plus connectionString/networkPortInstance/sourceNetwork/sourceAddress.
    • SendEstablishConnectionToNetwork (cl. 6.4.9, #98) / SendDisconnectConnectionToNetwork (cl. 6.4.10, #99) - plain generated wrappers.
    • BuildWriteBroadcastDistributionTable + SendWriteBroadcastDistributionTable (Annex J.4) and SendDeleteForeignDeviceTableEntry (Annex J.5) - plain generated wrappers. Per the header’s own doc, the two Send* exports have no destination-address/destinationNetwork pair - they always target the BBMD named directly by connectionString.
    • RegisterHookReadBroadcastDistributionTableAck and RegisterHookReadForeignDeviceTableAck (#804) - hand-written receive-side hooks, same convention as the routing-table Ack hook above.
    • All 3 hooks copy every buffer/array handed to the JS callback via CopyOptionalBuffer (Napi::Buffer<T>::Copy under the hood, or a zero-length Buffer for a NULL/0-length source) rather than aliasing the C hook’s pointers, which are only valid for the duration of the call - the issue’s own acceptance criterion.
    • api.ts gained 13 typed wrappers (10 sends/builders + 3 hooks), reusing checkUint8Width/checkUint16Width (added in a-20260914-86/#1862) to throw RangeError on every narrowed-width scalar (network numbers as uint16_t; performance index / reject reason / portsCount / termination time as uint8_t; every *Length parameter as uint8_t).
    • SendRouterBusyToNetwork’s api.ts wrapper deliberately omits destinationNetwork/destinationAddress/destinationAddressLength from its exposed signature (fixed to 0/empty/0 internally) even though the native C export still carries them - satisfying the issue’s “no unicast form” acceptance criterion at the TypeScript layer. Its sibling SendRouterAvailableToNetwork keeps them, per source/BACnetTestToolInterface.h’s own doc comment (“destinationNetwork / destinationAddress remain available… cl. 6.4.6 does not forbid routing this message”). No existing convention or test in this codebase statically asserts an api.ts wrapper’s PARAMETER SHAPE (as opposed to its return type, which check-node-api-returns.js already covers against the .cc source) - adapter.self-test.ts/property-arrays.self-test.ts both exercise runtime behaviour against a built addon. Added adapters/node/@chipkin/cas-bacnet-stack/src/router-busy-signature.self-test.ts, a minimal source-only shape check (Function.length + a .toString() scan against the imported api object - no LoadBACnetFunctions()/built addon needed, since an api.ts wrapper only calls getAddon() at call time) asserting SendRouterBusyToNetwork.length === 5 with no destinationAddress/destinationNetwork/broadcast in its source, plus a control that SendRouterAvailableToNetwork still has all 8 of its parameters. Verified live: npx tsc + node dist/router-busy-signature.self-test.js both pass. Wired into ci_scripts/node-adapter-selftest.mjs as leg 1b (alongside leg 1, since neither needs the addon); that script’s own remaining legs still require the full build, deferred per #1641.
    • Note on scope: the issue’s own section header says “the 11 sends/builders” but its own enumerated list (and its evidence table) names exactly 10 distinct native functions; all 10 are bound here, plus the 3 hooks, for 13 total - matching the evidence table’s 12 rows (one row bundles the accumulator + its send).
    • Pre-existing #1641 gate-skip state confirmed unchanged: generate-napi-wrappers.js --gate, adapter-roles-verify.js, check-node-api-returns.js, node-adapter-argument-order.js, node-adapter-coverage.sh and addon-compile.sh all report SKIPPED - deferred per #1641 rather than running, so no native build was attempted this batch. generate-napi-wrappers.js --self-test (9/9) and node-adapter-coverage.sh --self-test (15/15) are both LIVE and pass. generate-node-adapter.js --check (not deferred) passes after regenerating adapter-manifest.ts / adapter-version.ts / adapter-exports.ts / index.ts (368 -> 381 napi exports). npx tsc --noEmit over the whole adapter package also passes with no errors.
  • rb a-20260921-8 (#2012, part B of #2006’s client-side segmented-send decomposition): implemented 135-2024 cl. 5.4.4.1’s SendConfirmedSegmented transition and cl. 5.4.3’s FillWindow function, and corrected BACnetConfirmedRequestPDU::WouldRequireCannotSendAbort (#2011) to compute the real required-segments arithmetic instead of treating “the destination’s Max_Segments_Accepted is known” alone as sufficient for CannotSend. When an oversized outgoing Confirmed-Request’s APDU fits within the destination’s Max_Segments_Accepted once split into segments (or that property is not known at all), BACnetInterface::SendConfirmedRequest now sends the FIRST segment (sequence-number=0, more-follows=true, segmented-message=true) via the new reusable FillWindow, and records the SEGMENTED_REQUEST(client) transaction state (InitialSequenceNumber, ActualWindowSize, SegmentRetryCount, SegmentTimer, and the segments already sent) on SentConfirmedRequestValue, instead of unconditionally aborting.
  • This issue covers ONLY sending segment 0 of an outgoing segmented Confirmed-Request. It does NOT implement any part of the SEGMENTED_REQUEST(client) state’s own incoming-PDU handling (NewACK_Received, DuplicateACK_Received, FinalACK_Received, Timeout, FinalTimeout, AbortPDU_Received, SendAbort) - no SegmentACK is ever processed, and no further segments are ever sent by this change. That is #2013 (part C), which remains blocked until this lands.
  • rb b-20260907-10 (#1233 CR-F02c-009): new tests/TestBACnetObjectSelector.cpp - no test file existed for this class at all.
  • rb b-20260907-10 (#1233 CR-F02c-010): documented that IsValidPropertyIdentifier checks “within the implemented numeric range,” not true spec membership, and accepts reserved-but-unassigned gap values. Docs-only.
  • rb b-20260907-11 (#1233 CR-F02c-011): new tests/TestBACnetServicesSupported.cpp - no test file existed for this class before.
  • rb b-20260907-9 (#1233 CR-F02c-007): new tests/TestBACnetStackObjectSettings.cpp gives BACnetStackObjectSetting its first-ever test coverage.
  • rb b-20260907-9 (#1233 CR-F02c-008): added TryGetPropertyWritable/TryGetPropertySubscribable presence/absence coverage and a BACnetDBProperty::operator= self-assignment (#373 guard) test.
  • rb b-20260907-9 (#1233 CR-F02b-014, CR-F02c-012): documented BACnetDBProperty::SetProperty’s parameter-transposition risk and confirmed it has no cross-field validation of its own. Docs-only.
  • rb bs-20260922-1 (#2223): the <acceptUri>|client=<token> connection-string convention a BACnet/SC hub or direct-connect-accept host must use for each accepted WebSocket is documented on BACnetStack_RegisterCallbackSCStartListening (and referenced from the send/receive callbacks), and in the BACnet/SC manual 18.3.1, including which string goes in sourceConnectionString and destinationConnectionString. Pinned by DoesConfiguredUriMatch_AcceptedPeerConnectionStringContract.
  • rb bs-20260922-1 (#2228): send-callback delivery semantics (“accepted for transmission, not delivered”, no later failure channel, return 0 when the transmit queue is full, never block, report late socket failures with WebsocketStatus_Error/WebsocketStatus_Disconnected), in the header and the BACnet/SC manual 18.1.1. No new API.
  • rb bs-20260922-1 (#1119): the BACnet/SC manual (Appendix D.2) and a source comment state that hub-function forwarding is unconditional per 135-2024 cl. AB.5.1 (no rate limit; one peer’s broadcast is copied to every other connection) and that flood control is a deployment responsibility.
  • rb bs-20260922-1 (#2157): adapters/README.md “Contributing to the adapters”: adapter fixes from outside the team come in as issues (not pull requests), generated files are fixed in their generator, and the reporter verifies with their own toolchain.
  • rb bs-20260922-3 (#2280): the Java and Rust bindings carry the header’s public enums (BACnetDecodeType, BACnetPropertyIdentifier, BACnetStackSetting), generated from source/CASBACnetStackDLL.h. In Java they are int constants: ICASBACnetStackLibrary.BACnetPropertyIdentifier.presentValue. In Rust they are u32 constants: BACnetPropertyIdentifier::presentValue. check-adapter-enums.js now checks all five generated bindings. The C# enums from #1599 were compiled with dotnet build for the first time (0 warnings).
  • rb bs-20260922-5 (#2296): prebuilt N-API binaries for win32-x64 and linux-x64 (glibc) ship inside the package in prebuilds/, so on those platforms npm install needs no C++ toolchain. Measured: installing the packed package took 4 s on Linux and 10 s on Windows. The Linux binary is built on Debian bullseye with libstdc++ linked statically, so it needs only the host libc; the release job fails if it needs a glibc newer than 2.28 (Node 24’s minimum), and a local build of the recipe on 2026-09-22 needed GLIBC_2.17. The new install script (adapters/node/@chipkin/cas-bacnet-stack/install.cjs) test-loads the matching prebuilt binary. It falls back to a parallel source build when there is none, when it does not load, with --bacnet_testtool=0 (the prebuilt binaries are the default test-tool variant), or with --build_from_source=true. At runtime the addon is found with node-gyp-build (a local build/Release wins over prebuilds/), which replaces the bindings dependency. The package therefore also loads when install scripts are disabled.
  • rb c-20260908-1 (#1016): BACnetStack_InsertTrendLogRecord is a customer export again — declared in source/CASBACnetStackDLL.h and defined in CASBACnetStackDLL.cpp, alongside the BACnetStack_ReadTrendLogRecord whose own documentation calls its output “a string format that can be used to backup the record”. Until now that backup half shipped and the restore half did not, so a customer could export a Trend Log buffer and had no supported way to put it back. Additive: 229 → 230 exports, no existing signature changed, so nothing that links today is affected. The C++, C#, Java, Python and Rust adapters bind it automatically (regenerated); CASBACnetTime marshals as a fixed 64-bit in every one.
  • rb c-20260908-8 (#1593 / #123-B): source/CASBACnetStackDLL.h publishes enum BACnetPropertyIdentifier — all 522 BACnet property identifiers by name. Until now the public header carried no property-identifier enum at all, so every integration hard-coded 85 for Present_Value. Additive: no signature changes, no ABI break, and the header still compiles standalone as both C99 and C++17.
  • rb c-20260908-8 (#1593): the enum’s doc block states what the list is and is not — values outside it are legal and expected (cl. 23.1 reserves 512+ for proprietary identifiers); the list is complete rather than build-dependent, so naming an identifier does not mean the linked build implements it (56 are compiled conditionally by BACNET_PROTOCOL_REVISION); and the C++ ambiguity with the internal CASBACnetStack::BACnetPropertyIdentifier class is named, for anyone embedding the source with using namespace CASBACnetStack;.
  • rb d-20260908-1 (WP-0, #1641): ci_scripts/deferred-gates.txt and a shared helper (ci_scripts/lib/deferred-gate.{js,sh}) so the hand-written Node addon family (generate-napi-wrappers.js --gate, check-node-api-returns.js, node-adapter-argument-order.js, node-adapter-output-buffer-guards.js, addon-compile.sh, adapter-roles-verify.js, node-adapter-coverage.sh, node-adapter-parity.sh) reports SKIPPED, loudly, identically in CI and every local checkout, while the public interface moves under the interface session’s continuous packages. hand-adapter-coverage.sh (the Go gate) is not part of this family and stays live.
  • ci_scripts/check-ifc-entry.sh: fails a diff (against origin/issues/runbook) that changes one of the four tracked interface headers without adding a new docs/interface/*.md entry in the same diff — mechanizes “no entry, no merge” (docs/runbooks/runbook-interface.md rule 4). Wired into ci_scripts/pre-push.sh as gate 15.
  • rb d-20260910-57 (#963): new export BACnetStack_AddScheduleExceptionEventWithCalendarReference adds a Schedule Exception_Schedule special event whose period is a reference to a Calendar object (BACnetSpecialEvent’s calendar-reference [1] CHOICE arm), the counterpart to the existing BACnetStack_AddScheduleExceptionEventWithCalendarEntry. Validates the device, Schedule object and referenced Calendar object all exist. The referenced Calendar object’s Date_List is not yet resolved at evaluation time - the stack has no read path for it yet (tracked as #1758) - so an event added this way is stored but does not yet affect Schedule evaluation. See docs/interface/IFC-032.md.
  • rb e-20260915-13 (#1494): a new CI gate, ci_scripts/adapter-signature-gate.js, compares a hand-written adapter binding’s parameter list against ci_scripts/adapter-model.js’s typed header model for arity, per-parameter type, AND order - not just name or count, closing the gap that let #1019 (three missing C# parameters) and #1464 (a stale void extern against a bool export) both ship past every existing gate. Covers Go’s cgo trampolines (checked live against the header for the first time) and confirms the C# #region Async block’s four dead P/Invokes are still genuinely absent from the header.
  • rb e-20260915-21 (#2017): threaded BACnet/SC’s cl. 5.1.3 data_attributes (BVLL “Data Options”) from SC BVLL decode through BACnetDataLink::ProcessAPDU to BACnetStackReceivedSegmentedResponses::ProcessIncomingSegment (the client-side segmented-ComplexACK reassembly consumer), as new additive, default-NULL parameters with no behavior change to any existing call or data link. Non-SC data links (IPv4, MS/TP) are unaffected. No consistency check using this data yet (that is #2018’s scope) - this batch only makes the data arrive correctly and verifiably, via a new test-only GetDataOptionsForInProgress() accessor.
  • rb e-20260915-22 (#2018): implemented cl. 5.4’s NewSegmentReceived_InconsistentAttributes transition for the client-side segmented-ComplexACK receive path. A later segment carrying a “first segment” (every-segment=false) data_attribute, or whose “every segment” attributes disagree in presence/value with the first segment’s, aborts the reassembly: the partial accumulation is discarded, an outgoing Abort-PDU is sent to the peer (revision-guarded reason - inconsistentAttributes at rev >= 25, other on this build’s rev 24), and ABORT.indication is reported to the local application via the existing HookAbort callback. Mirrors #1845’s server-side shape and revision-guard pattern. Builds on #2017’s additive Data Options threading.
  • rb e-20260915-29 (#2037): BACnet/IP router ports can now bind to distinct BACnetDataLinkIPv4 instances by Network_Number, the same multi-instance router-port-binding #304 already gave BACnet/SC. Previously every BACnet/IP router port hardcoded binding to data-link instance 0 regardless of how many BACnet/IP interfaces (#556) or router ports existed, so a global broadcast fanned out once per BACnet/IP port rather than once per real network - correct only when there was in fact one interface. A router with two or more genuinely separate BACnet/IP interfaces now sends one copy per real interface; two router ports that collide on one interface (no matching Network Port object for one of them) are left unattributable in both directions rather than risking a duplicate frame on one wire. MS/TP is unaffected and remains single-instance - it has none of #556’s per-instance scaffolding, and extending it is a separate, larger effort.
  • rb e-20260916-45 (#2099): added ci_scripts/node-adapter-argument-count.js, a static gate comparing each RegisterCallback*’s api.ts declared handler parameter count against its matching *Wrapped lambda’s actual .Call({...}) argument count in interface_callbacks.cc. This bug class (a stale/extra declared parameter the addon never passes) was found and fixed three times this session (#2087, #2098) via ad-hoc review — this gate checks 54 of ~64 RegisterCallback* pairs and would have caught both automatically; the remaining 10 (a different declaration syntax, and a naming asymmetry between two functions) are outside its current reach, tracked separately as #2127. Wired into ci_scripts/pre-push.sh; deferred per #1641/WP-0 like the rest of the hand-written Node addon gate family, since the hand-written napi layer isn’t kept current while the interface still moves. CI wiring (.github/workflows/tests.yml) is left for session D, since .github/ is on the resolver’s own ban list.
  • rb e-20260916-6 (#1599): the Node adapter’s generator (ci_scripts/generate-node-adapter.js) now emits BACnetPropertyIdentifier (all 522 entries from source/CASBACnetStackDLL.h) as a new generated file, adapter-enums.ts, exported from the package entry alongside every other public name. Previously no generated language binding carried any public header enum at all - a Node customer had to hardcode property-identifier numbers (85 instead of BACnetPropertyIdentifier.presentValue). BACnetDecodeType and BACnetStackSetting (the two smaller public enums) already have a correct, complete hand-written copy in api.ts and are unaffected; reconciling those with a generated equivalent, and doing the same for C# and Python, remains open on #1599 as follow-up scope.
  • rb e-20260918-93 (#2158): a shared DecodeAs(JSON) fixture (an Unconfirmed-Request I-Am) documented in adapters/README.md, pinned by a new C++ test, as the oracle every future per-language adapter sample must reproduce byte-for-byte.
  • rb e-20260919-101 (#2155): MS/TP router-port binding now resolves per-instance in BindRouterPort, mirroring BACnet/SC (#304) and BACnet/IP (#2037) — ResolveMstpRouterPortBinding/ApplyMstpRouterPortBinding added alongside a FindMstpDataLinkByRouterPort lookup (not yet wired into SendRoutedPacket’s MS/TP case; tracked as follow-up #2214). Completes #2055 (parts A/#2153, B/#2154, C/#2155).
  • rb e-20260919-99 (#2154): MS/TP Network Port instance <-> data link tracking (ClaimMstpDataLinkForNetworkPort, FindMstpDataLinkByNetworkPort, FindMstpDataLinkByNetworkPortInstance, FindMstpDataLinkByNetworkNumber, and a secondary ambiguity-logging FindMstpDataLinkByMacAddress), mirroring BACnet/IP’s equivalent surface. Part B of #2055; prepares the ground for #2155 (router-port binding).
  • rb e-20260922-112 (#2013, part C of #2006’s client-side segmented-send decomposition): implemented 135-2024 cl. 5.4.4.2’s SEGMENTED_REQUEST(client) SegmentACK-driven transitions (DuplicateACK_Received, NewACK_Received, FinalACK_Received) and its Timeout/FinalTimeout SegmentTimer retry/abort logic. BACnetBusinessLogic.cpp’s client-role PDU dispatch now routes an incoming BACnet-SegmentACK-PDU with server=TRUE to a new BACnetSegmentAckProcessor::Process, instead of the prior unconditional return; - a SegmentACK now advances the SEGMENTED_REQUEST(client) transaction #2012 started (segment 0 already sent), sending the next segment via BACnetPacket::CreateSegment on NewACK_Received, or transitioning to AWAIT_CONFIRMATION (restarting the ordinary ApduTimeout via SentConfirmedRequestValue::ArmTimeout) on FinalACK_Received. BACnetStackSentConfirmedRequests::Loop() (already the polling home of the ordinary ApduTimeout retry logic) now also polls each still-segmenting entry’s SegmentTimer, retransmitting the current window’s already-sent segment(s) up to Number_Of_APDU_Retries and then firing HookAbort with tsmTimeout and removing the entry - while an entry IsSegmenting(), its ordinary ApduTimeout/retry handling is deliberately skipped, since cl. 5.4.4.2’s RequestTimer does not start until FinalACK_Received.
  • Implements cl. 5.4.2.1’s InWindow as SentConfirmedRequestValue::InWindow using (uint8_t)(a - b) narrowing (RB185, docs/runbooks/runbook-resolver-lessons.md), not % 256 on int-promoted operands.
  • Extends SentConfirmedRequestValue/BACnetStackSentConfirmedRequests with m_apduSegmentTimeout and m_sentAllSegments (both threaded through from BACnetInterface::SendConfirmedSegmented, #2012, which previously computed but discarded sentAllSegments) and new mutators (AdvanceSegmentationWindow, RestartSegmentTimer, RetrySegmentTimer, StopSegmentingAndStartRequestTimer) mirroring the shape of #2012’s own SetSegmentationState.
  • New file source/BACnetSegmentAckProcessor.h/.cpp.
  • AbortPDU_Received required no code change: the existing generic BACnetAbortProcessor::Process (unconditionally reached for any incoming Abort-PDU on the client role) already removes the matching SentConfirmedRequests entry by invoke-id, and SentConfirmedRequestValue’s destructor/Reset() (extended by #2012) already frees a still-segmenting entry’s retained outgoing packet and sent-segments map correctly.
  • UnexpectedPDU_Received and the local-application-initiated SendAbort transition are NOT implemented by this issue - see the lessons file for why, and what would be needed.
  • rb e-20260922-116 (#2252): bound BACnetStack_SetBACnetSCCompatibilityFlags in the node adapter - the export was already implemented and exported from source/CASBACnetStackDLL.h and bound by every other language adapter (C++, C#, Java, Python, Rust), but the node adapter had no napi wrapper for it, blocking chipkin-bacnet-explorer#83’s BACnet/SC “hub compatibility mode” feature. Mirrored the sibling device-wide SC setter SetBACnetSCHelloCapabilities’s exact shape (one uint8_t in, bool out): added the napi wrapper (regenerated via ci_scripts/generate-napi-wrappers.js --write, which reported the hand-written wrapper already byte-identical to the generated one - confirming this export needed no bespoke logic), regenerated the derived TypeScript surface via ci_scripts/generate-node-adapter.js, and removed the now-stale dll-only:BACnetStack_SetBACnetSCCompatibilityFlags suppression from ci_scripts/node-adapter-parity-allowlist.txt. The api.ts declaration carries the header doc comment’s full warning forward as a JSDoc block - the flag is a deliberate, device-wide deviation from ASHRAE 135 (135-2024 AB.2.2’s mandatory Hello destination option), so a Node caller cannot enable it without seeing that warning. adapters/golang/ remains unbound, out of scope per the issue’s own text (no consumer is asking).
  • rb e-20260922-125 (#1866, part A of #1487, #894 Stage 4): taught the Node adapter’s napi-wrapper generator family to handle CALLBACK-shaped exports (function-pointer parameters), which generate-napi-wrappers.js skips outright (// callbacks: separate shape). Added a sibling generator, ci_scripts/generate-napi-callbacks.js, scoped to this issue’s slice - the 26 RegisterCallbackGetProperty*/RegisterCallbackSetProperty* registrations in adapters/node/@chipkin/cas-bacnet-stack/src/interface_callbacks.cc, the most uniform shape of the 73-registration callback family. It reads each registration’s FPCallback* typedef from source/BACnetCallbackInterface.h (via adapter-model.js’s already-parsed callbackTypedefs) and a new callbackRoles section of ci_scripts/adapter-roles.json describing how each typedef parameter marshals across the C-calls-into-JS boundary (in-scalar, in-buffer, out-scalar, out-buffer-max/count, error-code), and holds the result against the hand-written registrations with --gate, the same byte-for-byte discipline generate-napi-wrappers.js uses for the non-callback family. ci_scripts/adapter-roles-verify.js was extended to validate the new callbackRoles section independently (keyed by typedef, not by export - a RegisterCallback* export’s own parameter list is always exactly one function pointer, so the existing per-export validation could not see it). 21 of the 26 registrations generate byte-identical to their current hand-written form and are now marked // GENERATED; 5 are on a new ci_scripts/napi-callback-bespoke-allowlist.txt for a hand-written/typedef naming disagreement (GetPropertyDate/SetPropertyDate: typedef’s yearMinus1900 vs. hand-written year; GetPropertyTime/SetPropertyTime: typedef’s hundredthSecond vs. hand-written hundredthSeconds/hundrethSeconds) - each is naming-only and behaviourally inert. One finding worth flagging without fixing: RegisterCallbackSetPropertyBoolWrapped marshals its const bool value parameter to JS with Napi::Number::New, not Napi::Boolean::New - every registered CallbackSetPropertyBool JS callback receives 0/1, not true/false, unlike its GetProperty counterpart; left unchanged and allowlisted with the finding recorded, since silently changing an existing callback’s JS-visible value type is outside this batch’s authority. --self-test proves the comparison catches the #1464 shape (a lambda parameter type drifted from its typedef) and the #1464/#390 shape control arm 2 exercises (two same-typed call arguments reordered, invisible to the compiler). Wired --self-test and --gate into ci_scripts/pre-push.sh alongside the existing non-callback napi gates. No source/ change; no .github/ change.
  • rb e-20260922-127 (#1867, part B of #1487): extended ci_scripts/generate-napi-callbacks.js (the callback-registration generator #1866 built) to a second slice of the customer header’s RegisterCallback*/RegisterHook* registrations in adapters/node/@chipkin/cas-bacnet-stack/src/interface_callbacks.cc. Re-verified the issue’s own evidence at tip first: the issue’s ~41-item list (as of 3d0878fae) was stale in two ways beyond the two callbacks it already names as removed - ValidateBACnetSCOperationalCertificate and GenerateBACnetSCCertificateSigningRequest are confirmed gone everywhere (#988; only a removal comment survives, in ci_scripts/check-contract-pins.js) - the real current scope is 39 registrations, none of which the generator covered before this batch. Added one small, real generator extension: an optional error-code role (most of this slice’s typedefs have no errorCode out-parameter at all, unlike every one of #1866’s 26), and extended the behaviour-signature comparison’s local-buffer-name heuristic to also resolve this slice’s <param>Buffer suffix naming convention (this slice’s hand-written code uses it throughout, where #1866’s slice used a napi<Param> prefix convention exclusively) - the same class of comparator fix docs/runbooks/lessons/e-20260922-125.md already needed twice for the prefix form. 3 registrations generate byte-identical to their current hand-written form and are now marked // GENERATED: GetSequenceLiftRegisteredCarCall, GetSequenceLiftAssignedLandingCall, GetSequenceLiftLandingDoorStatus (added callbackRoles entries for their three typedefs in ci_scripts/adapter-roles.json). The other 36 are on ci_scripts/napi-callback-bespoke-allowlist.txt, each with a specific, line-cited reason - 4 distinct shape/idiom classes this generator does not (yet) have: a hardcoded bool lambda return (13 void/non-bool-return registrations, including all 8 RegisterHook* exports, which the generator also cannot target by name - only RegisterCallback*), a return value that carries a payload instead of success/failure (ReceiveMessageForPort, SendMessageForPort, GetSystemTime), an object-return JS calling convention instead of positional out-buffers (ReadFile, ReadFileRecord, WriteFile, WriteFileRecord - the latter two also pack/unpack a variable-length array of records, a shape with no role-vocabulary equivalent), a NULL-guarded in-buffer helper the generator’s plain in-buffer role does not call (SetElevatorGroupLandingCallControl, ReinitializeDevice, DeviceCommunicationControl, LifeSafetyOperation - each cited guard traces to a real prior defect fix, #2087/#2097/#2105), and a multi-out-scalar-plus-secondary-out-buffer-max composition whose JS call-argument order also deviates from typedef declaration order (GetListElevatorGroupLandingCallStatus). A further group (WriteGroupInhibitDelay, CreateObject, DeleteObject, PrepareBackup, CompleteBackup, PrepareRestore, CompleteRestore, InitiateWebsocket, SCStartListening) otherwise fits the vocabulary but uses a return result.As<Napi::Boolean>() (or .Value()) idiom instead of the generator’s canonical if (result... == false) { return false; } guard - left as an idiom-drift finding rather than silently normalised. Two FINDINGS surfaced without being fixed, per the same “adjudicated, not resolved in the generator’s favour” rule #1866’s SetPropertyBool entry established: RegisterCallbackGetListOfEnumerationsWrapped never inspects the JS callback’s boolean result at all (writebacks and return true; run unconditionally), and RegisterCallbackSetSystemTimeWrapped does the same (return true; unconditional, plus the typedef/hand-written naming drift yearMinus1900/year and hundredthSecond/hundrethSeconds already seen on GetPropertyDate/GetPropertyTime) - both mean a JS callback’s false return is currently silently discarded; left unchanged. A naming-only drift (no behaviour change) is also recorded for AcknowledgeAlarm (eventTimeStampYearMinus1900/eventTimeStampYear, British vs. American “acknowledg(e)ment” spelling throughout). Every out-buffer-with-max-length registration in this slice (ReadFile/ReadFileRecord/GetListElevatorGroupLandingCallStatus) was checked bound-by-bound against its hand-written form and left hand-written, not regenerated, so no bound can have been silently dropped; each bound is cited by line in the allowlist. node-adapter-output-buffer-guards.js (#391) re-verified green (self-test 23/6, and its one real finding - SendReadPropertyWithLengthWrapped in interface_send.cc, issue #392 - is confirmed pre-existing and unrelated: identical on the pre-batch tip, and not a file this batch touches). Stopped at the hooks/SC seam as the issue itself permits: no RegisterHook* and no SC-specific registration (BACnetSCStateChange, SCStartListening, SCStopListening) is generated in this batch - all are on the bespoke allowlist with their specific reasons, ready for a follow-up issue rather than forced through with shaky verification. No source/ change; no .github/ change.
  • rb e-20260922-129 (#1868, part C of #1487): re-verified this issue’s real scope against tip (not the issue’s stale evidence at 3d0878fae). CASBACnetStackTestToolDLL.h declares 18 Register* exports; the addon binds 10 of them inside interface.cc’s #if defined(BACNET_STACK_TESTTOOL) block (currently lines 296-539, not the issue’s stale :289-503 hint): GetPropertyConstructed/SetPropertyConstructed (already generated, #1866), the 3 RegisterHook*Ack router/BVLL hooks (#1863, part B of #1312 - confirmed already landed via git log -S, predating this whole e-20260922 series, and out of this issue’s named 5-item scope regardless), and the 5 this issue names: AuditNotificationReceived, EventNotificationReceived, FSMTransition, InvokeIdGenerator, ProgramChangeRequested. The other 8 test-tool exports remain unbound and untouched - out of scope, not bound here. None of the 5 are generated. ci_scripts/generate-napi-callbacks.js has no typedef model for any test-tool callback at all: its typedef source (adapter-model.js‘s callbackTypedefs) parses only source/BACnetCallbackInterface.h, using a regex matched only against a top-level typedef <ret> (*<name>)(...) statement - these 5 callbacks’ function-pointer types are instead declared inline as a parameter of a BACnetTestToolInterface class method, in a different file entirely (source/BACnetTestToolInterface.h), a declaration shape the parser does not recognise. Making these generatable would mean teaching the model both a second header and a second declaration grammar - not a small role-table addition, so per this batch’s “stop rather than force it” instruction it was not attempted. Independently, all 5 hand-written wrappers also use idioms the generator’s canonical form cannot produce: a null/undefined-to-unregister branch with a thrown Napi::TypeError (FSMTransition, InvokeIdGenerator, ProgramChangeRequested), a void-returning lambda (AuditNotificationReceived, EventNotificationReceived), and a non-bool (uint8_t) lambda return type with an empty typedef parameter list (InvokeIdGenerator). All 5 are recorded on ci_scripts/napi-callback-bespoke-allowlist.txt with specific, line-cited reasons; --gate ignores allowlist entries outside its own SLICE, so this addition does not change --gate’s pass/fail behaviour (still 24 generated, now 46 allowlisted, up from 41). InvokeIdGenerator’s acceptance criterion (out-of-range handling for the JS callback’s returned invoke ID): read interface_callbacks.cc:3446-3456. Napi::Number::Uint32Value() applies the JS/ECMA-262 ToUint32 conversion to the callback’s return value (truncate toward zero, then reduce modulo 2^32 - so e.g. -1 becomes 4294967295, 3.9 becomes 3, NaN/Infinity become 0), and the resulting uint32_t is then narrowed to uint8_t via static_cast, which C++ defines as reduction modulo 256 for an unsigned target - well-defined, not UB. Net effect: an out-of-range JS return value wraps deterministically modulo 256 (e.g. 256 -> 0, 300 -> 44, -1 -> 255). This is a FINDING, recorded per the issue’s acceptance criterion; nothing in this file changed, so today’s exact behaviour is unconditionally preserved. TESTTOOL gating verified intact: all 5 registrations, and every other test-tool-only registration in interface_callbacks.cc, sit inside that file’s own #if defined(BACNET_STACK_TESTTOOL) block (:3209-3646); interface.cc’s own gate (:296-539) is unchanged (no registration moved). Ran the required gating control arm for real (moved the already-generated RegisterCallbackGetPropertyConstructed registration outside interface.cc’s TESTTOOL block, bypassing the #1641 deferral locally, not committed, then reverted): node-adapter-coverage.sh went red as expected (napi export(s) registered OUTSIDE #if defined(BACNET_STACK_TESTTOOL) whose C twin is declared only in source/CASBACnetStackTestToolDLL.h ... This is #450: RegisterCallbackGetPropertyConstructed). addon-compile.sh’s CAS_BACNET_PRODUCTION_BUILD configuration, contrary to the issue’s own framing, stayed GREEN under this exact mutation - a finding, not an oversight: addon-compile.sh is -fsyntax-only per translation unit, never a link (its own header says so explicitly: “no objects, no link, no node-gyp”). The escaped exports.Set(...) line in interface.cc only takes the address of RegisterCallbackGetPropertyConstructedWrapped, whose DECLARATION in adapters/node/@chipkin/cas-bacnet-stack/src/interface_common.h:265 is unconditional even though its DEFINITION in interface_callbacks.cc stays correctly gated - so interface.cc’s syntax check has everything it needs and never reaches the missing BACnetStack_RegisterCallback... export at all. The real production failure mode for this control arm is the LNK2001 link error node-adapter-coverage.sh’s own error text names, not a compile error - addon-compile.sh cannot and does not claim to catch it; node-adapter-coverage.sh is what actually gates this defect class. Reverted; both green again (coverage OK, compile OK under both configurations). generate-napi-callbacks.js --self-test/--gate, adapter-callback-signature-check.js, and generate-node-adapter.js --check all pass for real. adapter-roles-verify.js, check-node-api-returns.js, node-adapter-argument-order.js, addon-compile.sh, and node-adapter-coverage.sh report SKIPPED under the normal invocation (still deferred per #1641, confirmed unchanged, same six/nine-gate deferral #1866/#1867 already documented) - each of the last two was also re-run for real via a local, uncommitted bypass of ci_scripts/deferred-gates.txt for genuine confidence: node-adapter-coverage.sh green (380 napi exports; production manifest covers 219 ungated names; gating agrees with the declaring header both ways), addon-compile.sh green (6 translation units x 2 configurations, 12 compiles, g++, node v20.20.2). No source/ change; no .github/ change; adapter-roles.json untouched (no new callbackRoles entries - nothing generated).
  • rb e-20260922-132 (#1870, part E of #1487): new gate ci_scripts/check-node-api-params.js, the parameter-list sibling of check-node-api-returns.js, wired into ci_scripts/pre-push.sh (self-test and real run). It compares every api.ts function’s forwarded arguments - top-level functions and the arrow members of the api object alike - with the info[k] slots its napi wrapper reads: order (by name, so two swapped same-typed numbers are caught where tsc certifies them), arity, kind, nullable and optional. It compares against the WRAPPER, not the header model: the wrapper decides where a value lands, and the hand-written addon deliberately lags the header until #1641 WP-9. At the batch tip: 375 checked, 3 pinned in the new ci_scripts/node-api-param-allowlist.txt (each hand-verified against its wrapper; a pin excuses only the name check on a slot nothing names, never a forward/arity/kind problem; the list may only shrink), 5 with no wrapper. The self-test swaps every adjacent parameter pair across the checked functions on each run and fails if any swap passes (2,167 swaps at the tip, 1,290 of them between same-typed parameters). No api.ts signature changed: no parameter list disagreed with its own wrapper.
  • rb e-20260922-133 (#1599): the public enums of source/CASBACnetStackDLL.h (BACnetDecodeType 2, BACnetPropertyIdentifier 522, BACnetStackSetting 4 members) are now generated into the C#, Python and Node bindings from the header, so an enum or member added there arrives by regeneration. C#: namespace-level public enum Name : uint in adapters/csharp/CASBACnetStackAdapterBindings.cs. Python: class Name(IntEnum) in adapters/python/CASBACnetStackAdapterBindings.py. Node: the generated adapter-enums.ts, each enum exported by name from the package entry (BACnetStackSetting was reachable before only as api.BACnetStackSetting). Members drop the C prefix (BACnetPropertyIdentifier.presentValue) and carry the header’s per-member comments. ci_scripts/adapter-model.js parses the enums (a shape it cannot read is fatal, pinned by its --self-test), and ci_scripts/adapter-enum-emit.js renders them for all three generators. The hand-written C# PROPERTY_IDENTIFIER_* constants and Python dictionaries are unchanged; the names do not collide. New gate ci_scripts/check-adapter-enums.js (in pre-push.sh, self-test and real run) reads each committed binding back against the header - every enum, member, value and order - which the drift gates cannot do, since a generator that drops a member agrees with its own output. The C# enums are not compile-verified (no dotnet on the machine that built this); Python import-verified; Java and Rust follow in #2280. Revert-red: check-adapter-enums: ... enum BACnetStackSetting: MISSING member DABRangeClusterTolerance.
  • rb e-20260922-134 (#2277, stretch of #1870): new ci_scripts/generate-node-api-params.js derives each api.ts function’s parameter list and forwarding call from the info[] slots its napi wrapper reads (the slot map check-node-api-params.js computes, not the header model, which the addon lags until #1641 WP-9). --gate (wired into ci_scripts/pre-push.sh with its self-test) holds the 237 functions whose wrapper determines the list completely to that text exactly, whitespace aside, so a re-spelling the params gate accepts ((number), a missing | null the wrapper allows) goes red; --write fills in a stub function X(): T { return getAddon().X(); }, so a new export gets its typed parameter list without hand-writing it. The 138 functions it cannot derive (with a callback slot, array slots, a default, a facade-filled or doubly-named slot, or a year-minus-1900 slot: 71, 30, 13 and 24, and 3 of the 13 also carry a year-minus-1900 slot) are listed with their exact reasons in the new shrink-only ci_scripts/node-api-param-shape-allowlist.txt. No api.ts line changed: every derivable declaration already matched.
  • docs/reviews/2026-09-08-public-api-freeze-review.md and its interactive companion: a function-by-function review of all 837 public functions across the four public surfaces (CASBACnetStackDLL.h, CASBACnetStackTestToolDLL.h, BACnetInterface.h, BACnetTestToolInterface.h), with purpose, description and per-parameter documentation for every one, the consistency findings measured over the whole surface, and the owner rulings of 2026-09-08.
  • docs/sprints/sprint-199-network-port-instance-callbacks.md: the sprint plan for networkType → networkPortInstance on the transport boundary (#822), including the integrator migration note.
  • docs/runbooks/runbook-interface.md: the runbook for the public-interface session (D), a peer of runbook-resolver.md that inverts its frozen-surface rule for the four tracked headers.
  • rb napi-20260908-2 (#1531): ci_scripts/check-node-api-returns.js compares every adapters/node/@chipkin/cas-bacnet-stack/src/api.ts declaration against the napi kind its wrapper actually returns, and pre-push.sh runs it. Nothing did before: generate-napi-wrappers.js --gate compares wrappers to the C headers, node-adapter-parity.sh compares names, and addon-compile.sh compiles C++ — so when #1509 changed six wrappers’ return kinds, the hand-written TypeScript facade kept declaring the old ones and every gate stayed green. A wrong return type there is worse than none, because tsc certifies it: const ok: boolean = GetBDTEntry(...) compiled, received a byte count, and treated a successful read as a failure. 325 declarations are checked; the 3 whose returns cannot be derived (each builds a result object) are pinned to their declared type in ci_scripts/node-api-return-allowlist.txt, and that list may only shrink. Ships with a --self-test that reintroduces the #1531 defect and asserts the gate names it.
  • rb napi-20260909-1 (#1532): ci_scripts/node-adapter-coverage.sh --self-test proves its own header-vs-gating check, in both directions, and pre-push.sh runs it before the gate. The check that closes #1532 and #450 shipped with its proof recorded in a batch record rather than wired in — and a recorded proof does not re-run. Each corpus stages a copy of the tree, splices one export, and runs the real, unmodified gate at it via NODE_ADAPTER_COVERAGE_ROOT; the two export names are derived from the tree at run time rather than hardcoded, so the corpus cannot rot into corrupting a name that no longer exists, and it fails rather than skipping if either cannot be picked.
  • rb 1487-swap (#1487): the Node addon’s DebugLogger is gone — 354 call sites, the declaration, the definition, the LOGGER_LEVEL_* / CURRENT_LOGGING_LEVEL constants, three now-dead hex-formatting std::stringstream blocks, 93 orphaned // DEBUG. markers, and the four standard headers (<iostream>, <fstream>, <sstream>, <iomanip>) that nothing else in the addon needed. It wrote every call’s arguments to std::cout and appended them to ./log.txt, unconditionally, from a shipped addon. Two reasons it had to go: the message text was hand-written prose per export, which is the single largest thing making the wrappers resist generation (#1487), and building those messages dereferenced caller-supplied buffers — which is how #391 and #395 turned an undersized Buffer into an over-read before the guard that was supposed to prevent it ran.
  • rb 1487-swap (#1487): this is not the stack’s logging. DebugLogger occurred 354 times in adapters/node/…/interface*.cc and zero times in source/. The stack’s own debug output is a separate mechanism — BACnetStack_RegisterCallbackLogDebugMessage (source/CASBACnetStackDLL.h), which the host registers a JS function against like any other callback — and it is untouched.
  • rb e-20260922-110 (#988): BREAKING (ABI removal) — BACnetStack_RegisterCallbackValidateBACnetSCOperationalCertificate and BACnetStack_RegisterCallbackGenerateBACnetSCCertificateSigningRequest, and their FPCallbackValidateBACnetSCOperationalCertificate/FPCallbackGenerateBACnetSCCertificateSigningRequest typedefs, removed. Neither was ever invoked by the stack — there is no TLS handshake path inside it (TLS is delegated entirely to the host) and no call site ever ran the registered function pointer, so a registered callback never did anything. Owner ruling (2026-09-21, superseding the 2026-09-07 “wire them up” ruling): a registered security callback that never runs is worse than no callback. No runtime behaviour changes — since neither callback was ever invoked, removing them changes nothing for any caller. Migration: delete the two registration calls and the two callback functions you registered with them; certificate validation and CSR generation remain entirely the host application’s own responsibility via its TLS/PKI library, exactly as before. See docs/CAS BACnet Stack - BACnet SC Manual_v6.md §16.3 for the corrected description of where that responsibility lives, and docs/interface/IFC-039.md / IFC-040.md for the full interface-change record.
  • rb b-20260907-6 (#1233 CR-F02a-015): deleted a 104-line stale, fully commented-out EnableObjectsOptionalProperties function with no declaration or tracking reference.
  • rb e-20260911-20 (#1234, CR-F03-025): removed BACnetPrimitiveBitSTRING(const size_t maxSize), a constructor that could never produce a validly-encodable object and had zero call sites repo-wide.
  • #1008: source/CASBACnetStackDLL.h’s API Conventions block now states the little-endian host assumption (pre-cleared frozen-header edit, documentation only — no declaration changes and no ABI impact). The new “Byte order” entry separates the two byte orders that appear in this header and are easy to conflate: the fixed, host-independent wire orders a parameter’s own documentation names (the 6-byte IPv4 connection string’s big-endian port, the AtomicWriteFile record-access big-endian length prefixes), versus the host-order hand-packed buffers whose field diagrams size each field by its own sizeof() — the propertyArrays buffer shared by SendReadProperty/SendReadPropertyWithLength/SendWriteProperty/SendCreateObject, the WriteGroup changeList, and the COV reference-list. It also records where the constraint is enforced (source/BACnetStackCommon.h’s #error, from #889/PR #1003), that the guard is deliberately not in the public header, and that CEndianness is not the enforcement mechanism.
  • #1008: source/endianness.h now says explicitly that it is not the enforcement mechanism — it holds runtime detection/conversion helpers, IsBigEndian() returning true stops nothing, and the actual guard is the #error in BACnetStackCommon.h. Extends the scope note added by #1232 (CR-F01c-013) so a reader who finds CEndianness first cannot conclude the packed-buffer ABI is handled.
  • rb 894-adapter-generation (#894): AGENTS.md, README.md and adapters/README.md now record how to regenerate the generated adapter surfaces (node ci_scripts/generate-cpp-adapter.js and node ci_scripts/generate-node-adapter.js, both, plus --check for the drift gate), exactly which 8 files that covers, and — the part that was missing — what it deliberately does not cover. adapters/README.md gains a per-file table saying what every non-generated file in the directory does and what keeps it honest, including why the per-language build manifests and self-test entry points have no by-name references anywhere (their toolchains discover them), and a section stating plainly that C#, Python, Go, Java and Rust have no generator at all today.
  • rb e-20260911-18 (#1236): documented BACnetStackSentConfirmedRequests::InsertRequest’s per-peer scan as a deliberate, low-priority tradeoff (CR-F05-028). No production behavior change; 5 other checklist items (CR-F05-002/011/017/022/023) were found already fixed and checked off.
  • rb e-20260911-21 (#1234, CR-F03-007): explained why BACnetTag::SetClass needs no range validation (its target field is a bool bitfield, already narrowing any input to one of exactly 2 valid states). No behavior change.
  • rb e-20260911-24 (#1234, CR-F03-003): documented BACnetPrimitiveNULL/BOOL::GetMinimumMemoryCount() as an approximate lower bound (undercounts when the context tag number requires the extended form). No production behavior change; verified no consumer relies on this as a hard bound.
  • rb f-20260917-2 (#2113): fixed CASBACnetStackAdapter.py’s stale “HELPERS ONLY, NOT A WORKING ADAPTER” docstring, which predated CASBACnetStackAdapterBindings.py’s generator landing and misled readers into thinking Python has no working adapter. No behavior change.
  • interface-batch-2 (#1232): SetBACnetSCCertificateFileObjects now documents its staged (Changes_Pending / ACTIVATE_CHANGES) transaction behavior (CR-F01a-011); the FPCallbackSetPropertyCharacterString doc block no longer describes BitString semantics (CR-F01b-019); FPHookTextMessage/FPHookError name errorCode instead of errorClass twice (CR-F01b-023); the DeviceCommunicationControl/ReinitializeDevice passwordLength width difference is explained rather than left as an apparent inconsistency (CR-F01b-024); SendRouterBusyToNetwork/SendRouterAvailableToNetwork document their dnets/dnetCount NULL and zero semantics (CR-F01c-012); endianness.h records that it answers only the protocol-value half of the endianness question, not the flat ABI’s little-endian host assumption (CR-F01c-013, scoping #892/#1008); a stale doc paragraph describing a member that was centralized away is deleted (CR-F01c-014); SendConfirmedRequestRaw’s required sentInvokeId is documented as a deliberate asymmetry (CR-F01c-015); SendAuditLogQuery‘s compile-time-only gating and the audit-notification sends’ deliberately ungated status are explained (CR-F01c-011, -020, -021); CASBACnetStackOptions.h’s closing #endif comment matches its #if (CR-F01c-023); IsPropertyCommandable’s Channel ToDo is resolved (Priority_Array and Relinquish_Default are required on Channel, so unconditional is correct — CR-F01b-015).
  • interface-batch-2 (#1232, CR-F01b-017): whether GetPriorityArray’s generic host-callback path needs a propertyArrayIndex upper bound. It does not — BACnetDBDevice::GetPropertyUsingCallbacks, the only caller that passes an array index, already rejects > BACNET_MAX_PRIORITY with invalidArrayIndex (which cl. 15.5.1.1.2 requires). Adding a bound here would be unreachable, and if reached its bare false would be remapped by that same caller to the misleading valueNotInitialized. Recorded in the code.
  • interface-batch-2 (#1232, CR-F01a-009): whether the BACnet/SC sync-failure path should roll the database object back “for consistency”. It should not. The add path is idempotent, so a re-add of a live, configured port reaches that line and the sync fails on live data-link state — deleting there would destroy working configuration. There is also no API to release the data-link claim, so the delete would strand one of the 1–2 STACK_OPTION_MAX_DATA_LINK_INSTANCES_SC slots permanently. Recorded in the code; the claim-failure path was guarded instead (above).
  • interface-batch-2 (#975): GetPriorityArrayBool sends the “is this slot relinquished?” probe and the “what is this slot’s value?” read to the host as byte-identical CallbackGetPropertyBool calls, so a Boolean commandable property can never report TRUE. NOT FIXED — every candidate fix changes the customer-facing registration surface and needs an owner decision. The audit this issue asked for is complete and recorded: Boolean is the only affected datatype, because every sibling reads its value through a different typed callback. tests/TestBACnetPriorityArrayBoolNullProbe.cpp proves the collision, pins the Real sibling as unambiguous as the control arm (the rest of the audit is by inspection, recorded in the code), and carries the desired behavior as a DISABLED_ test to re-enable as the revert-red once the shape is chosen.
  • rb c-20260908-2 (#1012): BACnetStackSettings gains TryGetSetting() and an explicit initial state. The two-layer lookup has to distinguish “no value here” from “explicitly set to BACNET_SETTING_DISABLED”, which GetSetting cannot — it returns the sentinel for both. The class now serves two roles with different initial states: the database’s copy is the seeded default layer, a device’s copy is the override layer and must start empty.
  • rb c-20260908-2 (#1012): Reset() seeds the DAB defaults from the same CAS_BACNET_DAB_DEFAULT_* constants DABTable’s own constructor uses, so the default layer cannot drift from the engine’s defaults; a test pins the literals so a change on either side is caught.
  • rb c-20260908-2 (#1012): routing moved from BACnetInterface to BACnetDatabase, which owns the device list, the defaults and AddDevice — all three are needed, because AddDevice must seed a new device’s DABTable or the “applies to devices added later” rule would hold for setting 0 and silently not for the DAB tunables. BACnetInterface’s two methods are now thin forwarders.
  • rb c-20260908-2 (#1012): a static_assert binds BACNET_STACK_SETTING_ALL_DEVICES to BACNET_UNKNOWN_DEVICE_INSTANCE in the one translation unit that sees both, so “safe by construction” is enforced rather than asserted in a comment.
  • rb 1487-testtool-gate (#1508): --gate now holds the // GENERATED by … marker line itself byte-for-byte. It was excluded from the compared region, so the marker sentence — the one line --write produces and nothing else does — was enforced by nothing: reverting all 33 markers in interface_send.cc to their pre-#1508 text left --gate reporting OK.
  • rb 1487-testtool-gate (#1508): three counts stated in code comments were wrong and are corrected — adapter-roles-extract.js said 24 of the 84 role-bearing exports are test-tool (it is 27, against 57 customer), generate-napi-wrappers.js said 30 of the 121 test-tool wrappers use a C-cast slot read (29), and the same comment said the customer wrappers “assign without a cast”, which understated the pre-#1508 blindness: 9 customer wrappers use the cast read and 5 index through an enum, so 14 of them were degraded too (none was ever generated, so nothing was mis-swapped). adapters/README.md framed all 107 test-tool exemptions as the single #1509 argument-guard decision; 61 differ only in the guard and 46 differ in what they accept or how they marshal as well, so an owner ruling on #1509 would resolve 61, not 107.
  • rb 1487-testtool-gate (#1508): five reasons recorded in ci_scripts/napi-wrapper-bespoke-allowlist.txt were corrected after review. That file is the record of why each of the 155 exempt wrappers has no gate, and a false reason there is an ungated export with a note claiming otherwise. Three exemptions attributed to “statements after the call” have no statements after the call — their real reason is a ValidateBufferLength the generator cannot derive because the export has no ci_scripts/adapter-roles.json entry, so resolving them toward the generator would delete a bounds check. SendAuthRequest was filed under “builds a std::vector”; it packs fixed stack arrays with clamps that reject oversize JS input. SetAccessZoneOccupancyCountDirect was named as omitting a C out-parameter; it does not, and IsAccessZonePassbackViolation is the wrapper that does. The audit note claimed only one direction had been checked; the reverse direction is now recorded, naming the nine wrappers whose hand-written bounds check the generated form would drop. And the customer class counts read 34/11/6 = 51 against a 48-entry section. No wrapper’s code changes.
  • rb 1487-testtool-gate (#1508): the // GENERATED by … marker written above each generated napi wrapper names both export headers. It said “Edit source/CASBACnetStackDLL.h”, which was true while the customer header was the only surface compared; 14 of the 111 generated wrappers now come from source/CASBACnetStackTestToolDLL.h and that sentence sent their reader to a file that does not declare them. The generator also recognises the marker by its invariant prefix rather than by the whole sentence — with exact matching, changing the sentence at all made --write stack a second marker and a second copy of the notes on every already-marked wrapper. Comment text only; no wrapper’s code changes.
  • rb 1487-testtool-gate (#1508): three defects in the comparison itself were found by extending it and are fixed here. A slot index written as an enum constant, and a slot read behind a C-style cast, were both invisible to the behaviour signature - for those wrappers it recorded no slot reads at all and silently stopped comparing the thing it exists to compare. A hand-written wrapper’s body was sliced to the next wrapper rather than to its own closing brace, folding an intervening helper function’s info[] reads into it. Both erred in the safe direction (a wrapper could be withheld from generation, never wrongly generated), and an unresolvable slot index is now reported rather than skipped.
  • The Windows PDB is a new artifact. The staging steps copy CASBACnetStack_x64_Release.pdb beside the DLL, and nothing in the packaging step excludes it, so the release zip now carries a PDB it never carried before. A PDB exposes private symbol names and build-machine source paths. Owner ruling, 2026-09-07: PDBs are not shipped. '*.pdb' is excluded from both release zips. The PDB remains available as a workflow-run artifact so support can symbolise a customer stack trace without the symbols leaving Chipkin. (Linux legs produce no PDBs.)
  • Not addressed here: adapters/{cpp,csharp,golang,rust} still select CASBACnetStack_x64_Debug binaries when the consumer builds with _DEBUG/_TESTING. Only the DLL of that name is gone - CASBACnetStack_x64_Debug.lib (from DebugLib|x64) and the Linux libCASBACnetStack_x64_Debug.{so,a} still exist - so this bites DLL-mode consumers only. adapters/cpp/CMakeLists.txt also still lists CASBACnetStack_Win32_Release as a find_library fallback, a name no surviving configuration can produce. Tracked separately.
  • Not addressed here: the unit tests still consume googletest through the Microsoft v140 NuGet package at 1.8.1.8 (2018), five toolset generations behind the v145 the stack compiles with, while Linux CI builds GoogleTest 1.14.0 from source. That split, and the long package path behind #128’s MAX_PATH failure, are the remaining staleness in the test build.
  • The ABI is year-2038-safe on both widths. A 32-bit build still stores time internally as the platform’s 32-bit time_t and so has its own 2038 limit — that is a property of the build, not of this ABI, and is stated in README.md rather than left to be discovered.
  • README.md’s support matrix and adapters/README.md’s time_t section are rewritten; the dated records in CHANGELOG.md, docs/sprints/ and docs/reviews/ are left alone, since they record what was decided at the time.
  • Supersedes #606, which is reopened to track the retirement of its assert, its pin and its contract text.
  • Not verified here: an actual 32-bit compile. The blocker is removed and the ABI is width-independent, but projects/linux/Release/x86 has not been built to prove it end to end.
  • rb napi-20260908-1 (#1509): the issue filed four of these six as needing a frozen-header ruling, on the grounds that the exports are declared uint32_t while their own \return documentation said they return true or false. That contradiction no longer existed: #895, #896, #897 and #898 had already rewritten those \return blocks to “The number of bytes written … 0 if failed. (was bool.)”. The declaration and its documentation agree, and the wrappers were the last thing still reading the old contract - so no change to source/CASBACnetStackDLL.h was needed or made.
  • rb napi-20260908-1 (#1509): 41 wrappers still check their own arguments and are not converted. CheckParametersCountAndType takes a positional list of DATA_TYPE_{BOOLEAN,NUMBER,BUFFER,FUNCTION} and nothing else, so it cannot express what they check: 26 accept a JavaScript Array, 5 a nullable argument, 3 a JS string, 17 do not check a contiguous 0..N-1 prefix, and 6 reject by returning a result object rather than undefined (a wrapper can have more than one of these). Extending the checker changes a function all 295 other wrappers call, so it is tracked on #1527 rather than attempted here.
  • rb napi-20260908-2 (#1527): 11 wrappers still check their own arguments, each for a reason the new codes do not address. Five reject by returning a result object — three as {success, invokeId} (SendConfirmedAuditNotification, SendAuditLogQuery, SendAuthRequest) and two as {success, credentialInstance, granted} (AccessPointAuthenticateFactor, AccessPointRequestAccess) — and two reject by returning false (SendWhoAmI, SendYouAre). Converting any of those seven replaces a structured failure value with an exception, a different customer-visible change from the undefined-versus-throw one ruled on in #1509, and left for the owner. Of the remaining four, two RegisterCallback* wrappers treat null/undefined as a distinct behaviour (unregister the callback) rather than a type, AddScheduleObjectPropertyReference coerces one slot with ToBoolean() instead of type-checking it, and SetIntrinsicChangeOfCharacterstringAlgorithm accepts a string or an array in one slot. 5 + 2 + 2 + 1 + 1 = 11.
  • rb a-20260921-6 (#2108): added protocol-level regression coverage for the null-Buffer<char>::Copy() defect trilogy fixed by #2087/#2097/#2105 (RegisterCallbackReinitializeDevice/ RegisterCallbackDeviceCommunicationControl/RegisterCallbackLifeSafetyOperation in adapters/node/@chipkin/cas-bacnet-stack/src/interface_callbacks.cc) - none of the three fixes had any. ci_scripts/node-adapter-selftest.mjs’s new leg 9 drives a real BVLC/NPDU/APDU frame for each service through RegisterCallbackReceiveMessageForPort + Tick() (the ReinitializeDevice and DeviceCommunicationControl vectors are tests/BTL_Tests/9_27_.../9_24_...’s own gtest-proven byte strings, adapted for BACNET_PROTOCOL_REVISION >= 20; LifeSafetyOperation has no existing BTL fixture, so that frame was hand-encoded and its correctness is confirmed by the SimpleACK the stack answers with) and asserts the registered JS callback receives a well-formed, non-throwing zero-length Buffer for the empty optional field, with a real-value control (password “123456”/“hello”) proving the same path still carries real content correctly. Test-only: no production behaviour changed.
  • Revert-red was attempted against all three guards (one at a time, rebuilt, restored) and is reported honestly rather than claimed: on this addon’s actual runtime (Node v24.4.1/win32/x64, node-addon-api’s Buffer<T>::Copy over N-API’s napi_create_buffer_copy), a NULL source pointer with length 0 does not throw or crash - it silently produces a Buffer identical to the guarded Buffer<char>::New(env, 0) path, so no JS-observable assertion can fail against the reverted code here. The new leg still fails under many other regressions (callback not firing, wrong service dispatched, decode breaking, wrong buffer length/content, or the argument-list drift #2087 itself already fixed separately), just not literally the reverted Copy(NULL, 0) call on this platform.
  • rb b-20260907-3 (#1240 CR-F09-017): dedicated truncated-payload decode tests for BACnetSCConnectRequest/BACnetSCConnectAccept’s shared CONNECT_REQUEST_PAYLOAD_SIZE guard.
  • rb b-20260907-3 (#1240 CR-F09-018): new dedicated test file for BACnetSCAddressResolutionACK’s space-delimited WebSocket-URI-list parser, covering the edge-case spacing (consecutive/leading/trailing spaces) Encode() itself never produces.
  • rb b-20260907-3 (#1240 CR-F09-020): dedicated positive/negative-control tests for BACnetDataLinkSC::ProcessIncomingPacket’s local-URI fallback when destinationConnectionString is absent.
  • rb b-20260907-3 (#1240 CR-F09-021): dedicated boundary-condition tests for the base BACnetSCBVLL::Decode/Encode. Along the way, added the missing out-of-class definition for BACnetSCBVLL::SC_BVLC_MINIMUM_LENGTH (a pre-existing static const that had never been bound by reference before; a link-time-only gap, not a behavior change).
  • rb b-20260907-4 (#1252 CR-F22-017): investigated and confirmed as a negative result (no code change) — #949’s “GetDatatype()==Invalid + unchecked static_cast” shape does not reach any of BACnetLogData/BACnetLogDatum/BACnetLogRecord/BACnetLogMultipleRecord’s IsEqual implementations, all of which gate their static_cast behind a datatype check first.
  • rb b-20260907-5 (#1252 CR-F22-016): new dedicated test file for BACnetStackTrendLogBase, the shared abstract infrastructure both concrete Trend Log classes derive from. No source behavior change.
  • rb f4-20260922-11 (#2320): fixed ci_scripts/check-ifc-entry.sh comparing a branch against origin/issues/runbook’s current (moving) tip instead of the actual, fixed merge-base commit, which could spuriously fail a branch that never touched a tracked interface header once another session’s later merge changed one. No behavior change for genuinely-affected diffs; only removes the false-positive.

(ABI-affecting public-interface fixes from the 2026-09-04 review, round 3 - see docs/sprints/batch-120-ledger.md for the full issue list)

  • batch-119 (#826): BACnetTextMessage::SetMessagePriority/Decode now reject a message-priority value outside the 2-value ENUMERATED domain (0-1) instead of forwarding it to the generic ENUM primitive unchecked.
  • batch-119 (#836): BACnetAuthenticationPolicy::GetElements now renders the mandatory policy SEQUENCE OF, which was previously omitted entirely from the XML/JSON view despite being correctly handled by Encode/Decode.
  • batch-119 (#854): BACnetGroupChannelValue::SetGroupChannelValue no longer rejects Channel_Number 0, which is a legal, non-special channel value per cl. 15.11.1.1.3 and was already accepted when received over the wire — the rejection made the builder inconsistent with the decoder/processor.
  • batch-119 (#855): BACnetDateRange::SetStartDate/SetEndDate now return the child DATE’s own validity instead of always true, so SetDateRange no longer marks the parent valid when passed an invalid start or end date.
  • batch-119 (#856): BACnetDateTimePattern::SetComplete now checks both the DATE and TIME child’s validity before marking the pattern valid, instead of doing so unconditionally.
  • batch-119 (#863): BACnetStackNotificationClass::InsertDestination and BACnetStackNotificationForwarder::InsertDestination now reject and discard a BACnetDestination that ends up invalid (bad recipient CHOICE, over-length address) instead of inserting it into the recipient list, where it previously broke every future ReadProperty(Recipient_List) on that object.
  • batch-119 (#889): source/BACnetStackCommon.h now #errors at compile time on a confirmed big-endian target (__BYTE_ORDER__ == __ORDER_BIG_ENDIAN__), enforcing the packed propertyArrays ABI’s existing little-endian-host assumption instead of silently mis-encoding on a big-endian build. The CASBACnetStackDLL.h documentation line and endianness.h disposition from the same issue are left for owner pre-clearance (frozen header).
  • loop-50, iteration 1 (#846): BACnetIAmRequest::SetSegmentationSupported/Decode now reject a segmentation-supported value outside the defined ENUMERATED domain (0-3) instead of forwarding it to HookIAm unchecked.
  • loop-50, iteration 1 (#848): BACnetIAmRequest::SetIAmRequest/BACnetIHaveRequest::SetIHaveRequest now invalidate the object before the first child update, so a partial failure on a rebuild leaves it invalid instead of stale-valid with a mix of old and new fields.
  • loop-50, iteration 1 (#849): BACnetWhoAmIRequest::Decode/BACnetYouAreRequest::Decode now reject a vendor ID exceeding Unsigned16 range instead of silently narrowing it at the GetVendorId() boundary.
  • loop-50, iteration 1 (#850): BACnetYouAreRequest::SetYouAreRequest/Decode now require at least one of Device-Identifier or Device-MAC-Address, matching the SendYouAre boundary policy.
  • loop-50, iteration 1 (#851): BACnetYouAreRequest::Decode now rejects a device-identifier that is not object type=device; BACnetYouAreProcessor adds the same check as defense-in-depth.
  • loop-50, iteration 1 (#852): BACnetYouAreRequest::Decode now peeks the next optional field’s application tag so a MAC-address-only You-Are decodes correctly instead of being rejected.
  • loop-50, iteration 2 (#837): BACnetAcknowledgeAlarmRequest::SetAcknowledgmentSource/BACnetEventNotificationRequest::SetMessageText now reject a NULL pointer with a nonzero declared length instead of constructing a std::string from it directly.
  • loop-50, iteration 2 (#838): BACnetAcknowledgeAlarmInfo::SetComplete now requires both required children to be valid before marking the SEQUENCE valid.
  • loop-50, iteration 2 (#841): BACnetGetEventInformationACK::InsertEventSummary(const BACnetEventSummary*) now rejects a NULL pointer instead of dereferencing it.
  • loop-50, iteration 2 (#842): BACnetEventSummary::GetElements no longer inserts the EventState element twice.
  • loop-50, iteration 2 (#844): Confirmed/UnconfirmedEventNotificationRequest builders and Clone now propagate a failed eventValues set instead of silently dropping it; Decode now distinguishes a present-but-malformed optional field from a genuinely absent one for messageText, ackRequired, fromState and eventValues.
  • loop-50, iteration 2 (#845): BACnetGetEnrollmentSummaryRequest::Decode now reapplies the closed-domain checks for acknowledgementFilter/eventStateFilter; the processor loops default-reject an unrecognized eventStateFilter value.
  • loop-50, iteration 3 (#824): BACnetTextMessage::CreateMessage now resets Message_Class, so reusing an object for an unclassified message no longer retransmits a stale classified value.
  • loop-50, iteration 3 (#830): BACnetUnconfirmedPrivateTransferRequest::operator= no longer leaks a BACnetAbstractSyntaxType allocation per assignment.
  • loop-50, iteration 3 (#835): Confirmed/UnconfirmedPrivateTransferRequest Decode now distinguishes a present-but-malformed optional serviceParameters field from a genuinely absent one.
  • loop-50, iteration 3 (#839): ConfirmedPrivateTransferACK/Error Decode now distinguish a present-but-malformed optional resultBlock/errorParameters wrapper from a genuinely absent one.
  • loop-50, iteration 3 (#840): BACnetLifeSafetyOperationRequest/Info/Processor now reject a reserved LifeSafetyOperation request value (10-63).
  • loop-50, iteration 3 (#843): BACnetLifeSafetyOperationRequest::SetLifeSafetyOperation now clears the object-identifier on an untargeted rebuild instead of retaining a stale target.
  • loop-50, iteration 4 (#823): BACnetNetworkLayerVendorProprietaryMessage::Clone now copies vendorId/data instead of returning a default-constructed object.
  • loop-50, iteration 4 (#825): BACnetNetworkLayerVendorProprietaryMessage/BACnetNetworkLayerRawMessage::GetData(buffer,len) now reject a NULL destination with a nonempty stored payload.
  • loop-50, iteration 4 (#827): BACnetRoutingTableEntry::Decode now rejects the reserved DNET values (0, 65535), matching SetConnectedDnet.
  • loop-50, iteration 4 (#828): BACnetVirtualRouter::AddVirtualDevice/UpdateVirtualDeviceInstance now reject a device instance above the 22-bit maximum.
  • loop-50, iteration 4 (#857): BACnetNameValue::SetValue now rejects an invalid value before taking ownership.
  • loop-50, iteration 4 (#858): BACnetNameValueCollection::InsertMemberAndTakeControl now rejects an invalid member; operator= now propagates a failed clone/insert instead of silently dropping a member.
  • loop-50, iteration 5 (#829): BACnetPrimitiveCharSTRING::IsEqual now compares the declared character-set encoding, not just the raw bytes.
  • loop-50, iteration 5 (#831): BACnetPrimitiveBitSTRING::Decode now rejects a nonzero undefined padding bit in the final octet (cl. 20.2.10).
  • loop-50, iteration 5 (#832): BACnetAbstractSyntaxType::operator= now fails atomically instead of leaving a corrupted “valid” copy when a child Clone() fails.
  • loop-50, iteration 5 (#847): BACnetDeviceCommunicationControlRequest::SetDeviceCommunicationControl now resets stale optional fields on reuse; Decode now distinguishes a present-but-malformed optional field from a genuinely absent one.
  • loop-50, iteration 5 (#853): BACnetDeviceObjectPropertyValue::SetDeviceIdentifier/Decode now reject a device-identifier that is not object type=device.
  • loop-50, iteration 5 (#859): BACnetObjectPropertyValue::SetPriority/Decode now enforce the 1-16 domain; SetComplete now requires all required fields to be valid.
  • loop-50, iteration 6 (#833): Read/WriteAccessSpecification and ReadAccessResult now require a valid mandatory identifier (and, for the specifications, a nonempty list) before reporting valid; their Clone() now fails atomically on a failed child clone.
  • loop-50, iteration 6 (#834): BACnetReadResult::SetPropertyValue now rejects NULL; BACnetPropertyAccessResult now requires both identifiers plus a valid CHOICE before validating; BACnetReadPropertyMultipleACK::Clone now fails atomically on a failed child clone.
  • loop-50, iteration 6 (#860): BACnetTextMessage::Decode now distinguishes a present-but-malformed optional Message_Class field from a genuinely absent one.
  • loop-50, iteration 6 (#861): TestBACnetDataLinkIpv4’s shared fixture now initializes BACnetNetworkLayer’s own m_database, fixing a latent null-deref for any unconfirmed-request test.
  • loop-50, iteration 6 (#862): BACnetEnrollmentSummary::Decode now peeks the application tag before deciding whether a trailing notificationClass is absent (next list entry) or present-but-malformed, without reopening the #684/#862 multi-entry regression; BACnetGetEnrollmentSummaryACK::Decode also now correctly reports invalid on a decode/insert failure instead of leaving Reset()’s “empty ACK is valid” state standing.
  • loop-50, iteration 9 (#873): BACnetLandingCallStatus::Decode now distinguishes a present-but-malformed optional FloorText field from a genuinely absent one.
  • loop-50, iteration 9 (#874): BACnetActionCommand::Decode now distinguishes a present-but-malformed optional field (deviceIdentifier, propertyArrayIndex, priority, postDelay) from a genuinely absent one.
  • loop-50, iteration 9 (#875): BACnetLightingCommand::Decode now distinguishes a present-but-malformed optional field (targetLevel, rampRate, stepIncrement, fadeTime, priority) from a genuinely absent one.
  • loop-50, iteration 9 (#876): BACnetPropertyStates::operator= now fails atomically when the child Clone() call fails instead of copying rhs.IsValid() onto a NULL m_propertyState.
  • loop-50, iteration 14 (#877): BACnetTimerStateChangeValue::operator= now fails atomically when the child Clone() call fails instead of copying rhs.IsValid() onto a NULL m_value.
  • loop-50, iteration 14 (#878): fixed adapters/python/CASBACnetStackAdapter.py’s bacnet_propertyIdentifier["zoneto"] (was 32, colliding with effectiveperiod; now the correct 321).
  • loop-50, iteration 14 (#879): adapters/csharp/CASBACnetStackAdapter.cs now marks 4 scalar bool callback parameters [In, MarshalAs(UnmanagedType.I1)], matching the file’s own established convention for every other such parameter and the native bool’s 1-byte width.
  • loop-50, iteration 14 (#880): removed 13 dead BACnetStack_RegisterCallback* overloads in adapters/java/src/main/java/com/chipkin/bacnet/ICASBACnetStackLibrary.java that declared the wrong native parameter list; the correct single-Callback-typed overload for each remains. (A further ~27 methods have only the wrong-signature version with no correct fallback at all — filed separately as #881, out of scope for this fix.)
  • loop-50, iteration 18 (#882): adapters/rust/cas_bacnet_stack_adapters.rs’s register_callback_receive_message callback type now has the correct 8 parameters matching the native FPCallbackReceiveMessage, instead of 6.
  • loop-50, iteration 18 (#883): BACnetRegisterForeignDevice::Encode now returns 0 when maxBufferLength is too small instead of logging and encoding anyway, which wrote past the caller’s buffer.
  • loop-50, iteration 18 (#884): BACnetSCHeaderOption::SetHeaderAsProprietary now rejects a proprietaryHeaderDataLength that would overflow its internal uint16_t length sum instead of undersizing the allocation and overflowing it.
  • loop-50, iteration 21 (#885): fixed an infinite-loop (uint8_t loop counter against a uint16_t/size_t entry count) in BACnetReadForeignDeviceTableAck::SetFDTEntries, BACnetReadBroadcastDistributionTableAck::SetBDTEntries, BACnetWriteBroadcastDistributionTable::SetBDTEntries, and BACnetBBMD::GetBroadcastDistributionTableEntryIndex for tables/lists with more than 255 entries.
  • loop-50, iteration 21 (#886): BACnetDeviceObjectPropertyValue, BACnetNameValue, BACnetNPDU, BACnetObjectPropertyValue, and BACnetGroupChannelValue’s operator= now fail atomically when the child Clone() call fails instead of copying rhs.IsValid() onto a NULL member.
  • loop-50, iteration 21 (#887): BACnetSequenceOf::operator= now aborts the whole copy (and reports invalid) when a child Clone()/Insert() fails, instead of silently dropping that entry and leaving the copy shorter than the source with no signal.
  • Batch 116-C (#778/#787): new BACnetPrimitiveUINT64 primitive datatype (Unsigned64, cl. 20.2.4) - source/BACnetPrimitiveUINT64.{h,cpp}.
  • Batch 116-C, sprint 236 (#647/#652/#654/#655/#656/#657/#659/#660/#661): NaN/Inf coercion, rollback, and validation fixes across the Lighting Output, Binary Lighting Output, Program, Channel Value, and Load Control engines.
  • Batch 116-C, sprint 237 (#683/#686/#689/#697/#698/#699/#748/#749/#750): Schedule/date-time/time-value/priority-array datatype fixes, including the full primitive-datatype write fan-out for Time_Value/Weekly_Schedule/Schedule_Default.
  • Batch 116-C, sprint 238 (#696/#704/#706/#707/#719/#746/#728/#730/#667): event/fault algorithm registry race fix, SetStatusFlags(NULL) clearing, absent-vs-malformed decode fix for BACnetNotificationParametersAccessEvent, sequence-of caps for characterstring/extended fault algorithms, AnalogOutput property-profile TimeStamp fields, BACnetDBDevice::GetPropertyArrayLength for #667’s array-length query.
  • Batch 116-C, sprint 239 (#714/#716/#717/#720/#721): Access Zone/Credential/Point validation and overflow fixes, PASSBACK_DETECTED reporting.
  • Batch 116-C, sprint 240 (#747/#766/#779/#781/#785/#792/#795/#813): BACnetStackTrendLog inverted SAFE_DELETE guard, auditLog added to DeleteObject’s engine-map cascade, dead BACnetStackListItems.h removed, BACnetStackNotificationForwarder::Drain retains failed deliveries, COV-notification-multiple build loops now leave a per-item failure queued instead of aborting the whole build.
  • Batch 116-C, sprint 241 - audit/authorization family hardening (#776/#778/#782/#784/#786/#787/#791/#794/#797/#798/#799/#800/#801/#802/#767): comment-encoding accessors instead of a hardcoded UTF-8 assumption; AuditLogQuery sequence-number/Total_Record_Count widened to Unsigned64; stale-optional-leak and target-priority range fixes in BACnetAuditLogQueryParameters; BACnetAuditLogRecord/BACnetAuditLogQueryACK/BACnetAuthRequest/BACnetAuthRequestACK/BACnetAuthorizationPolicy/BACnetAuthorizationEvent validity now requires every mandatory field, not just the last setter called; BACnetStackAuditLog property-read allocation-failure propagation and invalid-record rejection; AuditLogQuery device-address/property-identifier/array-index/target-priority filters now applied; BACnetSuccessFilter/BACnetAuthorizationPosture range validation; BACnetAuditNotification::Decode absent-vs-malformed fix across all 14 optional fields (CR-096 family, #638 continuation) plus 5 more files in the same family; BACnetAuditPriorityFilter fixed-16-bit-length enforcement.
  • Batch 116-C, sprint 242 close (found by the batch’s first real verification build - build-light mode deferred all builds until this point): BACnetDBDevice/BACnetBusinessLogic friend-access fix for sprint 237’s per-datatype write fan-out; BACnetBusinessLogic::SetProperty rejected every write to an Any-typed property (e.g. Schedule_Default) because its generic datatype-match check had no carve-out for BACnetStackDatatypes::Any.
  • Batch 118, sprint 230 (#773): BACnetPacket::Decode/DecodeUsingSupportedServices now reject a truncated ordinary NPDU instead of reading one byte past the caller’s buffer.
  • Batch 118, sprint 230 (#808): BACnetPacket::Clone (base/IPv4/MS-TP) now propagates a failed APDU clone instead of forcing the clone’s validity to the source’s.
  • Batch 118, sprint 230 (#811): BACnetIAmRouterToNetwork/RouterBusyToNetwork/RouterAvailableToNetwork::AddNetworkNumbers now reject a NULL array with a nonzero count instead of dereferencing it.
  • Batch 118, sprint 230 (#812): BACnetStackSentConfirmedRequests::InsertRequest now rejects a duplicate (invoke-id, destination) key instead of leaking the new value while silently keeping the old entry.
  • Batch 118, sprint 230 (#814): BACnetNetworkParameters::SetDestinationAddress/SetSourceAddress now clear the stored address on a NULL/zero-length input instead of retaining the prior one.
  • Batch 118, sprint 230 (#815): BACnetSourceAddress::SetPrimeDevice now returns true on success; SetSourceAddressFromDeviceInstance now replaces rather than concatenates.
  • Batch 118, sprint 230 (#809): BACnetWhoIsRequest/BACnetWhoHasRequest::Decode now enforce the 22-bit device-instance maximum.
  • Batch 118, sprint 231 (#741): local NPDU processing is now bounded by the declared BVLC Length REVERTED at close (see “Roadblocked / deferred” below) - broke cited BTL conformance tests whose hex vectors have a stale declared-length field.
  • Batch 118, sprint 231 (#804): Read-Broadcast-Distribution-Table-Ack/Read-Foreign-Device-Table-Ack are now surfaced through new test-tool-only hooks (BACnetStackTestTool_RegisterHookReadBroadcastDistributionTableAck/RegisterHookReadForeignDeviceTableAck) instead of being silently discarded.
  • Batch 118, sprint 231 (#439 residual): BACnetInterface::SetBACnetSCSettings now validates all six inputs before touching any BACnet/SC data link instance, instead of potentially applying a rejected call’s earlier, already-validated values to some instances and not others.
  • Batch 118, sprint 232 (#324): hub-connector and direct-connect BACnet/SC ingress now supply the hub’s/peer’s VMAC as the link peer, so #322’s held-message quota and Reject-Message-To-Network re-keying apply there as they already do on hub-function ingress.
  • Batch 118, sprint 233 (#774): BACnetPrimitiveCharSTRING::SetValue/SetEncoding now reject a reserved character-set encoding, matching Decode.
  • Batch 118, sprint 233 (#775): BACnetTag::Encode now checks buffer capacity against the full encoded size before writing, instead of only before the extended tag-number octet.
  • Batch 118, sprint 233 (#777): BACnetPrimitiveOctSTRING::Decode no longer narrows the declared length to uint16_t before the bounds check.
  • Batch 118, sprint 233 (#780): BACnetDestination::SetDestination now correctly reports invalid on every rejected recipient CHOICE.
  • Batch 118, sprint 233 (#783): BACnetActionCommand/BACnetActionList/BACnetAccessToken copy-assignment now reflects a failed child clone in the copy’s validity instead of the source’s.
  • Batch 118, sprint 233 (#788): BACnetEventNotificationSubscription now rejects a zero time-remaining directly instead of relying only on a secondary list-service check.
  • Batch 118, sprint 233 (#789): BACnetHostAddress::SetIpAddress/Decode now enforce the 4-or-16-octet ip-address length.
  • Batch 118, sprint 233 (#790): BACnetRecipient::SetDevice/SetAddress now reject an invalid child instead of marking the parent valid unconditionally.
  • Batch 118, sprint 233 (#793): BACnetDestination::SetRecipientAddress now validates its length parameter instead of silently narrowing it.
  • Batch 118, sprint 233 (#796): BACnetPortPermission::SetPortId now enforces its Unsigned8 range and both setters recompute validity automatically.
  • Batch 118, sprint 233 (#807): BACnetCovValue::Clone/SetPropertyValueAndTakeControl now handle a NULL property value instead of crashing/accepting it.
  • Batch 118, sprint 233 (#810): BACnetAbstractSyntaxType::DecodeString/InsertEncodedObjectAndCleanUpMemory now propagate/gate on the actual insertion result.
  • Batch 118, sprint 233 (#816): BACnetPropertyValue::Clone/SetValue now check for a failed child clone and release the previously owned value before replacing it.
  • Batch 118, sprint 233 (#803): BACnetSubscribeCOVPropertyMultipleRequest/BACnetCovSubscriptionSpecification now require their mandatory identifier field, not just a successful list insertion, for validity.
  • Batch 118, sprint 233 (#805): BACnetUnconfirmedCOVNotificationMultipleRequest::GetDatatype now returns the correct enum value; both COV-notification-multiple IsEqual implementations now compare optional-timestamp presence symmetrically.
  • Batch 118, sprint 233 (#684): BACnetEnrollmentSummary/BACnetCreateObjectRequest::Decode now distinguish an absent optional field from a present-but-malformed one.
  • Batch 118, sprint 233 (#767 continuation): applied the same absent-vs-malformed fix to 5 more sites (BACnetReadPropertyRequest, BACnetReadRangeRequest, BACnetReadRangeACK x2, BACnetReadPropertyAck).
  • Batch 118, sprint 234 (#820, #152 part b): the segmented-response retained-transaction-state clone now converts to the memory-buffer form before cloning, instead of after, avoiding a full decoded-object-graph clone.
  • Batch 118, sprint 234 (#821, CR-701): BACnetStackMemoryBuffer::Clone no longer leaks the clone object on a failed insert.
  • Batch 118, sprint 234 (#821, CR-702): BACnetStackRespondingStateMachineCurrentState::InsertSegmentAndTakeControl now rejects a duplicate sequence number instead of leaking the caller’s already-transferred segment.
  • #741 (batch 118, close): the declared-BVLC-Length clamp on local NPDU processing was reverted after the close build’s collect-all-failures pass found it broke DeviceCommunicationControlBTLTests.9_24_1_8/9/10 (cited BTL conformance tests) plus several other tests, all sharing the same root cause: pre-existing hand-written hex fixtures whose declared BVLC Length field does not match their actual byte count. Per the batch’s rule (a fix breaking a cited BTL/conformance test is reverted, matching batch 115’s precedent on this exact issue), reverted rather than auditing every affected fixture under this close’s time budget. Re-noted on #741.
  • #643 (batch 118): a network-number → data-link-type resolver for address-form Notification Class recipients - the fix belongs in this batch’s BACnetNetworkParameters.cpp, but its only caller is BACnetStackMessageGenerator.cpp, owned by the concurrent batch 116-C. Full accessor-chain analysis posted on the issue.
  • #625 (batch 118): adapter DecodeAs samples + smoke tests, deferred under this batch’s wall-clock budget.
  • Batch 116-B, sprint 224 (#649): BACnetListOfWriteAccessSpecifications’s Insert path had a missing NULL guard, fixed with a discriminating test.
  • Batch 116-B, sprint 224 (#651): BACnetCreateObjectProcessor/BACnetWritePropertyMultipleProcessor’s processor-internal OOM guards on new(std::nothrow) response allocations now surface a clean error instead of a NULL dereference (the dispatcher-side guards for the same defect were already fixed by batch 116-A’s #682).
  • Batch 116-B, sprint 224 (#667): BACnetReadPropertyMultipleProcessor::EstimateResponseLength’s ALL/REQUIRED/OPTIONAL preflight now expands against the device’s real property lists and scales with the actual property count, instead of pricing the whole expansion as a single small primitive (which produced abort-instead-of-segment behaviour for large objects). Close-round tuning: the per-expanded-property estimate uses a flat default rather than each property’s theoretical character-string maximum, which was itself overestimating and wrongly aborting small, ordinary reads.
  • Batch 116-B, sprint 224 (#671): BACnetStackWritePropertyAsync::ToBytes gained a bounds check.
  • Batch 116-B, sprint 224 (#674): BACnetWritePropertyRequest/BACnetWritePropertyMultipleRequest’s setters now reject a NULL propertyValue.
  • Batch 116-B, sprint 224 (#675): BACnetWritePropertyRequest::Decode now has an else branch for a missing mandatory tag instead of falling through silently.
  • Batch 116-B, sprint 224 (#681): BACnetReadPropertyProcessor now sets the error container on an OOM response-PDU allocation instead of leaving it unset.
  • Batch 116-B, sprint 224 (#687): BACnetStackCreateObjectAsync::ToBytes gained a bounds check.
  • Batch 116-B, sprint 224 (#673): BACnetPropertyValue::Clone now NULL-checks its allocation.
  • Batch 116-B, sprint 224 (#751): BACnetPropertyReference/BACnetPropertyValue::IsEqual are now symmetric.
  • Batch 116-B, sprint 225 (#662): BACnetCreateObjectRequest::Clone now copies the object specifier.
  • Batch 116-B, sprint 225 (#690): BACnetCreateObjectProcessor’s auto-instance generator now rejects with Error(object, noSpaceForObject) at the 22-bit instance ceiling instead of wrapping into the reserved wildcard instance.
  • Batch 116-B, sprint 225 (#691): a failed CallbackDeleteObject during CreateObject rollback is now logged and surfaced as Error(object, other) instead of being silently ignored.
  • Batch 116-B, sprint 225 (#734): re-verified as working-as-designed; no behaviour change.
  • Batch 116-B, sprint 225 (#752): BACnetDeviceObjectPropertyReference now rejects a 5th-or-later compact-string field instead of accepting it silently.
  • Batch 116-B, sprint 226 (#653): BACnetDeviceCommunicationControlProcessor/BACnetReinitializeDeviceProcessor now complete every admissibility check (password, state, DCC-disabled override) before any host callback fires, instead of after.
  • Batch 116-B, sprint 226 (#658): the UTCTimeSynchronization path now records an EventLog clock-change entry, matching the local-time TimeSynchronization path’s existing behaviour.
  • Batch 116-B, sprint 226 (#663): BACnetDeviceCommunicationControlRequest::operator= now clears fields rhs does not have, instead of leaving stale prior state.
  • Batch 116-B, sprint 226 (#664): a TimeSynchronization/UTCTimeSynchronization request carrying a DATE pattern value (a wildcard day/month/year/weekday) is now rejected with Reject(parameterOutOfRange).
  • Batch 116-B, sprint 226 (#665): BACnetUnconfirmedPrivateTransferRequest now NULL-checks its optional service parameters before use.
  • Batch 116-B, sprint 226 (#666): BACnetTextMessage::IsEqual (both Confirmed and Unconfirmed variants) fixed a cross-base cast defect.
  • Batch 116-B, sprint 226 (#676): BACnetGetEventInformationACK’s MoreEvents field is now correctly cloned and compared.
  • Batch 116-B, sprint 226 (#677): BACnetGetEventInformationACK/BACnetGetAlarmSummaryACK/BACnetGetEnrollmentSummaryACK fixed an offset-underflow defect.
  • Batch 116-B, sprint 226 (#678, #679): BACnetGetEventInformationProcessor’s pagination fixed a continuation-marker resolution defect and a premature MoreEvents flag across Event Enrollment objects.
  • Batch 116-B, sprint 226 (#680): BACnetGetEventInformationRequest::Reset now correctly handles the zero-length case.
  • Batch 116-B, sprint 226 (#693): the vendor ID field is now capped to Unsigned16 across the affected request/ACK classes.
  • Batch 116-B, sprint 226 (#700): BACnetWhoHasRequest now rejects a zero-length name/range.
  • Batch 116-B, sprint 226 (#701): BACnetWhoHasRequest::Decode now range-checks its device-instance-range fields.
  • Batch 116-B, sprint 226 (#702): a ReinitializeDevice request with ReinitializedStateOfDevice > 7 is now rejected with Reject(parameterOutOfRange) at decode.
  • Batch 116-B, sprint 226 (#711): BACnetUTCTimeSynchronizationProcessor::CalculateTimeUsingUTC now uses timegm()/gmtime() (host-timezone-independent) instead of mktime()/localtime(), which produced a different result depending on the deployment host’s configured timezone.
  • Batch 116-B, sprint 226 (#712): BACnetDeviceCommunicationControlRequest::IsEqual is now symmetric.
  • Batch 116-B, sprint 226 (#713): BACnetIAmRequest/BACnetIHaveRequest now validate the object type is device.
  • Batch 116-B, sprint 227 (#685): BACnetSubscribeCOVPropertyMultipleProcessor now validates every entry in a multi-entry SubscribeCOVPropertyMultiple request before committing any of them, instead of committing entries as they validate (leaving earlier entries subscribed when a later one fails).
  • Batch 116-B, sprint 227 (#688): a COV_Increment that is NaN or negative is now rejected with Error(property, valueOutOfRange).
  • Batch 116-B, sprint 227 (#694): BACnetGetEnrollmentSummaryProcessor’s enrollment filter now matches a NotificationClass recipient in address form, not only device-object-reference form.
  • Batch 116-B, sprint 227 (#708): BACnetReadRangeACK::SetEmptyResponse now clears stale item data from a previous fill.
  • Batch 116-B, sprint 227 (#759): BACnetSubscribeCOVRequest/BACnetSubscribeCOVPropertyMultipleRequest::IsEqual fixed a missing negation on several field comparisons.
  • Batch 116-B, sprint 227 (#760): BACnetStackActiveCOVSubscriptions::CancelSubscription fixed an operator-precedence defect (?: binding looser than &&) that defeated its service/array-index match discrimination.
  • Batch 116-B, sprint 227 (#761): BACnetStackCOVSubscription::SetValue now preserves the prior value on a failed clone instead of reporting success with a NULL value; BACnetStackActiveCOVSubscriptions::AddOrUpdateSubscription’s renewal path now surfaces that failure instead of discarding it.
  • Batch 116-B, sprint 227 (#762): the COV threshold comparison for integer/positive-integer properties now computes its delta in a widened type, fixing a signed-overflow/truncation defect near the type’s extrema.
  • Batch 116-B, sprint 227 (#763): BACnetStackActiveCOVSubscriptions’s per-key bucket allocation is now NULL-checked before insertion.
  • Batch 116-B, sprint 227 (#672): BACnetStackActiveCOVMultipleSubscriptions now treats a configured max of 0 as unbounded and no longer rejects a renewal of an existing subscription at a saturated bound.
  • Batch 116-B, sprint 227 (#668, #669): the COVNotificationMultiple request family now checks SetTimestamp/child-clone return values instead of silently reporting success (or a truncated clone) on failure.
  • Batch 116-B, sprint 228 (#670): BACnetFileAccessParameters/BACnetFileAccessMethodParameters::Decode now correctly record which CHOICE arm (stream vs record access) was decoded, instead of always reading back the Reset() default.
  • Batch 116-B, sprint 228 (#695): BACnetAtomicWriteFileProcessor’s stream-access branch now rejects a File Start Position below -1, matching its record-access sibling’s existing check.
  • Batch 116-B, sprint 228 (#703): BACnetInterface::SendAtomicWriteFile’s record-access branch now rejects a NULL fileData with a nonzero length instead of null-dereferencing.
  • Batch 116-B, sprint 228 (#705): BACnetInterface::SendAtomicReadFile now rejects a fileStart above INT32_MAX instead of silently reinterpreting it as negative. (The CASBACnetStackDLL.h doc-comment half of this issue is out of ownership scope for every batch and was split to #817.)
  • Batch 116-B, sprint 228 (#735): BACnetAtomicReadFileProcessor’s stream-access branch now clamps the host callback’s reported data length to requestedCount.
  • Batch 116-B, sprint 228 (#739): BACnetInterface::SendRegisterForeignDevice now validates connectionStringLength before sending, instead of sending first and discovering the mismatch afterward.
  • Batch 116-B, sprint 228 (#745): BACnetBVLLBDTEntry’s constructor now honours the caller-supplied ipAddressLength instead of assuming a fixed length; every BDT/FDT entry accessor now NULL-checks its pointer parameter; BACnetInterface::AddBDTEntry/GetBDTEntry now reject NULL buffers at the ABI boundary.
  • Batch 116-B, sprint 228 (#764): the buffer==NULL early-return path in Decode() now invalidates the object across the Atomic File / File Access family, instead of leaving a prior successful decode’s validity untouched (mirroring the existing BACnetFileRecordAccessData fix).
  • Batch 116-B, sprint 228 (#765): BACnetAtomicReadFileProcessor/BACnetAtomicWriteFileProcessor::Process now reject a NULL outgoingPDU before invoking any file callback, instead of crashing after the callback’s side effects have already run.
  • Batch 116-B, sprint 229 close (no issue; found by this batch’s own close-phase build): BACnetDeviceCommunicationControlRequest::operator= gained a self-assignment guard, per this codebase’s standing convention for a destructive operator=.
  • Batch 115, sprint 217 (#495 phase 1, #98, #99): NM-CE-A send-side codecs. New BACnetEstablishConnectionToNetwork / BACnetDisconnectConnectionToNetwork classes (cl. 6.4.9/6.4.10) wired into the BACnetNetworkLayerProtocolMessage Factory, replacing the “not yet implemented” panic for message types X’08’/X’09’ with real decode support. Test-tool-only sends BACnetStackTestTool_SendEstablishConnectionToNetwork / ..._SendDisconnectConnectionToNetwork (CASBACnetStackTestToolDLL.h) close #98/#99. The EXECUTE side (a half-router actually bringing up/tearing down a PTP connection) is out of scope; the receive dispatch still takes no action for either message type, preserving 135.1-2023 10.6.3’s silent-ignore requirement.
  • Batch 115, sprint 217 (#496 slice): BACnetStackTestTool_GetRouterRouteCount / ..._GetRouterRoute (test-tool header, per planner decision 2 — customer-header promotion stays the owner’s question) read back the router’s learned/configured routing table (BACnetPhysicalRouter::GetRouteCount/GetRoute, previously unexported).
  • Batch 115, sprint 217 (#732): CORRECTED AT BATCH CLOSE. The planner’s 2026-09-03 ruling proposed unifying notYetImplemented and reservedByAshrae on silent-ignore for both; implemented in sprint 217, then reverted when the batch’s one close-phase full test run surfaced a direct conflict with BACnetVirtualGatewayDeviceTests.135_1_2013_10_8_2_2_2_UnknownNetworkLayerMessageType — an explicitly-cited BTL conformance test (cl. 10.8.2.2.2) requiring an IUT to answer a reserved (0x14-0x7F) message type with Reject-Message-To-Network reason 3, and by ForwardedNPDUNetworkLayerTests.Issue461_TheRejectIsAddressedToTheOriginatingDeviceNotTheForwarder. The original (pre-batch-115) split turns out to satisfy both cited BTL procedures at once: notYetImplemented stays silent (cl. 10.6.3, pinned by Test_135_1_2013_2_1_1_10_6_3_IgnoreEstablishConnectionToNetwork); reservedByAshrae sets unknownNetworkMessage and triggers the reject (cl. 10.8.2.2.2). Restored to that split; CR-159’s original “inconsistency” finding was two BTL procedures making two different, deliberate demands of the two statuses, not a defect.
  • Batch 115, sprint 217 (#753): BACnetWhoIsRouterToNetwork::operator= did nothing when assigned a default (unqualified/broadcast) request, leaving a reused, previously-targeted object’s stale network number in place instead of clearing it.
  • Batch 115, sprint 217 (#754): BACnetIAmRouterToNetwork / BACnetRouterBusyToNetwork / BACnetRouterAvailableToNetwork::Decode appended decoded DNETs onto whatever the object already held instead of replacing them (including the syntactically-empty whole-router form for the latter two), so a reused decode object retained a previous message’s network numbers.
  • Batch 115, sprint 217 (#757): BACnetInitializeRoutingTable::Decode appended newly decoded port entries onto a reused object’s stale m_ports instead of replacing them; Encode/GetPort (which iterate m_ports[0..m_numberOfPorts-1]) then reported the OLD entries at the front of the vector rather than the new ones. Decode now builds into a local vector and only assigns it into the object once the whole message has decoded successfully, which also leaves a partial/failed decode’s previous state untouched.
  • Batch 115, sprint 217 (#758): BACnetInitializeRoutingTable::AddPort/AddPorts now refuse to grow past 255 entries instead of letting m_numberOfPorts (a wire uint8_t) silently wrap to 0 while m_ports kept growing, which would have desynchronized the encoded port count from the in-memory table.
  • Batch 115, sprint 218 (#738): a BVLC-Result now only confirms a pending foreign-device registration when it arrives from the specific BBMD the registration was sent to. BACnetDataLinkIPv4::ProcessBVLLResult previously gated purely on internal state (m_sentRegisterForeignDevice), so any host on the network path could spoof a Success/Nak response and this device would accept it as resolving the pending registration regardless of who actually sent it — a false-positive registration state reachable from unauthenticated, connectionless UDP.
  • Batch 115, sprint 218 (#740): BACnetBBMD::ProcessOutgoingMessageBBMD’s same-subnet Forwarded-NPDU loop-suppression (J.4.5) now returns true (a successful no-op), matching every other “nothing to do” early-out in that function. It previously returned false, which the caller treats identically to a genuine encode/send failure (internalError) — short-circuiting before the J.2.5 originating-device rewrite and local NPDU delivery that follow it, risking dropped local delivery for every same-subnet Forwarded-NPDU a BBMD legitimately, correctly ignores. Batch-close confirmation: the close-phase full test run showed this fix’s own intended effect for the first time — BACnetBBMDTests.cpp’s ForwardedNPDUFromSameSubnetDifferentPortPeer_IsNotReBroadcastLocally had (by the bug’s own side effect) been asserting that IUT sends nothing at all in this scenario; now that local delivery correctly proceeds, IUT answers the embedded Who-Is with its own I-Am broadcast, which is the fix working as intended. Updated that test’s assertion to what J.4.5 actually protects — IUT must not redistribute the same Forwarded-NPDU byte-for-byte back onto the local subnet (the loop) — rather than asserting IUT never responds to anything inside it.
  • Batch 115, sprint 218 (#741): ATTEMPTED, THEN REVERTED AT BATCH CLOSE. Tightened BACnetBVLLBase::Decode to reject a declared BVLC Length shorter than the received datagram (not just longer), aiming to make the declared length authoritative end-to-end and close the local-processing-vs-BBMD-forward-path disagreement CR-185 describes. The batch’s one close-phase full test run showed this broke roughly two dozen existing tests across several files (TestBACnetInterface_Hooks.cpp’s _WithPadding cases, RenderJSON/XMLRenderer, several BACnetInterface*Test suites) that deliberately pad a frame past its declared length — legitimate per Annex J, which does not forbid trailing octets (e.g. minimum-Ethernet-frame-size padding). Reverted to the original tolerant check (declared length may be shorter, never longer, than the datagram); CR-185’s actual gap (local NPDU processing and the BBMD forward paths can still disagree about where the message ends) remains open, needs the OTHER fix the issue named — clamping local processing to the declared length — as separate, not-yet-attempted work.
  • Batch 115, sprint 218 (#755): GetAttributes in BACnetDistributeBroadcastToNetwork/BACnetForwardedNPDU/BACnetOriginalBroadcastNPDU/BACnetOriginalUnicastNPDU formed &vector[0] unconditionally when rendering (STACK_OPTION_STRING_LITERALS), which is undefined behavior on an empty vector (a default-constructed or Reset() message) even though the resulting pointer was never dereferenced beyond the length-guarded helper it fed. All four now use .data(), which is well-defined for an empty vector.
  • Batch 115, sprint 218 (#756): BACnetForwardedNPDU::SetIpAddress/GetIpAddress/GetNPDU and the sibling GetNPDU methods on BACnetOriginalBroadcastNPDU/BACnetOriginalUnicastNPDU/BACnetDistributeBroadcastToNetwork dereferenced a caller-supplied buffer with no NULL check whenever the accompanying length satisfied the length check — a NULL pointer with a “long enough” length crashed via memcpy/an indexed write instead of being rejected as an invalid parameter, the standard convention already used elsewhere in the stack.
  • Batch 115, sprint 218 (#626, #627): re-verified at the tip — both already fixed by batch 114 (STACK_OPTION_MAX_FOREIGN_DEVICE_TABLE_ENTRIES + NAK 0x0030 for #626; BDT dedup by (address, mask) + identity-based host-index re-derivation for #627). No further change needed.
  • Batch 115, sprint 219 (#556, “#304 for BACnet/IP”): BACnetDataLinkLayer now holds an array of BACnetDataLinkIPv4 instances (STACK_OPTION_MAX_DATA_LINK_INSTANCES_IPV4, default 2 on TARGET_FULL / 1 otherwise), mirroring #304’s BACnet/SC design, plus BACnetDataLinkIPv4::TrackNetworkPort/IsTrackingNetworkPort/HasNetworkPort and BACnetDataLinkLayer::FindIpv4DataLinkByNetworkPort/ClaimIpv4DataLinkForNetworkPort. New test-tool-only export BACnetStackTestTool_SetNetworkPortIPv4DataLinkInstance binds a Network Port object to a claimed instance. Scope, stated plainly: this is bookkeeping/scaffolding — instance 0 is the historical single datalink (GetIpv4DataLink() no-arg is byte-for-byte unaffected; every existing caller, including BACnetInterface.cpp, is untouched), and there is currently no per-port wire-receive dispatch: an incoming datagram is still always processed by instance 0, because the host callback ABI carries no port identity to dispatch an incoming packet by. CASBACnetStackDLL.h is untouched; the production binding API shape stays #556’s own open owner question.
  • Batch 115, sprint 220 (#613): scPrimaryHubConnectionStatus/scFailoverHubConnectionStatus now return the never-connected placeholder value instead of a valueNotInitialized error. BACnetSCHubConnection::Reset() already leaves the object well-formed (hub-uri="", connection-state=SCConnectionState_NotConnected, both mandatory fields set) — the property getter’s own guard was refusing a value the class could already encode correctly the whole time.
  • Batch 115, sprint 221 (#768): MSTP_MasterNodeStateMachine_UseToken’s CUSTOM_FRAME branch now transitions to MSTP_STATE_DONE_WITH_TOKEN on a failed send regardless of the untransmitted frame’s expecting-reply bit, instead of still entering MSTP_STATE_WAIT_FOR_REPLY for a reply that was never solicited (a reply can only be pending for a frame that actually reached the wire) — previously cost a full Treply_timeout window before the state machine gave up and retried. #723/CR-176’s fix (the frame stays queued on a failed send) is unaffected and re-asserted as a liveness check in the new test.

Roadblocked (needs-human-review / follow-up work)

Section titled “Roadblocked (needs-human-review / follow-up work)”
  • Batch 115, sprint 219 (#439 residual): splitting BACnetDataLinkSC::m_hubConnector’s device-wide BVLC/NPDU-limit fields out into a shared BACnetSCDeviceWideSettings struct requires changing BACnetInterface::SetBACnetSCSettings (source/BACnetInterface.cpp) to write it once instead of looping every instance — that file is outside this batch’s file-ownership list (owned by batch 116-B). Not attempted; needs a batch that owns BACnetInterface.cpp, matching why batch 114 also could not complete it.
  • Batch 115, sprint 219 (#363 residual): node-adapter wiring for BACnetStackTestTool_RegisterHookInitializeRoutingTableAck (marshalling the parallel uint16_t*/uint8_t* arrays + concatenated portInfo through a hand-written N-API callback trampoline in adapters/node/@chipkin/cas-bacnet-stack/src/interface_callbacks.cc) is hand-written glue code the generate-node-adapter.js/generate-cpp-adapter.js scripts do not produce, and cannot be safely verified without a Node.js addon build — out of scope for this batch’s build-light, no-build-until-close discipline. The automated node-adapter drift/coverage/parity gates do not flag its absence (they check the generated TS surface’s internal self-consistency, not header-vs-addon totality), so this residual is not currently gated at all. Recommend a dedicated future sprint with node build tooling available.
  • Batch 115, sprint 220 (#324): investigated fully — BACnetSCHubConnector/BACnetSCHubConnector_Incoming.cpp tracks no VMAC for the hub it is connected to at all (unlike the hub-function ingress path, which resolves sourcePeer->vmac from its own accepted-peer table). Setting a link peer on hub-connector/direct-connect ingress “the way the hub-function path does” therefore requires first capturing and storing the hub’s VMAC from the Connect-Accept handshake — new state, not a one-line SetLinkPeerAddress call — which the issue’s own text anticipated (“that needs thinking through, not a one-liner”). Not attempted this batch; the finding is recorded on the issue for whoever picks up that state-tracking work.
  • Batch 115, sprint 220 (#643): investigated fully — fixing the IPv4-vs-BACnet/SC VMAC ambiguity in BACnetNetworkParameters::SetClientNetworkParameters(const BACnetAddress&) per the ruling (“recipient network type resolved from the network number via the data-link layer”) needs a network-number → data-link-type resolver that does not exist anywhere in the tree today (no ResolveNetworkType(dnet) equivalent) — it would need to check every compiled-in data link’s own directly-connected network number and fall back to the physical router’s routing table for anything not directly connected, which is new routing-resolution infrastructure, not a mechanical fix. BACnetStackMessageGenerator.cpp’s two call sites do have a dataLinkLayer pointer in scope, so the resolver would slot in cleanly once written. Not attempted this batch; recorded on the issue.
  • Batch 115, sprint 221 (#769): source/BACnetDataLinkMSTP.cpp read in full to determine whether MSTP.c’s engine is simply unused dead code, or whether MS/TP support is materially incomplete — it is the latter. BACnetDataLinkMSTP has zero references to any MSTP_-prefixed symbol; it processes an already-decoded NPDU buffer directly and calls the generic packet-level CallbackSendMessage/FPCallbackReceiveMessage ABI, the same one every other data link uses. MSTP.c’s public API (MSTP_Init) requires a completely different, BYTE-LEVEL host callback surface (mstp_SendByte/mstp_RecvByte/mstp_Sleep/mstp_TimerReset/mstp_TimerDifference/mstp_RS485EnableTXDPin — raw UART-style primitives, declared in source/MSTP.h), which nothing in the current BACnetCallbackInterface exposes. Wiring the two together therefore needs a new host-callback shape — this batch’s own instructions are explicit that this is a stop condition, not a batch decision. Not attempted; needs-human-review recorded on #769 with the two architectural options this implies (expose the byte-level MSTP.c callback surface as new BACnetStack_RegisterCallbackMSTP* exports and have this stack own MS/TP framing/token-passing end-to-end; or keep the current packet-level contract, in which case MSTP.c is dead code that should be removed rather than merely uncalled).
  • Batch 116-A, sprint 202 (#710, CR-171): MSTP_InsertFrameToSend’s capacity check was off by the frame-header size, so a payload in (493, 501] bytes passed the check and then overflowed the destination buffer by up to 8 bytes in the memcpy that copies it in after the reserved header gap. Separately, MSTP_SendFrame used to fall back to transmitting a zero-DataLength header when a payload did not fit the remaining header room, while the code below it still sent the real payload and CRC bytes on the wire — a self-inconsistent frame; that case is now a hard send failure (no bytes are transmitted) instead. Batch review found the first version of this fix left a gap: MSTP_InsertFrameToSend’s new bound (491) and MSTP_SendFrame’s pre-existing “does not fit” check disagreed by up to 8 bytes, so a payload just under the insert limit could be queued and then permanently refused by MSTP_SendFrame, occupying one of only 3 send-queue slots forever. Both checks are now reconciled to the same true safe bound (MSTP_MAX_PACKET_OCTETS - MSTP_FRAME_HEADER_LENGTH - 2, accounting for the trailing 2-byte DataCRC MSTP_SendFrame appends within the same buffer), verified end-to-end through the real queued-send path.
  • Batch 116-A, sprint 202 (#718, CR-175): a received frame with DataLength == MSTP_MAX_PACKET_OCTETS (501, the documented maximum) matched neither the “FrameTooLong” nor the “Data” receive-dispatch branch and fell into the “should never happen” panic case, abandoning the frame to IDLE without consuming its announced body/CRC bytes and desynchronizing the parser for the next frame. The Data branch now accepts the full documented range.
  • Batch 116-A, sprint 202 (#723, CR-176): a failed transport write (SendByte returning false) was ignored, and the higher-level frame being sent was dequeued anyway — silently dropping it with no retry. MSTP_SendFrame now returns whether the transport actually accepted the frame, and the queued-frame send path only dequeues on success.
  • Batch 116-A, sprint 202 (#725, CR-177/CR-178): PollStation’s token-recovery formula was ThisStation + 1 % (Nmax_master + 1), which — because % binds tighter than + — reduced to ThisStation + 1 with no wraparound at the ring boundary; it is now correctly parenthesized. MSTP_SetBaudRate’s bit-time-to-millisecond conversions divided two integers before applying ceil() (making ceil() a no-op and under-rounding every derived timing constant) and divided by zero for any baudrate < 1000; the division is now done in floating point and a zero baudrate is rejected instead of crashing.
  • Batch 116-A, sprint 202 (#727, CR-179): sending one higher-level (queued) frame set FrameCount straight to Nmax_info_frames instead of incrementing it, so a node configured for Max_Info_Frames > 1 never benefited from it for its own queued traffic — it always yielded the token after exactly one frame. FrameCount now increments per frame sent, like the other send arms.
  • Batch 116-A, sprint 202 (#729, CR-180): MSTP_Init’s station_id and MSTP_SetMaxMaster’s Max_Master were not validated against the documented 0-127 domain, which the token-ring modulo arithmetic assumes. MSTP_Init now rejects an out-of-range station_id; MSTP_SetMaxMaster clamps to 127.
  • Batch 116-A, sprint 202 (#731, CR-181): MSTP_CheckHeader never validated FrameType against a known/supported set, so an addressed frame with an ASHRAE-reserved (8-31, 34-127) FrameType passed header validation despite having no dispatch case anywhere in the stack. It is now rejected in CheckHeader.
  • Batch 116-A, sprint 202 (#733, CR-183): MSTP_SendFrame extracted the two-octet Length field’s bytes via a raw pointer cast over the uint16_t’s in-memory representation, which is only correct on a little-endian host. It now uses explicit >> 8 / & 0xFF extraction, verified with a byte-sequence-pinning test.
  • Batch 116-A, sprint 202 (#736, CR-184): Token / Poll For Master / Reply To Poll For Master / Reply Postponed frames — all defined to carry no data octets — were dispatched without checking DataLength == 0, so a malformed control frame carrying a payload was processed as legitimate. MSTP_CheckHeader now rejects a nonzero DataLength on these frame types.
  • Batch 116-A, sprint 204 (#709, CR-148): BACnetErrorPDU::operator= copied the owned m_error pointer verbatim instead of deep-cloning it, so two objects ended up sharing (and, on destruction, double-freeing) the same BACnetErrorBase. It now deep-clones.
  • Batch 116-A, sprint 204 (#715, CR-149): BACnetComplexACKPDU::SetApduMemoryBuffer inserted into the memory buffer without resetting it first, so a reused object’s payload was concatenated onto rather than replaced by a new assignment; operator= had the same gap for a reused destination switching between the memory-buffer and service-object forms. Both now reset first.
  • Batch 116-A, sprint 204 (#722, CR-150): a segmented ConfirmedRequestPDU/ComplexACKPDU’s proposed-window-size octet was assigned directly during decode, bypassing SetProposedWindowSize’s own bounds check, and that setter itself accepted 0 unconditionally. A segmented message’s proposed window is now required to be 1-127 (cl. 5.3/20.1.2.11); 0 stays legal for a non-segmented PDU, where the field is unused.
  • Batch 116-A, sprint 204 (#724, CR-151/CR-153): copying or cloning a decoded, parameterless getAlarmSummary ConfirmedRequestPDU (which has no service-request object, by design) silently lost which service the PDU represented, because operator=/Clone() only copied m_serviceChoice as a side effect of SetServiceRequest, which never ran when the request object is NULL. m_serviceChoice is now copied unconditionally.
  • Batch 116-A, sprint 204 (folded into #682, CR-152/CR-138/CR-131): the getAlarmSummary, getEnrollmentSummary, addListElement, and removeListElement dispatcher case blocks in BACnetConfirmedRequestProcessor.cpp dereferenced a failed nothrow allocation of their outgoing ACK PDU with no NULL check, unlike sibling case blocks (writeProperty, getEventInformation) that already carry the guard. All four now carry it; writePropertyMultiple’s dispatcher-side dereference of the processor’s *outgoingPDU output (CR-116’s dispatcher half) is now NULL-checked too.
  • Batch 116-A, sprint 204 (no new issue, CR-154): re-verified against closed issue #522/#523 — all six response-PDU classes’ Decode() already call Reset() before any check that can fail mid-decode; no further change needed.
  • Batch 116-A, sprint 204 (#737, CR-155/CR-160): none of the fixed- or variable-length response PDU decoders (SimpleACK, ComplexACK, Error, Reject, Abort, SegmentACK) or the NPDU network-message decoders (WhoIsRouterToNetwork, NetworkNumberIs) verified their decode consumed the entire supplied buffer, so trailing bytes past a valid PDU/message were silently ignored. Each now requires exact consumption, tolerating only zero-byte Ethernet padding at the PDU layer (matching BACnetConfirmedRequestPDU’s existing issue #9 allowance).
  • Batch 116-A, sprint 204 (#726, CR-156): BACnetConfirmedRequestPDU::SetServiceRequestAndTakeControl dereferenced its pointer parameter with no NULL check; SetServiceRequest freed this->m_serviceRequest before reading through its serviceRequest reference parameter, which is a use-after-free when a caller passes the PDU’s own current request back into itself. The former now NULL-checks; the latter now builds the replacement before freeing the old one.
  • Batch 116-A, sprint 204 (#732, CR-159): CONFIRMED (a spec-recognized-but-unimplemented network-layer message type is surfaced inconsistently with reservedByAshrae) but NOT fixed by unifying the two, as first attempted — that broke an existing BTL conformance test requiring a recognized-but-unimplemented message (e.g. establishConnectionToNetwork) to be silently ignored with no response at all. Reverted; the underlying inconsistency needs an owner decision, not a mechanical fix, and is tracked on #732.
  • Batch 116-A, sprint 204 (#692, CR-164): BACnetNPDU::DecodeNetworkLayerMessage could return 0 (the network-layer-message bit set but zero remaining bytes to decode it from) without setting error, which the top-level Decode()’s size == 0 && error.IsSet() check then read as a successful, valid zero-length decode; it now sets error. (A same-shaped fix in the outer DecodeUsingControl was found during batch close to reject a legitimate case — a priority-only NPDU with no addressing/network-layer bits and zero remaining bytes — and was narrowed back to only guard against a NULL buffer.)
  • Batch 116-A, sprint 208 (#644, CR-106): BACnetStackAuthorizationPolicy::IsAllowed never checked the extended (vendor-defined) scope form, so any extended-scope AuthRequest trivially passed regardless of the policy entry’s allowed scope. Every requested extended scope name must now also be present in the entry’s allowed list.
  • Batch 116-A, sprint 208 (#645, CR-107): BACnetStackAuthorizationCache::FindToken looked up a cached token by (audience, scope-bit) only, contradicting its own header’s documented (issuer, audience, scope-bit) lookup key — a token issued by an unrelated authorization server satisfied a lookup for the same audience/scope. FindToken now takes and checks issuer (this class has no external callers outside its own test, so the signature change carries no compatibility impact).
  • Batch 116-A, sprint 208 (#646, CR-108): BACnetAuditLogQueryProcessor’s record filter decoded the optional operations and mandatory successFilter query parameters but never consulted either (self-disclosed in an in-code comment) — every record matching the device-id filter was returned regardless of both. Both are now applied.
  • Batch 116-A, sprint 208 (#648, CR-109): BACnetAuditLogQueryProcessor stamped each returned record’s sequence number (and compared start-at-sequence-number) using the audit log’s current circular-buffer index, which shifts every time the oldest record is evicted, instead of the log’s own already-tracked monotonic GetTotalRecordCount() — breaking client-side paging after the first eviction. Both now use the recovered true sequence number.
  • Batch 116-A, sprint 208 (#650, CR-110): BACnetAuthRequestProcessor’s policy-granted path discarded the per-audience-element InsertAudienceAndTakeControl return value, unlike every other build step in the same function, so a failed insert (OOM or otherwise) still ACK’d a token with a silently incomplete audience list. It now aborts the response instead.
  • Batch 116-A, sprint 208 (#742, CR-186): adapters/csharp/CASBACnetStackAdapter.cs unconditionally #defined WINDOWS at file scope, so the #else (Linux .so) branch of its native-library filename selection could never compile in on any platform. Replaced with an extension-less DllImport name, letting the platform’s own native-library resolution (or, on Windows, LoadLibrary itself) select the correct file.
  • Batch 116-A, sprint 208 (#743, CR-187, doc fix): adapters/python/CASBACnetStackAdapter.py is relabeled as a platform-detection + callback-signature reference only — it never loads the native library or calls an export, despite its name. A real Python binding is a tracked feature request, not this file.
  • Batch 116-A, sprint 208 (#744, CR-188): every callback SimpleServerExample.java’s registerCallbacks() registered with the native library was an anonymous instance with nothing else referencing it afterward — the classic JNA callback-lifetime pitfall, where the GC is free to collect the callback object and leave the native side holding a dangling trampoline. Each callback is now retained in an instance field for the object’s lifetime; the Java adapter README documents the requirement.
  • Batch 116-A, close (build-script fix, no issue): projects/linux/test/makefile globbed only *.cpp under source/, so source/MSTP.c — the one C-language file in the tree — was never compiled into the Linux unit-test binary at all. Unnoticed because nothing outside MSTP.c itself calls any of its public API (filed separately as #769), so nothing ever needed to link against it, until this batch’s own tests/TestMSTPFrameBoundary.cpp did. Added a .c glob and pattern rule (forcing genuine C compilation via -x c, since g++ otherwise silently compiles a bare .c file as C++).
  • Batch 114, sprint 189 (#630): BACnetChangeOfStateEventAlgorithm’s SetMonitoredValueUInt/SetMonitoredValueInt stored the unsigned/signed wire tags swapped relative to their names; both, plus SetChangeOfStateParametersUnsigned’s compensating workaround, are fixed together.
  • Batch 114, sprint 189 (#631): a symmetric unsigned-overflow guard was added to the LOW_LIMIT→NORMAL branch in BACnetUnsignedOutOfRangeEventAlgorithm/BACnetUnsignedRangeEventAlgorithm, matching the existing HIGH_LIMIT→NORMAL guard - a lowLimit + deadband near UINT32_MAX could previously wrap and report a spurious recovery.
  • Batch 114, sprint 189 (#632): BACnetChangeOfLifeSafetyEventAlgorithm now uses a distinct pending-timer marker for the lifeSafetyAlarm target, so a mid-delay reclassification between offnormal and lifeSafetyAlarm restarts Time_Delay (cl. 13.3.8) instead of firing early against the other target’s stale expiry.
  • Batch 114, sprint 190 (#443): a COV-triggered Trend Log record now correctly evaluates BUFFER_READY. BACnetInterface::UpdateValue’s COV logging block previously appended records with no evaluation path at all - Total_Record_Count could cross Notification_Threshold on every COV update and BUFFER_READY would never fire for a COV-type log (only a polled log’s tick path was covered).
  • Batch 114, sprint 191 (#420, #628): DeleteObject now stops a deleted object’s autonomous engine, not just its Object_List entry - it previously kept ticking timers/algorithms/subscription queues (Trend Logs, Event Logs, Notification Classes/Forwarders, Event/Alert Enrollment, Channels, Groups, Schedules, Programs, Lighting Outputs, Timers, Averaging, Load Control, Life Safety Points/Zones, and the full Access family) indefinitely after deletion. The 7 Add* wrappers batch 110 listed now roll back their partial insert on any later failure, instead of leaving a half-provisioned object visible to ReadProperty.
  • Batch 114, sprint 191 (#442): BACnetStackTestTool_SetInvokeIdSeed now actually seeds the invoke-id counter SendConfirmedRequest* reads (BACnetDatabase’s per-peer map) in addition to the pre-existing notification-path counter - previously it seeded only the latter, which no confirmed-request send path ever consulted.
  • Batch 114, sprint 192 (#634): sent-confirmed-request timeouts now use millisecond precision when the host registers CallbackGetSystemTimeMs, and round UP to a whole second otherwise - a sub-second APDU_Timeout previously truncated to a 0-second deadline (apduTimeout / 1000), making the retry due immediately instead of after the configured wait.
  • Batch 114, sprint 193 (#636): ReadRange’s count is validated against its INTEGER16 wire range at decode; a wire-encoded value outside [-32768, 32767] is now rejected (parameterOutOfRange) instead of silently narrowed (a large positive count could flip negative). BACnetReadRangeACK::Decode now derives GetItemCount() from the actually-decoded item data rather than trusting the wire-claimed count.
  • Batch 114, sprint 193 (#637): WritePropertyMultiple now rejects an element whose wire Priority is out of range (0, or 17-255) with parameterOutOfRange, instead of silently coercing it to priority 16 for a commandable property.
  • Batch 114, sprint 193 (#638): fixed the absent-vs-malformed optional-field contract in BACnetPropertyReference/BACnetObjectPropertyReference/BACnetDeviceObjectPropertyReference’s Decode - a present-but-malformed optional field (e.g. a truncated propertyArrayIndex inside a ReadPropertyMultiple request) now fails the enclosing decode instead of being silently treated the same as a genuinely absent one.
  • Batch 114, sprint 193 (#629): BACnetDataLinkIPv4::ProcessBVLLDistributeBroadcastToNetwork now passes the correct (payload-only) NPDU length to SetForwardedNPDU, with a guard against a NULL NPDU leaving a stale, oversized length behind.
  • Batch 114, sprint 193 (#639): BACnetDataLinkMSTP::ProcessIncomingPacket now rejects an incoming frame whose source MAC is 0xFF (the MS/TP broadcast address, never legal as a station’s own source) before it can be stored and used as a reply destination.
  • Batch 114, sprint 194 (#626): new STACK_OPTION_MAX_FOREIGN_DEVICE_TABLE_ENTRIES (default 128) bounds the BACnet/IP Foreign Device Table; a registration that cannot be stored is answered with BVLC-Result NAK 0x0030 instead of growing the table without bound.
  • Batch 114, sprint 194 (#627): the Broadcast Distribution Table is now de-duplicated by (address, mask) on add, and RemoveBroadcastDistributionTableEntry’s host-index re-derivation after a delete matches on the same (address, mask) identity instead of address alone - closing a case where a duplicate of the host’s own address (with a different mask) could cause the tracked host index to silently mis-rebind.
  • Batch 114, sprint 191 (#507): BACnetObjectBase::DecodeWithContext’s “Invalid parameters” diagnostic now names the tag number and datatype involved (no behaviour change).
  • Batch 112, sprint 180 (#361): wire-learned router next-hop MACs now age out. A new STACK_OPTION_ROUTER_LEARNED_NEXT_HOP_TTL_SECONDS (default 900s; 0 = never age, the previous behaviour) bounds how long this stack keeps relaying to a MAC learned from an I-Am-Router-To-Network advertisement. cl. 6.6.3.3 does not mandate a lifetime, but an advertising router that dies silently previously left this stack unicasting to a dead MAC forever instead of falling back to the egress link’s broadcast. Only a network-learned MAC ages (BACnetPhysicalRouter::LearnRouteFromNetwork) — an operator-configured route (LearnRoute/AddRouterRoute) and a directly connected network never carry a TTL stamp and never age; the route itself is never evicted, only the MAC.

  • Batch 112, sprint 181 (#471): Reject-Message-To-Network emission is now rate-limited per true originator. A new per-source token bucket (BACNET_ROUTER_REJECT_BURST 4, BACNET_ROUTER_REJECTS_PER_SECOND 1) in BACnetNetworkLayer::SendRejectMessageToNetwork bounds how many rejects this router will emit toward any one originator — previously unbounded, so a peer that repeatedly triggered a reject condition (an unknown DNET, an oversized APDU, …) could make this router emit one reject per received frame with no limit at all.

  • Batch 112, sprint 182 (#363): test-tool hook for Initialize-Routing-Table-Ack (cl. 6.4.8). A query sent via BACnetStackTestTool_SendInitializeRoutingTable previously had no way to read the peer’s answer — the Ack was decoded and silently discarded. New BACnetStackTestTool_RegisterHookInitializeRoutingTableAck (test-tool-only, BACNET_STACK_TESTTOOL && STACK_OPTION_ROUTER) fires with the Ack’s entries flattened to the same parallel-array + concatenated-portInfo shape the send side already uses. No state is kept in the stack — the caller correlates the Ack to whatever query it sent.

  • Batch 112, sprint 184 (#439): SetBACnetSCHubConnector gained a per-port overload. The original signature always configured whichever BACnet/SC data link instance claimed array index 0 first, so a second BACnet/SC port could never be configured as a hub connector — the call silently reconfigured the first port instead. New BACnetStack_SetBACnetSCHubConnectorForNetworkPort(deviceInstance, scPortInstance, ...) (and the matching BACnetInterface::SetBACnetSCHubConnector C++ overload) names the port explicitly, matching SetBACnetSCHubFunctionConfig/SetBACnetSCDirectConnectConfig’s existing shape. Added rather than changing the original function’s arity, so no existing caller is affected; the original overload’s behaviour (and its “always instance 0” scope) is unchanged and now documented as such.

  • Batch 112, sprint 185 (#557): an activated SC_Direct_Connect_Initiate_Enable/SC_Direct_Connect_Accept_Enable now has real operational effect. #497 made these Network Port properties genuinely writable with full Changes_Pending/activate/discard shadow mechanics, but activation never reached BACnetDataLinkSC — a client could write TRUE, see Changes_Pending clear, and get a stack that still would not initiate a direct connection (or vice-versa for disabling). Activation now propagates into the data link’s real configuration and closes the connection the new value forbids (reusing the existing SetDirectConnectConfig lifecycle logic already used by the host-facing configuration API).

  • Batch 112, sprint 182 (#438): BACnet/SC router-port rebinding is now non-destructive. BACnetDataLinkLayer::RebindRouterPorts used to unconditionally clear every BACnet/SC port binding before re-deriving them, so a re-derivation that failed for one port (e.g. a Network_Number written mid-reconfiguration, leaving another port’s Network Port object transiently unrecognisable) left that port permanently unbound even though its OLD binding was still valid. It now resolves a plan for every port first, gates on whether every currently-bound port still resolves under that plan, and only then applies it — a failed re-resolve leaves every existing binding untouched.

  • Batch 112, sprint 183 (#385): a confirmed event notification to a broadcast-shaped Notification Class recipient is now refused before it is ever sent, rather than reaching BACnetDataLinkIPv4::SendOutgoingPacket’s foreign-device Distribute-Broadcast-To-Network path with no APDU timeout, no retries and no ACK correlation (cl. 5.4 expects exactly one respondent to a confirmed request). BACnetStackMessageGenerator::DeliverEventNotificationToRecipient and its ...UsingDataLink sibling now check this before building the request.

  • Batch 112, sprint 181 (#437): a router now answers a fully-qualified directed request (DNET = one of its own directly connected networks, DADR present) only when the DADR actually names it. Previously (#311, batch 110) any such request was answered unconditionally once the DNET matched, regardless of whose MAC the DADR named — correct for a device with no configured Network Port MAC to compare against (preserved exactly, see below), but wider than cl. 6.5.4’s first case for a device that DOES have one. When this device’s own MAC_Address for the ingress Network Port is resolvable, a DADR naming a different device on the same physical network is no longer answered (silently — this was never a reject condition; #311’s fix that a directly-connected DNET is never rejected as unreachable is unaffected). A DADR that matches, or a device with no Network Port object configured for that link (nothing to compare against), or an NPDU with no DADR at all, are all unaffected and still answer exactly as before.

  • Batch 112, sprint 186 (#621, CR-216): SubscribeCOV-Property-Multiple with Lifetime=0 (indefinite, cl. 13.14/13.15) self-cancelled on the very next Loop() tick. The COV-Multiple path stored currentTime + lifetime unconditionally (so 0 became “now” rather than “never”) and had no != 0 exemption in its expiry check, unlike the single-property SubscribeCOV path, which already handles both correctly. Both fixed to mirror the single-property sibling.

  • Batch 112, sprint 186 (#612): five fixed-length primitive Decode()s (DATE, DOUBLE, ObjectIdentifier, REAL, TIME) rejected a wrong-length tag with parameterOutOfRange instead of invalidTag. A tag whose length field doesn’t match the datatype’s required fixed wire size is a malformed encoding (cl. 18.3.1(6)), not a value that decoded successfully but is out of range (cl. 18.3.1(9)) — BACnetPrimitiveUINT/BACnetPrimitiveINT already use invalidTag for the identical situation. All five switched to match.

  • Batch 112, sprint 184 (#366): BACnet/SC egress could not carry cl. 6.6’s mandatory minimum of 4192 octets of data_attributes content. BACnetDataLinkSC::SendOutgoingPacket, the hub-function relay, and BACnetSCHubConnector::ProcessOutgoingMessage all encoded into a 1529-octet buffer sized for the NPDU and framing alone; a hop carrying header options that large failed in Encode rather than merely dropping the options. All three now heap-allocate a buffer sized for the NPDU plus the mandatory minimum (BACNET_SC_ENCODED_FRAME_MAX_LENGTH). Default Max_BVLC_Length_Accepted (cl. 12.65.7) raised 1600 → 6144 under STACK_OPTION_TARGET_FULL, so a conformant peer forwarding the minimum to this device is not itself truncated by what this device advertised.

  • Batch 112, sprint 184 (#379): raw network-layer payload caps (BACnetNetworkLayerRawMessage/BACnetNetworkLayerVendorProprietaryMessage::MAX_DATA_LENGTH) were derived from the BACnet/IP NPDU limit (1497) even on a BACnet/SC build, where cl. 6.6 allows NPDUs up to 61327 octets — a payload between 1493 and the SC ceiling was rejected at decode where it previously decoded. Both now derive from BACnetSCConstants::BACNET_SC_MAXIMUM_NPDU_LENGTH when STACK_OPTION_DATA_LINK_LAYER_SC is compiled.

  • Batch 111, sprint 174 (#623): BACnetNotificationParametersChangeOfStatusFlags required present-value [0] on both Encode and Decode, even though cl. 13.8’s ASN.1 marks it OPTIONAL. A conformant peer that legitimately omits present-value (the field “depends on referenced property”) was rejected on Decode, and this stack could never produce a change-of-status-flags notification without one — an interop defect, not a style choice. Decode now peeks the leading tag and accepts a message that starts straight at context 1 (referenced-flags); Encode omits context 0 when unset; IsValid()/Clone()/IsEqual()/GetElements() all treat a NULL present-value as a legitimate state instead of an incomplete one.

  • Batch 111, sprint 174 (#620): the three UseCallbackGetProperty{BitString,CharString,OctetString} wrappers in BACnetBusinessLogic.cpp trusted a host callback’s reported output length with no check against the capacity they gave it. A host callback that writes correctly but misreports its own output count (a host bug, not a malicious input) caused the primitive constructor immediately afterward to read past the local stack buffer. All three now treat a reported length greater than the passed-in capacity as a callback failure.

  • Batch 111, sprint 174 (#619): BACnetLogDatum/BACnetSingleLogData’s numeric string parsers (real/enumerated/unsigned/integer/timeChange/failure arms, SetLogDatumFromString/SetSingleLogDataFromString) still used ChipkinConvert::ToValue<>(), which silently accepts malformed/junk/out-of-range text as a default-ish value (#567’s class, already fixed elsewhere in batch 108). Switched every remaining site to TryParse<>(), which rejects the whole call on any malformed field and leaves the datum unchanged.

  • Batch 111, sprint 174 (#618): BACnetPriorityFilter::Decode narrowed an unconstrained (32-bit) wire Unsigned to uint8_t with no range check. minPriority/maxPriority are decoded into a BACnetPrimitiveUINT but read back through accessors returning uint8_t; a wire value above 255 silently truncated instead of being rejected. Decode now rejects either field with Reject(parameterOutOfRange) when its decoded value exceeds 255.

  • Batch 111, sprint 174 (#617): BACnetSequenceOf<T>::Decode had no element-count cap. A host-supplied SEQUENCE OF with a very large element count drove unbounded heap growth purely from wire input (one allocation per element). Added STACK_OPTION_MAX_SEQUENCE_OF_ELEMENTS (default 1024, tiered by build target like its siblings) and Decode now rejects the (cap+1)-th element with Abort(bufferOverflow) instead of continuing to grow.

  • Batch 111, sprint 174 (#616): BACnetSequenceOf<T>::IsEqual compared GetSize() only — any two same-length sequences were reported equal regardless of content, for every BACnetSequenceOf<T> instantiation in the codebase. Now compares element-wise (modelled on BACnetListOfReadAccessResults::IsEqual), preserving order-sensitivity.

  • Batch 111, sprint 174 (#622): BACnetCovValue and BACnetPropertyStates had no negative Decode tests — only success-only Decode suites existed for these wire-reachable decoders. Appended truncated-buffer and wrong/unrecognized-tag-number cases to both test files; both decoders correctly reject the malformed input (no production code change, test-coverage only).

  • Batch 111, sprint 174 (#623): BACnetNotificationParametersChangeOfStatusFlags required present-value [0] on both Encode and Decode, even though cl. 13.8’s ASN.1 marks it OPTIONAL. A conformant peer that legitimately omits present-value (the field “depends on referenced property”) was rejected on Decode, and this stack could never produce a change-of-status-flags notification without one — an interop defect, not a style choice. Decode now peeks the leading tag and accepts a message that starts straight at context 1 (referenced-flags); Encode omits context 0 when unset; IsValid()/Clone()/IsEqual()/GetElements() all treat a NULL present-value as a legitimate state instead of an incomplete one.

  • Batch 111, sprint 174 (#620): the three UseCallbackGetProperty{BitString,CharString,OctetString} wrappers in BACnetBusinessLogic.cpp trusted a host callback’s reported output length with no check against the capacity they gave it. A host callback that writes correctly but misreports its own output count (a host bug, not a malicious input) caused the primitive constructor immediately afterward to read past the local stack buffer. All three now treat a reported length greater than the passed-in capacity as a callback failure.

  • Batch 111, sprint 174 (#619): BACnetLogDatum/BACnetSingleLogData’s numeric string parsers (real/enumerated/unsigned/integer/timeChange/failure arms, SetLogDatumFromString/SetSingleLogDataFromString) still used ChipkinConvert::ToValue<>(), which silently accepts malformed/junk/out-of-range text as a default-ish value (#567’s class, already fixed elsewhere in batch 108). Switched every remaining site to TryParse<>(), which rejects the whole call on any malformed field and leaves the datum unchanged.

  • Batch 111, sprint 174 (#618): BACnetPriorityFilter::Decode narrowed an unconstrained (32-bit) wire Unsigned to uint8_t with no range check. minPriority/maxPriority are decoded into a BACnetPrimitiveUINT but read back through accessors returning uint8_t; a wire value above 255 silently truncated instead of being rejected. Decode now rejects either field with Reject(parameterOutOfRange) when its decoded value exceeds 255.

  • Batch 111, sprint 174 (#617): BACnetSequenceOf<T>::Decode had no element-count cap. A host-supplied SEQUENCE OF with a very large element count drove unbounded heap growth purely from wire input (one allocation per element). Added STACK_OPTION_MAX_SEQUENCE_OF_ELEMENTS (default 1024, tiered by build target like its siblings) and Decode now rejects the (cap+1)-th element with Abort(bufferOverflow) instead of continuing to grow.

  • Batch 111, sprint 174 (#616): BACnetSequenceOf<T>::IsEqual compared GetSize() only — any two same-length sequences were reported equal regardless of content, for every BACnetSequenceOf<T> instantiation in the codebase. Now compares element-wise (modelled on BACnetListOfReadAccessResults::IsEqual), preserving order-sensitivity.

  • Batch 111, sprint 174 (#622): BACnetCovValue and BACnetPropertyStates had no negative Decode tests — only success-only Decode suites existed for these wire-reachable decoders. Appended truncated-buffer and wrong/unrecognized-tag-number cases to both test files; both decoders correctly reject the malformed input (no production code change, test-coverage only).

  • Batch 109 (#580 — owner ruling, 2026-09-02: adopt the planner’s recommendation, byte-preserving passthrough): BACnetPrimitiveCharSTRING::Encode() now writes m_value back verbatim for every EncodingType, not just UTF-8. m_value stores the raw encoded octets of whatever charset was declared at decode time (which may not be UTF-8/decodable text), so encoding must be symmetric with decoding regardless of charset.

  • Batch 109 (#582 — owner ruling, 2026-09-02: adopt the planner’s recommendation): BACnetPrimitiveTIME::SetHundrethSeconds() now rejects values in 100-254 (only 0-99 and the 255 “unspecified” sentinel are valid per cl. 20.2.13) instead of silently accepting them.

  • Batch 109 (#578 — owner ruling, 2026-09-02: adopt the planner’s recommendation, Option A/tolerant ingress): documented, with no decoder change, that BACnetPrimitiveUINT/INT/ENUM decoders deliberately accept a non-minimal integer encoding for interoperability — cl. 20.2.4/.5/.11’s “smallest number of octets possible” rule binds producers, not consumers; Encode() always re-emits the minimal form.

  • Batch 109 (#577 — owner ruling, 2026-09-02: adopt the planner’s recommendation): BACnetPrimitiveINT::GetEncodeByteCount() replaced a (uint8_t*)&value byte-by-byte scan (correct only on a little-endian host) with endianness-independent boundary comparisons, the same idiom BACnetStackCommon::ConvertUIntToByteArray already uses.

  • Batch 109 (#573 — owner ruling, 2026-09-02: adopt the planner’s recommendation, Part 1 + Part 2): DateToString/TimeToString no longer truncate a widened sentinel field to 2 characters (255 was rendering as 25). StringToDate/StringToTime now parse structurally (via a new shared ParseUInt8Field helper) instead of by fixed offset/length, enforcing BACnet ranges per field (date: month 1-12/255, day 1-31/255, weekday 1-7/255, year 1900-2155; time: hour 0-23/255, minute/second 0-59/255, hundredths 0-99/255) while still accepting the 255 “unspecified” sentinel in any field regardless of width; StringToTime drops its old fixed length == 11 requirement, which rejected every sentinel-widened field.

  • Batch 109 (#599): BACnetAbstractSyntaxType::EncodeString’s only bounds check used to be maxLength != 0 — every arm wrote the encoded string with no comparison against the caller’s actual buffer capacity, an overflow shape made real by OctstringToString/BitstringToString’s 1+2N expansion. All 12 arms (including Null) now route through a new CopyBounded helper; contract documented as NOT NUL-terminated. Added the DateTime and ObjectIdentifier arms EncodeString was missing (DecodeString already supported both), so both can now round-trip.

  • Batch 109 (#611 — found while fixing #566): BACnetNetworkParameters::SetOriginatingVirtualAddress(string) ignored HexStringToByteArray’s return value and unconditionally reported success; a 12-character string with a non-hex character left the VMAC buffer stale but reported true. Now checks the decoded length and rejects/leaves state untouched on failure.

  • Batch 109 (#610): BACnetPacket::Encode sized the APDU sub-encode from m_maxApduLength (the configured budget) instead of the caller buffer’s actual remaining capacity, at both the primary encode and the Abort-substitution retry. Fixed to take the smaller of the two; not found to be reachable under the stack’s current constants (traced on the issue), but the two are independent invariants that should not rely on today’s specific values staying aligned.

  • Batch 109 (#601 — owner ruling, 2026-09-02: adopt the planner’s recommendation, Option A): BACnetStackLifeSafetyObject::SetCoreProperty’s trackingValue arm now rejects a WriteProperty for a LifeSafetyState outside 0-34 (ASHRAE 135-2024) with Error(PROPERTY, VALUE_OUT_OF_RANGE), mirroring the existing Mode arm’s range check. The host-side setters (SetTrackingValueFromHost/SetPresentValueDirect/SetTrackingValueDirect) stay unvalidated per #485’s host-owned contract — this closes only the wire-write path, where a remote peer could otherwise push a nonconformant value into Present_Value and every subsequent ChangeOfLifeSafety notification.

  • Batch 109 review continuation, sprint 163 (WP-06 BVLL/BBMD/IPv4, CR-053): BACnetBVLLBDTEntry::Reset() zeroed only 4 of the 6 bytes of m_bbmdAddress (the wrong length constant), leaving the port bytes uninitialized after default-construction/reset until SetHostIpAddress/Decode populated the whole array.

  • Batch 109 review continuation, sprint 163 (CR-054): BACnetBVLLBDTEntry::operator= passed an overstated capacity bound (BDT_ENTRY_LENGTH, 10) instead of the mask’s actual 4-byte capacity to GetDistributionMask — harmless only because that getter ignores the excess today; fixed so the bound is truthful.

  • Batch 109 review continuation, sprint 163 (CR-055): BACnetBVLLFDTEntry::FDTEntryTick computed the remaining-TTL value and narrowed straight to uint16_t with no clamp, unlike SetTimeToLive’s own clamp for the same near-UINT16_MAX range — a wire-supplied TTL of 65535 wrapped to 29 immediately at registration instead of clamping to 65535, and the wrapped value was reported back out over the wire via Read-Foreign-Device-Table-Ack.

  • Batch 109 review continuation, sprint 163 (CR-056): BACnetBVLLBase::Decode accepted any value in the BVLC Type octet with no validation against BVLCTYPE_BACNETIP (0x81) — dormant on the production wire path (BACnetBVLL::Decode already rejects a wrong type before dispatch), but a caller reaching a subtype’s Decode() directly would have silently accepted a wrong-typed frame.

  • Batch 109 review continuation, sprint 163 (CR-057): four BACnetBVLL methods (HasNPDU, GetNPDULength, SetNPDULength, and the private SetLength helper several Set* mutators route through) dereferenced m_bvllBase with no NULL guard, unlike every sibling accessor (GetType/GetFunction/GetLength) — not reachable from the production wire path (both callers Decode() first), but a default-constructed or post-OOM BACnetBVLL calling one of these first would crash.

  • Batch 109 review continuation, sprint 164 (WP-04 service codecs, CR-061): BACnetReadPropertyACK::IsEqual dereferenced m_propertyValue with no NULL check — a legitimately-NULL owned pointer (both constructors and Reset() leave it NULL; Encode() treats NULL as a supported state) — crashing on any comparison involving a default-constructed/Reset() object.

  • Batch 109 review continuation, sprint 164 (CR-062): BACnetReadPropertyProcessor::Process’s two Network-Port-4194303 resolution-failure early returns left errorContainer unset, unlike every other early return in the function — the caller then marked the ACK valid regardless, sending an empty Complex-ACK with no service payload instead of an Abort. The sibling BACnetReadPropertyMultipleProcessor’s identical case already set SetAbort(other); mirrored here.

  • Batch 109 review continuation, sprint 164 (CR-063): a missing ! in the property-array-index (and, in BACnetWritePropertyRequest, also the priority) equality check meant two objects with the SAME valid value were reported unequal (and, since the branch otherwise falls through to “equal,” two objects with DIFFERENT valid values were reported equal) — the same S159-class inversion already fixed in several sibling classes, still present in BACnetReadRangeRequest, BACnetReadRangeACK, BACnetAddListElementRequest, BACnetRemoveListElementRequest, and BACnetWritePropertyRequest (both its array-index and priority blocks).

  • Batch 109 review continuation, sprint 164 (CR-064): BACnetReadPropertyMultipleAckProcessor::Process’s outer readAccessResult loop dereferenced its element with no NULL check, unlike the nested readResult loop in the same function, which guards its own element — BACnetListOfReadAccessResults::Insert/BACnetReadPropertyMultipleACK::InsertReadAccessResult accept a NULL pointer with no check of their own. Not wire-reachable (Decode() never inserts NULL) but a real crash for any caller that does. First test file added for this processor (tests/TestBACnetReadPropertyMultipleAckProcessor.cpp).

  • Batch 109 review continuation, sprint 165 (WP-05 network layer / WP-07 MS/TP, CR-071): BACnetNetworkLayerVendorProprietaryMessage::Encode’s only guard was buffer==NULL||maxBufferLength==0 — a 1-byte buffer passed that and then had the second byte of the 2-byte vendor ID written past its end (the same defect shape issue #594 fixed in BACnetWhoIsRouterToNetwork::Encode). The stale offset also underflowed maxBufferLength - bufferOffset (uint16_t 1 - 2 = 65535) on the following GetData() call, which would have let that call’s own bounds check pass and write the whole stored payload past the caller’s real buffer end.

  • Batch 109 close (collect-all-failures — completes #578’s own contract): BACnetPrimitiveUINT/INT/ENUM::Decode() accepted a non-minimal wire encoding (per #578’s tolerant-ingress ruling) but never renormalized m_tag afterward, so a subsequent Encode() with no intervening SetValue() echoed back the wire’s own non-minimal length instead of the minimal form #578’s own plan comment documented (“Encode() (always minimal) then re-encodes it shorter”). All three now call SetValue(m_value) at the end of Decode(), which recomputes the minimal tag length — caught by sprint 159’s own Issue578_Decode_RedundantLeadingZero_Accepted/RedundantLeadingFF_Accepted tests the first time this batch’s changes were actually built and run (build-light mode defers the first real build to batch close).

  • Batch 109 close (deferred-ladder / interim-promise follow-through on #610): added the exact-capacity and guard-byte coverage the sprint 160 plan comment promised — TestBACnetPacket.cpp’s new EncodeApduCapacityIsBoundedByTheCallerBufferNotJustMaxApduLength proves the APDU sub-encode is bounded by the caller’s actual buffer even when m_maxApduLength is configured far larger.

  • Batch 109 (#604(a) — owner ruling, 2026-09-02: adopt the planner’s recommendation, pre-cleared frozen-header edit): removed #include "stdafx.h" from source/CASBACnetStackDLL.h — source/stdafx.h has been a zero-byte MSVC-PCH stub since 2016, so this changed no compiled output; it only stopped a consumer who copies just this one header (rather than the whole source/ directory) from failing to build. Added a durable gate for it, ci_scripts/check-header-standalone.sh, compiling a three-line consumer against only the copied header as both C99 and C++17. Part (b) of the issue (build-controlled dllexport/dllimport) stays open, deliberately not bundled into this batch per the owner’s own ruling.
  • Batch 109 (#606 — owner ruling, 2026-09-02: adopt the planner’s recommendation, Option A): documented the public ABI’s time_t as contractually 64-bit (new “Supported target matrix” section in README.md, and a matching section in adapters/README.md) — only 64-bit binaries ship, and the C#/Python/Rust/Java bindings all hard-code a fixed 64-bit integer for it (Go is exempt; cgo’s own time_t always matches). Added a sizeof(time_t) == 8 compile-time assert in the non-frozen source/CASBACnetStackDLL.cpp, and ci_scripts/check-contract-pins.js pins the frozen header’s time_t site count at 6 so a new site forces a conscious decision about this contract.
  • Batch 109 (#607 — owner ruling, 2026-09-02: adopt the planner’s recommendation, pre-cleared frozen-header edit): reworded source/CASBACnetStackDLL.h’s Object Access Services migration note, which called each send call “stateless, thread-safe” — a note about no longer needing per-call builder state, misread as a concurrency guarantee the stack (zero synchronization primitives anywhere in source/) does not make. Added a “Threading model” section to README.md and BUILD.md stating the real contract (single-threaded; the host must serialize all BACnetStack_*/BACnetStack_Tick() calls), corrected the same sentence in both v5→v6 breaking-changes docs, and extended ci_scripts/check-contract-pins.js to gate both the retired wording and the documented section’s continued existence.
  • Batch 109 (#434 item 2): disclosed, retroactively, that BACnetStackTestTool_SendAuditLogQuery (test-tool-only) was widened by seven trailing parameters in the same PR that added InsertNotificationLogRecordWithEventValues (#92) without the disclosure that PR’s own two COV-notification widens carried. An existing binary linked against the pre-#92 signature read the trailing slots from uninitialised stack. Documented in the header doc-comment and both v5→v6 migration docs; no further signature change (the widen already shipped).
  • Batch 109 (#571 — owner ruling, 2026-09-02: adopt the planner’s recommendation, Option A): deleted source/ChipkinEndianness.h/.cpp — ChipkinCommon::CEndianness, a second, unused implementation of the same six operations CASBACnetStack::CEndianness (source/endianness.h, 20+ real call sites) already provides, with zero callers anywhere in source/, adapters/, or projects/. Ported the one embedded test with no counterpart in tests/TestEndianness.cpp (ToBigEndianLargeBuffer, a >4-byte buffer case) before deleting, so coverage isn’t lost. Removed the two now-dead #includes and the file’s four .vcxproj/.vcxproj.filters entries.
  • Batch 109 (#569 — owner ruling, 2026-09-02: adopt the planner’s recommendation, Option C): stripped the dangling docs/todo-deferrals.md D-identifier references from the three source/*.cpp comments that carried them (BACnetBusinessLogic.cpp, BACnetDBDevice.cpp, BACnetStackSchedule.cpp) — that file has never existed in this repository. The archived _spec/spec-coverage/** docs, which also reference it, are left alone as explicitly historical. New gate ci_scripts/check-md-links.js resolves every .md path referenced from a source/*.cpp comment or _spec/spec-coverage/** doc against the real tree, with a shrinking allowlist (ci_scripts/md-links-allowlist.txt) for the 72 pre-existing dangling references (overwhelmingly to a non-committed _spec/spec-coverage/reports/*.md directory) found the first time the rule ran.
  • Batch 109 (#588 — owner ruling, 2026-09-02: adopt the planner’s recommendation, Option A): deleted .gitlab-ci.yml and .gitlab/issue_templates/ — dead since 2024 (no GitLab remote configured; GitHub Actions is the only CI that runs), and its NoSC jobs were themselves broken (writing legacy macros nothing in source/CASBACnetStackOptions.h recognizes, so the build would have resolved to STACK_OPTION_TARGET_FULL with BACnet/SC left on while no_sc.sh had stripped the SC sources). Updated the README.md pointer. Confirmed GitHub CI already covers the NoSC configuration: .github/workflows/release.yml builds and packages it (with an explicit assert step that it does not silently re-enable SC), on every version tag — the PR path itself deliberately does not build it, an already-documented, owner-approved tradeoff (tests.yml’s own header comment) trading early warning for CI cost, not an unrecorded gap. No follow-up issue filed.
  • Batch 109 (#561 — owner-authorized .github/workflows/** edit, 2026-09-02: adopt the planner’s recommendation): added a fast-gates job to .github/workflows/tests.yml running every no-build ci_scripts/pre-push.sh gate not already on the PR path (six were already there: doc-lint, the C++/node adapter drift+coverage+parity gates, and the C++ adapter compile check) — check-msvs-test-registration.sh, hand-adapter-coverage.sh, node-adapter-argument-order.js, node-adapter-output-buffer-guards.js, check-self-assignment-guards.js, check-getelements-completeness.js, check-todo-markers.js, check-header-standalone.sh, check-contract-pins.js, and check-md-links.js, each self-test first. check-test-files-contribute.sh (needs the built binary) now runs at the end of the existing test job, right after the unit tests it depends on. Added an explicit timeout-minutes to both jobs (previously unset, defaulting to GitHub’s 360-minute cap).
  • Batch 107 (#389 + #441 — owner ruling A, 2026-09-02): the foreign-device broadcast rule now lives at one chokepoint, and the ten inline copies are gone. J.2.12 scopes Original-Broadcast-NPDU by ROLE — it “is used by B/IP devices and routers which are not foreign devices to broadcast NPDUs on a B/IP network” — so a registered foreign device must never emit 81 0B. That rule is now stated once, in the new BACnetDataLinkIPv4::SetOutgoingLocalBroadcast, which both PrepareOutgoingBvll and PrepareOutgoingBvllWithAPDU call everywhere they would otherwise have chosen Original-Broadcast — including PrepareOutgoingBvll’s already-valid-BVLL early return, which is the path the no-APDU emitters (I-Am-Router-To-Network, Who-Is-Router-To-Network, Network-Number-Is) actually take. Deleted with it: ten verbatim inline copies of the rule across BACnetInterface and BACnetTestToolInterface, the three ApplyForeignDeviceBroadcastBvll helpers and their nineteen call sites, BACnetInterface::PreparePacket’s packet-level copy, and BACnetDataLinkLayer::SendRoutedPacket’s own copy — whose comment had asked for exactly this decision. This closes a defect class reopened eight times (#364, #370, #375, #378, #381, #382, #384, #424): each round was one more emitter that had not been told a rule it should never have had to know. One carve-out, and it is #385’s rather than this rule’s: a CONFIRMED request keeps Original-Broadcast, because SendOutgoingPacket’s Distribute-Broadcast-To-Network branch returns without calling HandleConfirmedRequest — promoting a confirmed APDU onto it loses the APDU timeout, the retries and the ACK correlation, which was tried on that branch and reverted as actively harmful. The carve-out preserves exactly the behaviour BACnetStackMessageGenerator’s !IssueConfirmedNotifications() gate was preserving, and now also covers confirmed broadcasts that never came through a Notification Class. Not in this change and still open: #389 item 1 (one BACnetDataLinkIPv4 per Network Port — filed separately, it changes the public port-config shape) and #389 item 3’s origination leg (parked on #385).

  • Batch 107 (#344 — owner ruling B, 2026-09-02): a BACnet/SC hub-function Encapsulated-NPDU whose Originating-Virtual-Address is not the VMAC that connection authenticated at Connect-Request time is now discarded at ingress (BACnetSCHubFunctionManager.cpp) — not relayed, not processed locally, and not answered. AB.3.1.2 makes the OVA “the VMAC of the node that originally initiated the BVLC message”, with a forwarder inserting the connection peer’s VMAC; on a hub-FUNCTION ingress the connection peer is the originator, so a mismatch is never conformant. This supersedes #298’s process-and-neutralise behaviour for this one case: #298 deliberately kept processing such a frame and neutralised the OVA at each consumer that used it as addressing state, and the three tests that pinned that intent (AnScAdvertisementNamingAnotherPeersVmac_DoesNotSteerTheRelayToIt, AnScPeerVaryingTheOriginatingVmac_CannotEscapeThePerSourceHoldQuota, AnScPeerForgingTheOriginatingVmac_CannotSteerARejectAtAnotherPeer) are rewritten under this ruling to assert the discard, each keeping the #298 property as its honest-OVA control arm. The reason for the reversal is that neutralising consumers one at a time kept finding another one — next-hop MAC learning (#298), the per-source hold quota and the cl. 6.4.4 reject destination (#301), and then reply addressing (#440); discarding the frame closes the class. Scope: hub-function ingress only — hub-connector and direct-connect ingress (#301/#324) enter through their own functions and are untouched, because there the peer legitimately differs from the originator. Stated precisely: this is a hardening choice the ruling authorized, not a spec-compelled discard — an OVA/peer-VMAC mismatch is not one of AB.3.1.5’s enumerated receiver error situations, and the BVLC-Result NAK that AB.3.1.5’s nearest case (INCONSISTENT_PARAMETERS) would call for is deliberately withheld so a spoofing peer gets no confirmation. Batch-close adversarial review (the ninth attempt at the #389/#441 class above) found a real tenth inline copy — BACnetDataLinkIPv4::SendIAmAsForeignDevice still hand-wrote the Distribute-Broadcast-To-Network BVLC function directly instead of going through SetOutgoingLocalBroadcast (not a live defect, since the function’s own foreign-device guard meant the chokepoint would compute the same answer, but exactly the latent shape that reopened this class eight times) — and a related altitude gap, the #385 confirmed-request carve-out sitting beside the chokepoint rather than inside it. Both fixed: the carve-out is now an isConfirmedRequest parameter on SetOutgoingLocalBroadcast itself.

  • Batch 107 (#84, #86, #88): test-tool white-box hooks — a pluggable invoke-id generator and FSM introspection, plus their full node-adapter chain. BACnetStackInvokeIds (#84) accepts a registered generator (RegisterCallbackInvokeIdGenerator) whose answer is used VERBATIM on the confirmed-request path, bypassing the uniqueness search in BACnetDatabase::GetInvokeId — deliberately, since forcing a duplicate/non-monotonic id is exactly what BTL 13.9.1/13.9.2 need; NULL restores the stack’s own counter, resuming from (generator’s last answer + 1) rather than a fresh seed. BACnetRespondingStateMachine (#86 + #88) gains a dense per-instance m_instanceKey and a NotifyTransition hook that fires on every ACTUAL state change (never a same-state write) — both SetCurrentState and the previously-separate Reset() write path now funnel through one WriteCurrentState helper, since Reset() turned out to be the MAJORITY of return-to-Idle sites (~14 of 17), not SetCurrentState(Idle)’s 3. BACnetTestToolInterface::FSM_Query(fsmId, fsmInstanceKey, stateOut) reads live state; fsmId=0 (RESPONDING) is the only value accepted today. Full node-adapter chain (napi wrappers, interface.cc registration, api.ts typing, generated manifest) for all three hooks.
  • Batch 107 (#453): pack-helper input validation (issue #444’s bounds/priority/tag/TEXT-warning checks) ported from the node reference implementation to all five other adapter languages (C++, C#, Go, Java, Python, Rust) — assertUnsignedField/assertSentinelField/assertWritePriority/assertValueBuffer-equivalents plus a float32-saturation check on covIncrement, each following its own language’s existing error-reporting idiom (exceptions, Result<T,E>, multi-value returns). C++ previously had NO datatype-tag or priority bound at all. Found and fixed a real bug during verification: the Python port’s covIncrement saturation check assumed struct.pack("<f", ...) silently saturates an out-of-range double the way JavaScript’s Buffer.writeFloatLE does — it does not, CPython raises OverflowError instead, which was escaping uncaught past the intended ValueError contract.
  • Batch 107 (#434 items 1+4): SendRouterBusyToNetwork/SendRouterAvailableToNetwork now refuse a non-zero dnetCount with a NULL dnets array instead of silently degrading into the cl. 6.4.5/6.4.6 “all networks” broadcast form (modelled on SendInitializeRoutingTable’s existing refusal for the identical shape); BACnetRawConfirmedServiceRequest::IsEqual’s type guard was GetDatatype() != GetDatatype(), and GetDatatype() returns Invalid for this class AND several unrelated confirmed-request classes (BACnetAuthRequest among them) — a static_cast across genuinely different types was possible whenever both sides returned Invalid. Replaced with an exact typeid(*other) != typeid(*this) check.

  • Batch 107 (#436): SendUnconfirmedCOVNotificationMultiple had zero references anywhere under adapters/node — the DLL export existed with no napi wrapper, no interface.cc registration, and no api.ts typing; all three added. BACnetStackTestTool_SendAuthRequest linked via a bare adapter-local extern in interface_send.cc with no declaration in either DLL header — declared in CASBACnetStackTestToolDLL.h’s STACK_OPTION_DEVICE_AUTHORIZATION gate with extern "C" linkage instead.

  • Batch 107 (#473 item 1): the “is this wire-supplied DADR a plausible MAC for the egress datalink?” decision BACnetNetworkLayer.cpp had grown three separate inline copies of (found one review round at a time under #432) is now one CheckDadrWidthForEgress helper returning a verdict enum; each of the three call sites keeps its own disposal (reject-and-consume / reject-and-continue / ignore-and-free relayPacket).

  • Batch 107 close: a collect-all-failures build caught a pre-existing (sprint 147) missing closing brace in tests/TestBACnetRespondingStateMachine.cpp that had silently nested every subsequent TEST_F inside one test body. A JSDoc comment in the node adapter’s api.ts (Last_Access_*/Credential_Disable) contained a literal */ that closed its own block comment early, corrupting tsc parsing of everything after it — found via a due-diligence tsc --noEmit run (not part of any existing gate). A real, pre-existing (unrelated to this batch) linkage defect: source/CASBACnetStackTestToolDLL.cpp included its own header before CASBACnetStackOptions.h, so every STACK_OPTION_DEVICE_AUTHORIZATION-gated function definition in that file (the AA-* Authorization exports, SendAuditLogQuery, SendAuthRequest) was compiling with ordinary C++ (mangled) linkage instead of the extern "C" its own header declares — invisible until the node adapter’s first-ever real node-gyp build linked against the plain C name and failed with 7 unresolved externals. Fixed the include order there and in interface_common.h (same bug, same fix).

  • Batch 107 (#115, reduced scope per triage re-scope): CASBACnetStackDLL.h’s ReadProperty/WriteProperty propertyArrays byte diagrams now cite BACnetStackReadPropertyAsync::SIZE_OF_ARRAY / BACnetStackWritePropertyAsync::BACNET_WRITE_PROPERTY_ASYNC_MIN_ARRAY_SIZE instead of restating byte counts as prose, with the little-endian-by-construction assumption noted inline. The cross-repo golden-bytes check (tying this to the TypeScript producer in chipkin-bacnet-testing-tool) needs an owner CI-ownership decision and is split into #558.

  • Batch 107 (#473 item 2): ci_scripts/dev-build.sh --lean now prints a disclosure banner naming the cross-datalink router suites it cannot have run (BACnet/SC and MS/TP compiled out), so a green --lean run is never again read as full coverage.

  • Batch 108 (#608): BACnetInterface::Tick() now rejects (rather than trusts) an out-of-range length returned by CallbackReceiveMessage before handing it to Process(), and Process() itself gates the same three lengths as defence in depth for any other caller. Batch-close correction: Process()’s own length guard was bounded against BACNET_INTERFACE_MAX_INPUT_BUFFER_LENGTH (Tick()’s own NPDU-only stack buffer size) instead of BACNET_ENCODED_FRAME_MAX_LENGTH (the real ceiling for an already-encoded wire frame including its BVLC/data-link header) — Process() does not own a fixed buffer the way Tick() does, so the wrong constant rejected legitimate maximum-length frames (caught by a BBMD forwarding test in the batch’s own collect-all-failures build). Now bound against the correct, larger constant.

  • Batch 108 (#605): GetInfo() now writes the NUL terminator its own capacity check already reserved a byte for; DecodeAs() now rejects a NULL input or output pointer paired with a nonzero length before doing any decode work, and NUL-terminates its output when the caller left room.

  • Batch 108 (#597): BACnetStackDebug’s vsnprintf result is no longer narrowed into a uint8_t — a negative (encoding-failure) return now produces a fixed error string instead of reading uninitialized stack, and an over-length message truncates to 255 characters instead of the previous 256-wraps-to-empty behaviour.

  • Batch 108 (#596): behaviour change — BACnetStack_SetPropertyWritable/BACnetStack_SetPropertySubscribable with writable/subscribable = false now actually revokes access on a property whose default profile is true; previously a false override on such a property was silently discarded and the default (true) governed.

  • Batch 108 (#602): behaviour change — a ReadRange with a negative count near the start of an Event Log, Trend Log, or Trend Log Multiple (reference < |count|) now returns the clamped range (starting at the first retained record) instead of an unsigned-underflow-induced empty ACK. Fixed identically in all three log classes (BACnetStackEventLog, BACnetStackTrendLog, BACnetStackTrendLogMultiple).

  • Batch 108 (#603): the four ReadRange builders in each of BACnetStackEventLog, BACnetStackTrendLog, and BACnetStackTrendLogMultiple now check Clone()/InsertItemDataAndTakeControl() and stop (rather than continuing to count) on failure, so an allocation failure can no longer make an ACK advertise a record it does not actually carry.

  • Batch 108 (#595): SetEventAlgorithmByObjectType/SetEventAlgorithmByEventType (BACnetStackAlarmAndEventObject) now build the replacement algorithm before committing it, matching the #505 fix already applied to the fault-algorithm side — re-arming an EventEnrollment object with a different Event_Type no longer leaks the previous algorithm, and a factory failure no longer detaches a working one.

  • Batch 108 (#594): BACnetNPDU::Encode’s hop-count and network-layer-message-type octets, and BACnetWhoIsRouterToNetwork::Encode’s network-number field, are now individually bounds-checked immediately before each write, instead of relying on an up-front minimum-size guard that the preceding, independently-checked address encoders could already have exhausted.

  • Batch 108 (#593): BACnetNPDU::SetDestination/SetSource now reject a NULL address paired with a nonzero length before writing any member, and zero the stored address whenever the length is zero, so a reused object cannot encode a stale address under a shorter length. BACnetNetworkParameters::SetClientNetworkParameters now propagates SetDestinationAddress’s failure instead of discarding it.

  • Batch 108 (#592): BACnetIPPacket::Encode now reserves the BVLL header’s fixed size (BACNET_BVLL_MIN_LENGTH, plus the Forwarded-NPDU address length where applicable) instead of the mutable BVLC length field the same call mutates before returning — re-encoding the same packet object no longer corrupts the frame.

  • Batch 108 (#589): BACnetSimpleACKPDU::Decode now validates the raw service-ack-choice octet against BACnetConfirmedServiceChoice::authRequest before casting it to the enum, matching the existing BACnetComplexAckPDU/BACnetConfirmedRequestPDU fix for the same class of undefined behaviour (#297). Behaviour change (widening): BACnetRejectReason now has a maxProprietaryRejectReason = 255 range marker (mirroring BACnetAbortReason), so BACnetRejectPDU::Decode can hold any proprietary reject reason (64-255) without undefined behaviour; this was already accepted on the wire, just not safely representable.

  • Batch 108 (#587): BACnetConfirmedRequestPDU::Encode and BACnetComplexACKPDU::Encode now compute their required minimum size from the object’s own segmented-message flag before writing anything, instead of always checking the non-segmented minimum — a segmented PDU no longer overruns an exactly-4-or-5-byte (ConfirmedRequest) or exactly-3-or-4-byte (ComplexAck) caller buffer. BACnetComplexACKPDU::Encode’s memory-buffer remaining-length check is now underflow-safe.

  • Batch 108 (#575): BACnetStackMemoryBuffer now has a deep copy constructor (the implicit one bitwise-copied the owning buffer pointer, risking a double-free/use-after-free on copy); Clone() is now const. Insert() now detects when its source aliases the buffer’s own storage and rebases it after a growing resize instead of copying from freed memory, and uses memmove (not memcpy) so a non-growing overlapping insert is well-defined. BACnetComplexACKPDU::operator= now copies the memory-buffer form of an APDU (previously only Clone() did), so a copy no longer silently loses an already-encoded APDU.

  • Batch 108 (#583): BACnetPrimitiveBitSTRING::SetBitString no longer clears its stored value before reading its argument, so object.SetBitString(object.GetBitString()) no longer silently discards the value; it now also applies the extended-tag handling SetValue already did.

  • Batch 108 (#581): BitSTRING::SetValue and CharSTRING’s 3-arg constructor now reject a NULL value paired with a nonzero length (matching OctSTRING::SetValue’s existing guard) instead of dereferencing it.

  • Batch 108 (#579): OctSTRING::Encode now checks its own GetMinimumMemoryCount() instead of a hard-coded minimum copied from CharSTRING (which, unlike OctSTRING, genuinely has an encoding octet) — an empty OctetString can now encode into the exact one byte it advertises needing. GetMinimumMemoryCount() itself now also accounts for an extended context tag number (>= 15), which it previously omitted.

  • Batch 108 (#576): behaviour change — BACnetTag now rejects the ASHRAE-reserved extended tag number 255 in SetTagNumber, Decode, and Encode (cl. 20.2.1.2); a peer sending it is now refused at the wire boundary instead of being accepted and surfacing a misleading later error.

  • Batch 108 (#565): BACnetPrimitiveDOUBLE::Encode and BACnetPrimitiveBitSTRING::Encode now check GetMinimumMemoryCount() instead of a GetValue() + 1 one-byte-tag assumption that undercounts once the tag’s LVT requires the extended-length form — both could write one byte past an exactly-undersized caller buffer. BACnetPrimitiveREAL’s identical-looking check was audited and found correct by construction (its LVT is always 4, never extended).

  • Batch 108 (#564): BACnetPrimitiveBOOL::Decode’s context-tag form now requires LVT == 1 (cl. 20.2.3) before reading its single content octet; an LVT of 0 or >= 2 is now refused instead of desynchronizing the enclosing decoder’s parse.

  • Batch 108 (#563): BACnetPrimitiveNULL::Decode’s two failure exits now set BACnetRejectReason::invalidTag on the shared error container, matching every sibling primitive decoder, instead of returning failure with stale or empty error state.

  • Batch 108 (#562): BACnetPrimitiveObjectIdentifier::GenerateObjectIdentifier and SetObjectInstance now cast objectType to uint32_t before left-shifting it by 22, removing undefined behaviour (an integer-promoted signed shift not representable in a 32-bit int) for object types 512-1023.

  • Batch 108 (#600): BACnetAbstractSyntaxType::DecodeByDatatype’s Any-value decode path now checks for a NULL Clone() result and a refused InsertObjectAndTakeOwnership() (previously unconditional, so an allocation failure could report a positive decode with the advertised child silently missing); the checked-insert fix generalizes to every datatype decoded through this shared path, not only Any. BACnetStackObjectHelpers::GetArrayProperty now builds into a local BACnetAbstractSyntaxType and commits it into the caller’s output only on full success, instead of inserting elements directly into the caller’s output and stopping mid-array on the first factory/insert failure.

  • Batch 108 (#598): BACnetObjectSpecifier::SetObjectType/SetObjectIdentifier now clear the opposing CHOICE alternative before setting the chosen one (a type-then-identifier sequence previously serialized the stale type, since Encode() prefers the object-type arm); operator= now resets the destination first for the same reason. Batch-close correction: the operator= rewrite above had no self-assignment guard (caught by check-self-assignment-guards.js in the batch’s own collect-all-failures build) — x = x emptied the object and then read the emptied members back onto themselves; a guard is now the first statement.

  • Batch 108 (#591): BACnetDateTime::SetDate/SetTime now return the child’s own IsValid() instead of always true; SetDateTime is now transactional — a call with an invalid date or time is refused before either member is touched, so the previous value survives a failed call intact instead of the parent reporting valid with an invalid child. Batch-close correction: the rejection branch also called SetValid(false) on the parent’s own validity flag, clobbering a previously-valid object’s state even though its date/time members were correctly left untouched — removed, so a rejected call now leaves the whole object, not just its members, exactly as it was.

  • Batch 108 (#590): BACnetAbstractSyntaxType::GetObjectByDatatypeAndTakeControl’s decode loop now treats a zero-progress decode as failure regardless of whether an error was set, closing a denial-of-service where a decoder returning 0 without setting an error looped forever re-decoding the same bytes.

  • Batch 108 (#584): RegisterCallbackReceiveMessage’s callback signature was out of sync between the DLL header (8 parameters) and the hand-written C#, Go, Java, and Python adapter bindings (6 parameters, missing the network-layer message type and vendor ID) — corrected in all four, plus the Java SimpleServerExample call site and its signature test.

  • Batch 108 (#585): the Rust adapter carried a dead bacnet_loop() binding for a BACnetStack_Loop export that does not exist (bacnet_tick() is the real one) and two commented-out bindings with stale names/signatures (BACnetStack_ValueUpdated → BACnetStack_UpdateValue, and the OctetString get-property’s actual 9-argument form); all three corrected.

  • Batch 108 (#586): the hand-written-adapter coverage gate (ci_scripts/hand-adapter-coverage.sh, ci_scripts/adapter-binding-symbols.js) previously checked only the node and C# adapters for dead bindings and nothing for callback parameter-count drift; added --python/--golang/--rust dead-binding modes and a new ci_scripts/adapter-callback-signature-check.js gate (with a MUST_FLAG/MUST_NOT_FLAG self-test corpus) that checks RegisterCallbackReceiveMessage’s parameter count across C#/Java/Python/Go — exactly the drift #584 found.

  • Batch 108 (#572): swsutilities::timegm’s GNU/Linux branch now fails to compile (#error) instead of falling off the end of a non-void function when none of __APPLE__/_BSD_SOURCE/_SVID_SOURCE/_GNU_SOURCE is defined — previously an indeterminate time_t could be returned silently. BACnetStackCommon::StringToBitstring now validates every character of the payload before writing anything: a bare "b" (no payload) or any non-binary character is now rejected instead of being silently treated as a clear bit; BACnetAbstractSyntaxType::DecodeString’s BitString case rejects the same malformed input end-to-end.

  • Batch 108 (#568): ChipkinConvert::IPAddressToBytes used an istringstream-based parse with no NULL check, no extraction-success check, no delimiter/trailing-input check, and no 0-255 range check — "999.999.999.999" and arbitrary junk were silently accepted. Rewritten as a hand-rolled scanner requiring the complete, well-formed dotted-quad form (leading zeros tolerated); BACnetSourceAddress’s IP constructor inherits the fix.

  • Batch 108 (#566): two hex-conversion helpers shared one root bug — a capacity guard computed with truncating integer division (size / 2) followed by a loop that effectively ran (size + 1) / 2 times, writing one byte past the destination on both odd-length and exact-capacity input. ChipkinConvert::HexStringToByteArray and ChipkinConvert::HexStringToBin are now two-pass (validate every character, then write) and reject odd-length input outright; the silent invalid-character-maps-to-zero behaviour (HexCharToInt) now has a checked counterpart (HexCharToIntChecked) that both use. BACnetStackCommon::StringToOctstring explicitly rejects an odd-length hex payload before any length math, closing a stack-buffer overflow reachable from BACnetStack_SendWriteGroup/WriteProperty/CreateObject with an application-supplied OctetString value near STACK_OPTION_MAX_OCTET_STRING_SIZE.

  • Batch 108 (#567): ChipkinConvert::ToValue<T> never checked whether its stringstream extraction succeeded or consumed the whole string, and BACnetAbstractSyntaxType::DecodeString had no failure branch for six primitive datatypes (Boolean, UnsignedInteger, SignedInteger, Real, Double, Enumerated) — malformed property text ("junk", "12junk", a negative value into an unsigned field, an out-of-range value, or "true" into a Boolean, which silently became FALSE) was accepted as a successful write. Added ChipkinConvert::TryParse<T>, which requires an exact, fully-consumed, non-overflowing parse; DecodeString’s six numeric arms and the ObjectIdentifier case (which also now range-checks the object type to 0-1023 and the instance to 0-4194303) now use it and return failure before inserting anything, preserving DecodeString’s documented accumulate-across-calls contract. Behaviour change, one statement for the #566/#567/#572 class: BACnetStack_SendWriteProperty/_SendCreateObject/_SendWriteGroup now reject malformed text values (junk numerics, non-binary bitstring payloads, odd-length or non-hex octet strings) they previously accepted silently.

  • Batch 108 (#554): AddAlertEnrollmentObjectWithoutAddObjectFirstFailsWithUnknownObject was vacuous — it failed for the wrong reason (a missing EnableNotificationClassProperties call) rather than exercising the #543 ObjectExists guard it was named for. Now calls EnableNotificationClassProperties first, so the guard under test is the only thing that can still fail the call.

  • Batch 108 (#555): added a File_Size Changes_Pending ladder test beside #498’s existing AtomicWriteFile one (the File_Size write path — cl. 12.56.100 — had no test coverage at all). BACnetStackNetworkPortSC::ReferencesCertificateFile/ReferencesWritableCertificateFile were byte-for-byte identical; collapsed to one function, deleted the dead ReferencesOperationalCredentialFile, and removed BACnetDBDevice::NotifyFileObjectChanged’s useWritableCheck parameter (a distinction with no difference).

  • Batch 108 (#570): added ci_scripts/check-todo-markers.js (wired into pre-push.sh): every TODO/FIXME/XXX/HACK marker in source//tests//adapters//ci_scripts/ must now carry a #NNN issue or Dnnn-sNNN ledger reference on its own line. Triaged the 9 markers the gate found: 5 filed as #612 (fixed-length primitive decoders reject a wrong-length tag with parameterOutOfRange, inconsistent with sibling decoders’ invalidTag), 2 filed as #613 (SC hub-connection-status “no-connection” placeholder, explicit supported limitation), 2 filed as #614 (two disabled BTL 8.3.5 tests need owner input), and 5 self-referential mentions in the gate’s own allowlist file reworded to avoid the literal marker word.

  • Batch 108 (#560): added docs/architecture/overview.md, a one-page contributor map of the codebase (six areas: external API/adapters, interface/business-logic boundary, object/property database, PDU/NPDU/data links, encode/decode/rendering, tests). Fixed a stale contributor-specific WSL path in AGENTS.md; surfaced .devcontainer/ with its startup command in AGENTS.md and BUILD.md; cross-linked BUILD.md, AGENTS.md’s gate list, and adapters/README.md.

  • Batch 108 (#559): rewrote README.md’s top section — replaced Modbus/GitLab-era build instructions (a dead Ubuntu 18.04/dotnet-sdk-2.1 toolchain, a personal path, a retired branch-merge flow) with a short contributing block and a corrected, verified directory listing; the NodeJS section and everything after it is unchanged. Added the same contributor links to docs/index.md.

  • Batch 106 (#433, #497, #498, #467, #452 — stacks on batch 105, not yet merged): BACnet/SC + BBMD hardening. BACnetSCBVLL::ClearDataOptions() (#433) now also clears the Data-Options control-flag bit it previously left asserted after clearing the options list — a cleared BVLL used to still encode with the bit high and no options present, corrupting the frame for the first caller relying on a true clear (the one live caller, a test fixup that always re-inserts every option immediately after, was unaffected). Read AB.1.5.2/AB.1.6/AB.1.7/AB.1.8/H.7.3 for #433’s second item (distinct per-port VMACs on a multi-port SC router) and found VMAC uniqueness is a per-NETWORK property, not per-device — a single VMAC per device is conformant; marked not-a-defect, no code change. SC_Direct_Connect_Initiate_Enable/Accept_Enable (#497) are now genuinely writable per cl. 12.56.94/.95 (split apart from the sibling scHubFunctionEnable, which correctly stays non-writable per its weaker “shall be configurable” wording) with full Changes_Pending/activate/discard shadow semantics — closing a batch-close review round-1 finding along the way: the resync that mirrors these two properties from the underlying BACnetDataLinkSC configuration was unconditional, so any of over a dozen triggers (any Tick(), any websocket status update, several other SC config-API calls) landing between a client’s WriteProperty and their own ReinitializeDevice(ACTIVATE_CHANGES) silently overwrote the pending value while Changes_Pending stayed stuck TRUE — now gated behind the same pending-changes guard the sibling hub-config sync already uses. Rewriting an already-bound certificate File object’s CONTENT (via AtomicWriteFile or a File_Size write) now correctly raises Changes_Pending on every SC Network Port referencing it (#498, cl. 12.56.100) — previously invisible since the port’s own File-object-identifier binding never changed. BACnetBBMD.cpp’s Forwarded-NPDU loop guard (#467) now uses J.4.5’s actual masked same-IP-subnet test (restored from previously-commented-out dead code, corrected to read the mask from the host BDT entry and to exclude the UDP port from the comparison entirely) instead of an exact address+port match — two BBMDs on the same subnet share an IP but differ in port, so the old compare double-broadcast a frame the subnet had already received directly from the peer. New v6 BACnet/SC integration manual (#452, docs/CAS BACnet Stack - BACnet SC Manual_v6.md) replacing the v5.3.5-stamped guide’s removed-API sections (certificate loading, File-object creation) and documenting the #497/#498 changes above; the v5 file is left in place for 5.x customers. Full ladder (falsified-first + revert-red or mutation-confirmed) on all four code-bearing issues; 2-round batch-close review loop (round 1 found and fixed the #497 resync gap above plus a vacuous test assertion in the same sprint’s own test; round 2 found and closed a coverage gap in round 1’s own fix — no test exercised a Tick() landing while a change was still pending — before finding the fix itself sound). pre-push.sh: 5774/5774 tests passed, ALL GATES PASSED.

  • Batch 105 close (collect-all-failures + 2-round review loop): the first full compile+test pass after sprints 128-132 found 7 test-only bugs (5 in this batch’s own new tests, 2 in pre-existing tests that pinned exactly the behavior #460 intentionally changed) — zero production defects at that stage; fixed as one attributed batch (docs/sprints/batch-105-metrics.md). The subsequent batch-close review loop then found and fixed a real production bug in #493’s DS-VSI-B capture: BACnetStackLightingOutput’s single per-object m_pendingCommandSource/m_pendingCommandTime fields are set synchronously by a direct write, but a WARN_OFF/WARN_RELINQUISH egress resolves LATER (Tick()’s natural expiry, or a subsequent unrelated write’s halt check) — any intervening write from a different client, at any priority, corrupted the eventually-recorded Value_Source attribution for the resolved priority. Fixed by snapshotting the arming client’s source/time at BACnetLightingEgressTimer::Arm() time and using that snapshot (not the live fields) when the egress resolves, on both the natural-expiry and halt-mid-write paths. Also closed a real test-coverage gap for #540 (its Priority range-validation check had zero coverage anywhere in the repo) and corrected two stale CASBACnetStackTestToolDLL.h doc comments (AddProgramObject/AddAlertEnrollmentObject) that still described the pre-#543 “creates the object” contract. pre-push.sh: 5769/5769 tests passed, ALL GATES PASSED.

  • Sprint 131 (#435, #460, #501, batch 105, build-light — verified at batch close; #511 investigated, does not reproduce): router/network correctness + BVLL sends. BACnetNetworkLayer::ProcessInitializeRoutingTable (#435) now answers cl. 6.4.7’s query form (Number-of-Ports=0) for a physical-only router — previously it answered nothing at all (“logic not yet implemented”). Finds a port id unused by any configured port (0 is not guaranteed free — AddPort doesn’t reserve it), calls BACnetPhysicalRouter::GetReachableNetworks with it to get the COMPLETE table (not the I-Am-Router-To-Network “exclude one port” shape), resolves each network’s port via LookupEgressPort, and omits (with a logged error, not an aborted Ack) any network the two calls disagree on. BACnetNPDU::DecodeDestinationAddress/DecodeSourceAddress (#460) now distinguish a DLEN/SLEN value out of range (cl. 6.4.4 reason 6 — “the length of the DADR or SADR was determined to be invalid”) from an ordinary truncated buffer via a new decode-transient flag; BACnetNetworkLayer::ProcessingIncomingNPDU, on a physical router, resolves the ingress port and DNET (parsed before DLEN, so it survives the failure) and emits the reason-6 reject that was previously silently dropped — gated tightly to the addressing-value case specifically, so an ordinary short/malformed frame does not become a reject-storm vector. Test-tool exports BACnetStackTestTool_BuildWriteBroadcastDistributionTable/SendWriteBroadcastDistributionTable/SendDeleteForeignDeviceTableEntry (#501, Annex J.4/J.5) are thin wrappers over BACnetInterface’s already-fully-implemented encode/send methods (export-wiring only, no new BVLL logic) — both are direct BBMD-to-BBMD unicasts, structurally unrelated to the Annex J.2.10 foreign-device broadcast addressing class. #511 (source-routed Who-Is/Who-Has replies) was investigated and found to already work correctly at the current tip: BACnetWhoIsProcessor/BACnetWhoHasProcessor never touch NPDU addressing, which is decided generically by the pre-existing BACnetNPDU::SetFromNetworkParameters (already targets the incoming request’s source network whenever networkParameters carries one) — the gap this issue reported no longer reproduces. New coverage, since no existing test exercised this specific shape (a source-routed request with no DNET of its own, answered by a non-router device): tests/TestBACnetInterface_SourceRoutedReply.cpp.

  • Sprint 132 (#545, batch 105, build-light — verified at batch close): the Abort(OTHER)-vs-Error(PROPERTY,invalidDataType) fork for wrong-datatype writes, audited and unified outside batch 100/103’s own files. Batch 103’s review loop already found this fork and fixed its own three new engines to use Error(PROPERTY, invalidDataType) (the more spec-appropriate response for a datatype mismatch); this sprint is the codebase-wide sweep #545 asked for. Enumerated by grepping every GetObjectByDatatype(...) == NULL check immediately followed by SetAbort(BACnetAbortReason::other) — 22 such sites across BACnetStackEventLog::SetEventLogProperty (6), BACnetStackTrendLog::SetTrendLogProperty (8), and BACnetStackTrendLogMultiple::SetTrendLogProperty (8) — all now answer Error(BACnetErrorClass::property, BACnetErrorCode::invalidDataType). The enumeration explicitly EXCLUDES two shapes that matched a broader SetAbort(other) grep but are not this fork: the genuinely malformed-call guards at the top of each of these same three functions (value == NULL/callbacks == NULL, left as Abort(OTHER) deliberately, matching batch 103’s own precedent), and the encode-side “could not retrieve an already-stored value as its expected type” checks in BACnetLogDatum/BACnetSingleLogData/BACnetSubscribeCOVProcessorHelpers (internal read-back-for-encoding invariants, not a client’s WriteProperty being rejected — a different failure class the issue’s own scope doesn’t cover). New discriminating tests (3 per file: a Boolean-typed property written with the wrong primitive, an UnsignedInteger-typed property written with the wrong primitive, and a control proving the value==NULL guard is unchanged) prove the fix and are mutation-gated (reverting to Abort(OTHER) fails them).

  • Sprint 130 (#493, batch 105, build-light — verified at batch close): DS-VSI-B, the cl. 19.5 value-source mechanism — scoped to stack-held Lighting Output objects. New BACnetStackValueSourceState (per-object, 16-slot arrays of BACnetValueSource/BACnetTimeStamp, mirroring the existing stack-held priority-array shape) tracks who commanded each priority slot and when. Capture is wired through the REAL WriteProperty pipeline end-to-end: BACnetConfirmedRequestProcessor → a new networkParameters parameter on BACnetWritePropertyProcessor::Process → a new commandSource parameter on BACnetBusinessLogic::SetProperty/BACnetDBDevice::SetGeneratedPropertyValue (all trailing, defaulted, non-breaking for every pre-existing call site) → BACnetStackLightingOutput::WriteDirectPresentValue/ApplyLightingCommand’s single write chokepoint (WriteArraySlotReal/WriteArraySlotNull, which every write path — direct writes, every Lighting_Command variant, WARN/WARN_OFF/WARN_RELINQUISH, STEP_*, STOP — already funnels through). Per cl. 19.5.1.2’s own “if the device instance is not known” fallback, the requester’s network ADDRESS is recorded (no Device-Address-Binding reverse-lookup from address to instance is attempted); a relinquish (NULL write) clears that priority’s recorded source. Value_Source/Last_Command_Time are served as the DERIVED view of whichever priority is currently winning (None/absent when every priority is NULL, per 7.3.1.28.3); Value_Source_Array/Command_Time_Array are served via the #544 GetArrayProperty helper. Documented scope cuts, not silent gaps: only Lighting Output is wired this sprint (Binary Lighting Output/Access Door/Timer share the same stack-held-priority-array shape and are natural follow-ups, not done here); the cl. 19.5.1.3 write-back ownership rule (a direct WriteProperty to Value_Source_Array[p]) is not implemented — all 4 properties are read-only this sprint, matching the required=false/writable=false declarations already present (but previously unimplemented) for these same property identifiers in several other object profiles in this codebase; COV inclusion (Table 13-1a-2) is not wired. New gate STACK_OPTION_DS_VSI_VALUE_SOURCE (implied by STACK_OPTION_ENABLE_ALL_BIBBS). No public/test-tool export surface changes — every new property is served through the existing generic ReadProperty pipeline. New tests/TestBACnetStackValueSourceMechanism.cpp.

  • Sprint 129 (#542, #543, batch 105, build-light — verified at batch close; #541 research-only, no code change): Program close-out + provisioning unification. BACnetStackProgram (#542) now tracks Out_Of_Service directly and derives it mechanically from Program_State in SettleState, per cl. 12.22.13’s unconditional “if Program_State has the value IDLE, then Out_Of_Service shall be TRUE” — settling into IDLE forces it TRUE, settling into any other state forces it FALSE (a documented engineering decision grounded in the same clause’s “in service means…properly loaded and initialized” definition text, since the clause itself only states the IDLE direction explicitly). Out_Of_Service is read-only on this object (matching the default property profile’s existing required=true/writable=false setting), rejected explicitly alongside the object’s other stack-settled properties. BEHAVIOUR CHANGE (#543): AddProgramObject and AddAlertEnrollmentObject now REQUIRE the generic AddObject() to be called first, converting them from their previous self-sufficient (create-the-DB-object-themselves) shape to the majority provisioning contract every other stack-held engine added since batch 103 already uses (AddLoadControlObject’s shape) — a caller relying on the old one-call convenience constructor now gets unknownObject/false instead of success; existing test fixtures for both were updated, and each ships a dedicated mutation-gated regression test proving the new rejection. Full audit of all 21 Add*Object engine-attachers in BACnetDBDevice.h (not sampled): 16 already required AddObject first (Averaging, LoadControl, LifeSafetyPoint, LifeSafetyZone, AccessDoor, AccessCredential, AccessRights, AccessZone, CredentialDataInput, AccessPoint, LightingOutput, BinaryLightingOutput, Timer, Channel, Group, Schedule); AddFileObject, AddEventEnrollmentObject, and AddNotificationForwarderObject remain self-sufficient by design (each has its own additional prerequisite-object gating already, a materially different shape from Program/AlertEnrollment’s plain pass-through) and were not converted by this issue. #541 (Figure 12-3 BTL double-check): 135.1-2023 cl. 7.3.2.22.1’s own Program test provides no edge-level transition table to cross-check against — its test steps only exercise a single generic “write a non-READY value, verify it settles” case and its own Notes to Tester explicitly punt validity to “depending on the current Program_State, and the implementation” — so the reconstructed table in BACnetStackProgram::IsRequestValid is neither confirmed nor refuted by the Method of Test; #541 stays open, honestly, pending the BTL rig or an authoritative Figure 12-3 rendering.

  • Sprint 128 (#549, #540, #547, #546, batch 105, build-light — verified at batch close): primitive/decode debt from batches 103-104’s review loops. BACnetPrimitiveDATE::IsUnspecified() (#549) no longer treats a wildcarded (0xFF) Weekday octet the same as a wildcarded Year/Month/Day — Weekday is fully derivable from Year/Month/Day and carries no independent date information, so a concrete Year/Month/Day with the common BACnet “don’t care about weekday” convention is now correctly resolvable by BACnetDateTime::ToTimeT/OnOrAfterTime/BeforeTime instead of being silently refused as unspecified; operator<’s Weekday tiebreak (which produced a wrong ordering whenever one side’s Weekday was wildcarded and the other’s was not) is removed — two dates with equal Year/Month/Day are equal under operator< regardless of Weekday. Every IsUnspecified/IsUnspecifiedOrDatePattern/ToTimeT/operator< call site in the stack was audited (TrendLog/TrendLogMultiple/EventLog start/stop-time gating, Access Credential’s Tick, COV/Database timestamp derivation) — all are consistent with the fix and none relied on the old behaviour. BACnetStackAccessCredential’s local ToComparableTimeT retry-with-normalized-weekday workaround (added in batch 104 sprint 123) is retired now that the fix lives in the primitive itself. BACnetPropertyValue::Decode/DecodeWithObjectType (#540) now validate a wire-decoded Priority field against cl. 15.1.1.3’s 1-16 range (previously validated only on the programmatic-construction path via SetPriority) — an out-of-range value poisons the element (IsValid()==false) rather than hard-failing the whole decode, so the bytes are still consumed (the caller’s SEQUENCE OF loop keeps decoding) and the element becomes reachable by existing loop-level “!IsValid()” rejection (e.g. ChangeOfReliability::Decode, the #529 mechanism this issue was filed to make reachable from real wire traffic). BACnetStackTimer::GetProperty’s stateChangeValues/listOfObjectPropertyReferences OOM-while-cloning paths (#547) now poison the error container (Abort(other)) before returning false, matching the sibling OOM pattern elsewhere in this codebase — previously a real out-of-memory condition fell through BACnetDBDevice::GetGeneratedPropertyValue’s dispatch and was misreported as unknownProperty. ci_scripts/node-adapter-parity.sh’s allowlist comparison (#546) is now deduplicated (sort -u, matching the already-deduplicated actual set) so an accidental duplicate allowlist line degrades gracefully instead of spuriously failing CI as a “stale” entry.

  • Batch 104 close (#486-#492 + #544): the access-control object family (Access Door/Credential/User/Rights/Zone/Credential Data Input/Access Point, cl. 12.26/12.31-12.36) is verified and closed out. This is the batch’s own first real compile+test pass — sprints 124-127 ran under an experimental “build-light” mode (verification deliberately deferred to batch close; see docs/runbooks/runbook-issues.md‘s “EXPERIMENT — build-light batch mode”). Found and fixed 6 attributed defect rows getting the build/test suite green (9 individual errors counting each link error separately; one row test-only — counts reconciled by the planner’s pre-merge review, and this sentence is the canonical form): (three missing explicit BACnetStackObjectHelpers::GetArrayProperty<T> template instantiations/link errors; an infinite-recursion stack overflow and a NULL-function-pointer segfault, both in Access Point’s BACnetDBDevice dispatch; two writable=true-but-required=false property-profile gaps that silently disabled otherwise-correct writable properties), then 4 more real defects (two of them spec-conformance bugs independently confirmed by two of four parallel review passes) via a full-diff review loop: Access Rights’ cross-object negative-rule pooling (cl. 12.34.9.2) was OR’d per-object instead of pooled, letting one object’s positive rule override a different object’s veto; Access Zone’s SOFT_PASSBACK was enforced identically to HARD_PASSBACK instead of “checked and reported but not enforced” (cl. 12.32.21); Access Point’s Threat_Level check ran during authentication instead of authorization, firing DENIED_THREAT_LEVEL even under Authorization_Mode=NONE/DENY_ALL; and a misconfigured Access_Doors entry was silently treated as a successful door pulse instead of reporting LOCKED_BY_HIGHER_PRIORITY. Full fix→sprint attribution in docs/sprints/batch-104-metrics.md on the batch branch. pre-push.sh: 5719/5719 tests passed, ALL GATES PASSED.

  • Sprint 127 (#488 part 2, batch 104): Access Point authorization + door commanding (cl. 12.31.14/.23-.25/.32.1), completing the family’s factor → credential → rights → zone → door story. BACnetDBDevice::AccessPointRequestAccess is the real end-to-end entry point: dispatches on Authorization_Mode (AUTHORIZE/GRANT_ACTIVE/DENY_ALL/NONE — VERIFICATION_REQUIRED/AUTHORIZATION_DELAYED are out of scope this batch, need an async external-verification state machine), evaluates the credential’s Assigned_Access_Rights (sprint 124’s engine), checks passback and occupancy-limit enforcement against Zone_To/Zone_From, and on grant adjusts zone occupancy and pulses Access_Doors at Priority_For_Writing. UseAccessCredential now fires exactly once with the FINAL post-authorization outcome (revised out of sprint 126’s AccessPointAuthenticateFactor, which is authentication-only). Test-tool exports + full node-adapter chain (deferred from sprints 124-127 to one consolidated pass at batch close, per the batch’s own acceleration rules).

  • Sprint 126 (#488 part 1, batch 104): Access Point authentication engine (cl. 12.31). New BACnetStackAccessPoint implements 12.31.12.1’s single-factor synchronous authentication sequence (Lockout → Out_Of_Service → factor-read-error → credential resolution via a new linear search of every stack-held Access Credential’s Authentication_Factors → Credential_Status → success), with Authentication_Status served as a pure function of Out_Of_Service rather than stored free-running state (multi-factor policy sequencing/timeout is explicitly out of scope), and ms-driven Lockout_Relinquish_Time/Failed_Attempts_Time countdowns (mirroring BACnetStackAccessDoor‘s own established ms-timer idiom, so a sub-second Loop() tick cadence can’t truncate a whole-second countdown to a no-op). Also fixes a pre-existing gap: Threat_Level/Threat_Authority had no working codec anywhere in the stack (BACnetStackDatatypes::AccessThreatLevel dead-ended) — retyped both to UnsignedInteger in the property profile. Lockout/Threat_Level/Active_Authentication_Policy are flipped to BACnet-writable per their clauses’ explicit “shall be writable” language.

  • Sprint 118 (#476, batch 103): the stack now owns the Program object engine (cl. 12.22) — Program_State/Program_Change/Reason_For_Halt/Description_Of_Halt/Program_Location were previously served as inert host-supplied data with no state machine at all. New BACnetStackProgram gates a Program_Change write with the READY rule (cl. 12.22.5: a non-READY Program_Change rejects any further write outright) then validates it against a reconstructed Figure 12-3 transition table; an accepted request parks Program_Change at the requested value, offers it to the host via a new FPCallbackProgramChangeRequested callback, and auto-completes synchronously to the spec-conformant terminal state when no host callback is registered or it declines to handle the request asynchronously. Corrects the originating issue’s own plan: cl. 12.22 explicitly states Program objects doing intrinsic reporting “shall only perform the first stage of reliability-evaluation and shall not apply a standardized fault algorithm… shall not apply an event algorithm” — this engine does not compose the generic A&E algorithm core. The extracted Figure 12-3 text is OCR-scrambled (not a clean table); the implemented edge set is a documented, best-effort reconstruction — see the follow-up filed for a BTL-level double-check against an authoritative rendering [#541]. Test-tool-only exports (CASBACnetStackDLL.h untouched): AddProgramObject, SetProgramState, RegisterCallbackProgramChangeRequested. New tests/TestBACnetStackProgram.cpp.

  • Sprint 119 (#477, batch 103): the stack now owns the Averaging object engine (cl. 12.5) — a tick-driven sliding-window ring buffer sampler that previously did not exist. Every Window_Interval/Window_Samples seconds, the stack reads the referenced local property, converts it to Real per cl. 12.5.13’s Boolean/Integer/Unsigned/Enumerated/Real conversion rule, and records the sample — evicting the oldest ring-buffer slot and recomputing Minimum/Maximum/Average/Variance_Value (+ Min/Max timestamps) from the buffer’s currently-valid entries. A write to Attempted_Samples (only zero accepted), Window_Interval, or Window_Samples (must be > 0) invalidates the whole buffer and resets to the clause’s INF/NaN/NaN/-INF sentinels (Table 12-5 footnote 1). Variance_Value’s formula is not given explicitly by the clause; implemented as population variance over the window’s valid samples, consistent with Average_Value’s own explicit “divide by the number of non-missed buffer samples” wording, pinned by a hand-calculated literal test case. Remote Object_Property_Reference sampling is explicitly deferred, mirroring BACnetStackTrendLog’s own remote-reference precedent — local-only, with an explicit “not yet implemented” log line rather than a silent no-op. Test-tool-only export: AddAveragingObject. New tests/TestBACnetStackAveraging.cpp.

  • Sprint 120 (#478, batch 103): the stack now owns the Load Control object engine (cl. 12.28, Figure 12-5 shed state machine) — the batch’s largest engine. Implements every Figure 12-5 edge (ReceiveShedRequest, CancelShed, Reconfigure*/UnsuccessfulShedReconfigured/SuccessfulShedReconfigured, CannotMeetShed/AbleToMeetShed, FinishedUnsuccessful/SuccessfulShed, CanNowComplyWithShed/CanNoLongerComplyWithShed) against a ticked clock, keyed precisely per the clause text: the INACTIVE→PENDING edge fires specifically on a Start_Time write (not any of the four request properties), while the Reconfigure edges fire on a write to any of the four once already active. The optional PrepareToShed pre-Start_Time early-shed edge is explicitly out of scope, per the clause’s own “may choose to” wording. Corrects a wrong claim in the originating issue’s own test plan: cl. 12.28.18 does NOT reject a LEVEL write outside the configured Shed_Levels array — it snaps down to the nearest numerically-lower configured entry; the cited MOT clause (135.1-2023 §7.3.2.38.2) is actually just a Shed_Levels-writability test and says nothing about rejection. A new FPCallbackLoadControlShed host callback answers “can you achieve this level, and what did you land on” — the stack owns every state/timing decision on its own, and when unregistered assumes full compliance so a bare/host-less test IUT can exercise the complete state machine. Reuses the existing generic BACnetChangeOfStateEventAlgorithm for intrinsic reporting, no new algorithm class. Test-tool-only exports: AddLoadControlObject, SetLoadControlShedLevels, SetLoadControlShedLevelDescriptions, SetLoadControlFullDutyBaseline, SetLoadControlEnable, RegisterCallbackLoadControlShed. New tests/TestBACnetStackLoadControl.cpp.

  • Sprint 125 (#492, batch 104) (verified at batch close — the historical build-light tag is retired from this entry): Credential Data Input (cl. 12.36) and the CDI COV “never fires on identical content” defect investigation. Pre-flight found the S62/S63-era COV plumbing already correct: BACnetSubscribeCOVProcessor::GetSubscriptionProperty watches Update_Time (not Present_Value) for credentialDataInput, and the generic COV comparison arm compares whatever’s watched — so once a real engine exists that advances Update_Time on every genuine update (per 12.36.11’s unqualified “shall update its value on each update of the Present_Value”), the existing mechanism notifies correctly with no COV-code changes needed. New BACnetStackCredentialDataInput supplies exactly that: Update_Time uses the Sequence-Number BACnetTimeStamp variant (deliberately not DateTime, sidestepping #549’s Weekday-comparison gap entirely) and bumps unconditionally on every PushFactor/direct-write call; Present_Value follows 12.36.4’s UNDEFINED/ERROR rules (an unsupported or errored factor type is served as UNDEFINED/ERROR with Format-Class forced to 0). Fixed the verified AST-factory gap: BACnetAbstractSyntaxType::ObjectFactory had no arm for BACnetStackDatatypes::AuthenticationFactor (its sibling AuthenticationFactorFormat already did) — added, gated identically. The property profile was already fully correct (no changes needed). A new discriminating regression test (tests/TestBACnetStackCOVSubscription.cpp) pins the exact defect scenario — two identical-content pushes with advancing Update_Time must both report “changed” — plus a negative control proving the test isn’t vacuous. This test’s actual verdict has not been observed yet VERDICT OBSERVED AT BATCH CLOSE: PASSED against the pre-existing COV machinery — task 1’s PASS branch; no COV-engine change was needed, as the static reading predicted (correction applied by the planner’s pre-merge review; #492’s only attributed close fix was a missing GetArrayProperty<BACnetAuthenticationFactorFormat> instantiation). Test-tool-only exports + full node-adapter chain. New tests/TestBACnetStackCredentialDataInput.cpp.

  • Sprint 124 (#489 + #491, batch 104) (verified at batch close — the historical build-light tag is retired from this entry): the stack now owns Access Rights rule evaluation (cl. 12.34) and Access Zone occupancy/passback tracking (cl. 12.32). New BACnetStackAccessRights implements the full cl. 12.34.9.1/12.34.9.2 per-object evaluation: Enable gates the whole object, negative rules are evaluated before positive rules (any enabled match denies outright), then positive rules in array order (first enabled match grants); each rule’s Time-Range (ALWAYS, or SPECIFIED with the clause’s Unsigned/Integer/BACnetBinaryPV/Null coercion table, read via BACnetBusinessLogic::GetProperty against the referenced object) and Location (ALL, a specific Access Point by identity, or an Access Zone via its Entry_Points) both gate the match alongside the rule’s own Enable field. This is the per-object building block; the cross-object “walk every Access Rights object a credential is assigned” authorization check is sprint 127’s job. New BACnetStackAccessZone implements Occupancy_State derivation (cl. 12.32.6, with a documented tie-break when Occupancy_Count equals both limits at once), Adjust_Value’s atomic write sequence (cl. 12.32.13 — store, then add-with-zero-floor-clamp or reset-to-zero-on-zero-write, respecting both Occupancy_Count_Enable and Out_Of_Service, which have different effects on the sequence), Credentials_In_Zone/Last_Credential_Added/Last_Credential_Removed tracking via internal Enter/Exit entry points (exposed for direct testing now; sprint 127’s Access Point engine is the eventual caller), and lazy passback-violation detection keyed per credential against Passback_Timeout. The Adjust_Value property profile is flipped to writable (cl. 12.32.13’s own mechanism requires it); Occupancy_Upper_Limit writes reject a nonzero value not greater than the configured lower limit, per the clause. Test-tool-only exports for both object types (CASBACnetStackDLL.h untouched), full Node adapter chain. New tests/TestBACnetStackAccessRights.cpp, tests/TestBACnetStackAccessZone.cpp. Verification for this sprint is deferred to batch close verification COMPLETED at batch close (5719/5719 green; this sprint’s attributed close fixes: the GetArrayProperty<BACnetAccessRule> instantiation and the adjustValue/occupancyCount required-flag trap — see docs/sprints/batch-104-metrics.md); the always-report-COV-even-on-identical-value nuance for Last_Credential_Added/Last_Credential_Removed (cl. 12.32.17/.19) is a documented, known gap not wired this sprint (the underlying data model updates correctly on every Enter/Exit; only the COV-suppression override for the same-credential-re-entering-back-to-back edge case is unimplemented).

  • Sprint 123 (#487 + #490, batch 104): the stack now owns Access Credential derivation (cl. 12.35) and Access User referential integrity (cl. 12.33). New BACnetStackAccessCredential derives Credential_Status purely from whether Reason_For_Disable is empty, with five independent disabling sources (Activation_Time/Expiration_Time windows, Days_Remaining, Uses_Remaining, Absentee_Limit, and the writable Credential_Disable command) each mapping to a distinct disable-reason value and recomputed fresh on every read/tick — no separate add/remove bookkeeping needed, since no two sources can ever produce the same reason. Belongs_To’s DISABLED_UNASSIGNED trigger is deliberately never auto-added (the clause conditions it on an unobservable site policy — documented scope decision, not a gap). A new internal Use() entry point (exposed for direct testing this sprint; sprint 126’s Access Point engine is the eventual caller) decrements Uses_Remaining every granted access and Days_Remaining at most once per calendar day of granted-access use, then stamps Last_Access_Point/Last_Access_Event/Last_Use_Time unconditionally — granted or denied — per 12.35.21’s explicit wording. Review found and fixed a real defect before commit: BACnetPrimitiveDATE’s comparison operators treat a wildcarded Weekday octet (0xFF, BACnet’s normal “don’t care” convention for a field fully derivable from Year/Month/Day) the same as a fully-unspecified date, silently disabling Activation_Time/Expiration_Time evaluation for any credential configured the ordinary way; worked around locally in BACnetStackAccessCredential (a shared-code fix was judged too broad-blast-radius for this sprint — filed as a follow-up issue). New BACnetStackAccessUser is deliberately minimal per cl. 12.33’s own “not directly involved in authentication… used for informational purposes” wording: write-time validation only, rejecting Members/Member_Of references that aren’t Access User objects and Credentials references that aren’t Access Credential objects (12.33.12-14); Status_Flags was confirmed already correct ahead of this sprint (IN_ALARM/OVERRIDDEN/OUT_OF_SERVICE pinned FALSE, FAULT tracks Reliability) and needed no change. Test-tool-only exports for both (CASBACnetStackDLL.h untouched), full Node adapter chain including node-adapter-parity-allowlist.txt entries. New tests/TestBACnetStackAccessCredential.cpp, tests/TestBACnetStackAccessUser.cpp.

  • Sprint 122 (#544 + #486, batch 104): shared array-property helper, then the stack now owns the Access Door object engine (cl. 12.26). New BACnetStackObjectHelpers::GetArrayProperty<T> factors the “propertyArrayIndex==0 returns count / out-of-range sets invalidArrayIndex / whole-array walk” shape that had been independently reimplemented 7 times across BACnetStackLoadControl (Shed_Levels/Shed_Level_Descriptions), BACnetStackLifeSafetyObject (Accepted_Modes/Alarm_Values/Fault_Values/Life_Safety_Alarm_Values), and BACnetStackLifeSafetyZone (Zone_Members) into one templated helper taking a per-element-type factory function; all 7 sites converted, behaviour-preserving (existing tests are the regression suite), plus one direct test per element-construction flavor (primitive ctor/new+SetValue/Clone). New BACnetStackAccessDoor (cl. 12.26) implements the full commandable Present_Value state machine — LOCK/UNLOCK/PULSE_UNLOCK/EXTENDED_PULSE_UNLOCK through a stack-held 16-slot priority array (mirroring the batch-101 lighting/timer priority-array convention), pulse auto-relinquish keyed to whichever priority slot is actually winning (not merely the raw commanded value — a same-value pulse write superseding at a different priority gets its own fresh, correctly-timed countdown rather than inheriting a stale one), Door_Unlock_Delay_Time-gated LOCK→unlock transitions, Door_Open_Too_Long_Time tracking per cl. 12.26.19’s three conditions, and Door_Alarm_State derivation with Masked_Alarm_Values substitution (cl. 12.26.20/.21 — masking a currently-active alarm state reverts it to NORMAL immediately). Secured_Status (cl. 12.26.14) is derived at the BACnetDBDevice level from Door_Status/Lock_Status/Present_Value/Status_Flags.IN_ALARM/Masked_Alarm_Values, since it needs cross-property reads the object engine itself has no visibility into. Reuses the existing generic BACnetChangeOfStateEventAlgorithm for intrinsic reporting on Door_Alarm_State (already mapped accessDoor→doorAlarmState; no new algorithm) and the existing D86 HasAccessDoorValueChanged COV path — AddAccessDoorObject enables the profile-OPTIONAL Door_Alarm_State/Secured_Status properties at Add time, matching Load Control’s Shed_Levels precedent. Provisioning follows the require-AddObject-first contract (AddLoadControlObject’s shape), not Program’s self-creating outlier (#543). Test-tool-only exports (CASBACnetStackDLL.h untouched): AddAccessDoorObject, SetAccessDoorRelinquishDefault, SetAccessDoorTimers, SetAccessDoorMembers, SetAccessDoorMaskedAlarmValues, with full Node adapter N-API bindings. New tests/TestBACnetStackObjectHelpers.cpp, tests/TestBACnetStackAccessDoor.cpp.

  • Sprint 121 (#485, batch 103): the stack now owns Life Safety Point/Zone object-level state (cl. 12.15/12.16) — Mode/Accepted_Modes enforcement, the Present_Value latching vs. Tracking_Value split, Silenced, and LifeSafetyOperation’s actual effect were previously entirely host-owned (the CHANGE_OF_LIFE_SAFETY event algorithm and FAULT_LIFE_SAFETY fault algorithm already existed and were never the gap — confirmed per the originating issue’s own doc-correction). New BACnetStackLifeSafetyObject shared core (Point and Zone both derive from it) latches Present_Value on any non-quiet Tracking_Value transition until a RESET-family operation; RESET unlatches unconditionally, RESET_ALARM/RESET_FAULT only unlatch when the currently-latched value is classified in Alarm_Values/Fault_Values respectively (a documented reading — the clause names the enum values but doesn’t itself elaborate the split); SILENCE/UNSILENCE drive both audible+visual channels while the _AUDIBLE/_VISUAL variants touch only their own channel and combine rather than overwrite. BACnetLifeSafetyOperationProcessor now validates the operation against the host callback (if registered) BEFORE applying it to the stack-held object — never the reverse, since a rejected operation must leave state untouched (135-2024 13.13.2: a Result(-) means the operation did not happen) — then still offers the callback unchanged for existing host integrations, broadcasting to every Zone_Members entry for a zone-addressed request. Corrects a wrong claim in the originating issue’s own implementation plan: clause 12.16 does NOT define a Zone Present_Value precedence rule (“alarm > trouble > … > quiet”) — 12.16.4 is the identical “local matter” wording as the Point’s own clause, with no ordering given anywhere. Implemented as a simple, documented 2-tier convention instead: a Zone is quiet only when every member is quiet; otherwise it reports the first non-quiet member found in Zone_Members’ own fixed order, with nested zones settling via a bounded fixed-point recompute loop. Test-tool-only exports: AddLifeSafetyPointObject, AddLifeSafetyZoneObject, SetLifeSafetyZoneMembers, SetLifeSafetyAcceptedModes/AlarmValues/FaultValues/LifeSafetyAlarmValues, SetLifeSafetyTrackingValue. New tests/TestBACnetStackLifeSafetyPoint.cpp, tests/TestBACnetStackLifeSafetyZone.cpp.

  • Sprint 106 (#480 foundations, batch 101): a millisecond-resolution elapsed-time source for BACnetDBDevice::Loop, and a stack-held BACnetPriorityArray component landed via a minimal BACnetStackLightingOutput object shell - the two seams sprints 107-109 (Timer, Lighting Output, Binary Lighting Output engines) build on. No engine behaviour yet (no fade/ramp/step/warn, no timer countdown) - this sprint lands only the foundations, with tests proving them end-to-end. The ms tick: a new optional FPCallbackGetSystemTimeMs callback (registered via the new test-tool-only BACnetStackTestTool_RegisterCallbackGetSystemTimeMs, since the customer-facing CASBACnetStackDLL.h is frozen and a production registration need is an owner decision per the batch plan) lets a host supply a higher-resolution monotonic-or-wall-clock millisecond source; BACnetDBDevice::ComputeElapsedMs, called once at the top of every Loop(), prefers it and otherwise derives a coarse millisecond value from the existing seconds-resolution currentTime Loop() is always called with - never assuming a call rate, since the owner’s #474 question about how often the host calls Loop() is still open. Either source clamps a backward clock jump to 0 elapsed rather than underflowing an unsigned subtraction. The stack-held priority array: BACnetBusinessLogic::GetPriorityArray now serves a Lighting Output object’s Present_Value priority array directly from its own BACnetPriorityArray when the device holds one (BACnetDBDevice::GetLightingOutput), before falling through to the host-callback dispatch every other object type still uses unchanged - scoped by BOTH object type and instance number, so an Analog Output object sharing a Lighting Output test object’s instance number is unaffected (regression-tested explicitly, and confirmed by mutation: dropping the object-type half of that check fails exactly that regression test and nothing else). BACnetDBDevice::SetGeneratedPropertyValue gained an internal (non-exported) priority parameter so it can write into that array on a commandable write, honouring cl. 19.2’s relinquish (NULL-write) and next-highest-priority-wins semantics; priority 6 is deliberately NOT reserved for Lighting Output, since cl. 19.2.3’s reservation is scoped verbatim to “the Present_Value property of a Binary Output object or a Binary Value object” with the optional Minimum_On_Time/Minimum_Off_Time properties - a test pins priority 6 as an ordinary accepted slot. BACnetPriorityArray::GetHighestNonNullValue is a new reusable cl. 19.2 winner-selection helper sprints 108/109 need for their own Present_Value evaluation. A test-only BACnetDBDevice::AddLightingOutputObjectForTest (not exported anywhere) lets this sprint’s gtests exercise the full SetProperty/GetProperty dispatch chain end-to-end; sprint 108 replaces it with the real public AddLightingOutputObject API. New tests/TestBACnetStackFoundations106.cpp.

  • Sprint 107 (#479, batch 101): the stack now owns the Timer object engine (cl. 12.57), replacing the host-emulated timer-engine.ts the testing tool previously had to supply. New BACnetStackTimer (modelled on BACnetStackSchedule’s tick-driven shell shape and BACnetStackEventEnrollmentObject’s object-owned GetProperty/SetProperty dispatch) implements the full 7-transition state machine (BACnetTimerTransition: idle-to-running / running-to-idle / running-to-running / running-to-expired / forced-to-expired / expired-to-idle / expired-to-running) with the accept/ignore/reject write semantics of cl. 12.57.5 (Present_Value)/12.57.10 (Timer_State)/12.57.11 (Timer_Running): writing an in-range non-zero Present_Value (or Timer_Running=TRUE) forces RUNNING from any prior state, including RESTART-while-RUNNING (runningToRunning, resetting Present_Value/Initial_Timeout/Expiration_Time to the new timeout - deliberately NOT the “accepted but ignored” behaviour the host emulation had), writing 0 (or Timer_Running=FALSE) while RUNNING is a forced-expire request, and the same while IDLE/EXPIRED (or Timer_State=IDLE while already IDLE) is a documented no-op. Present_Value/Initial_Timeout/Default_Timeout/Min_Pres_Value/Max_Pres_Value/Resolution are all MILLISECONDS (cl. 12.57.5/.15-.19 verbatim) - the host emulation treated Present_Value as seconds, which this engine does not port. BACnetDBDevice::RunSchedule‘s ~45-line List_Of_Object_Property_References write-fan-out (internal targets via CallbackSetProperty*, external targets via BACnetBusinessLogic::SendExternalWriteProperty through the device’s DAB) is extracted into a new shared BACnetDBDevice::WriteValueToPropertyReferences, called by both RunSchedule (behaviourally unchanged - every existing Schedule test still passes against the extracted helper) and the new State_Change_Values fan-out, so there is exactly one write-dispatch implementation for both object types rather than a second fork. That fan-out is keyed by transition, not by state (State_Change_Values[transition], cl. 12.57.20’s “array index equal to the numerical value of the BACnetTimerTransition enumeration” - the host emulation kept one slot per STATE, which cannot distinguish e.g. idleToRunning from expiredToRunning), and writes at Priority_For_Writing (cl. 12.57.22), not the host emulation’s hard-coded priority 8. BACnetDBDevice::RunTimer (tick-driven, from Loop(), using sprint 106’s ms-resolution elapsedMs) and the timer branch of SetGeneratedPropertyValue (write-driven) share one FireTimerTransition tail for the fan-out + CHANGE_OF_TIMER evaluation (BACnetInterface::UpdateValue(deviceInstance, timer, objectInstance, timerState) - the same internal pipeline the host’s own UpdateValue(timerState) call used, guarded for a NULL m_interface the same way every other NULL-interface-safe engine on BACnetDBDevice is). A device with no Timer objects costs a single m_timers.empty() check per Loop() call. Scope cut, not a gap: State_Change_Values’ constructed-value/datetime/lighting-command CHOICE alternatives are not yet supported by the fan-out (only the applicationPrimitive alternative - Real/UnsignedInteger/SignedInteger/Enumerated/Boolean, the same 5 primitives the extracted write helper already handles); an element written with one of those three CHOICEs is silently skipped rather than fabricated. New test-tool-only exports (source/CASBACnetStackTestToolDLL.h - CASBACnetStackDLL.h is FROZEN and untouched): BACnetStackTestTool_AddTimerObject, …_SetTimerStateChangeValueNoValue, …_SetTimerStateChangeValue, …_AddTimerObjectPropertyReference, …_SetTimerPriorityForWriting, each with a BACnetTestToolInterface/BACnetDBDevice two-layer implementation mirroring the Schedule exports’ own shape; being test-tool-only they need no node-adapter-parity-allowlist entry (the generator classifies those automatically). New tests/TestBACnetStackTimer.cpp.

  • Sprint 108 (#480, engine part 1, batch 101): the stack now owns the Lighting Output fade/ramp/step engine (cl. 12.54), extending sprint 106’s stack-held priority array shell with BACnetStackLightingOutput’s Tracking_Value/In_Progress/Transition/Default_Fade_Time/Default_Ramp_Rate/Default_Step_Increment/Lighting_Command_Default_Priority state and the write-driven (ApplyLightingCommand) + tick-driven (Tick) engine that computes them. Lighting_Command (FADE_TO/RAMP_TO/STEP_UP/STEP_DOWN/STEP_ON/STEP_OFF/STOP/NONE) writes the target level to the priority array slot then re-evaluates it (cl. 12.54.6) - a FADE_TO/RAMP_TO whose priority does NOT win the re-evaluation is stored but does not start interpolating (the discriminator this sprint’s tests pin against a naive “always fade” implementation); STEP_UP/STEP_DOWN/STEP_ON/STEP_OFF apply instantly (no interpolation, per Table 12-67’s literal text - not the sprint brief’s initial assumption that they clamp to Min/Max_Actual_Value); STOP halts and writes the current Tracking_Value back into the array slot. A direct Present_Value write/relinquish with Transition=FADE/RAMP starts an interpolation using Default_Fade_Time/Default_Ramp_Rate (cl. 12.54.19); Transition=NONE (the default) snaps instantly. A higher-or-equal-priority write halts an in-progress command (cl. 12.54.6.1). Spec discrepancy resolved against the verified clause text, not the sprint brief: the brief assumed Table 12-65 maps Present_Value’s special value -4.0 to STOP; the actual 135-2024 text extracted for this repo maps -4.0..-7.0 to RESTORE_ON/DEFAULT_ON/TOGGLE_RESTORE/TOGGLE_DEFAULT instead (there is no special-value STOP at all - STOP is Lighting_Command-only). This implementation follows the verified text: -1.0..-7.0 are all recognized-and-no-op’d this sprint (none are stored literally in the priority array, per cl. 12.54.4), WARN/WARN_RELINQUISH/WARN_OFF are Sprint 109’s job (#481, blink-warn + Egress_Time), and RESTORE_ON/DEFAULT_ON/TOGGLE_RESTORE/TOGGLE_DEFAULT are out of scope for both 108 and 109. New optional FPCallbackLightingOutputTrackingValueChanged (test-tool-registered via BACnetStackTestTool_RegisterCallbackLightingOutputTrackingValueChanged, following sprint 106’s FPCallbackGetSystemTimeMs precedent) is the physical-output hand-off, fired from BACnetDBDevice::RunLightingOutput (the Loop() tick, mirroring RunTimer’s shape and zero-cost-with-zero-objects guard) whenever Tick reports Tracking_Value changed. New test-tool-only exports (CASBACnetStackDLL.h is FROZEN and untouched): BACnetStackTestTool_AddLightingOutputObject (ADDITIVE to sprint 106’s test-only AddLightingOutputObjectForTest, which is unchanged and still used by that sprint’s own tests), …_SetLightingOutputTransition, …_SetLightingOutputActualRange, …_RegisterCallbackLightingOutputTrackingValueChanged. Egress_Time/Blink_Warn_Enable/Feedback_Value configuration (SetLightingOutputEgress/SetLightingOutputFeedbackValue) is Sprint 109’s job - not added here. New tests/TestBACnetStackLightingOutput.cpp.

  • Sprint 109 (#480 close-out + #481, batch 101): blink-warn/egress on Lighting Output, and a new Binary Lighting Output engine (cl. 12.55), sharing one warn-egress countdown component between them. New BACnetLightingEgressTimer (source/BACnetLightingEgressTimer.h/.cpp) is the single-slot “one priority may have an active egress timer at a time” state machine cl. 12.54.6.2/12.55.10.1 both specify verbatim - BACnetStackLightingOutput and the new BACnetStackBinaryLightingOutput each hold one instance and call its Arm/Cancel/ExpireImmediately/Tick directly (proven shared, not parallel copies, by a cross-type test that mutates the shared class once and confirms both engines’ tests fail). BACnetStackLightingOutput::ApplyLightingCommand’s WARN/WARN_OFF/WARN_RELINQUISH cases (previously recognized-and-no-op’d) and WriteDirectPresentValue’s -1.0/-2.0/-3.0 special values (previously recognized-and-no-op’d) now implement cl. 12.54.6.2’s blink-warn choreography verbatim: WARN fires a notification only if Blink_Warn_Enable is TRUE, Present_Value isn’t 0.0%, and the priority is the current command priority (array/Tracking_Value always unchanged); WARN_OFF/WARN_RELINQUISH fire a notification then arm Egress_Time (seconds on the wire, per cl. 12.54.14’s literal text - converted to ms only at Arm() time) before writing 0.0%/relinquishing, or resolve immediately with no notification/delay when their own (a)/(b)/(c) suppression conditions are met. STOP is EXTENDED (not forked) to also cancel a pending egress at the same priority WITHOUT touching the array (a different sub-behaviour from its existing fade/ramp halt, which DOES write Tracking_Value back). A new higher-or-equal-priority write (direct or via Lighting_Command) now also halts a pending egress, APPLYING its effect immediately (cl. 12.54.6.1) rather than dropping it - unlike a fade/ramp halt. Design decision (undocumented by the clause; corrected during batch-101 review — the version below is what shipped): a second WARN_OFF/WARN_RELINQUISH at a DIFFERENT priority never needs to touch an already-pending egress at all — arming an egress never changes the priority array, so the pending egress’s priority stays the winning one for as long as it is active, and any request at a lower priority is suppressed by its own (a)/(c) “not the current command priority” condition before ever reaching the egress state; a request at the same or a higher priority is correctly halted by the existing HaltPendingEgressAtOrAbove check, which already runs before every non-STOP dispatch. The single BACnetLightingEgressTimer member per object therefore never needs to hold two priorities at once, by construction, with no separate “second warn” handling required. New Egress_Time/Egress_Active/Blink_Warn_Enable are read-only “R” properties (no WriteProperty path - configured only via the new test-tool BACnetStackTestTool_SetLightingOutputEgress, the one API #480’s original scope deferred), and a new optional FPCallbackLightingOutputBlinkWarn (…_RegisterCallbackLightingOutputBlinkWarn) hands the notification to the host, following FPCallbackLightingOutputTrackingValueChanged’s exact pattern. #480’s clause behaviours are now fully stack-owned; Feedback_Value remains host/callback-only (the default property profile already marks it non-stack-served, matching this sprint’s own choice for Binary Lighting Output below). New BACnetStackBinaryLightingOutput (source/BACnetStackBinaryLightingOutput.h/.cpp, cl. 12.55) implements the discrete ON/OFF priority-array engine: WritePresentValue handles OFF/ON/WARN/WARN_OFF/WARN_RELINQUISH/STOP/TOGGLE per cl. 12.55.4’s Table 12-70 - suppression conditions deliberately different wording from Lighting Output’s own ((a) priority not the highest active priority, (b) checks the value at that slot rather than the evaluated Present_Value for WARN/WARN_RELINQUISH, (c) WARN_RELINQUISH additionally checks whether the next-highest non-Null priority, including Relinquish_Default, would already be ON) - and TOGGLE’s “numerically higher priority than the current command priority ⇒ successful but array unmodified” rule (cl. 12.55.4.1 covers the same higher-priority-halt-with-immediate-effect and implicit-same-priority-overwrite rules as 12.54.6.1). Elapsed_Active_Time/Strike_Count (cl. 12.55.18/.20) accrue against the evaluated Present_Value (the spec permits either Present_Value or Feedback_Value as “a local matter”); a zero-write resets Time_Of_Active_Time_Reset/Time_Of_Strike_Count_Reset to BACnetDBDevice’s most recent Loop() wall-clock time (SetGeneratedPropertyValue has no now parameter of its own to thread through). Feedback_Value IS stack-held here (unlike Lighting Output) via the new test-tool BACnetStackTestTool_SetBinaryLightingOutputFeedbackValue - a host-pushed readback, absent until configured, never written over BACnet. New optional FPCallbackBinaryLightingOutputPresentValueChanged/FPCallbackBinaryLightingOutputBlinkWarn callbacks (Polarity applied only at the callback boundary, never to the stored logical value); unlike Lighting Output’s Tick-only physical-output notification, the present-value-changed callback also fires synchronously on any write that changes the evaluated Present_Value, since binary transitions are always instant. BACnetDBDevice gains m_binaryLightingOutputs (mirrors m_lightingOutputs exactly: Loop() tick, Reset() cleanup, GetBinaryLightingOutput) and BACnetBusinessLogic::GetPriorityArray gains an explicit binaryLightingOutput stack-held-priority-array gate (the sprint brief’s assumption that the generic commandable-Present_Value path already covered this for free was WRONG - without this gate, Present_Value reads fell through to unregistered host callbacks and failed; Lighting Output’s own sprint 106 gate was the precedent this mirrors). New test-tool-only exports: …_AddBinaryLightingOutputObject, …_SetBinaryLightingOutputFeedbackValue, …_RegisterCallbackBinaryLightingOutputPresentValueChanged, …_RegisterCallbackBinaryLightingOutputBlinkWarn. STACK_OPTION_OBJECT_TYPE_BINARY_LIGHTING_OUTPUT already existed (unused) in CASBACnetStackOptions.h; this sprint is its first consumer. New tests/TestBACnetStackBinaryLightingOutput.cpp; tests/TestBACnetStackLightingOutput.cpp extended for the blink-warn/egress close-out.

  • Node adapter: N-API wrapper for BACnetStackTestTool_SendWhatIsNetworkNumber (cl. 6.4.19). The native export existed with no wrapper at all — a BTL testing tool measuring its own export coverage found this as one of seven unbound network-message sends. Test-tool-only (no CASBACnetStackDLL.h production twin), gated behind BACNET_STACK_TESTTOOL alongside the existing COV-notification wrappers, following SendWhoIsRouterToNetworkWrapped’s own parameter-marshaling pattern including its buffer-length validation (#392). ci_scripts/generate-node-adapter.js regenerated (250→251 napi exports across the two commits in this change); ci_scripts/node-adapter-parity-allowlist.txt gains test-tool:SendWhatIsNetworkNumber.

  • Node adapter: N-API wrapper for BACnetStack_SendNetworkNumberIs (cl. 6.4.20), a production customer-facing export with no wrapper. The router core already sends this automatically — BACnetNetworkLayer::ProcessWhatIsNetworkNumber’s own dispatch calls the C export directly when answering an incoming What-Is-Network-Number — so this binding is for direct/manual control, not a dependency of that automatic path; no behaviour change for any existing caller. Removes the now-stale dll-only:BACnetStack_SendNetworkNumberIs line from the parity allowlist (the wrapper closes that divergence).

  • Node adapter: N-API wrappers for the five Event Enrollment property-reference/fault-discriminator test-tool exports that had none — SetEventEnrollmentAlgorithmPropertyReference, SetEventEnrollmentFaultAlgorithmPropertyReference, SetEventEnrollmentFaultStatePropertyStatesType, AddEventEnrollmentFaultExtendedParameter, SetEventEnrollmentFaultExtendedParameters. Without these a node-driven IUT could not supply the mandatory property reference that floating-limit/command-failure/change-of-life-safety/change-of-timer/access-event Event_Parameters need, nor fault-state’s discriminator, nor fault-extended’s vendor identity — so those alternatives refused to build on a real IUT. Modeled on the existing AddEventEnrollmentObjectWithExtendedAlgorithm/AddEventEnrollmentExtendedParameter wrappers; test-tool: parity-allowlist entries added for all five (needed because these are real napi bindings, not just orphaned C exports). [#475]

  • Sprint 111 (#483/#484 foundation, batch 102): two shared seams the rest of the batch builds on. The “offer event notification to local objects” hook — BACnetEventNotificationOfferConsumer (an abstract registration interface) plus BACnetDBDevice::Register/UnregisterEventNotificationOfferConsumer and OfferEventNotificationToLocalObjects — lets any number of local objects see the stack’s own and received event notifications without SendEventNotification/BACnetEventNotificationProcessor::Process knowing what they are; both the send and receive paths now call the hook once per notification, ahead of the existing per-recipient fan-out. The internal recipient fan-out generator is the reusable “build one notification, deliver it to every recipient in a list” path both #482 (Alert Enrollment) and #484 (Notification Forwarder) need; it also carries the new forceAckRequiredFalse parameter Alert Enrollment’s cl. 12.52.8 semantics require, defaulted false for every existing caller so no other notification class is affected. Also closes two batch-101 strays discovered in these same files: #530 (diagnose-first) and #531 (a tiny fix). CORRECTION (2026-08-31, planner post-merge review): FALSE — neither #530 nor #531 was touched by this batch. The routing edit that added them to sprint 111’s brief was committed after the sprint had already run against the pre-routing brief; the close-out was then written from the routed plan. Both issues remain open and are scheduled in batch 103. (Runbook L74.)

  • Sprint 112 (#483, batch 102): the stack now owns the Event Log engine (cl. 12.27). New BACnetStackEventLog mirrors BACnetStackTrendLog’s ShouldLog gate and LoggedRecordRef/CheckLogBufferReady/BUFFER_READY wiring, registers as a BACnetEventNotificationOfferConsumer (sprint 111’s hook) so it captures the stack’s own and received event notifications automatically, and gates each direction independently via Capture_Initiated/Capture_Received (Enable/StopWhenFull/RecordCount-purge follow the TrendLog pattern verbatim, including the reserved-slot fill-boundary behaviour). New test-tool exports SetEventLogCapturePolicy/SetEventLogStartStopTime. New tests/TestBACnetStackEventLog.cpp.

  • Sprint 113 (#482, batch 102): the stack now owns Alert Enrollment (cl. 12.52). New BACnetStackAlertEnrollmentObject (modelled on BACnetStackEventEnrollmentObject’s object-owned GetProperty/SetProperty shape, deliberately not embedded in the generic BACnetStackAlarmAndEventObject — the two have too little in common to share a base) originates alerts via BACnetDBDevice::RaiseAlert, routed through sprint 111’s internal recipient generator with forceAckRequiredFalse=true — the discriminating behaviour cl. 12.52.8 specifies and the one a plain re-use of the generator would have gotten wrong. New tests/TestBACnetStackAlertEnrollmentObject.cpp, plus an Event Log integration test proving a raised alert is captured end-to-end.

  • Sprint 114 (#484 part 1 of 2, batch 102): Notification Forwarder core (cl. 12.51) — the object, Accepts()’s five loop-prevention/filter gates (Out_Of_Service, Process_Identifier_Filter, Port_Filter, Local_Forwarding_Only, and the mandatory global-broadcast-is-ignored rule), and CollectTargets()’s Recipient_List/Subscribed_Recipients fan-out, reusing BACnetDestination::IsValidDayAndTime and BACnetStackMessageGenerator::RecipientAcceptsTransition rather than re-implementing day/time and transition filtering. Real production bug found and fixed during implementation (not a test bug): BACnetProcessIdSelection::Reset() sets its choice to noChoice (never-configured), not nullValue (explicit Null) — Accepts()’s Process_Identifier_Filter check was written as != nullValue, which treated every freshly-constructed Notification Forwarder as filtered on process identifier 0 and rejected everything; changed to the correct == processIdentifier affirmative check. New test-tool exports for object creation, recipient configuration and filter properties. New tests/TestBACnetStackNotificationForwarder.cpp.

  • Sprint 115 (#484 part 2 of 2 + batch close, batch 102): Notification Forwarder close-out — Subscribed_Recipients list-service (AddListElement/RemoveListElement/whole-list SetProperty) reusing batch 100’s #466 generic list-merge extension points, Time_Remaining minute-countdown Tick() with silent-expiry persistence-callback hand-off (cl. 12.51.9), and the loop-prevention rules this batch’s plan named (a)/(b)/(d)/(e) (rule (c) is out of scope, tracked separately). Discovered during pre-flight, not a fundamental limitation as sprint 114 had assumed: BACnetNetworkParameters already carries GetIngressPortId/GetSourceNetwork/ReceivedMessageWasBroadcasted, live-populated on the receive path, so Port_Filter (rule (e)) needed no new plumbing. Batch close, round 1 (subagent review): fixed AddNotificationForwarderObject to be get-or-create (a repeat call for the same instance leaked the object and permanently registered it as an orphaned notification consumer — same class of bug AddAlertEnrollmentObject already avoided), added missing preprocessor guards to the two sprint-112 Event Log test-tool exports (latent compile failure under STACK_OPTION_TARGET_EMBEDDED + Event Log without Alarms-and-Events), corrected stale comments claiming sprint 115 built the NF local-origin (“NC recipient == self”) forwarding leg (it did not — filed as #534), and strengthened two Event Log capture-policy tests that only verified the suppressed direction. Batch close, round 2 (a second subagent review, run after re-gating round 1 — the round-1 commit had been made without a gate run because the build environment was briefly unavailable): round 1’s own new regression test had a test bug of its own (asserted a notification would forward while the test itself had set Local_Forwarding_Only=true on a notification from a non-owner device, which Accepts() correctly rejects) — fixed. Two independent review passes (semantic-conflict and fresh-eyes) then converged on one real defect: EnableEventLogProperties (sprint 112) had the identical leak/ghost-consumer bug round 1 fixed for Notification Forwarder, never back-ported to Event Log — fixed the same way, with a new test-only GetEventNotificationOfferConsumerCountForTest() accessor and regression test, since GetEventLog() pointer identity cannot observe this particular bug. A third, narrowly-scoped review round confirmed the fix and found nothing further. (A 2026-08-31 planner correction briefly withdrew this sentence as “unevidenced”; that withdrawal was wrong — it rested on a truncated read of docs/sprints/batch-102-metrics.md, whose merged copy records round 3 in full: 2026-08-31T19:24→20:05Z, 41 min, 2 targeted subagents, both PASS, quiet round. Withdrawal retracted 2026-08-31; the sentence stands. Runbook L79.)

  • Fault_Parameters is WRITABLE over BACnet, and Fault_Type is correctly refused as read-only (cl. 12.12.22 / 12.12.23). An Event Enrollment previously received no writes at all: it had no SetProperty and GetAlarmAndEventObject returns NULL for eventEnrollment at every one of its call sites, so it was unreachable from the WriteProperty dispatch. A correction to earlier entries in this section: they said cl. 12.12.22 makes writing Fault_Type select the algorithm. It does not. Verbatim, 12.12.22 says Fault_Type is a “read-only property” that “is determined by the chosen fault parameters in the Fault_Parameters property”, and 12.12.23 says Fault_Parameters “determines the fault algorithm used to monitor the referenced object and provides the parameter values needed”. The writable property is Fault_Parameters; Fault_Type is derived. The behaviour shipped earlier was right — re-arming replaces the algorithm — but the citation was inverted, and making Fault_Type writable would have been a conformance defect. Writing Fault_Parameters now selects the algorithm from the CHOICE tag, configures it through a new ApplyFaultParameters (the inverse of BuildFaultParameters, implemented for all 8 alternatives), and attaches it build-then-commit — a new SetFaultAlgorithmAndTakeControl means a failed write cannot replace a working algorithm with a correctly-typed but unconfigured one. Writing Fault_Type returns PROPERTY / WRITE_ACCESS_DENIED explicitly rather than falling through to unknownProperty, which is what a read-only property owes a writer. Two rules the clause names are implemented and one is unreachable, deliberately recorded rather than dressed up: an unsupported fault algorithm must return PROPERTY / OPTIONAL_FUNCTIONALITY_NOT_SUPPORTED, and that path cannot currently be reached because all 8 fault algorithms are registered unconditionally in every build and a BACnetFaultParameter can only carry tags 0–7 — the mutation reverting that error code survives the suite, and says so. fault-state’s written values supply the BACnetPropertyStates discriminator the arming API could not, and a list mixing arms is refused rather than having one picked for it. The EE gets its own branch in the write dispatch rather than widening GetAlarmAndEventObject: phase B declined that widening (31 call sites changing at once), and faultParameters / faultType are not classified as alarm-and-event property identifiers so they would not reach that block anyway. [#475]

  • The last two Fault_Parameters alternatives — fault-extended [2] and fault-state [4]. All 8 CHOICE alternatives are now served, completing #475 phase D’s Fault_Parameters work. fault-extended was a third instance of the same stub: like the notification and event extended codecs before it, BACnetFaultParameterFaultExtended had no parameters [2] member at all and both Encode and Decode returned 0 with “not yet implemented”. Its algorithm stored only whether a fault had been detected, so its identity was missing too — vendor-id [0] and extended-fault-type [1] are both mandatory and, unlike a property reference, have no plausible-looking placeholder: emitting zeros would name vendor 0’s fault type 0 as though an operator had configured it. Both are now stored and supplied through new test-tool exports. The element class is SHARED with the event production, and that is a reversal worth explaining. Part 1 deliberately refused to share the event and notification element classes because their fourteenth alternatives genuinely differ (reference [0] vs property-value [0]). Checked verbatim against clause 21, the event and fault productions do not: the only difference is that the event one names date/time as DatePattern/TimePattern where the fault one names them Date/Time — the same application tags and the same encoding, differing only in whether wildcard octets are permitted. So the reason not to share does not apply, and this reuses BACnetEventParameterExtendedParameter rather than adding a third near-identical class. fault-state needed a discriminator, not a field. Its list-of-fault-values [0] is a SEQUENCE OF BACnetPropertyStates — a discriminated CHOICE — while the algorithm stores undiscriminated uint32_t and only learns at Run() time whether the monitored property is Unsigned or Enumerated. Fault_Parameters must be readable before any run, so which arm the configured values belong to is now recorded at arming time. There is no safe default: picking an arm silently re-types the operator’s configuration into a different property-state space while still encoding cleanly. The two non-enumerated arms (unsignedValue, booleanValue) are routed to their own setters rather than through SetEnumeratedValue. Both algorithms clear the new state in Reset, so a reset algorithm cannot report “not configured” while still holding the previous configuration. [#475]

  • The three fault alternatives needing a property reference, and Fault_Type replace semantics (#475 phase D parts 3b/3c). fault-life-safety [3], fault-status-flags [5] and fault-listed [7] each carry a BACnetDeviceObjectPropertyReference that is mandatory in their production and that no arming API supplied — fault-status-flags’s algorithm stores nothing else at all, so the reference is the entire content of its production. New BACnetStackTestTool_SetEventEnrollmentFaultAlgorithmPropertyReference supplies it, taking Fault_Parameters from 3 of 8 alternatives to 6 of 8. It is a separate export from its event-side twin rather than a flag on it: an enrollment can carry an event algorithm and a fault algorithm simultaneously, each with its own reference, so one entry point would have to guess which was meant. Re-arming an EE with a different Fault_Type now REPLACES the algorithm rather than being refused. Phase C refused it, which was strictly better than the alternative at the time — without a check the first algorithm was handed back, cast to the second type and configured through the wrong vtable — but refusing is not what cl. 12.12.22 says, which is that Fault_Type selects the algorithm. Replacing discards the previous algorithm’s reliability state; that is inherent in changing Fault_Type, and is why phase C would not do it as a side effect of arming. Re-arming with the same type deliberately does not churn the algorithm, so an ordinary re-configuration does not throw away reliability state. The intrinsic path is untouched: there the object type derives the algorithm, so there is no Fault_Type for a caller to change and no replace case. [#505] is fixed but remains latent, and the mutation run says so. SetFaultAlgorithmByObjectType now builds before it commits, so it no longer leaks on replace or detaches on failure — but its single call site is still guarded by “nothing attached yet”, so the defect is still unreachable and the mutation reverting the fix survives the suite. Recorded rather than dressed up as a gated fix: the handoff predicted part 3c would make it reachable, and it does not, because the replace semantics land on the ByFaultType sibling instead. [#475]

  • Fault_Type and Fault_Parameters are served on the Event Enrollment object (cl. 12.12.22 / 12.12.23). Phase C armed EE fault algorithms natively but modelled neither property, so an EE armed that way could not be read back over BACnet at all. Fault_Type is now served for every enrollment, and Fault_Parameters for 3 of the 8 CHOICE alternatives — none [0], fault-characterstring [1] and fault-out-of-range [6]. With no fault algorithm attached both answer NONE, which is a real value meaning “no fault detection is configured”, not a missing one, so unknownProperty would be wrong there. BuildFaultParameters mirrors the event side, including the rule that earned its keep: the base is the NONE fault algorithm and answers none [0] only for faultType none. That matters more here than on the event side — none [0] does not mean “unknown”, it means “no fault detection”, so a derived algorithm that forgot to override and inherited it would tell a peer the enrollment does no fault detection while it is actively detecting faults. fault-out-of-range’s min-normal-value [0] / max-normal-value [1] are each a CHOICE over Real / Unsigned / Double / Integer, and the armed datatype selects the arm: emitting the wrong one re-types the operator’s configured bound while still encoding cleanly, so a Real 20.0 would read back as an Unsigned. The other 5 are precisely characterised and deliberately not served, all for want of state the stack never stored — verified against 135-2024 clause 21 with the search bounded to the BACnetFaultParameter production. fault-life-safety [3], fault-status-flags [5] and fault-listed [7] each need a mandatory BACnetDeviceObjectPropertyReference (fault-status-flags’s algorithm stores nothing at all); fault-extended [2] needs vendor-id, extended-fault-type and parameters while its algorithm stores only whether a fault was detected — its identity is absent, not just a field; and fault-state [4]’s list-of-fault-values [0] is a SEQUENCE OF BACnetPropertyStates, a discriminated CHOICE, while the algorithm stores undiscriminated uint32_t values and only learns at Run time whether they are Unsigned or Enumerated — and Fault_Parameters must be readable before any run. Each refuses rather than guessing an arm or fabricating a mandatory field. [#475]

  • The five Event_Parameters alternatives that needed a property reference the stack never stored — floating-limit [4], command-failure [3], change-of-life-safety [8], change-of-timer [22] and access-event [13] each carry a BACnetDeviceObjectPropertyReference that is MANDATORY in their production, while the arming API only ever supplied the resolved value (a setpoint, a feedback value, a mode). Verified verbatim against 135-2024 clause 21 rather than from the in-repo comment — setpoint-reference [1] BACnetDeviceObjectPropertyReference, not OPTIONAL. With nothing storing the reference there was no conformant answer to build, so those five kept returning unknownProperty after part 2a. New BACnetStackTestTool_SetEventEnrollmentAlgorithmPropertyReference supplies it, and all 19 alternatives are now served. The surface change is additive on purpose: extending the five existing SetIntrinsic*Algorithm signatures would have been a breaking change for every current caller, so this is one new test-tool export instead — which is also where the Event Enrollment surface already lives (the three AddEventEnrollmentObjectWith* exports have no customer-facing twin). One generic entry point rather than five, because each algorithm has exactly one such reference and therefore knows which field it is; the base class refuses the call for an algorithm whose production has no reference field rather than accepting and ignoring it, so a caller cannot come to believe it configured something it did not. Being a BACnetStackTestTool_* export it needs no parity-allowlist entry (the generator classifies those automatically), and node-adapter-parity.sh stayed OK. The subtle one is FLOATING_LIMIT. Its production carries low-diff-limit [2] / high-diff-limit [3] — differences from the setpoint — while the algorithm stores m_lowLimit = setpoint - errorLimit and m_highLimit = setpoint + errorLimit. Emitting the stored limits would report the wrong quantity while looking entirely plausible (46 and 54 are reasonable REALs), so both diff limits carry m_errorLimit; the test asserts the correct values and asserts the absolute ones absent. Each of the five refuses to build until its reference is supplied — emitting the SEQUENCE without a mandatory field, or with a default-constructed reference, would put a fabricated object/property identifier on the wire as configuration. [#475]

  • Event_Parameters is served on the Event Enrollment object (cl. 12.12.7) — a REQUIRED property that answered unknownProperty until now, so a plain ReadProperty walk of an EE violated cl. 12.12. Each event algorithm gains BuildEventParameters, which constructs the BACnetEventParameter CHOICE alternative describing what that algorithm was armed with; the EE’s GetProperty builds through it. 14 of the 19 alternatives are served by this change — none [20], all four *-out-of-range arms, unsigned-range, buffer-ready, change-of-bitstring, change-of-state, change-of-value, change-of-characterstring, change-of-discrete-value, change-of-status-flags and extended. The design decision that matters is in the base class. The registry maps BACnetEventType::none to a plain BACnetEventAlgorithm, so the base class is the NONE algorithm and none [20] is its correct answer — but it returns that only when its event type is none. Every derived algorithm inherits the base until it overrides, so an unconditional base would make an algorithm that forgot to override report none [20]: a well-formed value a peer cannot distinguish from a correct one. Refusing instead degrades to unknownProperty, which is wrong-but-visible rather than wrong-and-plausible, and it is what the five not-yet-served alternatives do today. Three arms are deliberately not field-for-field copies: unsigned-range omits the deadband the algorithm carries for API symmetry but the production does not have; change-of-value’s cov-criteria [1] is itself a CHOICE and only the armed arm is emitted; and extended’s pParameters must be converted, not copied — the algorithm holds the notification flavour of that element (whose fourteenth alternative is property-value [0]) while Event_Parameters needs the event flavour (whose fourteenth is reference [0]), so an element that is a property-value is refused rather than silently dropped. Also corrected: a second wrong ASN.1 comment in BACnetEventParameter.h. The change-of-discrete-value [21] arm was documented as SEQUENCE { time-delay [0] Unsigned }; 135-2024 clause 21 gives it new-value [0] CHOICE {…} and status-flags [1] BACnetStatusFlags — alone among the SEQUENCE arms it mirrors the notification production rather than carrying configuration. The implementation was already right and the comment was wrong, which is the opposite of what it looked like: an unbounded spec search ran past the end of the production into the next one and made the class look defective. Verified by bounding the search to the BACnetEventParameter production itself. [#475]

  • extended [9] BACnetEventParameter — encode and decode, replacing the stub that made the configured half of a proprietary event algorithm unrepresentable. BACnetEventParameterExtended existed with vendor-id and extended-event-type as typed scalars and no parameters [2] member at all; its Encode and Decode logged “not yet implemented” and returned 0, and its IsEqual documented that it did not compare the deferred list — so two Event Enrollments configured with different proprietary parameters compared equal. The CHOICE factory has always built this class for BACnetEventParameter::extended, so a BACnetEventParameter whose active alternative was [9] failed to decode outright: the arm was reachable and dead. This is what cl. 12.12.7 calls the property that “provides the parameter values needed” by the algorithm, and cl. 13.3.10 defines those as pVendorId, pEventType and pParameters. parameters [2] SEQUENCE OF CHOICE is modelled as a typed element (BACnetEventParameterExtendedParameter), not an opaque blob, for the reason recorded on the notification-side twin: BACnetAbstractSyntaxType::Encode emits no enclosing tags, so a decoded context-tagged alternative re-encodes as a bare value and silently changes meaning. The two extended CHOICEs are deliberately NOT merged. Thirteen of their fourteen alternatives are identical, but the fourteenth is not editorial: this production has reference [0] BACnetDeviceObjectPropertyReference where BACnetNotificationParameters has property-value [0] BACnetDeviceObjectPropertyValue — a reference names a property, a property-value carries one. Sharing one element class would make an Event_Parameters list silently accept a notification-shaped element, so neither class decodes the other’s context-tagged arm. vendor-id is Unsigned16 and is bounded on both sides — SetVendorId refuses a wider value and Decode rejects one from a non-conformant peer — and Decode fails a truncated parameters list rather than accepting a short read, since a SEQUENCE OF loop that exits on buffer exhaustion reports a clipped list as a complete one. IsEqual now compares the list (length and element values, in order — it is a SEQUENCE OF, so order is part of the value) and additionally checks the CHOICE tag before its static_cast: every BACnetEventParameter* alternative reports the same GetDatatype(), so the datatype check alone does not establish that the other object is this alternative. That is latent, not live — BACnetEventParameter::IsEqual compares the tag before it reaches any alternative — and the guard is added because that wrapper is currently the only thing making the cast safe; the identically-shaped check in the other 21 alternatives is deliberately left alone so this diff stays scoped. Also corrected: the in-repo ASN.1 comment for extended [9] in BACnetEventParameter.h listed nine alternatives where 135-2024 clause 21 has fourteen — it omitted characterstring, date, time and objectidentifier, and wrote integer Unsigned where the standard has two separate arms, unsigned Unsigned and integer Integer. Checked verbatim against the extracted standard, not from memory. The new element class needs its own BACnetStackDatatypes value, and it is appended at the end of that enum, gated, rather than inserted next to its sibling. Inserting shifts every value below it, and the comment at the existing NotificationParametersExtended entry asserted that was safe because the enum “is not persisted, not on the wire, and appears in no adapter or DLL header” — that is wrong, and it cost a red suite to find out. BACnetStackWritePropertyAsync byte-serializes a datatype across the test-tool flat boundary, and TestBACnetInterface_WritePropertyAsync.cpp pins m_dataType = 75; // DailySchedule as a literal; an insertion above DailySchedule turned those two tests red. The misleading comment is corrected in place and now says to append, as EventLogDatum already did. The two existing mid-enum entries are left where they are — they sit below everything currently pinned, which is the only reason they are harmless. What this does NOT do: the Event Enrollment object still answers unknownProperty for Event_Parameters — nothing yet builds one of these from a configured algorithm, and Fault_Type / Fault_Parameters remain unmodelled. This is the codec those need, landed on its own, exactly as the notification-side codec landed before the algorithm that drives it. [#475]

  • Test tool: the Event Enrollment fault path, and the three fault algorithms nothing could arm — BACnetStackTestTool_SetFaultStateAlgorithm, …_SetFaultCharacterstringAlgorithm and …_SetFaultExtendedAlgorithm. FAULT_STATE (cl. 13.4.5), FAULT_CHARACTERSTRING (13.4.2) and FAULT_EXTENDED (13.4.3) each had a class, a registry entry in EnsureFaultAlgorithmsRegistered and their own gtests, and no way to arm any of them — not from the customer API, not from the test tool, not intrinsically. They were unreachable code whose tests exercised the classes directly and therefore never noticed. [#475] describes this as “three fault algorithms have classes but no public SetFault* API”; it was worse, because an Event Enrollment could not be given a fault algorithm of ANY type, for two independent reasons that both had to go. SetFaultAlgorithmByObjectType(eventEnrollment) looks up the EE’s own object type in a table of intrinsic fault behaviour and finds nothing — cl. 12.12.22 makes an EE’s Fault_Type property choose the algorithm, so there is no object type to derive it from; hence the new SetFaultAlgorithmByFaultType. And IsFaultTypeSupportedForObjectType(faultType, eventEnrollment) answered no for the same reason its event-side twin did: an EE is EXTERNAL (cl. 12.12), so “is eventEnrollment among the object types that detect fault X intrinsically” is a different question from the one the caller is asking, and always answers no. One shared EnsureFaultAlgorithm seam now creates the algorithm the way the source requires — by Fault_Type for an EE, by object type otherwise — and the four existing EnableFault* route through it, which is why GetEventAlgorithmTarget is renamed GetAlarmAndEventTarget: a seam whose name describes only half its callers is how the next bug gets written. That seam also verifies the algorithm that ended up attached is the one being armed, which is new behaviour rather than a refactor — an object whose type maps to a different fault algorithm was previously cast to the requested type and configured through the wrong vtable. Re-arming an EE with a different fault type is consequently refused for now rather than silently mis-armed; replacing a live algorithm means destroying its reliability state, which belongs with the phase that makes Fault_Type a real writable property. FAULT_CHARACTERSTRING re-arming replaces the value list rather than appending to it: 13.4.2 makes the match a SUBSTRING test, so an accumulated stale value reports a fault the operator never configured. Test-tool surface, following the existing Event Enrollment exports — note the deliberate asymmetry that the four existing SetFault* siblings are customer-facing, which is flagged rather than resolved here. A gate that was green for the wrong reason: every gate passed before the node layer existed, because node-adapter-parity.sh models three categories — dll-only, test-tool, and no-c-twin — and a test-tool C export with no napi twin falls through all three, so three exports nothing could call were invisible to it; adding the wrappers is what made it see them (FAIL→OK on three deliberate entries). Same shape as a hollow test: green because the case falls outside what the check models, not because the case is fine. Five named mutations were run against the change, and three initially survived — including one where the test called Reset() between two runs and Reset() clears the very value list under test, so the assertion held no matter what the arming did. All three now fail the test that owns them. SetFaultAlgorithmByFaultType builds the algorithm and only then commits it, rather than assigning the factory result straight over the owned m_faultAlgorithm pointer: the obvious spelling leaks the old algorithm on a replace and, on a factory failure, leaves the object holding NULL - reporting false while having silently detached a perfectly good algorithm the caller was told nothing about. The identically-shaped pre-existing sibling SetFaultAlgorithmByObjectType is deliberately left alone and filed as [#505]. Still open: Fault_Type and Fault_Parameters are not modelled on the EE object, so an EE armed this way is configured natively and cannot be read back or written over BACnet. [#475]

  • Test tool: the EXTENDED Event Enrollment surface — BACnetStackTestTool_AddEventEnrollmentObjectWithExtendedAlgorithm, …_AddEventEnrollmentExtendedParameter and …_IndicateEventEnrollmentExtendedTransition. The cl. 13.3.10 algorithm shipped one change earlier but nothing outside the stack could construct one: the registry could build it, no caller could name it. Test-tool only, which is where the Event Enrollment surface already lives — the three existing AddEventEnrollmentObjectWith*Algorithm exports have zero hits in CASBACnetStackDLL.h or BACnetInterface.h, so this follows them rather than opening a customer-facing surface for a proprietary algorithm. Three calls rather than one, because a proprietary algorithm is not configured the way the others are: 13.3.10 leaves the conditions to the vendor, so the stack owns the parameters, the notification and which transitions are legal — but not the evaluation, which is pushed in and consumed by the next algorithm run. pParameters elements are supplied as the element’s application-tagged wire encoding rather than through typed setters: one entry point then covers all thirteen primitive alternatives instead of thirteen exports, and it decodes through the same element decoder the receive path uses, so anything it accepts is by construction something the stack can read back off the wire. Exactly one value per call — trailing bytes mean the caller passed two, and keeping the first would hide it. One deliberate departure from the siblings, and it is load-bearing: there is no monitored-property datatype check. Those constructors validate because their algorithms operate on a specific datatype (OUT_OF_RANGE needs a REAL; the bitstring form of CHANGE_OF_VALUE needs a BIT STRING), but 13.3.10 places no restriction on what an EXTENDED algorithm monitors — the monitored value is the vendor’s to interpret — so a check here would make EXTENDED narrower than the clause. The node adapter’s derived files are regenerated rather than hand-edited, the buffer parameter carries a ValidateBufferLength guard naming it (the C side reads length octets through that pointer), and node-adapter-parity.sh went FAIL→OK: it reported three new napi/C-header divergences, now three deliberate test-tool: allowlist entries. [#475]

  • The EXTENDED event algorithm (cl. 13.3.10) — the last absent arm of clause 13.3, which spans subclauses 13.3.1–13.3.19 with 13.3.17 being NONE. The stack had 17 of those 18 non-NONE algorithms as registered classes; BACnetEventType::extended was the one the factory could not build, so a vendor-proprietary event algorithm could not exist at all. To be precise about what this does not claim: BACnetEventType also carries changeOfReliability, which is not a clause-13.3 algorithm — it is the event type produced by fault detection (13.2, Table 13-4) — and the factory still returns NULL for it by design. Because “the proprietary event algorithm uses parameters and conditions defined by the vendor”, the stack cannot compute the transition — what it owns, and what this implements, is the parameters, the notification the transition produces, and which transitions are legal. The vendor’s evaluation is pushed in and consumed by the next Run(). Three clause rules are enforced rather than assumed: FAULT may not be indicated (“for the purpose of proprietary evaluation of unreliability conditions that may result in FAULT event state, a FAULT_EXTENDED fault algorithm shall be used”), and a rejected indication leaves nothing pending rather than firing on the next Run; proprietary offnormal event states are accepted, which the base class could not carry — BACnetEventAlgorithm::ChangeState takes a uint8_t while m_currentState and the notification’s toState are both already uint16_t, so a proprietary state of 640 arrived as 128 and the peer would have been told a different event state, silently (the first implementation had exactly that bug and the test caught it red before the fix existed); and a “transition to the same event state” is reported rather than collapsed to noChange, because cl. 13.3 puts the final determination in the Event-State-Detection process, not in the algorithm. Note also what the clause does not define: EXTENDED has no pTimeDelay — 13.3.10 lists exactly pCurrentState, pVendorId, pEventType, pParameters, and the extended [9] Event_Parameters production has no time-delay field either — so Run ignores the current time and says why. IsDatatypeValid accepts every datatype (the monitored value is the vendor’s to interpret) and CanObjectPropertyBeMonitoredByEvent returns true, where the base’s false would have made an EXTENDED Event Enrollment silently never run. Not yet reachable from outside the stack: there is no DLL export, no node-adapter wrapper and no Event Enrollment constructor for it — that surface is the next phase of [#475]. [#475]

  • extended [9] BACnetNotificationParameters — encode and decode, replacing the three PANIC: Not yet implemented sites that made the cl. 13.3.10 EXTENDED event algorithm unreachable in both directions: BACnetNotificationParameters::SetExtended() returned false, the CHOICE decode dispatch Aborted the entire ConfirmedEventNotification rather than the one alternative it could not parse, and BACnetNotificationParametersFactory returned NULL for the tag. A vendor-proprietary event could therefore be neither emitted nor read, and an inbound one from a conformant peer took the whole PDU down with it. parameters [2] SEQUENCE OF CHOICE is modelled as a typed element (BACnetNotificationParametersExtendedParameter), not an opaque blob: each of the thirteen application-tagged alternatives decodes to its concrete BACnetPrimitive*, and the fourteenth — the only context-tagged one, property-value [0] BACnetDeviceObjectPropertyValue — keeps its [0] wrapper, so both round-trip byte-for-byte. Deliberately not built on BACnetAbstractSyntaxType, which is the obvious carrier and is what the sibling change-of-status-flags present-value [0] uses: its Encode walks its children and emits no enclosing tags, so a decoded property-value [0] re-encodes as a bare value and the element silently changes meaning on the way back out. vendor-id is Unsigned16 and is bounded on both sides — SetVendorId refuses a wider value, and Decode rejects one from a non-conformant peer rather than building an object it cannot legally re-emit. Decode also fails a truncated parameters list instead of accepting a short read: the sibling SEQUENCE OF loop this was modelled on (BACnetNotificationParametersChangeOfReliability) exits on buffer exhaustion and reports success, so a clipped list there reads as a complete one. Five named mutations, each rebuilt and run: stubbing SetExtended, deleting the extended arm of the decode dispatch, deleting the truncation check, removing the Unsigned16 bound (set side and decode side), and dropping the property-value [0] wrapper on encode — each turns exactly the test(s) that own it red and leaves the rest of the 37 *Extended* tests green. This is the codec only; the EXTENDED event algorithm and the Event Enrollment surface that drives it are not in this change. [#475]

  • DecodeWritePropertyValue’s EventNotificationSubscription arm. CreateFromCompactString was well covered; its caller was not. The ';' split is asserted by COMPARING the two-subscription encoding against the one-subscription encoding rather than by “was transmitted” - a split that decoded only the first token would still transmit, still succeed, and still pass a length-greater-than-zero check. Also covered: a trailing ';' producing an empty token, which must be skipped without changing the encoded value; a malformed subscription, which must not reach the wire; and the deliberate behaviour that reaching the end with ZERO subscriptions is SUCCESS - an empty Subscribed_Recipients is a legal value (cl. 12.51.9) and is how a test clears the property, which is exactly the behaviour a later reader is most likely to “fix” into a rejection. Mutation-verified: decoding only the first token fails the split test while the other three pass. [#417]

  • The error paths of RemoveListElementFromHostServedList, none of which had a test. Error paths are where revert-green hides best: the happy path had three tests and not one of them could tell a correct rejection from a wrong one, because none ever took a rejection. Covered: the host rejecting with writeAccessDenied (passed through) versus any other code (mapped to valueOutOfRange) - the discriminating pair, since without the second an implementation reporting writeAccessDenied unconditionally would tell every rejected client the wrong reason; an element of the wrong datatype; removal from an already-empty list; and an undecodable served payload, which must NOT be treated as an empty list and silently overwritten. Each rejection also asserts the host’s property was left untouched. Mutation-verified: reporting writeAccessDenied unconditionally fails the pair’s second half while all 22 other tests pass. [#417]

  • allowEmptyValue finally has a test. The identifier appeared in no file under tests/ at all, and the arm it guards is what makes an EMPTIED host-served LIST readable instead of Aborting - reachable the moment RemoveListElement takes the last element out, when the host legitimately serves zero bytes and rejecting it would Abort the very ReadProperty the removal is verified with. It was unreachable from the existing suites for a concrete reason: both host mocks there always serve a non-empty blob, so blobLength == 0 is never hit. Driven through GetProperty rather than the private helper, so it exercises the routing that decides allowEmptyValue and not just the arm. Verified by disabling the arm, which fails the test while its control still passes. [#417]

  • Tests for the merged list-service and notification-filtering work that a revert would have left green (#411’s remainder). The recipient-transition filter gains the (0,0,0) fully-opted-out case - the configuration a fail-open bug is maximally visible in, and the only one that cannot be satisfied by accident, since all three existing E2E cases have at least one transition enabled - plus (1,0,1), and TO_FAULT asserted in both directions at the classifier. RemoveListElement’s removeMatched[j] consume-each-match loop, the only non-obvious logic in that function, gains the two cases a naive “mark every matching entry” implementation gets wrong: a list holding two identical subscriptions with the request naming it once must remove EXACTLY one, and a request naming the same element twice against a single list entry must be listElementNotFound with nothing written. Both are mutation-verified rather than merely revert-verified - against a deliberately naive implementation they fail while the other 16 tests in the file pass. [#417]

  • ci_scripts/check-getelements-completeness.js now models the three render paths it was blind to, cutting its allowlist from 141 entries to 85 and its findings to one real gap. It only ever parsed GetElements, so it reported as unrendered: members emitted through GetAttributes/ToString (the idiom every leaf and frame class uses); members rendered by a BASE class - every primitive’s GetAttributes opens with BACnetPrimitiveBase::GetAttributes(attributes), and the base is what renders m_tag, so one blind spot produced eleven wrong findings at once; and members rendered through their own accessor, ToString(this->GetLength()), where the member never appears in the render body at all. Each is ADDED as a known path rather than exempted - an exemption hides a finding, a modelled path explains it. extractBody also became comment- and string-aware: it brace-matched raw text while stripComments ran only afterwards, so a single { inside a // comment truncated a render body and reported every member below it as a gap. That one was found the hard way, by adding a comment that quoted an if (...) { line and watching three correctly-rendering members turn into findings. [#428]

  • ci_scripts/node-adapter-output-buffer-guards.js gained the read side to match its write side: a JS buffer reaching a const pointer parameter immediately followed by a numeric length, with no preceding ValidateBufferLength naming that buffer. It reuses the write side’s resolution machinery, so it inherits every blind spot already fixed there - the auto spelling and the enum-constant indices that once left 40 wrappers outside the model. Ships with a scan floor, a read-pair count printed on success so a silent loss of coverage is visible, and its own nine-case evasion corpus (auto, enum indices, a guard naming a different buffer, a guard in a comment, a guard placed after the call, plus three that must stay quiet). One of those clean cases is a false positive the rule genuinely shipped with and which running it against the tree exposed: a guard whose Length() carries element-size arithmetic - payload.Length() / sizeof(uint32_t) - was reported as missing, because the matcher demanded an exact .Length() and so could not see a MORE careful guard than it expected. [#392]

  • ci_scripts/check-self-assignment-guards.js gained the rule its own header comment had been describing in prose since #396: delete a member, then read the SAME member off rhs. On x = x those name one object, so the read is a use-after-free or a read of the pointer the delete just NULLed. All ten classes #396 fixed were previously classified safe-shape - which this gate defines as “this gate has no opinion” - by a gate that had their exact failure mode written out at the top of it. A rule that exists only as a comment is not a rule. Nine operators move from unmodelled to destructive and guarded (109 from 100), the diagnostic now names WHICH destructive shape it found rather than printing the Reset() explanation for both, and the evasion corpus gains ten runnable cases - six attacks (SAFE_DELETE, bare delete, delete[], SAFE_DELETE_ARRAY, no this->, and a guard placed below the delete it should protect) and four boundaries that must stay quiet, including a member whose name merely starts with the deleted one, and a read of rhs taken BEFORE the delete. [#398]

  • BACnetPrimitiveBitSTRING::HasBit(offset) and TryGetBit(offset, out) - the way to tell an ABSENT bit from a PRESENT-but-clear one. GetBit answers FALSE for an out-of-range offset, so the two were indistinguishable and a caller could not fail safe even when it wanted to; the source said outright that nobody had decided this was right (// ToDo, confirm what this should be...). GetBit’s fail-closed behaviour is unchanged - with 191 call sites across 55 files in source/, most of them reading BACnetStatusFlags out of decoded peer data, changing what it returns would be a behavioural change across the whole event/alarm surface with no way to test the blast radius. Instead the contract is now stated at the declaration, the ToDo is gone, and callers that cannot treat “absent” as “clear” have an accessor that says so. Deliberately not logged: several callers read past the end ON PURPOSE and say so in their own comments - BACnetStackAuthorizationPolicy and BACnetStackAuthorizationCache both rely on “false past the requested length” to make a bitstring subset test work - so a log there fired an error on a correct authorization denial, once per cached token in the cache’s loop. The accessor cannot tell an intentional bounded subset test from an accidental short-bitstring read. The one caller migrated here is BACnetDestination::IsWeekDayValid (see Fixed). [#405]

  • Selftest leg 8 (ci_scripts/node-adapter-selftest.mjs): the date-field range guards fire against the real addon, including the reported SendTimeSynchronization(2026, ...) call verbatim. Every wrapper it exercises carries an ACCEPTING control alongside its throwing case - wire years 0/126/254/255, the cl. 20.2.12 month and day patterns, a fully unspecified date, and the all-zero “unused” timeOfAcknowledgment filler for the sequenceNumber form - so a wrapper that threw unconditionally cannot satisfy the leg. Test-tool-only exports are skipped with a message rather than failing in a production build. [#406]

  • ci_scripts/generate-node-adapter.js and node-adapter-coverage.sh gained a cross-check that a wrapper DEFINED inside the test-tool gate is also REGISTERED inside it, and vice versa. The un-gating above had to be done across three separate layers (header declaration, definition, registration) and getting only two of them produced a compile error rather than anything the manifest could see.

  • docs/CAS BACnet Stack - Public interface prototype change candidates.md - the cases found during the public-interface review where the SIGNATURE is the defect and documentation can only warn about the trap rather than remove it. Recommendations only, nothing implemented: the year-1900 wire octet leaking into 7+ exports as uint8_t (the #406 class, and internally inconsistent with SetScheduleEffectivePeriod’s 4-digit years), two different enumerations both named networkType, three exports still using the int invoke-id return the header’s own conventions block calls deprecated, the Channel/Group/Schedule family being unable to address a virtual device, and on the node side the 20 wrappers that take a 1-byte out-parameter Buffer where returning the invoke-id would remove most of the #391 surface. Each entry records whether it is ABI-breaking so the cheap non-breaking ones can be separated from the major-version ones.

  • Node adapter: UnloadBACnetFunctions() - resets the stack and drops every strong reference the addon holds on a registered JS callback, so a host can release stack state deterministically and LoadBACnetFunctions() can be called again afterwards. Paired with a napi env cleanup hook that clears the same 69 Napi::FunctionReferences (65 map slots plus 4 file-callback statics) while the environment is still alive: they are namespace-scope/static objects whose destructors otherwise run during static teardown, potentially after their napi_env is gone - the classic native-addon crash-at-exit. Note the reported symptom did not reproduce: the addon registers no libuv handles (no timers, threads or ThreadSafeFunctions) and the stack is entirely cooperative, so a loaded process already exits on its own; a process that will not exit is being held open by the host’s own UDP socket and Tick() timer. Both facts are now documented in README.md, and selftest leg 7 pins the clean exit. [#446]

  • Node adapter: IsTestToolBuild() - reports the compile-time state of BACNET_STACK_TESTTOOL for the addon actually loaded. Registered unconditionally, in both builds, which is the point: a function that only existed in the test-tool build could not report the absence of the test-tool build. Before this the only signal was duck-typing an export that happens to be gated. [#450]

  • binding.gyp can now build a production addon without BACNET_STACK_TESTTOOL (npm install --bacnet_testtool=0, or npm_config_bacnet_testtool=0 npm install); the default is unchanged and still the test-tool build. adapter-manifest.ts gained EXPECTED_PRODUCTION_ADDON_EXPORTS (190 of the 249 names), and LoadBACnetFunctions() asks the addon which build it is and validates against the matching manifest, so both load cleanly. [#450]

  • Node adapter: BACnetFileAccessMethod and BACnetFileAccessMethodParameters are exported as named constants. The two enums are inverses of each other - the File_Access_Method property enum is record = 0, stream = 1 while the AtomicReadFile/AtomicWriteFile wire choice is stream = 0, record = 1 - and both reached JavaScript as bare number parameters with no JSDoc, so passing one where the other belongs silently selected the opposite access method with no type error, no exception and no runtime check. [#449]

  • Selftest legs 6 and 7 (ci_scripts/node-adapter-selftest.mjs): the packed-buffer count guards fire against the real addon, and a process that loads, uses and unloads the stack exits on its own with no event-loop handles held. Both carry correctly-sized controls, so a wrapper that threw unconditionally cannot satisfy them. [#446] [#447]

  • ci_scripts/check-getelements-completeness.js, wired into pre-push.sh’s fast gates (self-test first): fails when a member a class ENCODES is absent from its GetElements — the silent render-gap class behind #418/#419, where the wire is correct while DecodeAsXML/DecodeAsJSON shows a truncated object. Also flags classes that define Encode with no GetElements at all (48 found, previously invisible to any check), refuses a vacuous pass below a scan floor, and FAILS on stale allowlist entries so the backlog cannot rot. The PR #423 adversarial review rebuilt its detector: the first version’s brittle regex mis-seeded 56 of its own false positives into the backlog as “render gaps”, and nothing invoked the script at all. It runs from pre-push.sh only — no GitHub workflow invokes it. [#418]

  • Test tool: BACnetStackTestTool_InsertNotificationLogRecordWithEventValues — the 27-parameter original delegates to it; carries a changeOfState eventValues CHOICE into an inserted Event Log record (other CHOICE members still need their own builder, as on the TX side). The original export’s behaviour is unchanged. [#419]

  • Test: FromBytes_AdmitsEveryDatatypeDecodeWritePropertyValueBuilds — a coverage test for the BACnetStackWritePropertyAsync::FromBytes allowlist as a class, rather than one more per-datatype regression test. BACnetInterface.cpp’s DecodeWritePropertyValue can build seven constructed datatypes from their compact value string, but it is only ever reached if FromBytes admits the datatype byte first, so adding a builder without its allowlist entry leaves unreachable dead code that fails silently — FromBytes returns 0, SendWriteProperty emits nothing, and the caller sees a timeout with no wire activity. That trap has fired twice: Sprint 237 (CalendarEntry, SpecialEvent, DailySchedule, LightingCommand, unreachable since PRs #209/#210/#211 landed) and again for EventNotificationSubscription. Each was fixed with a per-instance test; this one fails the moment a builder is added without its gate. Verified revert-red: removing the EventNotificationSubscription entry from the allowlist makes it fail and name both the datatype and the fix. Asserts admission only, not decode success — the per-datatype Send* tests own decoding. [#417]

  • ci_scripts/node-adapter-output-buffer-guards.js, wired into pre-push.sh’s fast gates: fails when a JavaScript-supplied Napi::Buffer reaches a non-const pointer parameter of a BACnetStack_* / BACnetStackTestTool_* export without a preceding ValidateOutputBufferLength. It follows C casts, reinterpret_cast aliases and ternaries, resolves declarations from both export headers as well as the in-file externs, keys on the declaration’s constness rather than on a parameter name, and treats a .Data() argument it cannot resolve as a failure rather than a silent skip. --self-test runs twelve evasions as runnable inputs — a guard placed after the call, one naming a different buffer, one commented out, one inside a string literal, one in a neighbouring wrapper, one using the read-side helper, and the cast/alias/ternary/arity routes — plus three clean cases that must not be flagged. It runs from pre-push.sh only — no GitHub workflow invokes it, so nothing on the PR path enforces it yet. [#391]

  • ci_scripts/check-msvs-test-registration.sh gained a library-project half: every source/*.{c,cpp} must be built by CASBACnetStack.vcxproj, and every entry must exist. The existing check only ever looked at the test project, which is how a missing source file (BACnetRouterEntry.cpp, a link failure) and a case-wrong entry survived in the library project. No allowlist — that project is at zero in both directions, so any discrepancy is new. #386

  • ci_scripts/check-self-assignment-guards.js, wired into pre-push.sh’s fast gates: fails when an out-of-line X& X::operator=(const X& rhs) calls Reset() without a this == &rhs test. Scoped to that destructive shape on purpose — the other 186 copy-assignment operators copy member by member and are safe as written, and a rule demanding a guard everywhere is the version that collects 186 exemptions and then gets switched off. --self-test runs eight evasions as runnable inputs: a guard that exists only in a // comment, in a block comment, in a string literal, one naming a different object, one naming a longer identifier that merely starts with the parameter name, Reset() called without this->, and a guard sitting in the preceding function. It runs from pre-push.sh only — no GitHub workflow invokes it, so nothing on the PR path enforces it yet. [#373]

  • ci_scripts/check-msvs-test-registration.sh, wired into pre-push.sh’s fast gates: fails when a test source is present on disk but not built by the MSVS test project (it would never compile on Windows), when the project lists a file that is not there (MSBuild gives C1083), or when the project builds a test file from outside tests/ (the Linux makefile globs tests only, so it never compiles on CI). The files already in the first state are carried in a shrinking, self-validating backlog so the list cannot grow. It runs from pre-push.sh only — no GitHub workflow invokes it, so nothing on the PR path enforces it yet. #386

  • ci_scripts/hand-adapter-coverage.sh, wired into pre-push.sh’s fast gates: the C# and Java adapters are hand-written, unlike the generated cpp and node ones, and nothing checked them. It fails on a new binding to an export the library does not declare, on a newly unreachable export, and on a stale allowlist entry in either direction. The bindings already broken (see below) are carried in a self-validating backlog and reported as a warning, so neither count can grow. It checks names, not signatures. #51

  • BACnetStack_RemoveGroupMember — the companion to BACnetStack_AddGroupMember, which was elevated to the customer API without one, leaving no supported way to take a member out of a Group’s List_Of_Group_Members. Its semantics are taken from the RemoveListElement service on the same property rather than invented: the member is matched by object identifier alone, removing one that is not present fails rather than being idempotent, and the survivors keep their order. #142

  • Test tool: BACnetStackTestTool_SetInvokeIdSeed — sets the next invoke id the stack assigns to a confirmed request. cl. 5.4 assigns invoke ids inside the stack and returns them only through a Send* OUT parameter, so a conformance script could not provoke a specific id, or drive the 8-bit wrap, from outside. #83

  • Configure a router route with an operator-supplied next-hop MAC via BACnetStack_AddRouterRoute; required before B-RTR can be claimed. #305

  • Secure connect router: one BACnet/SC data link instance per Network Port object (cl. 6.6 “at least two BACnet/SC network ports”), and cl. 6.6 header-option relay including Secure Path. #304

  • The physical router answers Who-Is-Router-To-Network from its physical routing table (NM-RC-B execution side). #303

  • Physical routing: route learning, I-Am-Router-To-Network initiation, the held-message queue, egress addressed by port id, relay as unicast to the next-hop router, and broadcast fan-out. #274

  • STACK_OPTION_DISABLE_ROUTER_PHYSICAL so a TARGET_FULL build can decline physical routing. #278

  • File object: full AtomicReadFile/AtomicWriteFile record access with RegisterCallbackReadFileRecord / RegisterCallbackWriteFileRecord and a declared record limit. #275

  • LifeSafetyOperation confirmed service (cl. 13.8, service choice 27) — request model, execute and send.

  • BACnetDateTime values can now be carried by a single-element WriteProperty (BTL 7.3.1.24 / 7.3.1.25).

  • BACnetStack_SendWhoHasWithObjectIdentifierAndLimits and …WithObjectNameAndLimits expose the device-instance range on Who-Has (BTL 9.32).

  • CASBACnetStack::Adapter CMake target under adapters/cpp/, with a CAS_BACNET_STACK_LINK mode selector (SOURCE/STATIC/DLL).

  • Every language adapter (cpp, csharp, golang, java, node, python, rust) now ships working propertyArrays pack-helpers for the five send functions that take a caller-packed buffer.

  • docs/property-profile-reference.md — generated reference of all 64 object types / 1793 property rows.

  • Doxygen documentation for the remaining 119 undocumented BACnetStack_* exports.

  • Test tool: five cl. 6.4 network-layer control-message emitters (SendICouldBeRouterToNetwork and siblings).

  • Test tool: SendInitializeRoutingTable (cl. 6.4.7, query and set forms). #80

  • Test tool: SendConfirmedRequestRaw — confirmed request with a caller-supplied service choice and payload. #81

  • Test tool: SendUnknownNetworkLayerMessage — network layer message with a caller-chosen type octet, deliberately unvalidated. #94

  • Test tool: SendIHave — the I-Have unconfirmed request (cl. 16.9). #93

  • Test tool: broadcast delivery for the unconfirmed COV notification sends. #57, #91

  • Test tool: the five previously unreachable AuditLogQuery query-parameter fields, plus first test coverage. #92

  • Test tool: BACnetStackTestTool_{Add,Remove}ListElementEventNotificationSubscriptions for Subscribed_Recipients, and the ReadAccessSpecification forms for Group.List_Of_Group_Members.

  • Test tool: BACnetStackTestTool_AddGroupObject / AddGroupMember.

  • First test coverage for the confirmed and unconfirmed PrivateTransfer sends. #100, #101

  • Regression coverage for SendIAm with an out-of-range device instance #24, for the virtual-network Network_Number write #28, and for the large object-list encode and segmented-transmit paths #152.

  • CI runs the unit tests under AddressSanitizer and UndefinedBehaviorSanitizer #307; the sanitized suite now also runs automatically on a release tag #316.

  • Live-path test harness for the physical router (tests/TestBACnetPhysicalRouterLivePath.cpp). #274

  • Naming rule made explicit in AGENTS.md and .github/copilot-instructions.md, and applied to the code that prompted it. PR #424’s change-request was entirely about simplified/temporary variable names, so the rule is now written down rather than left to review to catch each time: no single letters, truncations or throwaway names; i in a for loop is the one exception and only when nothing better exists; and a fixed-layout array indexed by bare numbers (f[4 + 1]) is the same defect, to be replaced by named FIELD_INDEX_* constants. Renamed accordingly, with no behaviour change: BACnetEventNotificationSubscription::IsListServiceMatch and CreateFromCompactString (the worked example), BACnetRawConfirmedServiceRequest::IsEqual, the memcmp results in BACnetStackNetworkKey and BACnetStackCOVMultipleContext ordering operators, the two LoggedRecordRef fills in BACnetDBDevice, and the two non-nested j element loops in BACnetBusinessLogic’s Add/RemoveListElement. Short names in code these changes do not otherwise touch were deliberately left alone rather than swept into the diff.

  • The router’s Tick backstops now derive from one named, overridable rate — STACK_OPTION_ROUTER_TICKS_PER_SECOND, defaulting to 5000, so no window moves and no pinned value changes. The “~5 kHz Loop” assumption was encoded as a bare 10000 in the held-message backstop, with the other two constants derived from it rather than from the rate - so the figure appeared only in prose, there was nothing to grep for, and which of the three numbers was the assumption was not evident from any of them. That assumption is load-bearing and easy to be wrong about by two or three orders of magnitude: m_routerLoopTicks advances once per stack Loop(), and an integration driving that from an application main loop is far likelier to run at 1–100 Hz. At 1 Hz the held-message backstop is 2 h 47 m instead of TOmin, the busy backstop is 41 hours instead of cl. 6.6.3.6’s 30 seconds, and one search-refill period is 83 minutes instead of a second — which defeats the purpose that constant’s own comment claims for it (“route discovery is a function, not a privilege”). Which rate the product targets is an owner decision (#426) and is not made here; this is the knob that decision needs, and an integrator who knows their own Loop rate can correct their deployment today. The busy backstop is also now derived from the rate and its own clause timeout rather than from the held-message backstop scaled by a ratio — the ratio form gave the same answer only because both encoded the same rate, and it hid which number was the assumption and which was the clause. A first version of this shipped assertions relating each constant back to the override macro and a comment claiming, in capitals, that they were not definitional. They were — the header now defines each constant as macro * clause, so constant == macro * clause expands to x == x exactly as the refill == held / TOmin assertion the file already flags. Caught by review, proved by compiling them as static_asserts clean at an arbitrary override of 137, and removed rather than reworded: the literal pins are the only real gate, and the derivation is testable only in a second translation unit built with a different macro value, which this suite cannot produce. The knob therefore has no automated coverage at any non-default value, and that is recorded on #426 rather than papered over. Those literal pins are now #if-guarded to the default rate, because without the guard an integrator who sets the option — the entire point of it — got a red suite for doing so. [#426]

  • BACnetStack_SetCOVSettings: maxSupportedCOVLifetime = 0 now means use the device default (28800 s / 8 h, MAX_SUPPORTED_COV_LIFETIME) instead of being taken literally. The export replaced SetMaxActiveCOVSubscriptions, which never expressed a lifetime, so every migrating caller is forced to invent one with no v5 equivalent to copy - and 0 was both the most likely guess and the most dangerous, because a zero bound rejected every request carrying a non-zero lifetime (they all exceed the maximum) while still admitting indefinite, lifetime-0 subscriptions. That behaviour is no longer reachable through this export. maxNumberOfSubscriptions = 0 is unchanged and still means “no limit”. [#448]

  • Node adapter: SendUnconfirmedCOVNotification gained an optional trailing broadcast (default false) and SetAlarmsAndEventsForObjectEnabled an optional trailing enabled (default true). Both flags exist on the flat C exports but were hardcoded in the bindings, so cl. 13.1 broadcast COVU delivery was unreachable from JavaScript - while the sibling SendUnsubscribedUnconfirmedCOVNotification did expose it - and a function named SetAlarmsAndEventsForObjectEnabled could only ever enable. Both are optional, so existing callers are unaffected.

  • Node adapter: the 17 (...args: any[]) passthroughs on the api object are now fully typed. They reached the shipped dist/api.d.ts as any[] - no arity checking, no parameter names, no hover help, and no way for a reader to discover that three of them (SendConfirmedAuditNotification, SendAuditLogQuery, SendAuthRequest) return { success, invokeId } rather than a boolean, so if (SendAuthRequest(...)) is truthy even on failure. Signatures were taken from each napi wrapper’s own argument guard, so they reject exactly what the binding already rejected at runtime. Two traps documented while typing them: SendYouAre’s deviceObjectType is accepted and IGNORED (the C export dropped it) but must still be present, and InsertAuditLogRecord/SetAuthorizationPolicyEntry take a length parameter that is not validated against its buffer at any layer.

  • Node adapter pack-helpers (property-arrays.ts) now validate every numeric field against the writer backing it and throw an error naming the element index, the field and the limit - elements[2].objectType = 70000 is not an integer in 0..65535 (uint16) - instead of letting a raw RangeError [ERR_OUT_OF_RANGE] out of Buffer.writeUIntXX naming an internal buffer offset. Consumers no longer have to re-implement the library’s bounds tables to produce a usable error. Two deliberate tightenings beyond the writer widths: priority/overridingPriority are checked against BACnet’s 1..16 (cl. 19.2), which 17..255 fits as a byte but can only ever produce a wire-level error; and encodeCovPropertyMultipleReferences lost the >>> 0 coercion that made it the one encoder to mask bad input into a wrong-but-valid value rather than report it. The documented negative-means-absent sentinels are preserved, but a NaN no longer silently selects the absent branch (NaN >= 0 is false) - the silent sentinel reinterpretation in the report. All five frozen byte vectors are unchanged. [#444]

  • Node adapter: all five packed-buffer sends now validate the caller’s buffer against the element count before entering the DLL, which trusts the count and does not re-validate it - turning what was an out-of-bounds read in native code (no error, no false return, nothing JS could catch) into a thrown JS error naming the argument and both sizes. Exact for the two fixed-stride layouts (SendReadProperty 15 bytes, SendSubscribeCOVPropertyMultiple 19), a minimum-size bound for the three variable-length ones. The check lives at the napi boundary rather than in api.ts because the addon is also reachable through the package’s default short-name export object, which does not route through the TS wrappers. No ABI change. [#447]

  • SendConfirmedCOVNotification, SendUnconfirmedCOVNotification and SendConfirmedCOVNotificationMultiple moved inside interface.cc’s BACNET_STACK_TESTTOOL gate. All three are backed by BACnetStackTestTool_* entry points with no production equivalent in CASBACnetStackDLL.h, but were defined and registered in the unconditional half of the file - which is why the addon could not be compiled without the define at all (three C3861: identifier not found errors). Fixing this is what makes the production configuration buildable. generate-node-adapter.js and node-adapter-coverage.sh now both fail on a BACnetStackTestTool_* reference outside the gate, so it cannot recur. [#450]

  • package.json files now ships adapters/README.md and the v5-to-v6 breaking-changes doc. The docs/ tree was excluded, so the one written record of the inverse file-access-method enums was unreachable from an npm install - a consumer had to already know to clone the source repo and search it. [#449]

  • DecodeAsXML/DecodeAsJSON output gains members that were always encoded but never rendered (#418/#419): PropertyAccessResult now renders PropertyArrayIndex, DeviceIdentifier and its value/error CHOICE arm; EventLogRecord now renders its EventLogDatum (the full notification subtree). Additionally the EventLogRecord Timestamp element now carries a context="0" attribute where it previously had none. Consumers that iterate children positionally, assert child counts, or size output buffers against the old (smaller) rendering can be affected — DecodeAs is all-or-nothing, so an undersized buffer now yields 0 bytes rather than a truncated render. [#418] [#419]

  • BUFFER_READY intrinsic reporting is now evaluated when a natively-polled log appends a record from the device tick, not only from UpdateValue — a polled Trend Log that crossed Notification_Threshold through its own sampling previously never fired. Host-side Insert* calls remain deliberately silent (pinned by a gtest; a wider hook regressed 8 corpus rows). Per the PR #423 review, the tick report is attributed to the OWNING device (not the first device in the list) and gated on an actual Total_Record_Count delta, so no-append paths (an unchanged COV-type log; an empty TrendLogMultiple) do not trigger evaluation every tick. [#419]

  • Event Log records inserted via the test tool are stamped with the SUPPLIED timestamp instead of the insert wall-clock (hundredths remain zero — DateTimeFromTimeT hard-codes them; BACnet wildcard/pattern date fields fall back to the insert time, loudly, since a record stamp must be a real calendar datetime). [#419]

  • BACnetInterface::AddDeviceToVirtualNetwork no longer leaves partial state on failure: the call now PRE-VALIDATES the router-side conditions (a nonexistent network or an already-mapped instance fails BEFORE the device ever enters the database — itself a caller-visible change), the NetworkPort object is added BEFORE the router mapping (there is no RemoveVirtualDevice API, so the mapping must be the last fallible step), and a failed call rolls back the database entry ONLY if that call created it — the PR #423 review found the first version deleted a PRE-EXISTING live device on a failed retry, and its round 2 found the reorder alone let a failed call pin a stale Network_Number. [#420]

  • BACnetRejectMessageToNetwork’s documentation for rejection reason 5 now matches 135-2024 cl. 6.4.4 (“This rejection reason value has been removed”) rather than the withdrawn 135-2020 security-error text. The securityError = 5 enumerator is deliberately unchanged — renaming it would be an API break, and a test tool has a legitimate reason to emit a legacy reason 5. #362

  • A COV Trend_Log now actually records on BACnetStack_ValueUpdated. GetTrendLogsByObject took its out-parameter vector by value, so the caller’s vector was always empty and the entire COV logging block was dead code — a COV trend log recorded nothing. Filed as a defect, but the effect on an existing deployment is a behaviour change, not just a fix: log buffers now fill, Record_Count advances, and an allocation happens per matching update. BACnetStack_ValueUpdated still returns true when a log fails: the first draft returned false out of the loop, which silently skipped CheckForAlarmsAndEvents for every object after the failing log, so review replaced it with a continue. A failing COV trend log is therefore not reportable through this export. #383

  • BREAKING (flat C export only) — BACnetStack_SendUnsubscribedUnconfirmedCOVNotification gains a trailing broadcast parameter. It previously hardcoded unicast, so a COVU_Recipients entry configured as a broadcast address could not be served at all; 135-2024 cl. 13.1 makes broadcast the configured-recipient delivery mode for an unsubscribed COV notification. The C++ overload and the node wrapper default it to false, so callers of those are unaffected; the flat export carries no defaults and changes arity. The clause’s “Process Identifier … shall be zero” rule is satisfied by construction — this entry point always sends 0. #364

  • BREAKING — Object Access send API: the stateful Build*/Clear*/Send*Async pattern is replaced by single stateless calls BACnetStack_SendReadProperty, SendWriteProperty and SendCreateObject. WritePropertyMultiple is now implemented. #114

  • BREAKING — BACnetStack_DecodeAsXML and BACnetStack_DecodeAsJSON are unified into BACnetStack_DecodeAs(format, …).

  • BREAKING — DecodeAsXML / DecodeAsJSON output reshaped (element-name normalization, attribute renames, XML declaration, indentation, list containers always arrays, real JSON booleans). Addresses 14 of the 17 v5.4.1 audit findings; see the migration guide linked above.

  • BREAKING — BACnetStack_SetMaxActiveCOVMultipleSubscriptions renamed to BACnetStack_SetCOVMultipleSettings.

  • BREAKING — BACnetStack_SetDeviceBackupFailureTimeout folded into a new backupFailureTimeout parameter on BACnetStack_SetBackupAndRestoreEnabled.

  • BREAKING — BACnetStack_AddNetworkPortObjectWithNetworkNumber renamed to BACnetStack_AddNetworkPortObject; signature and behaviour unchanged. #265

  • BREAKING — C++ adapter v2.0.0: the fp* function-pointer surface is replaced by direct BACnetStack_* names in every link mode, generated from CASBACnetStackDLL.h.

  • BREAKING — npm package 6.1.0: LoadBACnetFunctions() is mandatory before any stack call, and the adapter surface is generated to match the C++ adapter contract.

  • DecodeAsJSON uses a new in-house JSONRenderer instead of the third-party xml2json (plus its vendored rapidjson/rapidxml, ~940 KB); the IRenderable tree is now the single source of truth for all output formats.

  • An arriving frame now names its ingress router port instead of having one deduced from the network type — a prerequisite for two router ports of the same network type. #304

  • Whole-array WriteProperty can resize Action_Text and Subordinate_Annotations.

  • BACnetConfirmedRequestPDU::m_serviceChoice is now a uint8_t, so a wire octet ≥ 0x40 is no longer an out-of-range enum load. GetServiceChoice()’s return type is unchanged.

  • 27 property-profile required/writable flags corrected against the 135-2024 clause-12 conformance codes, across 9 object types.

  • The SANITIZE=1 build treats every UndefinedBehaviorSanitizer finding as fatal; the known-site allowlist is gone. #325, #297

  • CI triggers cut to pull requests and release tags #296, and the PR gate reduced to a single job (~62% fewer billed runner-minutes) #319.

  • ci_scripts/dev-build.sh caps ccache at 5G rather than re-applying a 50G cap on every run. #299

  • Ported the bug fixes that accumulated on 5.x-github since 6.x branched, including BACnet/SC COV #119 and hub compatibility flags #121. #217

  • Customer builds no longer compile BACnetDestination::CreateFromCompactString — it is test-tool-only surface and is now gated behind BACNET_STACK_TESTTOOL (its sole production caller already was). Any customer code calling it directly must define the test-tool flag or migrate; the make NO_TESTTOOL=1 reproduction build verifies the gate both ways. [#416]
  • BREAKING — the 13 BACnetStack_RegisterHookProperty* per-datatype observation hooks. Decode the raw inbound frame instead.
  • BREAKING — BACnetStack_SetObjectTypeSupported; SetObjectTypeCreatable now implies supported.
  • The ARM Keil adapter (adapters/armkeil/) — its vendored pack-helper documented the wrong value encoding.
  • Dead code: struct SendReadPropertyAsync / SendWritePropertyAsync and the commented-out decode blocks in CASBACnetStackDLL.cpp; the RenderTag/RenderOpenTag/RenderCloseTag helpers; the submodules/xml2json directory and all build references to it.
  • Writing SC_Direct_Connect_Initiate_Enable / SC_Direct_Connect_Accept_Enable has no operational effect yet (#553). #497 made both properties writable with Changes_Pending/shadow semantics, but activating the change does not propagate the enable/disable into the SC data link’s operational configuration, and the value is repainted from the data link on the next Tick() after activation — so the write currently affects the property display and Changes_Pending only. Because the properties are also only enabled when the data link already has direct connect on, the only reachable write is on→off, which then reverts. A forward-propagation attempt caused a 13-test regression and was reverted; the real fix (a data-link setter) is tracked in [#553]. [#497]
  • RemoveListElement has no host-callback path, so a host-held constructed LIST can be appended to but not shortened. RESOLVED in this release — see the RemoveListElement entry under Fixed. The entry said closing this “needs an explicit whole-list-replace callback shape (or a documented empty-list sentinel), not a patch at the call site”, and that is what landed: RemoveListElementFromHostServedList validates every named element first, then performs ONE whole-property rewrite through the host’s existing constructed-Set callback, with an emptied list passed as the documented zero-length sentinel. Removing the LAST element — the case the entry called out as inexpressible — is the case it is tested on. BTL 135.1 7.3.2.30.8.5 is no longer blocked by this.
  • The C# and Java adapters bind entry points that no longer exist. adapters/csharp binds 19 and adapters/java binds 20 symbols the v6 library does not export — almost all of them the stateful Build*/Clear*/Send*Async object-access API removed in this release. They do not fail at build or load time: the first call throws EntryPointNotFoundException (C#) or UnsatisfiedLinkError (Java). The full list, with the replacement export for each, is in #388. adapters/cpp and adapters/node are generated and unaffected. #388
  • Batch 103’s own mandatory batch-close review loop found and fixed two real defects before merge. Load Control’s Actual_Shed_Level was hard-coded to the LEVEL choice with value 0 while SHED_INACTIVE/SHED_REQUEST_PENDING, regardless of which choice Requested_Shed_Level actually used — producing an inconsistent CHOICE tag across Requested_Shed_Level/Expected_Shed_Level/Actual_Shed_Level on the same object. Clause 12.28.17: “When the object is in the SHED_INACTIVE state, this value shall be equal to the default value of Requested_Shed_Level”; the citation the code used (12.28.10) is about the Shed_Levels/LEVEL-choice numbering convention, not Actual_Shed_Level’s own representation. And BACnetLifeSafetyOperationProcessor applied a LifeSafetyOperation to the stack-held object’s state (Silenced/Present_Value, including broadcasting to every Zone_Members entry) BEFORE consulting the host callback, with no rollback if the callback then rejected the request — 135-2024 13.13.2 implies a Result(-) means the operation did not happen, so a host’s rejection could be silently overridden by a stack-side mutation that had already occurred. Both findings were independently re-verified against the actual spec text before fixing; both are now pinned by regression tests. [#478], [#485]

  • Timer’s GetGeneratedPropertyValue dispatch did an unconditional return timer->GetProperty(...), silently preventing the generic alarm-and-event fallback later in the same function from ever running when Timer’s own handler returned “not handled” for a property it doesn’t own (found while wiring in the four new stack-held object engines this batch adds, which all follow the “only return true on success, only return false early when the error container is actually SET, otherwise fall through” discipline this fix establishes as the pattern going forward). A class sweep confirmed Timer was the only object type with this shape — the sibling Lighting Output/Binary Lighting Output dispatches already used a switch+default+break that avoids it. Regression test reads Event_State through the full dispatch chain and asserts on the algorithm’s real (offnormal) state. [#530], batch 103 sprint 116

  • A NULL check missing on one branch of UseCallbackGetLiftProperty’s assignedLandingCalls case — the sibling registeredCarCall case already had it; a failed new(std::nothrow) would have dereferenced NULL on this one branch only. [#531], batch 103 sprint 116

  • BACnetListedFaultAlgorithm/BACnetOutOfRangeFaultAlgorithm’s CreateEventValues leaked changeOfReliability on every failure branch that returned after allocating it (both the propertyValue == NULL check and, for BACnetOutOfRangeFaultAlgorithm, all four datatype-specific SetPropertyIdentifier failure arms). Fixed by freeing changeOfReliability on every early-return path in both classes. Genuine, measured leak-detection regression tests using the stack’s own MemTracker OOM-injection harness (SetFailAfter/leakedBlocks), not inferred from code reading alone. [#536], batch 103 sprint 116

  • Event Log never recorded a time-change entry on TimeSynchronization, unlike Trend Log’s own established wiring for the identical situation. BACnetTimeSynchronizationProcessor now walks Event Log objects the same way it already walks Trend Log objects, inserting a SetTimeChange log record via InsertLogRecord. [#533], batch 103 sprint 117

  • Notification Forwarder’s local-origin path — “an NC recipient naming the local device routes into the NF” — was entirely unimplemented, a documented gap from an earlier batch. New BACnetDBDevice::OfferLocallyOriginatedNotificationToForwarders is a dedicated NF-only entry point that bypasses the shared Event-Log/NF consumer hook (so it can’t double-trigger Event Log’s own already-complete capture logic), reusing NF’s existing OfferEventNotification/CollectTargets loop-prevention logic unchanged; BACnetStackMessageGenerator’s per-recipient send loops detect a self-addressed recipient and route to the new entry point in addition to normal delivery. [#534], batch 103 sprint 117

  • Two CI gate blind spots found and fixed alongside the above: ci_scripts/node-adapter-parity.sh could not see BACnetStack_-prefixed exports declared in the test-tool header (same prefix as a customer DLL export, but test-tool-only surface — several were silently misclassified) nor a declaration whose macro sits alone on its own line with the name on the next physical line; both fixed, with a genuinely-planted, previously-invisible export proven caught by the fix. ci_scripts/dev-build.sh’s mandatory stale-object scan cost ~24 of a 49-minute gate run on a slow mount by re-invoking file on every object every run; added a (path, mtime, size)-keyed verdict cache so only changed objects get rescanned (measured ~12,780x faster warm-cache on the real object tree), with a missing/corrupted cache entry always falling back to a real scan so it can never manufacture a false pass. [#538], [#539], batch 103 sprint 117

  • FloatingLimit’s Event_Parameters and live notifications silently desynced from Low_Limit/High_Limit once those were written independently (owner-ruled 2026-08-29, option (a): report the live limits). BuildEventParameters (cl. 12.12.7’s Event_Parameters read path) and CreateEventValues (the actual EventNotification’s Error_Limit parameter) both reconstructed the band from the stale m_errorLimit set at the last SetFloatingLimitParameters call, rather than the live m_lowLimit/m_highLimit/m_setpoint an independent Low_Limit/High_Limit SetProperty write can make asymmetric — so a client reading Event_Parameters, or an actual alarm notification, described thresholds that no longer matched what Run() was evaluating. Fixed by deriving both live diffs (m_setpoint - m_lowLimit, m_highLimit - m_setpoint) at both sites; CreateEventValues‘s single shared Error_Limit field reports whichever diff matches the transition that just occurred (the limit just crossed, or just cleared on a return to NORMAL). New tests write an asymmetric band and pin both paths’ live values, revert-red. [#527]

  • GetProperty’s LIST-read discriminator (#462) didn’t know about the object-type-specific constructed-LIST callbacks (CallbackGetListElevatorGroupLandingCallStatus and Lift’s assigned-landing-call sequence callback) — only the two generic callbacks (CallbackGetListOfEnumerations/CallbackGetPropertyConstructed). A registered callback that declined mid-list (after already producing 1+ real elements) looked identical to “nothing registered”, so the discriminator reported the elements inserted before the decline as a valid, silently truncated list. Fixed by threading an explicit “a relevant object-type-specific callback ran and declined” signal out of UseCallbackGetElevatorGroupProperty/UseCallbackGetLiftProperty up through UseCallbackGetSpecificProperty/GetValueUsingCallbacks to GetProperty’s discriminator, checked before the two-pointer fallback. New test: a fixture callback returning 2 real elements then declining on the 3rd now fails the read instead of reporting a 2-element success; liveness confirms the callback actually ran 3 times first. [#526]

  • ChangeOfReliability silently dropped a rejected property-value element and still reported the decoded object valid (residual of #517). Three call sites (BACnetListedFaultAlgorithm.cpp, BACnetOutOfRangeFaultAlgorithm.cpp, and BACnetNotificationParametersChangeOfReliability::Decode’s own element loop) discarded InsertPropertyValueAndTakeControl’s bool return, so a rejected element vanished while the caller reported success, and Decode’s final SetValid(true) was unconditional regardless. Fixed: all three sites now fail (decode aborts / fault-algorithm build reports failure) on a rejected insert, and Decode derives final validity from AreAllParametersValid(). A hand-crafted buffer whose N-th element is invalid now fails decode outright (revert-red) instead of decoding to a valid-but-short object. Correction (batch 103, #535/L72): no such test existed, and the literal claim is not buildable as stated — BACnetPropertyValue::Decode() unconditionally marks itself valid on a successful wire decode (it never re-validates the Priority field’s 1-16 range the way the constructor does, filed separately as #540), so the decode-loop’s InsertPropertyValueAndTakeControl rejection is defensible but not reachable from wire input for a non-NULL element. What batch 103 actually pinned instead: a hand-crafted buffer whose N-th element is structurally truncated fails decode outright rather than reporting a valid-but-short object (DecodeFailsWhenAnElementInTheSequenceIsTruncated, tests/TestBACnetNotificationParametersChangeOfReliability.cpp) — the genuinely-reachable version of this behaviour, and the discriminator that was actually never tested before. [#529]

  • Fault_Type/Fault_Parameters answered unknownProperty over the wire on an Event Enrollment that had a fault algorithm armed, despite GetProperty always having a real value to give. Found by a new BTL test (btl-auto-ee-props-b, chipkin-bacnet-testing-tool), not by this suite: the existing gtest coverage (FaultTypeAndFaultParametersAreServed) calls BACnetStackEventEnrollmentObject::GetProperty directly, bypassing BACnetBusinessLogic::ValidateObjectAndProperty’s IsPropertyEnabled gate — the one a real ReadProperty request actually goes through. Both properties are registered OPTIONAL in BACnetDBPropertyProfile.cpp (SetProperty(false, ...), unlike REQUIRED Event_Parameters), so IsPropertyEnabled’s “required implies enabled” fallback never covered them; nothing ever called BACnetDBDevice::SetPropertyEnabled for either property when a fault algorithm was armed through any of the 7 EnableFault*Algorithm entry points. Fixed once, in EnsureFaultAlgorithm — the single function all 7 funnel through — rather than at each call site: this same #475 review chain has repeatedly found “fixed the reported instance, missed the sibling with the identical shape” at every other layer, and a per-call-site fix here would have been exactly that mistake with 7 chances to leave one out. Two new gtests exercise BACnetDBDevice::IsPropertyEnabled directly (what the sibling test does not), one proving the fix for FAULT_CHARACTERSTRING and one for FAULT_STATE to confirm the fix lives in the shared function and not a per-algorithm special case; both mutation-gated (the SetPropertyEnabled calls reverted, both new tests confirmed red, restored, full suite green at 5312/645/0). [#475]

  • An Event Enrollment never reported MONITORED_OBJECT_FAULT, and a monitored object that carried its own intrinsic alarm-and-event object could block the EE re-evaluation walk from running at all. cl. 12.12.21’s Reliability-evaluation order says the EE’s Reliability must become MONITORED_OBJECT_FAULT when the monitored object’s own Reliability is not NO_FAULT_DETECTED (verified against 135.1 8.5.17.7.2: this takes precedence over the EE’s own fault-algorithm result) — nothing computed or served that value. Separately, the re-evaluation walk (BACnetBusinessLogic::CheckForAlarmsAndEvents) only re-ran an EE on an EXACT property match, so a Reliability or Status_Flags change on the monitored object never reached an EE watching a different property of that object, even though cl. 12.12.21/13.3 make both depend on the monitored object’s health regardless of which property is watched. Fixed: BACnetStackEventEnrollmentObject gained a monitoredObjectFault flag (set by the walk; an unreachable monitored object counts as a fault) that overrides GetProperty(reliability)’s answer, and the walk now also triggers on a Reliability/Status_Flags change on the monitored object, reading the EE’s actual watched property (not the triggering one) once it does. A real pre-existing bug surfaced while wiring this up and is fixed alongside it: every early return in the intrinsic alarm-and-event block at the top of CheckForAlarmsAndEvents exited the entire function, silently skipping the Event Enrollment section that follows it — so a monitored object carrying both its own intrinsic A&E object and an EE watching it could never reach the EE walk for a property (Reliability) the intrinsic side doesn’t itself handle. Fixed by wrapping the intrinsic block in an immediately-invoked lambda so its early returns stop skipping code that has nothing to do with them. CONFIGURATION_ERROR (12.12.21’s five algorithm-pair cross-checks) is not implemented — a separate, larger per-algorithm validation matrix, left as a follow-up. Four new gtests; full suite green at 5316/5316 across ten --gtest_shuffle seeds. [#475]

  • Three CHOICE-holding destructors leaked their owned object on every destruction. BACnetNotificationParameters, BACnetEventParameter and BACnetFaultParameter each NULLed their owned pointer in the destructor before calling Reset(), which is the only place that frees it — so Reset() always saw a NULL pointer and never had anything to delete. Every notification and Event Enrollment carrying a configured CHOICE alternative leaked it on teardown. Fixed by removing the premature NULL assignment and letting Reset() (already relied on by the constructor and every Set* method) do the freeing. Three new gtests, one per class. [#503]

  • InsertFaultValueAndTakeControl did not take ownership of its argument on a failed Insert(), in BACnetFaultParameterFaultCharacterstring/FaultLifeSafety/FaultState, unlike the FaultExtended sibling’s documented “takes ownership on every path” AndTakeControl contract. Split the NULL check (nothing to free) from the Insert-failure check (a real object the caller handed over) and free on the latter; removed the two call sites’ (BACnetCharacterstringFaultAlgorithm.cpp, BACnetStateFaultAlgorithm.cpp) now-redundant defensive delete, which would otherwise double-free against this fix. Insert()’s only clean-return failure is a NULL argument — BACnetSequenceOf<T>::Insert wraps std::vector::push_back, which either succeeds or throws, never returns false for a non-NULL argument — so the ownership-on-failure branch this fix adds is unreachable via any current public API or the suite’s OOM-injection hook (which only fails nothrow allocations; push_back’s internal allocation throws). Recorded as latent rather than gated by a test that cannot currently reach it, same shape as [#505]’s untestable branch. Three new gtests pin the reachable (success) ownership contract. [#515]

  • BACnetNotificationParametersChangeOfReliability::InsertPropertyValueAndTakeControl stored any argument unconditionally, and AreAllParametersValid() never inspected the stored list at all — so a NULL element (stored when the copy-assignment operator’s Clone() returns NULL under memory pressure) left the object reporting VALID right up until Encode/GetMinimumMemoryCount/IsEqual dereferenced it unconditionally, a NULL dereference rather than a recoverable error. Fixed to reject NULL (nothing to free) and reject-and-free an invalid value, matching the BACnetNotificationParametersExtended sibling’s contract; AreAllParametersValid() now walks the list for NULL/invalid elements; operator= now checks the insert’s return value and aborts (Reset + return) rather than silently continuing with a short list once a copy fails. Two new gtests. [#517]

  • IsEqual on every BACnetEventParameterBase/BACnetFaultParameterBase alternative checked only GetDatatype() — identical across every alternative of the CHOICE — before an unconditional static_cast to the concrete alternative. The BACnetEventParameterExtended/BACnetFaultParameterFaultExtended siblings already carried the correct fix as the model: compare GetType() (the CHOICE context tag) through the common base pointer first, and only cast after that matches. Applied the same guard to the remaining 20 “with-cast” alternatives (17 event, 3 fault; the other 3 fault alternatives — FaultCharacterstring/FaultLifeSafety/FaultState — landed with the #515 fix above since they’re the same files) plus the 2 “None” alternatives (EventParameterNone, FaultParameterNone), which have no cast but the identical bug shape: “any two are equal” was reachable for two different alternatives, not just two nones. Not reachable through the wrapper today — BACnetEventParameter::IsEqual/BACnetFaultParameter::IsEqual already compare the CHOICE tag before delegating here — so a plain gtest assertion cannot gate a regression of this guard; removing it produces undefined behaviour that usually still returns false rather than a wrong return value, and UBSAN is the instrument that would see it. No new gtest coverage added for that reason (existing IsEqual tests for these 26 classes already exercise the fixed code path’s unchanged normal behaviour), matching how [#515]’s equally-unreachable-today branch above was handled. [#513]

  • BACnetPrimitiveBitSTRING::Decode’s unusedBits octet was read straight off the wire with no range check against 135-2024 cl. 20.2.10 (0-7). A value of 8+ made 8 - unusedBits (int arithmetic assigned to uint16_t) underflow into tens of thousands, turning a 3-byte PDU into that many push_backs off a single content octet — a ~20000x amplification an unauthenticated peer could drive with BitStrings appearing in ordinary decodable places (Status_Flags, Limit_Enable, any WriteProperty of a bitstring-valued property, …). Fixed: rejects with invalidDataEncoding when unusedBits > 7. Hostile-buffer gtest plus accepting controls at the 0/7 boundary. [#504]

  • BACnetSequenceOf<T> and BACnetListOfReadAccessSpecifications leaked their elements when destroyed without an explicit Reset() first. Neither declared a destructor; ~BACnetObjectBase() calls this->Reset(), but virtual dispatch during base-class destruction resolves to BACnetObjectBase::Reset, not the derived override, so the implicitly-generated destructor destroyed the element vector without deleting what its raw pointers point to. BACnetListOfReadAccessSpecifications is the “blind spot” class of file [#524]’s own round-9 find — a BACnetObjectBase-direct derivative sharing this exact shape; its siblings (WriteAccessSpecifications, ReadAccessResults) already had a destructor, this one did not. Two new gtests. [#519]

  • Three ListOf* Decode() OOM branches reported Abort reason via SetReject, silently truncating a BACnetAbortReason value into a BACnetRejectReason parameter. BACnetListOfReadAccessSpecifications/WriteAccessSpecifications/ReadAccessResults all called error.SetReject(BACnetAbortReason::outOfResources) on their element-allocation-failed path. Fixed all three to SetAbort, matching how the rest of the stack reports OOM. Gated with MemTracker::SetFailAfter(1) to fail the loop’s own new(std::nothrow) on its first iteration — one new gtest per file. [#521]

  • An absent OPTIONAL SEQUENCE OF would decode as a valid empty list, indistinguishable from a present-but-empty one. Since [#475], BACnetSequenceOf::Reset() leaves a sequence VALID; DecodeWithContext’s two optional early-outs (absent tag via a zero-length buffer, and via a present-but-wrong-context-tag) returned without SetValid(false), so an absent optional sequence field was left valid-and-empty. Latent — no DecodeWithContext(..., true) call exists on any BACnetSequenceOf member anywhere in the stack today. Both early-outs now poison before returning. Three new gtests, including the accepting control (present, legitimately empty) that keeps the fix honest. [#520]

  • Six more Decode() bad-parameter branches ran before Reset() and returned 0 without poisoning, same shape as [#524]’s round-9 fix and the [#519]/[#521] finds above. BACnetGetEventInformationACK, BACnetGetEnrollmentSummaryACK and BACnetGetAlarmSummaryACK each have a legal-empty-body branch right after the bad-parameter check that already SetValid(true) correctly — only the buffer == NULL branch above it was missing the poison. BACnetAuthRequest and BACnetAuditLogQueryACK’s combined buffer==NULL||bufferLength==0 branch needed it too (BACnetAuditLogQueryACK had no test file at all before this change). BACnetConfirmedRequestPDU::Decode had the identical pair of missing pokes at both its bad-parameter branches, plus the identical pair again in its sibling DecodeUsingSupportedServices. One new gtest per class, following the established BadParameterDecodeReportsInvalid pattern. [#522]

  • BACnetConfirmedRequestPDU::Decode’s nested-service-request failure gate trusted error.IsSet() as a proxy for “did the decode fail”, which is not reliable. BACnetTag::Decode does NOREF(error) and never populates its error parameter on failure, so a nested service-request decode failing via a raw BACnetTag::Decode call deep inside it (e.g. the audience-list scan in BACnetAuthRequest::Decode) returned size == 0 with error.IsSet() == false — the old gate size == 0 && error.IsSet() skipped entirely on that combination. Fixed to check !this->m_serviceRequest->IsValid() instead, which every service request’s own Decode() already maintains regardless of whether error got populated, and still correctly allows the legitimate “every parameter optional, none present, 0 bytes consumed but VALID” case. Same fix applied to the sibling DecodeUsingSupportedServices. The issue’s suggested repro (a malformed ReadPropertyMultipleRequest reaching dispatch) does not reach unsafe dispatch — BACnetReadPropertyMultipleProcessor::Process has its own redundant IsValid() check on the nested request. Constructing a concrete malformed-PDU demonstration via BACnetAuthRequest (whose processor has no equivalent check — only a CHOICE-discriminator check that stays true across this exact silent-failure path) found that both Decode()’s padding-tolerance loop and DecodeUsingSupportedServices’s stricter leftover-bytes check provide independent, unconditional protection anyway: Decode() never partial-credits, so bufferOffset is never advanced on a failed nested decode, and the trailing-bytes check always re-examines the same (necessarily non-zero-prefixed) bytes and rejects regardless of this gate. No gtest added — none could be constructed that distinguishes old from new behaviour at these two call sites (verified: reverting the fix turns no test red). Fixed anyway on principle, since the padding-tolerance protection is incidental rather than designed for this. [#523]

  • Dead validity conjunct in BACnetFileRecordAccessData::IsValid(). m_returnedRecordCount.IsValid() was checked, but Reset() forces that flag true unconditionally and its only other writer (BACnetPrimitiveUINT::SetValue) also sets it true unconditionally with no failure path of its own — no code path could ever leave it false. Not a live defect, just dead code reading as validation that wasn’t happening (the same shape was live elsewhere: BACnetEventParameterChangeOfTimer’s predicate tested a flag its Reset forced true, letting an invalid element through mid-encode). m_fileRecordData.IsValid() is not dead and is left alone. Cross-checked against [#507]’s “two DecodeWithContext ‘Invalid parameters’ per record-access AtomicReadFile cycle” symptom: unrelated, this fix touches only IsValid()’s predicate, not any DecodeWithContext call site. No new test — pure dead-code removal, confirmed by the two existing gtests passing unchanged. [#518]

  • TestBACnetInterface_ReadPropertyAsync.cpp failed under --gtest_shuffle (found while shuffle-verifying the change above) — the same “0 is a legal invoke id” false invariant fixed for TestBACnetInterface.cpp in the #468 sprint-100 pass, independently present here in 11 ASSERT_NE(sentInvokeId, 0) sites, plus outMessageLength never reset in SetUp() (several tests assert it is 0 without resetting it themselves, relying on whatever the previous test in execution order left behind). Both fixed the same way as the sibling file. Reproduced red at seed 17 before the fix, green after across seeds {3, 7, 17, 42, 99, 123}. [#468]

  • AddListElement/RemoveListElement on a BACnetARRAY of BACnetLIST silently overwrote the whole array instead of honouring the named index — the array index was decoded and then discarded. cl. 15.1.1.1.3/15.2.1.1.3 require the index to select one array element; this stack cannot honour one end-to-end for a LIST element today (no whole-list primitive Set — filed as the #459 interface-change candidate — and RemoveGeneratedPropertyValueElement has no array-index parameter at all), so an array index on a BACnetLIST property is now rejected with PROPERTY/INVALID_ARRAY_INDEX rather than silently accepted and misapplied. Structurally unreachable through any current object profile (none sets both IsArray() and IsList()); no test exercises the branch, the same shape as #513/#515’s equally-unreachable-today guards. [#463]

  • GetProperty could not report a failed read for a LIST of a primitive datatype at all. An unregistered callback, or one that failed partway through a list, fell through to GetDefaultDatatypeValue, which manufactured one default element and reported success — AddListElement then built its post-add list on top of that phantom element. Fixed by discriminating “no callback registered for this property’s element datatype” (still answers with an empty list, so a required-but-unimplemented property still answers ReadPropertyMultiple, same as every other such property) from “a callback is registered and declined” (now a genuine failure, same as any other property). An earlier version of this fix that answered unconditionally with an empty list, or unconditionally fell through, regressed BACnetProprietaryProperty’s required-property tests and BACnetInterfaceMSTPTest’s RPMAll suite respectively before this discriminator was found. [#462]

  • Three list-service error-reporting defects, found by an spec-compliance audit of AddListElement/RemoveListElement’s error paths against cl. 15.1.1.3/15.2.1.3/18.1/18.3. RemoveListElement answered a host refusal with PROPERTY/VALUE_OUT_OF_RANGE, a pair absent from cl. 15.2.1.3.1’s table and scoped to writes by cl. 18.3; now PROPERTY/OTHER. AddListElement answered a host refusal (any code other than valueOutOfRange/writeAccessDenied) with RESOURCES/NO_SPACE_TO_ADD_LIST_ELEMENT — a false out-of-memory diagnosis for a host declining for its own reasons; now PROPERTY/OTHER at both the constructed-write and primitive-typed-write sites. And Add/Remove disagreed on PDU type for the identical “could not be carried out” cause (Add answered Error(SERVICES, OTHER), Remove fell through to Abort(other)) though both clauses require a Result(-) for any failure of either service; both now answer Error(DEVICE, OPERATIONAL_PROBLEM). [#465]

  • Subscribed_Recipients (cl. 12.51.9) needed two rules stricter than the datatype-generic AddListElement path expresses: a verbatim re-add must still renew (write), not be silently ignored as a no-op the way every other list property’s identical re-add already is; and an entry with Time_Remaining == 0 must be rejected. Two new virtual methods added to BACnetObjectBase alongside the existing IsListServiceMatch, in the same override style: AllowsListServiceIgnoreWhenEqual() (default true, preserving today’s behaviour everywhere else) and MeetsListServiceValueConstraints() (default true). BACnetEventNotificationSubscription overrides both. Two pre-existing tests elsewhere pinned the old “identical re-add is a no-op” behaviour specifically for this property and were renamed/rewritten to assert the write now happens. [#466]

  • BACnetFloatingLimitEventAlgorithm implemented OUT_OF_RANGE’s condition set (cl. 13.3.6) and gated four of its six transitions on Limit_Enable — a parameter FLOATING_LIMIT (cl. 13.3.5) does not have, on a Loop object (cl. 12.17) that does not define the property either. A prior bit-assignment fix (#456) made the consequence live: clearing the high bit now suppressed cl. 13.3.5’s real conditions (a)/(c)/(e) and forced a spurious NORMAL transition the clause does not sanction. Fixed: Run() now implements the clause’s actual six conditions unconditionally (both optional cross-transitions supported together, the only way to satisfy the standard’s all-or-nothing rule for an algorithm with no per-instance enable/disable knob), and Limit_Enable is no longer a recognised property on this class at all — it used to accept a write that then had no effect, worse than refusing it outright. SetFloatingLimitParameters/the exported BACnetStack_SetIntrinsicFloatingLimitAlgorithm keep their enableLowLimit/enableHighLimit parameters unchanged (now silently ignored) to avoid an ABI-breaking export-signature change without a separate owner decision. Eight new tests; four pre-existing tests pinning the old gated behaviour removed (nothing left to test on this class). [#464]

  • The test-tool send surface could transmit an EventNotification’s changeOfState variant and only that one — no arm existed for outOfRange (cl. 13.3.6). Added SendConfirmedEventNotificationWithOutOfRangeValues and its unconfirmed twin, following the existing changeOfState pair’s exact shape; BACnetNotificationParameters::SetOutOfRange already existed (used internally by the algorithm) and needed only the send-surface wiring. Four new tests. [#506]

  • BACnetStackDatatypes values cross the test-tool flat boundary as raw bytes (BACnetStackWritePropertyAsync::ToBytes/FromBytes), and four tests pinned them as bare integer literals with nothing pinning the enum itself — an insertion mid-enum silently changes what those literals mean, with no build error. Replaced the four literals with (uint8_t)BACnetStackDatatypes::X casts and added a test pinning every constructed datatype that actually crosses the boundary (DateTime, DeviceObjectPropertyReference, CalendarEntry, Destination, SpecialEvent, DailySchedule, EventNotificationSubscription, LightingCommand) to a concrete literal, following BACnetPhysicalRouterConstantsTest’s existing pattern. Verified against the exact failure mode reported: a mid-enum insertion now fails one named test instead of a scattering of unrelated ones. [#512]

  • A build-integrity regression from the #462 fix above: GetProperty’s LIST-handling step referenced callbacks->CallbackGetPropertyConstructed unguarded, a struct member that is BACNET_STACK_TESTTOOL-only, breaking make NO_TESTTOOL=1. Same defect class #525 fixed a sprint ago. Fixed by matching the file’s own pre-existing pattern: under NO_TESTTOOL, a constructed-element LIST always takes the “nothing registered” branch, since there is no way to register a constructed-datatype callback in that build at all.

  • RESEARCH ONLY, no code change: CHANGE_OF_DISCRETE_VALUE’s clause-21 wire production has no Time_Delay field, but Table 12-15 (cl. 12.12.7) requires Event_Parameters to supply one. Verified both citations verbatim; confirmed the stack’s existing clause-21-following behaviour is not blocked by any BTL row (135.1-2023 8.4.18/8.5.18 never read Time_Delay back out of the notification form). Recommended the owner treat this as confirmed-correct-as-is and consider filing an ASHRAE interpretation request separately. [#516]

  • A subagent code review of this batch’s PR found four further defects, one per reviewed dimension, none previously filed as an issue.

    • BACnetComplexAckPDU::Decode’s nested service-ACK failure gate had the identical error.IsSet() unreliability #523 fixed for BACnetConfirmedRequestPDU — its natural request/ACK sibling — left unfixed. BACnetTag::Decode never populates error on failure, so a nested service-ACK decode failing that way returned size == 0 with error.IsSet() == false, and the old gate silently accepted it as a legitimate empty-body ACK. Fixed identically: !this->m_serviceAck->IsValid(). No gtest — mutation-verified nothing in the current suite distinguishes old from new behaviour here either, the same conclusion #523 reached at its own two call sites; fixed anyway on principle.
    • BACnetFileRecordAccessData::Decode’s bad-parameter branch did not poison validity — flagged as a deferred follow-up in #518’s own commit message (this batch) but never tracked or fixed. Two false starts before landing the fix: SetValid(false) was a no-op because this class’s IsValid() override never consults the base class’s flag at all; making it consult that flag broke two pre-existing round-trip tests via a self-referential SetValid(this->IsValid()) idiom in the setters. Landed fix invalidates m_fileRecordData directly — the member this class already treats as its load-bearing validity signal. New test, both false starts documented in-source.
    • BACnetPrimitiveBitSTRING::Decode enforced only half of 135-2024 cl. 20.2.10’s constraint on an empty bit string. #504 (this batch) closed the unusedBits > 7 underflow, but the clause separately requires unusedBits == 0 when the string is empty — a value like unusedBits=5 with zero content octets passed the range check but is still malformed. New test with the exact wire bytes (0x81 0x05).
    • #504’s own boundary coverage had a gap: nothing tested unusedBits=8, the first value that must be rejected — only a comfortably-outside value (200) and the accepted 0/7 edge were covered, so a shifted-off-by-one bound would have passed the whole suite. New test added directly at the boundary.
  • A fourth class of files carried the identical poisoning defect, found because it does NOT derive from BACnetSequenceOf<T>. Round-9 review, against the round-8 fix below (itself made in this same session). Every prior round’s search for this defect grepped for BACnetSequenceOf<T> subclasses or checked the two derived-Decode overrides named in the original refactor; none of them checked BACnetListOfReadAccessSpecifications, BACnetListOfWriteAccessSpecifications or BACnetListOfReadAccessResults, which derive from BACnetObjectBase directly and hand-roll the identical list-decode loop. All three share the exact shape already fixed twice elsewhere: Decode() has no opening/closing tag, so a bad-parameter call returned 0 with no SetValid(false) at all (the BACnetSequenceOf.cpp A2 shape), and a first-element decode failure breaks out of the loop straight into an unconditional this->SetValid(true) (the BACnetSequenceOfPropertyValue.cpp A1 shape) — here a STRICT violation, not a tolerated one, because none of the three classes consumes an opening tag before the loop, so bufferOffset genuinely can be 0 on return. Concretely: a ReadPropertyMultiple request whose first BACnetReadAccessSpecification is malformed made BACnetListOfReadAccessSpecifications::Decode return 0 while reporting valid — contained today only because BACnetReadPropertyMultipleRequest.cpp checks size == 0 without consulting IsValid(), the same containment pattern that hid the original defect. Fixed identically in all three: SetValid(false) on the bad-parameter and out-of-memory branches, and an elementDecodeFailed && bufferOffset == 0 guard before the final SetValid(true). Two new tests per class (BadParameterDecodeReportsInvalid, FirstElementFailedDecodeReportsInvalid), each independently mutation-gated the same way as the round-8 fixes: production fix reverted, the specific new test went red, fix restored, full suite green with no mutation markers left behind. A parallel sweep by hand for any further instance of the shape (SAFE_DELETE immediately followed by a bare break inside a decode-like loop, across every .cpp in source/) found one more candidate, BACnetAbstractSyntaxType.cpp — verified NOT an instance: that file’s Decode() was fixed for the equivalent defect under issue #365 and already distinguishes a genuine tag-decode failure (poisons via this->Reset()) from a legal clean end-of-buffer (stays valid); the flagged SAFE_DELETE/break pair belongs to GetObjectByDatatypeAndTakeControl, an unrelated lazy-decode accessor that returns a pointer directly rather than participating in the IsValid() contract at all. [#475]

  • The BACnetSequenceOf poisoning fix was claimed complete three commits running. It was not, until now. Round-8 review, against 7c1ac79f. That commit’s own CHANGELOG entry (below) said “ALL FIVE DERIVED POISONS WERE… fixed and gated” — three of the five were not: (1) BACnetSequenceOfPropertyValue.cpp’s element-decode-failure break (its own version of the template’s “third exit,” fixed at BACnetSequenceOf.cpp:291-294 two commits ago and left byte-for-byte unfixed in the derived class) still fell through to SetValid(true) regardless of whether the failed element was the first, so a malformed first BACnetPropertyValue inside a WritePropertyMultiple listOfProperties [1] returned 0 while reporting valid — contained today only because the one live caller, BACnetWriteAccessSpecification.cpp:206-209, treats size == 0 as failure without consulting IsValid(), i.e. the guard this invariant exists to serve was not applied at its own call site. (2) The bad-parameter branch in both BACnetSequenceOf.cpp and BACnetSequenceOfPropertyValue.cpp returned 0 on a NULL/zero-length buffer without poisoning — Reset() has not run yet at that point, so a sequence already valid from a prior decode stayed valid. BACnetListOfResults.cpp already poisoned this path; the other two now match it. (3) BACnetListOfResults.cpp’s element-decode-failure break never called error.Reset(), unlike the template and its sibling — not a poisoning defect (the opening tag is always consumed first, so this path can never return 0 as a failure), but error leaked to the caller from a path that reports SUCCESS with a partial element set, and the CHANGELOG’s own description of this exact scenario (“left the list reporting VALID with a partial element set”) is what motivated fixing the leak rather than filing it. Each of the three is independently mutation-gated by a new test in tests/TestBACnetSequenceOfDerivedDecoders.cpp; the third also needed a fourth new test because mutation testing showed the already-correct error.IsSet() poison in BACnetSequenceOfPropertyValue.cpp (the branch adjacent to (1)) was itself reachable but ungated — nothing had ever driven an element decode failure that sets error through this class. Also fixed: a UTF-8 BOM the round-7 tooling introduced into CASBACnetStackTests.vcxproj/.vcxproj.filters (no sibling project carries one). Sweeping the class rather than the instance is why round 7 and round 8 both missed something the previous round had just fixed elsewhere in the same file family — see the review-process notes this batch’s handoff recorded. [#475]

  • The BACnetSequenceOf change completed: five unpoisoned failure paths in the derived decoders, and a third exit in the template itself. Round-7 review. Because Reset() now leaves a sequence valid, every Decode failure path must poison it again — and the claim that they all did was false. BACnetListOfResults and BACnetSequenceOfPropertyValue override Decode with their own failure paths (five between them), and the base template has a third exit the fix had not accounted for: an element that fails to decode breaks out of the loop rather than returning, and the loop tail marks the sequence valid — so a failure on the first element returned 0 while reporting valid. A legal empty list breaks at that same point having consumed the same nothing, so the two are now told apart by why the loop broke, not by how much was consumed, and the test carries an empty-list control to keep that honest. Consequences were contained — callers check size == 0 and bail — but the object was left in a lying state. All five derived-decoder poisons were ungated, proven by a mutation that removed them with the suite staying green; the first test written for them was ungated too, because BACnetListOfResults already poisoned its opening-tag path, so a buffer that fails immediately passes either way. The test now seeds a well-formed element through the real encoder and truncates after it, reaching the in-loop failure. Also completed: three Reset lines the earlier sweep missed because they are packed onto one line with other statements. Filed rather than fixed: #519, a pre-existing leak — BACnetSequenceOf declares no destructor, so ~BACnetObjectBase’s Reset() dispatches to the base and the elements leak when a sequence is destroyed without an explicit Reset — and #520, a latent trap where an absent OPTIONAL SEQUENCE OF would now decode as a valid empty list (no live call site today). One behaviour change worth naming that the refactor did not call out: a BACnetLogData holding logData with an empty list previously aborted and now encodes an empty [1] list — the same direction as the intended fix. [#475]

  • BACnetSequenceOf now treats an empty sequence as the legal, encodable empty list it is — retiring a convention that four classes had already forgotten. The Fault_Parameters/Event_Parameters fixes above worked by having each owning class mark its own list valid in Reset. That is a convention someone must remember: 22 members carried the line, 17 did not, and the line had been independently rediscovered at least four separate times in the tree (including a comment in BACnetFileRecordAccessData::Reset reading “Empty fileRecordData with returnedRecordCount = 0 is valid”). Two of the 17 aborted mid-encode today — BACnetCOVMultipleSubscription and BACnetSubscribeCOVPropertyMultipleRequest, both of which commit object validity without touching the list and then encode it unconditionally — and the other 15 were one setter change away from the same. Fixed at the source instead: Reset leaves a sequence valid, and IsValid() is now a content check (every element non-NULL and valid), which subsumes both halves of the earlier fix and removes the tautology trap it created. The 19 per-class Reset lines and the nine hand-written predicate loops are deleted; the predicates read as they originally did and are now correct. Three things the change had to preserve, each mutation-gated (correction: the second of these was stated as covering every Decode failure path when first written; it covered the base template’s two and missed five more in the derived decoders plus a third exit in the template itself — all now fixed and gated): an explicit SetValid(false) still poisons a sequence (callers use it to reject a bad insert), every Decode failure path explicitly re-poisons — otherwise a malformed PDU returns 0 while reporting valid and DecodeWithContext’s size == 0 && !IsValid() guard stops discriminating it from a legal empty list — and element types outside BACnetObjectBase (e.g. BACnetAuditNotification, which implements ICodable/IRenderable directly and has no IsValid()) are handled by overload rather than by requiring a base. BACnetAuthorizationStatus’s four Has*() accessors read the list flag as “field present”, which an always-valid empty sequence would make tautologically true, so they move to the GetSize() > 0 test the class’s own Encode already used; they had no callers. Four tests asserting the old “a fresh sequence is invalid” contract are updated to the new one rather than deleted. [#475]

  • The empty-list gates were only as good as the algorithms that happened to exercise them. Round-5 review. Every Reset line making an empty SEQUENCE OF encodable was gated through an algorithm, so coverage depended on whether that algorithm leaves its list empty — and change-of-bitstring’s always inserts exactly one element, leaving its line load-bearing and completely ungated: deleting it kept every test green while a hand-built empty alternative aborted mid-encode. Each event alternative is now encoded directly with an empty list, independent of any algorithm’s arming behaviour, and a mutation removing all six lines now names all six. (The fault side is only partly direct: fault-state and fault-characterstring have no mandatory scalar, so nothing commits their validity on a fresh Reset and a hand-built empty alternative cannot encode by construction — which is exactly why their BuildFaultParameters commits it explicitly. For those two the algorithm path is the only honest one.) Two further gaps of the same kind: the fault side’s new build-implies-encode invariant kept a bare continue on refusal — the exact gap named and fixed on the event side in the same commit — so it silently absorbed fault-state and fault-extended, never encoding one of the three alternatives the fix is about; it now arms both discriminators, requires all seven to build and encode, and treats any refusal as a failure. And change-of-life-safety carries two lists while only one was exercised, so the other loop was deletable with every test green. Verified unchanged by the same review: no scalar conjunct was dropped from any of the nine rewritten predicates, and decode still discriminates a truncated PDU. One dead validity check found outside this work is filed as #518 rather than fixed here. [#475]

  • Two more instances of the empty-list mid-encode abort, one of them created by its own fix, and the converse coverage that was claimed but not written. change-of-timer [22] was the alternative the other six were patterned on — it already marked its empty list encodable — but its predicate still read m_alarmValues.IsValid(), and once Reset forces that flag true the check becomes a tautology: an invalid element passed validation and the element’s own encode gate then aborted mid-response. The same defect, reached from the opposite direction, and the earlier fix is what completed the loop. change-of-bitstring [0] was missed by the sweep altogether — its Reset never marked the list valid and its predicate trusted the flag; no live abort, because its algorithm always inserts exactly one element, but both halves were wrong. (Correction: only one half was then gated. Because the rewritten predicate no longer reads the list flag, the empty-accepted check passed even with the Reset line deleted, and the mutation evidence quoted for that change named eventTypes 1, 8, 13, 17 and 22 — eventType 0 is absent, and that absence was the tell. A following change gates it.) The converse controls covered 2 of 9 predicates while the previous commit message and CHANGELOG entry claimed the direction was gated generally. Reverting the content walk in five of the seven then-fixed alternatives left every test green. Both test files now check both directions per alternative — empty accepted, invalid element refused — because either alone is satisfiable by an always-true or always-false implementation; a single mutation neutering all nine content checks now names all nine. The fault side also gains the build-success-implies-encodability invariant the event side had, so a new fault alternative with this shape cannot ship uncaught. The event sweep no longer continues silently past an algorithm that regresses from building to refusing: the one legitimate refusal (change-of-discrete-value, awaiting its newValue) is named, and any other is a failure. Verified against clause 21: all seven productions are unconstrained SEQUENCE OF, so an empty list is legal in every one. [#475]

  • The same empty-list mid-encode abort in FOUR Event_Parameters alternatives — change-of-state [1], change-of-life-safety [8], access-event [13] and change-of-characterstring [17]. Found by sweeping the defect class by hand after it was fixed on the fault side, rather than one instance at a time: all 44 AreAllParametersValid implementations were enumerated, and every registered event algorithm was probed through build and encode. The existing coverage sweep stops at BuildEventParameters, which is precisely why four live instances were invisible — so this adds the general invariant build success must imply encodability, stated once rather than as four bespoke tests, so the next alternative with this shape fails in CI instead of shipping. These are more reachable than the fault-side instance: no EnableIntrinsic*Algorithm entry point guards its value count at all, so alarmValueCount = 0 passes straight through, and the affected alternatives are served as Event_Parameters on the Event Enrollment — a plain ReadProperty aborts. The fix already existed in the codebase, applied to exactly one of the six list-holding alternatives: BACnetEventParameterChangeOfTimer carries SetValid(true); // empty SEQUENCE OF is encodable in its Reset, and the other four Reset bodies are identical but for that line. This was a fix that never propagated, not a design question — which also independently corroborates the fault-side direction. Predicates now walk the list content rather than trusting a flag Insert/Decode alone sets, so the converse cannot decay either: an invalid element still invalidates the alternative. (Correction: when first written, that converse was gated for one alternative of nine while this entry implied it held generally; a following change extends it to all nine.) change-of-discrete-value [21] still refuses to build without its newValue; refusing is a legitimate answer and is left alone. Each of the four is independently mutation-gated. [#475]

  • An Event Enrollment configured with an EMPTY fault-value list aborted the read mid-encode. fault-characterstring [1], fault-state [4] and fault-life-safety [3] each derived their validity from BACnetSequenceOf::IsValid(), a flag set only by Insert or Decode. An empty list-of-fault-values therefore left the alternative invalid — while BuildFaultParameters still reported success. Nothing between building and encoding revalidates, so the wrapper’s Encode failed its own IsValid() gate and the peer received an APDU Abort(other) part-way through the response instead of a value or a clean property-level error. An empty list is not a malformed configuration: the production is a SEQUENCE OF with no minimum, the arming API explicitly permits faultValueCount == 0, and it means “no value triggers a fault”. Reachable from the public API in one call — EnableFaultCharacterstringAlgorithm(type, instance, NULL, NULL, 0, true). Fixed at all three levels it was broken: the validity predicates now check the content of the list rather than the fact that something was inserted, the two BuildFaultParameters that never reach Insert on an empty list commit validity explicitly, and an empty SEQUENCE OF is marked valid at Reset so it clears EncodeWithContext’s constructed-object gate. This is the same defect class fixed earlier for fault-extended; that fix was applied only to the extended pair, and the coverage sweep could not catch the rest because it arms every algorithm with at least one value. Each of the three parts is independently mutation-gated. [#475]

  • Every build with STACK_OPTION_ROUTER_PHYSICAL off failed to compile. BACnetDataLinkLayer.h declares static const uint16_t DATA_LINK_INSTANCE_UNBOUND inside #ifdef STACK_OPTION_ROUTER_PHYSICAL, and every use of it — GetDataLinkInstanceId, three sites in BACnetNetworkLayer.cpp and four in TestBACnetPhysicalRouterLivePath.cpp, each under that guard or a strict subset of it — is inside the same guard, but the out-of-line definition at BACnetDataLinkLayer.cpp:64 was not, so with physical routing compiled out that line defined a member that does not exist: error: '…DATA_LINK_INSTANCE_UNBOUND' is not a static data member. That is every lean customer configuration, not an edge case. Introduced by the PR #424 review fix that added the constant, and already present in the gitlink the test tool currently commits — not a tip-only regression. Nothing on the PR path could see it, and that is the more useful half of this entry: the definition is correct and necessary in every configuration CI actually compiles (TARGET_FULL has ROUTER_PHYSICAL on, so the guard is satisfied), and ci_scripts/gating-sweep.sh — the only thing that compiles a lean configuration — is workflow_dispatch only by an explicit cost decision (#296), so an option-gating defect is caught only if a human runs the sweep by hand. Found by doing exactly that. Red→green measured with the sweep itself: the STACK_OPTION_OBJECT_TYPE_EVENT_ENROLLMENT, STACK_OPTION_ALARMS_AND_EVENTS and STACK_OPTION_STRING_LITERALS one-hot configs all FAIL on this one translation unit before and pass after. Note separately that make NO_TESTTOOL=1 is also red at this tip for an unrelated pre-existing reason — tests/TestBACnetAuditLogQuery.cpp uses BACnetAuditLogQueryProcessor, BACnetDBDevice and BACnetComplexACKPDU with no BACNET_STACK_TESTTOOL guard anywhere in the file — and make halts at the first failing translation unit, so the number of affected test files is ≥ 1 and not yet established. That one is reported, not fixed here.

  • ci_scripts/dev-build.sh reported a failed compile as a pass. The script’s own $status was already captured correctly via PIPESTATUS[0], but the normal invocation — piped through tail/grep, how this is run interactively and by agents — reports the exit status of the last command in that outer pipe, not the script’s: dev-build.sh '*Foo*' | tail -20; echo $? printed 0 for an injected #error that made make fail. Fixed with an unmistakable BUILD OK / BUILD FAILED sentinel line printed last, so a piped caller can grep the log instead of trusting $?. Also added a pre-build refusal if projects/linux/**/obj* holds a non-ELF object file — a Windows/MSVC-built .o with a newer mtime than its source makes make skip recompiling that translation unit silently, which is how 375 PE/COFF objects previously produced a build that had not actually run (round-4 review of PR #455). The refusal check itself shipped with the hole it exists to catch (runbook L33): file right-pads the filename column when given many paths on one command line (“foo.o: ELF 64-bit...”, not “foo.o: ELF ...”), so a literal grep -v ': ELF ' matched none of them and would have refused every build on a normal-sized object tree. Caught by planting a real non-ELF object (a plain-text .o) and observing the refusal fire on every object in the tree, not just the planted one; fixed by matching ': *ELF '. Verified with both attacks run to completion: an injected compile error still reports BUILD FAILED through the exact piped invocation the header recommends, and a planted non-ELF object is refused by name while a clean tree reports BUILD OK. ci_scripts/pre-push.sh was audited for the same pattern — its own set -uo pipefail and unconditional if ! ./ci_scripts/dev-build.sh; then already capture the real exit code, and it already prints unambiguous ALL GATES PASSED / BUILD OR TESTS FAILED / FAST GATES FAILED lines, so no change was needed there. [#472]

  • make NO_TESTTOOL=1 is now green — full build, full lean suite, 4944/4944 passing. Sweeping the class the way #472’s own note above described it (make halts at the first failing translation unit, so the affected-file count was not established): six test files used test-tool-only symbols with no BACNET_STACK_TESTTOOL guard, not the one file first reported. TestBACnetAuditLogQuery.cpp — BACnetAuditLogQueryProcessor/BACnetDBDevice/BACnetComplexACKPDU come from BACnetAuditLogQueryProcessor.h, itself gated on BACNET_STACK_TESTTOOL && STACK_OPTION_OBJECT_TYPE_AUDIT_LOG; guarded the same way. TestBusinessLogic.cpp assigned callbacks.CallbackGetPropertyConstructed (a struct member that does not exist under NO_TESTTOOL, per BACnetCallbackInterface.h’s own “Sprint 109 - test-tool only”) at three sites; guarded each. The other three surfaced only after the tree linked — BACnetConfirmedRequestPDU.DecodeStillRecognisesTheHighestDefinedServiceChoice asserted service choice 34 (authRequest) always decodes as recognised, but authRequest’s factory case is itself #ifdef BACNET_STACK_TESTTOOL (“Sprint 115 Phase 2”), so under NO_TESTTOOL it is correctly unrecognised and the test’s premise only holds in a test-tool build. SendWriteProperty_StateTextCharacterStringArray and the four SendWriteProperty_SubscribedRecipients_* tests exercised two BACnetInterface.cpp write-value arms (stateText/groupMemberNames 0x1F-splitting, and the EventNotificationSubscription datatype) that are themselves #ifdef BACNET_STACK_TESTTOOL — “the production write path never emits this” per the source’s own comment — so under NO_TESTTOOL the 0x1F delimiter leaked straight into the encoded payload instead of being split, and EventNotificationSubscription was rejected by FromBytes’s allowlist before reaching the arm at all. All six guarded to match the symbols/arms they actually exercise. Verified revert-red per file (guards removed, make -k NO_TESTTOOL=1 reproduces the original compile or assertion failure; restored, the full lean suite builds and passes) and the standard/test-tool build stays at 5312/5312 with no test lost from that side. [#525]

  • The BBMD/virtual-network-port/invoke-id test suite failed under --gtest_shuffle in three files, verified reproducing on the issue’s named seeds (7/8/9/11, 2/3/4) and fixed by three distinct root causes rather than one. tests/BACnetBBMDTests.cpp: the shared mask[6] global is deliberately mutated at the top of ~13 tests (0x00 ↔ 0xFF) to select a subnet width, and every one of those tests sets it explicitly before use — safe on its own — but 14_2_1_1_ExecuteForwardedNPDUOneHopDistribution never sets it and implicitly relies on the declared default (0x00); under shuffle it instead inherited whatever a different, non-adjacent test last left mask[3] at, changing the BDT entries’ effective subnet and the forwarded frame’s length (measured: IUTbroadcastOutMessageLength[0] 18 instead of 25). Fixed by resetting mask[3] in SetUp(), matching every other capture buffer in the same fixture; the file’s loose globals are also moved into an anonymous namespace and SetUp()’s AddDevice/AddNetworkPortObject/AddBDTEntry/SetBBMD calls now assert their bool returns, both defense in depth (a whole-suite nm symbol scan found no live ODR collision) rather than the root cause. tests/TestBACnetIPNetworkPort_VirtualNetwork.cpp: g_networkNumber, a class-static member declared = 2000, is permanently overwritten to 5000 by VirtualNetworkPort_WritingToNetworkNumber_WithCallbackSetPropertyUInt and never restored; VirtualNetworkPort_NetworkNumber never sets it itself, so a shuffle order placing the writer test first fed the wrong network number into AddVirtualNetwork and the hardcoded wire bytes stopped matching. Fixed by resetting it to 2000 in SetUp(). tests/TestBACnetInterface.cpp: BACnetStackInvokeIds::GetInvokeId seeds a never-seen peer’s invoke id with rand() % 256 (issue #71) and lets it wrap 255→0 naturally (cl. 20.1.2.6: 0 is a legal invoke id) — rand()’s PRNG state is process-global, so which value a peer’s first call gets depends on how many other tests already called rand() before it, which shuffle changes. Three tests asserted a false invariant against that legal range: two compared a post-call id to originalInvokeId + 1 without casting to uint8_t first (so a wrap from 255 was compared against the unwrapped int 256), and eleven more (plus two identical instances in the same three named tests) asserted sentInvokeId > 0 after a successful send, which a legally-seeded 0 fails. Cast the two comparisons; removed the thirteen false > 0 assertions (matching an existing 2026-06-21 precedent in the same file for the identical mistake) — the ASSERT_TRUE/ASSERT_EQ already present on the call itself is what establishes the send succeeded. Verified with the full 5312-test suite green across ten --gtest_shuffle seeds including all originally-failing ones (2, 3, 4, 7, 8, 9, 11), not just the three named files in isolation. [#468]

  • Two Alarm-and-Event write blocks in BACnetDBDevice::SetGeneratedPropertyValue sat inside the Notification Class #ifdef they do not depend on, unlike the read side which already hoists the equivalent blocks correctly. In a build with STACK_OPTION_ALARMS_AND_EVENTS but without STACK_OPTION_OBJECT_TYPE_NOTIFICATION_CLASS, every intrinsic A&E property write (eventEnable, notifyType, eventDetectionEnable, eventAlgorithmInhibit, …) would have fallen through to the generic property store silently instead of reaching BACnetStackAlarmAndEventObject::SetProperty. That specific configuration is currently unreachable, verified by compiling it directly: CASBACnetStackOptions.h force-defines STACK_OPTION_OBJECT_TYPE_NOTIFICATION_CLASS wherever STACK_OPTION_ALARMS_AND_EVENTS is defined (line ~1104), and separately wherever STACK_OPTION_OBJECT_TYPE_EVENT_ENROLLMENT is defined (line ~1329, added specifically because Event Enrollment can turn A&E on after the first cascade already ran) — a minimal -DSTACK_OPTION_ALARMS_AND_EVENTS -DSTACK_OPTION_OBJECT_TYPE_EVENT_ENROLLMENT compile confirms STACK_OPTION_OBJECT_TYPE_NOTIFICATION_CLASS ends up defined either way, so the nesting bug could not silently ship in any build the current header can produce. Fixed anyway rather than left as a documented latent defect: it is real in the code as written, it is a one-line-guard fix, it matches the read side’s already-correct structure, and it removes a false compile-time dependency a future header change could re-expose. Also fixed alongside it, per the issue’s second note: the A&E block dereferenced errorContainer->IsSet() unguarded while its neighbouring Event Enrollment branch (added in #475 phase D) already guards with errorContainer != NULL && — the two are now consistent, and the comment on the EE branch documenting the inconsistency is corrected to say so. Verified with the full suite green at 5312/5312 (declaration order and ten --gtest_shuffle seeds). [#514]

  • J.2.5’s source-address substitution reached the “vouched-for peer”, defeating the #427 quota. Annex J.2.5 requires a Forwarded-NPDU’s Originating Device field to be used as the sender’s source address; that substitution rewrites the connection string, and on a non-SC port the network layer resolves the datalink-vouched peer by falling back to that same connection string. So applying it before the network layer runs handed an attacker the one field #301 exists to keep out of reach: four Forwarded-NPDUs naming four different Originating Devices became four quota buckets, one host could fill the whole four-slot held-message queue, a cl. 6.4.4 reject became aimable at a third party, and a Forwarded-NPDU carrying I-Am-Router-To-Network could teach the router a victim’s address as the next hop - with no IP spoofing, no BBMD role and no subnet adjacency. Fixed by pinning the vouched-for peer to the true UDP source before the substitution, and teaching ResolveIngressPeerAddress to prefer an explicitly-set link peer on a non-SC port. This entry originally claimed all three consumers were closed. Two were - the next-hop one was not, because that call site never went through the resolver being fixed; see #470 below, and read the discrepancy as what it is: a fix verified against the code it changed rather than against the vector it claimed to close. Replies go to the originator as J.2.5 and cl. 6.4.4 require; decisions about the sender stay with what the datalink actually saw. Two further gaps closed with it: J.2.5’s own precondition (the clause conditions the substitution on the two addresses differing) was not implemented, and the originating field was unvalidated - Decode fills it straight from the wire, so an all-zero address would have become the reply destination, the synthesised SADR, and a routing-table entry. [#469]

  • Next-hop learning read the attacker-chosen address, which #469 said it had stopped doing. ProcessIAmRouterToNetwork inlined its own SC/non-SC split instead of calling ResolveIngressPeerAddress, and on the non-SC side read ResolveIngressSourceAddress - the CLAIMED source, precisely the field #301 exists to keep out of a decision about a sender. Harmless while the claimed source on IPv4 was the true UDP source; #461’s hoist made the connection string attacker-chosen and #469 then repaired the two consumers that went through the resolver, missing this one because it never called it. One 25-octet datagram was enough - 81 04 0019 | <victim> | 01 80 01 0003, a Forwarded-NPDU naming the victim as Originating Device and carrying I-Am-Router-To-Network for DNET 3 - after which every NPDU the router relays to that network is unicast at whatever host the attacker named. No spoofing, no BBMD role, no subnet adjacency. The site now calls the shared resolver, which both fixes it and deletes the duplicated branch that let the two answers drift apart. Found by asking a reviewer to verify #469’s own claim against the vector rather than against the diff, which is the only reason it was not shipped as fixed. [#470]

  • The Forwarded-NPDU Originating Device is validated as a usable unicast address, not merely as non-zero. The field is filled straight from the wire by Decode with no validation and is then used as the reply destination, the synthesised SADR, and (until #470) the learned next hop. The limited broadcast is the one that matters: accepted, it makes the device answer a unicast to 255.255.255.255, and as a next hop it turns every relayed frame for that network into a subnet-wide broadcast the router itself receives. A zero UDP port is rejected for the same reason. The J.2.5 substitution is also now gated on the vouched-for-peer pin having succeeded - SetLinkPeerAddress clears the peer and returns false on an over-long length, and a rewrite going ahead after that would send the resolver back to the string it had just rewritten, silently restoring the #469 defect. [#469, #470]

  • A BBMD reported success having distributed nothing. ProcessOutgoingMessageBBMD’s outBufferLength == 0 && error.IsSet() arm returned true. It was unreachable dead code until #425 made BACnetBVLL::Encode report its refusals into the error container - which made the arm live for the first time and turned an encode failure into a silent success. It returns false now, so the caller sees the failure it already has handling for. A fix in one file making a dormant path live in another is the hazard worth naming here. [#425]

  • The J.2.5 substitution now has tests, which it shipped without. One pins the reply’s DESTINATION - the existing Forwarded-NPDU test asserted the reply’s bytes and that it was a unicast, but never who it went to, which is exactly why a real regression there was invisible. The others pin the two security properties above, and they are on the router fixture, not the Forwarded-NPDU one: the first version of the quota test used the latter, whose GetHeldMessageCount() is 0 both at baseline and under mutation, so its ASSERT_LE(count, 2) held unconditionally and the test gated nothing. A mutation run caught it. Both tests now assert liveness first - that the path under test was reached at all - before asserting anything about it, which is the general answer to that failure mode and the fourth time on this branch a vacuous assertion has been found holding a false claim in place. [#461, #469, #470]

  • A third unvalidated wire-DADR-to-egress-MAC site, in the relayed-Reject-Message-To-Network path. #432 fixed one, a review found the held-message release path, and a second review found this. Same shape each time: a DADR bounded by the decoder only to the NPCI field width, never to the egress link’s MAC width. Three sites found one at a time is the argument for a shared helper rather than a third copy of the predicate - recorded on #432, not done here, because merging them means unifying three different failure responses. [#432]

  • A BBMD reflected a Forwarded-NPDU back at the peer that sent it, and duplicated its distribution. PR #424 added an outgoingWasAlreadyValid capture meant to stop ProcessBVLL’s tail re-distributing a frame the Forwarded-NPDU arm had already sent. It never fired: outgoingPacket is a fresh local constructed per incoming message, so IsValid() at the capture point is unconditionally false and the guard it fed was unconditionally open. Measured on a BBMD fed one Forwarded-NPDU carrying Who-Is-Router-To-Network: three frames left the stack — the correct J.4.5 re-broadcast, a byte-exact duplicate, and a third reflected at the sender. That third one is the serious one: the peer is itself a BBMD receiving a Forwarded-NPDU and answers identically, so two peers bounce it between them without bound, and the same-subnet guard cannot stop it because the addresses differ. The fix is not a better flag but the shape the sibling arms already had: Reset() the outgoing packet after distributing inline, exactly as the Original-Broadcast and DBTN arms do. That clears both the validity that made it look like a reply to send and the m_isBBMD flag that made #425’s tail distribute it again — and it recovers the network-layer reply on this arm, which is #389’s stated remainder, so a Who-Is-Router-To-Network arriving via a peer BBMD now gets its cl. 6.6.3.3 answer. Why the suite could not see it: the Forwarded-NPDU guard asserted only that something was sent, which three frames satisfy as readily as one — and its frame carried an ASHRAE-reserved message type that fails NPDU decode outright, so it never reached the reply path its comment claimed to exercise. A vacuous assertion had been holding a false comment in place. Both are fixed, and the guard now asserts the destination. [#461]

  • A wire DADR was used as an egress MAC with no length validation, and cl. 6.4.4 reject reason 6 was emitted nowhere in the network layer. On the last hop the incoming NPDU’s DADR is copied verbatim into the egress destination address; SetDestinationAddress bounds it to BACNET_NPDU_MAX_DLEN (7) — the NPCI field width — and nothing compared it to the width of the datalink it was about to be used on. So DNET=<a network reached over BACnet/SC>, DLEN=1 handed a one-octet “VMAC” to CallbackSendMessage: a host that reads the documented six octets reads past the buffer, one that honours the length simply fails to send, and the mirror case delivers a six-octet “MAC” to MS/TP. Reason 6 — “The source message was rejected due to errors in the addressing. The length of the DADR or SADR was determined to be invalid” — is now emitted for it - the first emit site in the file, which already quoted the clause as the right answer. A second site was added later in this branch: the held-message release path had the same unvalidated copy, and its comment claiming both paths shared a rule became false when the first fix landed. That second site is guarded but is not covered by the four tests, which all drive the arrival path. DLEN = 0 is untouched and has its own test: with a specific DNET it means “broadcast on that network” (cl. 6.2.2), and a check written as a bare != expected would break every remote broadcast the router relays — a far more common frame than the malformed one being defended against. An unknown network type returns “cannot say” and the frame passes through unchanged, so a fourth datalink added later is not silently unroutable. One of the three reported items was not reachable, and adding code for it would have been worse than leaving it. The report has an out-of-range DLEN/SLEN “silently consumed” on the search path after a Who-Is-Router-To-Network and a rate-limit token have already been spent. BACnetNPDU::Decode refuses DLEN and SLEN above the maximum outright and returns 0, so such a frame never becomes a forward at all — nothing is consumed and no search is spent. The two length guards there are correctly defence in depth on their fixed-array memcpys, and now say so. A reason-6 reject for that case would have to come from the decode path, which has no ingress port to answer on; that is recorded on the issue rather than half-done. An unreachable emit at that point would have been dead code that looks like a live protocol path. [#432]

  • The router’s per-source held-message quota did not exist on BACnet/IP. It was keyed on the whole connection string, which on Annex J is IP(4) + UDP source port(2) — a field the sender chooses. Four frames from one host on four source ports were four distinct quota keys, filled the entire four-slot queue, and every legitimate peer’s search request was refused for the whole TOmin window. That is the reverse of what MAX_ROUTER_HELD_MESSAGES_PER_SOURCE promises (“two slots leaves at least two other originators able to be served while one peer floods”). The obvious fix would have broken something else, which is the part worth recording: ingressLinkPeerAddress is not only the quota key, it is also the cl. 6.4.4 Reject-Message-To-Network destination at every reject site in the file. Narrowing the stored value to four octets — the shape the report suggested — passes every quota test and addresses every reject to UDP port 0. So the comparison is narrowed instead: the header gains ingressLinkPeerIdentityLength, the full address is stored untouched, and a dedicated test pins that a refused hold is still rejected to the sender’s full IP and port. Identity is derived from the ingress port’s network type, not from the address length, so two datalinks producing equal-length peer addresses cannot alias; entries are counted only when they agree on how much of the address is identity. Every non-IPv4 datalink keeps full-length comparison, so nothing else moves. This does not make the quota trustworthy — the IPv4 datalink is unauthenticated and the address is still spoofable (PICS deviation #12). It makes an attacker pay a spoofed address per bucket instead of a free bind(). The added octet moved sizeof(HeldMessage) from 1560 to 1568 and the queue from 6240 to 6272 bytes, still inside the 6400-byte compile-time budget. The test that pins those exact figures failed, which is precisely what it is for; the numbers were updated with the change rather than the test being loosened into a band that would have absorbed it silently. Verified by mutation rather than by revert: reverting the signature breaks the compile, which verify-fails-without.sh correctly reports as inconclusive rather than as evidence. Restoring the pre-fix key while keeping the signature fails exactly the two discriminating cases. [#427]

  • A BBMD dropped every network-layer reply built on the Original-Broadcast and Distribute-Broadcast-To-Network paths. Both arms distribute their forwarded frame inline, Reset() the outgoing packet, then hand it to ProcessNPDU. When the network layer builds a reply it sets that packet valid but never sets a BVLL — there is not one SetBVLL call in BACnetNetworkLayer.cpp — so ProcessBVLL’s tail ran a second BBMD distribution on a valid, BVLL-less packet. GetFunction() answers Result on a NULL base, Encode returns 0, and every BDT peer and FDT entry got a zero-length datagram before the whole thing failed with internalError — so ProcessMessage never reached the arm that sends the reply. On a BBMD that cost the I-Am-Router-To-Network cl. 6.6.3.3 requires for a Who-Is-Router-To-Network naming a routed network, and every Reject-Message-To-Network from HandleProcessingErrors. The tail is now gated on the packet’s m_isBBMD flag rather than on a second IsValid() capture. That flag already means “this packet is a frame the BBMD should distribute”: it is set in exactly the three arms that build one, and BACnetIPPacket::Reset() clears it — so the two broken arms clear it as a side effect of the reset they already do, while the Forwarded-NPDU arm did not reset - which at the time kept PR #424’s fix on that arm bit-for-bit unchanged. That is no longer true: #461 (below) found PR #424’s guard never fired and made all three arms reset, so the tail is now skipped for every arm that distributes inline. A state flag the code already maintains, rather than a second capture that has to be kept in step with it. Also fixed, because it is why the existing guard was inert: BACnetBVLL::Encode opened with NOREF(error) and returned 0 from all four of its refusals without touching the error container, so BACnetBBMD’s length == 0 && error.IsSet() check could never fire. Each refusal now reports. The zero return is unchanged, so only callers that actually consult the container can notice. The test capture counts and records every send; the single-shot captures elsewhere in this repo structurally cannot see either half — the duplicate distribution needs a frame count and the zero-length sends need every length. One thing worth recording about the tests: the first fixture gave the BBMD its own BDT entry alone, reasoning that with nothing to distribute to, any frame on the wire must be the reply. It was unambiguous and useless — a BBMD does not forward to itself, so the second distribution attempted nothing, returned true, and the reply went out fine without the fix. Two of the five tests then in the file passed in both directions. Adding one peer is what makes the defect reachable, and that is now stated at the fixture so it is not re-simplified later. [#425]

  • Limit_Enable: two out-of-range event algorithms assigned the HIGH-limit bit into m_enableLowLimit. BACnetFloatingLimitEventAlgorithm and BACnetOutOfRangeEventAlgorithm (the analogInput / analogValue OUT_OF_RANGE algorithm) both wrote GetBit(highLimitEnable) into the LOW flag, so m_enableHighLimit was never written from the property at all and m_enableLowLimit held the wrong bit. Both flags were wrong, not one — and the second half is the one that gets missed: a Limit_Enable of (low=TRUE, high=FALSE) disabled low-limit reporting, and (low=FALSE, high=TRUE) wrongly reported lowLimit, so the symptom a user reports is most likely about the low limit. Four sibling algorithms implement the identical two lines correctly, which is what makes it a copy-paste slip rather than a decision. The fix is one token per file; the test is the work, because this is exactly the shape that ships past a green suite. Two things a test here has to get right, and only one of them was obvious: the bits must be varied independently (both set or both clear passes with the defect present, since the two assignments agree whenever the bits agree), and the algorithm must start with both flags FALSE. Every existing test in this family constructs with both limits enabled, so a test built on that fixture would find m_enableHighLimit already true and the “high-limit reporting never updates” half would be invisible — the property write would look like it worked because the constructor had already done it. Each combination is asserted in both directions, since “it reported” alone is satisfied by an algorithm that reports everything. The same cases run against two of the four correct siblings, so the coverage is a rule the family is held to rather than two regression tests bolted onto the files that happened to be wrong. Revert-red fails 6 of the 14 — the three discriminating cases on each broken algorithm — while the sibling cases and the one non-discriminating direction check stay green in both directions, which is the shape a correct test set should have here. [#456]

  • AddListElement was not atomic across elements. cl. 15.1.2: “If one or more elements cannot be added to, or updated in, the BACnetLIST, a ‘Result(-)’ response primitive shall be issued and no elements shall be added to, or updated in, the BACnetLIST. […] The effect of this service shall be to add […] all of the specified elements, or to neither add nor update any elements at all.” It validated every element up front, then wrote them one at a time and returned on the first write failure — leaving elements 0..k-1 applied while answering Result(-). Pre-existing, but newly reachable: before #411 a host-served list’s append index was structurally always 1, so a multi-element append could not physically accumulate; once appends landed at successive indices, a partially-applied list became a real outcome. AddListElement now does what RemoveListElementFromHostServedList already did — decide everything first, then perform exactly one whole-property write. The issue recorded the stack-held store as needing “its own thought” for rollback; it needs none. BACnetStackGroup::SetProperty’s no-index branch already builds the replacement in full before it deletes anything and swaps, so both stores already expose an atomic whole-property write and one change fixes both paths. The incremental writes were the only reason a rollback looked necessary. It also removed machinery: with a whole-list build, “renew entry j” (#412) is an assignment into a vector, so the occupiedIndex tracking and the base + 1 + appended arithmetic are gone, and matching against the list under construction handles the duplicate-within-one-request case for free — that previously needed a second comparison loop. The commit is still net larger (237 insertions / 162 deletions), because it also added the routing helper that decides which write path a property takes. Building on borrowed pointers means a rejected request clones no elements - the working vector still allocates its own storage. Three related behaviours are deliberate and tested: a request whose elements are all already present and identical writes nothing (a whole-property write is observable to the host, so a no-op must not look like a modification); an undecodable served list is still refused, a guard the rewrite makes more load-bearing rather than less, since falling through would now destroy every element the host holds in one call rather than overwriting a prefix; and a post-add list that no longer fits the blob ceiling is NO_SPACE_TO_ADD_LIST_ELEMENT detected before anything is applied. One case is not fixed and is stated in-source rather than implied: a LIST whose element datatype is a primitive is served by a typed Set callback, those take one value, and the public interface has no whole-list primitive Set — so atomicity is not expressible there without widening it. No writable-LIST profile in the build has a primitive element datatype by default, so it is reachable only if a user makes one writable at runtime. The routing question — “would this property’s writes go through the constructed callback?” — is answered from the property’s datatype, not from whether the last read happened to return decodable bytes. That was the first attempt and it was wrong in the case that matters most: an empty host-served list serves nothing, so the very first subscription a device ever received took the typed path and failed. The three existing tests that pinned the per-element indexed write are re-expressed, not deleted: each now asserts the CONTENT of the single write, which is strictly stronger — an index check cannot see a lost, reordered or duplicated entry, and a request that appended a duplicate at a fourth slot and then rewrote the third would have satisfied the old assertion. [#414]

  • Subscribed_Recipients ignored cl. 12.51.9’s Recipient + Process-Identifier match rule, so renewals appended duplicates and removals reported listElementNotFound for subscriptions that were plainly present. Both list-service comparison sites used whole-element equality, and BACnetEventNotificationSubscription::IsEqual compares all four fields including Time Remaining - which counts DOWN on the server. A client that read the list a moment ago and named an entry by value therefore presented a value that no longer matched: the removal half is the normal case, not an edge one, and the list grew on every renewal. Two things had to be got right that the report did not cover. First, cl. 15.1.2 is stronger than “already present => silently ignored”: it says an element already present “shall be updated […] the existing element is over-written”, and only “optionally, if the provided element is exactly the same […] in every way, it can be ignored”. The old matched => skip was legal purely because the match was whole-element, which made “matched” and “identical in every way” the same condition; narrowing the key breaks that coincidence, so the two questions are now asked separately. Second, the write loop recorded nothing about where an element landed, which is what made an overwrite inexpressible - it gained per-element index tracking so a second renewal inside one request resolved onto the first’s slot instead of appending a third copy. (Superseded by #414, which replaced the whole write loop with a single whole-property write; renewal became an assignment into the list being built and the index tracking went away.) The mechanism is a virtual, BACnetObjectBase::IsListServiceMatch, whose default is IsEqual - so the list services stay datatype-generic with no downcast and no include of a header gated more narrowly than they are, which is the blocker that kept this deferred through #411. Hanging a property-specific rule on the datatype is exact rather than merely convenient here: BACnetStackDatatypes::EventNotificationSubscription appears once in the property profile table, as Subscribed_Recipients, so property and datatype are one-to-one; the constraint is recorded at the virtual so a future reader hits it rather than rediscovering it. The consequence worth checking is that for every other datatype “matched but not identical” is unreachable and the new overwrite branch is dead code - every other LIST behaves exactly as before, and two Group List_Of_Group_Members tests assert that rather than assuming it. On the open design question of who owns renewal, the stack does: it already wrote host-served list elements one at a time at an explicit array index, so an overwrite was the same call with the matched entry’s index (again, superseded by #414’s whole-property write) - nothing new is invented and no interface widens - whereas letting the host dedupe would require every host to implement cl. 12.51.9 to be conformant and would do nothing at all for the removal half. The renewal test asserts where the renewal landed, not just that a write happened (the array index when this landed; after #414, the contents of the single whole-property write, which is strictly stronger): “the callback fired” and “the bytes carry 1800” are both satisfied by the old code, which appended the renewal as a third entry carrying exactly those bytes. [#412]

  • AddListElement and RemoveListElement never emitted the mandatory ‘First Failed Element Number’. Table 15-1 (cl. 15.1.1) and Table 15-2 (cl. 15.2.1) mark it M in the Result(-), cl. 15.1.1.3.2 and cl. 15.2.1.3.2 define its value, and clause 21 defines its carrier (ChangeList-Error ::= SEQUENCE { error-type [0] Error, first-failed-element-number [1] Unsigned }), but CreateErrorPDUFromErrorContainer built a plain two-parameter BACnetErrorBase for every service choice, so a client that sent four elements and got an error could not tell which one failed - the entire purpose of the parameter - and a conformance tester checking the Result(-) shape saw a malformed response. BACnetChangeListError already existed but was constructed only on the DECODE side: the mechanism was never implemented, only believed. (#411 had already corrected two comments in BACnetBusinessLogic.cpp that asserted it worked; both of those comments are now correct for the third time and for the first time describe running code.) Two details of the clause decide the implementation and neither was in the issue: the position is 1-based, so the value is i + 1 and not the loop index - an implementation using the raw index reports element 1 as element 0 - and 0 is a reserved sentinel meaning “invalid for reasons other than the ‘List of Elements’”, so it is a MEANINGFUL value rather than “absent”, which is why CErrorContainer carries a separate HAS flag instead of treating 0 as unset. The carrier is selected on service choice, not on whether a number happens to have been recorded: the parameter is mandatory for these two services, so the short form is malformed however the request failed, and a path reaching the builder without a number emits 0 rather than degrading. Set at every failure that is genuinely about an element - both validate loops, the write failure in AddListElement (a per-element write when this landed, one whole-property write after #414), listElementNotFound in both the host-served path and the stack-held Group List_Of_Group_Members path (BACnetStackGroup::RemoveProperty, which is the path most clients actually take). Failures that are not about an element - unknown object, property is not a list, the host rejecting the whole shortened list - deliberately leave it unset and emit 0. Asserted on the ENCODED PDU decoded back through BACnetChangeListError::Decode, not on a return value: asserting on the container alone would have passed against the old code the moment the container gained a field, while the bytes on the wire stayed short. The off-by-one case uses a three-element request failing at the second element, so the two candidate answers are 1 and 2 and no implementation satisfies both. [#413]

  • BACnet/SC: a BVLC-Result NAK rendered the fact that it had failed but not WHY. BACnetSCBVLCResult::GetAttributes emitted resultForBVLCFunction, resultCode and errorHeaderMarker but never the NAK payload - error-class, error-code and the error-details string - so DecodeAsXML/DecodeAsJSON showed an SC failure with no diagnosis, on exactly the path someone reads when something has gone wrong. Correct on the wire, invisible to consumers: the #418/#419 failure class. The commented-out ToString above that function is where it was started - it opens if (this->m_resultCode == BVLC_RESULT_CODE_NAK) { with an EMPTY block. Found by teaching check-getelements-completeness.js the render paths it could not model, which reduced its output to this one real finding. [#428]

  • Node adapter: 142 JS buffers were read through with a caller-supplied length that nothing compared to the buffer - api.SendReadProperty(..., Buffer.alloc(2), 200, ...) told the DLL to read 200 bytes out of 2, an out-of-bounds READ from ordinary JavaScript with no unsafe API and no error at any layer. CheckParametersCountAndType only checks that the argument IS a Buffer, never how big. All are now guarded with ValidateBufferLength before the call. [#392]

  • Node adapter / host API: InsertTrendLogRecord, InsertEventLogRecord and InsertTrendLogMultipleRecord segfaulted when no device had been added. BACnetDatabase::GetDevice() returns NULL on an empty device list, and all three dereferenced it unguarded to look up utcOffset, so calling any of them before AddDevice() crashed the process for ANY arguments. InsertTrendLogRecord is reachable from ordinary JavaScript through the node adapter, so a JS caller could take the process down without doing anything unusual. All three now skip the utcOffset ENRICHMENT rather than aborting: the value already defaults to 0 and the database call underneath does its own device lookup, so a missing device takes exactly the path an unknown deviceInstance always took. The issue reported one site; a sweep found three, and source/ now has no unguarded GetDevice()-> dereference at all. [#398]

  • Build options: three STACK_OPTION_DISABLE_* opt-outs silently lost to an explicit enable. Defining both STACK_OPTION_DISABLE_ROUTER_PHYSICAL and STACK_OPTION_ROUTER_PHYSICAL left the opt-out completely inert - the auto-enable is skipped, but the explicit define already stands - so physical routing compiled in with no diagnostic and the integrator shipped a device doing what they had asked it not to do. An option that is rejected costs five minutes; one that is silently ignored ships. Now an #error, matching the STACK_OPTION_DISABLE_DATA_LINK_SC precedent. The issue named one option; the sweep found STACK_OPTION_DISABLE_PRIVATE_TRANSFER and STACK_OPTION_DISABLE_WHO_AM_I_YOU_ARE share the identical shape, and all three are fixed. Each #error sits BEFORE its auto-enable block - placed after, it would reject every legitimate DISABLE build, which is the configuration the option exists to serve. [#430]

  • BACnetReadResult::SetPropertyValue overwrote an owning pointer without deleting it, leaking a BACnetAbstractSyntaxType on every assignment to a populated object. Its sibling SetPropertyAccessError twelve lines down already deleted before reassigning, which is what establishes the intent: the class owns m_propertyValue and only one of its two setters honoured that. The new guard is also self-assignment safe - handing back the pointer the object already holds is a no-op rather than a delete-then-dangle. [#396] [#398]

  • BACnetPrimitiveOctSTRING::operator= self-assignment: SetValue does m_value.clear() and then reads value[i] out of the vector it just cleared, and GetValue() returns a pointer INTO m_value. Unlike the ten guarded under #396 this loses no data today, because clear() retains capacity - which makes it undefined behaviour (an ASAN container-overflow) rather than a wrong result, with no obligation on any compiler or allocator to keep it working. BACnetAddress::operator= funnels into the same setter. [#398]

  • Notification Class recipients: BACnetDestination::IsValidDayAndTime compared hour, minute and second INDEPENDENTLY against the corresponding field of From Time / To Time, rather than comparing the instant as a whole. For a window of 08:30:00 - 17:00:00 the instant 09:15:00 was rejected, because 15 < 30 failed the minute test - so any window whose bounds were not 00:00:00/23:59:59 silently dropped valid instants and the recipient was not notified. cl. 12.21 describes the pair as “The window of time (inclusive) during which the destination is viable”, so the comparison is now made once on a normalized seconds-since-midnight scalar, with both bounds inclusive. Seconds rather than hundredths because the only clock it is compared against is localtime()‘s struct tm, which has one-second resolution: carrying the bounds’ hundredths into the scalar while “now” is always at .00 skews the two ends in OPPOSITE directions, so a From_Time of 08:30:00.50 would have EXCLUDED the whole of the 08:30:00 second. An unspecified (X’FF’) field in either bound is substituted per FIELD and cascades to the less significant ones - the From bound floors, the To bound ceils - so From 08:00:00.FF still means “from 8am” rather than “no restriction”, and a To_Time of 17:FF:00 ceils to 17:59:59 rather than the incoherent 17:59:00. A From Time LATER than its To Time is an EMPTY window, matching what the old field-by-field comparison did in practice: cl. 12.21 defines no wrap-around rule, and inventing one is incoherent with the per-day test applied first (with Valid Days = Monday, a 22:00 - 06:00 pair would make MONDAY 00:00-06:00 viable - a span belonging to Sunday night - while leaving TUESDAY 00:00-06:00 closed). localtime() returning NULL is also now handled rather than dereferenced. [#415]

  • Notification Class recipients: BACnetDestination::Reset() set Valid Days to all FALSE, directly under a comment quoting cl. 12.21’s default of “Valid Days set to all days”. Every other field in that function already implemented the same paragraph correctly - From Time 00:00:00.0, To_Time 23:59:59.99, Process Identifier 0, Issue Confirmed Notifications FALSE, all Transitions TRUE - so Valid Days was the single field contradicting the function’s own stated contract. A destination left at its Reset defaults matched no day, which made IsValidDayAndTime() return false forever and the recipient was never notified. [#415]

  • Notification Class recipients: BACnetDestination::SetValidDays(mask) only ever SET the bits present in the mask and never cleared the ones absent from it - an OR, not an assignment. This was invisible only because Reset() left every day FALSE; with Reset() corrected above, a set-only mutator would have been unable to express anything except “all days” and SetValidDays(0x00) would have been a silent no-op. Found while fixing the two defects above. [#415]

  • Notification Class recipients: a Valid Days bitstring too SHORT to answer for all seven days is now refused at BACnetDestination::Decode, and IsWeekDayValid reads it through the new TryGetBit so “cannot answer” is an explicit branch rather than being silently indistinguishable from “not valid”. A peer can write a short bitstring through Recipient_List, and reading it with GetBit silently suppressed notifications for every missing day. Refusing at decode is a better fix than deciding at read time, because either answer would have disagreed with what the device REPORTS: Encode() re-emits the same short bitstring, so answering “valid” would have the stack notifying on a day that a ReadProperty of Recipient_List says is not valid. Behaviour and read-back must not diverge. A bitstring LONGER than seven bits is still accepted - padding is not a defect worth an interop failure, and the extra bits are never read. [#405]

  • BACnetPrimitiveBOOL::Encode wrote the context-form content octet with no bounds check against the caller’s maxBufferLength. BACnetTag::Encode succeeds when exactly one byte remains, so a context-tagged Boolean encoding into a 1-byte bound wrote its tag into the last legal byte and then wrote the content octet one past the caller’s buffer - while returning 2, i.e. reporting success having written more bytes than it was allowed to, so a caller doing its own accounting could not detect it. Where that landed depended on the caller: on the BACnetAbstractSyntaxType::CaptureRawBytesFromEncode path the scratch buffer is a local, making it a stack-frame smash rather than a heap scribble. This is the write-side twin of a guard Decode already carried at its own context branch. [#407]

  • BACnetRouterEntry::Decode (cl. 12.56.62 / 21.6, the element type behind Routing_Table) bounded nothing: it accepted a mac-address OctetString of any length, any status octet, and any network-number the UINT decoder would take. The accessors then narrowed with (uint8_t) / (uint16_t) casts, so an over-range value came back as a DIFFERENT, valid-looking one rather than as an error - a 256-octet mac-address made GetMacAddressLength() report 0 while GetMacAddress() still returned 256 bytes, breaking the pairing contract the header states. Decode now refuses (rather than truncates) a mac-address longer than BACNET_NPDU_MAX_DLEN, a network-number above Unsigned16, a performance-index above Unsigned8, and a status above Unsigned8. Note the last one is bounded at what GetStatus() can REPRESENT, not at the three values cl. 21.6 names: this type is decoded when we read a PEER’s Routing_Table, and BACnetAbstractSyntaxType’s list decoder fails the WHOLE property value if any single element is refused - there is no per-element skip - so rejecting an unfamiliar status would turn one vendor-proprietary row into an empty read of the entire table. BACnet ENUMERATEDs are extensible; an unrecognised status is accepted verbatim and logged. SetRouterEntry - the path by which this device builds its OWN Routing_Table - keeps the strict three-value rule, so the stack is liberal in what it accepts and strict in what it sends, and the MAC bound applies on both paths. The MAC bound is stack POLICY, not a clause requirement - the ASN.1 says a bare OctetString - and is the same rule BACnetPhysicalRouter::StoreRoute already applies to a learned next-hop MAC. Latent: no caller consumes those accessors today. [#429]

  • Node adapter: date-bearing exports silently truncated the year. uint8_t year = info[i].As<Napi::Number>().Uint32Value() turned a JS 2026 into 234 - a perfectly legal wire year meaning 2134 - and nothing on any layer rejected, logged or clamped it. Two independent integrators shipped this same bug months apart. The napi boundary is the only layer that can catch it: per cl. 20.2.12 the year octet is “the year minus 1900” and X’FF’ is “unspecified”, so all 256 values are legitimate once narrowed and there is nothing left for the DLL to reject; the caller’s full-width value exists only here. All 60 date octets across twelve wrappers are now range-checked before the cast, throwing a named error that states the conversion (“For the calendar year 2026, pass 126”): SetTrendLogStartStopTime, SendReadRangeWithTime, SendTimeSynchronization, SendUTCTimeSynchronization, SendAcknowledgeAlarm, SendAcknowledgeAlarmWithTimeOfAck, and the six of the event-notification family - InsertNotificationLogRecord, SendConfirmedEventNotification, SendUnconfirmedEventNotification, SendConfirmedEventNotificationWithTimestampForm, and the two WithEventValues variants - which carry a timestamp on every alarm and are therefore the likeliest surface of all. On the four wrappers that take a caller-supplied BACnetTimeStamp form, the date is validated only for the forms that actually read it: the time(0) and sequenceNumber(1) branches never touch those octets, the documented idiom for the unused slots is all-zero filler, and validating them unconditionally would reject a correct call. Non-integer and negative values are rejected too, since Uint32Value() truncates and wraps both silently. The cl. 20.2.12 date PATTERNS stay legal: month 13/14 (odd/even months), day 32/33/34 (last/odd/even), and 255 (unspecified) in any field. C/C++, C# and Java callers are unprotected - their value is narrowed before the stack is entered, so for them this ships as documentation only, on every date-bearing parameter in both export headers. Widening the parameter is ABI-breaking and remains a recommendation in docs/CAS BACnet Stack - Public interface prototype change candidates.md. [#406]

  • Segmentation: apduSegmentTimeout was interpreted in the wrong unit. It is advertised over the wire as APDU_Segment_Timeout, which cl. 12.11.27 defines in MILLISECONDS, and the header documents it as milliseconds - but the internal segment timer added the raw millisecond count to CallbackGetSystemTime(), which is in SECONDS. The stack’s own DEFAULT_APDU_SEGMENT_TIMEOUT of 5000, meant by the spec as five seconds, therefore produced a 5000-second (1 h 23 m) segment timeout: segmentation effectively never timed out and the cl. 5.4 retry path was unreachable. The timer now converts, rounding UP with a one-second floor because the clock it is compared against has one-second resolution - truncating a sub-second timeout to 0 would make the deadline already-expired on the next pass and fire the retry path continuously, which is a worse failure than rounding 500 ms up to 1 s. Migration: a caller compensating for the old behaviour by passing 5 to mean five seconds now gets 5 ms, which the floor turns into one second; pass 5000. Found by the public-interface documentation review, not reported.

  • Node adapter: the 20 documented customer exports that a production build was silently missing. SendWriteGroup, SendGetEventInformation, SendAcknowledgeAlarm, SendSubscribeCOVPropertyMultiple, SendWhoAmI, SendYouAre and the entire Channel / Group / Schedule object family are declared DllExport in the CUSTOMER header and defined ungated in CASBACnetStackDLL.cpp, and none of their wrappers calls a BACnetStackTestTool_* entry point - but all three of their napi layers (declaration, definition, registration) sat inside BACNET_STACK_TESTTOOL purely by position. A --bacnet_testtool=0 build therefore lost a large slice of documented API. All 20 are now outside the gate; the production manifest goes from 190 to 210 names and the gated set from 59 to 39. [#450]

  • Docs: public-interface review. source/CASBACnetStackDLL.h gained the units, ranges, sentinels and failure semantics that were missing on the exports most likely to be got wrong silently, and four outright errors were corrected: AddObject’s documented instance ceiling was a digit-transposed 4914302 (the real limit is 4194302, with 4194303 reserved); AddRecipientToNotificationClass documented Sunday as bit 7 of validDays when it is bit 6 and bit 7 is silently ignored, so a caller following the doc got a destination with no valid days and no error; both text-message senders cited a BACnetCommunicationClass type that has never existed in the codebase (it is BACnetMessagePriority); and AddLoggedObjectToTrendLogMultiple documented an objectInstance parameter its signature does not have. Also newly stated: RemoveObject returns false if and only if the DEVICE is missing (the inner removal result is discarded, so the return cannot tell you whether an object was actually removed); SetTrendLogTypeToPolled’s logInterval is hundredths of a second but resolves to whole seconds, so anything below 100 - including 0 - logs on every Tick(); SetSegmentationEnabled accepts only 2 of the 4 enumeration values it lists, enforces undocumented 1..127 window-size ranges, and ignores every other argument when enabled is false; and SendRegisterForeignDevice never auto-renews, treats timeToLive = 0 as expire-on-next-tick rather than forever, requires a registered clock callback, and can return false AFTER the frame has already gone out.

  • Docs: node adapter api.ts gained a conventions block covering the three things that apply across the whole surface - sentInvokeId is a write-through OUT-parameter needing at least one byte, networkType is IP=0/MSTP=1/SC=2 and is NOT the BACnetNetworkType that AddNetworkPortObject takes, and connectionString is raw address bytes rather than a formatted string - plus JSDoc on 13 of the highest-risk wrappers. Those cover every remaining out-parameter Buffer whose required size was undiscoverable from the TypeScript (GetBDTEntry exactly 6 and 4 bytes, GetFDTEntry 6/2/2, ReadTrendLogRecord’s seven, ReadTrendLogMultipleRecord’s five, GetInfo), the bare-number parameters that are really enumerations (SetServiceEnabled, AddNetworkPortObject, SetProprietaryProperty, InsertTrendLogRecord), and the unstated units and sentinels (SetTrendLogTypeToPolled hundredths, SendRegisterForeignDevice seconds with no auto-renew, SetSchedulePriorityForWriting‘s silent clamp to 16, SetCOVMultipleSettings’ deliberately-absent third parameter). A stale reference to DecodeAsJSON as a standalone function was corrected to DecodeAs(BACnetDecodeType.JSON, ...).

  • Docs: property-arrays.ts now carries the TEXT-value warning its C++ sibling has always had. Values in the write/create/WriteGroup packed buffers are ASCII/UTF-8 TEXT parsed by the stack’s DecodeString - a Real 72.5 is the 4 bytes "72.5", not an IEEE-754 float - and passing raw binary is a SILENT wire-level bug: the Boolean, Unsigned, Signed, Real and Double branches of DecodeString return success unconditionally, so unparseable bytes decode to 0 and the request is sent, encoded and acknowledged with no error at any layer. Only OctetString and BitString fail loudly, which is why a test on those two tags does not reveal the mistake on the other five. Two of the three value fields previously said only “Already-encoded value bytes”, which reads as binary.

  • Docs: the v5-to-v6 breaking-changes doc’s section 3.4 removed-exports table gained the BACnetStack_SetFileObjectStreamAccessContents row it was missing. The export was removed in 5.3.6 and the omission mattered twice over, because the (v5-era) BACnet SC Manual still documents it in five places as the way to load certificates into File objects - so an integrator following that manual writes code against a function that does not link. That manual now carries a v6 notice at the top and inline v6 notes covering all five sites, and section 6.3.1 of the breaking-changes doc adds the worked ‘seed a read-only certificate File object’ example. The replacement is a storage-ownership change, not a rename: contents move from stack-owned storage to host-served RegisterCallbackReadFile/WriteFile. [#445]

  • Docs: SetCOVSettings’s migration guidance said to supply ‘your desired max lifetime (or the device default)’ while nothing named a value expressing that default, forcing every migrating caller to invent one. The device default is 28800 seconds (8 hours) (MAX_SUPPORTED_COV_LIFETIME), and that is the value that makes the migration behaviour-preserving. Also now stated, in sections 3.4/3.4.2, the Doxygen block and the C#/Java/node copies: requests above the maximum are rejected with valueOutOfRange, never clamped - so a very large value does not reproduce v5, it accepts subscriptions v5 refused - and 0 is not ‘unlimited’ and not ‘default’: it rejects every finite-lifetime subscription while still admitting indefinite ones, the exact opposite of maxNumberOfSubscriptions = 0, which does mean no limit. [#448]

  • Docs: AddFileObject now documents that it creates the File object and must not be preceded by an AddObject call, along with which properties the stack stores versus which the application must serve through callbacks. Previously determinable only by reading BACnetDatabase.cpp/BACnetDBDevice.cpp. [#449]

  • Docs: README’s node-gyp/ClangCL warning now covers the Electron case. Pinning node-gyp@^11 in your own package.json is not sufficient - electron-builder/electron-rebuild pull in @electron/node-gyp (a node-gyp 10 fork) transitively, and npm’s node_modules/.bin hoisting can make a bare node-gyp resolve to that fork instead of the pin, reproducing the documented Node 24 failure despite a correct pin. [#451]

  • Docs: README now states plainly that the published npm package is always a TestTool build by default - a statement about the compiled binary, not just about which JS names are in the named-import surface. The previous note described the two-tier naming scheme, which reads as though avoiding the test-tool names gets you a production surface; it does not. [#450]

  • Every broadcast a router emitted on an MS/TP port reached the host with no address and broadcast = false, so it never got onto the bus. BACnetDataLinkLayer::SendRoutedPacket sets the connection string only when the frame is not a broadcast, and BACnetDataLinkMSTP::ProcessOutgoingPacket then hardcoded broadcast = false under a standing ToDo — so the host callback was handed a NULL destination and no broadcast flag. BACnet/IP escaped this only because its BVLL function is converted back into a broadcast on the way out. On an IP + MS/TP router the cl. 6.3.2 fan-out, the cl. 6.6.2 start-up I-Am-Router-To-Network announcement and Who-Is-Router-To-Network route discovery all reached nobody on the MS/TP leg. The MS/TP data link now derives the flag from the absence of a destination address, which is the contract the network layer already documented.

  • A BACnet/SC router port that could not be matched to a data link instance suppressed another port’s broadcast. GetDataLinkInstanceId reported an unbound port as sharing instance 0 — a positive claim of sameness rather than an absence — and the three de-duplication loops (broadcast fan-out, Who-Is-Router-To-Network search, start-up announcement) believed it. With one port bound and another unbound, a global broadcast reached one BACnet/SC network and never the other. An unbound port now reports a sentinel that compares equal to nothing, including to another unbound port.

  • AddListElement overwrote a host-served list it could not decode, and still SimpleACKed. When the host-served list failed to decode, the append base stayed at 0 and the write loop rewrote the existing elements from index 1 instead of extending past them. The fall-through was justified as “callers fall back to their pre-existing behaviour” — sound for a reader, but this caller mutates, and its pre-existing behaviour on an uncountable list is to overwrite it. It now returns an error and never invokes the host’s Set callback with an index derived from a count it failed to establish.

  • A single AddListElement request carrying the same element twice appended it twice. The cl. 15.1.1.3.1 already-present check compared each incoming element only against a snapshot taken before the write loop, which is never re-read. This affects the stack-held List_Of_Group_Members path as well as the host-served one. Pre-existing.

  • Test tool: an InsertNotificationLogRecord call whose arguments a model setter rejects wrote an all-zero record into the Event Log and returned success. The notification constructor logs and continues rather than reporting failure, and nothing downstream checked validity — so an out-of-range eventObjectInstance, or an over-long message text, produced a record naming process-id 0, initiating device 0, object (analog-input, 0), which a later ReadRange served as truth. Now refused. useMessageText with a NULL pointer and a non-zero length is also refused at the flat-export boundary rather than crashing inside the DLL.

  • A registered foreign device could delete an entry past the end of its Foreign Device Table. BACnetBBMD::RemoveForeignDevice’s bound was size() + 1 < index, which admits both index == size() and index == size() + 1, so std::vector::erase ran at end() and one past it — undefined behaviour reachable straight from the public BACnetStack_DeleteFDTEntry export with a host-supplied index and no other bound in between. SetHostBDTIndex carried the identical typo; it corrupts nothing by itself (the index is only read through a bounds-checked accessor) but stored a value that can never resolve, so the device silently stopped forwarding to its FDT instead of refusing the configuration. Both now use the size() <= index form the sibling RemoveBroadcastDistributionTableEntry already had. Pre-existing.

  • A second BACnet/SC data link instance dropped every frame on a direct connection it dialled. BACnetDataLinkSC::OwnsLocalUri ran three of the four tests ProcessIncomingPacket uses to attribute an incoming frame — its comment claimed all three were the same three — and the missing one was the direct-connect initiator websocket. The caller’s source-string fallback does not cover it, because a dialled peer’s URI shares no prefix with any accept URI. So on an instance configured with initiateEnabled, ingress resolved to no instance, fell back to instance 0, and was dropped there as an unknown URI.

  • A BACnet/SC data link instance claimed after SetBACnetSCSettings kept the default timeouts. InheritDeviceWideSettingsFrom copied the UUID, hello capabilities, compatibility flags and the two length limits, but not connectTimeout, wsConnectTimeout, wsReconnectTimeout or heartbeatTimeout — which are set by the same device-wide entry point. A device with two BACnet/SC Network Port objects therefore published two different SC_Connect_Wait_Timeout / SC_Heartbeat_Timeout / SC_Minimum_Reconnect_Time values depending on the order the ports were added, which is the order-dependence that entry point’s loop exists to eliminate.

  • An operator-configured router route could be deleted, repointed or suppressed by one unauthenticated frame. BACnetPhysicalRouter.h documents routes created by AddPort and by LearnRoute (the BACnetStack_AddRouterRoute API) as never evicted, but ForgetLearnedRoute, SetLearnedRouteBusy, SetLearnedRoutesBusyOnPort and StoreRoute’s refresh branch all tested only directlyConnected and never learnedFromNetwork. So a Reject-Message-To-Network deleted a configured route and its next-hop MAC; an I-Am-Router-To-Network repointed it to the advertising port, replaced its MAC and converted it into an evictable learned route; and a Router-Busy-To-Network with no network list suppressed it. The conversion also bypassed the per-port learned-route quota, since the refresh branch skips the quota check on the grounds that refreshing costs no capacity — true only once refreshing can no longer create a learned route out of an operator one. All four now consult learnedFromNetwork, and each new test carries a learned-route control so the fix cannot be satisfied by refusing everything.

  • A log object’s Notification_Threshold, Records_Since_Notification and Last_Notify_Record were served WRONG (Trend Log / Trend Log Multiple) or STALE (Event Log). All three are cl. 12.25/12.27 properties of a log with intrinsic BUFFER_READY reporting, and both object types returned a fabricated value for them while the event algorithm held the real one. This is wire-visible in customer builds and is a conformance defect, not a cosmetic one: because the property profile ENABLES these properties, ReadProperty SUCCEEDED and returned 0 rather than erroring — a wrong answer is worse than a refusal, and any client using these to track notification progress was reading fiction. Measured over the wire before the fix: a Trend Log with Notification_Threshold configured to 3 and an Event Log configured to 5 both served 0 / 0 / 0. Two distinct causes, one per object type. Trend Log / Trend Log Multiple: BACnetPropertyIdentifier::IsAlarmAndEventPropertyIdentifier did not list the three, and BACnetDBDevice::GetProperty gates its alarm-and-event dispatch on that predicate — so the read never reached BACnetBufferReadyEventAlgorithm::GetProperty, which serves all three correctly and was therefore dead code for reads, and fell through to the generic host-callback store. The same predicate does list deadband/highLimit/lowLimit, which is why the analogous OutOfRange rows could always read their limits while these could not. Event Log: BACnetDBDevice::GetProperty dispatches to BACnetStackEventLog::GetEventLogProperty before the alarm-and-event check, and that served the three from its own members, which are decoupled from the algorithm — m_notificationThreshold is set only by WriteProperty (the intrinsic enable never pushes the configured threshold in), m_recordsSinceNotification increments on insert and is reset only by a Record_Count write and never on notification, and m_lastNotifyRecord is initialised to 0 and never written again anywhere in the file. The Event Log now DEFERS these three to the alarm-and-event object when that object actually has a BUFFER_READY algorithm, so the algorithm is the single source of truth; otherwise the members remain the answer and behaviour is unchanged. The narrower gate matters: an alarm-and-event object is created with a NULL event algorithm and the algorithm is installed separately, so deferring on the object’s mere existence would send the read to something that returns false without setting an error — falling through to the generic store and fabricating a 0, which is the defect being fixed. The same gate is applied to Trend Log / Trend Log Multiple writes, which previously returned before the alarm-and-event dispatch could be reached — without that, routing only the reads would have recreated the write-9-read-back-the-old-value split for Trend Logs while removing it for Event Logs. Notification_Threshold writes follow the reads: BACnetBufferReadyEventAlgorithm gained a SetProperty override, and the Event Log write path defers alongside the read path — previously a write landed in a store that disagreed with the threshold the algorithm actually fires on, so a client could write 9, read back 9, and still be notified on the old threshold. Records_Since_Notification and Last_Notify_Record remain algorithm-maintained and are still rejected for writes.

  • BACnetAbstractSyntaxType’s copy assignment silently dropped three members. operator= copied m_objects, m_context, m_isValid and m_memoryBuffer but not m_isArray, m_isList or (test-tool builds) m_capturedRawBytes, while Reset() clears all of them — so a copy lost the list/array shape and the captured payload while looking like a faithful clone. Found reviewing the AddListElement change above, which is what makes the missing m_capturedRawBytes reachable: a copy-built value would arrive at the constructed-Set callback with no bytes and be declined, reintroducing the exact silent timeout that fix removes.

  • Every context-tagged Boolean carrying FALSE was corrupted by a decode → re-encode round trip. A Boolean tag’s length/value/type field means two different things by tag class (cl. 20.2.3): for an application-tagged Boolean there are no content octets and the LVT is the value; for a context-tagged Boolean the LVT is a LENGTH, always 1, with the value in the following content octet. BACnetPrimitiveBOOL::SetValue derived the LVT from the value unconditionally, overwriting the context form with the application form — emitting a tag that claims length 0 while Encode still writes the content octet, which any peer parsing it then walks off the end of. Because Decode’s context branch reads the content octet and then calls SetValue, this hit every context-tagged Boolean that was decoded and re-encoded — measured as ... 19 02 [28 00] 39 01 where ... 19 02 [29 00] 39 01 was required. Only FALSE was affected: SetValue(true) happened to write LVT=1, which is exactly the length the context form wants. This fix is NOT test-tool-gated and is wire-visible in customer builds — confirmed reachable via BACnetPropertyStates (an Event_Enrollment Event_Parameters Boolean written by a client and read back) and BACnetActionCommand’s quitOnFailure / writeSuccessful. The emitted bytes change from malformed to conformant; a peer that had been written to tolerate the malformed form should be re-checked. Found while chasing the AddListElement gap above.

  • A host-held constructed LIST had no working AddListElement path at all. BACnetBusinessLogic::AddListElement builds its per-element value with Clone() + InsertObjectAndTakeOwnership, but the generic BLOB Set pathway (UseCallbackSetPropertyConstructed, test-tool builds) refuses to invoke the host callback unless the value carries captured raw bytes — and only DecodeByDatatype ever captured any. So for any writable constructed LIST the host owns rather than the stack (a Notification Forwarder’s Subscribed_Recipients, for instance) the request decoded correctly, no callback fired, no reply was ever transmitted, and the peer simply timed out with nothing to diagnose. New BACnetAbstractSyntaxType::CaptureRawBytesFromEncode captures the element’s own encoding at the append site, on the host-callback fallback only — after the stack-store attempt, so the stack-held path (Group List_Of_Group_Members) is untouched. Encode() emits exactly the child objects’ encodings with no enclosing opening/closing context tags, which is the same payload span DecodeByDatatype captures, so the host decodes identically-shaped bytes either way. A capture failure is reported as an error rather than silently skipped. Found by BTL 135.1 7.3.2.30.8.2. RemoveListElement’s host path landed in the same release — see its own entry below.

  • A Notification Class recipient’s Transitions field had no effect. Both recipient loops in BACnetStackMessageGenerator (SendEventNotification and SendEventNotificationUsingDataLink) checked only the recipient’s valid day and time and then dispatched, so every recipient inside its window was notified of every event-state transition — including the ones it had explicitly opted out of. The field decoded, stored and re-encoded correctly the whole time, and BACnetDestination::UseToOffNormalTransition and its siblings already existed; nothing under source/ had ever called them. 135-2024 12.21.8 defines the field as “A set of three flags that indicate those transitions {TO_OFFNORMAL, TO_FAULT, TO_NORMAL} for which this recipient is suitable”. Found by BTL 135.1 7.3.2.21.3.4 against a live IUT. Wire-visible behaviour change: a recipient configured with a restricted Transitions bitmask now receives fewer notifications — which is the conformant behaviour, but if a site was (knowingly or not) relying on receiving suppressed transitions, this is where that changed. Nothing changes for the default (TRUE, TRUE, TRUE) recipient that 12.21.8 prescribes and BACnetDestination::Reset installs, and the classification is shared with GetPriorityByEventState / GetAckRequiredByEventState so all three agree on which transition class an event state belongs to. Two consequences of that sharing are worth stating explicitly, because they are wire-visible and neither is exercised by an existing test (at the time, every AddRecipientToNotificationClass call site in tests/ used (TRUE, TRUE, TRUE), so nothing in the suite could have caught them; tests/TestBACnetEventNotificationRecipientTransitions.cpp now adds non-all-true call sites, though ACK_NOTIFICATION and the no-state-change path remain uncovered): (1) an ACK_NOTIFICATION is dispatched with the original event’s toState (BACnetAcknowledgeAlarmProcessor.cpp), so a recipient with to-offnormal cleared no longer receives the acknowledgement notification for an acknowledged TO_OFFNORMAL alarm; (2) no-state-change notifications — BUFFER_READY, ACCESS_EVENT and the change-of-value/discrete-value no-transition paths — carry toState = GetCurrentState() (BACnetStackAlarmAndEventObject.cpp), normally NORMAL, so they are now filtered by the recipient’s TO_NORMAL bit. Both readings look right to us (the recipient’s bit is the counterpart of the object’s Event_Enable bit the standard already gates these on) but both are behaviour changes a reviewer should confirm rather than inherit.

  • Every AddListElement append to a host-served constructed LIST overwrote the same slot. The append index came from BACnetAbstractSyntaxType::GetSize(), which counts child objects — but GetProperty’s host arm stores the served bytes in the AST’s memory buffer and adds no children, so a host-served list measured structurally zero, always, and every append landed on index 1. The list never grew past one entry and each new element silently replaced the previous one. The same always-zero count also fed the cl. 15.1.2 already-present check, so duplicates were never detected either — fixing only the index would have shipped a half-fix that passed its own tests while duplicate recipients accumulated on any re-add. Both now read the decoded element count (DecodeHostServedList). Found by BTL 135.1 7.3.2.30.14.2, which appends eight Subscribed_Recipients entries and reads all eight back.

  • RemoveListElement now has a host-callback path (closes the Known issue of the same name). RemoveListElementFromHostServedList matches every element named in the request against the decoded current list, consuming each match so a request naming an element twice needs it present twice, and only then rewrites the whole property once through the host’s existing constructed-Set callback — atomic in the sense cl. 15.2.2 requires (“no elements shall be removed” on failure). An emptied list is passed as a zero-length value, the documented sentinel. Two knock-on fixes were needed to make it usable: a host-served list that is legitimately EMPTY can now be READ back rather than Aborting (allowEmptyValue) — removing the last element made that state reachable for the first time — and the zero-length sentinel buffer is zero-initialized, because it is handed to a host callback that until now could never see a zero length. Found by BTL 135.1 7.3.2.30.8.5. Not implemented: the Result(-) ‘First Failed Element Number’ that cl. 15.1.1.3 / 15.2.1.3 make mandatory — this stack still emits a plain Error with only errorClass/errorCode, and BACnetChangeListError is used only when decoding. Comments in the source previously asserted otherwise and have been corrected.

  • A whole Subscribed_Recipients list can now be written with WriteProperty (test tool builds). BACnetEventNotificationSubscription::CreateFromCompactString builds the elements from the compact string form the string FFI can carry, mirroring BACnetDestination’s Recipient_List mechanism one property over. The parser validates: a field that is empty, non-numeric, has trailing garbage, or exceeds Unsigned32 is rejected rather than silently read as 0, and an address-choice MAC must be non-empty even-length hex — previously "||||||" produced a valid-looking subscription addressed to device 0, and "zzzz" produced MAC 00:00. This shipped in the same release as the fix that makes it reachable: BACnetStackWritePropertyAsync::FromBytes gates which datatype bytes reach DecodeWritePropertyValue, and the new datatype was not in that allowlist — so the builder was unreachable dead code, FromBytes returned 0, and the TD emitted no PDU at all while the peer timed out waiting for a SimpleACK. This is the same trap as the Sprint 237 entry in this file (CalendarEntry / SpecialEvent / DailySchedule / LightingCommand, all dead for the same reason): adding a datatype’s builder is only half the change. A regression test now guards the fifth case.

  • Live C stack memory could reach the BACnet wire. Four napi GetProperty* callbacks wrote a JavaScript-supplied element count straight through to C (*valueElementCount = napiValueElementCount.Data()[0]). The memcpy_s beside it was bounded; the count was not, so the stack was told a buffer held N bytes when it held at most maxElementCount, and thirteen consumers trust that count against fixed stack arrays — one of them four bytes. It needs an ordinary JavaScript bug rather than an attacker: Buffer.copy truncates silently at the destination size, the length written beside it does not. Measured: a callback reporting 8192 against a 256-byte array put 7,934 bytes of live stack memory into a ReadProperty ACK. All four now clamp with ClampOutputLength, the pattern already used four times in the same file. [#397]

  • Scalar delete on a new[] block in BACnetStackMemoryBuffer, on the encode path for essentially every frame (both BACnetAbstractSyntaxType and BACnetComplexACKPDU hold one by value). ASAN reports alloc-dealloc-mismatch. SAFE_DELETE_ARRAY is the next macro after the SAFE_DELETE that was used. [#397]

  • Node adapter: twelve more write-through buffers were taken off anonymous Napi::Buffer temporaries, so their Length() was gone before it could be checked and the #391 sweep could not see them. ReadTrendLogRecord with a zero-length view was a JavaScript-triggerable access violation — its logging dereferenced four of them unconditionally and built a std::string from an out-of-bounds length. All eighteen anonymous temporaries in the file are now named buffers, the write-through ones guarded, and the lengths used for logging clamped to the buffer they index. [#395]

  • Ten more operator= that empty the object one call frame down — via a setter that clears what the getter aliases, or a SAFE_DELETE that NULLs a member the next line reads off rhs — now guard against self-assignment. #373 missed them because both its sweep and its gate key on a literal Reset() in the operator body. Includes a use-after-free of a polymorphic object on the unconfirmed packet path (BACnetUnconfirmedRequestPDU, Who-Is/I-Am/COV/WriteGroup) that leaves the PDU invalid. [#396]

  • The Windows test suite now matches Linux exactly — 4980 tests from 618 cases, all passing. The last 138 test files, which had never been compiled on Windows, are registered in the MSVS test project. The issue expected “real MSVC compile errors — a quarter of the suite has never seen that compiler”; registering all 138 produced exactly one, and it was a missing include directory (projects/linux/test/makefile.testtool passes -I<repo root>; the MSVS project did not). The files were never broken — they had simply never been compiled, and every real barrier was project configuration. The known-missing backlog is now empty. #386

  • The Windows test suite runs to completion — 4120 tests from 445 cases, 0 failures. tests/TestBACnetComplexAckProcessor.cpp declared its BACnetStackSentConfirmedRequests as a fixture member and handed the address to SetBACnetStackSentConfirmedRequests, which is an owning setter — so ~BACnetDatabase called delete on storage it did not own, every teardown. The MSVC debug CRT validates that pointer and stops; glibc happened not to, so the whole fixture hung on Windows and had always been green on Linux. It is a test defect, not a stack one, and it was the only thing behind the hang. (At the time this was fixed the Windows suite ran 4120 tests to Linux’s 4980; registering the last 138 files, above, closed that gap in the same batch.) [#394]

  • The Windows MSVS build compiles and links again. Four independent defects, none of them the 138 unregistered test files #386 is about. The test project did not define BACNET_STACK_TESTTOOL (the Linux test makefile does), so it compiled test-tool-only tests against a customer-configuration library. Neither project set a C++ standard, so MSVC used C++14 against a C++17 codebase — the same defect binding.gyp had until #387. source/BACnetRouterEntry.cpp, added with #302, was never listed in the library project, so the Windows library did not contain it and the link failed with LNK2019 — a product defect, not a test one. And the library project listed source/BACnetReadPropertyACK.cpp where the tracked file is BACnetReadPropertyAck.cpp, which builds only because NTFS is case-insensitive. #386

  • A single received Forwarded-NPDU no longer crashes a device that is not a BBMD. BACnetDataLinkIPv4::ProcessBVLL called BACnetBBMD::ProcessOutgoingMessageBBMD for every response on every device — every other call site is already inside an if (IsBBMD()) — and that function read this device’s own broadcast-distribution-table entry with neither an Enabled() check nor a bounds check. On a non-BBMD the table is empty, so it was std::vector::operator[] on an empty vector and the member call ran with this == NULL. A BBMD sends Forwarded-NPDUs to every foreign device in its FDT, so an ordinary registered foreign device receives them as a matter of course; any one that provoked a response (an unknown network-layer message answered with a Reject, for instance) segfaulted a device configured with nothing but AddDevice(). Fixed at both levels: the function declines when it is not an enabled BBMD, and all four host-index reads now go through a bounds-checked accessor. [#393]

  • A router behind a BBMD now answers a Who-Is-Router-To-Network that reaches it as a Forwarded-NPDU. Two defects in one path discarded the reply: ProcessBVLLForwardedNPDU reported requiresApduProcessing for a payload that is a network-layer message and therefore has no APDU, so BACnetDataLink::ProcessAPDU declined the whole packet; and PrepareOutgoingBvll had no Forwarded-NPDU arm, so once that was fixed the reply was dropped again as an “invalid BVLL function”. ASHRAE 135-2024 cl. 6.6.3.3 requires that reply to be broadcast locally — or, on a registered foreign device, sent as a Distribute-Broadcast-To-Network through its BBMD (J.2.12). The same path was discarding the Reject-Message-To-Network for an unknown network-layer message. [#389]

  • Node adapter: a JavaScript-supplied Buffer handed to a C export that writes through it is now size-checked before the call, at the 27 argument sites where the buffer is a NAMED Napi::Buffer variable. This is not all of them. Adversarial review found 12 more in ReadTrendLogRecordWrapped and ReadTrendLogMultipleRecordWrapped, which take .Data() off an ANONYMOUS temporary into a raw pointer - a shape neither the sweep nor its gate could see. One of them crashes today. Tracked separately; do not read this entry as covering them. The C side guards its out-parameters with != NULL only, and a zero-length Buffer that is a view into a live allocation (buf.subarray(0, 0), Buffer.from(arrayBuffer, 0, 0)) carries that allocation’s data pointer, so it is not null and the guard passed. The worst instance was not the invoke-id byte the issue was filed on: GetInfo takes its bound from a second JavaScript argument that nothing compared against the buffer, so GetInfo(Buffer.alloc(64).subarray(0, 0), 65536) wrote 2688 bytes past the end of a 64-byte allocation and returned that count as a success. A freshly allocated Buffer.alloc(0) does have a null data pointer and does not reproduce — which is why this needed a gate rather than a review. [#391]

  • Self-assignment no longer empties an object. 95 classes in source/ had a copy-assignment operator that called Reset() and then copied from rhs, so x = x cleared every member and copied the emptied values back onto themselves — leaving a well-formed, completely wrong object with no crash and no diagnostic. Nobody writes x = x; v[i] = v[j] with i == j does, and nothing at that call site says the indices are equal. All 95 now return early on self-assignment, and ci_scripts/check-self-assignment-guards.js keeps it that way. [#373]

  • BACnetAbstractSyntaxType::Decode’s return value and its object state now agree. A buffer sized exactly to its content — no trailing closing tag — is reported as decoded rather than as a failure, and every failure exit now resets the object instead of handing back a valid, populated one. #365

  • A host that does not register BACnetStack_RegisterCallbackGetSystemTime — a configuration the stack supports elsewhere — no longer crashes on a null function pointer. Ten call sites invoked the clock unguarded, including a ReadProperty of Active_COV_Multiple_Subscriptions on a device that has enabled that optional property. Without a clock the stack now declines to send a segmented response, aborting with SEGMENTATION_NOT_SUPPORTED rather than holding a transaction nothing could time out. #380

  • A registered foreign device’s alarm to a broadcast Notification Class recipient now goes through its BBMD. The production BACnetStackMessageGenerator event-notification path used the BACnetAddress overload of SetClientNetworkParameters, which infers broadcast from the MAC and picks Original-Broadcast-NPDU with no foreign-device check — so the alarm reached nobody. #381

  • The foreign-device Distribute-Broadcast-To-Network send no longer hands the host callback a 255-octet length for a 6-octet BBMD address. #382

  • Test tool: a registered foreign device’s broadcast SendUnconfirmedEventNotification (and its WithEventValues sibling) emitted an Original-Broadcast-NPDU onto the device’s own subnet — reaching nobody on the target BACnet/IP network — instead of a Distribute-Broadcast-To-Network through its BBMD (ASHRAE 135-2024 J.2.12). #378

  • tests/TestRendererJSON.cpp’s 7 cases run again. The file sat in projects/msvs/CASBACnetStackTests/ rather than tests/, so the Linux makefile never compiled it — and it still called DecodeAsJSON, replaced by DecodeAs(BACnetDecodeType_JSON, …) in the v5 → v6 change, so the MSVS test project it was listed in has not compiled since. Moved and migrated. #386

  • A router whose BACnet/IP port is a registered foreign device now sends a routed broadcast through its BBMD. BACnetDataLinkLayer::SendRoutedPacket set the egress BVLL directly with no foreign-device check, so the frame went onto the router’s own subnet and reached nobody. ASHRAE 135-2024 J.2.12 names routers explicitly. The no-APDU BVLL preparation also gained the Distribute-Broadcast-To-Network arm it was missing, without which the corrected frame could not be expressed at all. #384 Note that this applies to every broadcast the router emits on that port, including its cl. 6.4.2 I-Am-Router-To-Network announcement and its Who-Is-Router-To-Network route discovery: on a foreign device those become a Distribute-Broadcast-To-Network to the BBMD rather than a local-subnet broadcast, which is the only way a foreign device can reach that audience at all.

  • node adapter: AddDeviceToVirtualNetwork passed the device instance as the network number, and SetCOVSettings passed it as both the subscription cap and the lifetime cap. Both wrappers read info[0] for every parameter, so the arity and types still matched, CheckParametersCountAndType passed, and the C call returned normally — a device configured from JavaScript simply got values it never asked for. A new pre-push gate, ci_scripts/node-adapter-argument-order.js, fails on any napi wrapper that extracts one info[] index into two variables.

  • A crash when BACnet/IP and MS/TP datalinks are active at the same time: BACnetStackNetworkKey::operator< truncated memcmp’s result to int16_t, breaking the strict weak ordering of the std::maps keyed on it. Reported against v4.5.7. #31

  • Cloning a BACnetConfirmedRequestPDU no longer reinterprets a raw request as a typed one (undefined behaviour, reachable from the BACnet/SC and IP clone paths).

  • A device that is both a virtual-network gateway and a physical router no longer loses every directed request to its virtual devices. #306, #294

  • A fully-qualified directed request naming one of our own networks is now answered instead of silently dropped. #308

  • IsConfigurationFile could never return true, so the cl. 19.1 backup keepalive never reset Backup_Failure_Timeout.

  • Enabling Backup and Restore silently emptied Configuration_Files; either declaration order is now valid.

  • A failed whole-array WriteProperty no longer leaves the host’s array grown at a length the client never asked for.

  • A programmatically-built BACnetEventNotificationSubscription could never encode, because its mutators never re-evaluated validity.

  • A record-bearing ReadRange-ACK carrying StatusFlags aborted on encode. #54

  • An undecodable tag is rejected as invalid-tag rather than the catch-all other (cl. 18.8). Part of the #29 family.

  • The Any-value decode path guards the inner-object dereference before cloning it.

  • Option guards in CASBACnetStackDLL.h silently removed RegisterCallbackWriteGroupInhibitDelay, SendWhoAmI and SendYouAre from every ReleaseDll build.

  • SendUnconfirmedCOVNotificationMultiple rejected a foreign initiating device. #91

  • A broadcast unconfirmed COV notification is refused unless Process Identifier is 0 (cl. 13.1). #57, #91

  • SendWhoHasWithObjectName node binding read arg0 as a string instead of a Buffer, throwing on every by-name Who-Has.

  • Node ReadFile binding returned true on callback failure, suppressing the BACnet-Error. #44

  • Post-merge review follow-ups for File record access, including an AtomicWriteFile-ACK that could report a negative position. #275

  • A read at File Start Position == 0 on an empty file is always valid. #45

  • Pushing a release tag shipped nothing — the NoSC release configuration did not build, and publish_npm.yml published package.json’s version instead of the tag’s. #310

  • ci_scripts/*.sh were mode 644 in git, which silently disabled verify-fails-without.sh. #330

  • Review follow-ups: several CI gates that did not gate, including a deleted export definition that passed --check, adapter-coverage.sh and doc-lint.sh. Also pinned the 128-record AtomicReadFile clamp and three other bounds that no test gated. #309

  • C++ adapter hardening: version handshake before symbol bind, idempotent LoadBACnetFunctions(), _WIN32 platform dispatch, and pack-helper examples that taught the wrong (raw IEEE-754 rather than textual) value encoding.

  • DM-BR: the backup/restore failure-timer expiry path now restores System_Status (BTL 13.8.1.x).

  • An unauthenticated 4-octet UDP datagram caused a ~64 KB out-of-bounds read on the primary BACnet/IP receive path — the wire-declared BVLC Length was never compared against the octets that actually arrived. #307
  • On BACnet/SC, a peer could choose its own hold quota and where this router aimed its rejects, by filling in the BVLC Originating-Virtual-Address. Reachable by any node the hub function has accepted. #301, #298
  • Bounded four decode paths where a wire value sized an operation on a fixed buffer, including BACnetRoutingTableEntry::Decode’s unchecked PortInfoLength. #300, #314, #297