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