Skip to main content

bacnet_types/
primitives.rs

1//! BACnet primitive data types per ASHRAE 135-2020 Clause 20.2.
2//!
3//! Core types used throughout the protocol: [`ObjectIdentifier`], [`Date`],
4//! [`Time`], and the [`PropertyValue`] sum type.
5
6#[cfg(not(feature = "std"))]
7use alloc::{format, string::String, vec::Vec};
8
9use crate::enums::ObjectType;
10use crate::error::Error;
11
12// ---------------------------------------------------------------------------
13// ObjectIdentifier (Clause 20.2.14)
14// ---------------------------------------------------------------------------
15
16/// BACnet Object Identifier: 10-bit type + 22-bit instance number.
17///
18/// Uniquely identifies a BACnet object within a device. Encoded as a
19/// 4-byte big-endian value: `(object_type << 22) | instance_number`.
20/// Checked construction and wire decoding preserve both field widths, so
21/// encoding cannot silently truncate an identifier.
22///
23/// The all-ones instance value [`WILDCARD_INSTANCE`](Self::WILDCARD_INSTANCE)
24/// is still a valid 22-bit wire value, and some service contexts give it
25/// wildcard meaning. Use [`ObjectIdentifier::new`] or [`ObjectIdentifier::decode`]
26/// when preserving wire-level values. Use [`ObjectIdentifier::new_addressable`]
27/// when constructing identifiers for concrete objects that can be stored,
28/// addressed, or compared as actual object instances.
29#[derive(Clone, Copy, PartialEq, Eq, Hash)]
30pub struct ObjectIdentifier {
31    object_type: ObjectType,
32    instance_number: u32,
33}
34
35impl ObjectIdentifier {
36    /// Maximum encodable instance number (2^22 - 1 = 4,194,303).
37    ///
38    /// This is also [`WILDCARD_INSTANCE`](Self::WILDCARD_INSTANCE). It is
39    /// accepted by [`ObjectIdentifier::new`] and [`ObjectIdentifier::decode`]
40    /// for wire compatibility, but callers that need a concrete addressable
41    /// object should use [`MAX_ADDRESSABLE_INSTANCE`](Self::MAX_ADDRESSABLE_INSTANCE)
42    /// or [`ObjectIdentifier::new_addressable`].
43    pub const MAX_INSTANCE: u32 = 0x3F_FFFF;
44
45    /// The reserved wildcard instance number (4,194,303).
46    ///
47    /// BACnet service clauses assign this value special "any/local instance"
48    /// meaning in selected contexts, for example Who-Is/Who-Am-I discovery and
49    /// Device/Network Port object access. It is not a concrete object instance
50    /// for addressable-object storage.
51    pub const WILDCARD_INSTANCE: u32 = Self::MAX_INSTANCE;
52
53    /// Maximum instance number for a concrete addressable object (4,194,302).
54    ///
55    /// This excludes [`WILDCARD_INSTANCE`](Self::WILDCARD_INSTANCE).
56    pub const MAX_ADDRESSABLE_INSTANCE: u32 = Self::WILDCARD_INSTANCE - 1;
57
58    /// Create an ObjectIdentifier from a 10-bit type and 22-bit wire instance.
59    ///
60    /// This constructor preserves [`WILDCARD_INSTANCE`](Self::WILDCARD_INSTANCE)
61    /// because it is a valid encoded value in BACnet service payloads. Use
62    /// [`ObjectIdentifier::new_addressable`] when the identifier must name a
63    /// concrete object instance.
64    ///
65    /// # Errors
66    /// Returns `Err` if the object type exceeds 1023 or `instance_number` exceeds
67    /// [`MAX_INSTANCE`](Self::MAX_INSTANCE).
68    pub fn new(object_type: ObjectType, instance_number: u32) -> Result<Self, Error> {
69        if instance_number > Self::MAX_INSTANCE {
70            return Err(Error::OutOfRange(alloc_or_std_format!(
71                "instance number {} exceeds max {}",
72                instance_number,
73                Self::MAX_INSTANCE
74            )));
75        }
76        if object_type.to_raw() > 0x3FF {
77            return Err(Error::OutOfRange(alloc_or_std_format!(
78                "object type {} exceeds max 1023",
79                object_type.to_raw()
80            )));
81        }
82        Ok(Self {
83            object_type,
84            instance_number,
85        })
86    }
87
88    /// Create a new ObjectIdentifier for a concrete addressable object.
89    ///
90    /// This rejects [`WILDCARD_INSTANCE`](Self::WILDCARD_INSTANCE), while
91    /// [`ObjectIdentifier::new`] keeps accepting that value for wire-level
92    /// round trips and BACnet service contexts where it is meaningful.
93    ///
94    /// # Errors
95    /// Returns `Err` if the object type exceeds 1023, or `instance_number` exceeds
96    /// [`MAX_INSTANCE`](Self::MAX_INSTANCE) or equals
97    /// [`WILDCARD_INSTANCE`](Self::WILDCARD_INSTANCE).
98    pub fn new_addressable(object_type: ObjectType, instance_number: u32) -> Result<Self, Error> {
99        let identifier = Self::new(object_type, instance_number)?;
100        if instance_number == Self::WILDCARD_INSTANCE {
101            return Err(Error::OutOfRange(alloc_or_std_format!(
102                "instance number {} is reserved as the wildcard value; maximum addressable instance is {}",
103                instance_number,
104                Self::MAX_ADDRESSABLE_INSTANCE
105            )));
106        }
107        Ok(identifier)
108    }
109
110    /// The object type.
111    pub const fn object_type(&self) -> ObjectType {
112        self.object_type
113    }
114
115    /// The instance number (0 to 4,194,303).
116    ///
117    /// Values returned from wire decoding may equal
118    /// [`WILDCARD_INSTANCE`](Self::WILDCARD_INSTANCE). Treat that value as a
119    /// concrete addressable object only after the relevant service context has
120    /// resolved its wildcard semantics.
121    pub const fn instance_number(&self) -> u32 {
122        self.instance_number
123    }
124
125    /// Encode to the 4-byte BACnet wire format (big-endian).
126    pub fn encode(&self) -> [u8; 4] {
127        assert!(
128            self.object_type.to_raw() <= 0x3FF,
129            "ObjectType {} exceeds 10-bit field",
130            self.object_type.to_raw()
131        );
132        assert!(
133            self.instance_number <= Self::MAX_INSTANCE,
134            "Instance {} exceeds MAX_INSTANCE",
135            self.instance_number
136        );
137        let value = (self.object_type.to_raw() << 22) | self.instance_number;
138        value.to_be_bytes()
139    }
140
141    /// Decode from the 4-byte BACnet wire format (big-endian).
142    pub fn decode(data: &[u8]) -> Result<Self, Error> {
143        if data.len() != 4 {
144            return Err(Error::decoding(
145                0,
146                format!(
147                    "ObjectIdentifier expects exactly 4 bytes, got {}",
148                    data.len()
149                ),
150            ));
151        }
152        let value = u32::from_be_bytes([data[0], data[1], data[2], data[3]]);
153        let type_raw = (value >> 22) & 0x3FF;
154        let instance = value & Self::MAX_INSTANCE;
155        Ok(Self {
156            object_type: ObjectType::from_raw(type_raw),
157            instance_number: instance,
158        })
159    }
160}
161
162impl core::fmt::Debug for ObjectIdentifier {
163    fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
164        write!(
165            f,
166            "ObjectIdentifier({:?}, {})",
167            self.object_type, self.instance_number
168        )
169    }
170}
171
172impl core::fmt::Display for ObjectIdentifier {
173    fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
174        write!(f, "{},{}", self.object_type, self.instance_number)
175    }
176}
177
178// ---------------------------------------------------------------------------
179// Date (Clause 20.2.12)
180// ---------------------------------------------------------------------------
181
182/// BACnet Date: year, month, day, day-of-week.
183///
184/// - Year: 0-254 relative to 1900 (0xFF = unspecified)
185/// - Month: 1-14 (13=odd, 14=even, 0xFF=unspecified)
186/// - Day: 1-34 (32=last, 33=odd, 34=even, 0xFF=unspecified)
187/// - Day of week: 1=Monday..7=Sunday (0xFF=unspecified)
188#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
189pub struct Date {
190    /// Year minus 1900 (0-254, or 0xFF for unspecified).
191    pub year: u8,
192    /// Month (1-14, or 0xFF for unspecified).
193    pub month: u8,
194    /// Day of month (1-34, or 0xFF for unspecified).
195    pub day: u8,
196    /// Day of week (1=Monday..7=Sunday, or 0xFF for unspecified).
197    pub day_of_week: u8,
198}
199
200impl Date {
201    /// Value indicating "unspecified" for any date field.
202    pub const UNSPECIFIED: u8 = 0xFF;
203
204    /// Encode to 4 bytes.
205    pub fn encode(&self) -> [u8; 4] {
206        [self.year, self.month, self.day, self.day_of_week]
207    }
208
209    /// Decode from 4 bytes.
210    pub fn decode(data: &[u8]) -> Result<Self, Error> {
211        if data.len() != 4 {
212            return Err(Error::decoding(
213                0,
214                format!("Date expects exactly 4 bytes, got {}", data.len()),
215            ));
216        }
217        Ok(Self {
218            year: data[0],
219            month: data[1],
220            day: data[2],
221            day_of_week: data[3],
222        })
223    }
224
225    /// Get the actual year (1900 + year field), or None if unspecified.
226    pub fn actual_year(&self) -> Option<u16> {
227        if self.year == Self::UNSPECIFIED {
228            None
229        } else {
230            Some(1900 + self.year as u16)
231        }
232    }
233}
234
235// ---------------------------------------------------------------------------
236// Time (Clause 20.2.13)
237// ---------------------------------------------------------------------------
238
239/// BACnet Time: hour, minute, second, hundredths.
240///
241/// Each field can be 0xFF for "unspecified".
242#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
243pub struct Time {
244    /// Hour (0-23, or 0xFF for unspecified).
245    pub hour: u8,
246    /// Minute (0-59, or 0xFF for unspecified).
247    pub minute: u8,
248    /// Second (0-59, or 0xFF for unspecified).
249    pub second: u8,
250    /// Hundredths of a second (0-99, or 0xFF for unspecified).
251    pub hundredths: u8,
252}
253
254impl Time {
255    /// Value indicating "unspecified" for any time field.
256    pub const UNSPECIFIED: u8 = 0xFF;
257
258    /// Encode to 4 bytes.
259    pub fn encode(&self) -> [u8; 4] {
260        [self.hour, self.minute, self.second, self.hundredths]
261    }
262
263    /// Decode from 4 bytes.
264    pub fn decode(data: &[u8]) -> Result<Self, Error> {
265        if data.len() != 4 {
266            return Err(Error::decoding(
267                0,
268                format!("Time expects exactly 4 bytes, got {}", data.len()),
269            ));
270        }
271        Ok(Self {
272            hour: data[0],
273            minute: data[1],
274            second: data[2],
275            hundredths: data[3],
276        })
277    }
278}
279
280// ---------------------------------------------------------------------------
281// BACnetTimeStamp (Clause 20.2.1.5)
282// ---------------------------------------------------------------------------
283
284/// BACnet timestamp -- a CHOICE of Time, sequence number, or DateTime.
285#[derive(Debug, Clone, PartialEq)]
286pub enum BACnetTimeStamp {
287    /// Context tag 0: Time
288    Time(Time),
289    /// Context tag 1: Unsigned sequence number in the required 0..=65535 range.
290    SequenceNumber(u16),
291    /// Context tag 2: BACnetDateTime (Date + Time)
292    DateTime {
293        /// Calendar date component.
294        date: Date,
295        /// Time-of-day component.
296        time: Time,
297    },
298}
299
300// ---------------------------------------------------------------------------
301// StatusFlags (Clause 12.X -- used by many object types)
302// ---------------------------------------------------------------------------
303
304bitflags::bitflags! {
305    /// BACnet StatusFlags -- 4-bit bitstring present on most objects.
306    #[derive(Clone, Copy, PartialEq, Eq, Hash)]
307    pub struct StatusFlags: u8 {
308        /// Event_State is anything other than NORMAL.
309        const IN_ALARM = 0b1000;
310        /// Reliability is present and is not NO_FAULT_DETECTED.
311        const FAULT = 0b0100;
312        /// The value is being supplied by a local override rather than the physical input.
313        const OVERRIDDEN = 0b0010;
314        /// Out_Of_Service is TRUE.
315        const OUT_OF_SERVICE = 0b0001;
316    }
317}
318
319impl core::fmt::Display for StatusFlags {
320    /// Renders the set flags by their Clause-21 names (`IN_ALARM | FAULT`), or
321    /// `()` when none are set — never a raw number.
322    fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
323        let mut first = true;
324        for (name, _) in self.iter_names() {
325            if !first {
326                f.write_str(" | ")?;
327            }
328            f.write_str(name)?;
329            first = false;
330        }
331        if first {
332            f.write_str("()")?;
333        }
334        Ok(())
335    }
336}
337
338impl core::fmt::Debug for StatusFlags {
339    fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
340        core::fmt::Display::fmt(self, f)
341    }
342}
343
344// ---------------------------------------------------------------------------
345// PropertyValue -- sum type for BACnet property values
346// ---------------------------------------------------------------------------
347
348/// A BACnet application-layer value.
349///
350/// This enum covers all primitive value types that can appear as property
351/// values in BACnet objects. Constructed types (lists, sequences) are
352/// represented as nested structures.
353#[derive(Debug, Clone, PartialEq)]
354pub enum PropertyValue {
355    /// Null value.
356    Null,
357    /// Boolean value.
358    Boolean(bool),
359    /// Unsigned integer (up to 64-bit for BACnet Unsigned64).
360    Unsigned(u64),
361    /// Signed integer.
362    Signed(i32),
363    /// IEEE 754 single-precision float.
364    Real(f32),
365    /// IEEE 754 double-precision float.
366    Double(f64),
367    /// Octet string (raw bytes).
368    OctetString(Vec<u8>),
369    /// Character string (UTF-8).
370    CharacterString(String),
371    /// Bit string (variable length).
372    BitString {
373        /// Number of unused bits in the last byte.
374        unused_bits: u8,
375        /// The bit data bytes.
376        data: Vec<u8>,
377    },
378    /// Enumerated value.
379    Enumerated(u32),
380    /// Date value.
381    Date(Date),
382    /// Time value.
383    Time(Time),
384    /// Object identifier.
385    ObjectIdentifier(ObjectIdentifier),
386    /// A sequence (array) of property values.
387    ///
388    /// Used when reading an entire array property with `arrayIndex` absent
389    /// (Clause 15.5.1). Each element is encoded as its own application-tagged
390    /// value, concatenated in order.
391    List(Vec<PropertyValue>),
392    /// Raw, already-encoded application-layer bytes for a property whose wire
393    /// form is a context-tagged (CHOICE/SEQUENCE) production that the flat
394    /// application-tagged model above cannot express — e.g. a framed
395    /// `BACnetEventParameter` or a `Recipient_List` of `BACnetDestination`.
396    ///
397    /// The bytes are emitted verbatim by
398    /// `bacnet_encoding::primitives::encode_property_value` and are produced
399    /// by `decode_application_value` when it encounters a context-tagged
400    /// element; they always span exactly one complete tagged element,
401    /// including its context tag header(s). Objects that serve structured
402    /// properties decode these with the matching framed codec in
403    /// `bacnet-encoding`; a property writer that is not expecting this variant
404    /// should reject it as `INVALID_DATA_TYPE`.
405    ApplicationData(Vec<u8>),
406}
407
408impl PropertyValue {
409    /// Whether this is a value of a primitive datatype, one application tag
410    /// on the wire: every variant except `List` and `ApplicationData`.
411    pub fn is_primitive(&self) -> bool {
412        match self {
413            Self::Null
414            | Self::Boolean(_)
415            | Self::Unsigned(_)
416            | Self::Signed(_)
417            | Self::Real(_)
418            | Self::Double(_)
419            | Self::OctetString(_)
420            | Self::CharacterString(_)
421            | Self::BitString { .. }
422            | Self::Enumerated(_)
423            | Self::Date(_)
424            | Self::Time(_)
425            | Self::ObjectIdentifier(_) => true,
426            Self::List(_) | Self::ApplicationData(_) => false,
427        }
428    }
429}
430
431// ---------------------------------------------------------------------------
432// Formatting helper macro (works in both std and no_std+alloc)
433// ---------------------------------------------------------------------------
434
435/// Format a string using either std or alloc.
436#[cfg(feature = "std")]
437macro_rules! alloc_or_std_format {
438    ($($arg:tt)*) => { format!($($arg)*) }
439}
440
441#[cfg(not(feature = "std"))]
442macro_rules! alloc_or_std_format {
443    ($($arg:tt)*) => { alloc::format!($($arg)*) }
444}
445
446use alloc_or_std_format;
447
448// ---------------------------------------------------------------------------
449// Tests
450// ---------------------------------------------------------------------------
451
452#[cfg(test)]
453mod tests {
454    use super::*;
455
456    #[test]
457    fn object_identifier_encode_decode_round_trip() {
458        let oid = ObjectIdentifier::new(ObjectType::ANALOG_INPUT, 1).unwrap();
459        let bytes = oid.encode();
460        let decoded = ObjectIdentifier::decode(&bytes).unwrap();
461        assert_eq!(oid, decoded);
462    }
463
464    #[test]
465    fn object_identifier_wire_format() {
466        // AnalogInput (type=0) instance=1: (0 << 22) | 1 = 0x00000001
467        let oid = ObjectIdentifier::new(ObjectType::ANALOG_INPUT, 1).unwrap();
468        assert_eq!(oid.encode(), [0x00, 0x00, 0x00, 0x01]);
469
470        // Device (type=8) instance=1234: (8 << 22) | 1234 = 0x020004D2
471        let oid = ObjectIdentifier::new(ObjectType::DEVICE, 1234).unwrap();
472        let expected = ((8u32 << 22) | 1234u32).to_be_bytes();
473        assert_eq!(oid.encode(), expected);
474    }
475
476    #[test]
477    fn object_identifier_max_instance() {
478        let oid =
479            ObjectIdentifier::new(ObjectType::DEVICE, ObjectIdentifier::MAX_INSTANCE).unwrap();
480        assert_eq!(oid.instance_number(), 0x3F_FFFF);
481        let bytes = oid.encode();
482        let decoded = ObjectIdentifier::decode(&bytes).unwrap();
483        assert_eq!(decoded.instance_number(), 0x3F_FFFF);
484    }
485
486    #[test]
487    fn object_identifier_invalid_instance() {
488        let result = ObjectIdentifier::new(ObjectType::DEVICE, ObjectIdentifier::MAX_INSTANCE + 1);
489        assert!(result.is_err());
490    }
491
492    #[test]
493    fn object_identifier_buffer_too_short() {
494        let result = ObjectIdentifier::decode(&[0x00, 0x00]);
495        assert!(result.is_err());
496    }
497
498    #[test]
499    fn object_identifier_overlong_errors() {
500        assert!(ObjectIdentifier::decode(&[0x00, 0x00, 0x00, 0x01, 0xFF]).is_err());
501    }
502
503    #[test]
504    fn date_encode_decode_round_trip() {
505        let date = Date {
506            year: 124, // 2024
507            month: 6,
508            day: 15,
509            day_of_week: 6, // Saturday
510        };
511        let bytes = date.encode();
512        let decoded = Date::decode(&bytes).unwrap();
513        assert_eq!(date, decoded);
514        assert_eq!(decoded.actual_year(), Some(2024));
515    }
516
517    #[test]
518    fn date_unspecified_year() {
519        let date = Date {
520            year: Date::UNSPECIFIED,
521            month: 1,
522            day: 1,
523            day_of_week: Date::UNSPECIFIED,
524        };
525        assert_eq!(date.actual_year(), None);
526    }
527
528    #[test]
529    fn date_overlong_errors() {
530        assert!(Date::decode(&[124, 6, 15, 6, 0]).is_err());
531    }
532
533    #[test]
534    fn time_encode_decode_round_trip() {
535        let time = Time {
536            hour: 14,
537            minute: 30,
538            second: 45,
539            hundredths: 50,
540        };
541        let bytes = time.encode();
542        let decoded = Time::decode(&bytes).unwrap();
543        assert_eq!(time, decoded);
544    }
545
546    #[test]
547    fn time_overlong_errors() {
548        assert!(Time::decode(&[14, 30, 45, 50, 0]).is_err());
549    }
550
551    #[test]
552    fn status_flags_operations() {
553        let flags = StatusFlags::IN_ALARM | StatusFlags::OUT_OF_SERVICE;
554        assert!(flags.contains(StatusFlags::IN_ALARM));
555        assert!(flags.contains(StatusFlags::OUT_OF_SERVICE));
556        assert!(!flags.contains(StatusFlags::FAULT));
557        assert!(!flags.contains(StatusFlags::OVERRIDDEN));
558    }
559
560    // --- OID edge cases ---
561
562    #[test]
563    fn object_identifier_instance_zero() {
564        // Instance 0 is valid and commonly used (e.g., Device,0)
565        let oid = ObjectIdentifier::new(ObjectType::DEVICE, 0).unwrap();
566        assert_eq!(oid.instance_number(), 0);
567        let bytes = oid.encode();
568        let decoded = ObjectIdentifier::decode(&bytes).unwrap();
569        assert_eq!(decoded.instance_number(), 0);
570        assert_eq!(decoded.object_type(), ObjectType::DEVICE);
571    }
572
573    #[test]
574    fn object_identifier_all_types_instance_zero() {
575        // Instance 0 for each common type should round-trip correctly
576        for type_raw in [0u32, 1, 2, 3, 4, 5, 6, 8, 10, 13, 14, 17, 19] {
577            let obj_type = ObjectType::from_raw(type_raw);
578            let oid = ObjectIdentifier::new(obj_type, 0).unwrap();
579            let bytes = oid.encode();
580            let decoded = ObjectIdentifier::decode(&bytes).unwrap();
581            assert_eq!(decoded.object_type(), obj_type, "type {type_raw} failed");
582            assert_eq!(
583                decoded.instance_number(),
584                0,
585                "type {type_raw} instance failed"
586            );
587        }
588    }
589
590    #[test]
591    fn object_identifier_wildcard_instance() {
592        let oid =
593            ObjectIdentifier::new(ObjectType::DEVICE, ObjectIdentifier::WILDCARD_INSTANCE).unwrap();
594        assert_eq!(oid.instance_number(), ObjectIdentifier::MAX_INSTANCE);
595        let bytes = oid.encode();
596        let decoded = ObjectIdentifier::decode(&bytes).unwrap();
597        assert_eq!(decoded.instance_number(), ObjectIdentifier::MAX_INSTANCE);
598
599        let raw_wire_value = ((ObjectType::DEVICE.to_raw() << 22)
600            | ObjectIdentifier::WILDCARD_INSTANCE)
601            .to_be_bytes();
602        let decoded = ObjectIdentifier::decode(&raw_wire_value).unwrap();
603        assert_eq!(decoded.object_type(), ObjectType::DEVICE);
604        assert_eq!(
605            decoded.instance_number(),
606            ObjectIdentifier::WILDCARD_INSTANCE
607        );
608    }
609
610    #[test]
611    fn object_identifier_new_addressable_rejects_wildcard() {
612        assert_eq!(
613            ObjectIdentifier::MAX_ADDRESSABLE_INSTANCE,
614            ObjectIdentifier::WILDCARD_INSTANCE - 1
615        );
616
617        let oid = ObjectIdentifier::new_addressable(
618            ObjectType::DEVICE,
619            ObjectIdentifier::MAX_ADDRESSABLE_INSTANCE,
620        )
621        .unwrap();
622        assert_eq!(
623            oid.instance_number(),
624            ObjectIdentifier::MAX_ADDRESSABLE_INSTANCE
625        );
626
627        let analog_input = ObjectIdentifier::new_addressable(
628            ObjectType::ANALOG_INPUT,
629            ObjectIdentifier::MAX_ADDRESSABLE_INSTANCE,
630        )
631        .unwrap();
632        assert_eq!(analog_input.object_type(), ObjectType::ANALOG_INPUT);
633        assert_eq!(
634            analog_input.instance_number(),
635            ObjectIdentifier::MAX_ADDRESSABLE_INSTANCE
636        );
637
638        let wildcard =
639            ObjectIdentifier::new_addressable(ObjectType::DEVICE, ObjectIdentifier::MAX_INSTANCE)
640                .unwrap_err();
641        assert!(wildcard.to_string().contains("wildcard"));
642
643        let too_large = ObjectIdentifier::new_addressable(
644            ObjectType::DEVICE,
645            ObjectIdentifier::MAX_INSTANCE + 1,
646        );
647        assert!(too_large.is_err());
648    }
649
650    #[test]
651    fn object_identifier_decode_extra_bytes_errors() {
652        let oid = ObjectIdentifier::new(ObjectType::ANALOG_INPUT, 42).unwrap();
653        let mut bytes = oid.encode().to_vec();
654        bytes.extend_from_slice(&[0xFF, 0xFF]);
655        assert!(ObjectIdentifier::decode(&bytes).is_err());
656    }
657
658    #[test]
659    fn object_identifier_new_rejects_type_overflow() {
660        for raw in [1024, u32::MAX] {
661            assert!(matches!(
662                ObjectIdentifier::new(ObjectType::from_raw(raw), 0),
663                Err(Error::OutOfRange(_))
664            ));
665        }
666    }
667
668    #[test]
669    fn object_identifier_addressable_rejects_type_overflow() {
670        for raw in [1024, u32::MAX] {
671            assert!(matches!(
672                ObjectIdentifier::new_addressable(ObjectType::from_raw(raw), 0),
673                Err(Error::OutOfRange(_))
674            ));
675        }
676    }
677
678    #[test]
679    fn object_identifier_checked_instance_boundaries() {
680        for instance in [ObjectIdentifier::MAX_INSTANCE + 1, u32::MAX] {
681            for constructor in [ObjectIdentifier::new, ObjectIdentifier::new_addressable] {
682                assert!(matches!(
683                    constructor(ObjectType::from_raw(1023), instance),
684                    Err(Error::OutOfRange(_))
685                ));
686            }
687        }
688    }
689
690    #[test]
691    fn object_identifier_wire_boundaries_preserve_identity() {
692        for (raw_type, instance, wire) in [
693            (0, 0, [0, 0, 0, 0]),
694            (1023, 0, [0xff, 0xc0, 0, 0]),
695            (
696                1023,
697                ObjectIdentifier::MAX_ADDRESSABLE_INSTANCE,
698                [0xff, 0xff, 0xff, 0xfe],
699            ),
700            (
701                1023,
702                ObjectIdentifier::MAX_INSTANCE,
703                [0xff, 0xff, 0xff, 0xff],
704            ),
705        ] {
706            let object_type = ObjectType::from_raw(raw_type);
707            let oid = ObjectIdentifier::new(object_type, instance).unwrap();
708            assert_eq!(oid.encode(), wire);
709            assert_eq!(ObjectIdentifier::decode(&wire).unwrap(), oid);
710            if instance == ObjectIdentifier::WILDCARD_INSTANCE {
711                assert!(ObjectIdentifier::new_addressable(object_type, instance).is_err());
712            } else {
713                assert_eq!(
714                    ObjectIdentifier::new_addressable(object_type, instance).unwrap(),
715                    oid
716                );
717            }
718        }
719        // Raw ObjectType remains a generic selector outside ObjectIdentifier.
720        assert_eq!(ObjectType::from_raw(u32::MAX).to_raw(), u32::MAX);
721    }
722
723    #[test]
724    fn property_value_variants() {
725        let null = PropertyValue::Null;
726        let boolean = PropertyValue::Boolean(true);
727        let real = PropertyValue::Real(72.5);
728        let string = PropertyValue::CharacterString("test".into());
729
730        assert_eq!(null, PropertyValue::Null);
731        assert_eq!(boolean, PropertyValue::Boolean(true));
732        assert_ne!(real, PropertyValue::Real(73.0));
733        assert_eq!(string, PropertyValue::CharacterString("test".into()));
734    }
735}