use std::collections::HashSet;
use std::sync::atomic::{AtomicU8, Ordering};
use std::time::{Duration, Instant};
use bacnet_encoding::primitives::encode_property_value;
use bacnet_objects::database::ObjectDatabase;
use bacnet_services::alarm_event::{
AcknowledgeAlarmRequest, EventSummary, GetEventInformationAck, GetEventInformationRequest,
};
use bacnet_services::cov::SubscribeCOVRequest;
use bacnet_services::device_mgmt::{DeviceCommunicationControlRequest, ReinitializeDeviceRequest};
use bacnet_services::object_mgmt::{CreateObjectRequest, DeleteObjectRequest, ObjectSpecifier};
use bacnet_services::read_property::{ReadPropertyACK, ReadPropertyRequest};
use bacnet_services::rpm::{
ReadAccessResult, ReadPropertyMultipleACK, ReadPropertyMultipleRequest, ReadResultElement,
};
use bacnet_services::who_has::{IHaveRequest, WhoHasObject, WhoHasRequest};
use bacnet_services::wpm::WritePropertyMultipleRequest;
use bacnet_services::write_property::WritePropertyRequest;
use bacnet_types::enums::{
EnableDisable, ErrorClass, ErrorCode, EventState, ObjectType, PropertyIdentifier,
};
use bacnet_types::error::Error;
use bacnet_types::primitives::{BACnetTimeStamp, ObjectIdentifier, PropertyValue};
use bacnet_types::MacAddr;
const PROP_FILE_DATA: u32 = 0x0041;
use bytes::BytesMut;
use crate::cov::{CovSubscription, CovSubscriptionTable};
pub fn handle_read_property(
db: &ObjectDatabase,
service_data: &[u8],
buf: &mut BytesMut,
) -> Result<(), Error> {
let request = ReadPropertyRequest::decode(service_data)?;
let lookup_oid = resolve_device_wildcard(db, &request.object_identifier);
let object = db.get(&lookup_oid).ok_or(Error::Protocol {
class: ErrorClass::OBJECT.to_raw() as u32,
code: ErrorCode::UNKNOWN_OBJECT.to_raw() as u32,
})?;
if request.property_array_index.is_some() {
let is_array_property = matches!(
request.property_identifier,
p if p == PropertyIdentifier::PRIORITY_ARRAY
|| p == PropertyIdentifier::OBJECT_LIST
|| p == PropertyIdentifier::PROPERTY_LIST
|| p == PropertyIdentifier::WEEKLY_SCHEDULE
|| p == PropertyIdentifier::EXCEPTION_SCHEDULE
|| p == PropertyIdentifier::DATE_LIST
|| p == PropertyIdentifier::LIST_OF_GROUP_MEMBERS
|| p == PropertyIdentifier::RECIPIENT_LIST
|| p == PropertyIdentifier::LOG_BUFFER
|| p == PropertyIdentifier::STATE_TEXT
|| p == PropertyIdentifier::ALARM_VALUES
|| p == PropertyIdentifier::FAULT_VALUES
|| p == PropertyIdentifier::EVENT_TIME_STAMPS
|| p == PropertyIdentifier::EVENT_MESSAGE_TEXTS
|| p == PropertyIdentifier::LIST_OF_OBJECT_PROPERTY_REFERENCES
|| p == PropertyIdentifier::DEVICE_ADDRESS_BINDING
|| p == PropertyIdentifier::ACTIVE_COV_SUBSCRIPTIONS
|| p == PropertyIdentifier::TAGS
);
if !is_array_property {
return Err(Error::Protocol {
class: ErrorClass::PROPERTY.to_raw() as u32,
code: ErrorCode::PROPERTY_IS_NOT_AN_ARRAY.to_raw() as u32,
});
}
}
let value = object.read_property(request.property_identifier, request.property_array_index)?;
let mut value_buf = BytesMut::new();
encode_property_value(&mut value_buf, &value)?;
let ack = ReadPropertyACK {
object_identifier: lookup_oid,
property_identifier: request.property_identifier,
property_array_index: request.property_array_index,
property_value: value_buf.to_vec(),
};
ack.encode(buf);
Ok(())
}
fn resolve_device_wildcard(db: &ObjectDatabase, oid: &ObjectIdentifier) -> ObjectIdentifier {
if oid.object_type() == ObjectType::DEVICE && oid.instance_number() == 4194303 {
for candidate in db.list_objects() {
if candidate.object_type() == ObjectType::DEVICE {
return candidate;
}
}
}
*oid
}
pub fn handle_read_property_multiple(
db: &ObjectDatabase,
service_data: &[u8],
buf: &mut BytesMut,
) -> Result<(), Error> {
let request = ReadPropertyMultipleRequest::decode(service_data)?;
let mut results = Vec::new();
for spec in &request.list_of_read_access_specs {
let mut elements = Vec::new();
let lookup_oid = resolve_device_wildcard(db, &spec.object_identifier);
match db.get(&lookup_oid) {
Some(object) => {
for prop_ref in &spec.list_of_property_references {
let prop_ids: Vec<PropertyIdentifier> = match prop_ref.property_identifier {
PropertyIdentifier::ALL => object.property_list().to_vec(),
PropertyIdentifier::REQUIRED => object.required_properties().to_vec(),
PropertyIdentifier::OPTIONAL => {
let required: std::collections::HashSet<PropertyIdentifier> =
object.required_properties().iter().copied().collect();
object
.property_list()
.iter()
.copied()
.filter(|p| !required.contains(p))
.collect()
}
other => vec![other],
};
for prop_id in prop_ids {
let array_index = if prop_ref.property_identifier == prop_id {
prop_ref.property_array_index
} else {
None
};
match object.read_property(prop_id, array_index) {
Ok(value) => {
let mut value_buf = BytesMut::new();
match encode_property_value(&mut value_buf, &value) {
Ok(()) => {
elements.push(ReadResultElement {
property_identifier: prop_id,
property_array_index: array_index,
property_value: Some(value_buf.to_vec()),
error: None,
});
}
Err(_) => {
elements.push(ReadResultElement {
property_identifier: prop_id,
property_array_index: array_index,
property_value: None,
error: Some((ErrorClass::PROPERTY, ErrorCode::OTHER)),
});
}
}
}
Err(e) => {
let (err_class, err_code) = match &e {
Error::Protocol { class, code } => (
ErrorClass::from_raw(*class as u16),
ErrorCode::from_raw(*code as u16),
),
_ => (ErrorClass::PROPERTY, ErrorCode::UNKNOWN_PROPERTY),
};
elements.push(ReadResultElement {
property_identifier: prop_id,
property_array_index: array_index,
property_value: None,
error: Some((err_class, err_code)),
});
}
}
}
}
}
None => {
for prop_ref in &spec.list_of_property_references {
elements.push(ReadResultElement {
property_identifier: prop_ref.property_identifier,
property_array_index: prop_ref.property_array_index,
property_value: None,
error: Some((ErrorClass::OBJECT, ErrorCode::UNKNOWN_OBJECT)),
});
}
}
}
results.push(ReadAccessResult {
object_identifier: spec.object_identifier,
list_of_results: elements,
});
}
let ack = ReadPropertyMultipleACK {
list_of_read_access_results: results,
};
ack.encode(buf);
Ok(())
}
pub fn handle_write_property_multiple(
db: &mut ObjectDatabase,
service_data: &[u8],
) -> Result<Vec<ObjectIdentifier>, Error> {
let request = WritePropertyMultipleRequest::decode(service_data)?;
#[allow(clippy::type_complexity)]
let mut decoded_writes: Vec<(
ObjectIdentifier,
PropertyIdentifier,
Option<u32>,
PropertyValue,
Option<u8>,
)> = Vec::new();
for spec in &request.list_of_write_access_specs {
let oid = spec.object_identifier;
if db.get(&oid).is_none() {
return Err(Error::Protocol {
class: ErrorClass::OBJECT.to_raw() as u32,
code: ErrorCode::UNKNOWN_OBJECT.to_raw() as u32,
});
}
for prop in &spec.list_of_properties {
let (value, _) = bacnet_encoding::primitives::decode_application_value(&prop.value, 0)?;
decoded_writes.push((
oid,
prop.property_identifier,
prop.property_array_index,
value,
prop.priority,
));
}
}
let mut applied: Vec<(
ObjectIdentifier,
PropertyIdentifier,
Option<u32>,
PropertyValue,
)> = Vec::new();
for (oid, prop_id, array_index, value, priority) in &decoded_writes {
let object = db.get_mut(oid).unwrap();
let old_value = object.read_property(*prop_id, *array_index).ok();
match object.write_property(*prop_id, *array_index, value.clone(), *priority) {
Ok(()) => {
if let Some(old) = old_value {
applied.push((*oid, *prop_id, *array_index, old));
}
}
Err(e) => {
for (rb_oid, rb_prop, rb_idx, rb_val) in applied.into_iter().rev() {
if let Some(obj) = db.get_mut(&rb_oid) {
let _ = obj.write_property(rb_prop, rb_idx, rb_val, None);
}
}
return Err(e);
}
}
}
let mut written_oids = Vec::new();
for (oid, _, _, _, _) in &decoded_writes {
if !written_oids.contains(oid) {
written_oids.push(*oid);
}
}
Ok(written_oids)
}
pub fn handle_write_property(
db: &mut ObjectDatabase,
service_data: &[u8],
) -> Result<ObjectIdentifier, Error> {
let request = WritePropertyRequest::decode(service_data)?;
let oid = request.object_identifier;
let object = db.get_mut(&oid).ok_or(Error::Protocol {
class: ErrorClass::OBJECT.to_raw() as u32,
code: ErrorCode::UNKNOWN_OBJECT.to_raw() as u32,
})?;
let (value, _) =
bacnet_encoding::primitives::decode_application_value(&request.property_value, 0)?;
object.write_property(
request.property_identifier,
request.property_array_index,
value,
request.priority,
)?;
Ok(oid)
}
pub fn handle_subscribe_cov(
table: &mut CovSubscriptionTable,
db: &ObjectDatabase,
source_mac: &[u8],
service_data: &[u8],
) -> Result<(), Error> {
let request = SubscribeCOVRequest::decode(service_data)?;
if request.is_cancellation() {
table.unsubscribe(
source_mac,
request.subscriber_process_identifier,
request.monitored_object_identifier,
);
return Ok(());
}
match db.get(&request.monitored_object_identifier) {
None => {
return Err(Error::Protocol {
class: ErrorClass::OBJECT.to_raw() as u32,
code: ErrorCode::UNKNOWN_OBJECT.to_raw() as u32,
});
}
Some(obj) if !obj.supports_cov() => {
return Err(Error::Protocol {
class: ErrorClass::OBJECT.to_raw() as u32,
code: ErrorCode::OPTIONAL_FUNCTIONALITY_NOT_SUPPORTED.to_raw() as u32,
});
}
_ => {}
}
const MAX_COV_SUBSCRIPTIONS: usize = 1024;
if table.len() >= MAX_COV_SUBSCRIPTIONS {
return Err(Error::Protocol {
class: ErrorClass::RESOURCES.to_raw() as u32,
code: ErrorCode::NO_SPACE_TO_ADD_LIST_ELEMENT.to_raw() as u32,
});
}
let expires_at = request.lifetime.and_then(|secs| {
if secs == 0 {
None
} else {
Some(Instant::now() + Duration::from_secs(secs as u64))
}
});
table.subscribe(CovSubscription {
subscriber_mac: MacAddr::from_slice(source_mac),
subscriber_process_identifier: request.subscriber_process_identifier,
monitored_object_identifier: request.monitored_object_identifier,
issue_confirmed_notifications: request.issue_confirmed_notifications.unwrap_or(false),
expires_at,
last_notified_value: None,
monitored_property: None,
monitored_property_array_index: None,
cov_increment: None,
});
Ok(())
}
pub fn handle_subscribe_cov_property(
table: &mut CovSubscriptionTable,
db: &ObjectDatabase,
source_mac: &[u8],
service_data: &[u8],
) -> Result<(), Error> {
use bacnet_services::cov::SubscribeCOVPropertyRequest;
let request = SubscribeCOVPropertyRequest::decode(service_data)?;
if request.is_cancellation() {
table.unsubscribe_property(
source_mac,
request.subscriber_process_identifier,
request.monitored_object_identifier,
request.monitored_property_identifier,
);
return Ok(());
}
let object = db
.get(&request.monitored_object_identifier)
.ok_or(Error::Protocol {
class: ErrorClass::OBJECT.to_raw() as u32,
code: ErrorCode::UNKNOWN_OBJECT.to_raw() as u32,
})?;
object
.read_property(
request.monitored_property_identifier,
request.monitored_property_array_index,
)
.map_err(|_| Error::Protocol {
class: ErrorClass::PROPERTY.to_raw() as u32,
code: ErrorCode::UNKNOWN_PROPERTY.to_raw() as u32,
})?;
const MAX_COV_SUBSCRIPTIONS: usize = 1024;
if table.len() >= MAX_COV_SUBSCRIPTIONS {
return Err(Error::Protocol {
class: ErrorClass::RESOURCES.to_raw() as u32,
code: ErrorCode::NO_SPACE_TO_ADD_LIST_ELEMENT.to_raw() as u32,
});
}
let expires_at = request.lifetime.and_then(|secs| {
if secs == 0 {
None
} else {
Some(Instant::now() + Duration::from_secs(secs as u64))
}
});
table.subscribe(CovSubscription {
subscriber_mac: MacAddr::from_slice(source_mac),
subscriber_process_identifier: request.subscriber_process_identifier,
monitored_object_identifier: request.monitored_object_identifier,
issue_confirmed_notifications: request.issue_confirmed_notifications.unwrap_or(false),
expires_at,
last_notified_value: None,
monitored_property: Some(request.monitored_property_identifier),
monitored_property_array_index: request.monitored_property_array_index,
cov_increment: request.cov_increment,
});
Ok(())
}
pub fn handle_who_has(
db: &ObjectDatabase,
service_data: &[u8],
device_oid: ObjectIdentifier,
) -> Result<Option<IHaveRequest>, Error> {
let request = WhoHasRequest::decode(service_data)?;
let instance = device_oid.instance_number();
if let (Some(low), Some(high)) = (request.low_limit, request.high_limit) {
if instance < low || instance > high {
return Ok(None);
}
}
match &request.object {
WhoHasObject::Identifier(oid) => {
if let Some(obj) = db.get(oid) {
return Ok(Some(IHaveRequest {
device_identifier: device_oid,
object_identifier: *oid,
object_name: obj.object_name().to_string(),
}));
}
}
WhoHasObject::Name(name) => {
for (oid, obj) in db.iter_objects() {
if obj.object_name() == name {
return Ok(Some(IHaveRequest {
device_identifier: device_oid,
object_identifier: oid,
object_name: name.clone(),
}));
}
}
}
}
Ok(None)
}
pub fn handle_create_object(
db: &mut ObjectDatabase,
service_data: &[u8],
buf: &mut BytesMut,
) -> Result<(), Error> {
let request = CreateObjectRequest::decode(service_data)?;
const MAX_OBJECTS: usize = 10_000;
if db.len() >= MAX_OBJECTS {
return Err(Error::Protocol {
class: ErrorClass::RESOURCES.to_raw() as u32,
code: ErrorCode::NO_SPACE_FOR_OBJECT.to_raw() as u32,
});
}
let (object_type, instance) = match &request.object_specifier {
ObjectSpecifier::Type(obj_type) => {
let existing: HashSet<u32> = db
.find_by_type(*obj_type)
.iter()
.map(|oid| oid.instance_number())
.collect();
let next = (1u32..=4_194_303)
.find(|i| !existing.contains(i))
.ok_or_else(|| Error::Protocol {
class: ErrorClass::RESOURCES.to_raw() as u32,
code: ErrorCode::NO_SPACE_FOR_OBJECT.to_raw() as u32,
})?;
(*obj_type, next)
}
ObjectSpecifier::Identifier(oid) => {
if db.get(oid).is_some() {
return Err(Error::Protocol {
class: ErrorClass::OBJECT.to_raw() as u32,
code: ErrorCode::OBJECT_IDENTIFIER_ALREADY_EXISTS.to_raw() as u32,
});
}
(oid.object_type(), oid.instance_number())
}
};
let name = format!("{:?}-{}", object_type, instance);
let object: Box<dyn bacnet_objects::traits::BACnetObject> =
if object_type == ObjectType::ANALOG_INPUT {
Box::new(bacnet_objects::analog::AnalogInputObject::new(
instance, &name, 95,
)?)
} else if object_type == ObjectType::ANALOG_OUTPUT {
Box::new(bacnet_objects::analog::AnalogOutputObject::new(
instance, &name, 95,
)?)
} else if object_type == ObjectType::BINARY_INPUT {
Box::new(bacnet_objects::binary::BinaryInputObject::new(
instance, &name,
)?)
} else if object_type == ObjectType::BINARY_OUTPUT {
Box::new(bacnet_objects::binary::BinaryOutputObject::new(
instance, &name,
)?)
} else if object_type == ObjectType::BINARY_VALUE {
Box::new(bacnet_objects::binary::BinaryValueObject::new(
instance, &name,
)?)
} else if object_type == ObjectType::MULTI_STATE_INPUT {
Box::new(bacnet_objects::multistate::MultiStateInputObject::new(
instance, &name, 2,
)?)
} else if object_type == ObjectType::MULTI_STATE_OUTPUT {
Box::new(bacnet_objects::multistate::MultiStateOutputObject::new(
instance, &name, 2,
)?)
} else if object_type == ObjectType::MULTI_STATE_VALUE {
Box::new(bacnet_objects::multistate::MultiStateValueObject::new(
instance, &name, 2,
)?)
} else {
return Err(Error::Protocol {
class: ErrorClass::OBJECT.to_raw() as u32,
code: ErrorCode::UNSUPPORTED_OBJECT_TYPE.to_raw() as u32,
});
};
let created_oid = object.object_identifier();
db.add(object)?;
for pv in &request.list_of_initial_values {
let (value, _) = match bacnet_encoding::primitives::decode_application_value(&pv.value, 0) {
Ok(v) => v,
Err(e) => {
db.remove(&created_oid);
return Err(e);
}
};
if let Some(obj) = db.get_mut(&created_oid) {
if let Err(e) = obj.write_property(
pv.property_identifier,
pv.property_array_index,
value,
pv.priority,
) {
db.remove(&created_oid);
return Err(e);
}
}
}
bacnet_encoding::primitives::encode_app_object_id(buf, &created_oid);
Ok(())
}
pub fn handle_delete_object(db: &mut ObjectDatabase, service_data: &[u8]) -> Result<(), Error> {
let request = DeleteObjectRequest::decode(service_data)?;
if request.object_identifier.object_type() == ObjectType::DEVICE {
return Err(Error::Protocol {
class: ErrorClass::OBJECT.to_raw() as u32,
code: ErrorCode::OBJECT_DELETION_NOT_PERMITTED.to_raw() as u32,
});
}
db.remove(&request.object_identifier)
.ok_or(Error::Protocol {
class: ErrorClass::OBJECT.to_raw() as u32,
code: ErrorCode::UNKNOWN_OBJECT.to_raw() as u32,
})?;
Ok(())
}
fn validate_password(
configured: &Option<String>,
request_pw: &Option<String>,
) -> Result<(), Error> {
if let Some(ref expected) = configured {
match request_pw {
Some(ref pw) if constant_time_eq(pw.as_bytes(), expected.as_bytes()) => Ok(()),
_ => Err(Error::Protocol {
class: ErrorClass::SECURITY.to_raw() as u32,
code: ErrorCode::PASSWORD_FAILURE.to_raw() as u32,
}),
}
} else {
Ok(())
}
}
fn constant_time_eq(a: &[u8], b: &[u8]) -> bool {
let len = a.len().max(b.len());
let mut diff = (a.len() != b.len()) as u8;
for i in 0..len {
let x = if i < a.len() { a[i] } else { 0 };
let y = if i < b.len() { b[i] } else { 0 };
diff |= x ^ y;
}
diff == 0
}
pub fn handle_device_communication_control(
service_data: &[u8],
comm_state: &AtomicU8,
dcc_password: &Option<String>,
) -> Result<(EnableDisable, Option<u16>), Error> {
let request = DeviceCommunicationControlRequest::decode(service_data)?;
validate_password(dcc_password, &request.password)?;
let new_state = if request.enable_disable == EnableDisable::ENABLE {
0u8
} else if request.enable_disable == EnableDisable::DISABLE {
1u8
} else if request.enable_disable == EnableDisable::DISABLE_INITIATION {
2u8
} else {
return Err(Error::Encoding("unknown EnableDisable value".into()));
};
comm_state.store(new_state, Ordering::Release);
tracing::debug!(
"DeviceCommunicationControl: state set to {:?} ({}), duration={:?} min",
request.enable_disable,
new_state,
request.time_duration
);
Ok((request.enable_disable, request.time_duration))
}
pub fn handle_reinitialize_device(
service_data: &[u8],
reinit_password: &Option<String>,
) -> Result<(), Error> {
let request = ReinitializeDeviceRequest::decode(service_data)?;
validate_password(reinit_password, &request.password)?;
Ok(())
}
pub fn handle_get_event_information(
db: &ObjectDatabase,
service_data: &[u8],
buf: &mut BytesMut,
) -> Result<(), Error> {
const MAX_SUMMARIES: usize = 25;
let request = GetEventInformationRequest::decode(service_data)?;
let mut summaries = Vec::new();
let mut skipping = request.last_received_object_identifier.is_some();
let mut more_events = false;
for (oid, object) in db.iter_objects() {
if skipping {
if Some(oid) == request.last_received_object_identifier {
skipping = false;
}
continue;
}
if let Ok(PropertyValue::Enumerated(state)) =
object.read_property(PropertyIdentifier::EVENT_STATE, None)
{
if state != bacnet_types::enums::EventState::NORMAL.to_raw() {
if summaries.len() >= MAX_SUMMARIES {
more_events = true;
break;
}
let notification_class = object
.read_property(PropertyIdentifier::NOTIFICATION_CLASS, None)
.ok()
.and_then(|v| match v {
PropertyValue::Unsigned(n) => Some(n as u32),
_ => None,
})
.unwrap_or(0);
let event_enable = object
.read_property(PropertyIdentifier::EVENT_ENABLE, None)
.ok()
.and_then(|v| match v {
PropertyValue::BitString { data, .. } => data.first().map(|b| b >> 5),
_ => None,
})
.unwrap_or(0x07);
let notify_type = object
.read_property(PropertyIdentifier::NOTIFY_TYPE, None)
.ok()
.and_then(|v| match v {
PropertyValue::Enumerated(n) => Some(n),
_ => None,
})
.unwrap_or(0);
let event_priorities =
ObjectIdentifier::new(ObjectType::NOTIFICATION_CLASS, notification_class)
.ok()
.and_then(|nc_oid| db.get(&nc_oid))
.and_then(|nc_obj| {
nc_obj
.read_property(PropertyIdentifier::PRIORITY, None)
.ok()
})
.and_then(|v| match v {
PropertyValue::OctetString(bytes) if bytes.len() == 3 => {
Some([bytes[0] as u32, bytes[1] as u32, bytes[2] as u32])
}
_ => None,
})
.unwrap_or([0, 0, 0]);
let acked = object
.read_property(PropertyIdentifier::ACKED_TRANSITIONS, None)
.ok()
.and_then(|v| match v {
PropertyValue::BitString { data, .. } => data.first().map(|b| b >> 5),
_ => None,
})
.unwrap_or(0x07);
let event_timestamps = object
.read_property(PropertyIdentifier::EVENT_TIME_STAMPS, None)
.ok()
.and_then(|v| match v {
PropertyValue::List(items) if items.len() == 3 => None,
_ => None,
})
.unwrap_or([
BACnetTimeStamp::SequenceNumber(0),
BACnetTimeStamp::SequenceNumber(0),
BACnetTimeStamp::SequenceNumber(0),
]);
summaries.push(EventSummary {
object_identifier: oid,
event_state: state,
acknowledged_transitions: acked,
event_timestamps,
notify_type,
event_enable,
event_priorities,
notification_class,
});
}
}
}
let ack = GetEventInformationAck {
list_of_event_summaries: summaries,
more_events,
};
ack.encode(buf);
Ok(())
}
pub fn handle_acknowledge_alarm(db: &mut ObjectDatabase, service_data: &[u8]) -> Result<(), Error> {
let request = AcknowledgeAlarmRequest::decode(service_data)?;
let transition_bit: u8 = match EventState::from_raw(request.event_state_acknowledged) {
s if s == EventState::NORMAL => 0x04, s if s == EventState::FAULT => 0x02, _ => 0x01, };
let object = db
.get_mut(&request.event_object_identifier)
.ok_or(Error::Protocol {
class: ErrorClass::OBJECT.to_raw() as u32,
code: ErrorCode::UNKNOWN_OBJECT.to_raw() as u32,
})?;
object.acknowledge_alarm(transition_bit)?;
Ok(())
}
pub fn handle_subscribe_cov_property_multiple(
table: &mut CovSubscriptionTable,
db: &ObjectDatabase,
source_mac: &[u8],
service_data: &[u8],
) -> Result<(), Error> {
use bacnet_services::cov_multiple::SubscribeCOVPropertyMultipleRequest;
let request = SubscribeCOVPropertyMultipleRequest::decode(service_data)?;
let confirmed = request.issue_confirmed_notifications.unwrap_or(false);
for spec in &request.list_of_cov_subscription_specifications {
match db.get(&spec.monitored_object_identifier) {
None => {
return Err(Error::Protocol {
class: ErrorClass::OBJECT.to_raw() as u32,
code: ErrorCode::UNKNOWN_OBJECT.to_raw() as u32,
});
}
Some(obj) if !obj.supports_cov() => {
return Err(Error::Protocol {
class: ErrorClass::OBJECT.to_raw() as u32,
code: ErrorCode::OPTIONAL_FUNCTIONALITY_NOT_SUPPORTED.to_raw() as u32,
});
}
_ => {}
}
for cov_ref in &spec.list_of_cov_references {
table.subscribe(CovSubscription {
subscriber_mac: MacAddr::from_slice(source_mac),
subscriber_process_identifier: request.subscriber_process_identifier,
monitored_object_identifier: spec.monitored_object_identifier,
issue_confirmed_notifications: confirmed,
expires_at: None, last_notified_value: None,
monitored_property: Some(cov_ref.monitored_property.property_identifier),
monitored_property_array_index: cov_ref.monitored_property.property_array_index,
cov_increment: cov_ref.cov_increment,
});
}
}
Ok(())
}
pub fn handle_write_group(
service_data: &[u8],
) -> Result<bacnet_services::write_group::WriteGroupRequest, Error> {
bacnet_services::write_group::WriteGroupRequest::decode(service_data)
}
pub fn handle_get_enrollment_summary(
db: &ObjectDatabase,
service_data: &[u8],
buf: &mut BytesMut,
) -> Result<(), Error> {
use bacnet_services::enrollment_summary::{
EnrollmentSummaryEntry, GetEnrollmentSummaryAck, GetEnrollmentSummaryRequest,
};
let request = GetEnrollmentSummaryRequest::decode(service_data)?;
let mut entries = Vec::new();
for (_oid, object) in db.iter_objects() {
let oid = object.object_identifier();
let event_state = object
.read_property(PropertyIdentifier::EVENT_STATE, None)
.ok()
.and_then(|v| match v {
PropertyValue::Enumerated(e) => Some(e),
_ => None,
})
.unwrap_or(0);
if let Some(filter_state) = request.event_state_filter {
if event_state != filter_state.to_raw() {
continue;
}
}
let notification_class = object
.read_property(PropertyIdentifier::NOTIFICATION_CLASS, None)
.ok()
.and_then(|v| match v {
PropertyValue::Unsigned(n) => Some(n as u16),
_ => None,
})
.unwrap_or(0);
if let Some(nc_filter) = request.notification_class_filter {
if notification_class != nc_filter {
continue;
}
}
if let Some(ref pf) = request.priority_filter {
let priority = ObjectIdentifier::new(
bacnet_types::enums::ObjectType::NOTIFICATION_CLASS,
notification_class as u32,
)
.ok()
.and_then(|nc_oid| db.get(&nc_oid))
.and_then(|nc_obj| {
nc_obj
.read_property(PropertyIdentifier::PRIORITY, Some(1))
.ok()
.and_then(|v| match v {
PropertyValue::Unsigned(p) => Some(p as u8),
_ => None,
})
})
.unwrap_or(0);
if priority < pf.min_priority || priority > pf.max_priority {
continue;
}
}
if event_state == 0 && request.event_state_filter.is_none() {
continue;
}
entries.push(EnrollmentSummaryEntry {
object_identifier: oid,
event_type: bacnet_types::enums::EventType::CHANGE_OF_STATE,
event_state: bacnet_types::enums::EventState::from_raw(event_state),
priority: 0,
notification_class,
});
}
let ack = GetEnrollmentSummaryAck { entries };
ack.encode(buf);
Ok(())
}
pub fn handle_text_message(
service_data: &[u8],
) -> Result<bacnet_services::text_message::TextMessageRequest, Error> {
bacnet_services::text_message::TextMessageRequest::decode(service_data)
}
pub fn handle_life_safety_operation(service_data: &[u8]) -> Result<(), Error> {
let _request = bacnet_services::life_safety::LifeSafetyOperationRequest::decode(service_data)?;
Ok(())
}
pub fn handle_get_alarm_summary(db: &ObjectDatabase, buf: &mut BytesMut) -> Result<(), Error> {
use bacnet_services::alarm_summary::{AlarmSummaryEntry, GetAlarmSummaryAck};
let mut entries = Vec::new();
for (_oid, object) in db.iter_objects() {
let oid = object.object_identifier();
let event_state = object
.read_property(PropertyIdentifier::EVENT_STATE, None)
.ok()
.and_then(|v| match v {
PropertyValue::Enumerated(e) => Some(e),
_ => None,
})
.unwrap_or(0);
if event_state != 0 {
let acked = object
.read_property(PropertyIdentifier::ACKED_TRANSITIONS, None)
.ok()
.and_then(|v| match v {
PropertyValue::BitString {
unused_bits, data, ..
} => Some((unused_bits, data)),
_ => None,
})
.unwrap_or((5, vec![0xE0]));
entries.push(AlarmSummaryEntry {
object_identifier: oid,
alarm_state: bacnet_types::enums::EventState::from_raw(event_state),
acknowledged_transitions: acked,
});
}
}
let ack = GetAlarmSummaryAck { entries };
ack.encode(buf);
Ok(())
}
pub fn handle_read_range(
db: &ObjectDatabase,
service_data: &[u8],
buf: &mut BytesMut,
) -> Result<(), Error> {
use bacnet_services::read_range::{RangeSpec, ReadRangeAck, ReadRangeRequest};
let request = ReadRangeRequest::decode(service_data)?;
let object = db.get(&request.object_identifier).ok_or(Error::Protocol {
class: ErrorClass::OBJECT.to_raw() as u32,
code: ErrorCode::UNKNOWN_OBJECT.to_raw() as u32,
})?;
let value = object.read_property(request.property_identifier, request.property_array_index)?;
let items = match value {
PropertyValue::List(items) => items,
_ => {
return Err(Error::Protocol {
class: ErrorClass::SERVICES.to_raw() as u32,
code: ErrorCode::PROPERTY_IS_NOT_A_LIST.to_raw() as u32,
});
}
};
let total = items.len();
let (selected, first_item, last_item) = match &request.range {
None => (items, true, true),
Some(RangeSpec::ByPosition {
reference_index,
count,
}) => {
let ref_idx = *reference_index as usize;
let cnt = *count;
if cnt == 0 || total == 0 || ref_idx == 0 || ref_idx > total {
(Vec::new(), true, true)
} else if cnt > 0 {
let start = ref_idx - 1; let end = (start + cnt as usize).min(total);
let first = start == 0;
let last = end >= total;
(items[start..end].to_vec(), first, last)
} else {
let abs_count = cnt.unsigned_abs() as usize;
let end = ref_idx; let start = end.saturating_sub(abs_count);
let first = start == 0;
let last = end >= total;
(items[start..end].to_vec(), first, last)
}
}
Some(RangeSpec::BySequenceNumber {
reference_seq,
count,
}) => {
let ref_idx = *reference_seq as usize;
let cnt = *count;
if cnt == 0 || total == 0 {
(Vec::new(), true, true)
} else if cnt > 0 {
let start = ref_idx.min(total).saturating_sub(1);
let end = (start + cnt as usize).min(total);
let first = start == 0;
let last = end >= total;
(items[start..end].to_vec(), first, last)
} else {
let abs_count = cnt.unsigned_abs() as usize;
let end = ref_idx.min(total);
let start = end.saturating_sub(abs_count);
let first = start == 0;
let last = end >= total;
(items[start..end].to_vec(), first, last)
}
}
Some(RangeSpec::ByTime { .. }) => {
return Err(Error::Protocol {
class: ErrorClass::SERVICES.to_raw() as u32,
code: ErrorCode::SERVICE_REQUEST_DENIED.to_raw() as u32,
});
}
};
let mut item_data = BytesMut::new();
let mut encoded_count: u32 = 0;
for item in &selected {
if encode_property_value(&mut item_data, item).is_err() {
continue;
}
encoded_count += 1;
}
let item_count = encoded_count;
let ack = ReadRangeAck {
object_identifier: request.object_identifier,
property_identifier: request.property_identifier,
property_array_index: request.property_array_index,
result_flags: (first_item, last_item, false),
item_count,
item_data: item_data.to_vec(),
first_sequence_number: None,
};
ack.encode(buf);
Ok(())
}
pub fn handle_atomic_read_file(
db: &ObjectDatabase,
service_data: &[u8],
buf: &mut BytesMut,
) -> Result<(), Error> {
use bacnet_services::file::{
AtomicReadFileAck, AtomicReadFileRequest, FileAccessMethod, FileReadAckMethod,
};
let request = AtomicReadFileRequest::decode(service_data)?;
let object = db.get(&request.file_identifier).ok_or(Error::Protocol {
class: ErrorClass::OBJECT.to_raw() as u32,
code: ErrorCode::UNKNOWN_OBJECT.to_raw() as u32,
})?;
if request.file_identifier.object_type() != ObjectType::FILE {
return Err(Error::Protocol {
class: ErrorClass::OBJECT.to_raw() as u32,
code: ErrorCode::UNSUPPORTED_OBJECT_TYPE.to_raw() as u32,
});
}
let file_size = object
.read_property(PropertyIdentifier::FILE_SIZE, None)
.ok()
.and_then(|v| match v {
PropertyValue::Unsigned(n) => Some(n),
_ => None,
})
.unwrap_or(0);
match request.access {
FileAccessMethod::Stream {
file_start_position,
requested_octet_count,
} => {
let start = file_start_position.max(0) as u64;
let count = requested_octet_count as u64;
let end_of_file = start + count >= file_size;
let file_data = object
.read_property(PropertyIdentifier::from_raw(PROP_FILE_DATA), None)
.ok()
.and_then(|v| match v {
PropertyValue::OctetString(d) => Some(d),
_ => None,
})
.unwrap_or_default();
let s = start as usize;
let e = (s + count as usize).min(file_data.len());
let data = if s < file_data.len() {
file_data[s..e].to_vec()
} else {
Vec::new()
};
let ack = AtomicReadFileAck {
end_of_file,
access: FileReadAckMethod::Stream {
file_start_position,
file_data: data,
},
};
ack.encode(buf);
Ok(())
}
FileAccessMethod::Record {
file_start_record,
requested_record_count,
} => {
let start = file_start_record.max(0) as usize;
let count = requested_record_count as usize;
let record_count = object
.read_property(PropertyIdentifier::RECORD_COUNT, None)
.ok()
.and_then(|v| match v {
PropertyValue::Unsigned(n) => Some(n as usize),
_ => None,
})
.unwrap_or(0);
let end = (start + count).min(record_count);
let end_of_file = end >= record_count;
let records_data: Vec<Vec<u8>> = (start..end).map(|_| Vec::new()).collect();
let ack = AtomicReadFileAck {
end_of_file,
access: FileReadAckMethod::Record {
file_start_record,
returned_record_count: records_data.len() as u32,
file_record_data: records_data,
},
};
ack.encode(buf);
Ok(())
}
}
}
pub fn handle_atomic_write_file(
db: &mut ObjectDatabase,
service_data: &[u8],
buf: &mut BytesMut,
) -> Result<(), Error> {
use bacnet_services::file::{
AtomicWriteFileAck, AtomicWriteFileRequest, FileWriteAccessMethod, FileWriteAckMethod,
};
let request = AtomicWriteFileRequest::decode(service_data)?;
let object = db.get(&request.file_identifier).ok_or(Error::Protocol {
class: ErrorClass::OBJECT.to_raw() as u32,
code: ErrorCode::UNKNOWN_OBJECT.to_raw() as u32,
})?;
if request.file_identifier.object_type() != ObjectType::FILE {
return Err(Error::Protocol {
class: ErrorClass::OBJECT.to_raw() as u32,
code: ErrorCode::UNSUPPORTED_OBJECT_TYPE.to_raw() as u32,
});
}
let read_only = object
.read_property(PropertyIdentifier::READ_ONLY, None)
.ok()
.and_then(|v| match v {
PropertyValue::Boolean(b) => Some(b),
_ => None,
})
.unwrap_or(false);
if read_only {
return Err(Error::Protocol {
class: ErrorClass::OBJECT.to_raw() as u32,
code: ErrorCode::FILE_ACCESS_DENIED.to_raw() as u32,
});
}
match request.access {
FileWriteAccessMethod::Stream {
file_start_position,
file_data,
} => {
let object = db
.get_mut(&request.file_identifier)
.ok_or(Error::Protocol {
class: ErrorClass::OBJECT.to_raw() as u32,
code: ErrorCode::UNKNOWN_OBJECT.to_raw() as u32,
})?;
let mut existing = object
.read_property(PropertyIdentifier::from_raw(PROP_FILE_DATA), None)
.ok()
.and_then(|v| match v {
PropertyValue::OctetString(d) => Some(d),
_ => None,
})
.unwrap_or_default();
let start = file_start_position.max(0) as usize;
if start + file_data.len() > existing.len() {
existing.resize(start + file_data.len(), 0);
}
existing[start..start + file_data.len()].copy_from_slice(&file_data);
object.write_property(
PropertyIdentifier::from_raw(PROP_FILE_DATA),
None,
PropertyValue::OctetString(existing),
None,
)?;
let ack = AtomicWriteFileAck {
access: FileWriteAckMethod::Stream {
file_start_position,
},
};
ack.encode(buf);
Ok(())
}
FileWriteAccessMethod::Record {
file_start_record, ..
} => {
let ack = AtomicWriteFileAck {
access: FileWriteAckMethod::Record { file_start_record },
};
ack.encode(buf);
Ok(())
}
}
}
pub fn handle_add_list_element(db: &mut ObjectDatabase, service_data: &[u8]) -> Result<(), Error> {
use bacnet_encoding::primitives::decode_application_value;
use bacnet_services::list_manipulation::ListElementRequest;
let request = ListElementRequest::decode(service_data)?;
let object = db
.get_mut(&request.object_identifier)
.ok_or(Error::Protocol {
class: ErrorClass::OBJECT.to_raw() as u32,
code: ErrorCode::UNKNOWN_OBJECT.to_raw() as u32,
})?;
let current =
object.read_property(request.property_identifier, request.property_array_index)?;
let mut items = match current {
PropertyValue::List(items) => items,
_ => Vec::new(),
};
let mut offset = 0;
let data = &request.list_of_elements;
while offset < data.len() {
match decode_application_value(data, offset) {
Ok((val, new_offset)) => {
items.push(val);
offset = new_offset;
}
Err(_) => break,
}
}
object.write_property(
request.property_identifier,
request.property_array_index,
PropertyValue::List(items),
None,
)?;
Ok(())
}
pub fn handle_remove_list_element(
db: &mut ObjectDatabase,
service_data: &[u8],
) -> Result<(), Error> {
use bacnet_encoding::primitives::decode_application_value;
use bacnet_services::list_manipulation::ListElementRequest;
let request = ListElementRequest::decode(service_data)?;
let object = db
.get_mut(&request.object_identifier)
.ok_or(Error::Protocol {
class: ErrorClass::OBJECT.to_raw() as u32,
code: ErrorCode::UNKNOWN_OBJECT.to_raw() as u32,
})?;
let current =
object.read_property(request.property_identifier, request.property_array_index)?;
let mut items = match current {
PropertyValue::List(items) => items,
_ => Vec::new(),
};
let mut to_remove = Vec::new();
let mut offset = 0;
let data = &request.list_of_elements;
while offset < data.len() {
match decode_application_value(data, offset) {
Ok((val, new_offset)) => {
to_remove.push(val);
offset = new_offset;
}
Err(_) => break,
}
}
items.retain(|item| !to_remove.contains(item));
object.write_property(
request.property_identifier,
request.property_array_index,
PropertyValue::List(items),
None,
)?;
Ok(())
}
#[cfg(test)]
mod tests {
use super::*;
use bacnet_objects::analog::AnalogInputObject;
use bacnet_objects::traits::BACnetObject;
fn make_db_with_ai() -> ObjectDatabase {
let mut db = ObjectDatabase::new();
let mut ai = AnalogInputObject::new(1, "AI-1", 62).unwrap();
ai.set_present_value(72.5);
db.add(Box::new(ai)).unwrap();
db
}
#[test]
fn read_property_handler_success() {
let db = make_db_with_ai();
let oid = ObjectIdentifier::new(ObjectType::ANALOG_INPUT, 1).unwrap();
let request = ReadPropertyRequest {
object_identifier: oid,
property_identifier: PropertyIdentifier::PRESENT_VALUE,
property_array_index: None,
};
let mut buf = BytesMut::new();
request.encode(&mut buf);
let mut ack_buf = BytesMut::new();
handle_read_property(&db, &buf, &mut ack_buf).unwrap();
let ack_bytes = ack_buf.to_vec();
let ack = ReadPropertyACK::decode(&ack_bytes).unwrap();
assert_eq!(ack.object_identifier, oid);
assert_eq!(ack.property_identifier, PropertyIdentifier::PRESENT_VALUE);
let (val, _) =
bacnet_encoding::primitives::decode_application_value(&ack.property_value, 0).unwrap();
assert_eq!(val, bacnet_types::primitives::PropertyValue::Real(72.5));
}
#[test]
fn read_property_unknown_object() {
let db = make_db_with_ai();
let oid = ObjectIdentifier::new(ObjectType::ANALOG_INPUT, 99).unwrap();
let request = ReadPropertyRequest {
object_identifier: oid,
property_identifier: PropertyIdentifier::PRESENT_VALUE,
property_array_index: None,
};
let mut buf = BytesMut::new();
request.encode(&mut buf);
let mut ack_buf = BytesMut::new();
let result = handle_read_property(&db, &buf, &mut ack_buf);
assert!(result.is_err());
}
#[test]
fn read_property_unknown_property() {
let db = make_db_with_ai();
let oid = ObjectIdentifier::new(ObjectType::ANALOG_INPUT, 1).unwrap();
let request = ReadPropertyRequest {
object_identifier: oid,
property_identifier: PropertyIdentifier::PRIORITY_ARRAY,
property_array_index: None,
};
let mut buf = BytesMut::new();
request.encode(&mut buf);
let mut ack_buf = BytesMut::new();
let result = handle_read_property(&db, &buf, &mut ack_buf);
assert!(result.is_err());
}
#[test]
fn write_property_handler_success() {
let mut db = ObjectDatabase::new();
let bv = bacnet_objects::binary::BinaryValueObject::new(1, "BV-1").unwrap();
db.add(Box::new(bv)).unwrap();
let oid = ObjectIdentifier::new(ObjectType::BINARY_VALUE, 1).unwrap();
let mut value_buf = BytesMut::new();
bacnet_encoding::primitives::encode_app_enumerated(&mut value_buf, 1);
let request = WritePropertyRequest {
object_identifier: oid,
property_identifier: PropertyIdentifier::PRESENT_VALUE,
property_array_index: None,
property_value: value_buf.to_vec(),
priority: None,
};
let mut buf = BytesMut::new();
request.encode(&mut buf);
handle_write_property(&mut db, &buf).unwrap();
let obj = db.get(&oid).unwrap();
let val = obj
.read_property(PropertyIdentifier::PRESENT_VALUE, None)
.unwrap();
assert_eq!(val, bacnet_types::primitives::PropertyValue::Enumerated(1));
}
#[test]
fn rpm_handler_success() {
let db = make_db_with_ai();
let oid = ObjectIdentifier::new(ObjectType::ANALOG_INPUT, 1).unwrap();
use bacnet_services::common::PropertyReference;
use bacnet_services::rpm::ReadAccessSpecification;
let request = bacnet_services::rpm::ReadPropertyMultipleRequest {
list_of_read_access_specs: vec![ReadAccessSpecification {
object_identifier: oid,
list_of_property_references: vec![
PropertyReference {
property_identifier: PropertyIdentifier::PRESENT_VALUE,
property_array_index: None,
},
PropertyReference {
property_identifier: PropertyIdentifier::OBJECT_NAME,
property_array_index: None,
},
],
}],
};
let mut buf = BytesMut::new();
request.encode(&mut buf);
let mut ack_buf = BytesMut::new();
handle_read_property_multiple(&db, &buf, &mut ack_buf).unwrap();
let ack_bytes = ack_buf.to_vec();
let ack = bacnet_services::rpm::ReadPropertyMultipleACK::decode(&ack_bytes).unwrap();
assert_eq!(ack.list_of_read_access_results.len(), 1);
let result = &ack.list_of_read_access_results[0];
assert_eq!(result.object_identifier, oid);
assert_eq!(result.list_of_results.len(), 2);
assert!(result.list_of_results[0].property_value.is_some());
assert!(result.list_of_results[1].property_value.is_some());
let (val, _) = bacnet_encoding::primitives::decode_application_value(
result.list_of_results[0].property_value.as_ref().unwrap(),
0,
)
.unwrap();
assert_eq!(val, bacnet_types::primitives::PropertyValue::Real(72.5));
}
#[test]
fn rpm_handler_unknown_property_returns_inline_error() {
let db = make_db_with_ai();
let oid = ObjectIdentifier::new(ObjectType::ANALOG_INPUT, 1).unwrap();
use bacnet_services::common::PropertyReference;
use bacnet_services::rpm::ReadAccessSpecification;
let request = bacnet_services::rpm::ReadPropertyMultipleRequest {
list_of_read_access_specs: vec![ReadAccessSpecification {
object_identifier: oid,
list_of_property_references: vec![
PropertyReference {
property_identifier: PropertyIdentifier::PRESENT_VALUE,
property_array_index: None,
},
PropertyReference {
property_identifier: PropertyIdentifier::PRIORITY_ARRAY,
property_array_index: None,
},
],
}],
};
let mut buf = BytesMut::new();
request.encode(&mut buf);
let mut ack_buf = BytesMut::new();
handle_read_property_multiple(&db, &buf, &mut ack_buf).unwrap();
let ack_bytes = ack_buf.to_vec();
let ack = bacnet_services::rpm::ReadPropertyMultipleACK::decode(&ack_bytes).unwrap();
let result = &ack.list_of_read_access_results[0];
assert!(result.list_of_results[0].property_value.is_some()); assert!(result.list_of_results[1].error.is_some()); }
#[test]
fn rpm_handler_unknown_object_returns_inline_error() {
let db = make_db_with_ai();
let oid = ObjectIdentifier::new(ObjectType::ANALOG_INPUT, 99).unwrap();
use bacnet_services::common::PropertyReference;
use bacnet_services::rpm::ReadAccessSpecification;
let request = bacnet_services::rpm::ReadPropertyMultipleRequest {
list_of_read_access_specs: vec![ReadAccessSpecification {
object_identifier: oid,
list_of_property_references: vec![PropertyReference {
property_identifier: PropertyIdentifier::PRESENT_VALUE,
property_array_index: None,
}],
}],
};
let mut buf = BytesMut::new();
request.encode(&mut buf);
let mut ack_buf = BytesMut::new();
handle_read_property_multiple(&db, &buf, &mut ack_buf).unwrap();
let ack_bytes = ack_buf.to_vec();
let ack = bacnet_services::rpm::ReadPropertyMultipleACK::decode(&ack_bytes).unwrap();
let result = &ack.list_of_read_access_results[0];
assert!(result.list_of_results[0].error.is_some());
}
#[test]
fn rpm_handler_all_properties_expanded() {
let db = make_db_with_ai();
let oid = ObjectIdentifier::new(ObjectType::ANALOG_INPUT, 1).unwrap();
use bacnet_services::common::PropertyReference;
use bacnet_services::rpm::ReadAccessSpecification;
let request = bacnet_services::rpm::ReadPropertyMultipleRequest {
list_of_read_access_specs: vec![ReadAccessSpecification {
object_identifier: oid,
list_of_property_references: vec![PropertyReference {
property_identifier: PropertyIdentifier::ALL,
property_array_index: None,
}],
}],
};
let mut buf = BytesMut::new();
request.encode(&mut buf);
let mut ack_buf = BytesMut::new();
handle_read_property_multiple(&db, &buf, &mut ack_buf).unwrap();
let ack_bytes = ack_buf.to_vec();
let ack = bacnet_services::rpm::ReadPropertyMultipleACK::decode(&ack_bytes).unwrap();
assert_eq!(ack.list_of_read_access_results.len(), 1);
let result = &ack.list_of_read_access_results[0];
assert_eq!(result.object_identifier, oid);
let obj = db.get(&oid).unwrap();
let expected_props = obj.property_list();
assert!(
expected_props.len() > 2,
"sanity: AI should have many properties"
);
assert_eq!(result.list_of_results.len(), expected_props.len());
for (elem, &expected_pid) in result.list_of_results.iter().zip(expected_props.iter()) {
assert_eq!(elem.property_identifier, expected_pid);
}
let pv_elem = result
.list_of_results
.iter()
.find(|e| e.property_identifier == PropertyIdentifier::PRESENT_VALUE)
.expect("PRESENT_VALUE should be in ALL results");
assert!(pv_elem.property_value.is_some());
}
#[test]
fn rpm_handler_required_vs_optional() {
let db = make_db_with_ai();
let oid = ObjectIdentifier::new(ObjectType::ANALOG_INPUT, 1).unwrap();
use bacnet_services::common::PropertyReference;
use bacnet_services::rpm::ReadAccessSpecification;
let req_required = bacnet_services::rpm::ReadPropertyMultipleRequest {
list_of_read_access_specs: vec![ReadAccessSpecification {
object_identifier: oid,
list_of_property_references: vec![PropertyReference {
property_identifier: PropertyIdentifier::REQUIRED,
property_array_index: None,
}],
}],
};
let mut buf = BytesMut::new();
req_required.encode(&mut buf);
let mut ack_buf = BytesMut::new();
handle_read_property_multiple(&db, &buf, &mut ack_buf).unwrap();
let ack_bytes = ack_buf.to_vec();
let ack = bacnet_services::rpm::ReadPropertyMultipleACK::decode(&ack_bytes).unwrap();
let required_results = &ack.list_of_read_access_results[0].list_of_results;
let req_optional = bacnet_services::rpm::ReadPropertyMultipleRequest {
list_of_read_access_specs: vec![ReadAccessSpecification {
object_identifier: oid,
list_of_property_references: vec![PropertyReference {
property_identifier: PropertyIdentifier::OPTIONAL,
property_array_index: None,
}],
}],
};
let mut buf = BytesMut::new();
req_optional.encode(&mut buf);
let mut ack_buf = BytesMut::new();
handle_read_property_multiple(&db, &buf, &mut ack_buf).unwrap();
let ack_bytes = ack_buf.to_vec();
let ack = bacnet_services::rpm::ReadPropertyMultipleACK::decode(&ack_bytes).unwrap();
let optional_results = &ack.list_of_read_access_results[0].list_of_results;
let req_pids: Vec<_> = required_results
.iter()
.map(|r| r.property_identifier)
.collect();
assert!(req_pids.contains(&PropertyIdentifier::OBJECT_IDENTIFIER));
assert!(req_pids.contains(&PropertyIdentifier::OBJECT_NAME));
assert!(req_pids.contains(&PropertyIdentifier::OBJECT_TYPE));
assert!(req_pids.contains(&PropertyIdentifier::PROPERTY_LIST));
let opt_pids: Vec<_> = optional_results
.iter()
.map(|r| r.property_identifier)
.collect();
for req_pid in &req_pids {
assert!(
!opt_pids.contains(req_pid),
"OPTIONAL should not contain {req_pid:?}"
);
}
let obj = db.get(&oid).unwrap();
let all_pids = obj.property_list();
let required_set: std::collections::HashSet<_> = req_pids.iter().collect();
let optional_set: std::collections::HashSet<_> = opt_pids.iter().collect();
for pid in all_pids.iter() {
assert!(
required_set.contains(pid) || optional_set.contains(pid),
"ALL property {pid:?} missing from REQUIRED and OPTIONAL"
);
}
}
#[test]
fn wpm_handler_success() {
let mut db = ObjectDatabase::new();
let bv = bacnet_objects::binary::BinaryValueObject::new(1, "BV-1").unwrap();
db.add(Box::new(bv)).unwrap();
let oid = ObjectIdentifier::new(ObjectType::BINARY_VALUE, 1).unwrap();
let mut value_buf = BytesMut::new();
bacnet_encoding::primitives::encode_app_enumerated(&mut value_buf, 1);
use bacnet_services::common::BACnetPropertyValue;
use bacnet_services::wpm::WriteAccessSpecification;
let request = bacnet_services::wpm::WritePropertyMultipleRequest {
list_of_write_access_specs: vec![WriteAccessSpecification {
object_identifier: oid,
list_of_properties: vec![BACnetPropertyValue {
property_identifier: PropertyIdentifier::PRESENT_VALUE,
property_array_index: None,
value: value_buf.to_vec(),
priority: None,
}],
}],
};
let mut buf = BytesMut::new();
request.encode(&mut buf);
handle_write_property_multiple(&mut db, &buf).unwrap();
let obj = db.get(&oid).unwrap();
let val = obj
.read_property(PropertyIdentifier::PRESENT_VALUE, None)
.unwrap();
assert_eq!(val, bacnet_types::primitives::PropertyValue::Enumerated(1));
}
#[test]
fn subscribe_cov_handler_success() {
let db = make_db_with_ai();
let mut table = CovSubscriptionTable::new();
let mac = vec![192, 168, 1, 1, 0xBA, 0xC0];
let oid = ObjectIdentifier::new(ObjectType::ANALOG_INPUT, 1).unwrap();
let request = SubscribeCOVRequest {
subscriber_process_identifier: 1,
monitored_object_identifier: oid,
issue_confirmed_notifications: Some(false),
lifetime: Some(300),
};
let mut buf = BytesMut::new();
request.encode(&mut buf);
handle_subscribe_cov(&mut table, &db, &mac, &buf).unwrap();
assert_eq!(table.len(), 1);
}
#[test]
fn subscribe_cov_unknown_object_fails() {
let db = make_db_with_ai();
let mut table = CovSubscriptionTable::new();
let mac = vec![192, 168, 1, 1, 0xBA, 0xC0];
let oid = ObjectIdentifier::new(ObjectType::ANALOG_INPUT, 99).unwrap();
let request = SubscribeCOVRequest {
subscriber_process_identifier: 1,
monitored_object_identifier: oid,
issue_confirmed_notifications: Some(false),
lifetime: Some(300),
};
let mut buf = BytesMut::new();
request.encode(&mut buf);
assert!(handle_subscribe_cov(&mut table, &db, &mac, &buf).is_err());
assert!(table.is_empty());
}
#[test]
fn subscribe_cov_cancellation() {
let db = make_db_with_ai();
let mut table = CovSubscriptionTable::new();
let mac = vec![192, 168, 1, 1, 0xBA, 0xC0];
let oid = ObjectIdentifier::new(ObjectType::ANALOG_INPUT, 1).unwrap();
let request = SubscribeCOVRequest {
subscriber_process_identifier: 1,
monitored_object_identifier: oid,
issue_confirmed_notifications: Some(false),
lifetime: Some(300),
};
let mut buf = BytesMut::new();
request.encode(&mut buf);
handle_subscribe_cov(&mut table, &db, &mac, &buf).unwrap();
assert_eq!(table.len(), 1);
let cancel = SubscribeCOVRequest {
subscriber_process_identifier: 1,
monitored_object_identifier: oid,
issue_confirmed_notifications: None,
lifetime: None,
};
let mut buf = BytesMut::new();
cancel.encode(&mut buf);
handle_subscribe_cov(&mut table, &db, &mac, &buf).unwrap();
assert!(table.is_empty());
}
#[test]
fn who_has_by_id_found() {
let db = make_db_with_ai();
let device_oid = ObjectIdentifier::new(ObjectType::DEVICE, 1).unwrap();
let ai_oid = ObjectIdentifier::new(ObjectType::ANALOG_INPUT, 1).unwrap();
let request = WhoHasRequest {
low_limit: None,
high_limit: None,
object: WhoHasObject::Identifier(ai_oid),
};
let mut buf = BytesMut::new();
request.encode(&mut buf).unwrap();
let result = handle_who_has(&db, &buf, device_oid).unwrap();
assert!(result.is_some());
let i_have = result.unwrap();
assert_eq!(i_have.object_identifier, ai_oid);
assert_eq!(i_have.object_name, "AI-1");
}
#[test]
fn who_has_by_name_found() {
let db = make_db_with_ai();
let device_oid = ObjectIdentifier::new(ObjectType::DEVICE, 1).unwrap();
let request = WhoHasRequest {
low_limit: None,
high_limit: None,
object: WhoHasObject::Name("AI-1".into()),
};
let mut buf = BytesMut::new();
request.encode(&mut buf).unwrap();
let result = handle_who_has(&db, &buf, device_oid).unwrap();
assert!(result.is_some());
}
#[test]
fn who_has_not_found() {
let db = make_db_with_ai();
let device_oid = ObjectIdentifier::new(ObjectType::DEVICE, 1).unwrap();
let missing_oid = ObjectIdentifier::new(ObjectType::ANALOG_INPUT, 99).unwrap();
let request = WhoHasRequest {
low_limit: None,
high_limit: None,
object: WhoHasObject::Identifier(missing_oid),
};
let mut buf = BytesMut::new();
request.encode(&mut buf).unwrap();
let result = handle_who_has(&db, &buf, device_oid).unwrap();
assert!(result.is_none());
}
#[test]
fn who_has_out_of_range() {
let db = make_db_with_ai();
let device_oid = ObjectIdentifier::new(ObjectType::DEVICE, 1).unwrap();
let ai_oid = ObjectIdentifier::new(ObjectType::ANALOG_INPUT, 1).unwrap();
let request = WhoHasRequest {
low_limit: Some(100),
high_limit: Some(200),
object: WhoHasObject::Identifier(ai_oid),
};
let mut buf = BytesMut::new();
request.encode(&mut buf).unwrap();
let result = handle_who_has(&db, &buf, device_oid).unwrap();
assert!(result.is_none()); }
#[test]
fn delete_object_success() {
let mut db = make_db_with_ai();
let oid = ObjectIdentifier::new(ObjectType::ANALOG_INPUT, 1).unwrap();
let request = bacnet_services::object_mgmt::DeleteObjectRequest {
object_identifier: oid,
};
let mut buf = BytesMut::new();
request.encode(&mut buf);
handle_delete_object(&mut db, &buf).unwrap();
assert!(db.get(&oid).is_none());
}
#[test]
fn delete_object_unknown_fails() {
let mut db = make_db_with_ai();
let oid = ObjectIdentifier::new(ObjectType::ANALOG_INPUT, 99).unwrap();
let request = bacnet_services::object_mgmt::DeleteObjectRequest {
object_identifier: oid,
};
let mut buf = BytesMut::new();
request.encode(&mut buf);
assert!(handle_delete_object(&mut db, &buf).is_err());
}
#[test]
fn delete_device_object_fails() {
let mut db = ObjectDatabase::new();
let device =
bacnet_objects::device::DeviceObject::new(bacnet_objects::device::DeviceConfig {
instance: 1,
name: "Dev".into(),
..Default::default()
})
.unwrap();
db.add(Box::new(device)).unwrap();
let oid = ObjectIdentifier::new(ObjectType::DEVICE, 1).unwrap();
let request = bacnet_services::object_mgmt::DeleteObjectRequest {
object_identifier: oid,
};
let mut buf = BytesMut::new();
request.encode(&mut buf);
assert!(handle_delete_object(&mut db, &buf).is_err());
}
#[test]
fn device_communication_control_handler() {
let comm_state = AtomicU8::new(0);
let request = bacnet_services::device_mgmt::DeviceCommunicationControlRequest {
time_duration: Some(60),
enable_disable: EnableDisable::DISABLE_INITIATION,
password: None,
};
let mut buf = BytesMut::new();
request.encode(&mut buf).unwrap();
let (state, duration) =
handle_device_communication_control(&buf, &comm_state, &None).unwrap();
assert_eq!(state, EnableDisable::DISABLE_INITIATION);
assert_eq!(duration, Some(60));
assert_eq!(comm_state.load(Ordering::Acquire), 2);
}
#[test]
fn device_communication_control_enable() {
let comm_state = AtomicU8::new(1);
let request = bacnet_services::device_mgmt::DeviceCommunicationControlRequest {
time_duration: None,
enable_disable: EnableDisable::ENABLE,
password: None,
};
let mut buf = BytesMut::new();
request.encode(&mut buf).unwrap();
let (state, duration) =
handle_device_communication_control(&buf, &comm_state, &None).unwrap();
assert_eq!(state, EnableDisable::ENABLE);
assert_eq!(duration, None);
assert_eq!(comm_state.load(Ordering::Acquire), 0);
}
#[test]
fn device_communication_control_disable_initiation() {
let comm_state = AtomicU8::new(0);
let request = bacnet_services::device_mgmt::DeviceCommunicationControlRequest {
time_duration: None,
enable_disable: EnableDisable::DISABLE_INITIATION,
password: None,
};
let mut buf = BytesMut::new();
request.encode(&mut buf).unwrap();
let (state, duration) =
handle_device_communication_control(&buf, &comm_state, &None).unwrap();
assert_eq!(state, EnableDisable::DISABLE_INITIATION);
assert_eq!(duration, None);
assert_eq!(comm_state.load(Ordering::Acquire), 2);
}
#[test]
fn reinitialize_device_handler() {
let request = bacnet_services::device_mgmt::ReinitializeDeviceRequest {
reinitialized_state: bacnet_types::enums::ReinitializedState::WARMSTART,
password: None,
};
let mut buf = BytesMut::new();
request.encode(&mut buf).unwrap();
handle_reinitialize_device(&buf, &None).unwrap();
}
#[test]
fn get_event_information_empty() {
let db = make_db_with_ai();
let request = bacnet_services::alarm_event::GetEventInformationRequest {
last_received_object_identifier: None,
};
let mut buf = BytesMut::new();
request.encode(&mut buf);
let mut ack_buf = BytesMut::new();
handle_get_event_information(&db, &buf, &mut ack_buf).unwrap();
let ack_bytes = ack_buf.to_vec();
assert!(!ack_bytes.is_empty());
}
#[test]
fn get_event_information_reports_non_normal_objects() {
use bacnet_objects::event::LimitEnable;
let mut db = ObjectDatabase::new();
let mut ai = AnalogInputObject::new(1, "AI-1", 62).unwrap();
ai.write_property(
PropertyIdentifier::HIGH_LIMIT,
None,
PropertyValue::Real(80.0),
None,
)
.unwrap();
ai.write_property(
PropertyIdentifier::LOW_LIMIT,
None,
PropertyValue::Real(20.0),
None,
)
.unwrap();
ai.write_property(
PropertyIdentifier::DEADBAND,
None,
PropertyValue::Real(2.0),
None,
)
.unwrap();
ai.write_property(
PropertyIdentifier::LIMIT_ENABLE,
None,
PropertyValue::BitString {
unused_bits: 6,
data: vec![LimitEnable::BOTH.to_bits()],
},
None,
)
.unwrap();
ai.write_property(
PropertyIdentifier::NOTIFICATION_CLASS,
None,
PropertyValue::Unsigned(5),
None,
)
.unwrap();
ai.set_present_value(85.0);
ai.evaluate_intrinsic_reporting(); db.add(Box::new(ai)).unwrap();
let request = bacnet_services::alarm_event::GetEventInformationRequest {
last_received_object_identifier: None,
};
let mut buf = BytesMut::new();
request.encode(&mut buf);
let mut ack_buf = BytesMut::new();
handle_get_event_information(&db, &buf, &mut ack_buf).unwrap();
let ack_bytes = ack_buf.to_vec();
assert!(ack_bytes.len() > 5); }
#[test]
fn get_event_information_reads_event_enable_and_notify_type() {
use bacnet_objects::event::LimitEnable;
use bacnet_objects::notification_class::NotificationClass;
let mut db = ObjectDatabase::new();
let mut ai = AnalogInputObject::new(1, "AI-1", 62).unwrap();
ai.write_property(
PropertyIdentifier::HIGH_LIMIT,
None,
PropertyValue::Real(80.0),
None,
)
.unwrap();
ai.write_property(
PropertyIdentifier::LOW_LIMIT,
None,
PropertyValue::Real(20.0),
None,
)
.unwrap();
ai.write_property(
PropertyIdentifier::DEADBAND,
None,
PropertyValue::Real(2.0),
None,
)
.unwrap();
ai.write_property(
PropertyIdentifier::LIMIT_ENABLE,
None,
PropertyValue::BitString {
unused_bits: 6,
data: vec![LimitEnable::BOTH.to_bits()],
},
None,
)
.unwrap();
ai.write_property(
PropertyIdentifier::EVENT_ENABLE,
None,
PropertyValue::BitString {
unused_bits: 5,
data: vec![0x05 << 5],
},
None,
)
.unwrap();
ai.write_property(
PropertyIdentifier::NOTIFY_TYPE,
None,
PropertyValue::Enumerated(1),
None,
)
.unwrap();
ai.write_property(
PropertyIdentifier::NOTIFICATION_CLASS,
None,
PropertyValue::Unsigned(5),
None,
)
.unwrap();
ai.set_present_value(85.0);
ai.evaluate_intrinsic_reporting();
db.add(Box::new(ai)).unwrap();
let mut nc = NotificationClass::new(5, "NC-5").unwrap();
nc.priority = [100, 150, 200];
db.add(Box::new(nc)).unwrap();
let request = bacnet_services::alarm_event::GetEventInformationRequest {
last_received_object_identifier: None,
};
let mut buf = BytesMut::new();
request.encode(&mut buf);
let mut ack_buf = BytesMut::new();
handle_get_event_information(&db, &buf, &mut ack_buf).unwrap();
let ack_bytes = ack_buf.to_vec();
assert!(
ack_bytes.len() > 10,
"ACK should contain event summary data"
);
let ai_oid = ObjectIdentifier::new(ObjectType::ANALOG_INPUT, 1).unwrap();
let ai_ref = db.get(&ai_oid).unwrap();
let ev_enable = ai_ref
.read_property(PropertyIdentifier::EVENT_ENABLE, None)
.unwrap();
match ev_enable {
PropertyValue::BitString { data, .. } => {
assert_eq!(data[0] >> 5, 0x05, "EVENT_ENABLE should be 0x05");
}
other => panic!("expected BitString, got {:?}", other),
}
let notify = ai_ref
.read_property(PropertyIdentifier::NOTIFY_TYPE, None)
.unwrap();
assert_eq!(
notify,
PropertyValue::Enumerated(1),
"NOTIFY_TYPE should be EVENT(1)"
);
}
#[test]
fn wpm_handler_unknown_object_fails() {
let mut db = make_db_with_ai();
let oid = ObjectIdentifier::new(ObjectType::BINARY_VALUE, 99).unwrap();
use bacnet_services::common::BACnetPropertyValue;
use bacnet_services::wpm::WriteAccessSpecification;
let request = bacnet_services::wpm::WritePropertyMultipleRequest {
list_of_write_access_specs: vec![WriteAccessSpecification {
object_identifier: oid,
list_of_properties: vec![BACnetPropertyValue {
property_identifier: PropertyIdentifier::PRESENT_VALUE,
property_array_index: None,
value: vec![0x91, 0x01],
priority: None,
}],
}],
};
let mut buf = BytesMut::new();
request.encode(&mut buf);
assert!(handle_write_property_multiple(&mut db, &buf).is_err());
}
#[test]
fn wpm_handler_atomicity_rollback() {
let mut db = make_db_with_ai();
let oid = ObjectIdentifier::new(ObjectType::ANALOG_INPUT, 1).unwrap();
let original_hl = match db
.get(&oid)
.unwrap()
.read_property(PropertyIdentifier::HIGH_LIMIT, None)
.unwrap()
{
PropertyValue::Real(v) => v,
_ => panic!("expected Real"),
};
let mut hl_buf = BytesMut::new();
bacnet_encoding::primitives::encode_app_real(&mut hl_buf, 999.0);
let mut ot_buf = BytesMut::new();
bacnet_encoding::primitives::encode_app_enumerated(&mut ot_buf, 0);
use bacnet_services::common::BACnetPropertyValue;
use bacnet_services::wpm::WriteAccessSpecification;
let request = bacnet_services::wpm::WritePropertyMultipleRequest {
list_of_write_access_specs: vec![WriteAccessSpecification {
object_identifier: oid,
list_of_properties: vec![
BACnetPropertyValue {
property_identifier: PropertyIdentifier::HIGH_LIMIT,
property_array_index: None,
value: hl_buf.to_vec(),
priority: None,
},
BACnetPropertyValue {
property_identifier: PropertyIdentifier::OBJECT_TYPE,
property_array_index: None,
value: ot_buf.to_vec(),
priority: None,
},
],
}],
};
let mut buf = BytesMut::new();
request.encode(&mut buf);
assert!(handle_write_property_multiple(&mut db, &buf).is_err());
let after_hl = match db
.get(&oid)
.unwrap()
.read_property(PropertyIdentifier::HIGH_LIMIT, None)
.unwrap()
{
PropertyValue::Real(v) => v,
_ => panic!("expected Real"),
};
assert_eq!(
original_hl, after_hl,
"HIGH_LIMIT should be rolled back after failed WPM"
);
}
fn make_db_with_device_and_ai() -> ObjectDatabase {
let mut db = ObjectDatabase::new();
let device =
bacnet_objects::device::DeviceObject::new(bacnet_objects::device::DeviceConfig {
instance: 1,
name: "TestDevice".into(),
..Default::default()
})
.unwrap();
db.add(Box::new(device)).unwrap();
db.add(Box::new(AnalogInputObject::new(1, "AI-1", 62).unwrap()))
.unwrap();
db
}
#[test]
fn create_object_by_type_assigns_next_instance() {
let mut db = make_db_with_device_and_ai();
let req = CreateObjectRequest {
object_specifier: ObjectSpecifier::Type(ObjectType::ANALOG_INPUT),
list_of_initial_values: vec![],
};
let mut buf = BytesMut::new();
req.encode(&mut buf);
let mut ack_buf = BytesMut::new();
let result = handle_create_object(&mut db, &buf, &mut ack_buf);
assert!(result.is_ok());
assert_eq!(db.len(), 3);
let ai2_oid = ObjectIdentifier::new(ObjectType::ANALOG_INPUT, 2).unwrap();
assert!(db.get(&ai2_oid).is_some());
}
#[test]
fn create_object_by_identifier() {
let mut db = make_db_with_device_and_ai();
let target_oid = ObjectIdentifier::new(ObjectType::BINARY_INPUT, 99).unwrap();
let req = CreateObjectRequest {
object_specifier: ObjectSpecifier::Identifier(target_oid),
list_of_initial_values: vec![],
};
let mut buf = BytesMut::new();
req.encode(&mut buf);
let mut ack_buf = BytesMut::new();
let result = handle_create_object(&mut db, &buf, &mut ack_buf);
assert!(result.is_ok());
assert!(db.get(&target_oid).is_some());
}
#[test]
fn create_object_duplicate_fails() {
let mut db = make_db_with_device_and_ai();
let existing_oid = ObjectIdentifier::new(ObjectType::ANALOG_INPUT, 1).unwrap();
let req = CreateObjectRequest {
object_specifier: ObjectSpecifier::Identifier(existing_oid),
list_of_initial_values: vec![],
};
let mut buf = BytesMut::new();
req.encode(&mut buf);
let mut ack_buf = BytesMut::new();
let result = handle_create_object(&mut db, &buf, &mut ack_buf);
assert!(result.is_err());
}
#[test]
fn create_unsupported_type_fails() {
let mut db = make_db_with_device_and_ai();
let req = CreateObjectRequest {
object_specifier: ObjectSpecifier::Type(ObjectType::DEVICE),
list_of_initial_values: vec![],
};
let mut buf = BytesMut::new();
req.encode(&mut buf);
let mut ack_buf = BytesMut::new();
let result = handle_create_object(&mut db, &buf, &mut ack_buf);
assert!(result.is_err());
}
#[test]
fn create_object_with_initial_values() {
let mut db = make_db_with_device_and_ai();
let mut desc_buf = BytesMut::new();
bacnet_encoding::primitives::encode_app_character_string(&mut desc_buf, "Test AI").unwrap();
let req = CreateObjectRequest {
object_specifier: ObjectSpecifier::Type(ObjectType::ANALOG_INPUT),
list_of_initial_values: vec![bacnet_services::common::BACnetPropertyValue {
property_identifier: PropertyIdentifier::DESCRIPTION,
property_array_index: None,
value: desc_buf.to_vec(),
priority: None,
}],
};
let mut buf = BytesMut::new();
req.encode(&mut buf);
let mut ack_buf = BytesMut::new();
handle_create_object(&mut db, &buf, &mut ack_buf).unwrap();
let (pv, _) = bacnet_encoding::primitives::decode_application_value(&ack_buf, 0).unwrap();
let created_oid = match pv {
PropertyValue::ObjectIdentifier(oid) => oid,
other => panic!("expected ObjectIdentifier, got {other:?}"),
};
let obj = db.get(&created_oid).unwrap();
let desc = obj
.read_property(PropertyIdentifier::DESCRIPTION, None)
.unwrap();
match desc {
PropertyValue::CharacterString(s) => assert_eq!(s, "Test AI"),
other => panic!("expected CharacterString, got {other:?}"),
}
}
#[test]
fn create_object_bad_initial_value_rolls_back() {
let mut db = make_db_with_device_and_ai();
let before_count = db.len();
let mut ot_buf = BytesMut::new();
bacnet_encoding::primitives::encode_app_enumerated(&mut ot_buf, 99);
let req = CreateObjectRequest {
object_specifier: ObjectSpecifier::Type(ObjectType::BINARY_INPUT),
list_of_initial_values: vec![bacnet_services::common::BACnetPropertyValue {
property_identifier: PropertyIdentifier::OBJECT_TYPE,
property_array_index: None,
value: ot_buf.to_vec(),
priority: None,
}],
};
let mut buf = BytesMut::new();
req.encode(&mut buf);
assert!(handle_create_object(&mut db, &buf, &mut BytesMut::new()).is_err());
assert_eq!(
db.len(),
before_count,
"object should be removed on failure"
);
}
#[test]
fn acknowledge_alarm_success() {
let mut db = make_db_with_ai();
let oid = ObjectIdentifier::new(ObjectType::ANALOG_INPUT, 1).unwrap();
let request = AcknowledgeAlarmRequest {
acknowledging_process_identifier: 1,
event_object_identifier: oid,
event_state_acknowledged: 3,
timestamp: BACnetTimeStamp::SequenceNumber(42),
acknowledgment_source: "operator".into(),
time_of_acknowledgment: BACnetTimeStamp::SequenceNumber(0),
};
let mut buf = BytesMut::new();
request.encode(&mut buf).unwrap();
handle_acknowledge_alarm(&mut db, &buf).unwrap();
}
#[test]
fn acknowledge_alarm_unknown_object_fails() {
let mut db = make_db_with_ai();
let oid = ObjectIdentifier::new(ObjectType::ANALOG_INPUT, 99).unwrap();
let request = AcknowledgeAlarmRequest {
acknowledging_process_identifier: 1,
event_object_identifier: oid,
event_state_acknowledged: 3,
timestamp: BACnetTimeStamp::SequenceNumber(42),
acknowledgment_source: "operator".into(),
time_of_acknowledgment: BACnetTimeStamp::SequenceNumber(0),
};
let mut buf = BytesMut::new();
request.encode(&mut buf).unwrap();
let err = handle_acknowledge_alarm(&mut db, &buf).unwrap_err();
match err {
Error::Protocol { class, code } => {
assert_eq!(class, ErrorClass::OBJECT.to_raw() as u32);
assert_eq!(code, ErrorCode::UNKNOWN_OBJECT.to_raw() as u32);
}
other => panic!("expected Protocol error, got: {other:?}"),
}
}
#[tokio::test(start_paused = true)]
async fn dcc_timer_auto_re_enables() {
use std::sync::Arc;
let comm_state = Arc::new(AtomicU8::new(0));
let request = bacnet_services::device_mgmt::DeviceCommunicationControlRequest {
time_duration: Some(1), enable_disable: EnableDisable::DISABLE_INITIATION,
password: None,
};
let mut buf = BytesMut::new();
request.encode(&mut buf).unwrap();
let (state, duration) =
handle_device_communication_control(&buf, &comm_state, &None).unwrap();
assert_eq!(state, EnableDisable::DISABLE_INITIATION);
assert_eq!(duration, Some(1));
assert_eq!(comm_state.load(Ordering::Acquire), 2);
let comm_clone = Arc::clone(&comm_state);
let minutes = duration.unwrap();
let handle = tokio::spawn(async move {
tokio::time::sleep(std::time::Duration::from_secs(minutes as u64 * 60)).await;
comm_clone.store(0, Ordering::Release);
});
assert_eq!(comm_state.load(Ordering::Acquire), 2);
tokio::time::advance(std::time::Duration::from_secs(61)).await;
handle.await.unwrap();
assert_eq!(comm_state.load(Ordering::Acquire), 0);
}
#[tokio::test(start_paused = true)]
async fn dcc_timer_cancelled_by_new_dcc() {
use std::sync::Arc;
use tokio::task::JoinHandle;
let comm_state = Arc::new(AtomicU8::new(0));
let request = bacnet_services::device_mgmt::DeviceCommunicationControlRequest {
time_duration: Some(2),
enable_disable: EnableDisable::DISABLE_INITIATION,
password: None,
};
let mut buf = BytesMut::new();
request.encode(&mut buf).unwrap();
let (_, duration) = handle_device_communication_control(&buf, &comm_state, &None).unwrap();
assert_eq!(comm_state.load(Ordering::Acquire), 2);
let comm_clone = Arc::clone(&comm_state);
let minutes = duration.unwrap();
let handle1: JoinHandle<()> = tokio::spawn(async move {
tokio::time::sleep(std::time::Duration::from_secs(minutes as u64 * 60)).await;
comm_clone.store(0, Ordering::Release);
});
let request2 = bacnet_services::device_mgmt::DeviceCommunicationControlRequest {
time_duration: None,
enable_disable: EnableDisable::ENABLE,
password: None,
};
let mut buf2 = BytesMut::new();
request2.encode(&mut buf2).unwrap();
let (state2, duration2) =
handle_device_communication_control(&buf2, &comm_state, &None).unwrap();
assert_eq!(state2, EnableDisable::ENABLE);
assert_eq!(duration2, None);
assert_eq!(comm_state.load(Ordering::Acquire), 0);
handle1.abort();
tokio::time::advance(std::time::Duration::from_secs(121)).await;
tokio::task::yield_now().await;
assert_eq!(comm_state.load(Ordering::Acquire), 0);
}
#[test]
fn dcc_correct_password_accepted() {
let comm_state = AtomicU8::new(0);
let pw = Some("secret".to_string());
let request = bacnet_services::device_mgmt::DeviceCommunicationControlRequest {
time_duration: None,
enable_disable: EnableDisable::DISABLE_INITIATION,
password: Some("secret".to_string()),
};
let mut buf = BytesMut::new();
request.encode(&mut buf).unwrap();
let (state, _) = handle_device_communication_control(&buf, &comm_state, &pw).unwrap();
assert_eq!(state, EnableDisable::DISABLE_INITIATION);
assert_eq!(comm_state.load(Ordering::Acquire), 2);
}
#[test]
fn dcc_wrong_password_rejected() {
let comm_state = AtomicU8::new(0);
let pw = Some("secret".to_string());
let request = bacnet_services::device_mgmt::DeviceCommunicationControlRequest {
time_duration: None,
enable_disable: EnableDisable::DISABLE_INITIATION,
password: Some("wrong".to_string()),
};
let mut buf = BytesMut::new();
request.encode(&mut buf).unwrap();
let err = handle_device_communication_control(&buf, &comm_state, &pw).unwrap_err();
match err {
Error::Protocol { class, code } => {
assert_eq!(class, ErrorClass::SECURITY.to_raw() as u32);
assert_eq!(code, ErrorCode::PASSWORD_FAILURE.to_raw() as u32);
}
other => panic!("expected Protocol error, got: {other:?}"),
}
assert_eq!(comm_state.load(Ordering::Acquire), 0);
}
#[test]
fn dcc_missing_password_when_required() {
let comm_state = AtomicU8::new(0);
let pw = Some("secret".to_string());
let request = bacnet_services::device_mgmt::DeviceCommunicationControlRequest {
time_duration: None,
enable_disable: EnableDisable::DISABLE_INITIATION,
password: None,
};
let mut buf = BytesMut::new();
request.encode(&mut buf).unwrap();
let err = handle_device_communication_control(&buf, &comm_state, &pw).unwrap_err();
match err {
Error::Protocol { class, code } => {
assert_eq!(class, ErrorClass::SECURITY.to_raw() as u32);
assert_eq!(code, ErrorCode::PASSWORD_FAILURE.to_raw() as u32);
}
other => panic!("expected Protocol error, got: {other:?}"),
}
assert_eq!(comm_state.load(Ordering::Acquire), 0);
}
#[test]
fn dcc_no_password_configured_accepts_any() {
let comm_state = AtomicU8::new(0);
let request = bacnet_services::device_mgmt::DeviceCommunicationControlRequest {
time_duration: None,
enable_disable: EnableDisable::DISABLE_INITIATION,
password: Some("anything".to_string()),
};
let mut buf = BytesMut::new();
request.encode(&mut buf).unwrap();
let (state, _) = handle_device_communication_control(&buf, &comm_state, &None).unwrap();
assert_eq!(state, EnableDisable::DISABLE_INITIATION);
assert_eq!(comm_state.load(Ordering::Acquire), 2);
}
#[test]
fn reinit_correct_password_accepted() {
let pw = Some("reinit-pw".to_string());
let request = bacnet_services::device_mgmt::ReinitializeDeviceRequest {
reinitialized_state: bacnet_types::enums::ReinitializedState::WARMSTART,
password: Some("reinit-pw".to_string()),
};
let mut buf = BytesMut::new();
request.encode(&mut buf).unwrap();
handle_reinitialize_device(&buf, &pw).unwrap();
}
#[test]
fn reinit_wrong_password_rejected() {
let pw = Some("reinit-pw".to_string());
let request = bacnet_services::device_mgmt::ReinitializeDeviceRequest {
reinitialized_state: bacnet_types::enums::ReinitializedState::WARMSTART,
password: Some("wrong".to_string()),
};
let mut buf = BytesMut::new();
request.encode(&mut buf).unwrap();
let err = handle_reinitialize_device(&buf, &pw).unwrap_err();
match err {
Error::Protocol { class, code } => {
assert_eq!(class, ErrorClass::SECURITY.to_raw() as u32);
assert_eq!(code, ErrorCode::PASSWORD_FAILURE.to_raw() as u32);
}
other => panic!("expected Protocol error, got: {other:?}"),
}
}
#[test]
fn reinit_missing_password_when_required() {
let pw = Some("reinit-pw".to_string());
let request = bacnet_services::device_mgmt::ReinitializeDeviceRequest {
reinitialized_state: bacnet_types::enums::ReinitializedState::WARMSTART,
password: None,
};
let mut buf = BytesMut::new();
request.encode(&mut buf).unwrap();
let err = handle_reinitialize_device(&buf, &pw).unwrap_err();
match err {
Error::Protocol { class, code } => {
assert_eq!(class, ErrorClass::SECURITY.to_raw() as u32);
assert_eq!(code, ErrorCode::PASSWORD_FAILURE.to_raw() as u32);
}
other => panic!("expected Protocol error, got: {other:?}"),
}
}
#[test]
fn reinit_no_password_configured_accepts_any() {
let request = bacnet_services::device_mgmt::ReinitializeDeviceRequest {
reinitialized_state: bacnet_types::enums::ReinitializedState::WARMSTART,
password: Some("anything".to_string()),
};
let mut buf = BytesMut::new();
request.encode(&mut buf).unwrap();
handle_reinitialize_device(&buf, &None).unwrap();
}
}