#[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 days_of_week::DaysOfWeek;
pub use priority_filter::BACnetPriorityFilter;
mod priority_filter;
fn wire_bit(data: &[u8], n: usize) -> bool {
let mask = 0x80u8 >> (n % 8);
data.get(n / 8).is_some_and(|b| b & mask != 0)
}
fn pack_octet(bits_lsb0: u8) -> u8 {
bits_lsb0.reverse_bits()
}
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
}
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
}
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)
}
}
};
}
mod days_of_week;
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! {
#[derive(Clone, Copy, PartialEq, Eq, Hash, Default)]
pub struct EventTransitionBits: u8 {
const TO_OFFNORMAL = 1 << 0;
const TO_FAULT = 1 << 1;
const TO_NORMAL = 1 << 2;
}
}
impl EventTransitionBits {
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
}
pub fn to_bacnet(self) -> u8 {
pack_octet(self.bits())
}
}
impl_named_bit_display!(EventTransitionBits);
bitflags::bitflags! {
#[derive(Clone, Copy, PartialEq, Eq, Hash, Default)]
pub struct LimitEnable: u8 {
const LOW_LIMIT_ENABLE = 1 << 0;
const HIGH_LIMIT_ENABLE = 1 << 1;
}
}
impl LimitEnable {
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
}
pub fn to_bacnet(self) -> u8 {
pack_octet(self.bits())
}
}
impl_named_bit_display!(LimitEnable);
bitflags::bitflags! {
#[derive(Clone, Copy, PartialEq, Eq, Hash, Default)]
pub struct LogStatus: u8 {
const LOG_DISABLED = 1 << 0;
const BUFFER_PURGED = 1 << 1;
const LOG_INTERRUPTED = 1 << 2;
}
}
impl LogStatus {
pub fn from_bacnet(data: &[u8]) -> Self {
Self::from_bits_truncate(unpack_octet(data, 3))
}
pub fn to_bacnet(self) -> u8 {
pack_octet(self.bits() & Self::all().bits())
}
}
impl_named_bit_display!(LogStatus);
#[derive(Clone, Copy, PartialEq, Eq, Hash, Default)]
pub struct ServicesSupported {
bits: u64,
}
impl ServicesSupported {
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 }
}
pub fn contains(&self, service: ServiceSupported) -> bool {
let n = service.to_raw() as u32;
n < 64 && self.bits & (1u64 << n) != 0
}
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)
}
}
#[derive(Clone, Copy, PartialEq, Eq, Hash, Default)]
pub struct ObjectTypesSupported {
bits: [u64; 16],
}
impl ObjectTypesSupported {
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 }
}
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
}
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)
}
}
#[derive(Clone, Copy, PartialEq, Eq, Hash, Default)]
pub struct AuditOperationFlags {
bits: u64,
}
impl AuditOperationFlags {
const RESERVED_BITS: u64 = 0x0000_0000_ffff_0000;
#[inline]
pub const fn empty() -> Self {
Self { bits: 0 }
}
#[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 })
}
#[inline]
pub const fn bits(self) -> u64 {
self.bits
}
#[inline]
pub const fn is_empty(self) -> bool {
self.bits == 0
}
#[inline]
pub const fn contains(self, operation: AuditOperation) -> bool {
let bit = operation.to_raw();
Self::valid_bit(bit) && self.bits & (1u64 << bit) != 0
}
#[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
}
#[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
}
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)
}
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 })
}
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() {
assert_eq!(pack_octet(0b001), 0x80); assert_eq!(pack_octet(0b100), 0x20); assert_eq!(pack_octet(0b0100_0000), 0x02); assert_eq!(unpack_octet(&[0x80], 3), 0b001);
assert_eq!(unpack_octet(&[0x20], 3), 0b100);
assert_eq!(unpack_octet(&[0xFE], 7), 0x7F); assert_eq!(unpack_octet(&[], 3), 0);
assert_eq!(unpack_octet(&[0xFF], 3), 0b111);
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 log_status_follows_the_bit0_first_wire_order() {
for (status, octet) in [
(LogStatus::LOG_DISABLED, 0x80),
(LogStatus::BUFFER_PURGED, 0x40),
(LogStatus::LOG_INTERRUPTED, 0x20),
(LogStatus::LOG_DISABLED | LogStatus::BUFFER_PURGED, 0xC0),
(LogStatus::empty(), 0x00),
] {
assert_eq!(status.to_bacnet(), octet, "{status}");
assert_eq!(LogStatus::from_bacnet(&[octet]), status);
}
assert_eq!(LogStatus::from_bits_retain(0xF9).to_bacnet(), 0x80);
assert_eq!(LogStatus::from_bacnet(&[0x9F]), LogStatus::LOG_DISABLED);
assert_eq!(LogStatus::from_bacnet(&[]), LogStatus::empty());
assert_eq!(
format!("{}", LogStatus::LOG_DISABLED | LogStatus::LOG_INTERRUPTED),
"LOG_DISABLED | LOG_INTERRUPTED"
);
}
#[test]
fn event_transition_bits_msb_first() {
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() {
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]
);
let ss = ObjectTypesSupported::from_bacnet(&[0, 0, 0, 0, 0xFD]);
assert!(ss.contains(ObjectType::from_raw(32)));
assert!(!ss.contains(ObjectType::NETWORK_SECURITY)); assert!(ss.contains(ObjectType::from_raw(39)));
}
#[test]
fn resolved_value_holds_names_not_bytes() {
let ots = ObjectTypesSupported::from_bacnet(&[0b1110_0000]); 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() {
let ss = ServicesSupported::from_bacnet(&[0xA4, 0x0B, 0x80, 0x35, 0x80, 0x00]);
assert!(ss.contains(ServiceSupported::ACKNOWLEDGE_ALARM)); assert!(ss.contains(ServiceSupported::SUBSCRIBE_COV)); assert!(ss.contains(ServiceSupported::READ_PROPERTY)); assert!(ss.contains(ServiceSupported::I_AM)); assert!(ss.contains(ServiceSupported::TIME_SYNCHRONIZATION)); assert!(!ss.contains(ServiceSupported::WHO_IS)); }
#[test]
fn status_flags_render_names_not_numbers() {
assert_eq!(
(StatusFlags::IN_ALARM | StatusFlags::FAULT).to_string(),
"IN_ALARM | FAULT"
);
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() {
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());
assert_eq!(
AuditOperationFlags::from_bacnet(7, &[0x80]).unwrap(),
AuditOperationFlags::from_bits(1).unwrap()
);
}
}