1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
//! Proven single-owner BACnet endpoint API (RB-18).
//!
//! Compose client and server roles for one BACnet device under one transport
//! owner. One [`EndpointSession`] owns the transport, ingress, and shared
//! outbound coordinator. Construct sessions with the endpoint builders:
//! [`bip::BipEndpointBuilder`], [`sc::ScEndpointBuilder`],
//! [`mstp::MstpEndpointBuilder`].
//!
//! # Quick start (B/IP, one device, both roles)
//!
//! ```no_run
//! use std::net::Ipv4Addr;
//!
//! use bacnet_endpoint::bip::BipEndpointBuilder;
//! use bacnet_endpoint::identity::DeviceIdentity;
//! use bacnet_endpoint::session::SessionRole;
//!
//! # #[tokio::main]
//! # 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)
//! .identity(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). 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 in `rb16_*_proof` tests).
//! - Loopback-only coverage for port/UUID/capability corners beyond that
//! matrix: [`DeviceIdentity`] Network-Port population, SC UUID sync into
//! `DEVICE_UUID`, and service-profile alignment are exercised on
//! `LoopbackTransport` / `LoopbackWebSocket` for determinism, not as
//! on-wire claims.
//! - Narrow server scope: the endpoint server role executes `ReadProperty`
//! (+ `Reject`/`Abort` + segmentation-`Abort`). Explicit
//! [`EndpointSession::with_device_writes`](session::EndpointSession::with_device_writes)
//! enables authorized writes to the one local Device's Description and its
//! installed source Audit recipient, and explicit
//! [`EndpointSession::with_reinitialize`](session::EndpointSession::with_reinitialize)
//! enables ReinitializeDevice through an application handler, each with
//! deterministic and real B/IP loopback tests. Full `bacnet-server`
//! dispatch 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_transport::bip::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 | Explicitly unsupported: no endpoint builder; use `bacnet-transport` directly |
//! | 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. `Drop`
//! aborts owned tasks and retains Audit membership through their quiescence. Role handles
//! expose no lifecycle methods.
//! - Role handles ([`ClientRoleHandle`], [`ServerRoleHandle`]) hold only a
//! [`Weak`](std::sync::Weak) session 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 `&mut` so only the owner can
//! drive lifecycle; `&self` borrows (`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.
//!
//! ```compile_fail,E0599
//! // Role handles expose no lifecycle: `stop` exists only on `EndpointSession`.
//! # use bacnet_endpoint::roles::ClientRoleHandle;
//! # fn forbidden(handle: ClientRoleHandle) {
//! handle.stop();
//! # }
//! ```
//!
//! ```compile_fail,E0599
//! // `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).identity(id).build_session()?` then `start()` |
//! | Client + server on one device | `BipEndpointBuilder::...role(Both).database(db).identity(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` + `identity` |
//! | `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`](bacnet_transport::bip::BipTransport);
//! [`bip::BipEndpointBuilder::mode`] sets the endpoint's B/IP mode before start.
//! BIPv6/Ethernet have 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`
//! and `read_property_with_destination`, addressed by
//! [`EndpointApduDestination`]) +
//! [`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.
pub use ;
pub use ;
pub use ;