13 releases (5 breaking)
| new 0.6.4 | Mar 7, 2026 |
|---|---|
| 0.6.3 | Mar 7, 2026 |
| 0.5.5 | Mar 6, 2026 |
| 0.4.0 | Mar 5, 2026 |
| 0.1.5 | Mar 5, 2026 |
#1054 in Network programming
Used in 2 crates
1MB
27K
SLoC
Rusty BACnet
A complete BACnet protocol stack (ASHRAE 135-2020) written in Rust, with first-class Python bindings via PyO3.
Features
- Full BACnet/IP stack — async client and server with 30+ service types
- 5 transports — BACnet/IP (UDP), BACnet/IPv6 (multicast), BACnet/SC (WebSocket+TLS with hub), MS/TP (serial), Ethernet (BPF)
- 62 object types — All standard BACnet objects including Analog/Binary/MultiState I/O, Device, Schedule, Calendar, Trend Log, Notification Class, Loop, Access Control, Lighting, Life Safety, Elevator, and more
- Python bindings — async client, server, and SC hub with full API parity via PyO3
- 1682 tests, 0 clippy warnings, CI on Linux/macOS/Windows
Quick Start (Python)
pip install rusty-bacnet
import asyncio
from rusty_bacnet import (
BACnetClient, ObjectType, ObjectIdentifier,
PropertyIdentifier, PropertyValue,
)
async def main():
async with BACnetClient() as client:
oid = ObjectIdentifier(ObjectType.ANALOG_INPUT, 1)
# Read a property
value = await client.read_property(
"192.168.1.100:47808", oid, PropertyIdentifier.PRESENT_VALUE
)
print(f"{value.tag}: {value.value}") # real: 72.5
# Write a property
await client.write_property(
"192.168.1.100:47808", oid, PropertyIdentifier.PRESENT_VALUE,
PropertyValue.real(75.0), priority=8,
)
# Discover devices
await client.who_is()
await asyncio.sleep(2)
for dev in await client.discovered_devices():
print(f"Device {dev.object_identifier.instance} vendor={dev.vendor_id}")
# Read multiple properties at once
results = await client.read_property_multiple("192.168.1.100:47808", [
(oid, [
(PropertyIdentifier.PRESENT_VALUE, None),
(PropertyIdentifier.OBJECT_NAME, None),
]),
])
asyncio.run(main())
Quick Start (Rust)
[dependencies]
bacnet-client = "0.1"
bacnet-types = "0.1"
bacnet-encoding = "0.1"
tokio = { version = "1", features = ["full"] }
use bacnet_client::client::BACnetClient;
use bacnet_types::enums::{ObjectType, PropertyIdentifier};
use bacnet_types::primitives::ObjectIdentifier;
use bacnet_encoding::primitives::decode_application_value;
use std::net::Ipv4Addr;
#[tokio::main]
async fn main() -> Result<(), Box<dyn std::error::Error>> {
let client = BACnetClient::bip_builder()
.interface(Ipv4Addr::UNSPECIFIED)
.port(0xBAC0)
.broadcast_address(Ipv4Addr::BROADCAST)
.build()
.await?;
let oid = ObjectIdentifier::new(ObjectType::ANALOG_INPUT, 1)?;
let mac = &[192, 168, 1, 100, 0xBA, 0xC0]; // IP:port as bytes
let ack = client
.read_property(mac, oid, PropertyIdentifier::PRESENT_VALUE, None)
.await?;
let (value, _) = decode_application_value(&ack.property_value, 0)?;
println!("Value: {:?}", value);
Ok(())
}
Running a Server (Python)
import asyncio
from rusty_bacnet import BACnetServer, ObjectType, ObjectIdentifier, PropertyIdentifier, PropertyValue
async def main():
server = BACnetServer(device_instance=1234, device_name="My Device")
server.add_analog_input(instance=1, name="Zone Temp", units=62, present_value=72.5)
server.add_binary_value(instance=1, name="Override")
await server.start()
# Read/write local objects at runtime
value = await server.read_property(
ObjectIdentifier(ObjectType.ANALOG_INPUT, 1),
PropertyIdentifier.PRESENT_VALUE,
)
print(f"Current temp: {value.value}")
await server.write_property_local(
ObjectIdentifier(ObjectType.ANALOG_INPUT, 1),
PropertyIdentifier.PRESENT_VALUE,
PropertyValue.real(73.5),
)
await asyncio.sleep(3600)
await server.stop()
asyncio.run(main())
BACnet/SC with Hub (Python)
import asyncio
from rusty_bacnet import BACnetClient, BACnetServer, ScHub
async def main():
# Start an SC hub (TLS WebSocket relay)
hub = ScHub(
listen="127.0.0.1:0",
cert="hub-cert.pem", key="hub-key.pem",
vmac=b"\xff\x00\x00\x00\x00\x01",
)
await hub.start()
hub_url = await hub.url() # "wss://127.0.0.1:<port>"
# Start a server connected to the hub
server = BACnetServer(
device_instance=1000, device_name="SC Device",
transport="sc", sc_hub=hub_url,
sc_vmac=b"\x00\x01\x02\x03\x04\x05",
sc_ca_cert="ca-cert.pem",
sc_client_cert="server-cert.pem", sc_client_key="server-key.pem",
)
server.add_analog_input(instance=1, name="Temp", units=62, present_value=72.5)
await server.start()
# Connect a client to the same hub
async with BACnetClient(
transport="sc", sc_hub=hub_url,
sc_vmac=b"\x00\x02\x03\x04\x05\x06",
sc_ca_cert="ca-cert.pem",
sc_client_cert="client-cert.pem", sc_client_key="client-key.pem",
) as client:
# Address server by its VMAC (hex-colon notation)
value = await client.read_property(
"00:01:02:03:04:05",
ObjectIdentifier(ObjectType.ANALOG_INPUT, 1),
PropertyIdentifier.PRESENT_VALUE,
)
print(f"SC read: {value.value}")
await server.stop()
await hub.stop()
asyncio.run(main())
Workspace Structure
crates/
bacnet-types/ Enums, primitives, errors
bacnet-encoding/ ASN.1 tags, APDU/NPDU codec, segmentation
bacnet-services/ 30+ services across 24 modules (RP, WP, RPM, WPM, COV, etc.)
bacnet-transport/ BIP, BIP6, BACnet/SC + Hub, MS/TP, BBMD, Ethernet
bacnet-network/ Network layer routing, router tables
bacnet-client/ Async client with TSM, segmentation, discovery
bacnet-objects/ BACnetObject trait, ObjectDatabase, 62 object types
bacnet-server/ Async server (RP/WP/RPM/WPM/COV/Events/DCC)
rusty-bacnet/ Python bindings via PyO3 (client, server, hub)
benchmarks/ Criterion benchmarks (9 suites) + Python mixed-mode
examples/ Rust, Python, and Docker examples
docs/ API documentation
Supported Services
| Service | Client | Server |
|---|---|---|
| ReadProperty | ✓ | ✓ |
| WriteProperty | ✓ | ✓ |
| ReadPropertyMultiple | ✓ | ✓ |
| WritePropertyMultiple | ✓ | ✓ |
| SubscribeCOV / UnsubscribeCOV | ✓ | ✓ |
| SubscribeCOVProperty | ✓ | ✓ |
| SubscribeCOVPropertyMultiple | ✓ | — |
| COV Notifications (confirmed + unconfirmed) | ✓ | ✓ |
| WhoIs / IAm | ✓ | ✓ |
| WhoHas / IHave | ✓ | ✓ |
| WhoAmI | ✓ | — |
| CreateObject | ✓ | ✓ |
| DeleteObject | ✓ | ✓ |
| DeviceCommunicationControl | ✓ | ✓ |
| ReinitializeDevice | ✓ | ✓ |
| AcknowledgeAlarm | ✓ | — |
| GetAlarmSummary | ✓ | — |
| GetEnrollmentSummary | ✓ | — |
| GetEventInformation | ✓ | ✓ |
| LifeSafetyOperation | ✓ | — |
| ReadRange | ✓ | — |
| AtomicReadFile / AtomicWriteFile | ✓ | — |
| AddListElement / RemoveListElement | ✓ | — |
| ConfirmedPrivateTransfer / UnconfirmedPrivateTransfer | ✓ | — |
| ConfirmedTextMessage / UnconfirmedTextMessage | ✓ | — |
| WriteGroup | ✓ | — |
| VTOpen / VTClose / VTData | ✓ | — |
| AuditNotification (confirmed + unconfirmed) | ✓ | — |
| AuditLogQuery | ✓ | — |
| TimeSynchronization | — | ✓ |
Transports
| Transport | Platforms | Feature Flag |
|---|---|---|
| BACnet/IP (UDP/IPv4) | All | default |
| BACnet/IPv6 (UDP multicast) | All | ipv6 |
| BACnet/SC (WebSocket + TLS) | All | sc-tls |
| BACnet/SC Hub (TLS relay) | All | sc-tls |
| MS/TP (serial token-passing) | Linux | serial |
| Ethernet (802.3 via BPF) | Linux | ethernet |
Python Bindings
The rusty-bacnet crate provides full Python API parity:
- 11 enum types with named constants:
ObjectType,PropertyIdentifier,ErrorClass,ErrorCode,EnableDisable,ReinitializedState,Segmentation,LifeSafetyOperation,EventState,EventType,MessagePriority - 42 client methods covering all services above (plus context manager and lifecycle)
- 6 server runtime methods:
start,stop,local_address,read_property,write_property_local,comm_state - 61 server object types via
add_*methods - SC hub management:
ScHubclass for running a BACnet/SC hub - COV async iterator:
async for notif in client.cov_notifications() - Typed exceptions:
BacnetError,BacnetProtocolError,BacnetTimeoutError,BacnetRejectError,BacnetAbortError
Development
# Run tests (1682 tests)
cargo test --workspace --exclude rusty-bacnet
# Check formatting
cargo fmt --all --check
# Lint (0 warnings required)
RUSTFLAGS="-Dwarnings" cargo clippy --workspace --exclude rusty-bacnet --all-targets
# Check Python bindings compile
cargo check -p rusty-bacnet --tests
# License/advisory checks
cargo deny check
Minimum Rust version: 1.93
Documentation
License
MIT
Dependencies
~5–7.5MB
~66K SLoC