#[cfg(not(feature = "std"))]
use alloc::{string::String, vec::Vec};
use crate::bitstring::{EventTransitionBits, LimitEnable, ObjectTypesSupported, ServicesSupported};
use crate::primitives::{Date, ObjectIdentifier, PropertyValue, StatusFlags, Time};
use super::*;
macro_rules! resolved_enum {
($($variant:ident($ty:ty)),+ $(,)?) => {
#[derive(Clone, Copy, PartialEq, Eq, Debug)]
pub enum ResolvedEnum {
$(
#[doc = concat!("A [`", stringify!($ty), "`] value.")]
$variant($ty),
)+
Unknown(u32),
}
impl core::fmt::Display for ResolvedEnum {
fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
match self {
$( Self::$variant(v) => core::fmt::Display::fmt(v, f), )+
Self::Unknown(n) => write!(f, "{n}"),
}
}
}
};
}
resolved_enum! {
ObjectType(ObjectType),
EventState(EventState),
EventType(EventType),
FaultType(FaultType),
NotifyType(NotifyType),
Reliability(Reliability),
DeviceStatus(DeviceStatus),
Segmentation(Segmentation),
EngineeringUnits(EngineeringUnits),
Polarity(Polarity),
BinaryPV(BinaryPV),
ProgramState(ProgramState),
ProgramChange(ProgramChange),
ProgramError(ProgramError),
NodeType(NodeType),
LoggingType(LoggingType),
FileAccessMethod(FileAccessMethod),
BackupAndRestoreState(BackupAndRestoreState),
RestartReason(RestartReason),
LifeSafetyMode(LifeSafetyMode),
LifeSafetyState(LifeSafetyState),
LifeSafetyOperation(LifeSafetyOperation),
SilencedState(SilencedState),
Maintenance(Maintenance),
DoorStatus(DoorStatus),
DoorAlarmState(DoorAlarmState),
DoorSecuredStatus(DoorSecuredStatus),
LockStatus(LockStatus),
AccessEvent(AccessEvent),
AuthorizationMode(AuthorizationMode),
AccessPassbackMode(AccessPassbackMode),
AccessUserType(AccessUserType),
AccessCredentialDisable(AccessCredentialDisable),
AccessCredentialDisableReason(AccessCredentialDisableReason),
AuthenticationStatus(AuthenticationStatus),
AuthorizationExemption(AuthorizationExemption),
AccessZoneOccupancyState(AccessZoneOccupancyState),
Relationship(Relationship),
NetworkType(NetworkType),
NetworkNumberQuality(NetworkNumberQuality),
IPMode(IPMode),
NetworkPortCommand(NetworkPortCommand),
ProtocolLevel(ProtocolLevel),
LightingInProgress(LightingInProgress),
LightingTransition(LightingTransition),
TimerState(TimerState),
TimerTransition(TimerTransition),
WriteStatus(WriteStatus),
LiftCarMode(LiftCarMode),
LiftGroupMode(LiftGroupMode),
EscalatorMode(EscalatorMode),
EscalatorOperationDirection(EscalatorOperationDirection),
LiftCarDriveStatus(LiftCarDriveStatus),
LiftCarDirection(LiftCarDirection),
LiftCarDoorCommand(LiftCarDoorCommand),
AuditLevel(AuditLevel),
}
impl ResolvedEnum {
pub fn from_property(property: PropertyIdentifier, value: u32) -> Self {
match property.to_raw() {
79 => Self::ObjectType(ObjectType::from_raw(value)), 36 => Self::EventState(EventState::from_raw(value)), 37 => Self::EventType(EventType::from_raw(value)), 359 => Self::FaultType(FaultType::from_raw(value)), 72 => Self::NotifyType(NotifyType::from_raw(value)), 103 => Self::Reliability(Reliability::from_raw(value)), 112 => Self::DeviceStatus(DeviceStatus::from_raw(value)), 107 if value <= u8::MAX as u32 => {
Self::Segmentation(Segmentation::from_raw(value as u8))
}
20 | 27 | 50 | 82 | 94 | 117 | 455 => {
Self::EngineeringUnits(EngineeringUnits::from_raw(value))
}
84 => Self::Polarity(Polarity::from_raw(value)), 6 => Self::BinaryPV(BinaryPV::from_raw(value)),
92 => Self::ProgramState(ProgramState::from_raw(value)), 90 => Self::ProgramChange(ProgramChange::from_raw(value)), 100 => Self::ProgramError(ProgramError::from_raw(value)), 208 => Self::NodeType(NodeType::from_raw(value)), 197 => Self::LoggingType(LoggingType::from_raw(value)), 41 => Self::FileAccessMethod(FileAccessMethod::from_raw(value)), 338 => Self::BackupAndRestoreState(BackupAndRestoreState::from_raw(value)), 196 => Self::RestartReason(RestartReason::from_raw(value)),
160 | 175 => Self::LifeSafetyMode(LifeSafetyMode::from_raw(value)), 164 => Self::LifeSafetyState(LifeSafetyState::from_raw(value)),
161 => Self::LifeSafetyOperation(LifeSafetyOperation::from_raw(value)), 163 => Self::SilencedState(SilencedState::from_raw(value)), 158 => Self::Maintenance(Maintenance::from_raw(value)),
231 => Self::DoorStatus(DoorStatus::from_raw(value)), 226 => Self::DoorAlarmState(DoorAlarmState::from_raw(value)), 233 => Self::LockStatus(LockStatus::from_raw(value)), 235 => Self::DoorSecuredStatus(DoorSecuredStatus::from_raw(value)),
247 | 275 => Self::AccessEvent(AccessEvent::from_raw(value)), 261 => Self::AuthorizationMode(AuthorizationMode::from_raw(value)), 300 => Self::AccessPassbackMode(AccessPassbackMode::from_raw(value)), 318 => Self::AccessUserType(AccessUserType::from_raw(value)), 263 => Self::AccessCredentialDisable(AccessCredentialDisable::from_raw(value)), 303 => {
Self::AccessCredentialDisableReason(AccessCredentialDisableReason::from_raw(value))
} 260 => Self::AuthenticationStatus(AuthenticationStatus::from_raw(value)), 296 => Self::AccessZoneOccupancyState(AccessZoneOccupancyState::from_raw(value)), 364 => Self::AuthorizationExemption(AuthorizationExemption::from_raw(value)),
427 => Self::NetworkType(NetworkType::from_raw(value)), 426 => Self::NetworkNumberQuality(NetworkNumberQuality::from_raw(value)), 408 | 435 => Self::IPMode(IPMode::from_raw(value)), 417 => Self::NetworkPortCommand(NetworkPortCommand::from_raw(value)), 482 => Self::ProtocolLevel(ProtocolLevel::from_raw(value)),
489 | 490 => Self::Relationship(Relationship::from_raw(value)),
378 => Self::LightingInProgress(LightingInProgress::from_raw(value)), 385 => Self::LightingTransition(LightingTransition::from_raw(value)), 398 => Self::TimerState(TimerState::from_raw(value)), 395 => Self::TimerTransition(TimerTransition::from_raw(value)), 370 => Self::WriteStatus(WriteStatus::from_raw(value)),
456 => Self::LiftCarMode(LiftCarMode::from_raw(value)), 467 => Self::LiftGroupMode(LiftGroupMode::from_raw(value)), 462 => Self::EscalatorMode(EscalatorMode::from_raw(value)), 477 => Self::EscalatorOperationDirection(EscalatorOperationDirection::from_raw(value)), 453 => Self::LiftCarDriveStatus(LiftCarDriveStatus::from_raw(value)), 448 | 457 => Self::LiftCarDirection(LiftCarDirection::from_raw(value)), 449 => Self::LiftCarDoorCommand(LiftCarDoorCommand::from_raw(value)), 450 => Self::DoorStatus(DoorStatus::from_raw(value)),
498 => Self::AuditLevel(AuditLevel::from_raw(value)),
_ => Self::Unknown(value),
}
}
}
pub fn resolve_enum(property: PropertyIdentifier, value: &PropertyValue) -> Option<ResolvedEnum> {
match value {
PropertyValue::Enumerated(n) => Some(ResolvedEnum::from_property(property, *n)),
_ => None,
}
}
#[derive(Clone, PartialEq, Eq, Debug)]
pub enum ResolvedBits {
StatusFlags(StatusFlags),
EventTransitionBits(EventTransitionBits),
LimitEnable(LimitEnable),
ServicesSupported(ServicesSupported),
ObjectTypesSupported(ObjectTypesSupported),
Unknown {
unused_bits: u8,
data: Vec<u8>,
},
}
impl ResolvedBits {
pub fn from_property(property: PropertyIdentifier, unused_bits: u8, data: &[u8]) -> Self {
match property.to_raw() {
111 | 347 => Self::StatusFlags(crate::bitstring::status_flags_from_bacnet(data)),
0 | 1 | 35 => Self::EventTransitionBits(EventTransitionBits::from_bacnet(data)),
52 => Self::LimitEnable(LimitEnable::from_bacnet(data)),
97 => Self::ServicesSupported(ServicesSupported::from_bacnet(data)),
96 => Self::ObjectTypesSupported(ObjectTypesSupported::from_bacnet(data)),
_ => Self::Unknown {
unused_bits,
data: data.to_vec(),
},
}
}
}
impl core::fmt::Display for ResolvedBits {
fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
match self {
Self::StatusFlags(v) => core::fmt::Display::fmt(v, f),
Self::EventTransitionBits(v) => core::fmt::Display::fmt(v, f),
Self::LimitEnable(v) => core::fmt::Display::fmt(v, f),
Self::ServicesSupported(v) => core::fmt::Display::fmt(v, f),
Self::ObjectTypesSupported(v) => core::fmt::Display::fmt(v, f),
Self::Unknown { data, .. } => {
f.write_str("0x")?;
for byte in data {
write!(f, "{byte:02X}")?;
}
Ok(())
}
}
}
}
pub fn resolve_bits(property: PropertyIdentifier, value: &PropertyValue) -> Option<ResolvedBits> {
match value {
PropertyValue::BitString { unused_bits, data } => {
Some(ResolvedBits::from_property(property, *unused_bits, data))
}
_ => None,
}
}
#[derive(Debug, Clone, PartialEq)]
pub enum ResolvedValue {
Null,
Boolean(bool),
Unsigned(u64),
Signed(i32),
Real(f32),
Double(f64),
OctetString(Vec<u8>),
CharacterString(String),
BitString(ResolvedBits),
Enumerated(ResolvedEnum),
Date(Date),
Time(Time),
ObjectIdentifier(ObjectIdentifier),
List(Vec<ResolvedValue>),
ApplicationData(Vec<u8>),
}
pub fn resolve_value(property: PropertyIdentifier, value: PropertyValue) -> ResolvedValue {
match value {
PropertyValue::Null => ResolvedValue::Null,
PropertyValue::Boolean(v) => ResolvedValue::Boolean(v),
PropertyValue::Unsigned(v) => ResolvedValue::Unsigned(v),
PropertyValue::Signed(v) => ResolvedValue::Signed(v),
PropertyValue::Real(v) => ResolvedValue::Real(v),
PropertyValue::Double(v) => ResolvedValue::Double(v),
PropertyValue::OctetString(v) => ResolvedValue::OctetString(v),
PropertyValue::CharacterString(v) => ResolvedValue::CharacterString(v),
PropertyValue::BitString { unused_bits, data } => {
ResolvedValue::BitString(ResolvedBits::from_property(property, unused_bits, &data))
}
PropertyValue::Enumerated(n) => {
ResolvedValue::Enumerated(ResolvedEnum::from_property(property, n))
}
PropertyValue::Date(v) => ResolvedValue::Date(v),
PropertyValue::Time(v) => ResolvedValue::Time(v),
PropertyValue::ObjectIdentifier(v) => ResolvedValue::ObjectIdentifier(v),
PropertyValue::List(values) => ResolvedValue::List(
values
.into_iter()
.map(|v| resolve_value(property, v))
.collect(),
),
PropertyValue::ApplicationData(bytes) => ResolvedValue::ApplicationData(bytes),
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn resolves_object_type() {
let r = ResolvedEnum::from_property(PropertyIdentifier::OBJECT_TYPE, 19);
assert_eq!(r, ResolvedEnum::ObjectType(ObjectType::MULTI_STATE_VALUE));
assert_eq!(r.to_string(), "MULTI_STATE_VALUE");
}
#[test]
fn resolves_units_across_aliases() {
for prop in [
PropertyIdentifier::UNITS,
PropertyIdentifier::OUTPUT_UNITS,
PropertyIdentifier::CAR_LOAD_UNITS,
] {
assert!(matches!(
ResolvedEnum::from_property(prop, 0),
ResolvedEnum::EngineeringUnits(_)
));
}
}
#[test]
fn unmapped_property_is_unknown() {
let r = ResolvedEnum::from_property(PropertyIdentifier::PRESENT_VALUE, 7);
assert_eq!(r, ResolvedEnum::Unknown(7));
assert_eq!(r.to_string(), "7");
}
#[test]
fn alarm_value_resolves_to_binary_pv() {
assert_eq!(
ResolvedEnum::from_property(PropertyIdentifier::ALARM_VALUE, 1),
ResolvedEnum::BinaryPV(BinaryPV::ACTIVE),
);
}
#[test]
fn plural_alarm_and_fault_values_remain_unknown() {
assert_eq!(
ResolvedEnum::from_property(PropertyIdentifier::ALARM_VALUES, 1),
ResolvedEnum::Unknown(1)
);
assert_eq!(
ResolvedEnum::from_property(PropertyIdentifier::FAULT_VALUES, 1),
ResolvedEnum::Unknown(1)
);
}
#[test]
fn vendor_object_type_passes_through() {
let r = ResolvedEnum::from_property(PropertyIdentifier::OBJECT_TYPE, 128);
assert_eq!(r, ResolvedEnum::ObjectType(ObjectType::from_raw(128)));
assert_eq!(r.to_string(), "128");
}
#[test]
fn resolve_value_promotes_enum_and_keeps_rest() {
assert_eq!(
resolve_value(
PropertyIdentifier::EVENT_STATE,
PropertyValue::Enumerated(0)
),
ResolvedValue::Enumerated(ResolvedEnum::EventState(EventState::from_raw(0))),
);
assert_eq!(
resolve_value(
PropertyIdentifier::OBJECT_NAME,
PropertyValue::CharacterString("AI-1".into())
),
ResolvedValue::CharacterString("AI-1".into()),
);
}
#[test]
fn resolve_value_recurses_into_lists() {
let list = PropertyValue::List(vec![
PropertyValue::Enumerated(0),
PropertyValue::Enumerated(1),
]);
let ResolvedValue::List(items) = resolve_value(PropertyIdentifier::ACCEPTED_MODES, list)
else {
panic!("expected list");
};
assert!(items.iter().all(|i| matches!(
i,
ResolvedValue::Enumerated(ResolvedEnum::LifeSafetyMode(_))
)));
}
#[test]
fn resolves_status_flags_bitstring() {
let bits = ResolvedBits::from_property(PropertyIdentifier::STATUS_FLAGS, 4, &[0x80]);
assert_eq!(bits, ResolvedBits::StatusFlags(StatusFlags::IN_ALARM));
assert_eq!(bits.to_string(), "IN_ALARM");
}
#[test]
fn status_flags_decode_ignores_declared_length() {
for unused in [0, 4, 5] {
assert_eq!(
ResolvedBits::from_property(PropertyIdentifier::STATUS_FLAGS, unused, &[0xC0]),
ResolvedBits::StatusFlags(StatusFlags::IN_ALARM | StatusFlags::FAULT),
);
}
}
#[test]
fn segmentation_out_of_range_stays_unknown() {
assert_eq!(
ResolvedEnum::from_property(PropertyIdentifier::SEGMENTATION_SUPPORTED, 259),
ResolvedEnum::Unknown(259),
);
assert_eq!(
ResolvedEnum::from_property(PropertyIdentifier::SEGMENTATION_SUPPORTED, 3),
ResolvedEnum::Segmentation(Segmentation::NONE),
);
}
#[test]
fn car_door_status_resolves_to_door_status() {
let r = ResolvedEnum::from_property(PropertyIdentifier::CAR_DOOR_STATUS, 3);
assert_eq!(r, ResolvedEnum::DoorStatus(DoorStatus::DOOR_FAULT));
assert_eq!(r.to_string(), "DOOR_FAULT");
assert_eq!(
ResolvedEnum::from_property(PropertyIdentifier::CAR_DOOR_STATUS, 0),
ResolvedEnum::DoorStatus(DoorStatus::CLOSED),
);
}
#[test]
fn backup_and_restore_state_failure_states_resolve_by_name() {
let r = ResolvedEnum::from_property(PropertyIdentifier::BACKUP_AND_RESTORE_STATE, 5);
assert_eq!(
r,
ResolvedEnum::BackupAndRestoreState(BackupAndRestoreState::BACKUP_FAILURE)
);
assert_eq!(r.to_string(), "BACKUP_FAILURE");
assert_eq!(
ResolvedEnum::from_property(PropertyIdentifier::BACKUP_AND_RESTORE_STATE, 6)
.to_string(),
"RESTORE_FAILURE"
);
}
#[test]
fn resolves_limit_enable_and_event_enable() {
assert!(matches!(
ResolvedBits::from_property(PropertyIdentifier::LIMIT_ENABLE, 6, &[0xC0]),
ResolvedBits::LimitEnable(_)
));
assert!(matches!(
ResolvedBits::from_property(PropertyIdentifier::EVENT_ENABLE, 5, &[0xE0]),
ResolvedBits::EventTransitionBits(_)
));
}
#[test]
fn resolves_object_types_supported() {
let mut data = vec![0u8; 9];
data[8] = 0x80; let bits = ResolvedBits::from_property(
PropertyIdentifier::PROTOCOL_OBJECT_TYPES_SUPPORTED,
7,
&data,
);
match bits {
ResolvedBits::ObjectTypesSupported(ots) => {
assert!(ots.contains(ObjectType::COLOR_TEMPERATURE));
}
_ => panic!("expected ObjectTypesSupported"),
}
}
#[test]
fn unmapped_bitstring_is_unknown() {
let bits = ResolvedBits::from_property(PropertyIdentifier::PROPERTY_LIST, 0, &[0xAB]);
assert_eq!(
bits,
ResolvedBits::Unknown {
unused_bits: 0,
data: vec![0xAB],
}
);
assert_eq!(bits.to_string(), "0xAB");
}
#[test]
fn resolve_value_promotes_bitstring() {
let v = resolve_value(
PropertyIdentifier::STATUS_FLAGS,
PropertyValue::BitString {
unused_bits: 4,
data: vec![0x40], },
);
assert_eq!(
v,
ResolvedValue::BitString(ResolvedBits::StatusFlags(StatusFlags::FAULT)),
);
}
#[test]
fn resolve_bits_ignores_non_bitstring() {
assert!(resolve_bits(
PropertyIdentifier::STATUS_FLAGS,
&PropertyValue::Unsigned(1)
)
.is_none());
}
#[test]
fn resolve_enum_ignores_non_enumerated() {
assert!(resolve_enum(
PropertyIdentifier::OBJECT_TYPE,
&PropertyValue::Unsigned(19)
)
.is_none());
assert!(matches!(
resolve_enum(
PropertyIdentifier::EVENT_STATE,
&PropertyValue::Enumerated(0)
),
Some(ResolvedEnum::EventState(_))
));
}
}