Skip to main content

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}