#[cfg(not(feature = "std"))]
use alloc::{vec, vec::Vec};
use crate::error::Error;
use crate::primitives::{Date, ObjectIdentifier, Time};
use crate::MacAddr;
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct BACnetDateRange {
pub start_date: Date,
pub end_date: Date,
}
impl BACnetDateRange {
pub fn encode(&self) -> [u8; 8] {
let mut out = [0u8; 8];
out[..4].copy_from_slice(&self.start_date.encode());
out[4..].copy_from_slice(&self.end_date.encode());
out
}
pub fn decode(data: &[u8]) -> Result<Self, Error> {
if data.len() < 8 {
return Err(Error::buffer_too_short(8, data.len()));
}
Ok(Self {
start_date: Date::decode(&data[0..4])?,
end_date: Date::decode(&data[4..8])?,
})
}
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct BACnetWeekNDay {
pub month: u8,
pub week_of_month: u8,
pub day_of_week: u8,
}
impl BACnetWeekNDay {
pub const ANY: u8 = 0xFF;
pub fn encode(&self) -> [u8; 3] {
[self.month, self.week_of_month, self.day_of_week]
}
pub fn decode(data: &[u8]) -> Result<Self, Error> {
if data.len() < 3 {
return Err(Error::buffer_too_short(3, data.len()));
}
Ok(Self {
month: data[0],
week_of_month: data[1],
day_of_week: data[2],
})
}
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub enum BACnetCalendarEntry {
Date(Date),
DateRange(BACnetDateRange),
WeekNDay(BACnetWeekNDay),
}
#[derive(Debug, Clone, PartialEq)]
pub struct BACnetTimeValue {
pub time: Time,
pub value: Vec<u8>,
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub enum SpecialEventPeriod {
CalendarEntry(BACnetCalendarEntry),
CalendarReference(ObjectIdentifier),
}
#[derive(Debug, Clone, PartialEq)]
pub struct BACnetSpecialEvent {
pub period: SpecialEventPeriod,
pub list_of_time_values: Vec<BACnetTimeValue>,
pub event_priority: u8,
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct BACnetObjectPropertyReference {
pub object_identifier: ObjectIdentifier,
pub property_identifier: u32,
pub property_array_index: Option<u32>,
}
impl BACnetObjectPropertyReference {
pub fn new(object_identifier: ObjectIdentifier, property_identifier: u32) -> Self {
Self {
object_identifier,
property_identifier,
property_array_index: None,
}
}
pub fn new_indexed(
object_identifier: ObjectIdentifier,
property_identifier: u32,
array_index: u32,
) -> Self {
Self {
object_identifier,
property_identifier,
property_array_index: Some(array_index),
}
}
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct BACnetDeviceObjectPropertyReference {
pub object_identifier: ObjectIdentifier,
pub property_identifier: u32,
pub property_array_index: Option<u32>,
pub device_identifier: Option<ObjectIdentifier>,
}
impl BACnetDeviceObjectPropertyReference {
pub fn new_local(object_identifier: ObjectIdentifier, property_identifier: u32) -> Self {
Self {
object_identifier,
property_identifier,
property_array_index: None,
device_identifier: None,
}
}
pub fn new_remote(
object_identifier: ObjectIdentifier,
property_identifier: u32,
device_identifier: ObjectIdentifier,
) -> Self {
Self {
object_identifier,
property_identifier,
property_array_index: None,
device_identifier: Some(device_identifier),
}
}
pub fn with_index(mut self, array_index: u32) -> Self {
self.property_array_index = Some(array_index);
self
}
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct BACnetAddress {
pub network_number: u16,
pub mac_address: MacAddr,
}
impl BACnetAddress {
pub fn local_broadcast() -> Self {
Self {
network_number: 0,
mac_address: MacAddr::new(),
}
}
pub fn from_ip(ip_port_bytes: [u8; 6]) -> Self {
Self {
network_number: 0,
mac_address: MacAddr::from_slice(&ip_port_bytes),
}
}
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub enum BACnetRecipient {
Device(ObjectIdentifier),
Address(BACnetAddress),
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct BACnetDestination {
pub valid_days: u8,
pub from_time: Time,
pub to_time: Time,
pub recipient: BACnetRecipient,
pub process_identifier: u32,
pub issue_confirmed_notifications: bool,
pub transitions: u8,
}
#[derive(Debug, Clone, PartialEq)]
pub enum LogDatum {
LogStatus(u8),
BooleanValue(bool),
RealValue(f32),
EnumValue(u32),
UnsignedValue(u64),
SignedValue(i64),
BitstringValue {
unused_bits: u8,
data: Vec<u8>,
},
NullValue,
Failure {
error_class: u32,
error_code: u32,
},
TimeChange(f32),
AnyValue(Vec<u8>),
}
#[derive(Debug, Clone, PartialEq)]
pub struct BACnetLogRecord {
pub date: Date,
pub time: Time,
pub log_datum: LogDatum,
pub status_flags: Option<u8>,
}
#[derive(Debug, Clone, PartialEq)]
pub enum BACnetScale {
FloatScale(f32),
IntegerScale(i32),
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct BACnetPrescale {
pub multiplier: u32,
pub modulo_divide: u32,
}
#[derive(Debug, Clone, PartialEq)]
pub enum BACnetPropertyStates {
BooleanValue(bool), BinaryValue(u32), EventType(u32), Polarity(u32), ProgramChange(u32), ProgramState(u32), ReasonForHalt(u32), Reliability(u32), State(u32), SystemStatus(u32), Units(u32), UnsignedValue(u32), LifeSafetyMode(u32), LifeSafetyState(u32), DoorAlarmState(u32), Action(u32), DoorSecuredStatus(u32), DoorStatus(u32), DoorValue(u32), LiftCarDirection(u32), LiftCarDoorCommand(u32), TimerState(u32), TimerTransition(u32), Other {
tag: u8,
data: Vec<u8>,
},
}
#[derive(Debug, Clone, PartialEq)]
pub enum BACnetShedLevel {
Percent(u32),
Level(u32),
Amount(f32),
}
#[derive(Debug, Clone, PartialEq)]
pub struct BACnetLightingCommand {
pub operation: u32,
pub target_level: Option<f32>,
pub ramp_rate: Option<f32>,
pub step_increment: Option<f32>,
pub fade_time: Option<u32>,
pub priority: Option<u32>,
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct BACnetDeviceObjectReference {
pub device_identifier: Option<ObjectIdentifier>,
pub object_identifier: ObjectIdentifier,
}
#[derive(Debug, Clone, PartialEq)]
pub struct BACnetAccessRule {
pub time_range_specifier: u32,
pub time_range: Option<(Date, Time, Date, Time)>,
pub location_specifier: u32,
pub location: Option<BACnetDeviceObjectReference>,
pub enable: bool,
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct BACnetAssignedAccessRights {
pub assigned_access_rights: ObjectIdentifier,
pub enable: bool,
}
#[derive(Debug, Clone, PartialEq)]
pub struct BACnetAssignedLandingCalls {
pub floor_number: u8,
pub direction: u32,
}
#[derive(Debug, Clone, PartialEq)]
pub enum FaultParameters {
FaultNone,
FaultCharacterString { fault_values: Vec<String> },
FaultExtended {
vendor_id: u16,
extended_fault_type: u32,
parameters: Vec<u8>,
},
FaultLifeSafety {
fault_values: Vec<u32>,
mode_for_reference: BACnetDeviceObjectPropertyReference,
},
FaultState {
fault_values: Vec<BACnetPropertyStates>,
},
FaultStatusFlags {
reference: BACnetDeviceObjectPropertyReference,
},
FaultOutOfRange { min_normal: f64, max_normal: f64 },
FaultListed {
reference: BACnetDeviceObjectPropertyReference,
},
}
#[derive(Debug, Clone, PartialEq)]
pub struct BACnetRecipientProcess {
pub recipient: BACnetRecipient,
pub process_identifier: u32,
}
#[derive(Debug, Clone, PartialEq)]
pub struct BACnetCOVSubscription {
pub recipient: BACnetRecipientProcess,
pub monitored_property_reference: BACnetObjectPropertyReference,
pub issue_confirmed_notifications: bool,
pub time_remaining: u32,
pub cov_increment: Option<f32>,
}
#[derive(Debug, Clone, PartialEq)]
pub enum BACnetValueSource {
None,
Object(ObjectIdentifier),
Address(BACnetAddress),
}
#[cfg(test)]
mod tests {
use super::*;
use crate::enums::{ObjectType, PropertyIdentifier};
#[test]
fn date_range_encode_decode_round_trip() {
let range = BACnetDateRange {
start_date: Date {
year: 124,
month: 1,
day: 1,
day_of_week: 1,
},
end_date: Date {
year: 124,
month: 12,
day: 31,
day_of_week: 2,
},
};
let encoded = range.encode();
assert_eq!(encoded.len(), 8);
let decoded = BACnetDateRange::decode(&encoded).unwrap();
assert_eq!(range, decoded);
}
#[test]
fn date_range_encode_decode_all_unspecified() {
let range = BACnetDateRange {
start_date: Date {
year: Date::UNSPECIFIED,
month: Date::UNSPECIFIED,
day: Date::UNSPECIFIED,
day_of_week: Date::UNSPECIFIED,
},
end_date: Date {
year: Date::UNSPECIFIED,
month: Date::UNSPECIFIED,
day: Date::UNSPECIFIED,
day_of_week: Date::UNSPECIFIED,
},
};
let encoded = range.encode();
let decoded = BACnetDateRange::decode(&encoded).unwrap();
assert_eq!(range, decoded);
}
#[test]
fn date_range_buffer_too_short() {
let result = BACnetDateRange::decode(&[0; 7]);
assert!(result.is_err());
match result.unwrap_err() {
Error::BufferTooShort { need, have } => {
assert_eq!(need, 8);
assert_eq!(have, 7);
}
other => panic!("unexpected error: {other:?}"),
}
}
#[test]
fn date_range_buffer_empty() {
let result = BACnetDateRange::decode(&[]);
assert!(result.is_err());
}
#[test]
fn date_range_extra_bytes_ignored() {
let range = BACnetDateRange {
start_date: Date {
year: 100,
month: 6,
day: 15,
day_of_week: 5,
},
end_date: Date {
year: 100,
month: 6,
day: 30,
day_of_week: 6,
},
};
let encoded = range.encode();
let mut extended = encoded.to_vec();
extended.extend_from_slice(&[0xFF, 0xFF]); let decoded = BACnetDateRange::decode(&extended).unwrap();
assert_eq!(range, decoded);
}
#[test]
fn week_n_day_encode_decode_round_trip() {
let wnd = BACnetWeekNDay {
month: 3,
week_of_month: 2,
day_of_week: 5, };
let encoded = wnd.encode();
assert_eq!(encoded.len(), 3);
let decoded = BACnetWeekNDay::decode(&encoded).unwrap();
assert_eq!(wnd, decoded);
}
#[test]
fn week_n_day_encode_decode_all_any() {
let wnd = BACnetWeekNDay {
month: BACnetWeekNDay::ANY,
week_of_month: BACnetWeekNDay::ANY,
day_of_week: BACnetWeekNDay::ANY,
};
let encoded = wnd.encode();
assert_eq!(encoded, [0xFF, 0xFF, 0xFF]);
let decoded = BACnetWeekNDay::decode(&encoded).unwrap();
assert_eq!(wnd, decoded);
}
#[test]
fn week_n_day_buffer_too_short() {
let result = BACnetWeekNDay::decode(&[0x03, 0x02]);
assert!(result.is_err());
match result.unwrap_err() {
Error::BufferTooShort { need, have } => {
assert_eq!(need, 3);
assert_eq!(have, 2);
}
other => panic!("unexpected error: {other:?}"),
}
}
#[test]
fn week_n_day_buffer_empty() {
let result = BACnetWeekNDay::decode(&[]);
assert!(result.is_err());
}
#[test]
fn object_property_reference_basic_construction() {
let oid = ObjectIdentifier::new(ObjectType::ANALOG_INPUT, 1).unwrap();
let opr = BACnetObjectPropertyReference::new(oid, 85); assert_eq!(opr.object_identifier, oid);
assert_eq!(opr.property_identifier, 85);
assert_eq!(opr.property_array_index, None);
}
#[test]
fn object_property_reference_with_index() {
let oid = ObjectIdentifier::new(ObjectType::ANALOG_INPUT, 1).unwrap();
let opr = BACnetObjectPropertyReference::new_indexed(oid, 85, 3);
assert_eq!(opr.property_array_index, Some(3));
}
#[test]
fn object_property_reference_equality() {
let oid = ObjectIdentifier::new(ObjectType::ANALOG_INPUT, 5).unwrap();
let a = BACnetObjectPropertyReference::new(oid, 85);
let b = BACnetObjectPropertyReference::new(oid, 85);
assert_eq!(a, b);
let c = BACnetObjectPropertyReference::new(oid, 77); assert_ne!(a, c);
}
#[test]
fn device_object_property_reference_local() {
let oid = ObjectIdentifier::new(ObjectType::ANALOG_INPUT, 10).unwrap();
let dopr = BACnetDeviceObjectPropertyReference::new_local(oid, 85);
assert_eq!(dopr.object_identifier, oid);
assert_eq!(dopr.property_identifier, 85);
assert_eq!(dopr.property_array_index, None);
assert_eq!(dopr.device_identifier, None);
}
#[test]
fn device_object_property_reference_remote() {
let oid = ObjectIdentifier::new(ObjectType::ANALOG_INPUT, 10).unwrap();
let dev_oid = ObjectIdentifier::new(ObjectType::DEVICE, 1234).unwrap();
let dopr = BACnetDeviceObjectPropertyReference::new_remote(oid, 85, dev_oid);
assert_eq!(dopr.device_identifier, Some(dev_oid));
}
#[test]
fn device_object_property_reference_with_index() {
let oid = ObjectIdentifier::new(ObjectType::MULTI_STATE_INPUT, 3).unwrap();
let dopr = BACnetDeviceObjectPropertyReference::new_local(oid, 74).with_index(2); assert_eq!(dopr.property_array_index, Some(2));
assert_eq!(dopr.device_identifier, None);
}
#[test]
fn bacnet_address_local_broadcast() {
let addr = BACnetAddress::local_broadcast();
assert_eq!(addr.network_number, 0);
assert!(addr.mac_address.is_empty());
}
#[test]
fn bacnet_address_from_ip() {
let ip_port: [u8; 6] = [192, 168, 1, 100, 0xBA, 0xC0]; let addr = BACnetAddress::from_ip(ip_port);
assert_eq!(addr.network_number, 0);
assert_eq!(addr.mac_address.as_slice(), &ip_port);
}
#[test]
fn bacnet_recipient_device_variant() {
let dev_oid = ObjectIdentifier::new(ObjectType::DEVICE, 42).unwrap();
let recipient = BACnetRecipient::Device(dev_oid);
match recipient {
BACnetRecipient::Device(oid) => assert_eq!(oid.instance_number(), 42),
BACnetRecipient::Address(_) => panic!("wrong variant"),
}
}
#[test]
fn bacnet_recipient_address_variant() {
let addr = BACnetAddress {
network_number: 100,
mac_address: MacAddr::from_slice(&[0x01, 0x02, 0x03]),
};
let recipient = BACnetRecipient::Address(addr.clone());
match recipient {
BACnetRecipient::Device(_) => panic!("wrong variant"),
BACnetRecipient::Address(a) => assert_eq!(a, addr),
}
}
#[test]
fn bacnet_destination_construction() {
let dev_oid = ObjectIdentifier::new(ObjectType::DEVICE, 99).unwrap();
let dest = BACnetDestination {
valid_days: 0b0111_1111, from_time: Time {
hour: 0,
minute: 0,
second: 0,
hundredths: 0,
},
to_time: Time {
hour: 23,
minute: 59,
second: 59,
hundredths: 99,
},
recipient: BACnetRecipient::Device(dev_oid),
process_identifier: 1,
issue_confirmed_notifications: true,
transitions: 0b0000_0111, };
assert_eq!(dest.valid_days & 0x7F, 0x7F);
assert!(dest.issue_confirmed_notifications);
assert_eq!(dest.transitions & 0x07, 0x07);
}
#[test]
fn log_datum_variants_clone_eq() {
let real = LogDatum::RealValue(72.5_f32);
assert_eq!(real.clone(), LogDatum::RealValue(72.5_f32));
let bits = LogDatum::BitstringValue {
unused_bits: 3,
data: vec![0b1010_0000],
};
assert_eq!(bits.clone(), bits);
let fail = LogDatum::Failure {
error_class: 2,
error_code: 31,
};
assert_eq!(fail.clone(), fail);
assert_eq!(LogDatum::NullValue, LogDatum::NullValue);
assert_ne!(LogDatum::BooleanValue(true), LogDatum::BooleanValue(false));
}
#[test]
fn log_record_construction() {
let record = BACnetLogRecord {
date: Date {
year: 124,
month: 3,
day: 15,
day_of_week: 5,
},
time: Time {
hour: 10,
minute: 30,
second: 0,
hundredths: 0,
},
log_datum: LogDatum::RealValue(23.4_f32),
status_flags: None,
};
assert_eq!(record.date.year, 124);
assert_eq!(record.status_flags, None);
}
#[test]
fn log_record_with_status_flags() {
let record = BACnetLogRecord {
date: Date {
year: 124,
month: 1,
day: 1,
day_of_week: 1,
},
time: Time {
hour: 0,
minute: 0,
second: 0,
hundredths: 0,
},
log_datum: LogDatum::LogStatus(0b010), status_flags: Some(0b0100), };
assert_eq!(record.status_flags, Some(0b0100));
match record.log_datum {
LogDatum::LogStatus(s) => assert_eq!(s, 0b010),
_ => panic!("wrong datum variant"),
}
}
#[test]
fn calendar_entry_variants() {
let date_entry = BACnetCalendarEntry::Date(Date {
year: 124,
month: 6,
day: 15,
day_of_week: 6,
});
let range_entry = BACnetCalendarEntry::DateRange(BACnetDateRange {
start_date: Date {
year: 124,
month: 1,
day: 1,
day_of_week: 1,
},
end_date: Date {
year: 124,
month: 12,
day: 31,
day_of_week: 2,
},
});
let wnd_entry = BACnetCalendarEntry::WeekNDay(BACnetWeekNDay {
month: BACnetWeekNDay::ANY,
week_of_month: 1,
day_of_week: 1, });
let _a = date_entry.clone();
let _b = range_entry.clone();
let _c = wnd_entry.clone();
}
#[test]
fn special_event_inline_calendar_entry() {
let event = BACnetSpecialEvent {
period: SpecialEventPeriod::CalendarEntry(BACnetCalendarEntry::WeekNDay(
BACnetWeekNDay {
month: 12,
week_of_month: BACnetWeekNDay::ANY,
day_of_week: BACnetWeekNDay::ANY,
},
)),
list_of_time_values: vec![BACnetTimeValue {
time: Time {
hour: 8,
minute: 0,
second: 0,
hundredths: 0,
},
value: vec![0x10, 0x00], }],
event_priority: 16, };
assert_eq!(event.event_priority, 16);
assert_eq!(event.list_of_time_values.len(), 1);
}
#[test]
fn special_event_calendar_reference() {
let cal_oid = ObjectIdentifier::new(ObjectType::CALENDAR, 0).unwrap();
let event = BACnetSpecialEvent {
period: SpecialEventPeriod::CalendarReference(cal_oid),
list_of_time_values: vec![],
event_priority: 1, };
match &event.period {
SpecialEventPeriod::CalendarReference(oid) => {
assert_eq!(oid.instance_number(), 0);
}
SpecialEventPeriod::CalendarEntry(_) => panic!("wrong period variant"),
}
}
#[test]
fn recipient_process_construction() {
let dev_oid = ObjectIdentifier::new(ObjectType::DEVICE, 100).unwrap();
let rp = BACnetRecipientProcess {
recipient: BACnetRecipient::Device(dev_oid),
process_identifier: 42,
};
assert_eq!(rp.process_identifier, 42);
match &rp.recipient {
BACnetRecipient::Device(oid) => assert_eq!(oid.instance_number(), 100),
BACnetRecipient::Address(_) => panic!("wrong variant"),
}
}
#[test]
fn cov_subscription_creation() {
let dev_oid = ObjectIdentifier::new(ObjectType::DEVICE, 200).unwrap();
let ai_oid = ObjectIdentifier::new(ObjectType::ANALOG_INPUT, 1).unwrap();
let sub = BACnetCOVSubscription {
recipient: BACnetRecipientProcess {
recipient: BACnetRecipient::Device(dev_oid),
process_identifier: 7,
},
monitored_property_reference: BACnetObjectPropertyReference::new(
ai_oid,
PropertyIdentifier::PRESENT_VALUE.to_raw(),
),
issue_confirmed_notifications: true,
time_remaining: 300,
cov_increment: Some(0.5),
};
assert_eq!(sub.recipient.process_identifier, 7);
assert_eq!(
sub.monitored_property_reference
.object_identifier
.instance_number(),
1
);
assert!(sub.issue_confirmed_notifications);
assert_eq!(sub.time_remaining, 300);
assert_eq!(sub.cov_increment, Some(0.5));
}
#[test]
fn cov_subscription_without_increment() {
let dev_oid = ObjectIdentifier::new(ObjectType::DEVICE, 50).unwrap();
let bv_oid = ObjectIdentifier::new(ObjectType::BINARY_VALUE, 3).unwrap();
let sub = BACnetCOVSubscription {
recipient: BACnetRecipientProcess {
recipient: BACnetRecipient::Device(dev_oid),
process_identifier: 1,
},
monitored_property_reference: BACnetObjectPropertyReference::new(
bv_oid,
PropertyIdentifier::PRESENT_VALUE.to_raw(),
),
issue_confirmed_notifications: false,
time_remaining: 0,
cov_increment: None,
};
assert!(!sub.issue_confirmed_notifications);
assert_eq!(sub.cov_increment, None);
}
#[test]
fn value_source_none_variant() {
let vs = BACnetValueSource::None;
assert_eq!(vs, BACnetValueSource::None);
}
#[test]
fn value_source_object_variant() {
let dev_oid = ObjectIdentifier::new(ObjectType::DEVICE, 1).unwrap();
let vs = BACnetValueSource::Object(dev_oid);
match vs {
BACnetValueSource::Object(oid) => assert_eq!(oid.instance_number(), 1),
_ => panic!("wrong variant"),
}
}
#[test]
fn value_source_address_variant() {
let addr = BACnetAddress::from_ip([192, 168, 1, 10, 0xBA, 0xC0]);
let vs = BACnetValueSource::Address(addr.clone());
match vs {
BACnetValueSource::Address(a) => assert_eq!(a, addr),
_ => panic!("wrong variant"),
}
}
}