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