bacnet-types 0.11.0

BACnet protocol types, enums, and primitives per ASHRAE 135-2020
Documentation
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
637
638
639
640
641
642
643
644
645
646
647
648
649
650
651
652
653
654
655
656
657
658
659
660
661
662
663
664
665
666
667
668
669
670
//! Named BACnet bit-string types (ASHRAE 135-2020 Clause 21).
//!
//! Several properties carry a `BitString` whose bits have a fixed, named
//! meaning defined by the standard. This module models those `BACnetXxx` BIT
//! STRING productions so a decoded [`crate::primitives::PropertyValue::BitString`]
//! can be promoted from raw `(unused_bits, data)` bytes to named flags, the same
//! way [`crate::enums::ResolvedEnum`] promotes an `Enumerated`.
//!
//! ## Bit order
//!
//! BACnet bit strings are transmitted most-significant-bit-first: bit 0 of the
//! string is the top bit (`0x80`) of the first content byte, bit 8 is the top
//! bit of the second byte, and so on. Every type here reads bit *N* of the
//! string as "feature N", matching how the device object packs these fields
//! (see `bacnet-objects`'s `compute_object_types_supported`).
//!
//! [`crate::primitives::StatusFlags`] predates this module and keeps its own
//! right-aligned representation; it is re-exported and mapped by the resolver as
//! well.

#[cfg(not(feature = "std"))]
use alloc::{format, vec::Vec};

use crate::enums::{AuditOperation, ObjectType, ServiceSupported};
use crate::error::{Error, Result};

pub use crate::primitives::StatusFlags;
pub use priority_filter::BACnetPriorityFilter;

mod priority_filter;

/// Read bit `n` of a BACnet bit string (MSB-first) from its content bytes.
///
/// `data` is the bit-string payload *without* the leading unused-bits count
/// (i.e. `PropertyValue::BitString::data`). Bits past the end read as `false`.
fn wire_bit(data: &[u8], n: usize) -> bool {
    let mask = 0x80u8 >> (n % 8);
    data.get(n / 8).is_some_and(|b| b & mask != 0)
}

/// Pack a bit0-first value into its Clause 20.2.10 wire octet.
///
/// Every ≤8-bit BACnet bit string in this stack keeps its internal layout as
/// `bit0 = first defined bit` (`TO_OFFNORMAL`, `monday`, `low-limit-enable`,
/// …), while the wire wants the first defined bit in the most significant bit
/// of the octet. Reversing the byte is that whole conversion: bit 0 lands at
/// `0x80`, bit 1 at `0x40`, and the result is left-aligned for any width.
pub fn pack_octet(bits_lsb0: u8) -> u8 {
    bits_lsb0.reverse_bits()
}

/// Inverse of [`pack_octet`]: read the first octet of a bit-string payload
/// back into bit0-first form, masked to the string's `defined_bits`.
///
/// Masking (rather than trusting the peer's declared unused-bit count) keeps
/// nonconformant padding out of the value; an empty payload reads as zero.
pub fn unpack_octet(data: &[u8], defined_bits: u32) -> u8 {
    let mask = if defined_bits >= 8 {
        u8::MAX
    } else {
        (1u8 << defined_bits) - 1
    };
    data.first().copied().unwrap_or(0).reverse_bits() & mask
}

/// Decode a `BACnetStatusFlags` bit-string payload (MSB-first) into
/// [`StatusFlags`], which keeps its right-aligned in-memory layout.
///
/// Reads wire bits 0–3 by position, so a peer that declares a wrong string
/// length still decodes correctly — Clause 20.2.10 puts bit 0 in the most
/// significant bit of the first octet regardless of length.
pub fn status_flags_from_bacnet(data: &[u8]) -> StatusFlags {
    let mut flags = StatusFlags::empty();
    flags.set(StatusFlags::IN_ALARM, wire_bit(data, 0));
    flags.set(StatusFlags::FAULT, wire_bit(data, 1));
    flags.set(StatusFlags::OVERRIDDEN, wire_bit(data, 2));
    flags.set(StatusFlags::OUT_OF_SERVICE, wire_bit(data, 3));
    flags
}

