bacnet_client/client/transport_access.rs
1//! The transport of a built client, and the BBMD helpers that reach through it.
2
3use bacnet_transport::bbmd::{BdtEntry, FdtEntryWire};
4use bacnet_transport::bip::{AsBip, BvlcExchangeError, ForeignRegistrationSnapshot};
5
6use super::*;
7
8impl<T: TransportPort + 'static> BACnetClient<T> {
9 /// The transport this client was built with.
10 ///
11 /// The client keeps its transport inside the network layer that its
12 /// dispatch task shares, and lends it out only as a shared borrow.
13 /// Methods that take `&mut self`, such as `start`, `stop` and the
14 /// configuration setters, stay out of reach; [`stop`](Self::stop) stops
15 /// the transport in place.
16 ///
17 /// # Supported through this borrow
18 ///
19 /// Reading transport state and diagnostics while the client runs:
20 ///
21 /// - BACnet/SC: the `connection_state_changes()` watch, the
22 /// `connection_events()` broadcast (reconnects, failover, primary
23 /// restore, disconnect causes), the `connection_status()` snapshot
24 /// (active hub, hub VMAC, heartbeat age and round-trip time),
25 /// `hub_peer_info()` and `npdu_drop_counts()`.
26 /// - B/IP: `management_counters()`, `fdt_counters()`,
27 /// `fanout_counters()`, `receive_counters()`, `send_counters()`, and
28 /// `bbmd_state()` for a brief read of the BBMD tables.
29 /// - MS/TP: the
30 /// [`diagnostics()`](bacnet_transport::mstp::MstpTransport::diagnostics)
31 /// handle.
32 /// - B/IP, B/IPv6, BACnet/SC and MS/TP: `frame_tap()`, to subscribe to
33 /// every link-layer frame the transport sends and receives (#957).
34 /// - Any transport: `local_mac()` and the APDU size limits.
35 ///
36 /// The watch receiver, the diagnostics handle and the BBMD state `Arc` are
37 /// owned and outlive the borrow, so a long-lived UI takes them once and
38 /// moves them into its own tasks, as MS/TP callers already do with
39 /// `diagnostics()` before handing a transport over. The counters are cheap
40 /// snapshots: poll them through the borrow as often as the UI redraws.
41 ///
42 /// # Not supported while the client runs
43 ///
44 /// These go around the client, which does not guard against them:
45 ///
46 /// - Sending through the transport (`send_unicast`, `send_broadcast` and
47 /// their variants). The client's transaction state machine never sees
48 /// the frame, so a hand-built confirmed request can reuse an invoke ID
49 /// that is already in flight. Send through the client's request methods,
50 /// and use its BBMD helpers for BBMD management.
51 /// - Holding the `bbmd_state()` lock across an await or a client call.
52 /// It is a synchronous mutex the receive loop and a broadcast from the
53 /// client take too, so holding it stalls the transport or deadlocks.
54 /// Lock, copy what you need and release.
55 ///
56 /// The live MS/TP master node and the BACnet/SC connection, whose locks
57 /// the token loop and the hub link take, are not public at all.
58 ///
59 /// # Why a borrow
60 ///
61 /// This is one accessor for every transport, rather than a bundle of
62 /// handles returned by each builder. Each transport already offers its
63 /// diagnostics on `&self`, so nothing new is needed per transport, and
64 /// clients from [`generic_builder`](Self::generic_builder) get the same
65 /// access. Sharing the transport through an `Arc` was ruled out too:
66 /// [`stop`](Self::stop) needs sole ownership of the network layer to stop
67 /// the transport, which an outstanding clone would block.
68 ///
69 /// ```
70 /// use std::net::Ipv4Addr;
71 /// use bacnet_client::client::BACnetClient;
72 ///
73 /// # fn main() -> Result<(), Box<dyn std::error::Error>> {
74 /// let runtime = tokio::runtime::Builder::new_current_thread().enable_all().build()?;
75 /// runtime.block_on(Box::pin(async {
76 /// let mut client = BACnetClient::bip_builder()
77 /// .interface(Ipv4Addr::LOCALHOST)
78 /// .port(0)
79 /// .build()
80 /// .await?;
81 /// // Counter snapshots: poll them whenever the UI redraws.
82 /// let counters = client.transport().management_counters();
83 /// assert_eq!(counters.read_bdt_responses, 0);
84 /// assert_eq!(client.transport().fanout_counters().packets_forwarded, 0);
85 /// // This client is not a BBMD, so it has no BBMD state or FDT.
86 /// assert!(client.transport().bbmd_state().is_none());
87 /// assert!(client.transport().fdt_counters().is_none());
88 /// client.stop().await?;
89 /// Ok::<_, bacnet_types::error::Error>(())
90 /// }))?;
91 /// # Ok(())
92 /// # }
93 /// ```
94 pub fn transport(&self) -> &T {
95 self.network.transport()
96 }
97}
98
99/// BBMD management helpers (Annex J), for a client whose transport is B/IP.
100///
101/// They work on a client over [`BipTransport`], and on one over
102/// [`AnyTransport`] when its variant is `Bip`. Each request fails with a
103/// [`BvlcExchangeError`]: a NAK, no reply, or a local failure. On any other
104/// variant each returns
105/// [`Error::UnsupportedTransport`], as [`BvlcExchangeError::Transport`],
106/// before sending anything.
107///
108/// ```
109/// use bacnet_client::client::BACnetClient;
110/// use bacnet_transport::any::AnyTransport;
111/// use bacnet_transport::bip::BvlcExchangeError;
112/// use bacnet_transport::loopback::LoopbackTransport;
113/// use bacnet_types::data_link::DataLink;
114/// use bacnet_types::error::Error;
115///
116/// # fn main() -> Result<(), Box<dyn std::error::Error>> {
117/// let runtime = tokio::runtime::Builder::new_current_thread().enable_all().build()?;
118/// runtime.block_on(Box::pin(async {
119/// let (ours, _peer) = LoopbackTransport::pair(vec![1], vec![2]);
120/// let mut client = BACnetClient::generic_builder()
121/// .transport(AnyTransport::Loopback(ours))
122/// .build()
123/// .await?;
124/// let bbmd = [127, 0, 0, 1, 0xBA, 0xC0];
125/// assert!(matches!(
126/// client.read_bdt(&bbmd).await,
127/// Err(BvlcExchangeError::Transport(Error::UnsupportedTransport {
128/// required: DataLink::Bip,
129/// actual: DataLink::Loopback,
130/// }))
131/// ));
132/// client.stop().await?;
133/// Ok::<_, Error>(())
134/// }))?;
135/// # Ok(())
136/// # }
137/// ```
138impl<T: TransportPort + AsBip + 'static> BACnetClient<T> {
139 /// How this client's foreign-device registration is going (#938): whether
140 /// it holds a live lease, the time left on it, and the last attempt's
141 /// result and outcome, a BBMD's refusal included. Outside
142 /// [`BipMode::Foreign`] only manual registrations show. It reads
143 /// [`BipTransport::bvlc_client_snapshot`] and sends nothing.
144 pub fn foreign_registration(&self) -> Result<ForeignRegistrationSnapshot, Error> {
145 Ok(self
146 .transport()
147 .as_bip()?
148 .bvlc_client_snapshot()
149 .foreign_registration)
150 }
151
152 /// Read the Broadcast Distribution Table from a BBMD.
153 pub async fn read_bdt(&self, target: &[u8]) -> Result<Vec<BdtEntry>, BvlcExchangeError> {
154 self.transport().as_bip()?.read_bdt(target).await
155 }
156
157 /// Write the Broadcast Distribution Table to a BBMD.
158 ///
159 /// A BBMD that follows the 2020 standard answers with the not-supported
160 /// result, [`BvlcExchangeError::Nak`], and keeps its table.
161 pub async fn write_bdt(
162 &self,
163 target: &[u8],
164 entries: &[BdtEntry],
165 ) -> Result<(), BvlcExchangeError> {
166 self.transport().as_bip()?.write_bdt(target, entries).await
167 }
168
169 /// Read the Foreign Device Table from a BBMD.
170 pub async fn read_fdt(&self, target: &[u8]) -> Result<Vec<FdtEntryWire>, BvlcExchangeError> {
171 self.transport().as_bip()?.read_fdt(target).await
172 }
173
174 /// Delete a Foreign Device Table entry on a BBMD.
175 pub async fn delete_fdt_entry(
176 &self,
177 target: &[u8],
178 ip: [u8; 4],
179 port: u16,
180 ) -> Result<(), BvlcExchangeError> {
181 self.transport()
182 .as_bip()?
183 .delete_fdt_entry(target, ip, port)
184 .await
185 }
186
187 /// Register as a foreign device with a BBMD; `Ok` when it accepts,
188 /// [`BvlcExchangeError::Nak`] when it refuses.
189 ///
190 /// This sends one registration. It does not make the transport a
191 /// foreign device for broadcasts or renew the registration.
192 pub async fn register_foreign_device_bvlc(
193 &self,
194 target: &[u8],
195 ttl: u16,
196 ) -> Result<(), BvlcExchangeError> {
197 self.transport()
198 .as_bip()?
199 .register_foreign_device_bvlc(target, ttl)
200 .await
201 }
202}