#[cfg(not(feature = "std"))]
use alloc::vec::Vec;
use bacnet_encoding::constructed::tagged::{
decode_ctx_boolean, decode_ctx_object_id, decode_ctx_real, decode_ctx_unsigned, expect_end,
misplaced_tag, next_is_context, unclosed_kind,
};
use bacnet_encoding::constructed::{decode_property_reference, encode_bacnet_property_value};
use bacnet_encoding::primitives;
use bacnet_encoding::tags;
use bacnet_types::enums::PropertyIdentifier;
use bacnet_types::error::Error;
use bacnet_types::primitives::ObjectIdentifier;
use bytes::BytesMut;
use crate::common::BACnetPropertyValue;
pub use crate::cov_decode::COVNotificationDecodeError;
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct SubscribeCOVRequest {
pub subscriber_process_identifier: u32,
pub monitored_object_identifier: ObjectIdentifier,
pub issue_confirmed_notifications: Option<bool>,
pub lifetime: Option<u32>,
}
impl SubscribeCOVRequest {
pub fn is_cancellation(&self) -> bool {
self.issue_confirmed_notifications.is_none() && self.lifetime.is_none()
}
pub fn validate(&self) -> Result<(), Error> {
if self.lifetime.is_some() && self.issue_confirmed_notifications.is_none() {
return Err(Error::encode("SubscribeCOV lifetime requires confirmed-notification mode"));
}
Ok(())
}
pub fn encode(&self, buf: &mut BytesMut) -> Result<(), Error> {
self.validate()?;
primitives::encode_ctx_unsigned(buf, 0, self.subscriber_process_identifier as u64);
primitives::encode_ctx_object_id(buf, 1, &self.monitored_object_identifier);
if let Some(confirmed) = self.issue_confirmed_notifications {
primitives::encode_ctx_boolean(buf, 2, confirmed);
}
if let Some(lifetime) = self.lifetime {
primitives::encode_ctx_unsigned(buf, 3, lifetime as u64);
}
Ok(())
}
pub fn decode(data: &[u8]) -> Result<Self, Error> {
let mut offset = 0;
let (subscriber_process_identifier, end) =
decode_ctx_unsigned::<u32>(data, offset, 0, "SubscribeCOV process-id")?;
offset = end;
let (monitored_object_identifier, end) =
decode_ctx_object_id(data, offset, 1, "SubscribeCOV object-id")?;
offset = end;
let mut issue_confirmed_notifications = None;
if next_is_context(data, offset, 2)? {
let (value, end) =
decode_ctx_boolean(data, offset, 2, "SubscribeCOV confirmed-notifications")?;
issue_confirmed_notifications = Some(value);
offset = end;
}
let mut lifetime = None;
if next_is_context(data, offset, 3)? {
let (value, end) =
decode_ctx_unsigned::<u32>(data, offset, 3, "SubscribeCOV lifetime")?;
lifetime = Some(value);
offset = end;
}
expect_end(data, offset, offset, "SubscribeCOV")?;
Ok(Self {
subscriber_process_identifier,
monitored_object_identifier,
issue_confirmed_notifications,
lifetime,
})
}
}
#[derive(Debug, Clone, PartialEq)]
pub struct SubscribeCOVPropertyRequest {
pub subscriber_process_identifier: u32,
pub monitored_object_identifier: ObjectIdentifier,
pub issue_confirmed_notifications: Option<bool>,
pub lifetime: Option<u32>,
pub monitored_property_identifier: PropertyIdentifier,
pub monitored_property_array_index: Option<u32>,
pub cov_increment: Option<f32>,
}
impl SubscribeCOVPropertyRequest {
pub fn is_cancellation(&self) -> bool {
self.issue_confirmed_notifications.is_none() && self.lifetime.is_none()
}
pub fn validate(&self) -> Result<(), Error> {
match (self.issue_confirmed_notifications, self.lifetime) {
(None, None) => Ok(()),
(Some(_), Some(lifetime)) if lifetime != 0 => Ok(()),
(Some(_), Some(0)) => Err(Error::encode("SubscribeCOVProperty requires a positive lifetime")),
_ => Err(Error::encode("SubscribeCOVProperty confirmed mode and lifetime must appear together")),
}
}
pub fn encode(&self, buf: &mut BytesMut) -> Result<(), Error> {
self.validate()?;
primitives::encode_ctx_unsigned(buf, 0, self.subscriber_process_identifier as u64);
primitives::encode_ctx_object_id(buf, 1, &self.monitored_object_identifier);
if let Some(v) = self.issue_confirmed_notifications {
primitives::encode_ctx_boolean(buf, 2, v);
}
if let Some(v) = self.lifetime {
primitives::encode_ctx_unsigned(buf, 3, v as u64);
}
tags::encode_opening_tag(buf, 4);
primitives::encode_ctx_unsigned(buf, 0, self.monitored_property_identifier.to_raw() as u64);
if let Some(idx) = self.monitored_property_array_index {
primitives::encode_ctx_unsigned(buf, 1, idx as u64);
}
tags::encode_closing_tag(buf, 4);
if let Some(v) = self.cov_increment {
primitives::encode_ctx_real(buf, 5, v);
}
Ok(())
}
pub fn decode(data: &[u8]) -> Result<Self, Error> {
let mut offset = 0;
let (subscriber_process_identifier, end) =
decode_ctx_unsigned::<u32>(data, offset, 0, "SubscribeCOVProperty process-id")?;
offset = end;
let (monitored_object_identifier, end) =
decode_ctx_object_id(data, offset, 1, "SubscribeCOVProperty object-id")?;
offset = end;
let mut issue_confirmed_notifications = None;
if next_is_context(data, offset, 2)? {
let (value, end) = decode_ctx_boolean(
data,
offset,
2,
"SubscribeCOVProperty confirmed-notifications",
)?;
issue_confirmed_notifications = Some(value);
offset = end;
}
let mut lifetime = None;
if next_is_context(data, offset, 3)? {
let (value, end) =
decode_ctx_unsigned::<u32>(data, offset, 3, "SubscribeCOVProperty lifetime")?;
lifetime = Some(value);
offset = end;
}
let (tag, pos) = tags::decode_tag(data, offset)?;
if !tag.is_opening_tag(4) {
return Err(misplaced_tag(
data,
&tag,
Some(4),
offset,
"SubscribeCOVProperty expected opening tag 4",
));
}
let (monitored_property, end) = decode_property_reference(data, pos)?;
offset = end;
let (tag, end) = tags::decode_tag(data, offset)?;
if !tag.is_closing_tag(4) {
return Err(Error::decoding_kind(
unclosed_kind(&tag),
offset,
"SubscribeCOVProperty expected closing tag 4",
));
}
offset = end;
let mut cov_increment = None;
if next_is_context(data, offset, 5)? {
let (increment, end) =
decode_ctx_real(data, offset, 5, "SubscribeCOVProperty COV increment")?;
cov_increment = Some(increment);
offset = end;
}
expect_end(data, offset, offset, "SubscribeCOVProperty")?;
Ok(Self {
subscriber_process_identifier,
monitored_object_identifier,
issue_confirmed_notifications,
lifetime,
monitored_property_identifier: monitored_property.property_identifier,
monitored_property_array_index: monitored_property.property_array_index,
cov_increment,
})
}
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct COVNotificationRequest {
pub subscriber_process_identifier: u32,
pub initiating_device_identifier: ObjectIdentifier,
pub monitored_object_identifier: ObjectIdentifier,
pub time_remaining: u32,
pub list_of_values: Vec<BACnetPropertyValue>,
}
impl COVNotificationRequest {
pub fn encode(&self, buf: &mut BytesMut) {
primitives::encode_ctx_unsigned(buf, 0, self.subscriber_process_identifier as u64);
primitives::encode_ctx_object_id(buf, 1, &self.initiating_device_identifier);
primitives::encode_ctx_object_id(buf, 2, &self.monitored_object_identifier);
primitives::encode_ctx_unsigned(buf, 3, self.time_remaining as u64);
tags::encode_opening_tag(buf, 4);
for pv in &self.list_of_values {
encode_bacnet_property_value(pv, buf);
}
tags::encode_closing_tag(buf, 4);
}
pub fn decode(data: &[u8]) -> Result<Self, Error> {
Self::decode_detailed(data).map_err(COVNotificationDecodeError::into_error)
}
}
#[cfg(test)]
#[path = "cov_width_tests.rs"]
mod width_tests;
#[cfg(test)]
mod tests {
use super::*;
use crate::common::MAX_DECODED_ITEMS;
use bacnet_types::enums::{ObjectType, PropertyIdentifier, RejectReason};
#[test]
fn subscribe_cov_round_trip() {
let req = SubscribeCOVRequest {
subscriber_process_identifier: 1,
monitored_object_identifier: ObjectIdentifier::new(ObjectType::ANALOG_INPUT, 1)
.unwrap(),
issue_confirmed_notifications: Some(true),
lifetime: Some(300),
};
let mut buf = BytesMut::new();
req.encode(&mut buf).unwrap();
let decoded = SubscribeCOVRequest::decode(&buf).unwrap();
assert_eq!(req, decoded);
assert!(!decoded.is_cancellation());
}
#[test]
fn subscribe_cov_cancellation() {
let req = SubscribeCOVRequest {
subscriber_process_identifier: 1,
monitored_object_identifier: ObjectIdentifier::new(ObjectType::ANALOG_INPUT, 1)
.unwrap(),
issue_confirmed_notifications: None,
lifetime: None,
};
let mut buf = BytesMut::new();
req.encode(&mut buf).unwrap();
let decoded = SubscribeCOVRequest::decode(&buf).unwrap();
assert_eq!(req, decoded);
assert!(decoded.is_cancellation());
}
#[test]
fn cov_notification_round_trip() {
let req = COVNotificationRequest {
subscriber_process_identifier: 1,
initiating_device_identifier: ObjectIdentifier::new(ObjectType::DEVICE, 1234).unwrap(),
monitored_object_identifier: ObjectIdentifier::new(ObjectType::ANALOG_INPUT, 1)
.unwrap(),
time_remaining: 60,
list_of_values: vec![
BACnetPropertyValue {
property_identifier: PropertyIdentifier::PRESENT_VALUE,
property_array_index: None,
value: vec![0x44, 0x42, 0x90, 0x00, 0x00],
priority: None,
},
BACnetPropertyValue {
property_identifier: PropertyIdentifier::STATUS_FLAGS,
property_array_index: None,
value: vec![0x82, 0x04, 0x00], priority: None,
},
],
};
let mut buf = BytesMut::new();
req.encode(&mut buf);
let decoded = COVNotificationRequest::decode(&buf).unwrap();
assert_eq!(req, decoded);
}
#[test]
fn test_decode_subscribe_cov_empty_input() {
assert!(SubscribeCOVRequest::decode(&[]).is_err());
}
#[test]
fn test_decode_subscribe_cov_truncated_1_byte() {
let req = SubscribeCOVRequest {
subscriber_process_identifier: 1,
monitored_object_identifier: ObjectIdentifier::new(ObjectType::ANALOG_INPUT, 1)
.unwrap(),
issue_confirmed_notifications: Some(true),
lifetime: Some(300),
};
let mut buf = BytesMut::new();
req.encode(&mut buf).unwrap();
assert!(SubscribeCOVRequest::decode(&buf[..1]).is_err());
}
#[test]
fn test_decode_subscribe_cov_truncated_2_bytes() {
let req = SubscribeCOVRequest {
subscriber_process_identifier: 1,
monitored_object_identifier: ObjectIdentifier::new(ObjectType::ANALOG_INPUT, 1)
.unwrap(),
issue_confirmed_notifications: Some(true),
lifetime: Some(300),
};
let mut buf = BytesMut::new();
req.encode(&mut buf).unwrap();
assert!(SubscribeCOVRequest::decode(&buf[..2]).is_err());
}
#[test]
fn test_decode_subscribe_cov_truncated_3_bytes() {
let req = SubscribeCOVRequest {
subscriber_process_identifier: 1,
monitored_object_identifier: ObjectIdentifier::new(ObjectType::ANALOG_INPUT, 1)
.unwrap(),
issue_confirmed_notifications: Some(true),
lifetime: Some(300),
};
let mut buf = BytesMut::new();
req.encode(&mut buf).unwrap();
assert!(SubscribeCOVRequest::decode(&buf[..3]).is_err());
}
#[test]
fn test_decode_subscribe_cov_invalid_tag() {
assert!(SubscribeCOVRequest::decode(&[0xFF, 0xFF, 0xFF]).is_err());
}
#[test]
fn test_decode_cov_notification_empty_input() {
assert!(COVNotificationRequest::decode(&[]).is_err());
assert_eq!(
COVNotificationRequest::decode_detailed(&[])
.unwrap_err()
.reject_reason(),
RejectReason::MISSING_REQUIRED_PARAMETER
);
}
#[test]
fn test_decode_cov_notification_classifies_missing_and_invalid_tags() {
assert_eq!(
COVNotificationRequest::decode_detailed(&[0x09, 0x01])
.unwrap_err()
.reject_reason(),
RejectReason::MISSING_REQUIRED_PARAMETER
);
assert_eq!(
COVNotificationRequest::decode_detailed(&[0x79, 0x01])
.unwrap_err()
.reject_reason(),
RejectReason::MISSING_REQUIRED_PARAMETER
);
assert_eq!(
COVNotificationRequest::decode_detailed(&[0x21, 0x01])
.unwrap_err()
.reject_reason(),
RejectReason::INVALID_TAG
);
assert_eq!(
COVNotificationRequest::decode_detailed(&[0x09, 0x01, 0x1B, 0x01, 0x02, 0x03])
.unwrap_err()
.reject_reason(),
RejectReason::INVALID_DATA_ENCODING
);
assert_eq!(
COVNotificationRequest::decode_detailed(&[0x0A, 0x00, 0x01])
.unwrap_err()
.reject_reason(),
RejectReason::INVALID_DATA_ENCODING
);
}
#[test]
fn test_decode_cov_notification_rejects_empty_list_of_values() {
let req = COVNotificationRequest {
subscriber_process_identifier: 1,
initiating_device_identifier: ObjectIdentifier::new(ObjectType::DEVICE, 1234).unwrap(),
monitored_object_identifier: ObjectIdentifier::new(ObjectType::ANALOG_INPUT, 1)
.unwrap(),
time_remaining: 60,
list_of_values: Vec::new(),
};
let mut buf = BytesMut::new();
req.encode(&mut buf);
assert_eq!(
COVNotificationRequest::decode_detailed(&buf)
.unwrap_err()
.reject_reason(),
RejectReason::PARAMETER_OUT_OF_RANGE
);
}
#[test]
fn test_decode_cov_notification_classifies_nested_invalid_tag() {
let req = COVNotificationRequest {
subscriber_process_identifier: 1,
initiating_device_identifier: ObjectIdentifier::new(ObjectType::DEVICE, 1234).unwrap(),
monitored_object_identifier: ObjectIdentifier::new(ObjectType::ANALOG_INPUT, 1)
.unwrap(),
time_remaining: 60,
list_of_values: vec![BACnetPropertyValue {
property_identifier: PropertyIdentifier::PRESENT_VALUE,
property_array_index: None,
value: vec![0x21, 0x01],
priority: None,
}],
};
let mut buf = BytesMut::new();
req.encode(&mut buf);
let list_start = buf.iter().position(|byte| *byte == 0x4E).unwrap();
buf[list_start + 1] = 0x21;
assert_eq!(
COVNotificationRequest::decode_detailed(&buf)
.unwrap_err()
.reject_reason(),
RejectReason::INVALID_TAG
);
}
#[test]
fn test_decode_cov_notification_enforces_value_cap() {
let value = BACnetPropertyValue {
property_identifier: PropertyIdentifier::PRESENT_VALUE,
property_array_index: None,
value: vec![0x21, 0x01],
priority: None,
};
let mut req = COVNotificationRequest {
subscriber_process_identifier: 1,
initiating_device_identifier: ObjectIdentifier::new(ObjectType::DEVICE, 1234).unwrap(),
monitored_object_identifier: ObjectIdentifier::new(ObjectType::ANALOG_INPUT, 1)
.unwrap(),
time_remaining: 60,
list_of_values: vec![value.clone(); MAX_DECODED_ITEMS],
};
let mut buf = BytesMut::new();
req.encode(&mut buf);
assert_eq!(
COVNotificationRequest::decode_detailed(&buf)
.unwrap()
.list_of_values
.len(),
MAX_DECODED_ITEMS
);
req.list_of_values.push(value);
buf.clear();
req.encode(&mut buf);
assert_eq!(
COVNotificationRequest::decode_detailed(&buf)
.unwrap_err()
.reject_reason(),
RejectReason::BUFFER_OVERFLOW
);
}
#[test]
fn test_decode_cov_notification_classifies_trailing_arguments() {
let req = COVNotificationRequest {
subscriber_process_identifier: 1,
initiating_device_identifier: ObjectIdentifier::new(ObjectType::DEVICE, 1234).unwrap(),
monitored_object_identifier: ObjectIdentifier::new(ObjectType::ANALOG_INPUT, 1)
.unwrap(),
time_remaining: 60,
list_of_values: vec![BACnetPropertyValue {
property_identifier: PropertyIdentifier::PRESENT_VALUE,
property_array_index: None,
value: vec![0x44, 0x42, 0x90, 0x00, 0x00],
priority: None,
}],
};
let mut buf = BytesMut::new();
req.encode(&mut buf);
buf.extend_from_slice(&[0x59, 0x01]);
assert_eq!(
COVNotificationRequest::decode_detailed(&buf)
.unwrap_err()
.reject_reason(),
RejectReason::TOO_MANY_ARGUMENTS
);
buf.truncate(buf.len() - 2);
buf.extend_from_slice(&[0xFF]);
assert_eq!(
COVNotificationRequest::decode_detailed(&buf)
.unwrap_err()
.reject_reason(),
RejectReason::INVALID_TAG
);
}
#[test]
fn test_decode_cov_notification_truncated_1_byte() {
let req = COVNotificationRequest {
subscriber_process_identifier: 1,
initiating_device_identifier: ObjectIdentifier::new(ObjectType::DEVICE, 1234).unwrap(),
monitored_object_identifier: ObjectIdentifier::new(ObjectType::ANALOG_INPUT, 1)
.unwrap(),
time_remaining: 60,
list_of_values: vec![BACnetPropertyValue {
property_identifier: PropertyIdentifier::PRESENT_VALUE,
property_array_index: None,
value: vec![0x44, 0x42, 0x90, 0x00, 0x00],
priority: None,
}],
};
let mut buf = BytesMut::new();
req.encode(&mut buf);
assert!(COVNotificationRequest::decode(&buf[..1]).is_err());
}
#[test]
fn test_decode_cov_notification_truncated_3_bytes() {
let req = COVNotificationRequest {
subscriber_process_identifier: 1,
initiating_device_identifier: ObjectIdentifier::new(ObjectType::DEVICE, 1234).unwrap(),
monitored_object_identifier: ObjectIdentifier::new(ObjectType::ANALOG_INPUT, 1)
.unwrap(),
time_remaining: 60,
list_of_values: vec![BACnetPropertyValue {
property_identifier: PropertyIdentifier::PRESENT_VALUE,
property_array_index: None,
value: vec![0x44, 0x42, 0x90, 0x00, 0x00],
priority: None,
}],
};
let mut buf = BytesMut::new();
req.encode(&mut buf);
assert!(COVNotificationRequest::decode(&buf[..3]).is_err());
}
#[test]
fn test_decode_cov_notification_truncated_half() {
let req = COVNotificationRequest {
subscriber_process_identifier: 1,
initiating_device_identifier: ObjectIdentifier::new(ObjectType::DEVICE, 1234).unwrap(),
monitored_object_identifier: ObjectIdentifier::new(ObjectType::ANALOG_INPUT, 1)
.unwrap(),
time_remaining: 60,
list_of_values: vec![BACnetPropertyValue {
property_identifier: PropertyIdentifier::PRESENT_VALUE,
property_array_index: None,
value: vec![0x44, 0x42, 0x90, 0x00, 0x00],
priority: None,
}],
};
let mut buf = BytesMut::new();
req.encode(&mut buf);
let half = buf.len() / 2;
assert!(COVNotificationRequest::decode(&buf[..half]).is_err());
}
#[test]
fn test_decode_cov_notification_invalid_tag() {
assert!(COVNotificationRequest::decode(&[0xFF, 0xFF, 0xFF]).is_err());
}
}
#[cfg(test)]
#[path = "cov_property_validation_tests.rs"]
mod property_validation_tests;
#[cfg(test)]
#[path = "cov_request_validation_tests.rs"]
mod request_validation_tests;