/// Render a bitflags value as its set Clause-21 names (` NAME | NAME `), or `()`
/// when none are set — for both `Display` and `Debug`, never a raw number.
macro_rules! impl_named_bit_display {
    ($t:ty) => {
        impl core::fmt::Display for $t {
            fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
                let mut first = true;
                for (name, _) in self.iter_names() {
                    if !first {
                        f.write_str(" | ")?;
                    }
                    f.write_str(name)?;
                    first = false;
                }
                if first {
                    f.write_str("()")?;
                }
                Ok(())
            }
        }

        impl core::fmt::Debug for $t {
            fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
                core::fmt::Display::fmt(self, f)
            }
        }
    };
}

/// Write `Display` items as ` A | B | C ` (or `()` when empty).
fn write_joined<T: core::fmt::Display>(
    f: &mut core::fmt::Formatter<'_>,
    items: impl Iterator<Item = T>,
) -> core::fmt::Result {
    let mut first = true;
    for item in items {
        if !first {
            f.write_str(" | ")?;
        }
        write!(f, "{item}")?;
        first = false;
    }
    if first {
        f.write_str("()")?;
    }
    Ok(())
}

bitflags::bitflags! {
    /// `BACnetEventTransitionBits` — 3-bit string for `event-enable`,
    /// `acked-transitions`, and `ack-required` (Clause 21).
    #[derive(Clone, Copy, PartialEq, Eq, Hash, Default)]
    pub struct EventTransitionBits: u8 {
        /// Transition to an off-normal event state (bit 0).
        const TO_OFFNORMAL = 1 << 0;
        /// Transition to the fault event state (bit 1).
        const TO_FAULT = 1 << 1;
        /// Transition to the normal event state (bit 2).
        const TO_NORMAL = 1 << 2;
    }
}

impl EventTransitionBits {
    /// Decode from a BACnet bit-string payload (MSB-first).
    pub fn from_bacnet(data: &[u8]) -> Self {
        let mut bits = Self::empty();
        bits.set(Self::TO_OFFNORMAL, wire_bit(data, 0));
        bits.set(Self::TO_FAULT, wire_bit(data, 1));
        bits.set(Self::TO_NORMAL, wire_bit(data, 2));
        bits
    }

    /// Encode to the single Clause 20.2.10 wire octet (`unused_bits: 5`):
    /// `TO_OFFNORMAL` at `0x80`, `TO_FAULT` at `0x40`, `TO_NORMAL` at `0x20`.
    pub fn to_bacnet(self) -> u8 {
        pack_octet(self.bits())
    }
}

impl_named_bit_display!(EventTransitionBits);

bitflags::bitflags! {
    /// `BACnetLimitEnable` — 2-bit string for the `limit-enable` property
    /// (Clause 21).
    #[derive(Clone, Copy, PartialEq, Eq, Hash, Default)]
    pub struct LimitEnable: u8 {
        /// Low-limit event detection is enabled (bit 0).
        const LOW_LIMIT_ENABLE = 1 << 0;
        /// High-limit event detection is enabled (bit 1).
        const HIGH_LIMIT_ENABLE = 1 << 1;
    }
}

impl LimitEnable {
    /// Decode from a BACnet bit-string payload (MSB-first).
    pub fn from_bacnet(data: &[u8]) -> Self {
        let mut bits = Self::empty();
        bits.set(Self::LOW_LIMIT_ENABLE, wire_bit(data, 0));
        bits.set(Self::HIGH_LIMIT_ENABLE, wire_bit(data, 1));
        bits
    }

    /// Encode to the single Clause 20.2.10 wire octet (`unused_bits: 6`):
    /// `LOW_LIMIT_ENABLE` at `0x80`, `HIGH_LIMIT_ENABLE` at `0x40`.
    pub fn to_bacnet(self) -> u8 {
        pack_octet(self.bits())
    }
}

