bacnet-endpoint
Compose client and server roles for one BACnet device under one transport
owner in the Rusty BACnet workspace.
One [EndpointSession] owns the transport, ingress and shared outbound
coordinator. Construct sessions directly with a transport or use
[bip::BipEndpointBuilder], [sc::ScEndpointBuilder] or
[mstp::MstpEndpointBuilder].
This hosted crate requires std, Tokio and allocating state. It has no
alloc-only or no-heap profile. Its one optional feature, sc-tls, enables
composition over the constrained-TLS SC hub connection. The generic [sc]
builder module is available without that feature. The lifecycle, service and
data-link limits below apply to every entry point.
Quickstart from development source
Place an application directory beside a checkout named rusty-bacnet and add
this dependency block. It describes development source, not a published crate
version; adjust the paths for another layout. The program declares all its
direct dependencies, including Tokio's current-thread runtime, macros and time
support.
[]
= { = "../rusty-bacnet/crates/bacnet-endpoint" }
= { = "../rusty-bacnet/crates/bacnet-objects" }
= { = "../rusty-bacnet/crates/bacnet-transport" }
= { = "../rusty-bacnet/crates/bacnet-types" }
= { = "1", = ["macros", "rt", "time"] }
Run a client-only session and a server-only session over paired in-process loopback channels. The server holds an Analog Input with Present_Value 72.5. The client reads it under a two-second timeout and checks the returned object, property, array index and literal application-tagged REAL bytes. Both sessions are stopped explicitly; no socket, retry or sleep is needed.
use ;
use AnalogInputObject;
use ObjectDatabase;
use LoopbackTransport;
use ;
use ObjectIdentifier;
use ;
async
Loopback delivers to its paired peer. This example establishes an in-process ReadProperty exchange and lifecycle cleanup; it does not establish destination filtering, UDP, TLS or serial operation, real-time behavior, firmware behavior, physical-device interoperability or BACnet conformance.
Features and documentation
| Feature selection | Availability |
|---|---|
Default or --no-default-features |
Hosted endpoint sessions and the B/IP, generic SC and MS/TP builders; std, Tokio and allocation remain required. |
sc-tls |
Adds ScEndpointBuilder::build_hub_session over the constrained-TLS SC connection; it does not enable full standalone-server dispatch parity. |
The docs.rs profile selects sc-tls with default features disabled for
x86_64-unknown-linux-gnu. The hub-session method is labeled with its feature
requirement; the generic SC module remains available without it. This README
is also the crate landing page, so the primary program runs as a doctest.
Optional B/IP socket example
Using the same dependencies, this separate example binds a real IPv4 UDP socket on an ephemeral port and broadcasts I-Am. It is compile-checked, not run as a doctest; choose interface and broadcast addresses appropriate to your network before running it. It does not extend the loopback quickstart's qualification.
use std::net::Ipv4Addr;
use bacnet_endpoint::bip::BipEndpointBuilder;
use bacnet_endpoint::identity::DeviceIdentity;
use bacnet_endpoint::session::SessionRole;
#[tokio::main(flavor = "current_thread")]
async fn main() -> Result<(), bacnet_types::error::Error> {
let identity = DeviceIdentity::new(1001, 42)?
.with_bip_port(1, 0, Ipv4Addr::LOCALHOST, 0)?;
let db = identity.build_database()?;
let mut session = BipEndpointBuilder::new(Ipv4Addr::LOCALHOST, 0, Ipv4Addr::BROADCAST)
.role(SessionRole::Both)
.database(db)
.device(identity)
.build_session()?;
session.start().await?;
session.broadcast_i_am().await?;
session.stop().await?;
Ok(())
}
What is proven
-
The client role initiates unsegmented ReadProperty, ReadRange and bounded explicit ReadPropertyMultiple (1–64 concrete-object references), plus [
ClientRoleHandle::write_property] to direct B/IP IPv4 unicast peers. WriteProperty requires an explicit commandability classification and a valid encoded value that fits the unsegmented request. Direct B/IP source reporting emits one value-free READ record per eligible RP/RR operation or RPM occurrence, sharing request cancellation/retry and notification ownership. Source filters may suppress records. -
One socket / one serial owner per session (RB-16 B/IP + SC-hub proofs, RB-17 MS/TP simulator proof). No second hidden socket or serial owner is created by the builders or the session.
-
I-Am identical to Device ReadProperty for the composed [
identity] on real B/IP loopback UDP and on the constrained-TLS SC hub (I-Am readback matrix inrb16_*_prooftests). -
Loopback-only coverage for port/UUID/capability corners beyond that matrix: [
DeviceIdentity] Network-Port population, SC UUID sync intoDEVICE_UUID, and service-profile alignment are exercised onLoopbackTransport/LoopbackWebSocketfor determinism, not as on-wire claims. -
Narrow server scope: the endpoint server role executes
ReadProperty(+Reject/Abort+ segmentation-Abort). ExplicitEndpointSession::with_device_writesenables authorized writes to the one local Device's Description and its installed source Audit recipient, and explicitEndpointSession::with_reinitializeenables ReinitializeDevice through an application handler, each with deterministic and real B/IP loopback tests. Fullbacnet-serverdispatch parity is out of scope. Accepted-direct replies preserve the original sealed response capability through the bounded endpoint queue, before any prompt reply channel. Retired/invalid authority never falls back by address; committed writes retain their admitting authorization. Saved direct link limits constrain response size; this responder uses its existing Abort when a ComplexACK cannot fit and does not add response segmentation.
Data-link support matrix (explicit, not silent)
| Data link | Endpoint builder | Evidence |
|---|---|---|
| B/IP IPv4 unicast + local broadcast | [bip::BipEndpointBuilder] |
Real loopback UDP proof (RB-16) |
| B/IP BBMD / foreign-device mode | [bip::BipEndpointBuilder::mode] |
The standalone builders' transport and modes; loopback Network Number wire coverage; [session::EndpointSession::foreign_registration] reports a registration; live BVLC queries stay on BipTransport |
| BACnet/SC via hub | [sc::ScEndpointBuilder] |
Real constrained-TLS hub proof (RB-16, sc-tls); loopback composition for unit validation |
| MS/TP | [mstp::MstpEndpointBuilder] |
Simulator evidence only (LoopbackSerial ownership + frame sequencing); no bench or on-wire conformance; timing is RB-26 |
| BIPv6 | None: compose EndpointSession::new over a Bip6Transport |
Real loopback UDP: a registered Network Port in NORMAL and FOREIGN mode (#1940) |
| Ethernet | None | Explicitly unsupported: no endpoint builder; use bacnet-transport directly |
Do not expect identical administration across data links: BBMD, SC hub dial, and MS/TP station/timing knobs stay transport-specific by design.
Ownership and lifecycle
- [
session::EndpointSession] is the sole lifecycle owner: start-once/completed-stop-once; canceled stop can resume joining.Dropaborts owned tasks and retains Audit membership through their quiescence. Role handles expose no lifecycle methods. - Role handles ([
ClientRoleHandle], [ServerRoleHandle]) hold only aWeaksession token plus role state. Dropping or stopping the session ends role work even when a handle was cloned out beforehand; cloned handles survive the drop as values but fail closed. start(&mut self)/stop(&mut self)take&mutso only the owner can drive lifecycle;&selfborrows (client,server, counters,broadcast_i_am) stay usable while running. Cancellation is await-boundary only: aborting a pending read future releases its exact coordinator lease via RAII for ordinary reads. Admitted audited reads are session-owned through their terminal outcome.stop()seals admission, cancels waiters, and joins dispatch exactly once.- All fallible boundaries return typed [
bacnet_types::error::Error]; queue-capacity, BBMD-ordering, SC identity/heartbeat, and MS/TP addressing misconfiguration fail fast at build time.
// Role handles expose no lifecycle: `stop` exists only on `EndpointSession`.
# use bacnet_endpoint::roles::ClientRoleHandle;
# fn forbidden(handle: ClientRoleHandle) {
handle.stop();
# }
// `close_for_owner` is owner-only (`pub(crate)`): external callers cannot
// drive role shutdown directly.
# use bacnet_endpoint::roles::ServerRoleHandle;
# fn forbidden(handle: ServerRoleHandle) {
handle.close_for_owner();
# }
Trust: verified-origin getters
Provenance stays on [bacnet_transport::port::TransportProvenance];
the endpoint preserves it structurally. Configured authorizers decide whether
that provenance permits an operation. Read it with the existing getters:
is_unverified()— legacy origin, no assertion (B/IP, MS/TP, loopback).is_direct_peer()— accepted direct SC-TLS ingress.direct_sc_identity()— its sealed verified leaf/incarnation snapshot; claimed VMAC and routed addresses remain separate claims.is_hub_channel()— scope-only channel used by Hub admission callbacks.is_relayed_origin()— hub-validated relayed origin (post source-admission; the hub peer is never substituted for the leaf).is_verified()— any verified scope; it need not carry a direct identity.
Only trusted SC validation code constructs verified values; every other transport (including caller-supplied doubles) reports unverified.
Choosing standalone or shared client/server roles
Standalone BACnetClient and BACnetServer remain public APIs with their own
service and data-link capabilities; they are not deprecated. Use the endpoint
builders when both roles need one transport and lifecycle owner, accounting
for the narrower endpoint responder scope above.
| Standalone construction | Shared endpoint composition |
|---|---|
BACnetClient::bip_builder()...build().await |
BipEndpointBuilder::new(iface, port, bcast).role(ClientOnly).build_session()? then start() |
BACnetServer::bip_builder()...build().await |
BipEndpointBuilder::new(iface, port, bcast).role(ServerOnly).database(db).device(id).build_session()? then start() |
| Client + server on one device | BipEndpointBuilder::...role(Both).database(db).device(id).build_session()? (or the SC / MS/TP builder) |
BACnetClient::sc_builder()...build().await |
ScEndpointBuilder::new(vmac, uuid)...build_hub_session(ws)? (sc-tls; dial first, then compose) |
BACnetServer::sc_builder()...build().await |
Same ScEndpointBuilder with ServerOnly + database + device |
generic_builder().transport(mstp)... |
MstpEndpointBuilder::new(serial, station)...build_session()? (one serial owner, simulator evidence) |
Standalone BBMD helpers (read_bdt / write_bdt / read_fdt / foreign
registration) stay on BipTransport;
[bip::BipEndpointBuilder::mode] sets the endpoint's B/IP mode before start.
B/IPv6 has no endpoint builder: compose the session over a Bip6Transport
and register its port with
with_registered_network_port.
Ethernet has no endpoint builder: keep the standalone path there.
See docs/rust-api.md for the same table with service-scope notes.
Module map
- [
session] — [EndpointSession], [SessionRole], [SessionConfig], [PolicyCountersSnapshot], [SessionExit]. - [
roles] — [ClientRoleHandle] (read_property,read_property_multipleandread_range, each with a_with_destinationform addressed by [EndpointApduDestination], plusread_log_pageandwrite_property) + [ServerRoleHandle] (inbound / liveness / suspend / notification admit-complete). - [
identity] — [DeviceIdentity], [NetworkPortEntry], [build_database_with_extra]. - [
bip], [sc], [mstp] — the three endpoint builders.
Hidden by design: SessionToken, single-admit/decode helpers,
complete_pre_admitted, close_for_owner, and all __endpoint_* hooks.