Upgrade from v0.14.0 to v0.15.0
Do not treat this pre-1.0 update as a drop-in replacement merely because the package name stayed the same. v0.15.0 moves the workspace to Rust 1.95, reports every refusal from a peer as one RemoteError, decodes alarms, event parameters, file and create-object replies instead of handing back octets, prints decoded values in the CLI, and changes Rust, Python and CLI APIs and some on-the-wire behavior. This page groups what changes for code and deployments built on v0.14.0.
Before updating
Section titled “Before updating”Record your current package/binary version, enabled features, platform, transport, and important application workflows. List the scripts that read the bacnet CLI’s output: values, alarms and created objects print differently, and JSON gains a decoded field. Check your Rust toolchain: building from source needs Rust 1.95. Keep a known-working deployment or environment available according to your operational policy.
Update the selected interface
Section titled “Update the selected interface”In a Python virtual environment:
python -m pip install --upgrade rusty-bacnet==0.15.0For Rust, move the bacnet-* dependencies to 0.15.0 together. bacnet-time versions on its own (#1768) and its API did not change, so it stays at 0.13.1. Twenty crates are on crates.io; two of them, bacnet-ops and bacnet-mcp, publish for the first time. For the CLI, download the v0.15.0 executable, check it against SHA256SUMS, and confirm its version and features.
Toolchain
Section titled “Toolchain”- Rust 1.95. The workspace uses Rust edition 2024, and the minimum compiler is now Rust 1.95 (was 1.93). Your own crate can keep its edition. Upgrade Rust and Cargo before building from source (#1011).
- Tokio 1.53.3. The crates need tokio 1.53.3 or later, which fixes RUSTSEC-2026-0386. A project with an older lock file runs
cargo update -p tokio(#2046). bacnet-objectsfeatures. The crate now has a defaultstdfeature. If you setdefault-features = false, addfeatures = ["std"]on a hosted target.["alloc"]is theno_stdbuild: it has no file persistence backends and no tracing, and saves run on the caller’s thread (#1615).
Rust API
Section titled “Rust API”- Remote errors. A peer’s Error, Reject or Abort PDU is one
RemoteErrorfrom the wire to the API.Error::Protocol,Error::RejectandError::Abortare nowError::Remote(RemoteError::Error { class, code }),RemoteError::Reject { reason }andRemoteError::Abort { reason }. Build them withError::protocol(class, code),Error::rejectandError::abort; the old three-argumentError::protocolisError::with_detail. The client’sTsmResponse::{Error, Reject, Abort}areTsmResponse::Remote { error, detail }, and the server’sRefusalisRemoteError, also inCovAckResult::Error(#1815). AnyTransport.AnyTransport<S>loses its serial-port parameter: writeAnyTransport, and callbuild_any()orbuild_any_with_events()without a turbofish.BACnetClient,BACnetServer,RouterPortandEndpointSessiondefault toAnyTransport.NoSerialis gone. Build the MS/TP variant withMstpTransport::new(port, config).into(), or name its typeMstpTransport<BoxedSerial>(#2039, #2043).- Managed COV.
manage_cov_subscriptionandmanage_cov_property_subscriptiontake aCovTargetinstead of the destination MAC:CovTarget::local(mac),CovTarget::routed(router_mac, network, address)orCovTarget::of_device(&device). Cancel withunsubscribe_cov_targetorunsubscribe_cov_property_targetat the same target (#1790). - Decoded replies.
get_event_informationreturns aGetEventInformationAckinstead ofBytes, so drop your ownGetEventInformationAck::decodecall; an ACK that doesn’t decode is now the call’s error.create_objectreturns the new object’sObjectIdentifierinstead of the ACK’s octets (#2077, #2080). - Event_Parameters.
BACnetEventParametermodels every algorithm. MatchBACnetEventParameter::Noneinstead ofOpaque { tag: 20, .. }, and the new variants instead ofOpaquefor tags 3, 8, 10, 11, 13 to 18, 21 and 22. The old flat[tag, OctetString]form for those tags no longer decodes.Datum::Constructedhas afieldsmember (#2090). - Router lifecycle.
BACnetRouter::stop()returnsResult<(), RouterStopError>instead of(). OnlyOkmeans cleanup finished; a repeated call returns the same result, andfailures()gives the transport errors. Dropping or cancelling alone doesn’t promise the endpoint can be reused (#2002). - Router origination.
OriginateError::ReservedNetworknow means DNET 0 only. A request to DNET 0xFFFF with an empty DADR goes out on every port; one with a DADR fails with the newOriginateError::GlobalWithDadr. One global request can replace a per-network broadcast workaround (#1809). - ServerPolicy threads.
authorize_mutationand the DeviceCommunicationControl, LifeSafetyOperation and AuditNotification decisions run on a Tokio blocking-pool thread with no lock held. Thread-locals from the request’s thread don’t carry over, a WritePropertyMultiple policy sees the database as it was before the request, and a policy that never returns keeps the process from exiting afterstop()(#2032). - Transport provenance.
TransportProvenancenow holds the verified leaf certificate, so it isClonebut no longerCopy, and its methods take&self.ScHubAdmissionInputis no longerCopyeither (#1965). - PICS.
PicsConfiglosesprotocol_version,protocol_revision,vendor_name,model_name,firmware_revisionandapplication_software_version. Set them on the Device object;generate_picsreads them from there and prints them empty without one.bacnet_types::PROTOCOL_VERSIONandPROTOCOL_REVISIONhold the bundled values (#2125, #2137). - Network Port entries.
NetworkPortEntryhas a newipv6field (zeros except on a B/IPv6 entry). Build entries withNetworkPortEntry::bip,bip6orsc, or addipv6: [0; 16]to a struct literal (#1940). - BBMD helpers. A request the B/IP transport refuses before sending fails with
BvlcExchangeError::Refused(ExchangeRefusal::..)instead of aTransport(Error::Encoding(..)); match the variant, not the text (#1841). - Device management passwords.
DeviceCommunicationControlRequest::encodeandReinitializeDeviceRequest::encode(bacnet_services::device_mgmt) already returned aResult; they now return an encoding error, before anything is sent, for a password that is empty or longer than 20 characters. PassNonefor no password;password_validgives the same check (#2085, #2094). - MS/TP monitor.
MstpMonitor::startis nowasync, takes aMonitorReadreader and fails withMonitorAdmissionError; checkrelease_confirmed()before you reopen the port.stoptakes&mut self, so keep the monitor mutable. UseDropOnCloseonly for a synchronous in-memory reader (#1854).
Python API
Section titled “Python API”- Enumerations.
ErrorClass,ErrorCode,RejectReason,AbortReasonandEngineeringUnitsareIntEnumclasses. Members are ints, sostr()gives the number (repr()still names it), and a value without a name is a pseudo-member whosenameis None. A number outside the field raisesValueErrorinstead ofOverflowError. Raised errors’error_class,error_codeandreasonare members that equal the wire integers (#1816, #2025). - Bit strings. Bit strings read, summarized, logged or in
result_flagsare aBitString(was a dict withunused_bitsanddatafrom a read, and a tuple in a log datum orresult_flags).bits[i]and iteration give bools; read.unused_bitsand.data, and compare.bits, not a tuple (#2080). - Decoded returns.
get_event_informationreturns{"summaries": [...], "more_events": ...}instead ofbytes; page with the last summary’sobject_idwhilemore_eventsis true.create_objectreturns anObjectIdentifier, andatomic_read_fileandatomic_write_filereturn dicts, all instead ofbytes(#2077, #2080). - Logs and event parameters. In
decode_log_records, a notification’sevent_valuesis a dict whosekindnames the event type, or None, instead of the encodedbytes. Event_Parameters for the newly modelled algorithms read as their ownkind, notother(#2073, #2090). - Passwords. A server’s
dcc_passwordorreinit_passwordthat is empty or longer than 20 characters raisesValueErrorat construction; passNonefor no password. Client DCC and reinitialize calls refuse an empty password or one over 20 characters withValueError(#2060, #2085, #2094). - Policies. A
mutation_policycallable runs on a blocking-pool thread with no lock held, every WritePropertyMultiple element decided first (#2032). A call queued behind a pendingasync withentry fails with “client closing” as soon asstop()is called (#2030).
CLI and TUI
Section titled “CLI and TUI”- Values print decoded.
read,readm,read-rangeandsubscribeshow values as the TUI does: enumerations and bits by name, schedules, timestamps and recipients rendered, and lists whole. Undecoded octets print as[raw 0e 0f](was[raw: ...]). JSONvaluestays a string; scripts that matchedenumerated(N),bits(...)or an object identifier inside it should read the newdecodedfield (#2078). See value output. - State texts.
readandreadmshow a binary or multi-state value with the object’s own text, such as2 (Low), at the cost of one extra read for each object. JSONvaluechanges for those states:inactivebecomesinactive (Inactive). Usedecoded(#2132).read-rangenames a Trend Log’s logged states the same way (#2091). - Alarms.
bacnet alarmsprints a decoded table of each object’s event state, transitions, timestamps, notify type and priorities, and follows the device’s pages up to--max. JSON hassummariesandmore_eventsinstead of the hexmessage(#2077). - Created objects.
create-objectprintsCreated TYPE:instanceinstead of the acknowledgment’s hex (#2080). - Event Logs.
read-rangeof an Event Log ends each notification with its event values, named for its event type, and adds typedevent_valuesto the record’s JSON (#2073). - Typed writes.
writesends a number as the property’s type: ENUMERATED for an enumeration, and REAL, Double, INTEGER or Unsigned for an analog or integer value. Before,42was always Unsigned and72.5always REAL. Bare dates and times parse, anddate:andtime:refuse out-of-range fields instead of wrapping (#2075). - Passwords.
controlandreinitrefuse--password ""(leave the flag out to send none) and a password over 20 characters, before sending (#2085, #2094). - TUI audit log. A reject or abort outcome’s
reasoninaudit.jsonlis the reason’s name as a string (or its decimal number, as a string, when unnamed) instead of a number. Records also name theirfront_end, and any field whose name holdstoken,authorization,secretorkeyis redacted, aspasswordwas (#1814, #2068). - Device text. Table output drops escape sequences, bidirectional overrides and other invisible characters from device names, values and messages; JSON keeps the exact text (#2122, #2144).
- TUI rows. The browser and watch list show a property the device couldn’t give in kebab case (
unknown-property,rejected: unrecognized-service) (#2065).
Profiles from v0.14.0 load unchanged. A profile that gains an [mcp] table can’t be read by a v0.14.0 binary, which refuses unknown keys.
Wire behavior
Section titled “Wire behavior”- BACnet/SC liveness. An idle node sends a heartbeat every interval instead of every second one, keeps a hub that answers, and drops an unanswered link one timeout after the last frame received (#2124). Direct connections take part in heartbeats: an accepted one answers Heartbeat-Request, and a dialed one sends a heartbeat after an interval of silence and closes the link when it goes unanswered (#2127).
- SC timeouts. A direct listener keeps an idle accepted link for 360 seconds instead of 60; raise it with
with_idle_timeoutfor initiators set above 300 seconds.DirectListener::startrefuses an idle timeout of zero or over a day, and a connect timeout of zero or over 300 seconds (#2133). The hub retires a silent connector only after 360 seconds without traffic since its probe, so a dead connector lingers about 8 minutes instead of 65 to 95 seconds;ScHubProbePolicy::with_liveness_boundand Python’sprobe_liveness_bound_msset it (#2141). - Passwords. A server or endpoint refuses to start with a configured DCC or ReinitializeDevice password that is empty or longer than 20 characters. It counts the 20-character limit in characters, not octets, and rejects a longer password from a peer with PARAMETER_OUT_OF_RANGE. An empty password from a peer counts as none (#2060, #2085, #2094).
- B/IPv6 ports. A server or endpoint refuses to start a registered B/IPv6 Network Port unless its
Bip6Transportwas built withSome(instance)of the served Device (#2057).Bip6Transport::startrefuses a foreign-device registration whose BBMD isn’t an IPv6 unicast host, or whose port or TTL is 0;Bip6ForeignDeviceConfig::checkruns the same test first (#1941). - Event Enrollment. A write of Event_Parameters whose newly modelled algorithm doesn’t follow the standard’s form is refused; it used to be kept as opaque octets (#2090).
- DateTime values. A DateTime Value or DateTime Pattern Value sends each set Priority_Array slot as one date-time element, so a whole read gives 16 slots (#2098).
- Routers. A router drops a global broadcast or Who-Is-Router whose source address is its own, counted in
own_source_drops(#2021). The opt-in duplicate suppression also drops repeated I-Am-Router, Router-Busy, Router-Available and relayed Who-Is-Router messages (#1810, #2020). - B/IP multicast. A BBMD serving a multicast group takes writes to BACnet_IP_Multicast_Address, and a ReinitializeDevice applies the new group and the BDT written beside it together, or neither (#1963).
- Client limits. The client’s advertised max-APDU and request segment size rise again when the transport’s limit grows after start, capped by the configured maximum (#2086).
New crates: bacnet-ops and bacnet-mcp
Section titled “New crates: bacnet-ops and bacnet-mcp”bacnet-ops and bacnet-mcp publish for the first time. bacnet-ops is the UI-free operations core that the CLI, its TUI and the MCP server share: targets, parsing, the value formatter, limits and the read engine (#2065). bacnet-mcp is the Model Context Protocol server behind bacnet mcp serve. It is experimental and read-only unless a profile opts in to guarded writes. The prebuilt CLI downloads include it as bacnet mcp serve; a source build enables the CLI’s mcp cargo feature (#2066, #2067, #2068). Neither changes code that doesn’t use it.
Verify more than import success
Section titled “Verify more than import success”Exercise a known read, error handling, any routing or segmentation paths you depend on, subscription lifecycle, relevant server objects, and controlled cleanup. Rerun every script that reads CLI output: values, states, alarms and created objects print differently, and JSON has decoded. Check code that matched on Error::Protocol, Reject or Abort, or on Python enum str(). Check that a configured DCC or reinitialize password is between 1 and 20 characters. On BACnet/SC, check links that sit idle for minutes, since heartbeat and idle timing changed.
Retest your application’s output parsing where it depends on CLI JSON keys, typed Python values or Rust models. Separate regressions from corrected behavior that an older integration may have depended on accidentally. The integration guides explain shared endpoints, Network Port registration and SC setup.
Sources and release scope
Section titled “Sources and release scope”This guide covers the move from v0.14.0 to v0.15.0. Source review is not hardware qualification.
v0.15.0 release notes · Changelog · Rust API guide · Python API guide.
Release v0.15.0 ·Build and integrate. Pre-1.0 APIs; partial conformance.Support & limitations