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::{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#[derive(Clone, Copy, PartialEq, Eq, Hash)]
21pub struct ObjectIdentifier {
22    object_type: ObjectType,
23    instance_number: u32,
24}
25
26impl ObjectIdentifier {
27    /// Maximum valid instance number (2^22 - 1 = 4,194,303).
28    pub const MAX_INSTANCE: u32 = 0x3F_FFFF;
29
30    /// The "wildcard" instance number used in WhoIs/IAm (4,194,303).
31    pub const WILDCARD_INSTANCE: u32 = Self::MAX_INSTANCE;
32
33    /// Create a new ObjectIdentifier.
34    ///
35    /// # Errors
36    /// Returns `Err` if `instance_number` exceeds [`MAX_INSTANCE`](Self::MAX_INSTANCE).
37    pub fn new(object_type: ObjectType, instance_number: u32) -> Result<Self, Error> {
38        if instance_number > Self::MAX_INSTANCE {
39            return Err(Error::OutOfRange(alloc_or_std_format!(
40                "instance number {} exceeds max {}",
41                instance_number,
42                Self::MAX_INSTANCE
43            )));
44        }
45        Ok(Self {
46            object_type,
47            instance_number,
48        })
49    }
50
51    /// Create without validation. Caller must ensure instance <= MAX_INSTANCE.
52    pub const fn new_unchecked(object_type: ObjectType, instance_number: u32) -> Self {
53        Self {
54            object_type,
55            instance_number,
56        }
57    }
58
59    /// The object type.
60    pub const fn object_type(&self) -> ObjectType {
61        self.object_type
62    }
63
64    /// The instance number (0 to 4,194,303).
65    pub const fn instance_number(&self) -> u32 {
66        self.instance_number
67    }
68
69    /// Encode to the 4-byte BACnet wire format (big-endian).
70    pub fn encode(&self) -> [u8; 4] {
71        let value = ((self.object_type.to_raw() & 0x3FF) << 22)
72            | (self.instance_number & Self::MAX_INSTANCE);
73        value.to_be_bytes()
74    }
75
76    /// Decode from the 4-byte BACnet wire format (big-endian).
77    pub fn decode(data: &[u8]) -> Result<Self, Error> {
78        if data.len() < 4 {
79            return Err(Error::buffer_too_short(4, data.len()));
80        }
81        let value = u32::from_be_bytes([data[0], data[1], data[2], data[3]]);
82        let type_raw = (value >> 22) & 0x3FF;
83        let instance = value & Self::MAX_INSTANCE;
84        Ok(Self {
85            object_type: ObjectType::from_raw(type_raw),
86            instance_number: instance,
87        })
88    }
89}
90
91impl core::fmt::Debug for ObjectIdentifier {
92    fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
93        write!(
94            f,
95            "ObjectIdentifier({:?}, {})",
96            self.object_type, self.instance_number
97        )
98    }
99}
100
101impl core::fmt::Display for ObjectIdentifier {
102    fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
103        write!(f, "{},{}", self.object_type, self.instance_number)
104    }
105}
106
107// ---------------------------------------------------------------------------
108// Date (Clause 20.2.12)
109// ---------------------------------------------------------------------------
110
111/// BACnet Date: year, month, day, day-of-week.
112///
113/// - Year: 0-254 relative to 1900 (0xFF = unspecified)
114/// - Month: 1-14 (13=odd, 14=even, 0xFF=unspecified)
115/// - Day: 1-34 (32=last, 33=odd, 34=even, 0xFF=unspecified)
116/// - Day of week: 1=Monday..7=Sunday (0xFF=unspecified)
117#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
118pub struct Date {
119    /// Year minus 1900 (0-254, or 0xFF for unspecified).
120    pub year: u8,
121    /// Month (1-14, or 0xFF for unspecified).
122    pub month: u8,
123    /// Day of month (1-34, or 0xFF for unspecified).
124    pub day: u8,
125    /// Day of week (1=Monday..7=Sunday, or 0xFF for unspecified).
126    pub day_of_week: u8,
127}
128
129impl Date {
130    /// Value indicating "unspecified" for any date field.
131    pub const UNSPECIFIED: u8 = 0xFF;
132
133    /// Encode to 4 bytes.
134    pub fn encode(&self) -> [u8; 4] {
135        [self.year, self.month, self.day, self.day_of_week]
136    }
137
138    /// Decode from 4 bytes.
139    pub fn decode(data: &[u8]) -> Result<Self, Error> {
140        if data.len() < 4 {
141            return Err(Error::buffer_too_short(4, data.len()));
142        }
143        Ok(Self {
144            year: data[0],
145            month: data[1],
146            day: data[2],
147            day_of_week: data[3],
148        })
149    }
150
151    /// Get the actual year (1900 + year field), or None if unspecified.
152    pub fn actual_year(&self) -> Option<u16> {
153        if self.year == Self::UNSPECIFIED {
154            None
155        } else {
156            Some(1900 + self.year as u16)
157        }
158    }
159}
160
161// ---------------------------------------------------------------------------
162// Time (Clause 20.2.13)
163// ---------------------------------------------------------------------------
164
165/// BACnet Time: hour, minute, second, hundredths.
166///
167/// Each field can be 0xFF for "unspecified".
168#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
169pub struct Time {
170    /// Hour (0-23, or 0xFF for unspecified).
171    pub hour: u8,
172    /// Minute (0-59, or 0xFF for unspecified).
173    pub minute: u8,
174    /// Second (0-59, or 0xFF for unspecified).
175    pub second: u8,
176    /// Hundredths of a second (0-99, or 0xFF for unspecified).
177    pub hundredths: u8,
178}
179
180impl Time {
181    /// Value indicating "unspecified" for any time field.
182    pub const UNSPECIFIED: u8 = 0xFF;
183
184    /// Encode to 4 bytes.
185    pub fn encode(&self) -> [u8; 4] {
186        [self.hour, self.minute, self.second, self.hundredths]
187    }
188
189    /// Decode from 4 bytes.
190    pub fn decode(data: &[u8]) -> Result<Self, Error> {
191        if data.len() < 4 {
192            return Err(Error::buffer_too_short(4, data.len()));
193        }
194        Ok(Self {
195            hour: data[0],
196            minute: data[1],
197            second: data[2],
198            hundredths: data[3],
199        })
200    }
201}
202
203// ---------------------------------------------------------------------------
204// BACnetTimeStamp (Clause 20.2.1.5)
205// ---------------------------------------------------------------------------
206
207/// BACnet timestamp -- a CHOICE of Time, sequence number, or DateTime.
208/// Per Clause 20.2.1.5 (ASHRAE 135-2020).
209#[derive(Debug, Clone, PartialEq)]
210pub enum BACnetTimeStamp {
211    /// Context tag 0: Time
212    Time(Time),
213    /// Context tag 1: Unsigned (sequence number)
214    SequenceNumber(u64),
215    /// Context tag 2: BACnetDateTime (Date + Time)
216    DateTime { date: Date, time: Time },
217}
218
219// ---------------------------------------------------------------------------
220// StatusFlags (Clause 12.X -- used by many object types)
221// ---------------------------------------------------------------------------
222
223bitflags::bitflags! {
224    /// BACnet StatusFlags -- 4-bit bitstring present on most objects.
225    #[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
226    pub struct StatusFlags: u8 {
227        const IN_ALARM = 0b1000;
228        const FAULT = 0b0100;
229        const OVERRIDDEN = 0b0010;
230        const OUT_OF_SERVICE = 0b0001;
231    }
232}
233
234// ---------------------------------------------------------------------------
235// PropertyValue -- sum type for BACnet property values
236// ---------------------------------------------------------------------------
237
238/// A BACnet application-layer value.
239///
240/// This enum covers all primitive value types that can appear as property
241/// values in BACnet objects. Constructed types (lists, sequences) are
242/// represented as nested structures.
243#[derive(Debug, Clone, PartialEq)]
244pub enum PropertyValue {
245    /// Null value.
246    Null,
247    /// Boolean value.
248    Boolean(bool),
249    /// Unsigned integer (up to 64-bit for BACnet Unsigned64).
250    Unsigned(u64),
251    /// Signed integer.
252    Signed(i32),
253    /// IEEE 754 single-precision float.
254    Real(f32),
255    /// IEEE 754 double-precision float.
256    Double(f64),
257    /// Octet string (raw bytes).
258    OctetString(Vec<u8>),
259    /// Character string (UTF-8).
260    CharacterString(String),
261    /// Bit string (variable length).
262    BitString {
263        /// Number of unused bits in the last byte.
264        unused_bits: u8,
265        /// The bit data bytes.
266        data: Vec<u8>,
267    },
268    /// Enumerated value.
269    Enumerated(u32),
270    /// Date value.
271    Date(Date),
272    /// Time value.
273    Time(Time),
274    /// Object identifier.
275    ObjectIdentifier(ObjectIdentifier),
276    /// A sequence (array) of property values.
277    ///
278    /// Used when reading an entire array property with `arrayIndex` absent
279    /// (Clause 15.5.1). Each element is encoded as its own application-tagged
280    /// value, concatenated in order.
281    List(Vec<PropertyValue>),
282}
283
284// ---------------------------------------------------------------------------
285// Formatting helper macro (works in both std and no_std+alloc)
286// ---------------------------------------------------------------------------
287
288/// Format a string using either std or alloc.
289#[cfg(feature = "std")]
290macro_rules! alloc_or_std_format {
291    ($($arg:tt)*) => { format!($($arg)*) }
292}
293
294#[cfg(not(feature = "std"))]
295macro_rules! alloc_or_std_format {
296    ($($arg:tt)*) => { alloc::format!($($arg)*) }
297}
298
299// Make macro usable within this module before its definition point
300use alloc_or_std_format;
301
302// ---------------------------------------------------------------------------
303// Tests
304// ---------------------------------------------------------------------------
305
306#[cfg(test)]
307mod tests {
308    use super::*;
309
310    #[test]
311    fn object_identifier_encode_decode_round_trip() {
312        let oid = ObjectIdentifier::new(ObjectType::ANALOG_INPUT, 1).unwrap();
313        let bytes = oid.encode();
314        let decoded = ObjectIdentifier::decode(&bytes).unwrap();
315        assert_eq!(oid, decoded);
316    }
317
318    #[test]
319    fn object_identifier_wire_format() {
320        // AnalogInput (type=0) instance=1: (0 << 22) | 1 = 0x00000001
321        let oid = ObjectIdentifier::new(ObjectType::ANALOG_INPUT, 1).unwrap();
322        assert_eq!(oid.encode(), [0x00, 0x00, 0x00, 0x01]);
323
324        // Device (type=8) instance=1234: (8 << 22) | 1234 = 0x020004D2
325        let oid = ObjectIdentifier::new(ObjectType::DEVICE, 1234).unwrap();
326        let expected = ((8u32 << 22) | 1234u32).to_be_bytes();
327        assert_eq!(oid.encode(), expected);
328    }
329
330    #[test]
331    fn object_identifier_max_instance() {
332        let oid =
333            ObjectIdentifier::new(ObjectType::DEVICE, ObjectIdentifier::MAX_INSTANCE).unwrap();
334        assert_eq!(oid.instance_number(), 0x3F_FFFF);
335        let bytes = oid.encode();
336        let decoded = ObjectIdentifier::decode(&bytes).unwrap();
337        assert_eq!(decoded.instance_number(), 0x3F_FFFF);
338    }
339
340    #[test]
341    fn object_identifier_invalid_instance() {
342        let result = ObjectIdentifier::new(ObjectType::DEVICE, ObjectIdentifier::MAX_INSTANCE + 1);
343        assert!(result.is_err());
344    }
345
346    #[test]
347    fn object_identifier_buffer_too_short() {
348        let result = ObjectIdentifier::decode(&[0x00, 0x00]);
349        assert!(result.is_err());
350    }
351
352    #[test]
353    fn date_encode_decode_round_trip() {
354        let date = Date {
355            year: 124, // 2024
356            month: 6,
357            day: 15,
358            day_of_week: 6, // Saturday
359        };
360        let bytes = date.encode();
361        let decoded = Date::decode(&bytes).unwrap();
362        assert_eq!(date, decoded);
363        assert_eq!(decoded.actual_year(), Some(2024));
364    }
365
366    #[test]
367    fn date_unspecified_year() {
368        let date = Date {
369            year: Date::UNSPECIFIED,
370            month: 1,
371            day: 1,
372            day_of_week: Date::UNSPECIFIED,
373        };
374        assert_eq!(date.actual_year(), None);
375    }
376
377    #[test]
378    fn time_encode_decode_round_trip() {
379        let time = Time {
380            hour: 14,
381            minute: 30,
382            second: 45,
383            hundredths: 50,
384        };
385        let bytes = time.encode();
386        let decoded = Time::decode(&bytes).unwrap();
387        assert_eq!(time, decoded);
388    }
389
390    #[test]
391    fn status_flags_operations() {
392        let flags = StatusFlags::IN_ALARM | StatusFlags::OUT_OF_SERVICE;
393        assert!(flags.contains(StatusFlags::IN_ALARM));
394        assert!(flags.contains(StatusFlags::OUT_OF_SERVICE));
395        assert!(!flags.contains(StatusFlags::FAULT));
396        assert!(!flags.contains(StatusFlags::OVERRIDDEN));
397    }
398
399    // --- OID edge cases ---
400
401    #[test]
402    fn object_identifier_instance_zero() {
403        // Instance 0 is valid and commonly used (e.g., Device,0)
404        let oid = ObjectIdentifier::new(ObjectType::DEVICE, 0).unwrap();
405        assert_eq!(oid.instance_number(), 0);
406        let bytes = oid.encode();
407        let decoded = ObjectIdentifier::decode(&bytes).unwrap();
408        assert_eq!(decoded.instance_number(), 0);
409        assert_eq!(decoded.object_type(), ObjectType::DEVICE);
410    }
411
412    #[test]
413    fn object_identifier_all_types_instance_zero() {
414        // Instance 0 for each common type should round-trip correctly
415        for type_raw in [0u32, 1, 2, 3, 4, 5, 6, 8, 10, 13, 14, 17, 19] {
416            let obj_type = ObjectType::from_raw(type_raw);
417            let oid = ObjectIdentifier::new(obj_type, 0).unwrap();
418            let bytes = oid.encode();
419            let decoded = ObjectIdentifier::decode(&bytes).unwrap();
420            assert_eq!(decoded.object_type(), obj_type, "type {type_raw} failed");
421            assert_eq!(
422                decoded.instance_number(),
423                0,
424                "type {type_raw} instance failed"
425            );
426        }
427    }
428
429    #[test]
430    fn object_identifier_wildcard_instance() {
431        let oid =
432            ObjectIdentifier::new(ObjectType::DEVICE, ObjectIdentifier::WILDCARD_INSTANCE).unwrap();
433        assert_eq!(oid.instance_number(), ObjectIdentifier::MAX_INSTANCE);
434        let bytes = oid.encode();
435        let decoded = ObjectIdentifier::decode(&bytes).unwrap();
436        assert_eq!(decoded.instance_number(), ObjectIdentifier::MAX_INSTANCE);
437    }
438
439    #[test]
440    fn object_identifier_decode_extra_bytes_ignored() {
441        // If we have more than 4 bytes, only the first 4 are used
442        let oid = ObjectIdentifier::new(ObjectType::ANALOG_INPUT, 42).unwrap();
443        let mut bytes = oid.encode().to_vec();
444        bytes.extend_from_slice(&[0xFF, 0xFF]); // extra garbage
445        let decoded = ObjectIdentifier::decode(&bytes).unwrap();
446        assert_eq!(decoded, oid);
447    }
448
449    #[test]
450    fn object_identifier_type_overflow_round_trip() {
451        // Verify that types > 1023 are masked to 10 bits
452        let oid = ObjectIdentifier::new_unchecked(ObjectType::from_raw(1024), 0);
453        let bytes = oid.encode();
454        let decoded = ObjectIdentifier::decode(&bytes).unwrap();
455        // After 10-bit masking, type 1024 = type 0
456        assert_eq!(decoded.object_type(), ObjectType::from_raw(0));
457    }
458
459    #[test]
460    fn property_value_variants() {
461        let null = PropertyValue::Null;
462        let boolean = PropertyValue::Boolean(true);
463        let real = PropertyValue::Real(72.5);
464        let string = PropertyValue::CharacterString("test".into());
465
466        assert_eq!(null, PropertyValue::Null);
467        assert_eq!(boolean, PropertyValue::Boolean(true));
468        assert_ne!(real, PropertyValue::Real(73.0));
469        assert_eq!(string, PropertyValue::CharacterString("test".into()));
470    }
471}