use super::*;
use bacnet_objects::analog::AnalogInputObject;
use bacnet_objects::device::{DeviceConfig, DeviceObject};
use bacnet_objects::event_enrollment::EventEnrollmentObject;
use bacnet_objects::traits::BACnetObject;
use bacnet_transport::port::TransportPort;
use bacnet_types::constructed::{BACnetDeviceObjectPropertyReference, BACnetEventParameter};
use bacnet_types::enums::{EventState, EventType};
use std::sync::{Arc as StdArc, Mutex as StdMutex};
use tokio::sync::mpsc;
#[derive(Clone, Default)]
struct RecordingTransport {
sent_broadcast: StdArc<StdMutex<Vec<bytes::Bytes>>>,
}
impl TransportPort for RecordingTransport {
async fn start(
&mut self,
) -> Result<mpsc::Receiver<bacnet_transport::port::ReceivedNpdu>, Error> {
let (_tx, rx) = mpsc::channel(1);
Ok(rx)
}
async fn stop(&mut self) -> Result<(), Error> {
Ok(())
}
async fn send_unicast(&self, _npdu: &[u8], _mac: &[u8]) -> Result<(), Error> {
Ok(())
}
async fn send_broadcast(&self, npdu: &[u8]) -> Result<(), Error> {
self.sent_broadcast
.lock()
.unwrap()
.push(bytes::Bytes::copy_from_slice(npdu));
Ok(())
}
fn local_mac(&self) -> &[u8] {
&[127, 0, 0, 1, 0xBA, 0xC0]
}
}
#[tokio::test(start_paused = true)]
async fn spawned_task_advances_and_fires_the_time_delay_countdown() {
let transport = RecordingTransport::default();
let sent = StdArc::clone(&transport.sent_broadcast);
let mut ai = AnalogInputObject::new(1, "AI-1", 62).unwrap();
ai.set_present_value(85.0); let ai_oid = ai.object_identifier();
let mut ee = EventEnrollmentObject::new(1, "EE-OOR", EventType::OUT_OF_RANGE.to_raw()).unwrap();
ee.set_object_property_reference(Some(BACnetDeviceObjectPropertyReference::new_local(
ai_oid,
PropertyIdentifier::PRESENT_VALUE.to_raw(),
)));
ee.set_event_parameters(BACnetEventParameter::OutOfRange {
time_delay: 2,
low_limit: 20.0,
high_limit: 80.0,
deadband: 2.0,
});
ee.set_event_enable(0x07);
let ee_oid = ee.object_identifier();
let mut db = ObjectDatabase::new();
db.add(Box::new(ai)).unwrap();
db.add(Box::new(ee)).unwrap();
db.add(Box::new(
DeviceObject::new(DeviceConfig {
instance: 1,
name: "Dev".into(),
..DeviceConfig::default()
})
.unwrap(),
))
.unwrap();
db.add(Box::new(
super::event_notifications_tests::notification_class_0_broadcasting(),
))
.unwrap();
let config = ServerConfig {
event_enrollment_interval_secs: 1,
..ServerConfig::default()
};
let server = BACnetServer::start(config, db, transport)
.await
.expect("server should start");
let read_state = |db: &ObjectDatabase| match db
.get(&ee_oid)
.unwrap()
.read_property(PropertyIdentifier::EVENT_STATE, None)
.unwrap()
{
PropertyValue::Enumerated(v) => EventState::from_raw(v),
other => panic!("EVENT_STATE must read Enumerated, got {other:?}"),
};
tokio::time::sleep(Duration::from_millis(100)).await;
assert_eq!(
read_state(&*server.database().read().await),
EventState::NORMAL,
"first pass seeds the countdown; the confirmed state must not move"
);
tokio::time::sleep(Duration::from_millis(1300)).await;
assert_eq!(
read_state(&*server.database().read().await),
EventState::NORMAL,
"delay not yet elapsed: NORMAL holds (13.2.4)"
);
tokio::time::sleep(Duration::from_millis(1200)).await;
assert_eq!(
read_state(&*server.database().read().await),
EventState::HIGH_LIMIT,
"Time_Delay elapsed under the real spawned task: the transition fired"
);
assert_eq!(
sent.lock().unwrap().len(),
1,
"the committed delayed Event Enrollment transition must distribute once"
);
}
async fn default_interval_server(
time_delay: u32,
) -> (
BACnetServer<RecordingTransport>,
ObjectIdentifier,
StdArc<StdMutex<Vec<bytes::Bytes>>>,
) {
let transport = RecordingTransport::default();
let sent = StdArc::clone(&transport.sent_broadcast);
let mut ai = AnalogInputObject::new(1, "AI-1", 62).unwrap();
ai.set_present_value(85.0);
let ai_oid = ai.object_identifier();
let mut ee = EventEnrollmentObject::new(1, "EE-OOR", EventType::OUT_OF_RANGE.to_raw()).unwrap();
ee.set_object_property_reference(Some(BACnetDeviceObjectPropertyReference::new_local(
ai_oid,
PropertyIdentifier::PRESENT_VALUE.to_raw(),
)));
ee.set_event_parameters(BACnetEventParameter::OutOfRange {
time_delay,
low_limit: 20.0,
high_limit: 80.0,
deadband: 2.0,
});
ee.set_event_enable(0x07);
let ee_oid = ee.object_identifier();
let mut db = ObjectDatabase::new();
db.add(Box::new(ai)).unwrap();
db.add(Box::new(ee)).unwrap();
db.add(Box::new(
DeviceObject::new(DeviceConfig {
instance: 1,
name: "Dev".into(),
..DeviceConfig::default()
})
.unwrap(),
))
.unwrap();
let server = BACnetServer::start(ServerConfig::default(), db, transport)
.await
.expect("server should start");
(server, ee_oid, sent)
}
fn ee_event_state(db: &ObjectDatabase, ee_oid: &ObjectIdentifier) -> EventState {
match db
.get(ee_oid)
.unwrap()
.read_property(PropertyIdentifier::EVENT_STATE, None)
.unwrap()
{
PropertyValue::Enumerated(v) => EventState::from_raw(v),
other => panic!("EVENT_STATE must read Enumerated, got {other:?}"),
}
}
#[tokio::test(start_paused = true)]
async fn delay_5s_fires_on_second_default_interval_tick() {
let (server, ee_oid, _sent) = default_interval_server(5).await;
tokio::time::sleep(Duration::from_millis(100)).await; tokio::time::sleep(Duration::from_millis(8900)).await; assert_eq!(
ee_event_state(&*server.database().read().await, &ee_oid),
EventState::NORMAL,
"one seeded pass still counting at t≈9s"
);
tokio::time::sleep(Duration::from_millis(2000)).await; assert_eq!(
ee_event_state(&*server.database().read().await, &ee_oid),
EventState::HIGH_LIMIT,
"ceil(5s/10s)=1 pass: fired by the t=10s tick — under the old \
per-pass misreading this would have taken ~50s"
);
}
#[tokio::test(start_paused = true)]
async fn delay_15s_fires_on_third_default_interval_tick() {
let (server, ee_oid, _sent) = default_interval_server(15).await;
tokio::time::sleep(Duration::from_secs(19)).await; assert_eq!(
ee_event_state(&*server.database().read().await, &ee_oid),
EventState::NORMAL,
"t≈19s: still counting (second seeded pass)"
);
tokio::time::sleep(Duration::from_secs(2)).await; assert_eq!(
ee_event_state(&*server.database().read().await, &ee_oid),
EventState::HIGH_LIMIT,
"ceil(15/10)=2 passes: fired by the t=20s tick"
);
}
#[tokio::test(start_paused = true)]
async fn delay_25s_fires_on_fourth_default_interval_tick() {
let (server, ee_oid, _sent) = default_interval_server(25).await;
tokio::time::sleep(Duration::from_secs(29)).await;
assert_eq!(
ee_event_state(&*server.database().read().await, &ee_oid),
EventState::NORMAL,
"t≈29s: three seeded passes not yet elapsed"
);
tokio::time::sleep(Duration::from_secs(2)).await; assert_eq!(
ee_event_state(&*server.database().read().await, &ee_oid),
EventState::HIGH_LIMIT,
"ceil(25/10)=3 passes: fired by the t=30s tick"
);
}
#[tokio::test(start_paused = true)]
async fn zero_delay_fires_on_the_first_tick() {
let (server, ee_oid, _sent) = default_interval_server(0).await;
tokio::time::sleep(Duration::from_millis(500)).await;
assert_eq!(
ee_event_state(&*server.database().read().await, &ee_oid),
EventState::HIGH_LIMIT,
"TD=0: the first (immediate) tick transitions without any countdown"
);
}