1mod 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
43const BVLC_ORIGINAL_UNICAST: u8 = 0x0A;
45const BVLC_ORIGINAL_BROADCAST: u8 = 0x0B;
47
48#[cfg(feature = "std")]
50pub struct BacnetClient {
51 socket: UdpSocket,
52 timeout: Duration,
53 #[allow(dead_code)]
56 retries: u8,
57 invoke_ids: InvokeIdAllocator,
59}
60
61#[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#[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#[derive(Debug, Clone, PartialEq)]
91pub enum WriteOutcome {
92 Verified,
94 NotEffective {
97 read_back: PropertyValue,
99 },
100}
101
102#[cfg(feature = "std")]
103impl BacnetClient {
104 pub fn new() -> Result<Self, ClientError> {
109 Self::from_config(ClientConfig::default())
110 }
111
112 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 pub fn builder() -> ClientBuilder {
128 ClientBuilder::new()
129 }
130
131 pub fn timeout(&self) -> Duration {
133 self.timeout
134 }
135
136 pub fn local_addr(&self) -> Result<SocketAddr, ClientError> {
138 Ok(self.socket.local_addr()?)
139 }
140
141 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 pub fn discover_device(&self, target_addr: SocketAddr) -> Result<DeviceInfo, ClientError> {
157 let whois = WhoIsRequest::new();
159 let mut buffer = Vec::new();
160 whois.encode(&mut buffer)?;
161
162 let message =
164 self.create_unconfirmed_message(UnconfirmedServiceChoice::WhoIs as u8, &buffer);
165 self.socket.send_to(&message, target_addr)?;
166
167 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 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 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 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 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 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 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 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); 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 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 if objects.is_empty() {
321 objects = Self::scan_object_identifiers(&response_data);
322 }
323
324 Ok(objects)
325 }
326
327 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 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 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 property_refs.push(PropertyReference::new(PropertyIdentifier::ObjectName)); property_refs.push(PropertyReference::new(PropertyIdentifier::Description)); 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)); property_refs.push(PropertyReference::new(PropertyIdentifier::StatusFlags));
387 }
389 _ => {}
390 }
391
392 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 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 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 std::thread::sleep(Duration::from_millis(100));
451 }
452
453 Ok(objects_info)
454 }
455
456 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 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 self.send_confirmed_request(
512 target_addr,
513 ConfirmedServiceChoice::WriteProperty,
514 &service_data,
515 )?;
516
517 Ok(())
518 }
519
520 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 const VERIFY_ATTEMPTS: u32 = 4;
546 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 let read_back = read_back.into_iter().next().unwrap_or(PropertyValue::Null);
565 Ok(WriteOutcome::NotEffective { read_back })
566 }
567
568 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 fn create_unconfirmed_bvlc(
576 &self,
577 service_choice: u8,
578 service_data: &[u8],
579 bvlc_function: u8,
580 ) -> Vec<u8> {
581 let mut npdu = Npdu::new();
583 npdu.control.expecting_reply = false;
584 npdu.control.priority = 0;
585 let npdu_buffer = npdu.encode();
586
587 let mut apdu = vec![0x10]; apdu.push(service_choice);
590 apdu.extend_from_slice(service_data);
591
592 let mut message = npdu_buffer;
594 message.extend_from_slice(&apdu);
595
596 let mut bvlc_message = vec![0x81, bvlc_function, 0x00, 0x00];
598 bvlc_message.extend_from_slice(&message);
599
600 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 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 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 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 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 fn parse_iam_response(&self, data: &[u8], source: SocketAddr) -> Option<DeviceInfo> {
689 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 let npdu_start = 4;
701 let (_npdu, npdu_len) = Npdu::decode(&data[npdu_start..]).ok()?;
702
703 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 fn interpret_confirmed_response(
747 &self,
748 data: &[u8],
749 expected_invoke_id: u8,
750 ) -> Result<Option<Vec<u8>>, ClientError> {
751 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 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 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 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#[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 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 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 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 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}