Skip to main content

bacnet_client/client/
property.rs

1use std::num::NonZeroUsize;
2
3use super::*;
4
5impl<T: TransportPort + 'static> BACnetClient<T> {
6    /// Read a property from a remote device.
7    ///
8    /// The ACK must match the requested object, property and array index.
9    /// Device/Network Port wildcard requests accept only a same-type concrete
10    /// peer-reported object; malformed or mismatched ACKs return a decoding error.
11    pub async fn read_property(
12        &self,
13        destination_mac: &[u8],
14        object_identifier: bacnet_types::primitives::ObjectIdentifier,
15        property_identifier: bacnet_types::enums::PropertyIdentifier,
16        property_array_index: Option<u32>,
17    ) -> Result<bacnet_services::read_property::ReadPropertyACK, Error> {
18        use bacnet_services::read_property::ReadPropertyRequest;
19
20        let request = ReadPropertyRequest {
21            object_identifier,
22            property_identifier,
23            property_array_index,
24        };
25        let mut buf = BytesMut::new();
26        request.encode(&mut buf);
27
28        let response_data = self
29            .confirmed_request(destination_mac, ConfirmedServiceChoice::READ_PROPERTY, &buf)
30            .await?;
31
32        crate::read_property::decode_ack(&request, &response_data)
33    }
34
35    /// Read a property from a discovered device, auto-routing if needed.
36    pub async fn read_property_from_device(
37        &self,
38        device_instance: u32,
39        object_identifier: bacnet_types::primitives::ObjectIdentifier,
40        property_identifier: bacnet_types::enums::PropertyIdentifier,
41        property_array_index: Option<u32>,
42    ) -> Result<bacnet_services::read_property::ReadPropertyACK, Error> {
43        let (mac, routing) = {
44            let dt = self.device_table.lock().await;
45            let device = dt.get(device_instance).ok_or_else(|| {
46                Error::Encoding(format!("device {device_instance} not in device table"))
47            })?;
48            let routing = match (&device.source_network, &device.source_address) {
49                (Some(snet), Some(sadr)) => Some((*snet, sadr.to_vec())),
50                _ => None,
51            };
52            (device.mac_address.to_vec(), routing)
53        };
54
55        if let Some((dnet, dadr)) = routing {
56            self.read_property_routed(
57                &mac,
58                dnet,
59                &dadr,
60                object_identifier,
61                property_identifier,
62                property_array_index,
63            )
64            .await
65        } else {
66            self.read_property(
67                &mac,
68                object_identifier,
69                property_identifier,
70                property_array_index,
71            )
72            .await
73        }
74    }
75
76    /// Read a property from a device on a remote BACnet network via a router.
77    pub async fn read_property_routed(
78        &self,
79        router_mac: &[u8],
80        dest_network: u16,
81        dest_mac: &[u8],
82        object_identifier: bacnet_types::primitives::ObjectIdentifier,
83        property_identifier: bacnet_types::enums::PropertyIdentifier,
84        property_array_index: Option<u32>,
85    ) -> Result<bacnet_services::read_property::ReadPropertyACK, Error> {
86        use bacnet_services::read_property::ReadPropertyRequest;
87
88        let request = ReadPropertyRequest {
89            object_identifier,
90            property_identifier,
91            property_array_index,
92        };
93        let mut buf = BytesMut::new();
94        request.encode(&mut buf);
95
96        let response_data = self
97            .confirmed_request_routed(
98                router_mac,
99                dest_network,
100                dest_mac,
101                ConfirmedServiceChoice::READ_PROPERTY,
102                &buf,
103            )
104            .await?;
105
106        crate::read_property::decode_ack(&request, &response_data)
107    }
108
109    /// Read multiple properties from one or more objects on a remote device.
110    pub async fn read_property_multiple(
111        &self,
112        destination_mac: &[u8],
113        specs: Vec<bacnet_types::constructed::ReadAccessSpecification>,
114    ) -> Result<bacnet_services::rpm::ReadPropertyMultipleACK, Error> {
115        use bacnet_services::rpm::{ReadPropertyMultipleACK, ReadPropertyMultipleRequest};
116
117        let request = ReadPropertyMultipleRequest {
118            list_of_read_access_specs: specs,
119        };
120        let mut buf = BytesMut::new();
121        request.encode(&mut buf)?;
122
123        let response_data = self
124            .confirmed_request(
125                destination_mac,
126                ConfirmedServiceChoice::READ_PROPERTY_MULTIPLE,
127                &buf,
128            )
129            .await?;
130
131        ReadPropertyMultipleACK::decode(&response_data)
132    }
133
134    /// Read multiple properties from a discovered device, auto-routing if needed.
135    pub async fn read_property_multiple_from_device(
136        &self,
137        device_instance: u32,
138        specs: Vec<bacnet_types::constructed::ReadAccessSpecification>,
139    ) -> Result<bacnet_services::rpm::ReadPropertyMultipleACK, Error> {
140        let (mac, routing) = self.resolve_device(device_instance).await?;
141
142        if let Some((dnet, dadr)) = routing {
143            use bacnet_services::rpm::{ReadPropertyMultipleACK, ReadPropertyMultipleRequest};
144
145            let request = ReadPropertyMultipleRequest {
146                list_of_read_access_specs: specs,
147            };
148            let mut buf = BytesMut::new();
149            request.encode(&mut buf)?;
150
151            let response_data = self
152                .confirmed_request_routed(
153                    &mac,
154                    dnet,
155                    &dadr,
156                    ConfirmedServiceChoice::READ_PROPERTY_MULTIPLE,
157                    &buf,
158                )
159                .await?;
160
161            ReadPropertyMultipleACK::decode(&response_data)
162        } else {
163            self.read_property_multiple(&mac, specs).await
164        }
165    }
166
167    /// Read one property from multiple discovered devices concurrently.
168    ///
169    /// All reads are dispatched concurrently (up to `max_concurrent`,
170    /// default 32). The nonzero limit prevents a stalled batch. Results are
171    /// returned in completion order; `request_index` identifies the original input
172    /// occurrence, including duplicates. Dropping the future cancels pending requests
173    /// and returns no partial vector.
174    pub async fn read_property_from_devices(
175        &self,
176        requests: Vec<DeviceReadRequest>,
177        max_concurrent: Option<NonZeroUsize>,
178    ) -> Vec<DeviceReadResult> {
179        use futures_util::stream::{self, StreamExt};
180
181        let concurrency = max_concurrent.map_or(DEFAULT_BATCH_CONCURRENCY, NonZeroUsize::get);
182
183        stream::iter(requests.into_iter().enumerate())
184            .map(|(request_index, req)| async move {
185                let result = self
186                    .read_property_from_device(
187                        req.device_instance,
188                        req.object_identifier,
189                        req.property_identifier,
190                        req.property_array_index,
191                    )
192                    .await;
193                DeviceReadResult {
194                    request_index,
195                    device_instance: req.device_instance,
196                    result,
197                }
198            })
199            .buffer_unordered(concurrency)
200            .collect()
201            .await
202    }
203
204    /// Read multiple properties from multiple devices concurrently (RPM batch).
205    ///
206    /// Sends an RPM to each device concurrently. This is the most efficient
207    /// way to poll many properties across many devices — RPM batches within
208    /// a single device, and this method batches across devices. A nonzero limit
209    /// bounds concurrency (None uses 32). Results use completion order and carry the
210    /// original input occurrence in `request_index`. Dropping the future cancels
211    /// pending requests and returns no partial vector.
212    pub async fn read_property_multiple_from_devices(
213        &self,
214        requests: Vec<DeviceRpmRequest>,
215        max_concurrent: Option<NonZeroUsize>,
216    ) -> Vec<DeviceRpmResult> {
217        use futures_util::stream::{self, StreamExt};
218
219        let concurrency = max_concurrent.map_or(DEFAULT_BATCH_CONCURRENCY, NonZeroUsize::get);
220
221        stream::iter(requests.into_iter().enumerate())
222            .map(|(request_index, req)| async move {
223                let result = self
224                    .read_property_multiple_from_device(req.device_instance, req.specs)
225                    .await;
226                DeviceRpmResult {
227                    request_index,
228                    device_instance: req.device_instance,
229                    result,
230                }
231            })
232            .buffer_unordered(concurrency)
233            .collect()
234            .await
235    }
236
237    /// Write a property on a remote device.
238    pub async fn write_property(
239        &self,
240        destination_mac: &[u8],
241        object_identifier: bacnet_types::primitives::ObjectIdentifier,
242        property_identifier: bacnet_types::enums::PropertyIdentifier,
243        property_array_index: Option<u32>,
244        property_value: Vec<u8>,
245        priority: Option<u8>,
246    ) -> Result<(), Error> {
247        use bacnet_services::write_property::WritePropertyRequest;
248
249        let request = WritePropertyRequest {
250            object_identifier,
251            property_identifier,
252            property_array_index,
253            property_value,
254            priority,
255        };
256        let mut buf = BytesMut::new();
257        request.encode(&mut buf)?;
258
259        let _ = self
260            .confirmed_request(
261                destination_mac,
262                ConfirmedServiceChoice::WRITE_PROPERTY,
263                &buf,
264            )
265            .await?;
266
267        Ok(())
268    }
269
270    /// Write multiple properties on one or more objects on a remote device.
271    pub async fn write_property_multiple(
272        &self,
273        destination_mac: &[u8],
274        specs: Vec<bacnet_services::wpm::WriteAccessSpecification>,
275    ) -> Result<(), Error> {
276        use bacnet_services::wpm::WritePropertyMultipleRequest;
277
278        let request = WritePropertyMultipleRequest {
279            list_of_write_access_specs: specs,
280        };
281        let mut buf = BytesMut::new();
282        request.encode(&mut buf)?;
283
284        let _ = self
285            .confirmed_request(
286                destination_mac,
287                ConfirmedServiceChoice::WRITE_PROPERTY_MULTIPLE,
288                &buf,
289            )
290            .await?;
291
292        Ok(())
293    }
294
295    // -----------------------------------------------------------------------
296    // Auto-routing _from_device variants (RPM, WP, WPM)
297    // -----------------------------------------------------------------------
298
299    /// Write a property on a discovered device, auto-routing if needed.
300    pub async fn write_property_to_device(
301        &self,
302        device_instance: u32,
303        object_identifier: bacnet_types::primitives::ObjectIdentifier,
304        property_identifier: bacnet_types::enums::PropertyIdentifier,
305        property_array_index: Option<u32>,
306        property_value: Vec<u8>,
307        priority: Option<u8>,
308    ) -> Result<(), Error> {
309        bacnet_services::write_property::validate_priority(priority)?;
310        let (mac, routing) = self.resolve_device(device_instance).await?;
311
312        if let Some((dnet, dadr)) = routing {
313            use bacnet_services::write_property::WritePropertyRequest;
314
315            let request = WritePropertyRequest {
316                object_identifier,
317                property_identifier,
318                property_array_index,
319                property_value,
320                priority,
321            };
322            let mut buf = BytesMut::new();
323            request.encode(&mut buf)?;
324
325            let _ = self
326                .confirmed_request_routed(
327                    &mac,
328                    dnet,
329                    &dadr,
330                    ConfirmedServiceChoice::WRITE_PROPERTY,
331                    &buf,
332                )
333                .await?;
334            Ok(())
335        } else {
336            self.write_property(
337                &mac,
338                object_identifier,
339                property_identifier,
340                property_array_index,
341                property_value,
342                priority,
343            )
344            .await
345        }
346    }
347
348    /// Write multiple properties on a discovered device, auto-routing if needed.
349    pub async fn write_property_multiple_to_device(
350        &self,
351        device_instance: u32,
352        specs: Vec<bacnet_services::wpm::WriteAccessSpecification>,
353    ) -> Result<(), Error> {
354        use bacnet_services::wpm::WritePropertyMultipleRequest;
355        let request = WritePropertyMultipleRequest {
356            list_of_write_access_specs: specs,
357        };
358        let mut buf = BytesMut::new();
359        request.encode(&mut buf)?;
360        let (mac, routing) = self.resolve_device(device_instance).await?;
361        if let Some((dnet, dadr)) = routing {
362            self.confirmed_request_routed(
363                &mac,
364                dnet,
365                &dadr,
366                ConfirmedServiceChoice::WRITE_PROPERTY_MULTIPLE,
367                &buf,
368            )
369            .await?;
370        } else {
371            self.confirmed_request(&mac, ConfirmedServiceChoice::WRITE_PROPERTY_MULTIPLE, &buf)
372                .await?;
373        }
374        Ok(())
375    }
376
377    /// Write one property on multiple discovered devices concurrently.
378    ///
379    /// All writes are dispatched concurrently (up to `max_concurrent`,
380    /// default 32). The nonzero limit prevents a stalled batch. Results are
381    /// returned in completion order; `request_index` identifies the original input
382    /// occurrence, including duplicates. Dropping the future cancels pending requests
383    /// and returns no partial vector.
384    pub async fn write_property_to_devices(
385        &self,
386        requests: Vec<DeviceWriteRequest>,
387        max_concurrent: Option<NonZeroUsize>,
388    ) -> Vec<DeviceWriteResult> {
389        use futures_util::stream::{self, StreamExt};
390
391        let concurrency = max_concurrent.map_or(DEFAULT_BATCH_CONCURRENCY, NonZeroUsize::get);
392
393        stream::iter(requests.into_iter().enumerate())
394            .map(|(request_index, req)| async move {
395                let result = self
396                    .write_property_to_device(
397                        req.device_instance,
398                        req.object_identifier,
399                        req.property_identifier,
400                        req.property_array_index,
401                        req.property_value,
402                        req.priority,
403                    )
404                    .await;
405                DeviceWriteResult {
406                    request_index,
407                    device_instance: req.device_instance,
408                    result,
409                }
410            })
411            .buffer_unordered(concurrency)
412            .collect()
413            .await
414    }
415}
416
417#[cfg(test)]
418mod doc_tests {
419    const SOURCE: &str = include_str!("property.rs");
420
421    fn doc_block_for(fn_name: &str) -> String {
422        let needle = format!("pub async fn {fn_name}(");
423        let lines: Vec<_> = SOURCE.lines().collect();
424        let signature_index = lines
425            .iter()
426            .position(|line| line.trim_start().starts_with(&needle))
427            .unwrap_or_else(|| panic!("missing function {fn_name}"));
428        let mut docs = Vec::new();
429        let mut index = signature_index;
430        while index > 0 {
431            index -= 1;
432            let line = lines[index].trim_start();
433            if let Some(doc) = line.strip_prefix("///") {
434                docs.push(doc.trim());
435            } else {
436                break;
437            }
438        }
439        docs.reverse();
440        docs.join(" ")
441    }
442
443    #[test]
444    fn property_method_docblocks_match_their_methods() {
445        let cases = [
446            (
447                "read_property_multiple",
448                "Read multiple properties from one or more objects on a remote device.",
449                "Write",
450            ),
451            (
452                "read_property_multiple_from_device",
453                "Read multiple properties from a discovered device, auto-routing if needed.",
454                "Write",
455            ),
456            (
457                "read_property_from_devices",
458                "Read one property from multiple discovered devices concurrently.",
459                "Write",
460            ),
461            (
462                "write_property",
463                "Write a property on a remote device.",
464                "multiple devices concurrently",
465            ),
466            (
467                "write_property_multiple",
468                "Write multiple properties on one or more objects on a remote device.",
469                "Read",
470            ),
471            (
472                "write_property_to_device",
473                "Write a property on a discovered device, auto-routing if needed.",
474                "Read",
475            ),
476            (
477                "write_property_to_devices",
478                "Write one property on multiple discovered devices concurrently.",
479                "Read",
480            ),
481        ];
482
483        for (fn_name, expected, forbidden) in cases {
484            let docs = doc_block_for(fn_name);
485            assert!(
486                docs.contains(expected),
487                "{fn_name} docs did not contain expected text: {docs}"
488            );
489            assert!(
490                !docs.contains(forbidden),
491                "{fn_name} docs still contain misattributed text: {docs}"
492            );
493        }
494    }
495}