use std::borrow::Cow;
use bacnet_encoding::primitives::encode_timestamp_choice;
use bacnet_objects::notification_class::NotificationClass;
use bacnet_objects::traits::BACnetObject;
use bacnet_services::alarm_event::GetEventInformationRequest;
use bacnet_types::enums::EventState;
use bacnet_types::primitives::{BACnetTimeStamp, Date, Time};
use super::*;
const INVOKE_ID: u8 = 77;
struct LargeSummaryObject {
oid: ObjectIdentifier,
timestamps: Vec<PropertyValue>,
}
impl LargeSummaryObject {
fn new() -> Self {
let timestamps = (1..=3)
.map(|hour| {
let timestamp = BACnetTimeStamp::DateTime {
date: Date {
year: 126,
month: 9,
day: hour,
day_of_week: hour,
},
time: Time {
hour,
minute: 2,
second: 3,
hundredths: 4,
},
};
let mut encoded = BytesMut::new();
encode_timestamp_choice(&mut encoded, ×tamp).unwrap();
PropertyValue::ApplicationData(encoded.to_vec())
})
.collect();
Self {
oid: ObjectIdentifier::new(ObjectType::ANALOG_INPUT, 1).unwrap(),
timestamps,
}
}
}
impl BACnetObject for LargeSummaryObject {
fn object_identifier(&self) -> ObjectIdentifier {
self.oid
}
fn object_name(&self) -> &str {
"LARGE-SUMMARY"
}
fn read_property(
&self,
property: PropertyIdentifier,
_array_index: Option<u32>,
) -> Result<PropertyValue, Error> {
match property {
p if p == PropertyIdentifier::EVENT_STATE => {
Ok(PropertyValue::Enumerated(EventState::HIGH_LIMIT.to_raw()))
}
p if p == PropertyIdentifier::ACKED_TRANSITIONS
|| p == PropertyIdentifier::EVENT_ENABLE =>
{
Ok(PropertyValue::BitString {
unused_bits: 5,
data: vec![0xe0],
})
}
p if p == PropertyIdentifier::EVENT_TIME_STAMPS => {
Ok(PropertyValue::List(self.timestamps.clone()))
}
p if p == PropertyIdentifier::NOTIFY_TYPE => Ok(PropertyValue::Enumerated(0)),
p if p == PropertyIdentifier::NOTIFICATION_CLASS => Ok(PropertyValue::Unsigned(42)),
_ => Err(Error::protocol(
ErrorClass::PROPERTY,
ErrorCode::UNKNOWN_PROPERTY,
)),
}
}
fn write_property(
&mut self,
_property: PropertyIdentifier,
_array_index: Option<u32>,
_value: PropertyValue,
_priority: Option<u8>,
) -> Result<(), Error> {
Err(Error::protocol(
ErrorClass::PROPERTY,
ErrorCode::WRITE_ACCESS_DENIED,
))
}
fn property_list(&self) -> Cow<'static, [PropertyIdentifier]> {
Cow::Borrowed(&[
PropertyIdentifier::EVENT_STATE,
PropertyIdentifier::ACKED_TRANSITIONS,
PropertyIdentifier::EVENT_TIME_STAMPS,
PropertyIdentifier::NOTIFY_TYPE,
PropertyIdentifier::EVENT_ENABLE,
PropertyIdentifier::NOTIFICATION_CLASS,
])
}
}
fn database() -> ObjectDatabase {
let mut db = ObjectDatabase::new();
db.add(Box::new(LargeSummaryObject::new())).unwrap();
let mut class = NotificationClass::new(99, "NC-99").unwrap();
class.notification_class = 42;
class.priority = [1, 100, 255];
db.add(Box::new(class)).unwrap();
db
}
fn service_request() -> Bytes {
let mut encoded = BytesMut::new();
GetEventInformationRequest {
last_received_object_identifier: None,
}
.encode(&mut encoded);
encoded.freeze()
}
fn full_service_ack() -> Bytes {
let mut encoded = BytesMut::new();
handlers::handle_get_event_information(&database(), &service_request(), &mut encoded).unwrap();
encoded.freeze()
}
fn unsegmented_apdu_len(service_ack: Bytes) -> usize {
let mut encoded = BytesMut::new();
encode_apdu(
&mut encoded,
&Apdu::ComplexAck(ComplexAck {
segmented: false,
more_follows: false,
invoke_id: INVOKE_ID,
sequence_number: None,
proposed_window_size: None,
service_choice: ConfirmedServiceChoice::GET_EVENT_INFORMATION,
service_ack,
}),
)
.unwrap();
encoded.len()
}
async fn dispatch(
segmentation: Segmentation,
client_accepts_segmented: bool,
client_max_apdu: u16,
local_max_apdu: u32,
) -> (
SendLog,
Arc<segmented_send::SegmentedSendRegistry>,
MacAddr,
Arc<crate::server::request_tasks::RequestTasks>,
) {
dispatch_with_budget(
segmentation,
client_accepts_segmented,
client_max_apdu,
local_max_apdu,
GetEventInformationBudget::default(),
)
.await
}
async fn dispatch_with_budget(
segmentation: Segmentation,
client_accepts_segmented: bool,
client_max_apdu: u16,
local_max_apdu: u32,
budget: GetEventInformationBudget,
) -> (
SendLog,
Arc<segmented_send::SegmentedSendRegistry>,
MacAddr,
Arc<crate::server::request_tasks::RequestTasks>,
) {
let (network, sent) = recording_network();
let db = Arc::new(RwLock::new(database()));
let seg_ack_senders = Arc::new(segmented_send::SegmentedSendRegistry::default());
let confirmed_request_tracker = Arc::new(ConfirmedRequestTracker::default());
let config = ServerConfig {
device: bacnet_objects::DeviceIdentity::default()
.with_max_apdu(u16::try_from(local_max_apdu).expect("Device APDU fits u16"))
.expect("valid Device APDU")
.with_segmentation(segmentation),
get_event_information_budget: budget,
..ServerConfig::default()
};
let source_mac = test_mac(41);
let request_tasks = Arc::new(crate::server::request_tasks::RequestTasks::default());
BACnetServer::<TestTransport>::handle_confirmed_request(
&RequestServices {
db: Arc::clone(&db),
seg_ack_senders: Arc::clone(&seg_ack_senders),
..RequestServices::for_test(Arc::clone(&network), config.clone())
},
&confirmed_request_tracker,
&request_tasks.spawner(),
source_mac.as_slice(),
None,
ConfirmedRequestPdu {
segmented: false,
more_follows: false,
segmented_response_accepted: client_accepts_segmented,
max_segments: None,
max_apdu_length: client_max_apdu,
invoke_id: INVOKE_ID,
sequence_number: None,
proposed_window_size: None,
service_choice: ConfirmedServiceChoice::GET_EVENT_INFORMATION,
service_request: service_request(),
},
None,
)
.await;
(sent, seg_ack_senders, source_mac, request_tasks)
}
#[tokio::test]
async fn first_summary_over_unsegmented_budget_uses_buffer_overflow_abort() {
assert!(unsegmented_apdu_len(full_service_ack()) > 50);
for (client_max_apdu, local_max_apdu) in [(50, 1476), (480, 50)] {
let (sent, _, _, _owner) =
dispatch(Segmentation::NONE, false, client_max_apdu, local_max_apdu).await;
wait_for_sent_len(&sent, 1).await;
assert_eq!(sent_count(&sent), 1);
assert_eq!(abort_reason(&sent, 0), AbortReason::BUFFER_OVERFLOW);
}
}
#[tokio::test]
async fn get_event_information_configured_abort_independent_of_segmentation() {
for segmentation in [Segmentation::NONE, Segmentation::BOTH] {
for (budget, expected) in [
(
GetEventInformationBudget {
max_objects: 1,
..Default::default()
},
AbortReason::OUT_OF_RESOURCES,
),
(
GetEventInformationBudget {
max_service_ack_bytes: 4,
..Default::default()
},
AbortReason::BUFFER_OVERFLOW,
),
] {
let (sent, _, _, owner) =
dispatch_with_budget(segmentation, true, 50, 1476, budget).await;
wait_for_sent_len(&sent, 1).await;
assert_eq!(abort_reason(&sent, 0), expected);
owner.close();
while owner.join_next().await.is_some() {}
}
}
}
#[tokio::test]
async fn segmentation_capable_dispatch_retains_the_complete_service_ack() {
let expected = full_service_ack();
assert!(unsegmented_apdu_len(expected.clone()) > 50);
let (sent, seg_ack_senders, source_mac, owner) = tokio::time::timeout(
Duration::from_secs(1),
dispatch(Segmentation::BOTH, true, 50, 1476),
)
.await
.expect("parent must return without awaiting segmented response ACKs");
let key = segmented_transaction_key(
&source_mac,
None,
INVOKE_ID,
bacnet_transport::port::TransportProvenance::unverified(),
);
let mut reconstructed = BytesMut::new();
let mut index = 0usize;
loop {
wait_for_sent_len(&sent, index + 1).await;
let Apdu::ComplexAck(ack) = decoded_sent_apdu(&sent, index) else {
panic!("expected segmented ComplexAck");
};
assert!(ack.segmented);
assert_eq!(
ack.service_choice,
ConfirmedServiceChoice::GET_EVENT_INFORMATION
);
assert_eq!(ack.sequence_number, Some(index as u8));
reconstructed.extend_from_slice(&ack.service_ack);
if !ack.more_follows {
break;
}
send_segment_ack(
&seg_ack_senders,
&key,
segment_ack(INVOKE_ID, false, index as u8),
)
.await;
index += 1;
}
assert_eq!(reconstructed.freeze(), expected);
owner.close();
while owner.join_next().await.is_some() {}
}