Expand description
§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.
[dependencies]
bacnet-endpoint = { path = "../rusty-bacnet/crates/bacnet-endpoint" }
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"] }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 bacnet_endpoint::{EndpointSession, SessionConfig, SessionRole};
use bacnet_objects::analog::AnalogInputObject;
use bacnet_objects::database::ObjectDatabase;
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 (client_port, server_port) = LoopbackTransport::pair(vec![1], vec![2]);
let mut analog = AnalogInputObject::new(7, "room-temperature", EngineeringUnits::DEGREES_FAHRENHEIT)?;
analog.set_present_value(72.5);
let mut database = ObjectDatabase::new();
database.add(Box::new(analog))?;
let config = SessionConfig {
queue_capacity: 16,
apdu_timeout_ms: 1_000,
apdu_retries: 0,
max_apdu_length: 480,
..SessionConfig::default()
};
let mut client = EndpointSession::new(client_port, SessionRole::ClientOnly, config.clone())?;
let mut server = EndpointSession::new(server_port, SessionRole::ServerOnly, config)?
.with_database(database);
client.start().await?;
server.start().await?;
let object = ObjectIdentifier::new(ObjectType::ANALOG_INPUT, 7)?;
let handle = client.client().expect("the client role is enabled");
let ack = timeout(
Duration::from_secs(2),
handle.read_property(&[2], object, PropertyIdentifier::PRESENT_VALUE, None),
)
.await??;
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, 0x91, 0x00, 0x00]);
client.stop().await?;
server.stop().await?;
Ok(())
}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_propertyto 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
identityon 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:
DeviceIdentityNetwork-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::EndpointSessionis 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`.
handle.stop();// `close_for_owner` is owner-only (`pub(crate)`): external callers cannot
// drive role shutdown directly.
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.
Re-exports§
pub use roles::ClientRoleHandle;pub use roles::ServerRoleHandle;pub use session::EndpointSession;pub use session::PolicyCountersSnapshot;pub use session::RESPONDER_OPT_IN_SERVICES;pub use session::RESPONDER_SERVICES;pub use session::ResponderOptIn;pub use session::SessionConfig;pub use session::SessionExit;pub use session::SessionRole;
Modules§
- bip
- Concrete B/IP endpoint builder (no generic-trait erasure).
- identity
- Shared Device identity and endpoint-specific configuration adapters.
- mstp
- Concrete MS/TP endpoint builder over one serial owner.
- roles
- Session-bound role handles above the sibling client/server roles.
- sc
- Concrete BACnet/SC controls at endpoint level (no generic-trait erasure).
- session
- Single-owner endpoint session: one ingress + shared coordinator.
Structs§
- Device
Identity - Single Device identity source.
- Network
Port Entry - One configured Network-Port entry (see module docs for the profile boundary).
Functions§
- build_
database_ with_ extra - Builds a database from an identity plus extra application objects.