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