Skip to main content

bacnet_rs/client/
mod.rs

1//! BACnet Client Utilities
2//!
3//! This module provides high-level client utilities for common BACnet operations
4//! such as device discovery, object enumeration, and property reading.
5//!
6//! The entry point is [`BacnetClient`]. Construct one with [`BacnetClient::new`]
7//! for defaults, or with [`BacnetClient::builder`] to customize the local
8//! interface, port, timeout, and retries. All methods return [`ClientError`] on
9//! failure.
10
11mod config;
12mod error;
13mod transaction;
14
15pub use config::{ClientBuilder, ClientConfig, DEFAULT_HOST, DEFAULT_TIMEOUT};
16pub use error::ClientError;
17
18use transaction::InvokeIdAllocator;
19
20#[cfg(feature = "std")]
21use std::net::{SocketAddr, ToSocketAddrs, UdpSocket};
22#[cfg(feature = "std")]
23use std::time::{Duration, Instant};
24
25#[cfg(not(feature = "std"))]
26use alloc::{collections::BTreeMap as HashMap, string::String, vec::Vec};
27
28use crate::{
29    app::{Apdu, MaxApduSize, MaxSegments},
30    datalink::bip::BACNET_IP_PORT,
31    encoding::decode_object_identifier,
32    network::Npdu,
33    object::{EngineeringUnits, ObjectIdentifier, ObjectType, PropertyIdentifier, Segmentation},
34    property::{encode_property_value, PropertyValue},
35    service::{
36        AbortReason, ConfirmedServiceChoice, IAmRequest, PropertyReference, PropertyResultValue,
37        ReadAccessResult, ReadAccessSpecification, ReadPropertyMultipleRequest,
38        ReadPropertyMultipleResponse, ReadPropertyRequest, ReadPropertyResponse,
39        UnconfirmedServiceChoice, WhoIsRequest, WritePropertyRequest,
40    },
41};
42
43/// BVLC function code: Original-Unicast-NPDU.
44const BVLC_ORIGINAL_UNICAST: u8 = 0x0A;
45/// BVLC function code: Original-Broadcast-NPDU (local subnet broadcast).
46const BVLC_ORIGINAL_BROADCAST: u8 = 0x0B;
47
48/// High-level BACnet client for device communication
49#[cfg(feature = "std")]
50pub struct BacnetClient {
51    socket: UdpSocket,
52    timeout: Duration,
53    /// Number of retries after an initial timeout (reserved for future use by
54    /// the request paths; not yet applied by the existing methods).
55    #[allow(dead_code)]
56    retries: u8,
57    /// Allocates invoke IDs for confirmed-request transactions.
58    invoke_ids: InvokeIdAllocator,
59}
60
61/// Discovered BACnet device information
62#[derive(Debug, Clone)]
63pub struct DeviceInfo {
64    pub device_id: u32,
65    pub address: SocketAddr,
66    pub vendor_id: u16,
67    pub vendor_name: String,
68    pub max_apdu: u32,
69    pub segmentation: Segmentation,
70}
71
72/// Object information with common properties
73#[derive(Debug, Clone)]
74pub struct ObjectInfo {
75    pub object_identifier: ObjectIdentifier,
76    pub object_name: Option<String>,
77    pub description: Option<String>,
78    pub present_value: Option<PropertyValue>,
79    pub units: Option<EngineeringUnits>,
80    pub status_flags: Option<Vec<bool>>,
81}
82
83/// Result of a verified write (see [`BacnetClient::write_property_verified`]).
84///
85/// A BACnet `SimpleAck` only confirms the device *accepted* the WriteProperty
86/// request — not that the value became effective. For commandable objects the
87/// `Present_Value` is resolved from the priority array, so an accepted write can
88/// still be overridden by a higher-priority slot (or the property may not be
89/// commandable at the chosen priority). This type makes that difference visible.
90#[derive(Debug, Clone, PartialEq)]
91pub enum WriteOutcome {
92    /// The device acknowledged the write and a read-back confirms the value.
93    Verified,
94    /// The device acknowledged the write, but reading the property back shows a
95    /// different value — the write did not take effect.
96    NotEffective {
97        /// The value the property actually holds after the write.
98        read_back: PropertyValue,
99    },
100}
101
102#[cfg(feature = "std")]
103impl BacnetClient {
104    /// Create a new BACnet client with default configuration.
105    ///
106    /// Binds to all interfaces on an OS-assigned ephemeral port with a 5 second
107    /// timeout. Use [`BacnetClient::builder`] to customize.
108    pub fn new() -> Result<Self, ClientError> {
109        Self::from_config(ClientConfig::default())
110    }
111
112    /// Create a new BACnet client with a specific local socket address (any
113    /// type implementing [`ToSocketAddrs`]).
114    pub fn new_with_local_addr<A: ToSocketAddrs>(addr: A) -> Result<Self, ClientError> {
115        let socket = UdpSocket::bind(addr)?;
116        socket.set_read_timeout(Some(DEFAULT_TIMEOUT))?;
117
118        Ok(Self {
119            socket,
120            timeout: DEFAULT_TIMEOUT,
121            retries: 0,
122            invoke_ids: InvokeIdAllocator::new(),
123        })
124    }
125
126    /// Begin building a client with custom configuration.
127    pub fn builder() -> ClientBuilder {
128        ClientBuilder::new()
129    }
130
131    /// The per-request timeout this client is configured with.
132    pub fn timeout(&self) -> Duration {
133        self.timeout
134    }
135
136    /// The local socket address the client is bound to.
137    pub fn local_addr(&self) -> Result<SocketAddr, ClientError> {
138        Ok(self.socket.local_addr()?)
139    }
140
141    /// Construct a client from a fully-specified [`ClientConfig`], binding the
142    /// UDP socket.
143    pub(crate) fn from_config(config: ClientConfig) -> Result<Self, ClientError> {
144        let socket = UdpSocket::bind(config.bind_addr())?;
145        socket.set_read_timeout(Some(config.timeout))?;
146
147        Ok(Self {
148            socket,
149            timeout: config.timeout,
150            retries: config.retries,
151            invoke_ids: InvokeIdAllocator::new(),
152        })
153    }
154
155    /// Discover a device by IP address
156    pub fn discover_device(&self, target_addr: SocketAddr) -> Result<DeviceInfo, ClientError> {
157        // Send Who-Is request
158        let whois = WhoIsRequest::new();
159        let mut buffer = Vec::new();
160        whois.encode(&mut buffer)?;
161
162        // Create and send message
163        let message =
164            self.create_unconfirmed_message(UnconfirmedServiceChoice::WhoIs as u8, &buffer);
165        self.socket.send_to(&message, target_addr)?;
166
167        // Wait for I-Am response
168        let mut recv_buffer = [0u8; 1500];
169        let start_time = Instant::now();
170
171        while start_time.elapsed() < self.timeout {
172            match self.socket.recv_from(&mut recv_buffer) {
173                Ok((len, source)) => {
174                    if source == target_addr {
175                        if let Some(device_info) =
176                            self.parse_iam_response(&recv_buffer[..len], source)
177                        {
178                            return Ok(device_info);
179                        }
180                    }
181                }
182                // A per-recv socket timeout is WouldBlock on Unix and TimedOut
183                // on Windows; both mean "nothing yet", so keep waiting until our
184                // own deadline elapses.
185                Err(e)
186                    if matches!(
187                        e.kind(),
188                        std::io::ErrorKind::WouldBlock | std::io::ErrorKind::TimedOut
189                    ) =>
190                {
191                    continue
192                }
193                Err(e) => return Err(e.into()),
194            }
195        }
196
197        Err(ClientError::Timeout)
198    }
199
200    /// Broadcast a Who-Is on the local subnet and collect every device that
201    /// answers with an I-Am, until the configured timeout elapses.
202    ///
203    /// `low_limit` and `high_limit` bound the device-instance range; pass both
204    /// to target a range, or `None`/`None` to ask every device. Results are
205    /// de-duplicated by device id.
206    ///
207    /// Unlike [`discover_device`](Self::discover_device) (which unicasts to a
208    /// single known address), this reaches all devices on the local network.
209    pub fn who_is(
210        &self,
211        low_limit: Option<u32>,
212        high_limit: Option<u32>,
213    ) -> Result<Vec<DeviceInfo>, ClientError> {
214        let broadcast = SocketAddr::from(([255, 255, 255, 255], BACNET_IP_PORT));
215        self.who_is_to(broadcast, low_limit, high_limit)
216    }
217
218    /// Send a Who-Is to a specific address (broadcast or unicast) and collect
219    /// all I-Am replies until the timeout elapses.
220    ///
221    /// This is the explicit-target form of [`who_is`](Self::who_is); use it for
222    /// subnet-directed broadcasts (e.g. `192.168.1.255:47808`) or to query a
223    /// BBMD/foreign-device peer directly.
224    pub fn who_is_to(
225        &self,
226        target_addr: SocketAddr,
227        low_limit: Option<u32>,
228        high_limit: Option<u32>,
229    ) -> Result<Vec<DeviceInfo>, ClientError> {
230        // Enable broadcast so sends to a broadcast address are permitted.
231        self.socket.set_broadcast(true)?;
232
233        let whois = match (low_limit, high_limit) {
234            (Some(low), Some(high)) => WhoIsRequest::for_range(low, high),
235            _ => WhoIsRequest::new(),
236        };
237        let mut buffer = Vec::new();
238        whois.encode(&mut buffer)?;
239
240        let message = self.create_unconfirmed_bvlc(
241            UnconfirmedServiceChoice::WhoIs as u8,
242            &buffer,
243            BVLC_ORIGINAL_BROADCAST,
244        );
245        self.socket.send_to(&message, target_addr)?;
246
247        // Collect every distinct device that replies before the timeout.
248        let mut devices = Vec::new();
249        let mut seen = std::collections::HashSet::new();
250        let mut recv_buffer = [0u8; 1500];
251        let start_time = Instant::now();
252
253        while start_time.elapsed() < self.timeout {
254            match self.socket.recv_from(&mut recv_buffer) {
255                Ok((len, source)) => {
256                    if let Some(info) = self.parse_iam_response(&recv_buffer[..len], source) {
257                        if seen.insert(info.device_id) {
258                            devices.push(info);
259                        }
260                    }
261                }
262                // A per-recv socket timeout is WouldBlock on Unix and TimedOut
263                // on Windows; both mean "nothing yet", so keep waiting until our
264                // own deadline elapses.
265                Err(e)
266                    if matches!(
267                        e.kind(),
268                        std::io::ErrorKind::WouldBlock | std::io::ErrorKind::TimedOut
269                    ) =>
270                {
271                    continue
272                }
273                Err(e) => return Err(e.into()),
274            }
275        }
276
277        Ok(devices)
278    }
279
280    /// Read the device's object list
281    pub fn read_object_list(
282        &self,
283        target_addr: SocketAddr,
284        device_id: u32,
285    ) -> Result<Vec<ObjectIdentifier>, ClientError> {
286        let device_object = ObjectIdentifier::new(ObjectType::Device, device_id);
287        let property_ref = PropertyReference::new(PropertyIdentifier::ObjectList); // Object_List property
288        let read_spec = ReadAccessSpecification::new(device_object, vec![property_ref]);
289        let rpm_request = ReadPropertyMultipleRequest::new(vec![read_spec]);
290
291        let response_data = self.send_confirmed_request(
292            target_addr,
293            ConfirmedServiceChoice::ReadPropertyMultiple,
294            &self.encode_rpm_request(&rpm_request)?,
295        )?;
296
297        // The Object_List property comes back as a list of object identifiers
298        // inside the ReadPropertyMultiple result. Prefer the structured decoder;
299        // the device object itself is dropped.
300        let mut objects = Vec::new();
301        if let Ok(response) = ReadPropertyMultipleResponse::decode(&response_data) {
302            for access in response.read_access_results {
303                for result in access.results {
304                    if let PropertyResultValue::Value(values) = result.value {
305                        for value in values {
306                            if let PropertyValue::ObjectIdentifier(oid) = value {
307                                if oid.object_type != ObjectType::Device {
308                                    objects.push(oid);
309                                }
310                            }
311                        }
312                    }
313                }
314            }
315        }
316
317        // Backstop: if the structured decode produced nothing (e.g. an encoding
318        // variant it doesn't yet fully handle), scan the raw response for object
319        // identifiers so discovery still works.
320        if objects.is_empty() {
321            objects = Self::scan_object_identifiers(&response_data);
322        }
323
324        Ok(objects)
325    }
326
327    /// Scan a raw response buffer for application-tagged object identifiers
328    /// (tag `0xC4`), skipping the device object. Used as a fallback when the
329    /// structured ReadPropertyMultiple decoder yields nothing.
330    fn scan_object_identifiers(data: &[u8]) -> Vec<ObjectIdentifier> {
331        let mut objects = Vec::new();
332        let mut pos = 0;
333
334        while pos < data.len() {
335            // 0xC4 is the application tag for a 4-byte object identifier.
336            if data[pos] == 0xC4 {
337                match decode_object_identifier(&data[pos..]) {
338                    Ok((identifier, consumed)) => {
339                        if identifier.object_type != ObjectType::Device {
340                            objects.push(identifier);
341                        }
342                        pos += consumed;
343                    }
344                    Err(_) => pos += 1,
345                }
346            } else {
347                pos += 1;
348            }
349        }
350
351        objects
352    }
353
354    /// Read properties for multiple objects
355    pub fn read_objects_properties(
356        &self,
357        target_addr: SocketAddr,
358        objects: &[ObjectIdentifier],
359    ) -> Result<Vec<ObjectInfo>, ClientError> {
360        let mut objects_info = Vec::new();
361        let batch_size = 5;
362
363        for chunk in objects.chunks(batch_size) {
364            let mut read_specs = Vec::new();
365
366            for obj in chunk {
367                let mut property_refs = Vec::new();
368
369                // Always read basic properties
370                property_refs.push(PropertyReference::new(PropertyIdentifier::ObjectName)); // Object_Name
371                property_refs.push(PropertyReference::new(PropertyIdentifier::Description)); // Description
372
373                // Add Present_Value for input/output/value objects
374                match obj.object_type {
375                    ObjectType::AnalogInput
376                    | ObjectType::AnalogOutput
377                    | ObjectType::AnalogValue
378                    | ObjectType::BinaryInput
379                    | ObjectType::BinaryOutput
380                    | ObjectType::BinaryValue
381                    | ObjectType::MultiStateInput
382                    | ObjectType::MultiStateOutput
383                    | ObjectType::MultiStateValue => {
384                        property_refs
385                            .push(PropertyReference::new(PropertyIdentifier::PresentValue)); // Present_Value
386                        property_refs.push(PropertyReference::new(PropertyIdentifier::StatusFlags));
387                        // Status_Flags
388                    }
389                    _ => {}
390                }
391
392                // Add Units for analog objects
393                match obj.object_type {
394                    ObjectType::AnalogInput
395                    | ObjectType::AnalogOutput
396                    | ObjectType::AnalogValue => {
397                        property_refs.push(PropertyReference::new(PropertyIdentifier::Units));
398                    }
399                    _ => {}
400                }
401
402                read_specs.push(ReadAccessSpecification::new(*obj, property_refs));
403            }
404
405            let rpm_request = ReadPropertyMultipleRequest::new(read_specs);
406
407            match self.send_confirmed_request(
408                target_addr,
409                ConfirmedServiceChoice::ReadPropertyMultiple,
410                &self.encode_rpm_request(&rpm_request)?,
411            ) {
412                Ok(response_data) => {
413                    match ReadPropertyMultipleResponse::decode(&response_data) {
414                        Ok(response) => {
415                            for access in response.read_access_results {
416                                objects_info.push(Self::object_info_from_access(access));
417                            }
418                        }
419                        Err(_) => {
420                            // Add objects with minimal info on parse failure
421                            for obj in chunk {
422                                objects_info.push(ObjectInfo {
423                                    object_identifier: *obj,
424                                    object_name: None,
425                                    description: None,
426                                    present_value: None,
427                                    units: None,
428                                    status_flags: None,
429                                });
430                            }
431                        }
432                    }
433                }
434                Err(_) => {
435                    // Add objects with minimal info on communication failure
436                    for obj in chunk {
437                        objects_info.push(ObjectInfo {
438                            object_identifier: *obj,
439                            object_name: None,
440                            description: None,
441                            present_value: None,
442                            units: None,
443                            status_flags: None,
444                        });
445                    }
446                }
447            }
448
449            // Small delay between requests
450            std::thread::sleep(Duration::from_millis(100));
451        }
452
453        Ok(objects_info)
454    }
455
456    /// Read a property of an object and return all decoded values.
457    ///
458    /// Most properties decode to a single value; arrays and lists (e.g.
459    /// `Object_List`, `Priority_Array`) decode to several, so the full set is
460    /// returned. Returns [`ClientError::PropertyError`] if the device reports
461    /// the property as unknown, or [`ClientError::Timeout`] if there is no
462    /// response.
463    pub fn read_property(
464        &self,
465        target_addr: SocketAddr,
466        object: ObjectIdentifier,
467        property: PropertyIdentifier,
468    ) -> Result<Vec<PropertyValue>, ClientError> {
469        let request = ReadPropertyRequest::new(object, property);
470        let mut service_data = Vec::new();
471        request.encode(&mut service_data)?;
472
473        let response_data = self.send_confirmed_request(
474            target_addr,
475            ConfirmedServiceChoice::ReadProperty,
476            &service_data,
477        )?;
478
479        Ok(ReadPropertyResponse::decode(&response_data)?.property_values)
480    }
481
482    /// Write a single property of an object.
483    ///
484    /// `priority` is the BACnet command priority (1-16) for commandable
485    /// properties such as Present_Value; pass `None` to omit it. A successful
486    /// write is acknowledged with a SimpleAck; a device-side failure surfaces as
487    /// [`ClientError::PropertyError`] / [`ClientError::Rejected`] /
488    /// [`ClientError::Abort`].
489    pub fn write_property(
490        &self,
491        target_addr: SocketAddr,
492        object: ObjectIdentifier,
493        property: PropertyIdentifier,
494        value: &PropertyValue,
495        priority: Option<u8>,
496    ) -> Result<(), ClientError> {
497        let mut encoded_value = Vec::new();
498        encode_property_value(value, &mut encoded_value)?;
499
500        let property_id: u32 = property.into();
501        let request = match priority {
502            Some(p) => WritePropertyRequest::with_priority(object, property_id, encoded_value, p),
503            None => WritePropertyRequest::new(object, property_id, encoded_value),
504        };
505
506        let mut service_data = Vec::new();
507        request.encode(&mut service_data)?;
508
509        // A successful WriteProperty is a SimpleAck (empty service data); any
510        // Error/Reject/Abort is surfaced as a typed error by the request path.
511        self.send_confirmed_request(
512            target_addr,
513            ConfirmedServiceChoice::WriteProperty,
514            &service_data,
515        )?;
516
517        Ok(())
518    }
519
520    /// Write a property and then read it back to confirm it took effect.
521    ///
522    /// This is the safe way to command a value: it returns
523    /// - `Err(..)` if the device *refused* the write (Error/Reject/Abort) or a
524    ///   transfer failed;
525    /// - `Ok(WriteOutcome::Verified)` if the read-back matches `value`;
526    /// - `Ok(WriteOutcome::NotEffective { read_back })` if the device
527    ///   acknowledged the write but the property still reports a different value
528    ///   (e.g. a higher-priority command is winning, or the property is not
529    ///   commandable at this priority).
530    ///
531    /// Floating-point values are compared with a small tolerance.
532    ///
533    /// A device commonly returns the SimpleAck *before* `Present_Value` reflects
534    /// the new command (priority-array resolution can lag), so the read-back is
535    /// polled a few times before concluding the write did not take effect.
536    pub fn write_property_verified(
537        &self,
538        target_addr: SocketAddr,
539        object: ObjectIdentifier,
540        property: PropertyIdentifier,
541        value: &PropertyValue,
542        priority: Option<u8>,
543    ) -> Result<WriteOutcome, ClientError> {
544        /// How many times to read back before concluding the write didn't take.
545        const VERIFY_ATTEMPTS: u32 = 4;
546        /// Delay between read-back attempts, giving the device time to apply the
547        /// command to `Present_Value`.
548        const VERIFY_DELAY: Duration = Duration::from_millis(150);
549
550        self.write_property(target_addr, object, property, value, priority)?;
551
552        let mut read_back = Vec::new();
553        for attempt in 0..VERIFY_ATTEMPTS {
554            if attempt > 0 {
555                std::thread::sleep(VERIFY_DELAY);
556            }
557            read_back = self.read_property(target_addr, object, property)?;
558            if read_back.iter().any(|v| values_equivalent(value, v)) {
559                return Ok(WriteOutcome::Verified);
560            }
561        }
562
563        // Not verified: report the value the property actually holds.
564        let read_back = read_back.into_iter().next().unwrap_or(PropertyValue::Null);
565        Ok(WriteOutcome::NotEffective { read_back })
566    }
567
568    /// Create an unconfirmed message
569    fn create_unconfirmed_message(&self, service_choice: u8, service_data: &[u8]) -> Vec<u8> {
570        self.create_unconfirmed_bvlc(service_choice, service_data, BVLC_ORIGINAL_UNICAST)
571    }
572
573    /// Build a BACnet/IP frame for an unconfirmed request, wrapped with the
574    /// given BVLC function (`0x0A` unicast or `0x0B` broadcast).
575    fn create_unconfirmed_bvlc(
576        &self,
577        service_choice: u8,
578        service_data: &[u8],
579        bvlc_function: u8,
580    ) -> Vec<u8> {
581        // Create NPDU
582        let mut npdu = Npdu::new();
583        npdu.control.expecting_reply = false;
584        npdu.control.priority = 0;
585        let npdu_buffer = npdu.encode();
586
587        // Create unconfirmed service request APDU
588        let mut apdu = vec![0x10]; // Unconfirmed-Request PDU type
589        apdu.push(service_choice);
590        apdu.extend_from_slice(service_data);
591
592        // Combine NPDU and APDU
593        let mut message = npdu_buffer;
594        message.extend_from_slice(&apdu);
595
596        // Wrap in BVLC header for BACnet/IP
597        let mut bvlc_message = vec![0x81, bvlc_function, 0x00, 0x00];
598        bvlc_message.extend_from_slice(&message);
599
600        // Update BVLC length
601        let total_len = bvlc_message.len() as u16;
602        bvlc_message[2] = (total_len >> 8) as u8;
603        bvlc_message[3] = (total_len & 0xFF) as u8;
604
605        bvlc_message
606    }
607
608    /// Send a confirmed request and wait for the matching response.
609    ///
610    /// A fresh invoke ID is allocated for the transaction. Returns the
611    /// ComplexAck service data on success, an empty `Vec` for a SimpleAck, or a
612    /// typed [`ClientError`] if the device responds with Error/Reject/Abort or
613    /// the request times out.
614    fn send_confirmed_request(
615        &self,
616        target_addr: SocketAddr,
617        service_choice: ConfirmedServiceChoice,
618        service_data: &[u8],
619    ) -> Result<Vec<u8>, ClientError> {
620        let invoke_id = self.invoke_ids.next_id();
621        let apdu = Apdu::ConfirmedRequest {
622            segmented: false,
623            more_follows: false,
624            segmented_response_accepted: true,
625            max_segments: MaxSegments::Unspecified,
626            max_response_size: MaxApduSize::Up1476,
627            invoke_id,
628            sequence_number: None,
629            proposed_window_size: None,
630            service_choice,
631            service_data: service_data.to_vec(),
632        };
633
634        let apdu_data = apdu.encode();
635        let mut npdu = Npdu::new();
636        npdu.control.expecting_reply = true;
637        npdu.control.priority = 0;
638        let npdu_data = npdu.encode();
639
640        let mut message = npdu_data;
641        message.extend_from_slice(&apdu_data);
642
643        let mut bvlc_message = vec![0x81, 0x0A, 0x00, 0x00];
644        bvlc_message.extend_from_slice(&message);
645
646        let total_len = bvlc_message.len() as u16;
647        bvlc_message[2] = (total_len >> 8) as u8;
648        bvlc_message[3] = (total_len & 0xFF) as u8;
649
650        self.socket.send_to(&bvlc_message, target_addr)?;
651
652        // Wait for response
653        let mut recv_buffer = [0u8; 1500];
654        let start_time = Instant::now();
655
656        while start_time.elapsed() < self.timeout {
657            match self.socket.recv_from(&mut recv_buffer) {
658                Ok((len, source)) => {
659                    if source == target_addr {
660                        // A matching Error/Reject/Abort surfaces as Err here; an
661                        // unrelated frame yields None so we keep waiting.
662                        if let Some(response_data) =
663                            self.interpret_confirmed_response(&recv_buffer[..len], invoke_id)?
664                        {
665                            return Ok(response_data);
666                        }
667                    }
668                }
669                // A per-recv socket timeout is WouldBlock on Unix and TimedOut
670                // on Windows; both mean "nothing yet", so keep waiting until our
671                // own deadline elapses.
672                Err(e)
673                    if matches!(
674                        e.kind(),
675                        std::io::ErrorKind::WouldBlock | std::io::ErrorKind::TimedOut
676                    ) =>
677                {
678                    continue
679                }
680                Err(e) => return Err(e.into()),
681            }
682        }
683
684        Err(ClientError::Timeout)
685    }
686
687    /// Parse I-Am response
688    fn parse_iam_response(&self, data: &[u8], source: SocketAddr) -> Option<DeviceInfo> {
689        // Check BVLC header
690        if data.len() < 4 || data[0] != 0x81 {
691            return None;
692        }
693
694        let bvlc_length = ((data[2] as u16) << 8) | (data[3] as u16);
695        if data.len() != bvlc_length as usize {
696            return None;
697        }
698
699        // Decode NPDU
700        let npdu_start = 4;
701        let (_npdu, npdu_len) = Npdu::decode(&data[npdu_start..]).ok()?;
702
703        // Decode APDU
704        let apdu_start = npdu_start + npdu_len;
705        let apdu = &data[apdu_start..];
706
707        if apdu.len() < 2 || apdu[0] != 0x10 || apdu[1] != UnconfirmedServiceChoice::IAm as u8 {
708            return None;
709        }
710
711        match IAmRequest::decode(&apdu[2..]) {
712            Ok(iam) => {
713                let vendor_name = crate::vendor::get_vendor_name(iam.vendor_identifier)
714                    .unwrap_or("Unknown Vendor")
715                    .to_string();
716
717                Some(DeviceInfo {
718                    device_id: iam.device_identifier.instance,
719                    address: source,
720                    vendor_id: iam.vendor_identifier,
721                    vendor_name,
722                    max_apdu: iam.max_apdu_length_accepted,
723                    segmentation: iam.segmentation_supported,
724                })
725            }
726            Err(_) => None,
727        }
728    }
729
730    /// Interpret a received datalink frame as a response to `expected_invoke_id`.
731    ///
732    /// Returns:
733    /// - `Ok(Some(data))` for the matching ComplexAck (service data) or
734    ///   SimpleAck (empty),
735    /// - `Err(..)` when the device returned a matching Error / Reject / Abort,
736    /// - `Ok(None)` when the frame is unrelated (wrong invoke ID, not a
737    ///   response, or unparseable) and the caller should keep waiting.
738    ///
739    /// `Ok(None)` (rather than an error) is deliberate: this is called from a
740    /// per-request receive loop with a single transaction in flight, so frames
741    /// that don't match are simply other traffic on the socket and must be
742    /// skipped, not treated as failures. If this ever moves behind a shared
743    /// event loop that demultiplexes all incoming messages, that loop would need
744    /// to dispatch frames to the waiting transaction by invoke ID instead of
745    /// dropping non-matching ones here.
746    fn interpret_confirmed_response(
747        &self,
748        data: &[u8],
749        expected_invoke_id: u8,
750    ) -> Result<Option<Vec<u8>>, ClientError> {
751        // Check BVLC header
752        if data.len() < 4 || data[0] != 0x81 {
753            return Ok(None);
754        }
755
756        let bvlc_length = ((data[2] as u16) << 8) | (data[3] as u16);
757        if data.len() != bvlc_length as usize {
758            return Ok(None);
759        }
760
761        // Decode NPDU and APDU; a frame we can't parse is simply not our reply.
762        let npdu_start = 4;
763        let (_npdu, npdu_len) = match Npdu::decode(&data[npdu_start..]) {
764            Ok(decoded) => decoded,
765            Err(_) => return Ok(None),
766        };
767
768        let apdu_start = npdu_start + npdu_len;
769        let apdu = match Apdu::decode(&data[apdu_start..]) {
770            Ok(apdu) => apdu,
771            Err(_) => return Ok(None),
772        };
773
774        match apdu {
775            Apdu::ComplexAck {
776                invoke_id,
777                service_data,
778                ..
779            } if invoke_id == expected_invoke_id => Ok(Some(service_data)),
780            Apdu::SimpleAck { invoke_id, .. } if invoke_id == expected_invoke_id => {
781                Ok(Some(Vec::new()))
782            }
783            Apdu::Error {
784                invoke_id,
785                error_class,
786                error_code,
787                ..
788            } if invoke_id == expected_invoke_id => Err(ClientError::PropertyError {
789                class: error_class as u32,
790                code: error_code as u32,
791            }),
792            Apdu::Reject {
793                invoke_id,
794                reject_reason,
795            } if invoke_id == expected_invoke_id => Err(ClientError::Rejected(reject_reason)),
796            Apdu::Abort {
797                invoke_id,
798                abort_reason,
799                ..
800            } if invoke_id == expected_invoke_id => {
801                Err(ClientError::Abort(AbortReason::from(abort_reason)))
802            }
803            _ => Ok(None),
804        }
805    }
806
807    /// Encode ReadPropertyMultiple request
808    fn encode_rpm_request(
809        &self,
810        request: &ReadPropertyMultipleRequest,
811    ) -> Result<Vec<u8>, ClientError> {
812        let mut buffer = Vec::new();
813
814        request.encode(&mut buffer)?;
815
816        Ok(buffer)
817    }
818
819    /// Map a decoded ReadPropertyMultiple result for a single object into the
820    /// client's [`ObjectInfo`] view, pulling out the common properties.
821    ///
822    /// Per-property errors (`PropertyResultValue::Error`) are skipped, leaving
823    /// that field `None`.
824    fn object_info_from_access(access: ReadAccessResult) -> ObjectInfo {
825        let mut info = ObjectInfo {
826            object_identifier: access.object_identifier,
827            object_name: None,
828            description: None,
829            present_value: None,
830            units: None,
831            status_flags: None,
832        };
833
834        for result in access.results {
835            let values = match result.value {
836                PropertyResultValue::Value(values) => values,
837                PropertyResultValue::Error(..) => continue,
838            };
839            let first = values.into_iter().next();
840
841            match result.property_identifier {
842                PropertyIdentifier::ObjectName => {
843                    if let Some(PropertyValue::CharacterString(s)) = first {
844                        info.object_name = Some(s);
845                    }
846                }
847                PropertyIdentifier::Description => {
848                    if let Some(PropertyValue::CharacterString(s)) = first {
849                        info.description = Some(s);
850                    }
851                }
852                PropertyIdentifier::PresentValue => {
853                    info.present_value = first;
854                }
855                PropertyIdentifier::Units => {
856                    if let Some(PropertyValue::Enumerated(units_id)) = first {
857                        info.units = Some(EngineeringUnits::from(units_id));
858                    }
859                }
860                PropertyIdentifier::StatusFlags => {
861                    if let Some(PropertyValue::BitString(bits)) = first {
862                        info.status_flags = Some(bits);
863                    }
864                }
865                _ => {}
866            }
867        }
868
869        info
870    }
871}
872
873/// Compare a written value against a read-back value when verifying a write,
874/// tolerating floating-point rounding for Real/Double (including a Real written
875/// value read back as a Double, or vice versa).
876#[cfg(feature = "std")]
877fn values_equivalent(written: &PropertyValue, read_back: &PropertyValue) -> bool {
878    const REAL_TOLERANCE: f64 = 1e-3;
879    match (written, read_back) {
880        (PropertyValue::Real(a), PropertyValue::Real(b)) => {
881            (f64::from(*a) - f64::from(*b)).abs() <= REAL_TOLERANCE
882        }
883        (PropertyValue::Double(a), PropertyValue::Double(b)) => (a - b).abs() <= REAL_TOLERANCE,
884        (PropertyValue::Real(a), PropertyValue::Double(b)) => {
885            (f64::from(*a) - b).abs() <= REAL_TOLERANCE
886        }
887        (PropertyValue::Double(a), PropertyValue::Real(b)) => {
888            (a - f64::from(*b)).abs() <= REAL_TOLERANCE
889        }
890        (a, b) => a == b,
891    }
892}
893
894#[cfg(test)]
895mod tests {
896    use super::*;
897
898    #[test]
899    fn test_object_id_encoding() {
900        let object_id = ObjectIdentifier::new(ObjectType::AnalogInput, 123);
901        let encoded: u32 = match object_id.try_into() {
902            Ok(value) => value,
903            Err(_) => panic!("Object identifier encoding failed"),
904        };
905        let decoded = ObjectIdentifier::from(encoded);
906        assert_eq!(decoded.object_type, ObjectType::AnalogInput);
907        assert_eq!(decoded.instance, 123);
908
909        let object_id = ObjectIdentifier::new(ObjectType::Device, 5047);
910        let encoded: u32 = match object_id.try_into() {
911            Ok(value) => value,
912            Err(_) => panic!("Object identifier encoding failed"),
913        };
914        let decoded = ObjectIdentifier::from(encoded);
915        assert_eq!(decoded.object_type, ObjectType::Device);
916        assert_eq!(decoded.instance, 5047);
917    }
918
919    #[test]
920    fn test_config_defaults() {
921        let config = ClientConfig::default();
922        assert_eq!(config.host, DEFAULT_HOST);
923        assert_eq!(config.port, 0);
924        assert_eq!(config.timeout, DEFAULT_TIMEOUT);
925        assert_eq!(config.retries, 0);
926        assert_eq!(config.bind_addr(), "0.0.0.0:0");
927    }
928
929    #[test]
930    fn test_builder_sets_fields() {
931        // Bind to loopback on an OS-assigned port so the test is hermetic.
932        let client = BacnetClient::builder()
933            .local_addr("127.0.0.1")
934            .port(0)
935            .timeout(Duration::from_millis(250))
936            .retries(3)
937            .build()
938            .expect("client should bind");
939
940        assert_eq!(client.timeout(), Duration::from_millis(250));
941        assert_eq!(client.retries, 3);
942
943        let local = client.local_addr().expect("local addr");
944        assert!(local.ip().is_loopback());
945        assert_ne!(local.port(), 0, "OS should assign a real port");
946    }
947
948    #[test]
949    fn test_new_uses_defaults() {
950        let client = BacnetClient::new().expect("client should bind");
951        assert_eq!(client.timeout(), DEFAULT_TIMEOUT);
952    }
953
954    #[test]
955    fn test_error_display() {
956        assert_eq!(ClientError::Timeout.to_string(), "request timed out");
957
958        // Known class + code are named, with the raw numbers retained.
959        let known = ClientError::PropertyError { class: 1, code: 31 };
960        assert_eq!(
961            known.to_string(),
962            "unknown-object (class object[1], code 31)"
963        );
964
965        // Property/write-access-denied — the case seen against real hardware.
966        let denied = ClientError::PropertyError { class: 2, code: 40 };
967        assert_eq!(
968            denied.to_string(),
969            "write-access-denied (class property[2], code 40)"
970        );
971
972        // Unknown code falls back to the numeric form.
973        let unknown = ClientError::PropertyError {
974            class: 2,
975            code: 222,
976        };
977        assert_eq!(
978            unknown.to_string(),
979            "BACnet error (class property[2], code 222)"
980        );
981    }
982}