impl_named_bit_display!(LimitEnable);

/// `BACnetServicesSupported` — the `protocol-services-supported` bit string,
/// one bit per protocol service (Clause 21).
///
/// Stored as a `u64` bitset: bit *N* is set iff [`ServiceSupported`] value *N*
/// is supported. `protocol-services-supported` is a closed enumeration whose
/// highest defined bit is 48 (with no vendor-proprietary range), so a `u64`
/// covers every standard service with headroom. Any nonstandard bit ≥ 64 — a
/// protocol violation — is ignored on decode. The public API still yields named
/// [`ServiceSupported`] values, and `Debug` prints names, never a raw number.
#[derive(Clone, Copy, PartialEq, Eq, Hash, Default)]
pub struct ServicesSupported {
    bits: u64,
}

impl ServicesSupported {
    /// Decode a bit-string payload (the `data` of `PropertyValue::BitString`)
    /// into the bitset of services it advertises.
    pub fn from_bacnet(data: &[u8]) -> Self {
        let mut bits = 0u64;
        for (i, &byte) in data.iter().take(8).enumerate() {
            bits |= (byte.reverse_bits() as u64) << (i * 8);
        }
        Self { bits }
    }

    /// Whether `service` is marked supported.
    pub fn contains(&self, service: ServiceSupported) -> bool {
        let n = service.to_raw() as u32;
        n < 64 && self.bits & (1u64 << n) != 0
    }

    /// Iterate the supported services in ascending bit order.
    pub fn iter(&self) -> impl Iterator<Item = ServiceSupported> + '_ {
        let bits = self.bits;
        (0..64)
            .filter(move |&n| bits & (1u64 << n) != 0)
            .map(|n| ServiceSupported::from_raw(n as u8))
    }
}

impl core::fmt::Display for ServicesSupported {
    fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
        write_joined(f, self.iter())
    }
}

impl core::fmt::Debug for ServicesSupported {
    fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
        core::fmt::Display::fmt(self, f)
    }
}

/// `BACnetObjectTypesSupported` — the `protocol-object-types-supported` bit
/// string, one bit per object type (Clause 21).
///
/// Stored as a `[u64; 16]` bitset (1024 bits): bit *N* is set iff [`ObjectType`]
/// value *N* is supported. The property covers only standardized object types
/// (Clause 12.11.15) and the bit-string production is closed, but
/// `BACnetObjectType` values run to 1023, so decode tolerates the enumeration's
/// full 10-bit range instead of discarding a nonconformant peer's extra bits —
/// hence the 16-word array, which is allocation-free and `Copy`. The public API
/// still yields named
/// [`ObjectType`] values (reusing that table rather than duplicating it), and
/// `Debug` prints names, never raw numbers.
#[derive(Clone, Copy, PartialEq, Eq, Hash, Default)]
pub struct ObjectTypesSupported {
    bits: [u64; 16],
}

impl ObjectTypesSupported {
    /// Decode a bit-string payload (the `data` of `PropertyValue::BitString`)
    /// into the bitset of object types it advertises.
    pub fn from_bacnet(data: &[u8]) -> Self {
        let mut bits = [0u64; 16];
        for (i, &byte) in data.iter().take(128).enumerate() {
            bits[i / 8] |= (byte.reverse_bits() as u64) << ((i % 8) * 8);
        }
        Self { bits }
    }

    /// Whether `object_type` is marked supported.
    pub fn contains(&self, object_type: ObjectType) -> bool {
        let n = object_type.to_raw();
        n < 1024 && self.bits[(n / 64) as usize] & (1u64 << (n % 64)) != 0
    }

    /// Iterate the supported object types in ascending bit order.
    pub fn iter(&self) -> impl Iterator<Item = ObjectType> + '_ {
        (0..1024u32)
            .filter(move |&n| self.bits[(n / 64) as usize] & (1u64 << (n % 64)) != 0)
            .map(ObjectType::from_raw)
    }
}

