Skip to main content

bacnet_client/client/
discovery.rs

1use super::*;
2use bacnet_services::who_has::{WhoHasObject, WhoHasRequest};
3use bacnet_services::who_is::{DeviceInstanceRange, WhoIsRequest};
4
5impl<T: TransportPort + 'static> BACnetClient<T> {
6    /// Resolve a device instance to its MAC address and optional routing info.
7    ///
8    /// # Snapshot/coherence contract
9    ///
10    /// This returns an address/routing snapshot taken under one
11    /// [`DeviceTable`] lock; the lock is released before any network I/O.
12    /// The request's later capability lookup (Max APDU/segmentation) takes a
13    /// second lock, so a table update between the two reads can make the
14    /// capability decision observe a newer entry than the address snapshot.
15    /// This is an intentional non-atomic snapshot boundary, not a promise
16    /// that both reads use the same row forever; holding the lock across the
17    /// request would serialize all in-flight requests.
18    pub(super) async fn resolve_device(
19        &self,
20        device_instance: u32,
21    ) -> Result<(Vec<u8>, Option<(u16, Vec<u8>)>), Error> {
22        let dt = self.device_table.lock().await;
23        let device = dt.get(device_instance).ok_or_else(|| {
24            Error::Encoding(format!("device {device_instance} not in device table"))
25        })?;
26        let routing = match (&device.source_network, &device.source_address) {
27            (Some(snet), Some(sadr)) => Some((*snet, sadr.to_vec())),
28            _ => None,
29        };
30        Ok((device.mac_address.to_vec(), routing))
31    }
32
33    // -----------------------------------------------------------------------
34    // Discovery
35    // -----------------------------------------------------------------------
36
37    /// Send a WhoIs broadcast to discover devices.
38    ///
39    /// `range` limits the devices that answer to those whose instance lies
40    /// in it; `None` asks every device. A [`DeviceInstanceRange`] holds both
41    /// limits, low no greater than high, so the request can't carry one
42    /// alone (Clause 16.10, #1483).
43    pub async fn who_is(&self, range: Option<DeviceInstanceRange>) -> Result<(), Error> {
44        let mut buf = BytesMut::new();
45        WhoIsRequest { range }.encode(&mut buf);
46
47        self.broadcast_global_unconfirmed(UnconfirmedServiceChoice::WHO_IS, &buf)
48            .await
49    }
50
51    /// Send a directed (unicast) WhoIs to a specific device, with the same
52    /// `range` as [`Self::who_is`].
53    pub async fn who_is_directed(
54        &self,
55        destination_mac: &[u8],
56        range: Option<DeviceInstanceRange>,
57    ) -> Result<(), Error> {
58        let mut buf = BytesMut::new();
59        WhoIsRequest { range }.encode(&mut buf);
60
61        self.unconfirmed_request(destination_mac, UnconfirmedServiceChoice::WHO_IS, &buf)
62            .await
63    }
64
65    /// Send a WhoIs broadcast to a specific remote network, with the same
66    /// `range` as [`Self::who_is`].
67    pub async fn who_is_network(
68        &self,
69        dest_network: u16,
70        range: Option<DeviceInstanceRange>,
71    ) -> Result<(), Error> {
72        let mut buf = BytesMut::new();
73        WhoIsRequest { range }.encode(&mut buf);
74
75        self.broadcast_network_unconfirmed(UnconfirmedServiceChoice::WHO_IS, &buf, dest_network)
76            .await
77    }
78
79    /// Send a WhoHas as a global broadcast, as [`Self::who_is`] does, so that
80    /// devices behind routers hear it too, to find an object by identifier
81    /// or name, asking the devices in `range`, or every device for `None`
82    /// (Clause 16.9, #1483).
83    pub async fn who_has(
84        &self,
85        object: WhoHasObject,
86        range: Option<DeviceInstanceRange>,
87    ) -> Result<(), Error> {
88        let mut buf = BytesMut::new();
89        WhoHasRequest { range, object }.encode(&mut buf)?;
90
91        self.broadcast_global_unconfirmed(UnconfirmedServiceChoice::WHO_HAS, &buf)
92            .await
93    }
94
95    /// Get a snapshot of all discovered devices.
96    pub async fn discovered_devices(&self) -> Vec<DiscoveredDevice> {
97        self.device_table.lock().await.all()
98    }
99
100    /// Look up a discovered device by instance number.
101    pub async fn get_device(&self, instance: u32) -> Option<DiscoveredDevice> {
102        self.device_table.lock().await.get(instance).cloned()
103    }
104
105    /// Clear the discovered devices table.
106    pub async fn clear_devices(&self) {
107        self.device_table.lock().await.clear();
108    }
109
110    /// Manually register a local device in the device table.
111    ///
112    /// Useful for adding known devices without requiring WhoIs/IAm exchange.
113    /// The metadata defaults below (max_apdu_length 1476, segmentation NONE,
114    /// vendor_id 0) are local/manual registration defaults, **not** values
115    /// learned from an I-Am; requests to this device are bounded by them
116    /// until a real I-Am refreshes the row. The row is unambiguously local:
117    /// routed lookups by SNET/SADR will not match it.
118    pub async fn add_device(&self, instance: u32, mac: &[u8]) -> Result<(), Error> {
119        let oid = bacnet_types::primitives::ObjectIdentifier::new(
120            bacnet_types::enums::ObjectType::DEVICE,
121            instance,
122        )?;
123        let device = DiscoveredDevice {
124            object_identifier: oid,
125            mac_address: MacAddr::from_slice(mac),
126            max_apdu_length: 1476,
127            segmentation_supported: bacnet_types::enums::Segmentation::NONE,
128            max_segments_accepted: None,
129            vendor_id: 0,
130            last_seen: std::time::Instant::now(),
131            source_network: None,
132            source_address: None,
133        };
134        // Placeholder capability defaults (NONE, 1476) are not I-Am evidence;
135        // they must never drive a segmentation-capability decision (#371).
136        self.device_table.lock().await.upsert_placeholder(device);
137        Ok(())
138    }
139
140    /// Manually register a routed peer whose address and capabilities are
141    /// already known, without fabricating an I-Am exchange.
142    ///
143    /// `router_mac` is only the immediate transport next hop (Clause 6.2.2);
144    /// the peer's identity is the (`remote_network`, `remote_mac`) SNET/SADR
145    /// pair, which is what [`BACnetClient::confirmed_request_routed`] and the
146    /// device-table routed lookup match against. The supplied Max APDU
147    /// Length Accepted, segmentation capability, and optional Max Segments
148    /// Accepted are treated as advertised peer limits for request sizing.
149    ///
150    /// Vendor ID is recorded as 0 when unknown; this does not affect
151    /// routing or request-size decisions.
152    ///
153    /// Malformed routing metadata is rejected deliberately: an empty
154    /// `router_mac` or `remote_mac` cannot identify a next hop or a peer, so
155    /// such a registration returns [`Error::Encoding`] and the table is left
156    /// unchanged (an empty SADR could never satisfy a routed lookup). A
157    /// `remote_network` outside 1..=65534 or a `remote_mac` longer than
158    /// [`MAX_MAC_LEN`](bacnet_npdu::MAX_MAC_LEN) is refused the same way, since
159    /// [`BACnetClient::confirmed_request_routed`] refuses that destination.
160    pub async fn add_routed_device(&self, config: RoutedDeviceConfig) -> Result<(), Error> {
161        if config.router_mac.is_empty() {
162            return Err(Error::Encoding(
163                "router_mac must not be empty: it is the immediate transport next hop".into(),
164            ));
165        }
166        if config.remote_mac.is_empty() {
167            return Err(Error::Encoding(
168                "remote_mac must not be empty: it is half of the routed peer's identity".into(),
169            ));
170        }
171        check_routed_unicast(config.remote_network, config.remote_mac.len())?;
172        let oid = bacnet_types::primitives::ObjectIdentifier::new(
173            bacnet_types::enums::ObjectType::DEVICE,
174            config.instance,
175        )?;
176        let device = DiscoveredDevice {
177            object_identifier: oid,
178            mac_address: MacAddr::from_slice(&config.router_mac),
179            max_apdu_length: config.max_apdu_length,
180            segmentation_supported: config.segmentation_supported,
181            max_segments_accepted: config.max_segments_accepted,
182            vendor_id: 0,
183            last_seen: std::time::Instant::now(),
184            source_network: Some(config.remote_network),
185            source_address: Some(MacAddr::from_slice(&config.remote_mac)),
186        };
187        self.device_table.lock().await.upsert(device);
188        Ok(())
189    }
190}