A BACnet protocol stack written in Rust, with Python bindings and a command-line tool. Use it to build BACnet clients, model devices and serve their objects, or explore protocol behavior in a local lab. It targets ASHRAE Standard 135-2020. It isn't BTL certified; Conformance explains what is covered.
Documentation · Install · Quickstart · Transports · Crates · Find it in the docs · Contributing
Note
Release and branch. The latest release is 0.11.0. The published
packages, the hosted guides and docs.rs all describe that release. This README
and the reference docs in docs/ follow the dev branch, which may
include unreleased changes. Those are listed under Unreleased in the
changelog.
Pre-1.0. Public APIs can change in any minor release while obsolete APIs are removed. They freeze at 1.0.0.
- Async Rust client and server. Built on Tokio. Transaction handling, segmentation, discovery, COV subscriptions, and alarm and event services, with the protocol layers split into separate crates you can depend on individually.
- Object models. A server-side object database with standard object types, property metadata, commandable outputs, intrinsic reporting, and draft PICS generation from your server's configuration.
- Five data links. BACnet/IP, BACnet/IPv6, BACnet/SC (secure WebSocket/TLS, including a hub), MS/TP over serial, and Ethernet on Linux. See Transports.
- Routing. A network layer with router tables, routed requests and BBMD and foreign-device support for BACnet/IP.
- Python bindings.
BACnetClient,BACnetServerandScHubwith asyncio support, typed enums and values, and async COV notification streams. Python and Rust expose different configuration surfaces, so check the Python API before assuming a Rust option exists there. - CLI. The
bacnettool does discovery, reads and writes, COV subscriptions, alarms, file transfer and BBMD management over BACnet/IP, BACnet/IPv6 and BACnet/SC, with an interactive shell and optional packet capture. - Shared endpoints (unreleased, on
dev). One device can send requests and answer a limited set of them (ReadProperty by default) through a single BACnet/IP, BACnet/SC or MS/TP transport, from Rust or Python. Use the standalone server when you need its full service set.
Important
Only use this on networks and devices you are authorized to access. Discovery generates network traffic. Writes, device management, time synchronization and file transfers can change real equipment. Start with the loopback examples below. Rusty BACnet makes no physical-safety guarantee.
CI runs the test suites on Linux, and natively on macOS (Apple Silicon) and Windows (x86_64).
python -m pip install rusty-bacnetThis needs Python 3.11 or newer. The import name is rusty_bacnet. Wheels are
published for CPython 3.11–3.13 on Linux (glibc; x86_64, aarch64), macOS
(x86_64 on 10.12 or later, arm64 on 11.0 or later) and Windows (x64), and from
0.12.0 for CPython 3.14 as well, on the same platforms. On any other Python
version, platform or musl-based Linux, pip builds from source, which needs
Rust 1.93 or newer and a C compiler. Add --only-binary=:all: to fail fast
instead.
Add only the crates you need. Most applications start with the client or the server crate:
[dependencies]
bacnet-client = "0.11"
bacnet-types = "0.11"
bacnet-encoding = "0.11"
tokio = { version = "1", features = ["macros", "rt-multi-thread"] }The minimum supported Rust version is 1.93.
Download the bacnet-<os>-<arch> file for your platform from the
latest release,
rename it to bacnet (bacnet.exe on Windows), make it executable and put it
on your PATH.
- 0.11.0 has builds for Linux (amd64, arm64), macOS (amd64, arm64) and Windows
(amd64), all with BACnet/SC. The Linux builds also include packet capture.
They need glibc 2.39 or newer (for example Ubuntu 24.04) and libpcap
(
libpcap0.8on Debian and Ubuntu). - From 0.12.0, the same five builds are published. The Linux builds need only glibc 2.17 or newer (RHEL/CentOS 7, Debian 8, Ubuntu 14.04 and later) and no libpcap package, because they link it statically. The macOS builds need macOS 10.12 (amd64) or 11.0 (arm64) or later. The Windows build links the C runtime statically, so it no longer needs the Visual C++ Redistributable.
From 0.12.0, bacnet-cli is also published on crates.io, so once that release
is out, cargo install bacnet-cli --locked --features sc-tls builds it on any
platform with Rust 1.93 or later. Until then, build it from a checkout. Add
,pcap to the features for capture, which needs the libpcap headers:
cargo install --path crates/bacnet-cli --locked --features sc-tlsTo try unreleased features from dev, such as shared endpoints, build from a
checkout. A dev build still reports version 0.11.0, so record the commit you
built.
- Python: in a virtual environment, run
python -m pip install "maturin>=1,<2", thenmaturin develop --release --manifest-path crates/rusty-bacnet/Cargo.toml --locked. - Rust: depend on the git branch, for example
bacnet-client = { git = "https://github.com/jscott3201/rusty-bacnet", branch = "dev" }.
These examples run entirely on loopback. Start the server in one terminal and leave it running. Then read from it in a second terminal with Python, Rust or the CLI.
Save this as local_server.py and run python local_server.py. It serves one
simulated temperature sensor on 127.0.0.1:47808. Stop it with Ctrl+C.
import asyncio
from rusty_bacnet import BACnetServer
async def main():
server = BACnetServer(
device_instance=1234,
device_name="Local BACnet lab",
interface="127.0.0.1",
port=47808,
broadcast_address="127.0.0.1",
)
# Units 62 = degrees Celsius. Add objects before starting the server.
server.add_analog_input(1, "Zone temperature", units=62, present_value=22.5)
try:
await server.start()
print(f"Listening at {await server.local_address()}", flush=True)
await asyncio.Event().wait()
finally:
await server.stop()
if __name__ == "__main__":
try:
asyncio.run(main())
except KeyboardInterrupt:
passBefore you leave loopback, pick a device instance that is unique on your network.
Python:
import asyncio
from rusty_bacnet import BACnetClient, ObjectIdentifier, ObjectType, PropertyIdentifier
async def main():
# Port 0 picks a free local port, so it doesn't clash with the server.
async with BACnetClient(
interface="127.0.0.1", port=0, broadcast_address="127.0.0.1"
) as client:
value = await client.read_property(
"127.0.0.1:47808",
ObjectIdentifier(ObjectType.ANALOG_INPUT, 1),
PropertyIdentifier.PRESENT_VALUE,
)
print(value.value) # 22.5
asyncio.run(main())Rust: create a project with cargo new, add the dependencies from
Install, replace src/main.rs with the following, then cargo run:
use bacnet_client::client::BACnetClient;
use bacnet_encoding::primitives::decode_application_value;
use bacnet_types::enums::{ObjectType, PropertyIdentifier};
use bacnet_types::primitives::ObjectIdentifier;
use std::net::Ipv4Addr;
#[tokio::main]
async fn main() -> Result<(), Box<dyn std::error::Error>> {
let oid = ObjectIdentifier::new(ObjectType::ANALOG_INPUT, 1)?;
let mut client = BACnetClient::bip_builder()
.interface(Ipv4Addr::LOCALHOST)
.port(0)
.broadcast_address(Ipv4Addr::LOCALHOST)
.build()
.await?;
// A BACnet/IP address: four IPv4 octets, then the UDP port (0xBAC0 = 47808).
let address = [127, 0, 0, 1, 0xBA, 0xC0];
let response = client
.read_property(&address, oid, PropertyIdentifier::PRESENT_VALUE, None)
.await;
client.stop().await?;
let ack = response?;
let (value, _) = decode_application_value(&ack.property_value, 0)?;
println!("{value:?}");
Ok(())
}CLI:
bacnet --interface 127.0.0.1 --port 0 read 127.0.0.1:47808 ai:1 pv
bacnet --interface 127.0.0.1 --port 0 --json readm 127.0.0.1:47808 ai:1 pv,object-nameai:1 is Analog Input 1 and pv is Present_Value. Next, try discovery, COV
subscriptions and multi-property reads in the
Python guide,
Rust guide or
CLI reference. The examples/ directory has
complete Rust, Python and Docker setups.
BACnet/IP is always available. The other transports are opt-in Cargo features
of bacnet-transport:
bacnet-clientalso hasipv6andsc-tlsfeatures.bacnet-serverandbacnet-endpointhavesc-tls.
These turn on each crate's builders for those transports. The Python package includes BACnet/IPv6, BACnet/SC and MS/TP, but not Ethernet.
| Transport | Feature | Notes |
|---|---|---|
| BACnet/IP (UDP/IPv4) | none | Includes BBMD and foreign-device registration. NAT traversal and B/IP multicast are not implemented. |
| BACnet/IPv6 | ipv6 |
Binds one concrete interface and address. With ::, startup fails if the host has more than one candidate, so pass a concrete address. |
| BACnet/SC | sc-tls |
Nodes, direct connections and a hub over TLS 1.3. Requires a site CA, a certificate and key for each device, and a provisioned device UUID. |
| MS/TP | serial (serial-gpio for GPIO direction control) |
Standard frames only (no extended or COBS frames). RS-485 kernel options and GPIO are Linux-only. Evidence comes from a simulator and loopback; on-wire timing isn't qualified on any adapter or OS. |
| Ethernet (802.3 LLC) | ethernet |
Linux only (AF_PACKET). Needs CAP_NET_RAW or root. |
The BACnet/IPv6 and BACnet/SC notes describe dev. In 0.11.0, BACnet/IPv6 can
fall back to a wildcard address, and the CLI loads SC trust from the system
roots. The BACnet/SC guide
covers the release.
How to configure each one:
| Crate | Purpose |
|---|---|
bacnet-types |
Enums, primitives, bit strings and errors (no_std capable) |
bacnet-encoding |
ASN.1 tags, APDU/NPDU codecs, segmentation |
bacnet-services |
Service request and response types |
bacnet-transport |
BACnet/IP, BACnet/IPv6, BACnet/SC, MS/TP and Ethernet data links |
bacnet-network |
Network layer and routing |
bacnet-client |
Async client |
bacnet-objects |
BACnetObject trait, object database and object types |
bacnet-server |
Async server: dispatch, COV, events, scheduling, PICS |
bacnet-endpoint-core |
Shared ownership and transaction coordination for endpoints |
bacnet-endpoint |
One transport owner for both client and server roles (on dev, not yet on crates.io) |
bacnet-cli |
The bacnet command-line tool (release binaries; not on crates.io) |
The others are on crates.io at 0.11.0. The Python package is built from
crates/rusty-bacnet with
maturin. The architecture guide
shows how the layers fit together.
The hosted guides cover the
0.11.0 release. The docs/ references track dev.
| Topic | Where to look |
|---|---|
| Installing and first steps | Installation, choose your path, local lab |
| Rust API | docs.rs (release), docs/rust-api.md (dev) |
| Python API | v0.11.0 (release), docs/python-api.md (dev) |
| CLI | v0.11.0 (release), docs/CLI.md (dev) |
| Discovery and COV | Discovery, observing changes |
| BACnet/SC: credentials, device identity, hub policy | Rust node, Rust hub, Python, hub certificate bindings |
| Shared endpoints (dev) | Rust, Python |
| Writes and authorization | Safe writes, mutation policy, Device Communication Control |
| Audit reporting | Rust, Python, target reporters |
| Policy and resource limits | Engineering docs index |
| Upgrading between releases | Upgrade guide, changelog |
| Troubleshooting | Troubleshooting guide |
Rusty BACnet is not BTL certified and does not claim full BACnet conformance. Support is tracked clause by clause, with the evidence and open gaps for each, in the support summary, Standard 135-2020 ledger and draft PICS. Check the specific service, object and transport you rely on there.
Bug reports, test cases, documentation fixes and focused patches are welcome. See the contributing guide.
git clone https://github.com/jscott3201/rusty-bacnet.git
cd rusty-bacnet
cargo install cargo-nextest --locked # 0.9.145 or newer
cargo build --locked
cargo nextest run --workspace --exclude rusty-bacnet --locked
cargo test --doc --workspace --exclude rusty-bacnet --lockednextest skips doctests, which is why cargo test --doc is a separate step. The
repository pins Rust 1.99.0 in rust-toolchain.toml. For Python binding
development (on Windows, activate with .venv\Scripts\activate; the BACnet/SC
tests also need the openssl command):
python -m venv .venv && source .venv/bin/activate
python -m pip install "maturin>=1,<2"
maturin develop --manifest-path crates/rusty-bacnet/Cargo.toml --locked
python -m unittest discover -s crates/rusty-bacnet/testsdocs/ci.md lists the full set of checks CI runs, including
clippy, rustdoc and the feature matrix.
When you open an issue, include:
- the version or commit;
- your OS and the transport you use;
- a minimal reproduction that leaves out credentials and captures from real networks.
Report security vulnerabilities privately as described in the security policy.