impl core::fmt::Display for ObjectTypesSupported {
    fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
        write_joined(f, self.iter())
    }
}

impl core::fmt::Debug for ObjectTypesSupported {
    fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
        core::fmt::Display::fmt(self, f)
    }
}

/// `BACnetAuditOperationFlags` — a bit string selecting audit operations
/// (Clause 21).
///
/// Bit positions 0 through 15 have the same meanings as the named
/// [`AuditOperation`] values. Positions 16 through 31 are currently undefined
/// and reserved for ASHRAE, so they must remain zero. Positions 32 through 63
/// are retained for proprietary operations.
#[derive(Clone, Copy, PartialEq, Eq, Hash, Default)]
pub struct AuditOperationFlags {
    bits: u64,
}

impl AuditOperationFlags {
    const RESERVED_BITS: u64 = 0x0000_0000_ffff_0000;

    /// An empty set of audit operations.
    #[inline]
    pub const fn empty() -> Self {
        Self { bits: 0 }
    }

    /// Construct from the bit-position-preserving representation.
    ///
    /// Currently undefined ASHRAE-reserved positions 16 through 31 are
    /// rejected rather than masked.
    #[inline]
    pub fn from_bits(bits: u64) -> Result<Self> {
        if bits & Self::RESERVED_BITS != 0 {
            return Err(Error::OutOfRange(
                "BACnetAuditOperationFlags: reserved bits 16-31 must be zero".into(),
            ));
        }
        Ok(Self { bits })
    }

    /// Return the complete bit-position-preserving representation.
    #[inline]
    pub const fn bits(self) -> u64 {
        self.bits
    }

    /// Whether no audit operation bit is set.
    #[inline]
    pub const fn is_empty(self) -> bool {
        self.bits == 0
    }

    /// Whether `operation` is selected.
    ///
    /// Raw [`AuditOperation`] values above 63 are outside the production's
    /// representable range and therefore return `false`.
    #[inline]
    pub const fn contains(self, operation: AuditOperation) -> bool {
        let bit = operation.to_raw();
        Self::valid_bit(bit) && self.bits & (1u64 << bit) != 0
    }

    /// Select `operation`, returning `false` without mutation when its raw
    /// value is outside the representable range 0 through 63.
    #[inline]
    pub fn insert(&mut self, operation: AuditOperation) -> bool {
        let bit = operation.to_raw();
        if !Self::valid_bit(bit) {
            return false;
        }
        self.bits |= 1u64 << bit;
        true
    }

    /// Clear `operation`, returning `false` when its raw value is outside the
    /// representable range 0 through 63.
    #[inline]
    pub fn remove(&mut self, operation: AuditOperation) -> bool {
        let bit = operation.to_raw();
        if !Self::valid_bit(bit) {
            return false;
        }
        self.bits &= !(1u64 << bit);
        true
    }

    /// Iterate selected operations in ascending bit-position order, including
    /// proprietary raw values.
    pub fn iter(self) -> impl Iterator<Item = AuditOperation> {
        let bits = self.bits;
        (0..64)
            .filter(move |&bit| bits & (1u64 << bit) != 0)
            .map(AuditOperation::from_raw)
    }

    const fn valid_bit(bit: u32) -> bool {
        bit <= 15 || (bit >= 32 && bit <= 63)
    }

