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}