Skip to main content

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}