    /// Decode a Clause 20.2.10 BIT STRING payload.
    ///
    /// `data` excludes the initial unused-bits octet. The decoder rejects
    /// values wider than the 64 positions allowed by
    /// `BACnetAuditOperationFlags`, invalid unused-bit counts, and nonzero
    /// padding in the final octet.
    pub fn from_bacnet(unused_bits: u8, data: &[u8]) -> Result<Self> {
        if unused_bits > 7 {
            return Err(Error::decoding(
                0,
                format!("BACnetAuditOperationFlags: unused_bits must be 0-7, got {unused_bits}"),
            ));
        }
        if data.is_empty() {
            if unused_bits != 0 {
                return Err(Error::decoding(
                    0,
                    "BACnetAuditOperationFlags: an empty bit string must have zero unused bits",
                ));
            }
            return Ok(Self::empty());
        }

        let meaningful_bits = data.len() * 8 - usize::from(unused_bits);
        if meaningful_bits > 64 {
            return Err(Error::decoding(
                0,
                format!(
                    "BACnetAuditOperationFlags: {meaningful_bits} bits exceeds the 64-bit limit"
                ),
            ));
        }
        if unused_bits != 0 {
            let padding_mask = (1u8 << unused_bits) - 1;
            if data.last().is_some_and(|last| last & padding_mask != 0) {
                return Err(Error::decoding(
                    0,
                    "BACnetAuditOperationFlags: final-octet padding bits must be zero",
                ));
            }
        }

        let mut bits = 0u64;
        for (index, &byte) in data.iter().enumerate() {
            bits |= u64::from(byte.reverse_bits()) << (index * 8);
        }
        if bits & Self::RESERVED_BITS != 0 {
            return Err(Error::decoding(
                0,
                "BACnetAuditOperationFlags: reserved bits 16-31 must be zero",
            ));
        }
        Ok(Self { bits })
    }

    /// Encode as a shortest-form Clause 20.2.10 BIT STRING payload.
    ///
    /// The returned `data` excludes the initial unused-bits octet. Bit 0 is
    /// placed in the most-significant position of the first data octet.
    pub fn to_bacnet(self) -> (u8, Vec<u8>) {
        if self.bits == 0 {
            return (0, Vec::new());
        }

        let bit_len = (u64::BITS - self.bits.leading_zeros()) as usize;
        let byte_len = bit_len.div_ceil(8);
        let unused_bits = (byte_len * 8 - bit_len) as u8;
        let mut data = Vec::with_capacity(byte_len);
        for index in 0..byte_len {
            data.push(((self.bits >> (index * 8)) as u8).reverse_bits());
        }
        (unused_bits, data)
    }
}

impl core::fmt::Display for AuditOperationFlags {
    fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
        write_joined(f, self.iter())
    }
}

impl core::fmt::Debug for AuditOperationFlags {
    fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
        core::fmt::Display::fmt(self, f)
    }
}

#[cfg(test)]
mod tests {
    use super::*;
    use crate::primitives::StatusFlags;

    #[test]
    fn pack_unpack_octet_spec_vectors() {
        // Clause 20.2.10: first defined bit -> MSB. Deliberately asymmetric
        // values — round trips alone cannot catch a symmetric encode/decode
        // inversion, only literal wire bytes can.
        assert_eq!(pack_octet(0b001), 0x80); // TO_OFFNORMAL / monday
        assert_eq!(pack_octet(0b100), 0x20); // TO_NORMAL
        assert_eq!(pack_octet(0b0100_0000), 0x02); // sunday (7-bit valid_days)
        assert_eq!(unpack_octet(&[0x80], 3), 0b001);
        assert_eq!(unpack_octet(&[0x20], 3), 0b100);
        assert_eq!(unpack_octet(&[0xFE], 7), 0x7F); // all seven days
        assert_eq!(unpack_octet(&[], 3), 0);
        // Nonconformant junk past the defined bits is masked off.
        assert_eq!(unpack_octet(&[0xFF], 3), 0b111);
        // The octet helpers and the typed codecs agree.
        assert_eq!(
            EventTransitionBits::from_bacnet(&[pack_octet(0b001)]),
            EventTransitionBits::TO_OFFNORMAL
        );
        assert_eq!(EventTransitionBits::TO_OFFNORMAL.to_bacnet(), 0x80);
        assert_eq!(LimitEnable::LOW_LIMIT_ENABLE.to_bacnet(), 0x80);
    }

