use std::borrow::Cow;
use std::collections::HashMap;
use std::sync::Arc;
use bacnet_types::enums::{
ErrorClass, ErrorCode, ObjectType, PropertyIdentifier, Segmentation, ServiceSupported,
};
use bacnet_types::error::Error;
use bacnet_types::primitives::{ObjectIdentifier, PropertyValue};
use crate::clock::{ClockFrame, ClockReader};
use crate::common::read_property_list_property;
use crate::object_profile::{ObjectProfile, ProfileState, TagsPersistence};
use crate::traits::BACnetObject;
mod audit_recipient;
mod metadata;
pub use audit_recipient::{AuditRecipientChangeSink, AuditWriteSource, DeviceAuthority};
pub const EXECUTED_SERVICES: &[ServiceSupported] = &[
ServiceSupported::ACKNOWLEDGE_ALARM,
ServiceSupported::CONFIRMED_EVENT_NOTIFICATION,
ServiceSupported::GET_ALARM_SUMMARY,
ServiceSupported::GET_ENROLLMENT_SUMMARY,
ServiceSupported::SUBSCRIBE_COV,
ServiceSupported::ATOMIC_READ_FILE,
ServiceSupported::ATOMIC_WRITE_FILE,
ServiceSupported::ADD_LIST_ELEMENT,
ServiceSupported::REMOVE_LIST_ELEMENT,
ServiceSupported::CREATE_OBJECT,
ServiceSupported::DELETE_OBJECT,
ServiceSupported::READ_PROPERTY,
ServiceSupported::READ_PROPERTY_MULTIPLE,
ServiceSupported::WRITE_PROPERTY,
ServiceSupported::WRITE_PROPERTY_MULTIPLE,
ServiceSupported::DEVICE_COMMUNICATION_CONTROL,
ServiceSupported::CONFIRMED_TEXT_MESSAGE,
ServiceSupported::REINITIALIZE_DEVICE,
ServiceSupported::UNCONFIRMED_EVENT_NOTIFICATION,
ServiceSupported::UNCONFIRMED_TEXT_MESSAGE,
ServiceSupported::TIME_SYNCHRONIZATION,
ServiceSupported::WHO_HAS,
ServiceSupported::WHO_IS,
ServiceSupported::READ_RANGE,
ServiceSupported::UTC_TIME_SYNCHRONIZATION,
ServiceSupported::LIFE_SAFETY_OPERATION,
ServiceSupported::SUBSCRIBE_COV_PROPERTY,
ServiceSupported::GET_EVENT_INFORMATION,
ServiceSupported::WRITE_GROUP,
ServiceSupported::SUBSCRIBE_COV_PROPERTY_MULTIPLE,
ServiceSupported::CONFIRMED_AUDIT_NOTIFICATION,
ServiceSupported::UNCONFIRMED_AUDIT_NOTIFICATION,
ServiceSupported::AUDIT_LOG_QUERY,
];
const SERVICES_SUPPORTED_BITS: usize = 49;
fn compute_services_supported(services: &[ServiceSupported]) -> Vec<u8> {
let mut bits = vec![0u8; SERVICES_SUPPORTED_BITS.div_ceil(8)];
for service in services {
let n = service.to_raw() as usize;
if n < SERVICES_SUPPORTED_BITS {
bits[n / 8] |= 0x80 >> (n % 8);
}
}
bits
}
fn compute_object_types_supported(types: &[u32]) -> Vec<u8> {
let max_type = types.iter().copied().max().unwrap_or(0) as usize;
let num_bytes = (max_type / 8) + 1;
let mut bitstring = vec![0u8; num_bytes];
for &t in types {
let byte_idx = (t as usize) / 8;
let bit_pos = 7 - ((t as usize) % 8);
if byte_idx < bitstring.len() {
bitstring[byte_idx] |= 1 << bit_pos;
}
}
bitstring
}
pub struct DeviceConfig {
pub instance: u32,
pub name: String,
pub vendor_name: String,
pub vendor_id: u16,
pub model_name: String,
pub firmware_revision: String,
pub application_software_version: String,
pub max_apdu_length: u32,
pub segmentation_supported: Segmentation,
pub apdu_segment_timeout: u64,
pub apdu_timeout: u32,
pub apdu_retries: u32,
}
impl Default for DeviceConfig {
fn default() -> Self {
Self {
instance: 1,
name: "BACnet Device".into(),
vendor_name: "Rusty BACnet".into(),
vendor_id: 0,
model_name: "rusty-bacnet".into(),
firmware_revision: "0.1.0".into(),
application_software_version: "0.1.0".into(),
max_apdu_length: 1476,
segmentation_supported: Segmentation::NONE,
apdu_segment_timeout: 5000,
apdu_timeout: 6000,
apdu_retries: 3,
}
}
}
pub struct DeviceObject {
oid: ObjectIdentifier,
recipient: audit_recipient::RecipientState,
properties: HashMap<PropertyIdentifier, PropertyValue>,
profile: ProfileState,
object_list: Vec<ObjectIdentifier>,
protocol_object_types_supported: Vec<u8>,
configured_services_supported: Vec<ServiceSupported>,
clock: Option<Arc<dyn ClockReader>>,
}
impl DeviceObject {
pub fn new(config: DeviceConfig) -> Result<Self, Error> {
let oid = ObjectIdentifier::new(ObjectType::DEVICE, config.instance)?;
let mut properties = HashMap::new();
properties.insert(
PropertyIdentifier::OBJECT_IDENTIFIER,
PropertyValue::ObjectIdentifier(oid),
);
properties.insert(
PropertyIdentifier::OBJECT_NAME,
PropertyValue::CharacterString(config.name),
);
properties.insert(
PropertyIdentifier::OBJECT_TYPE,
PropertyValue::Enumerated(ObjectType::DEVICE.to_raw()),
);
properties.insert(
PropertyIdentifier::SYSTEM_STATUS,
PropertyValue::Enumerated(0), );
properties.insert(
PropertyIdentifier::VENDOR_NAME,
PropertyValue::CharacterString(config.vendor_name),
);
properties.insert(
PropertyIdentifier::VENDOR_IDENTIFIER,
PropertyValue::Unsigned(config.vendor_id as u64),
);
properties.insert(
PropertyIdentifier::MODEL_NAME,
PropertyValue::CharacterString(config.model_name),
);
properties.insert(
PropertyIdentifier::FIRMWARE_REVISION,
PropertyValue::CharacterString(config.firmware_revision),
);
properties.insert(
PropertyIdentifier::APPLICATION_SOFTWARE_VERSION,
PropertyValue::CharacterString(config.application_software_version),
);
properties.insert(
PropertyIdentifier::PROTOCOL_VERSION,
PropertyValue::Unsigned(1),
);
properties.insert(
PropertyIdentifier::PROTOCOL_REVISION,
PropertyValue::Unsigned(22), );
properties.insert(
PropertyIdentifier::MAX_APDU_LENGTH_ACCEPTED,
PropertyValue::Unsigned(config.max_apdu_length as u64),
);
properties.insert(
PropertyIdentifier::SEGMENTATION_SUPPORTED,
PropertyValue::Enumerated(config.segmentation_supported.to_raw() as u32),
);
properties.insert(
PropertyIdentifier::APDU_TIMEOUT,
PropertyValue::Unsigned(config.apdu_timeout as u64),
);
properties.insert(
PropertyIdentifier::NUMBER_OF_APDU_RETRIES,
PropertyValue::Unsigned(config.apdu_retries as u64),
);
properties.insert(
PropertyIdentifier::DATABASE_REVISION,
PropertyValue::Unsigned(0),
);
properties.insert(
PropertyIdentifier::DESCRIPTION,
PropertyValue::CharacterString(String::new()),
);
properties.insert(
PropertyIdentifier::DEVICE_ADDRESS_BINDING,
PropertyValue::List(Vec::new()),
);
properties.insert(
PropertyIdentifier::LAST_RESTART_REASON,
PropertyValue::Enumerated(0), );
properties.insert(
PropertyIdentifier::DEVICE_UUID,
PropertyValue::OctetString(vec![0u8; 16]),
);
if config.segmentation_supported != Segmentation::NONE {
if config.apdu_segment_timeout == 0 {
return Err(Error::Encoding(
"APDU_Segment_Timeout must be positive when segmentation is supported".into(),
));
}
properties.insert(
PropertyIdentifier::APDU_SEGMENT_TIMEOUT,
PropertyValue::Unsigned(config.apdu_segment_timeout),
);
let max_segments_accepted = if config.segmentation_supported == Segmentation::TRANSMIT {
1
} else {
65
};
properties.insert(
PropertyIdentifier::MAX_SEGMENTS_ACCEPTED,
PropertyValue::Unsigned(max_segments_accepted),
);
}
let protocol_object_types_supported = compute_object_types_supported(&[
ObjectType::ANALOG_INPUT.to_raw(),
ObjectType::ANALOG_OUTPUT.to_raw(),
ObjectType::ANALOG_VALUE.to_raw(),
ObjectType::BINARY_INPUT.to_raw(),
ObjectType::BINARY_OUTPUT.to_raw(),
ObjectType::BINARY_VALUE.to_raw(),
ObjectType::CALENDAR.to_raw(),
ObjectType::COMMAND.to_raw(),
ObjectType::DEVICE.to_raw(),
ObjectType::EVENT_ENROLLMENT.to_raw(),
ObjectType::FILE.to_raw(),
ObjectType::GROUP.to_raw(),
ObjectType::LOOP.to_raw(),
ObjectType::MULTI_STATE_INPUT.to_raw(),
ObjectType::MULTI_STATE_OUTPUT.to_raw(),
ObjectType::NOTIFICATION_CLASS.to_raw(),
ObjectType::PROGRAM.to_raw(),
ObjectType::SCHEDULE.to_raw(),
ObjectType::AVERAGING.to_raw(),
ObjectType::MULTI_STATE_VALUE.to_raw(),
ObjectType::TREND_LOG.to_raw(),
ObjectType::LIFE_SAFETY_POINT.to_raw(),
ObjectType::LIFE_SAFETY_ZONE.to_raw(),
ObjectType::ACCUMULATOR.to_raw(),
ObjectType::PULSE_CONVERTER.to_raw(),
ObjectType::EVENT_LOG.to_raw(),
ObjectType::GLOBAL_GROUP.to_raw(),
ObjectType::TREND_LOG_MULTIPLE.to_raw(),
ObjectType::LOAD_CONTROL.to_raw(),
ObjectType::STRUCTURED_VIEW.to_raw(),
ObjectType::ACCESS_DOOR.to_raw(),
ObjectType::TIMER.to_raw(),
ObjectType::ACCESS_CREDENTIAL.to_raw(),
ObjectType::ACCESS_POINT.to_raw(),
ObjectType::ACCESS_RIGHTS.to_raw(),
ObjectType::ACCESS_USER.to_raw(),
ObjectType::ACCESS_ZONE.to_raw(),
ObjectType::CREDENTIAL_DATA_INPUT.to_raw(),
ObjectType::BITSTRING_VALUE.to_raw(),
ObjectType::CHARACTERSTRING_VALUE.to_raw(),
ObjectType::DATEPATTERN_VALUE.to_raw(),
ObjectType::DATE_VALUE.to_raw(),
ObjectType::DATETIMEPATTERN_VALUE.to_raw(),
ObjectType::DATETIME_VALUE.to_raw(),
ObjectType::INTEGER_VALUE.to_raw(),
ObjectType::LARGE_ANALOG_VALUE.to_raw(),
ObjectType::OCTETSTRING_VALUE.to_raw(),
ObjectType::POSITIVE_INTEGER_VALUE.to_raw(),
ObjectType::TIMEPATTERN_VALUE.to_raw(),
ObjectType::TIME_VALUE.to_raw(),
ObjectType::NOTIFICATION_FORWARDER.to_raw(),
ObjectType::ALERT_ENROLLMENT.to_raw(),
ObjectType::CHANNEL.to_raw(),
ObjectType::LIGHTING_OUTPUT.to_raw(),
ObjectType::BINARY_LIGHTING_OUTPUT.to_raw(),
ObjectType::NETWORK_PORT.to_raw(),
ObjectType::ELEVATOR_GROUP.to_raw(),
ObjectType::ESCALATOR.to_raw(),
ObjectType::LIFT.to_raw(),
ObjectType::STAGING.to_raw(),
ObjectType::AUDIT_LOG.to_raw(),
ObjectType::AUDIT_REPORTER.to_raw(),
ObjectType::COLOR.to_raw(),
ObjectType::COLOR_TEMPERATURE.to_raw(),
]);
Ok(Self {
oid,
recipient: Default::default(),
profile: ProfileState::default(),
properties,
object_list: vec![oid], protocol_object_types_supported,
configured_services_supported: EXECUTED_SERVICES.to_vec(),
clock: None,
})
}
pub fn with_tags_persistence(
config: DeviceConfig,
persistence: Arc<dyn TagsPersistence>,
) -> Result<Self, Error> {
let mut object = Self::new(config)?;
object.profile = ProfileState::persistent(object.oid, persistence)?;
Ok(object)
}
pub fn set_profile(&mut self, profile: ObjectProfile) -> Result<(), Error> {
self.profile.provision(profile)
}
pub fn wait_for_tag_saves(&self) {
self.profile.wait_for_saves();
}
pub fn set_object_list(&mut self, oids: Vec<ObjectIdentifier>) {
self.object_list = oids;
}
pub fn set_services_supported(&mut self, services: &[ServiceSupported]) {
self.configured_services_supported = services.to_vec();
}
pub fn instance(&self) -> u32 {
self.oid.instance_number()
}
pub fn set_description(&mut self, desc: impl Into<String>) {
self.properties.insert(
PropertyIdentifier::DESCRIPTION,
PropertyValue::CharacterString(desc.into()),
);
}
pub fn set_device_uuid(&mut self, uuid: [u8; 16]) {
self.properties.insert(
PropertyIdentifier::DEVICE_UUID,
PropertyValue::OctetString(uuid.to_vec()),
);
}
fn clock_frame(&self) -> Option<ClockFrame> {
self.clock.as_ref()?.read_clock()
}
fn cov_property_present(&self, property: PropertyIdentifier) -> bool {
match property {
PropertyIdentifier::ACTIVE_COV_SUBSCRIPTIONS => {
self.configured_services_supported.iter().any(|service| {
matches!(
*service,
ServiceSupported::SUBSCRIBE_COV | ServiceSupported::SUBSCRIBE_COV_PROPERTY
)
})
}
PropertyIdentifier::ACTIVE_COV_MULTIPLE_SUBSCRIPTIONS => self
.configured_services_supported
.contains(&ServiceSupported::SUBSCRIBE_COV_PROPERTY_MULTIPLE),
_ => true,
}
}
fn services_supported(&self) -> Vec<u8> {
let clock_available = self.clock_frame().is_some();
let services = self
.configured_services_supported
.iter()
.copied()
.filter(|service| {
clock_available
|| (*service != ServiceSupported::TIME_SYNCHRONIZATION
&& *service != ServiceSupported::UTC_TIME_SYNCHRONIZATION)
})
.collect::<Vec<_>>();
compute_services_supported(&services)
}
}
impl BACnetObject for DeviceObject {
fn device_authority_internal(&mut self) -> Option<DeviceAuthority<'_>> {
Some(DeviceAuthority(self))
}
fn object_identifier(&self) -> ObjectIdentifier {
self.oid
}
fn object_name(&self) -> &str {
match self.properties.get(&PropertyIdentifier::OBJECT_NAME) {
Some(PropertyValue::CharacterString(s)) => s,
_ => "Unknown",
}
}
fn read_property(
&self,
property: PropertyIdentifier,
array_index: Option<u32>,
) -> Result<PropertyValue, Error> {
if property == PropertyIdentifier::AUDIT_NOTIFICATION_RECIPIENT {
return self.read_audit_recipient(array_index);
}
if property == PropertyIdentifier::OBJECT_LIST {
return match array_index {
None => {
let elements = self
.object_list
.iter()
.map(|oid| PropertyValue::ObjectIdentifier(*oid))
.collect();
Ok(PropertyValue::List(elements))
}
Some(0) => {
Ok(PropertyValue::Unsigned(self.object_list.len() as u64))
}
Some(idx) => {
let i = (idx - 1) as usize; if i < self.object_list.len() {
Ok(PropertyValue::ObjectIdentifier(self.object_list[i]))
} else {
Err(Error::Protocol {
class: ErrorClass::PROPERTY.to_raw() as u32,
code: ErrorCode::INVALID_ARRAY_INDEX.to_raw() as u32,
})
}
}
};
}
if property == PropertyIdentifier::PROPERTY_LIST {
return read_property_list_property(&self.property_list(), array_index);
}
if matches!(
property,
PropertyIdentifier::LOCAL_DATE
| PropertyIdentifier::LOCAL_TIME
| PropertyIdentifier::UTC_OFFSET
| PropertyIdentifier::DAYLIGHT_SAVINGS_STATUS
) {
let frame = self.clock_frame().ok_or(Error::Protocol {
class: ErrorClass::PROPERTY.to_raw() as u32,
code: ErrorCode::UNKNOWN_PROPERTY.to_raw() as u32,
})?;
return Ok(match property {
PropertyIdentifier::LOCAL_DATE => PropertyValue::Date(frame.local_date),
PropertyIdentifier::LOCAL_TIME => PropertyValue::Time(frame.local_time),
PropertyIdentifier::UTC_OFFSET => {
PropertyValue::Signed(i32::from(frame.utc_offset))
}
PropertyIdentifier::DAYLIGHT_SAVINGS_STATUS => {
PropertyValue::Boolean(frame.daylight_savings_status)
}
_ => unreachable!(),
});
}
if property == PropertyIdentifier::PROTOCOL_OBJECT_TYPES_SUPPORTED {
let num_bytes = self.protocol_object_types_supported.len();
let total_bits = num_bytes * 8;
let mut max_type = 0u32;
for (byte_idx, &byte) in self.protocol_object_types_supported.iter().enumerate() {
for bit in 0..8 {
if byte & (1 << (7 - bit)) != 0 {
max_type = (byte_idx * 8 + bit) as u32;
}
}
}
let used_bits = max_type as usize + 1;
let unused = (total_bits - used_bits) as u8;
return Ok(PropertyValue::BitString {
unused_bits: unused,
data: self.protocol_object_types_supported.clone(),
});
}
if property == PropertyIdentifier::PROTOCOL_SERVICES_SUPPORTED {
let services_supported = self.services_supported();
let unused = (services_supported.len() * 8 - SERVICES_SUPPORTED_BITS) as u8;
return Ok(PropertyValue::BitString {
unused_bits: unused,
data: services_supported,
});
}
if matches!(
property,
PropertyIdentifier::ACTIVE_COV_SUBSCRIPTIONS
| PropertyIdentifier::ACTIVE_COV_MULTIPLE_SUBSCRIPTIONS
) {
return if self.cov_property_present(property) {
Ok(PropertyValue::ApplicationData(Vec::new()))
} else {
Err(Error::Protocol {
class: ErrorClass::PROPERTY.to_raw() as u32,
code: ErrorCode::UNKNOWN_PROPERTY.to_raw() as u32,
})
};
}
if let Some(value) = self.profile.read(property, array_index) {
return value;
}
self.properties
.get(&property)
.cloned()
.ok_or(Error::Protocol {
class: ErrorClass::PROPERTY.to_raw() as u32,
code: ErrorCode::UNKNOWN_PROPERTY.to_raw() as u32,
})
}
fn write_property(
&mut self,
property: PropertyIdentifier,
array_index: Option<u32>,
value: PropertyValue,
_priority: Option<u8>,
) -> Result<(), Error> {
if property == PropertyIdentifier::AUDIT_NOTIFICATION_RECIPIENT {
return self.write_audit_recipient(array_index, value, _priority, None);
}
if let Some(result) = self.profile.write(property, array_index, &value) {
return result;
}
if property == PropertyIdentifier::DESCRIPTION {
if array_index.is_some() {
return Err(Error::Protocol {
class: ErrorClass::PROPERTY.to_raw() as u32,
code: ErrorCode::PROPERTY_IS_NOT_AN_ARRAY.to_raw() as u32,
});
}
if value == PropertyValue::Null {
return Ok(());
}
if let PropertyValue::CharacterString(_) = &value {
self.properties.insert(property, value);
return Ok(());
}
return Err(Error::Protocol {
class: ErrorClass::PROPERTY.to_raw() as u32,
code: ErrorCode::INVALID_DATA_TYPE.to_raw() as u32,
});
}
Err(crate::common::unhandled_write_error(
self.property_metadata().as_ref(),
property,
array_index,
))
}
fn durable_writes_internal(&mut self) -> Option<&mut dyn crate::durable::DurableWrites> {
self.profile.capability()
}
fn advance_monotonic_time_internal(&mut self, now: std::time::Duration) -> bool {
self.profile.expire(now);
false
}
fn property_metadata(&self) -> Cow<'_, [crate::property_metadata::PropertyMetadata]> {
metadata::for_object(self)
}
fn property_list(&self) -> Cow<'static, [PropertyIdentifier]> {
Cow::Owned(
self.property_metadata()
.iter()
.map(|row| row.property_identifier)
.collect(),
)
}
fn is_createable(&self) -> bool {
false
}
fn is_deleteable(&self) -> bool {
false
}
fn bind_clock_internal(&mut self, clock: Option<Arc<dyn ClockReader>>) {
self.clock = clock;
}
}
#[cfg(test)]
mod tests;
#[cfg(test)]
mod service_profile_tests;