Skip to main content

bacnet_types/
bitstring.rs

1//! Named BACnet bit-string types (ASHRAE 135-2020 Clause 21).
2//!
3//! Several properties carry a `BitString` whose bits have a fixed, named
4//! meaning defined by the standard. This module models those `BACnetXxx` BIT
5//! STRING productions so a decoded [`crate::primitives::PropertyValue::BitString`]
6//! can be promoted from raw `(unused_bits, data)` bytes to named flags, the same
7//! way [`crate::enums::ResolvedEnum`] promotes an `Enumerated`.
8//!
9//! ## Bit order
10//!
11//! BACnet bit strings are transmitted most-significant-bit-first: bit 0 of the
12//! string is the top bit (`0x80`) of the first content byte, bit 8 is the top
13//! bit of the second byte, and so on. Every type here reads bit *N* of the
14//! string as "feature N", matching how the device object packs these fields
15//! (see `bacnet-objects`'s `compute_object_types_supported`).
16//!
17//! [`crate::primitives::StatusFlags`] predates this module and keeps its own
18//! right-aligned representation; it is re-exported and mapped by the resolver as
19//! well.
20
21#[cfg(not(feature = "std"))]
22use alloc::{format, vec::Vec};
23
24use crate::enums::{AuditOperation, ObjectType, ServiceSupported};
25use crate::error::{Error, Result};
26
27pub use crate::primitives::StatusFlags;
28pub use days_of_week::DaysOfWeek;
29pub use priority_filter::BACnetPriorityFilter;
30
31mod priority_filter;
32
33/// Read bit `n` of a BACnet bit string (MSB-first) from its content bytes.
34///
35/// `data` is the bit-string payload *without* the leading unused-bits count
36/// (i.e. `PropertyValue::BitString::data`). Bits past the end read as `false`.
37fn wire_bit(data: &[u8], n: usize) -> bool {
38    let mask = 0x80u8 >> (n % 8);
39    data.get(n / 8).is_some_and(|b| b & mask != 0)
40}
41
42/// Pack a bit0-first value into its Clause 20.2.10 wire octet.
43///
44/// Every ≤8-bit BACnet bit string in this stack keeps its internal layout as
45/// `bit0 = first defined bit` (`TO_OFFNORMAL`, `monday`, `low-limit-enable`,
46/// …), while the wire wants the first defined bit in the most significant bit
47/// of the octet. Reversing the byte is that whole conversion: bit 0 lands at
48/// `0x80`, bit 1 at `0x40`, and the result is left-aligned for any width.
49fn pack_octet(bits_lsb0: u8) -> u8 {
50    bits_lsb0.reverse_bits()
51}
52
53/// Inverse of [`pack_octet`]: read the first octet of a bit-string payload
54/// back into bit0-first form, masked to the string's `defined_bits`.
55///
56/// Masking (rather than trusting the peer's declared unused-bit count) keeps
57/// nonconformant padding out of the value; an empty payload reads as zero.
58fn unpack_octet(data: &[u8], defined_bits: u32) -> u8 {
59    let mask = if defined_bits >= 8 {
60        u8::MAX
61    } else {
62        (1u8 << defined_bits) - 1
63    };
64    data.first().copied().unwrap_or(0).reverse_bits() & mask
65}
66
67/// Decode a `BACnetStatusFlags` bit-string payload (MSB-first) into
68/// [`StatusFlags`], which keeps its right-aligned in-memory layout.
69///
70/// Reads wire bits 0–3 by position, so a peer that declares a wrong string
71/// length still decodes correctly — Clause 20.2.10 puts bit 0 in the most
72/// significant bit of the first octet regardless of length.
73pub fn status_flags_from_bacnet(data: &[u8]) -> StatusFlags {
74    let mut flags = StatusFlags::empty();
75    flags.set(StatusFlags::IN_ALARM, wire_bit(data, 0));
76    flags.set(StatusFlags::FAULT, wire_bit(data, 1));
77    flags.set(StatusFlags::OVERRIDDEN, wire_bit(data, 2));
78    flags.set(StatusFlags::OUT_OF_SERVICE, wire_bit(data, 3));
79    flags
80}
81
82/// Render a bitflags value as its set Clause-21 names (` NAME | NAME `), or `()`
83/// when none are set — for both `Display` and `Debug`, never a raw number.
84macro_rules! impl_named_bit_display {
85    ($t:ty) => {
86        impl core::fmt::Display for $t {
87            fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
88                let mut first = true;
89                for (name, _) in self.iter_names() {
90                    if !first {
91                        f.write_str(" | ")?;
92                    }
93                    f.write_str(name)?;
94                    first = false;
95                }
96                if first {
97                    f.write_str("()")?;
98                }
99                Ok(())
100            }
101        }
102
103        impl core::fmt::Debug for $t {
104            fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
105                core::fmt::Display::fmt(self, f)
106            }
107        }
108    };
109}
110
111// Declared after `impl_named_bit_display!` so the module can use the macro.
112mod days_of_week;
113
114/// Write `Display` items as ` A | B | C ` (or `()` when empty).
115fn write_joined<T: core::fmt::Display>(
116    f: &mut core::fmt::Formatter<'_>,
117    items: impl Iterator<Item = T>,
118) -> core::fmt::Result {
119    let mut first = true;
120    for item in items {
121        if !first {
122            f.write_str(" | ")?;
123        }
124        write!(f, "{item}")?;
125        first = false;
126    }
127    if first {
128        f.write_str("()")?;
129    }
130    Ok(())
131}
132
133bitflags::bitflags! {
134    /// `BACnetEventTransitionBits` — 3-bit string for `event-enable`,
135    /// `acked-transitions`, and `ack-required` (Clause 21).
136    #[derive(Clone, Copy, PartialEq, Eq, Hash, Default)]
137    pub struct EventTransitionBits: u8 {
138        /// Transition to an off-normal event state (bit 0).
139        const TO_OFFNORMAL = 1 << 0;
140        /// Transition to the fault event state (bit 1).
141        const TO_FAULT = 1 << 1;
142        /// Transition to the normal event state (bit 2).
143        const TO_NORMAL = 1 << 2;
144    }
145}
146
147impl EventTransitionBits {
148    /// Decode from a BACnet bit-string payload (MSB-first).
149    pub fn from_bacnet(data: &[u8]) -> Self {
150        let mut bits = Self::empty();
151        bits.set(Self::TO_OFFNORMAL, wire_bit(data, 0));
152        bits.set(Self::TO_FAULT, wire_bit(data, 1));
153        bits.set(Self::TO_NORMAL, wire_bit(data, 2));
154        bits
155    }
156
157    /// Encode to the single Clause 20.2.10 wire octet (`unused_bits: 5`):
158    /// `TO_OFFNORMAL` at `0x80`, `TO_FAULT` at `0x40`, `TO_NORMAL` at `0x20`.
159    pub fn to_bacnet(self) -> u8 {
160        pack_octet(self.bits())
161    }
162}
163
164impl_named_bit_display!(EventTransitionBits);
165
166bitflags::bitflags! {
167    /// `BACnetLimitEnable` — 2-bit string for the `limit-enable` property
168    /// (Clause 21).
169    #[derive(Clone, Copy, PartialEq, Eq, Hash, Default)]
170    pub struct LimitEnable: u8 {
171        /// Low-limit event detection is enabled (bit 0).
172        const LOW_LIMIT_ENABLE = 1 << 0;
173        /// High-limit event detection is enabled (bit 1).
174        const HIGH_LIMIT_ENABLE = 1 << 1;
175    }
176}
177
178impl LimitEnable {
179    /// Decode from a BACnet bit-string payload (MSB-first).
180    pub fn from_bacnet(data: &[u8]) -> Self {
181        let mut bits = Self::empty();
182        bits.set(Self::LOW_LIMIT_ENABLE, wire_bit(data, 0));
183        bits.set(Self::HIGH_LIMIT_ENABLE, wire_bit(data, 1));
184        bits
185    }
186
187    /// Encode to the single Clause 20.2.10 wire octet (`unused_bits: 6`):
188    /// `LOW_LIMIT_ENABLE` at `0x80`, `HIGH_LIMIT_ENABLE` at `0x40`.
189    pub fn to_bacnet(self) -> u8 {
190        pack_octet(self.bits())
191    }
192}
193
194impl_named_bit_display!(LimitEnable);
195
196bitflags::bitflags! {
197    /// `BACnetLogStatus` — the 3-bit string a log object records when its own
198    /// status or operation changes (Clause 21).
199    #[derive(Clone, Copy, PartialEq, Eq, Hash, Default)]
200    pub struct LogStatus: u8 {
201        /// Logging is disabled (bit 0).
202        const LOG_DISABLED = 1 << 0;
203        /// The buffer was purged (bit 1).
204        const BUFFER_PURGED = 1 << 1;
205        /// Logging was interrupted, so earlier samples may be missing (bit 2).
206        const LOG_INTERRUPTED = 1 << 2;
207    }
208}
209
210impl LogStatus {
211    /// Decode from a BACnet bit-string payload (MSB-first); bits past the
212    /// three defined ones are dropped.
213    pub fn from_bacnet(data: &[u8]) -> Self {
214        Self::from_bits_truncate(unpack_octet(data, 3))
215    }
216
217    /// Encode to the single Clause 20.2.10 wire octet (`unused_bits: 5`):
218    /// `LOG_DISABLED` at `0x80`, `BUFFER_PURGED` at `0x40`, `LOG_INTERRUPTED`
219    /// at `0x20`. Undefined bits a value may retain are never sent.
220    pub fn to_bacnet(self) -> u8 {
221        pack_octet(self.bits() & Self::all().bits())
222    }
223}
224
225impl_named_bit_display!(LogStatus);
226
227/// `BACnetServicesSupported` — the `protocol-services-supported` bit string,
228/// one bit per protocol service (Clause 21).
229///
230/// Stored as a `u64` bitset: bit *N* is set iff [`ServiceSupported`] value *N*
231/// is supported. `protocol-services-supported` is a closed enumeration whose
232/// highest defined bit is 48 (with no vendor-proprietary range), so a `u64`
233/// covers every standard service with headroom. Any nonstandard bit ≥ 64 — a
234/// protocol violation — is ignored on decode. The public API still yields named
235/// [`ServiceSupported`] values, and `Debug` prints names, never a raw number.
236#[derive(Clone, Copy, PartialEq, Eq, Hash, Default)]
237pub struct ServicesSupported {
238    bits: u64,
239}
240
241impl ServicesSupported {
242    /// Decode a bit-string payload (the `data` of `PropertyValue::BitString`)
243    /// into the bitset of services it advertises.
244    pub fn from_bacnet(data: &[u8]) -> Self {
245        let mut bits = 0u64;
246        for (i, &byte) in data.iter().take(8).enumerate() {
247            bits |= (byte.reverse_bits() as u64) << (i * 8);
248        }
249        Self { bits }
250    }
251
252    /// Whether `service` is marked supported.
253    pub fn contains(&self, service: ServiceSupported) -> bool {
254        let n = service.to_raw() as u32;
255        n < 64 && self.bits & (1u64 << n) != 0
256    }
257
258    /// Iterate the supported services in ascending bit order.
259    pub fn iter(&self) -> impl Iterator<Item = ServiceSupported> + '_ {
260        let bits = self.bits;
261        (0..64)
262            .filter(move |&n| bits & (1u64 << n) != 0)
263            .map(|n| ServiceSupported::from_raw(n as u8))
264    }
265}
266
267impl core::fmt::Display for ServicesSupported {
268    fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
269        write_joined(f, self.iter())
270    }
271}
272
273impl core::fmt::Debug for ServicesSupported {
274    fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
275        core::fmt::Display::fmt(self, f)
276    }
277}
278
279/// `BACnetObjectTypesSupported` — the `protocol-object-types-supported` bit
280/// string, one bit per object type (Clause 21).
281///
282/// Stored as a `[u64; 16]` bitset (1024 bits): bit *N* is set iff [`ObjectType`]
283/// value *N* is supported. The property covers only standardized object types
284/// (Clause 12.11.15) and the bit-string production is closed, but
285/// `BACnetObjectType` values run to 1023, so decode tolerates the enumeration's
286/// full 10-bit range instead of discarding a nonconformant peer's extra bits —
287/// hence the 16-word array, which is allocation-free and `Copy`. The public API
288/// still yields named
289/// [`ObjectType`] values (reusing that table rather than duplicating it), and
290/// `Debug` prints names, never raw numbers.
291#[derive(Clone, Copy, PartialEq, Eq, Hash, Default)]
292pub struct ObjectTypesSupported {
293    bits: [u64; 16],
294}
295
296impl ObjectTypesSupported {
297    /// Decode a bit-string payload (the `data` of `PropertyValue::BitString`)
298    /// into the bitset of object types it advertises.
299    pub fn from_bacnet(data: &[u8]) -> Self {
300        let mut bits = [0u64; 16];
301        for (i, &byte) in data.iter().take(128).enumerate() {
302            bits[i / 8] |= (byte.reverse_bits() as u64) << ((i % 8) * 8);
303        }
304        Self { bits }
305    }
306
307    /// Whether `object_type` is marked supported.
308    pub fn contains(&self, object_type: ObjectType) -> bool {
309        let n = object_type.to_raw();
310        n < 1024 && self.bits[(n / 64) as usize] & (1u64 << (n % 64)) != 0
311    }
312
313    /// Iterate the supported object types in ascending bit order.
314    pub fn iter(&self) -> impl Iterator<Item = ObjectType> + '_ {
315        (0..1024u32)
316            .filter(move |&n| self.bits[(n / 64) as usize] & (1u64 << (n % 64)) != 0)
317            .map(ObjectType::from_raw)
318    }
319}
320
321impl core::fmt::Display for ObjectTypesSupported {
322    fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
323        write_joined(f, self.iter())
324    }
325}
326
327impl core::fmt::Debug for ObjectTypesSupported {
328    fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
329        core::fmt::Display::fmt(self, f)
330    }
331}
332
333/// `BACnetAuditOperationFlags` — a bit string selecting audit operations
334/// (Clause 21).
335///
336/// Bit positions 0 through 15 have the same meanings as the named
337/// [`AuditOperation`] values. Positions 16 through 31 are currently undefined
338/// and reserved for ASHRAE, so they must remain zero. Positions 32 through 63
339/// are retained for proprietary operations.
340#[derive(Clone, Copy, PartialEq, Eq, Hash, Default)]
341pub struct AuditOperationFlags {
342    bits: u64,
343}
344
345impl AuditOperationFlags {
346    const RESERVED_BITS: u64 = 0x0000_0000_ffff_0000;
347
348    /// An empty set of audit operations.
349    #[inline]
350    pub const fn empty() -> Self {
351        Self { bits: 0 }
352    }
353
354    /// Construct from the bit-position-preserving representation.
355    ///
356    /// Currently undefined ASHRAE-reserved positions 16 through 31 are
357    /// rejected rather than masked.
358    #[inline]
359    pub fn from_bits(bits: u64) -> Result<Self> {
360        if bits & Self::RESERVED_BITS != 0 {
361            return Err(Error::OutOfRange(
362                "BACnetAuditOperationFlags: reserved bits 16-31 must be zero".into(),
363            ));
364        }
365        Ok(Self { bits })
366    }
367
368    /// Return the complete bit-position-preserving representation.
369    #[inline]
370    pub const fn bits(self) -> u64 {
371        self.bits
372    }
373
374    /// Whether no audit operation bit is set.
375    #[inline]
376    pub const fn is_empty(self) -> bool {
377        self.bits == 0
378    }
379
380    /// Whether `operation` is selected.
381    ///
382    /// Raw [`AuditOperation`] values above 63 are outside the production's
383    /// representable range and therefore return `false`.
384    #[inline]
385    pub const fn contains(self, operation: AuditOperation) -> bool {
386        let bit = operation.to_raw();
387        Self::valid_bit(bit) && self.bits & (1u64 << bit) != 0
388    }
389
390    /// Select `operation`, returning `false` without mutation when its raw
391    /// value is outside the representable range 0 through 63.
392    #[inline]
393    pub fn insert(&mut self, operation: AuditOperation) -> bool {
394        let bit = operation.to_raw();
395        if !Self::valid_bit(bit) {
396            return false;
397        }
398        self.bits |= 1u64 << bit;
399        true
400    }
401
402    /// Clear `operation`, returning `false` when its raw value is outside the
403    /// representable range 0 through 63.
404    #[inline]
405    pub fn remove(&mut self, operation: AuditOperation) -> bool {
406        let bit = operation.to_raw();
407        if !Self::valid_bit(bit) {
408            return false;
409        }
410        self.bits &= !(1u64 << bit);
411        true
412    }
413
414    /// Iterate selected operations in ascending bit-position order, including
415    /// proprietary raw values.
416    pub fn iter(self) -> impl Iterator<Item = AuditOperation> {
417        let bits = self.bits;
418        (0..64)
419            .filter(move |&bit| bits & (1u64 << bit) != 0)
420            .map(AuditOperation::from_raw)
421    }
422
423    const fn valid_bit(bit: u32) -> bool {
424        bit <= 15 || (bit >= 32 && bit <= 63)
425    }
426
427    /// Decode a Clause 20.2.10 BIT STRING payload.
428    ///
429    /// `data` excludes the initial unused-bits octet. The decoder rejects
430    /// values wider than the 64 positions allowed by
431    /// `BACnetAuditOperationFlags`, invalid unused-bit counts, and nonzero
432    /// padding in the final octet.
433    pub fn from_bacnet(unused_bits: u8, data: &[u8]) -> Result<Self> {
434        if unused_bits > 7 {
435            return Err(Error::decoding(
436                0,
437                format!("BACnetAuditOperationFlags: unused_bits must be 0-7, got {unused_bits}"),
438            ));
439        }
440        if data.is_empty() {
441            if unused_bits != 0 {
442                return Err(Error::decoding(
443                    0,
444                    "BACnetAuditOperationFlags: an empty bit string must have zero unused bits",
445                ));
446            }
447            return Ok(Self::empty());
448        }
449
450        let meaningful_bits = data.len() * 8 - usize::from(unused_bits);
451        if meaningful_bits > 64 {
452            return Err(Error::decoding(
453                0,
454                format!(
455                    "BACnetAuditOperationFlags: {meaningful_bits} bits exceeds the 64-bit limit"
456                ),
457            ));
458        }
459        if unused_bits != 0 {
460            let padding_mask = (1u8 << unused_bits) - 1;
461            if data.last().is_some_and(|last| last & padding_mask != 0) {
462                return Err(Error::decoding(
463                    0,
464                    "BACnetAuditOperationFlags: final-octet padding bits must be zero",
465                ));
466            }
467        }
468
469        let mut bits = 0u64;
470        for (index, &byte) in data.iter().enumerate() {
471            bits |= u64::from(byte.reverse_bits()) << (index * 8);
472        }
473        if bits & Self::RESERVED_BITS != 0 {
474            return Err(Error::decoding(
475                0,
476                "BACnetAuditOperationFlags: reserved bits 16-31 must be zero",
477            ));
478        }
479        Ok(Self { bits })
480    }
481
482    /// Encode as a shortest-form Clause 20.2.10 BIT STRING payload.
483    ///
484    /// The returned `data` excludes the initial unused-bits octet. Bit 0 is
485    /// placed in the most-significant position of the first data octet.
486    pub fn to_bacnet(self) -> (u8, Vec<u8>) {
487        if self.bits == 0 {
488            return (0, Vec::new());
489        }
490
491        let bit_len = (u64::BITS - self.bits.leading_zeros()) as usize;
492        let byte_len = bit_len.div_ceil(8);
493        let unused_bits = (byte_len * 8 - bit_len) as u8;
494        let mut data = Vec::with_capacity(byte_len);
495        for index in 0..byte_len {
496            data.push(((self.bits >> (index * 8)) as u8).reverse_bits());
497        }
498        (unused_bits, data)
499    }
500}
501
502impl core::fmt::Display for AuditOperationFlags {
503    fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
504        write_joined(f, self.iter())
505    }
506}
507
508impl core::fmt::Debug for AuditOperationFlags {
509    fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
510        core::fmt::Display::fmt(self, f)
511    }
512}
513
514#[cfg(test)]
515mod tests {
516    use super::*;
517    use crate::primitives::StatusFlags;
518
519    #[test]
520    fn pack_unpack_octet_spec_vectors() {
521        // Clause 20.2.10: first defined bit -> MSB. Deliberately asymmetric
522        // values — round trips alone cannot catch a symmetric encode/decode
523        // inversion, only literal wire bytes can.
524        assert_eq!(pack_octet(0b001), 0x80); // TO_OFFNORMAL / monday
525        assert_eq!(pack_octet(0b100), 0x20); // TO_NORMAL
526        assert_eq!(pack_octet(0b0100_0000), 0x02); // sunday (7-bit DaysOfWeek)
527        assert_eq!(unpack_octet(&[0x80], 3), 0b001);
528        assert_eq!(unpack_octet(&[0x20], 3), 0b100);
529        assert_eq!(unpack_octet(&[0xFE], 7), 0x7F); // all seven days
530        assert_eq!(unpack_octet(&[], 3), 0);
531        // Nonconformant junk past the defined bits is masked off.
532        assert_eq!(unpack_octet(&[0xFF], 3), 0b111);
533        // The octet helpers and the typed codecs agree.
534        assert_eq!(
535            EventTransitionBits::from_bacnet(&[pack_octet(0b001)]),
536            EventTransitionBits::TO_OFFNORMAL
537        );
538        assert_eq!(EventTransitionBits::TO_OFFNORMAL.to_bacnet(), 0x80);
539        assert_eq!(LimitEnable::LOW_LIMIT_ENABLE.to_bacnet(), 0x80);
540    }
541
542    #[test]
543    fn log_status_follows_the_bit0_first_wire_order() {
544        // Literal wire octets: log-disabled is bit 0, so the top bit.
545        for (status, octet) in [
546            (LogStatus::LOG_DISABLED, 0x80),
547            (LogStatus::BUFFER_PURGED, 0x40),
548            (LogStatus::LOG_INTERRUPTED, 0x20),
549            (LogStatus::LOG_DISABLED | LogStatus::BUFFER_PURGED, 0xC0),
550            (LogStatus::empty(), 0x00),
551        ] {
552            assert_eq!(status.to_bacnet(), octet, "{status}");
553            assert_eq!(LogStatus::from_bacnet(&[octet]), status);
554        }
555        // Undefined bits are neither sent nor read back.
556        assert_eq!(LogStatus::from_bits_retain(0xF9).to_bacnet(), 0x80);
557        assert_eq!(LogStatus::from_bacnet(&[0x9F]), LogStatus::LOG_DISABLED);
558        assert_eq!(LogStatus::from_bacnet(&[]), LogStatus::empty());
559        assert_eq!(
560            format!("{}", LogStatus::LOG_DISABLED | LogStatus::LOG_INTERRUPTED),
561            "LOG_DISABLED | LOG_INTERRUPTED"
562        );
563    }
564
565    #[test]
566    fn event_transition_bits_msb_first() {
567        // Wire byte 0b1010_0000 => bit0 (to-offnormal) and bit2 (to-normal).
568        let bits = EventTransitionBits::from_bacnet(&[0b1010_0000]);
569        assert_eq!(
570            bits,
571            EventTransitionBits::TO_OFFNORMAL | EventTransitionBits::TO_NORMAL
572        );
573        assert_eq!(bits.to_string(), "TO_OFFNORMAL | TO_NORMAL");
574    }
575
576    #[test]
577    fn limit_enable_msb_first() {
578        assert_eq!(
579            LimitEnable::from_bacnet(&[0x80]),
580            LimitEnable::LOW_LIMIT_ENABLE
581        );
582        assert_eq!(
583            LimitEnable::from_bacnet(&[0xC0]),
584            LimitEnable::LOW_LIMIT_ENABLE | LimitEnable::HIGH_LIMIT_ENABLE
585        );
586        assert_eq!(LimitEnable::from_bacnet(&[]).to_string(), "()");
587    }
588
589    #[test]
590    fn object_types_supported_matches_device_layout() {
591        // Byte 8 = 0x80 sets bit 64 => ColorTemperature (per device object test).
592        let mut data = vec![0u8; 9];
593        data[8] = 0x80;
594        let ots = ObjectTypesSupported::from_bacnet(&data);
595        assert!(ots.contains(ObjectType::COLOR_TEMPERATURE));
596        assert!(!ots.contains(ObjectType::ANALOG_INPUT));
597        assert_eq!(
598            ots.iter().collect::<Vec<_>>(),
599            vec![ObjectType::COLOR_TEMPERATURE]
600        );
601
602        // Byte 4 = 0xFD: types 32-37,39 set, 38 (NetworkSecurity) unset.
603        let ss = ObjectTypesSupported::from_bacnet(&[0, 0, 0, 0, 0xFD]);
604        assert!(ss.contains(ObjectType::from_raw(32)));
605        assert!(!ss.contains(ObjectType::NETWORK_SECURITY)); // type 38
606        assert!(ss.contains(ObjectType::from_raw(39)));
607    }
608
609    #[test]
610    fn resolved_value_holds_names_not_bytes() {
611        // The whole point: the decoded value contains named variants, and even
612        // its Debug shows names rather than the raw wire bytes.
613        let ots = ObjectTypesSupported::from_bacnet(&[0b1110_0000]); // types 0,1,2
614        assert_eq!(
615            ots.iter().collect::<Vec<_>>(),
616            [
617                ObjectType::ANALOG_INPUT,
618                ObjectType::ANALOG_OUTPUT,
619                ObjectType::ANALOG_VALUE
620            ]
621        );
622        let dbg = format!("{ots:?}");
623        assert!(
624            dbg.contains("ANALOG_INPUT"),
625            "Debug should name types: {dbg}"
626        );
627        assert!(
628            !dbg.contains("224"),
629            "Debug must not show raw byte 0xE0: {dbg}"
630        );
631    }
632
633    #[test]
634    fn services_supported_follows_clause_21() {
635        // Arbitrary decode vector (historically the device's pre-#192
636        // hardcoded constant — NOT the property the Device object serves).
637        let ss = ServicesSupported::from_bacnet(&[0xA4, 0x0B, 0x80, 0x35, 0x80, 0x00]);
638        assert!(ss.contains(ServiceSupported::ACKNOWLEDGE_ALARM)); // bit 0
639        assert!(ss.contains(ServiceSupported::SUBSCRIBE_COV)); // bit 5
640        assert!(ss.contains(ServiceSupported::READ_PROPERTY)); // bit 12
641        assert!(ss.contains(ServiceSupported::I_AM)); // bit 26
642        assert!(ss.contains(ServiceSupported::TIME_SYNCHRONIZATION)); // bit 32 (NOT who-is)
643        assert!(!ss.contains(ServiceSupported::WHO_IS)); // bit 34
644    }
645
646    #[test]
647    fn status_flags_render_names_not_numbers() {
648        assert_eq!(
649            (StatusFlags::IN_ALARM | StatusFlags::FAULT).to_string(),
650            "IN_ALARM | FAULT"
651        );
652        // Empty renders as "()", and Debug matches Display — never a raw number.
653        assert_eq!(StatusFlags::empty().to_string(), "()");
654        assert_eq!(format!("{:?}", StatusFlags::empty()), "()");
655        assert_eq!(format!("{:?}", StatusFlags::IN_ALARM), "IN_ALARM");
656    }
657
658    #[test]
659    fn small_types_debug_is_named_never_a_number() {
660        // bitflags' derived Debug prints 0x0 for an empty set; ours prints "()".
661        assert_eq!(format!("{:?}", EventTransitionBits::empty()), "()");
662        assert_eq!(format!("{:?}", LimitEnable::empty()), "()");
663        assert_eq!(format!("{:?}", EventTransitionBits::TO_FAULT), "TO_FAULT");
664    }
665
666    #[test]
667    fn audit_operation_flags_empty_and_named_bits_use_msb_first_wire_order() {
668        let empty = AuditOperationFlags::empty();
669        assert!(empty.is_empty());
670        assert_eq!(empty.to_bacnet(), (0, Vec::new()));
671        assert_eq!(AuditOperationFlags::from_bacnet(0, &[]).unwrap(), empty);
672
673        let mut flags = AuditOperationFlags::empty();
674        assert!(flags.insert(AuditOperation::READ));
675        assert!(flags.insert(AuditOperation::GENERAL));
676        assert!(flags.contains(AuditOperation::READ));
677        assert!(flags.contains(AuditOperation::GENERAL));
678        assert_eq!(flags.bits(), (1 << 0) | (1 << 15));
679        assert_eq!(flags.to_bacnet(), (0, vec![0x80, 0x01]));
680        assert_eq!(
681            AuditOperationFlags::from_bacnet(0, &[0x80, 0x01]).unwrap(),
682            flags
683        );
684        assert_eq!(flags.to_string(), "READ | GENERAL");
685    }
686
687    #[test]
688    fn audit_operation_flags_preserve_proprietary_bit_63() {
689        let proprietary_32 = AuditOperation::from_raw(32);
690        let proprietary = AuditOperation::from_raw(63);
691        let mut flags = AuditOperationFlags::from_bits(1u64 << 63).unwrap();
692        assert!(flags.insert(proprietary_32));
693        assert!(flags.contains(proprietary_32));
694        assert!(flags.contains(proprietary));
695        assert_eq!(flags.to_bacnet(), (0, vec![0, 0, 0, 0, 0x80, 0, 0, 1]));
696        assert_eq!(
697            AuditOperationFlags::from_bacnet(0, &[0, 0, 0, 0, 0x80, 0, 0, 1]).unwrap(),
698            flags
699        );
700        assert!(!flags.insert(AuditOperation::from_raw(64)));
701        assert!(!flags.contains(AuditOperation::from_raw(64)));
702        assert!(flags.remove(proprietary_32));
703        assert!(flags.remove(proprietary));
704        assert!(flags.is_empty());
705    }
706
707    #[test]
708    fn audit_operation_flags_reject_overlong_and_nonzero_padding() {
709        assert!(AuditOperationFlags::from_bacnet(7, &[0; 9]).is_err());
710        assert!(AuditOperationFlags::from_bacnet(1, &[0x01]).is_err());
711        assert!(AuditOperationFlags::from_bacnet(1, &[]).is_err());
712        assert!(AuditOperationFlags::from_bacnet(8, &[0]).is_err());
713        assert!(AuditOperationFlags::from_bits(1 << 16).is_err());
714        assert!(AuditOperationFlags::from_bits(1 << 31).is_err());
715        assert!(AuditOperationFlags::from_bacnet(7, &[0, 0, 0x80]).is_err());
716
717        let mut flags = AuditOperationFlags::empty();
718        assert!(!flags.insert(AuditOperation::from_raw(16)));
719        assert!(!flags.insert(AuditOperation::from_raw(31)));
720        assert!(flags.is_empty());
721
722        // Zero padding is accepted; seven unused bits leaves only bit 0.
723        assert_eq!(
724            AuditOperationFlags::from_bacnet(7, &[0x80]).unwrap(),
725            AuditOperationFlags::from_bits(1).unwrap()
726        );
727    }
728}