    #[test]
    fn event_transition_bits_msb_first() {
        // Wire byte 0b1010_0000 => bit0 (to-offnormal) and bit2 (to-normal).
        let bits = EventTransitionBits::from_bacnet(&[0b1010_0000]);
        assert_eq!(
            bits,
            EventTransitionBits::TO_OFFNORMAL | EventTransitionBits::TO_NORMAL
        );
        assert_eq!(bits.to_string(), "TO_OFFNORMAL | TO_NORMAL");
    }

    #[test]
    fn limit_enable_msb_first() {
        assert_eq!(
            LimitEnable::from_bacnet(&[0x80]),
            LimitEnable::LOW_LIMIT_ENABLE
        );
        assert_eq!(
            LimitEnable::from_bacnet(&[0xC0]),
            LimitEnable::LOW_LIMIT_ENABLE | LimitEnable::HIGH_LIMIT_ENABLE
        );
        assert_eq!(LimitEnable::from_bacnet(&[]).to_string(), "()");
    }

    #[test]
    fn object_types_supported_matches_device_layout() {
        // Byte 8 = 0x80 sets bit 64 => ColorTemperature (per device object test).
        let mut data = vec![0u8; 9];
        data[8] = 0x80;
        let ots = ObjectTypesSupported::from_bacnet(&data);
        assert!(ots.contains(ObjectType::COLOR_TEMPERATURE));
        assert!(!ots.contains(ObjectType::ANALOG_INPUT));
        assert_eq!(
            ots.iter().collect::<Vec<_>>(),
            vec![ObjectType::COLOR_TEMPERATURE]
        );

        // Byte 4 = 0xFD: types 32-37,39 set, 38 (NetworkSecurity) unset.
        let ss = ObjectTypesSupported::from_bacnet(&[0, 0, 0, 0, 0xFD]);
        assert!(ss.contains(ObjectType::from_raw(32)));
        assert!(!ss.contains(ObjectType::NETWORK_SECURITY)); // type 38
        assert!(ss.contains(ObjectType::from_raw(39)));
    }

    #[test]
    fn resolved_value_holds_names_not_bytes() {
        // The whole point: the decoded value contains named variants, and even
        // its Debug shows names rather than the raw wire bytes.
        let ots = ObjectTypesSupported::from_bacnet(&[0b1110_0000]); // types 0,1,2
        assert_eq!(
            ots.iter().collect::<Vec<_>>(),
            [
                ObjectType::ANALOG_INPUT,
                ObjectType::ANALOG_OUTPUT,
                ObjectType::ANALOG_VALUE
            ]
        );
        let dbg = format!("{ots:?}");
        assert!(
            dbg.contains("ANALOG_INPUT"),
            "Debug should name types: {dbg}"
        );
        assert!(
            !dbg.contains("224"),
            "Debug must not show raw byte 0xE0: {dbg}"
        );
    }

    #[test]
    fn services_supported_follows_clause_21() {
        // Arbitrary decode vector (historically the device's pre-#192
        // hardcoded constant — NOT the property the Device object serves).
        let ss = ServicesSupported::from_bacnet(&[0xA4, 0x0B, 0x80, 0x35, 0x80, 0x00]);
        assert!(ss.contains(ServiceSupported::ACKNOWLEDGE_ALARM)); // bit 0
        assert!(ss.contains(ServiceSupported::SUBSCRIBE_COV)); // bit 5
        assert!(ss.contains(ServiceSupported::READ_PROPERTY)); // bit 12
        assert!(ss.contains(ServiceSupported::I_AM)); // bit 26
        assert!(ss.contains(ServiceSupported::TIME_SYNCHRONIZATION)); // bit 32 (NOT who-is)
        assert!(!ss.contains(ServiceSupported::WHO_IS)); // bit 34
    }

    #[test]
    fn status_flags_render_names_not_numbers() {
        assert_eq!(
            (StatusFlags::IN_ALARM | StatusFlags::FAULT).to_string(),
            "IN_ALARM | FAULT"
        );
        // Empty renders as "()", and Debug matches Display — never a raw number.
        assert_eq!(StatusFlags::empty().to_string(), "()");
        assert_eq!(format!("{:?}", StatusFlags::empty()), "()");
        assert_eq!(format!("{:?}", StatusFlags::IN_ALARM), "IN_ALARM");
    }

