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 `PropertyValue::BitString`
6//! can be promoted from raw `(unused_bits, data)` bytes to named flags, the same
7//! way `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(all(feature = "alloc", not(feature = "std")))]
22use alloc::vec::Vec;
23
24use crate::enums::{AuditOperation, ObjectType, ServiceSupported};
25use crate::error::{DecodeError, DecodingKind, Error, RangeError, 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(RangeError::new(
362                "BACnetAuditOperationFlags: reserved bits 16-31 must be zero",
363            )
364            .into());
365        }
366        Ok(Self { bits })
367    }
368
369    /// Return the complete bit-position-preserving representation.
370    #[inline]
371    pub const fn bits(self) -> u64 {
372        self.bits
373    }
374
375    /// Whether no audit operation bit is set.
376    #[inline]
377    pub const fn is_empty(self) -> bool {
378        self.bits == 0
379    }
380
381    /// Whether `operation` is selected.
382    ///
383    /// Raw [`AuditOperation`] values above 63 are outside the production's
384    /// representable range and therefore return `false`.
385    #[inline]
386    pub const fn contains(self, operation: AuditOperation) -> bool {
387        let bit = operation.to_raw();
388        Self::valid_bit(bit) && self.bits & (1u64 << bit) != 0
389    }
390
391    /// Select `operation`, returning `false` without mutation when its raw
392    /// value is outside the representable range 0 through 63.
393    #[inline]
394    pub fn insert(&mut self, operation: AuditOperation) -> bool {
395        let bit = operation.to_raw();
396        if !Self::valid_bit(bit) {
397            return false;
398        }
399        self.bits |= 1u64 << bit;
400        true
401    }
402
403    /// Clear `operation`, returning `false` when its raw value is outside the
404    /// representable range 0 through 63.
405    #[inline]
406    pub fn remove(&mut self, operation: AuditOperation) -> bool {
407        let bit = operation.to_raw();
408        if !Self::valid_bit(bit) {
409            return false;
410        }
411        self.bits &= !(1u64 << bit);
412        true
413    }
414
415    /// Iterate selected operations in ascending bit-position order, including
416    /// proprietary raw values.
417    pub fn iter(self) -> impl Iterator<Item = AuditOperation> {
418        let bits = self.bits;
419        (0..64)
420            .filter(move |&bit| bits & (1u64 << bit) != 0)
421            .map(AuditOperation::from_raw)
422    }
423
424    const fn valid_bit(bit: u32) -> bool {
425        bit <= 15 || (bit >= 32 && bit <= 63)
426    }
427
428    /// Decode a Clause 20.2.10 BIT STRING payload.
429    ///
430    /// `data` excludes the initial unused-bits octet. The decoder rejects
431    /// values wider than the 64 positions allowed by
432    /// `BACnetAuditOperationFlags`, invalid unused-bit counts, and nonzero
433    /// padding in the final octet.
434    pub fn from_bacnet(unused_bits: u8, data: &[u8]) -> Result<Self> {
435        if unused_bits > 7 {
436            return Err(DecodeError::new(
437                DecodingKind::InvalidEncoding,
438                0,
439                "BACnetAuditOperationFlags: unused_bits must be 0-7, got {}",
440            )
441            .with_value(u64::from(unused_bits))
442            .into());
443        }
444        if data.is_empty() {
445            if unused_bits != 0 {
446                return Err(Error::decoding(
447                    0,
448                    "BACnetAuditOperationFlags: an empty bit string must have zero unused bits",
449                ));
450            }
451            return Ok(Self::empty());
452        }
453
454        let meaningful_bits = data.len() * 8 - usize::from(unused_bits);
455        if meaningful_bits > 64 {
456            return Err(DecodeError::new(
457                DecodingKind::InvalidEncoding,
458                0,
459                "BACnetAuditOperationFlags: {} bits exceeds the 64-bit limit",
460            )
461            .with_value(meaningful_bits as u64)
462            .into());
463        }
464        if unused_bits != 0 {
465            let padding_mask = (1u8 << unused_bits) - 1;
466            if data.last().is_some_and(|last| last & padding_mask != 0) {
467                return Err(Error::decoding(
468                    0,
469                    "BACnetAuditOperationFlags: final-octet padding bits must be zero",
470                ));
471            }
472        }
473
474        let mut bits = 0u64;
475        for (index, &byte) in data.iter().enumerate() {
476            bits |= u64::from(byte.reverse_bits()) << (index * 8);
477        }
478        if bits & Self::RESERVED_BITS != 0 {
479            return Err(Error::decoding(
480                0,
481                "BACnetAuditOperationFlags: reserved bits 16-31 must be zero",
482            ));
483        }
484        Ok(Self { bits })
485    }
486
487    /// Encode as a shortest-form Clause 20.2.10 BIT STRING payload.
488    ///
489    /// The returned `data` excludes the initial unused-bits octet. Bit 0 is
490    /// placed in the most-significant position of the first data octet. Only
491    /// with `alloc`.
492    #[cfg(feature = "alloc")]
493    pub fn to_bacnet(self) -> (u8, Vec<u8>) {
494        if self.bits == 0 {
495            return (0, Vec::new());
496        }
497
498        let bit_len = (u64::BITS - self.bits.leading_zeros()) as usize;
499        let byte_len = bit_len.div_ceil(8);
500        let unused_bits = (byte_len * 8 - bit_len) as u8;
501        let mut data = Vec::with_capacity(byte_len);
502        for index in 0..byte_len {
503            data.push(((self.bits >> (index * 8)) as u8).reverse_bits());
504        }
505        (unused_bits, data)
506    }
507}
508
509impl core::fmt::Display for AuditOperationFlags {
510    fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
511        write_joined(f, self.iter())
512    }
513}
514
515impl core::fmt::Debug for AuditOperationFlags {
516    fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
517        core::fmt::Display::fmt(self, f)
518    }
519}
520
521#[cfg(test)]
522mod tests {
523    use super::*;
524    use crate::primitives::StatusFlags;
525
526    #[test]
527    fn pack_unpack_octet_spec_vectors() {
528        // Clause 20.2.10: first defined bit -> MSB. Deliberately asymmetric
529        // values — round trips alone cannot catch a symmetric encode/decode
530        // inversion, only literal wire bytes can.
531        assert_eq!(pack_octet(0b001), 0x80); // TO_OFFNORMAL / monday
532        assert_eq!(pack_octet(0b100), 0x20); // TO_NORMAL
533        assert_eq!(pack_octet(0b0100_0000), 0x02); // sunday (7-bit DaysOfWeek)
534        assert_eq!(unpack_octet(&[0x80], 3), 0b001);
535        assert_eq!(unpack_octet(&[0x20], 3), 0b100);
536        assert_eq!(unpack_octet(&[0xFE], 7), 0x7F); // all seven days
537        assert_eq!(unpack_octet(&[], 3), 0);
538        // Nonconformant junk past the defined bits is masked off.
539        assert_eq!(unpack_octet(&[0xFF], 3), 0b111);
540        // The octet helpers and the typed codecs agree.
541        assert_eq!(
542            EventTransitionBits::from_bacnet(&[pack_octet(0b001)]),
543            EventTransitionBits::TO_OFFNORMAL
544        );
545        assert_eq!(EventTransitionBits::TO_OFFNORMAL.to_bacnet(), 0x80);
546        assert_eq!(LimitEnable::LOW_LIMIT_ENABLE.to_bacnet(), 0x80);
547    }
548
549    #[test]
550    fn log_status_follows_the_bit0_first_wire_order() {
551        // Literal wire octets: log-disabled is bit 0, so the top bit.
552        for (status, octet) in [
553            (LogStatus::LOG_DISABLED, 0x80),
554            (LogStatus::BUFFER_PURGED, 0x40),
555            (LogStatus::LOG_INTERRUPTED, 0x20),
556            (LogStatus::LOG_DISABLED | LogStatus::BUFFER_PURGED, 0xC0),
557            (LogStatus::empty(), 0x00),
558        ] {
559            assert_eq!(status.to_bacnet(), octet, "{status}");
560            assert_eq!(LogStatus::from_bacnet(&[octet]), status);
561        }
562        // Undefined bits are neither sent nor read back.
563        assert_eq!(LogStatus::from_bits_retain(0xF9).to_bacnet(), 0x80);
564        assert_eq!(LogStatus::from_bacnet(&[0x9F]), LogStatus::LOG_DISABLED);
565        assert_eq!(LogStatus::from_bacnet(&[]), LogStatus::empty());
566        assert_eq!(
567            format!("{}", LogStatus::LOG_DISABLED | LogStatus::LOG_INTERRUPTED),
568            "LOG_DISABLED | LOG_INTERRUPTED"
569        );
570    }
571
572    #[test]
573    fn event_transition_bits_msb_first() {
574        // Wire byte 0b1010_0000 => bit0 (to-offnormal) and bit2 (to-normal).
575        let bits = EventTransitionBits::from_bacnet(&[0b1010_0000]);
576        assert_eq!(
577            bits,
578            EventTransitionBits::TO_OFFNORMAL | EventTransitionBits::TO_NORMAL
579        );
580        assert_eq!(bits.to_string(), "TO_OFFNORMAL | TO_NORMAL");
581    }
582
583    #[test]
584    fn limit_enable_msb_first() {
585        assert_eq!(
586            LimitEnable::from_bacnet(&[0x80]),
587            LimitEnable::LOW_LIMIT_ENABLE
588        );
589        assert_eq!(
590            LimitEnable::from_bacnet(&[0xC0]),
591            LimitEnable::LOW_LIMIT_ENABLE | LimitEnable::HIGH_LIMIT_ENABLE
592        );
593        assert_eq!(LimitEnable::from_bacnet(&[]).to_string(), "()");
594    }
595
596    #[test]
597    fn object_types_supported_matches_device_layout() {
598        // Byte 8 = 0x80 sets bit 64 => ColorTemperature (per device object test).
599        let mut data = vec![0u8; 9];
600        data[8] = 0x80;
601        let ots = ObjectTypesSupported::from_bacnet(&data);
602        assert!(ots.contains(ObjectType::COLOR_TEMPERATURE));
603        assert!(!ots.contains(ObjectType::ANALOG_INPUT));
604        assert_eq!(
605            ots.iter().collect::<Vec<_>>(),
606            vec![ObjectType::COLOR_TEMPERATURE]
607        );
608
609        // Byte 4 = 0xFD: types 32-37,39 set, 38 (NetworkSecurity) unset.
610        let ss = ObjectTypesSupported::from_bacnet(&[0, 0, 0, 0, 0xFD]);
611        assert!(ss.contains(ObjectType::from_raw(32)));
612        assert!(!ss.contains(ObjectType::NETWORK_SECURITY)); // type 38
613        assert!(ss.contains(ObjectType::from_raw(39)));
614    }
615
616    #[test]
617    fn resolved_value_holds_names_not_bytes() {
618        // The whole point: the decoded value contains named variants, and even
619        // its Debug shows names rather than the raw wire bytes.
620        let ots = ObjectTypesSupported::from_bacnet(&[0b1110_0000]); // types 0,1,2
621        assert_eq!(
622            ots.iter().collect::<Vec<_>>(),
623            [
624                ObjectType::ANALOG_INPUT,
625                ObjectType::ANALOG_OUTPUT,
626                ObjectType::ANALOG_VALUE
627            ]
628        );
629        let dbg = format!("{ots:?}");
630        assert!(
631            dbg.contains("ANALOG_INPUT"),
632            "Debug should name types: {dbg}"
633        );
634        assert!(
635            !dbg.contains("224"),
636            "Debug must not show raw byte 0xE0: {dbg}"
637        );
638    }
639
640    #[test]
641    fn services_supported_follows_clause_21() {
642        // Arbitrary decode vector (historically the device's pre-#192
643        // hardcoded constant — NOT the property the Device object serves).
644        let ss = ServicesSupported::from_bacnet(&[0xA4, 0x0B, 0x80, 0x35, 0x80, 0x00]);
645        assert!(ss.contains(ServiceSupported::ACKNOWLEDGE_ALARM)); // bit 0
646        assert!(ss.contains(ServiceSupported::SUBSCRIBE_COV)); // bit 5
647        assert!(ss.contains(ServiceSupported::READ_PROPERTY)); // bit 12
648        assert!(ss.contains(ServiceSupported::I_AM)); // bit 26
649        assert!(ss.contains(ServiceSupported::TIME_SYNCHRONIZATION)); // bit 32 (NOT who-is)
650        assert!(!ss.contains(ServiceSupported::WHO_IS)); // bit 34
651    }
652
653    #[test]
654    fn status_flags_render_names_not_numbers() {
655        assert_eq!(
656            (StatusFlags::IN_ALARM | StatusFlags::FAULT).to_string(),
657            "IN_ALARM | FAULT"
658        );
659        // Empty renders as "()", and Debug matches Display — never a raw number.
660        assert_eq!(StatusFlags::empty().to_string(), "()");
661        assert_eq!(format!("{:?}", StatusFlags::empty()), "()");
662        assert_eq!(format!("{:?}", StatusFlags::IN_ALARM), "IN_ALARM");
663    }
664
665    #[test]
666    fn small_types_debug_is_named_never_a_number() {
667        // bitflags' derived Debug prints 0x0 for an empty set; ours prints "()".
668        assert_eq!(format!("{:?}", EventTransitionBits::empty()), "()");
669        assert_eq!(format!("{:?}", LimitEnable::empty()), "()");
670        assert_eq!(format!("{:?}", EventTransitionBits::TO_FAULT), "TO_FAULT");
671    }
672
673    #[cfg(feature = "alloc")]
674    #[test]
675    fn audit_operation_flags_empty_and_named_bits_use_msb_first_wire_order() {
676        let empty = AuditOperationFlags::empty();
677        assert!(empty.is_empty());
678        assert_eq!(empty.to_bacnet(), (0, Vec::new()));
679        assert_eq!(AuditOperationFlags::from_bacnet(0, &[]).unwrap(), empty);
680
681        let mut flags = AuditOperationFlags::empty();
682        assert!(flags.insert(AuditOperation::READ));
683        assert!(flags.insert(AuditOperation::GENERAL));
684        assert!(flags.contains(AuditOperation::READ));
685        assert!(flags.contains(AuditOperation::GENERAL));
686        assert_eq!(flags.bits(), (1 << 0) | (1 << 15));
687        assert_eq!(flags.to_bacnet(), (0, vec![0x80, 0x01]));
688        assert_eq!(
689            AuditOperationFlags::from_bacnet(0, &[0x80, 0x01]).unwrap(),
690            flags
691        );
692        assert_eq!(flags.to_string(), "READ | GENERAL");
693    }
694
695    #[cfg(feature = "alloc")]
696    #[test]
697    fn audit_operation_flags_preserve_proprietary_bit_63() {
698        let proprietary_32 = AuditOperation::from_raw(32);
699        let proprietary = AuditOperation::from_raw(63);
700        let mut flags = AuditOperationFlags::from_bits(1u64 << 63).unwrap();
701        assert!(flags.insert(proprietary_32));
702        assert!(flags.contains(proprietary_32));
703        assert!(flags.contains(proprietary));
704        assert_eq!(flags.to_bacnet(), (0, vec![0, 0, 0, 0, 0x80, 0, 0, 1]));
705        assert_eq!(
706            AuditOperationFlags::from_bacnet(0, &[0, 0, 0, 0, 0x80, 0, 0, 1]).unwrap(),
707            flags
708        );
709        assert!(!flags.insert(AuditOperation::from_raw(64)));
710        assert!(!flags.contains(AuditOperation::from_raw(64)));
711        assert!(flags.remove(proprietary_32));
712        assert!(flags.remove(proprietary));
713        assert!(flags.is_empty());
714    }
715
716    #[test]
717    fn audit_operation_flags_reject_overlong_and_nonzero_padding() {
718        assert!(AuditOperationFlags::from_bacnet(7, &[0; 9]).is_err());
719        assert!(AuditOperationFlags::from_bacnet(1, &[0x01]).is_err());
720        assert!(AuditOperationFlags::from_bacnet(1, &[]).is_err());
721        assert!(AuditOperationFlags::from_bacnet(8, &[0]).is_err());
722        assert!(AuditOperationFlags::from_bits(1 << 16).is_err());
723        assert!(AuditOperationFlags::from_bits(1 << 31).is_err());
724        assert!(AuditOperationFlags::from_bacnet(7, &[0, 0, 0x80]).is_err());
725
726        let mut flags = AuditOperationFlags::empty();
727        assert!(!flags.insert(AuditOperation::from_raw(16)));
728        assert!(!flags.insert(AuditOperation::from_raw(31)));
729        assert!(flags.is_empty());
730
731        // Zero padding is accepted; seven unused bits leaves only bit 0.
732        assert_eq!(
733            AuditOperationFlags::from_bacnet(7, &[0x80]).unwrap(),
734            AuditOperationFlags::from_bits(1).unwrap()
735        );
736    }
737}