Skip to main content

bacnet_ops/engine/
discover.rs

1//! Discovery through the engine: Who-Is under the rate cap, and the
2//! client's discovery table.
3
4use bacnet_client::client::{DeviceEvent, DiscoveredRouter};
5use bacnet_client::discovery::DiscoveredDevice;
6use bacnet_services::who_is::{DeviceInstanceRange, WhoIsRequest};
7use bacnet_transport::port::TransportPort;
8use bacnet_types::enums::UnconfirmedServiceChoice;
9use bacnet_types::error::Error;
10use bytes::BytesMut;
11
12use super::Engine;
13use crate::target::{self, AddressStyle};
14
15/// Why a directed Who-Is refuses a device instance or a routed target.
16const NOT_A_STATION: &str =
17    "a directed Who-Is needs a station address, not a device instance or routed target";
18
19/// Where a Who-Is goes.
20#[derive(Debug, Clone, Copy, PartialEq, Eq)]
21pub enum WhoIsScope<'a> {
22    /// Broadcast to this network alone.
23    Local,
24    /// A broadcast on one remote network, through its router.
25    Network(u16),
26    /// One station, by its address in the target grammar
27    /// ([`target::address_mac`]).
28    Directed(&'a str),
29    /// A global broadcast, which every router passes on.
30    Global,
31}
32
33impl<T: TransportPort + 'static> Engine<T> {
34    /// Send one Who-Is to `scope`, for the devices in `range` (all for
35    /// `None`), under the rate cap. The answers land in the discovery table
36    /// ([`devices`](Self::devices)) as they arrive.
37    pub async fn who_is(
38        &self,
39        scope: WhoIsScope<'_>,
40        range: Option<DeviceInstanceRange>,
41    ) -> Result<(), Error> {
42        let mac = match scope {
43            WhoIsScope::Directed(address) => Some(
44                target::address_mac(self.client.transport(), address, NOT_A_STATION)
45                    .await
46                    .map_err(Error::Encoding)?,
47            ),
48            _ => None,
49        };
50        // The network the Who-Is goes out on takes its slot, so a slow one
51        // is paced; a global one reaches every network and takes the
52        // global slot alone.
53        let network = match scope {
54            WhoIsScope::Local | WhoIsScope::Directed(_) => Some(0),
55            WhoIsScope::Network(network) => Some(network),
56            WhoIsScope::Global => None,
57        };
58        self.limits.slot(network).await;
59        let sent = match (scope, mac) {
60            (WhoIsScope::Local, _) => {
61                let mut buf = BytesMut::new();
62                WhoIsRequest { range }.encode(&mut buf);
63                self.client
64                    .broadcast_unconfirmed(UnconfirmedServiceChoice::WHO_IS, &buf)
65                    .await
66            }
67            (WhoIsScope::Network(network), _) => self.client.who_is_network(network, range).await,
68            (WhoIsScope::Directed(_), Some(mac)) => self.client.who_is_directed(&mac, range).await,
69            (WhoIsScope::Directed(_), None) => unreachable!("a directed Who-Is has its address"),
70            (WhoIsScope::Global, _) => self.client.who_is(range).await,
71        };
72        // An unconfirmed send that fails never left: the caller isn't
73        // charged for it.
74        if sent.is_err() {
75            crate::limits::refund();
76        }
77        sent
78    }
79
80    /// Broadcast one Who-Is-Router-To-Network on this network, for
81    /// `network` or every network (`None`), under the rate cap, then
82    /// collect the I-Am-Router-To-Network answers heard for `wait`, or
83    /// until `deadline` if that comes first (counted after the wait for
84    /// the rate slot): one entry per router, with the networks it reaches.
85    /// No answer is an empty list.
86    pub async fn discover_routers(
87        &self,
88        network: Option<u16>,
89        wait: std::time::Duration,
90        deadline: Option<tokio::time::Instant>,
91    ) -> Result<Vec<DiscoveredRouter>, Error> {
92        self.limits.slot(Some(0)).await;
93        let wait = match deadline {
94            Some(deadline) => {
95                wait.min(deadline.saturating_duration_since(tokio::time::Instant::now()))
96            }
97            None => wait,
98        };
99        let found = self.client.discover_routers(network, wait).await;
100
101        // A refused send never left, so it isn't charged.
102        if found.is_err() {
103            crate::limits::refund();
104        }
105        found
106    }
107
108    /// The link MAC of a station address in the target grammar, checked
109    /// without sending anything: a device instance or routed target is no
110    /// station address.
111    pub async fn station_mac(&self, address: &str) -> Result<Vec<u8>, Error> {
112        target::address_mac(self.client.transport(), address, NOT_A_STATION)
113            .await
114            .map_err(Error::Encoding)
115    }
116
117    /// Every device in the client's discovery table.
118    pub async fn devices(&self) -> Vec<DiscoveredDevice> {
119        self.client.discovered_devices().await
120    }
121
122    /// One device from the discovery table.
123    pub async fn device(&self, instance: u32) -> Option<DiscoveredDevice> {
124        self.client.get_device(instance).await
125    }
126
127    /// A receiver of the discovery table's changes: devices found,
128    /// refreshed and lost. Lossy under load, like every client channel; the
129    /// table ([`devices`](Self::devices)) stays the truth.
130    pub fn device_events(&self) -> tokio::sync::broadcast::Receiver<DeviceEvent> {
131        self.client.device_events()
132    }
133
134    /// How this client's link writes addresses.
135    pub fn address_style(&self) -> AddressStyle {
136        AddressStyle::of(self.client.transport())
137    }
138}