    #[test]
    fn small_types_debug_is_named_never_a_number() {
        // bitflags' derived Debug prints 0x0 for an empty set; ours prints "()".
        assert_eq!(format!("{:?}", EventTransitionBits::empty()), "()");
        assert_eq!(format!("{:?}", LimitEnable::empty()), "()");
        assert_eq!(format!("{:?}", EventTransitionBits::TO_FAULT), "TO_FAULT");
    }

    #[test]
    fn audit_operation_flags_empty_and_named_bits_use_msb_first_wire_order() {
        let empty = AuditOperationFlags::empty();
        assert!(empty.is_empty());
        assert_eq!(empty.to_bacnet(), (0, Vec::new()));
        assert_eq!(AuditOperationFlags::from_bacnet(0, &[]).unwrap(), empty);

        let mut flags = AuditOperationFlags::empty();
        assert!(flags.insert(AuditOperation::READ));
        assert!(flags.insert(AuditOperation::GENERAL));
        assert!(flags.contains(AuditOperation::READ));
        assert!(flags.contains(AuditOperation::GENERAL));
        assert_eq!(flags.bits(), (1 << 0) | (1 << 15));
        assert_eq!(flags.to_bacnet(), (0, vec![0x80, 0x01]));
        assert_eq!(
            AuditOperationFlags::from_bacnet(0, &[0x80, 0x01]).unwrap(),
            flags
        );
        assert_eq!(flags.to_string(), "READ | GENERAL");
    }

    #[test]
    fn audit_operation_flags_preserve_proprietary_bit_63() {
        let proprietary_32 = AuditOperation::from_raw(32);
        let proprietary = AuditOperation::from_raw(63);
        let mut flags = AuditOperationFlags::from_bits(1u64 << 63).unwrap();
        assert!(flags.insert(proprietary_32));
        assert!(flags.contains(proprietary_32));
        assert!(flags.contains(proprietary));
        assert_eq!(flags.to_bacnet(), (0, vec![0, 0, 0, 0, 0x80, 0, 0, 1]));
        assert_eq!(
            AuditOperationFlags::from_bacnet(0, &[0, 0, 0, 0, 0x80, 0, 0, 1]).unwrap(),
            flags
        );
        assert!(!flags.insert(AuditOperation::from_raw(64)));
        assert!(!flags.contains(AuditOperation::from_raw(64)));
        assert!(flags.remove(proprietary_32));
        assert!(flags.remove(proprietary));
        assert!(flags.is_empty());
    }

    #[test]
    fn audit_operation_flags_reject_overlong_and_nonzero_padding() {
        assert!(AuditOperationFlags::from_bacnet(7, &[0; 9]).is_err());
        assert!(AuditOperationFlags::from_bacnet(1, &[0x01]).is_err());
        assert!(AuditOperationFlags::from_bacnet(1, &[]).is_err());
        assert!(AuditOperationFlags::from_bacnet(8, &[0]).is_err());
        assert!(AuditOperationFlags::from_bits(1 << 16).is_err());
        assert!(AuditOperationFlags::from_bits(1 << 31).is_err());
        assert!(AuditOperationFlags::from_bacnet(7, &[0, 0, 0x80]).is_err());

        let mut flags = AuditOperationFlags::empty();
        assert!(!flags.insert(AuditOperation::from_raw(16)));
        assert!(!flags.insert(AuditOperation::from_raw(31)));
        assert!(flags.is_empty());

        // Zero padding is accepted; seven unused bits leaves only bit 0.
        assert_eq!(
            AuditOperationFlags::from_bacnet(7, &[0x80]).unwrap(),
            AuditOperationFlags::from_bits(1).unwrap()
        );
    }
}