bacnet-server 0.15.0

BACnet server: APDU dispatch and service handlers
Documentation

bacnet-server

Serve an object database with [server::BACnetServer] in the Rusty BACnet workspace. The server owns a transport and dispatches BACnet service requests against registered objects. [server::ServerBuilder] accepts a caller-selected transport, Device identity and database.

This hosted crate requires std, Tokio and allocating state. It has no alloc-only or no-heap profile. The standalone server's service dispatcher is separate from the narrower server role in bacnet-endpoint; their service contracts are not interchangeable.

Read a property using development source

Place an application directory beside a checkout named rusty-bacnet, then add this exact dependency block to the application's Cargo.toml. These paths refer to development source; they do not imply the shown APIs are published. Adjust paths for a different layout. Tokio uses its current-thread runtime, macros and time support; the program needs no multi-thread runtime feature.

[dependencies]
bacnet-client = { path = "../rusty-bacnet/crates/bacnet-client", default-features = false }
bacnet-server = { path = "../rusty-bacnet/crates/bacnet-server", default-features = false }
bacnet-objects = { path = "../rusty-bacnet/crates/bacnet-objects" }
bacnet-transport = { path = "../rusty-bacnet/crates/bacnet-transport" }
bacnet-types = { path = "../rusty-bacnet/crates/bacnet-types" }
tokio = { version = "1", features = ["macros", "rt", "time"] }

Save this complete program as src/main.rs. It initializes an Analog Input to 42.0 degrees Celsius, seeds a Device and its Object_List with build_database_with_extra, and starts a real client and server over bounded in-process loopback channels. The read has a two-second outer timeout, a one-second APDU timeout and no retries. Its acknowledgement is checked against independent application-tagged REAL bytes for 42.0.

use bacnet_client::client::{BACnetClient, ClientConfig};
use bacnet_objects::analog::AnalogInputObject;
use bacnet_objects::device_identity::{build_database_with_extra, DeviceIdentity};
use bacnet_server::server::BACnetServer;
use bacnet_transport::loopback::LoopbackTransport;
use bacnet_types::enums::{EngineeringUnits, ObjectType, PropertyIdentifier};
use bacnet_types::primitives::ObjectIdentifier;
use tokio::time::{timeout, Duration};

#[tokio::main(flavor = "current_thread")]
async fn main() -> Result<(), Box<dyn std::error::Error>> {
    let identity = DeviceIdentity::new(1001, 42)?;
    let mut analog = AnalogInputObject::new(
        7,
        "supply-temperature",
        EngineeringUnits::DEGREES_CELSIUS,
    )?;
    analog.set_present_value(42.0);
    let database = build_database_with_extra(&identity, vec![Box::new(analog)])?;
    assert!(database.get(&identity.device_oid()).is_some());

    let (client_port, server_port) = LoopbackTransport::pair(vec![1], vec![2]);
    let mut server = BACnetServer::generic_builder()
        .device(identity)
        .database(database)
        .transport(server_port)
        .build()
        .await?;
    let config = ClientConfig {
        apdu_timeout_ms: 1_000,
        apdu_retries: 0,
        ..ClientConfig::default()
    };
    let mut client = BACnetClient::start(config, client_port).await?;
    let object = ObjectIdentifier::new(ObjectType::ANALOG_INPUT, 7)?;
    let reply = timeout(
        Duration::from_secs(2),
        client.read_property(&[2], object, PropertyIdentifier::PRESENT_VALUE, None),
    )
    .await;

    let client_stopped = client.stop().await;
    let server_stopped = server.stop().await;
    let ack = reply??;
    client_stopped?;
    server_stopped?;
    assert_eq!(ack.object_identifier, object);
    assert_eq!(ack.property_identifier, PropertyIdentifier::PRESENT_VALUE);
    assert_eq!(ack.property_array_index, None);
    assert_eq!(ack.property_value, [0x44, 0x42, 0x28, 0x00, 0x00]);
    Ok(())
}

Both owners are explicitly stopped before the read result is propagated. The read timeout does not impose a shutdown deadline. Directly setting the analog value is initialization before server ownership; live updates need the server's local mutation APIs and their event/COV processing.

Loopback sends to its paired peer regardless of the requested destination. This program establishes an in-process request/response exchange and cleanup, not destination filtering, UDP, TLS, serial, physical equipment, timing, firmware behavior or BACnet conformance. Enabling an optional feature while running it does not exercise that feature's network transport.

Features and transport selection

Feature selection Public entry points
Default or --no-default-features BACnetServer::generic_builder and BACnetServer::bip_builder; hosted std/Tokio/allocation remain required.
sc-tls Adds ScServerBuilder and BACnetServer::sc_builder for a constrained-TLS BACnet/SC hub connection.

The server crate has no ipv6 feature or dedicated IPv6 builder. Its generic builder accepts a caller-selected transport; any transport feature selection belongs to that transport dependency. For the displayed application, --features bacnet-server/sc-tls enables the optional server API while the program still uses loopback. An actual SC connection needs validated TLS policy and a caller-provisioned nonzero device UUID. Authentication of an SC peer does not automatically grant BACnet service or mutation authority; install a ServerPolicy (see bacnet_server::policy), which sees the verified certificate principal of a direct SC peer and, with sc-tls, its verified leaf certificate (subject and Subject Alternative Names), and configure the other service and source policies separately.

The docs.rs profile selects sc-tls, disables defaults and targets x86_64-unknown-linux-gnu. The optional SC builder and factory carry feature badges; the always-present server module and generic builder remain visible. This README is the crate landing page and its program runs as a doctest. A local render does not establish a docs.rs deployment.

State, mutation and shutdown

[server::BACnetServer::database] exposes raw standalone object state. Use [server::BACnetServer::read_local] for the executor's live view, including served Device properties and live lists. Do not hold a database guard across network operations or shutdown. Use the server's local mutation APIs, such as [server::BACnetServer::set_present_value_local], to apply the documented ownership checks and event/COV processing.

Call [server::BACnetServer::stop] before dropping the server or reopening its storage. Stop seals egress, joins admitted work and stops the owned transport. Durable state may require waiting for queued saves; stalled storage can delay shutdown without a fixed deadline. Cancelling a stop waiter preserves cleanup ownership for a later stop. Keeping a database handle can outlive the server; follow the handle's documentation and [server::drop_database_off_runtime] when releasing durable object state from async code.

Select service and write policies for the application. A successful read in this example establishes neither write authorization nor support for every service/object combination. See the relevant handler, object and policy APIs for their limits.