Skip to content

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.

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.

In a Python virtual environment:

Terminal window
python -m pip install --upgrade rusty-bacnet==0.15.0

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

  • 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-objects features. The crate now has a default std feature. If you set default-features = false, add features = ["std"] on a hosted target. ["alloc"] is the no_std build: it has no file persistence backends and no tracing, and saves run on the caller’s thread (#1615).
  • Remote errors. A peer’s Error, Reject or Abort PDU is one RemoteError from the wire to the API. Error::Protocol, Error::Reject and Error::Abort are now Error::Remote(RemoteError::Error { class, code }), RemoteError::Reject { reason } and RemoteError::Abort { reason }. Build them with Error::protocol(class, code), Error::reject and Error::abort; the old three-argument Error::protocol is Error::with_detail. The client’s TsmResponse::{Error, Reject, Abort} are TsmResponse::Remote { error, detail }, and the server’s Refusal is RemoteError, also in CovAckResult::Error (#1815).
  • AnyTransport. AnyTransport<S> loses its serial-port parameter: write AnyTransport, and call build_any() or build_any_with_events() without a turbofish. BACnetClient, BACnetServer, RouterPort and EndpointSession default to AnyTransport. NoSerial is gone. Build the MS/TP variant with MstpTransport::new(port, config).into(), or name its type MstpTransport<BoxedSerial> (#2039, #2043).
  • Managed COV. manage_cov_subscription and manage_cov_property_subscription take a CovTarget instead of the destination MAC: CovTarget::local(mac), CovTarget::routed(router_mac, network, address) or CovTarget::of_device(&device). Cancel with unsubscribe_cov_target or unsubscribe_cov_property_target at the same target (#1790).
  • Decoded replies. get_event_information returns a GetEventInformationAck instead of Bytes, so drop your own GetEventInformationAck::decode call; an ACK that doesn’t decode is now the call’s error. create_object returns the new object’s ObjectIdentifier instead of the ACK’s octets (#2077, #2080).
  • Event_Parameters. BACnetEventParameter models every algorithm. Match BACnetEventParameter::None instead of Opaque { tag: 20, .. }, and the new variants instead of Opaque for tags 3, 8, 10, 11, 13 to 18, 21 and 22. The old flat [tag, OctetString] form for those tags no longer decodes. Datum::Constructed has a fields member (#2090).
  • Router lifecycle. BACnetRouter::stop() returns Result<(), RouterStopError> instead of (). Only Ok means cleanup finished; a repeated call returns the same result, and failures() gives the transport errors. Dropping or cancelling alone doesn’t promise the endpoint can be reused (#2002).
  • Router origination. OriginateError::ReservedNetwork now 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 new OriginateError::GlobalWithDadr. One global request can replace a per-network broadcast workaround (#1809).
  • ServerPolicy threads. authorize_mutation and 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 after stop() (#2032).
  • Transport provenance. TransportProvenance now holds the verified leaf certificate, so it is Clone but no longer Copy, and its methods take &self. ScHubAdmissionInput is no longer Copy either (#1965).
  • PICS. PicsConfig loses protocol_version, protocol_revision, vendor_name, model_name, firmware_revision and application_software_version. Set them on the Device object; generate_pics reads them from there and prints them empty without one. bacnet_types::PROTOCOL_VERSION and PROTOCOL_REVISION hold the bundled values (#2125, #2137).
  • Network Port entries. NetworkPortEntry has a new ipv6 field (zeros except on a B/IPv6 entry). Build entries with NetworkPortEntry::bip, bip6 or sc, or add ipv6: [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 a Transport(Error::Encoding(..)); match the variant, not the text (#1841).
  • Device management passwords. DeviceCommunicationControlRequest::encode and ReinitializeDeviceRequest::encode (bacnet_services::device_mgmt) already returned a Result; they now return an encoding error, before anything is sent, for a password that is empty or longer than 20 characters. Pass None for no password; password_valid gives the same check (#2085, #2094).
  • MS/TP monitor. MstpMonitor::start is now async, takes a MonitorRead reader and fails with MonitorAdmissionError; check release_confirmed() before you reopen the port. stop takes &mut self, so keep the monitor mutable. Use DropOnClose only for a synchronous in-memory reader (#1854).
  • Enumerations. ErrorClass, ErrorCode, RejectReason, AbortReason and EngineeringUnits are IntEnum classes. Members are ints, so str() gives the number (repr() still names it), and a value without a name is a pseudo-member whose name is None. A number outside the field raises ValueError instead of OverflowError. Raised errors’ error_class, error_code and reason are members that equal the wire integers (#1816, #2025).
  • Bit strings. Bit strings read, summarized, logged or in result_flags are a BitString (was a dict with unused_bits and data from a read, and a tuple in a log datum or result_flags). bits[i] and iteration give bools; read .unused_bits and .data, and compare .bits, not a tuple (#2080).
  • Decoded returns. get_event_information returns {"summaries": [...], "more_events": ...} instead of bytes; page with the last summary’s object_id while more_events is true. create_object returns an ObjectIdentifier, and atomic_read_file and atomic_write_file return dicts, all instead of bytes (#2077, #2080).
  • Logs and event parameters. In decode_log_records, a notification’s event_values is a dict whose kind names the event type, or None, instead of the encoded bytes. Event_Parameters for the newly modelled algorithms read as their own kind, not other (#2073, #2090).
  • Passwords. A server’s dcc_password or reinit_password that is empty or longer than 20 characters raises ValueError at construction; pass None for no password. Client DCC and reinitialize calls refuse an empty password or one over 20 characters with ValueError (#2060, #2085, #2094).
  • Policies. A mutation_policy callable runs on a blocking-pool thread with no lock held, every WritePropertyMultiple element decided first (#2032). A call queued behind a pending async with entry fails with “client closing” as soon as stop() is called (#2030).
  • Values print decoded. read, readm, read-range and subscribe show 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: ...]). JSON value stays a string; scripts that matched enumerated(N), bits(...) or an object identifier inside it should read the new decoded field (#2078). See value output.
  • State texts. read and readm show a binary or multi-state value with the object’s own text, such as 2 (Low), at the cost of one extra read for each object. JSON value changes for those states: inactive becomes inactive (Inactive). Use decoded (#2132). read-range names a Trend Log’s logged states the same way (#2091).
  • Alarms. bacnet alarms prints 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 has summaries and more_events instead of the hex message (#2077).
  • Created objects. create-object prints Created TYPE:instance instead of the acknowledgment’s hex (#2080).
  • Event Logs. read-range of an Event Log ends each notification with its event values, named for its event type, and adds typed event_values to the record’s JSON (#2073).
  • Typed writes. write sends a number as the property’s type: ENUMERATED for an enumeration, and REAL, Double, INTEGER or Unsigned for an analog or integer value. Before, 42 was always Unsigned and 72.5 always REAL. Bare dates and times parse, and date: and time: refuse out-of-range fields instead of wrapping (#2075).
  • Passwords. control and reinit refuse --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 reason in audit.jsonl is the reason’s name as a string (or its decimal number, as a string, when unnamed) instead of a number. Records also name their front_end, and any field whose name holds token, authorization, secret or key is redacted, as password was (#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.

  • 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_timeout for initiators set above 300 seconds. DirectListener::start refuses 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_bound and Python’s probe_liveness_bound_ms set 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 Bip6Transport was built with Some(instance) of the served Device (#2057). Bip6Transport::start refuses a foreign-device registration whose BBMD isn’t an IPv6 unicast host, or whose port or TTL is 0; Bip6ForeignDeviceConfig::check runs 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).

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.

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.

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