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`](bacnet_transport::any::AnyTransport) when its variant is
103/// `Bip`. Each request fails with a [`BvlcExchangeError`]: a NAK, no reply,
104/// or a local failure. On any other 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_transport::mstp::NoSerial;
114/// use bacnet_types::data_link::DataLink;
115/// use bacnet_types::error::Error;
116///
117/// # fn main() -> Result<(), Box<dyn std::error::Error>> {
118/// let runtime = tokio::runtime::Builder::new_current_thread().enable_all().build()?;
119/// runtime.block_on(Box::pin(async {
120/// let (ours, _peer) = LoopbackTransport::pair(vec![1], vec![2]);
121/// let mut client = BACnetClient::generic_builder()
122/// .transport(AnyTransport::<NoSerial>::Loopback(ours))
123/// .build()
124/// .await?;
125/// let bbmd = [127, 0, 0, 1, 0xBA, 0xC0];
126/// assert!(matches!(
127/// client.read_bdt(&bbmd).await,
128/// Err(BvlcExchangeError::Transport(Error::UnsupportedTransport {
129/// required: DataLink::Bip,
130/// actual: DataLink::Loopback,
131/// }))
132/// ));
133/// client.stop().await?;
134/// Ok::<_, Error>(())
135/// }))?;
136/// # Ok(())
137/// # }
138/// ```
139impl<T: TransportPort + AsBip + 'static> BACnetClient<T> {
140 /// How this client's foreign-device registration is going (#938): whether
141 /// it holds a live lease, the time left on it, and the last attempt's
142 /// result and outcome, a BBMD's refusal included. Outside
143 /// [`BipMode::Foreign`] only manual registrations show. It reads
144 /// [`BipTransport::bvlc_client_snapshot`] and sends nothing.
145 pub fn foreign_registration(&self) -> Result<ForeignRegistrationSnapshot, Error> {
146 Ok(self
147 .transport()
148 .as_bip()?
149 .bvlc_client_snapshot()
150 .foreign_registration)
151 }
152
153 /// Read the Broadcast Distribution Table from a BBMD.
154 pub async fn read_bdt(&self, target: &[u8]) -> Result<Vec<BdtEntry>, BvlcExchangeError> {
155 self.transport().as_bip()?.read_bdt(target).await
156 }
157
158 /// Write the Broadcast Distribution Table to a BBMD.
159 ///
160 /// A BBMD that follows the 2020 standard answers with the not-supported
161 /// result, [`BvlcExchangeError::Nak`], and keeps its table.
162 pub async fn write_bdt(
163 &self,
164 target: &[u8],
165 entries: &[BdtEntry],
166 ) -> Result<(), BvlcExchangeError> {
167 self.transport().as_bip()?.write_bdt(target, entries).await
168 }
169
170 /// Read the Foreign Device Table from a BBMD.
171 pub async fn read_fdt(&self, target: &[u8]) -> Result<Vec<FdtEntryWire>, BvlcExchangeError> {
172 self.transport().as_bip()?.read_fdt(target).await
173 }
174
175 /// Delete a Foreign Device Table entry on a BBMD.
176 pub async fn delete_fdt_entry(
177 &self,
178 target: &[u8],
179 ip: [u8; 4],
180 port: u16,
181 ) -> Result<(), BvlcExchangeError> {
182 self.transport()
183 .as_bip()?
184 .delete_fdt_entry(target, ip, port)
185 .await
186 }
187
188 /// Register as a foreign device with a BBMD; `Ok` when it accepts,
189 /// [`BvlcExchangeError::Nak`] when it refuses.
190 ///
191 /// This sends one registration. It does not make the transport a
192 /// foreign device for broadcasts or renew the registration.
193 pub async fn register_foreign_device_bvlc(
194 &self,
195 target: &[u8],
196 ttl: u16,
197 ) -> Result<(), BvlcExchangeError> {
198 self.transport()
199 .as_bip()?
200 .register_foreign_device_bvlc(target, ttl)
201 .await
202 }
203}