use super::cov_notifications_tests::recording_transport;
use super::cov_wire_test_support::real;
use super::*;
use crate::server::test_transport::{SendLog, TestTransport};
use bacnet_objects::device::{DeviceConfig, DeviceObject};
use bacnet_objects::lighting::LightingOutputObject;
use bacnet_objects::traits::BACnetObject;
use bacnet_services::cov::COVNotificationRequest;
const PV: PropertyIdentifier = PropertyIdentifier::PRESENT_VALUE;
const TV: PropertyIdentifier = PropertyIdentifier::TRACKING_VALUE;
const LC: PropertyIdentifier = PropertyIdentifier::LIGHTING_COMMAND;
const OBJECT: u32 = 1;
const TRACKING: u32 = 2;
async fn start(
configure: impl FnOnce(&mut LightingOutputObject),
) -> (BACnetServer<TestTransport>, ObjectIdentifier, SendLog) {
let (transport, sent) = recording_transport();
let mut object = LightingOutputObject::new(1, "LO-1").unwrap();
configure(&mut object);
let oid = object.object_identifier();
let mut db = ObjectDatabase::new();
db.add(Box::new(
DeviceObject::new(DeviceConfig {
identity: bacnet_objects::DeviceIdentity::new(1384, 0)
.expect("valid Device identity")
.with_name("Lighting-engine-device"),
..DeviceConfig::default()
})
.unwrap(),
))
.unwrap();
db.add(Box::new(object)).unwrap();
let config = ServerConfig {
device: bacnet_objects::DeviceIdentity::new(1384, 0).unwrap(),
enable_event_enrollment: false,
..ServerConfig::default()
};
let server = BACnetServer::start(config, db, transport).await.unwrap();
settle().await;
(server, oid, sent)
}
async fn settle() {
for _ in 0..16 {
tokio::task::yield_now().await;
}
}
async fn advance(milliseconds: u64) {
tokio::time::advance(Duration::from_millis(milliseconds)).await;
settle().await;
}
async fn subscribe(
server: &BACnetServer<TestTransport>,
oid: ObjectIdentifier,
increment: Option<f32>,
) {
let mut table = server.cov_table.write().await;
for (process, property, cov_increment) in
[(OBJECT, None, None), (TRACKING, Some(TV), increment)]
{
table
.subscribe(CovSubscription {
subscriber_mac: MacAddr::from_slice(&[127, 0, 0, 1, 0xBA, process as u8]),
subscriber_network: None,
subscriber_process_identifier: process,
monitored_object_identifier: oid,
issue_confirmed_notifications: false,
expires_at: None,
last_notified_observation: None,
monitored_property: property,
monitored_property_array_index: None,
cov_increment,
notification_kind: CovNotificationKind::Single,
timestamped: false,
})
.unwrap();
}
}
async fn write(
server: &BACnetServer<TestTransport>,
oid: ObjectIdentifier,
property: PropertyIdentifier,
value: PropertyValue,
priority: Option<u8>,
) {
server
.write_local(
&oid,
property,
None,
value,
priority,
crate::LocalCommandSource::ServerDevice,
)
.await
.unwrap();
}
fn command(octets: &[u8]) -> PropertyValue {
PropertyValue::ApplicationData(octets.to_vec())
}
fn reports(sent: &SendLog) -> Vec<(u32, PropertyIdentifier, Vec<u8>)> {
let mut reports: Vec<_> = sent
.take()
.into_iter()
.filter_map(|frame| match frame.apdu() {
Apdu::UnconfirmedRequest(request)
if request.service_choice
== UnconfirmedServiceChoice::UNCONFIRMED_COV_NOTIFICATION =>
{
let report = COVNotificationRequest::decode(&request.service_request).unwrap();
let first = &report.list_of_values[0];
Some((
report.subscriber_process_identifier,
first.property_identifier,
first.value.clone(),
))
}
_ => None,
})
.collect();
reports.sort_by_key(|report| report.0);
reports
}
async fn read(
server: &BACnetServer<TestTransport>,
oid: ObjectIdentifier,
property: PropertyIdentifier,
) -> PropertyValue {
server
.database()
.read()
.await
.get(&oid)
.unwrap()
.read_property(property, None)
.unwrap()
}
#[tokio::test(start_paused = true)]
async fn a_fade_shorter_than_the_routine_pass_is_sampled_and_ended_on_time() {
let (mut server, oid, sent) = start(|_| {}).await;
advance(1_200).await;
subscribe(&server, oid, None).await;
let fade = [
0x09, 0x01, 0x1C, 0x42, 0xB4, 0x00, 0x00, 0x4A, 0x01, 0x2C, 0x59, 0x08,
];
write(&server, oid, LC, command(&fade), None).await;
assert_eq!(
reports(&sent),
[(OBJECT, PV, real(90.0)), (TRACKING, TV, real(0.0))]
);
for expected in [30.0, 60.0, 90.0] {
advance(100).await;
assert_eq!(reports(&sent), [(TRACKING, TV, real(expected))]);
}
assert_eq!(
read(&server, oid, PropertyIdentifier::IN_PROGRESS).await,
PropertyValue::Enumerated(0)
);
let deadline = server
.database()
.read()
.await
.get(&oid)
.unwrap()
.next_monotonic_deadline_internal();
assert_eq!(deadline, None);
advance(5_000).await;
assert_eq!(reports(&sent), []);
server.stop().await.unwrap();
}
#[tokio::test(start_paused = true)]
async fn a_ramp_reports_tracking_value_by_increment_and_present_value_once() {
let (mut server, oid, sent) = start(|object| {
object
.write_property(
PropertyIdentifier::COV_INCREMENT,
None,
PropertyValue::Real(25.0),
None,
)
.unwrap();
})
.await;
subscribe(&server, oid, Some(50.0)).await;
let ramp = [
0x09, 0x02, 0x1C, 0x42, 0xC8, 0x00, 0x00, 0x2C, 0x42, 0x48, 0x00, 0x00,
];
write(&server, oid, LC, command(&ramp), None).await;
assert_eq!(
reports(&sent),
[(OBJECT, PV, real(100.0)), (TRACKING, TV, real(0.0))]
);
let mut heard = Vec::new();
for _ in 0..4 {
advance(500).await;
heard.push(reports(&sent));
}
assert_eq!(
heard,
[
vec![],
vec![(TRACKING, TV, real(50.0))],
vec![],
vec![(TRACKING, TV, real(100.0))],
]
);
assert_eq!(read(&server, oid, TV).await, PropertyValue::Real(100.0));
server.stop().await.unwrap();
}
#[tokio::test(start_paused = true)]
async fn an_egress_reports_present_value_when_it_runs_out() {
let (mut server, oid, sent) = start(|object| {
for (property, value) in [
(
PropertyIdentifier::BLINK_WARN_ENABLE,
PropertyValue::Boolean(true),
),
(PropertyIdentifier::EGRESS_TIME, PropertyValue::Unsigned(2)),
] {
object.write_property(property, None, value, None).unwrap();
}
})
.await;
write(&server, oid, PV, PropertyValue::Real(80.0), Some(8)).await;
subscribe(&server, oid, None).await;
write(&server, oid, PV, PropertyValue::Real(-3.0), Some(8)).await;
assert_eq!(
reports(&sent),
[(OBJECT, PV, real(80.0)), (TRACKING, TV, real(80.0))]
);
assert_eq!(
read(&server, oid, PropertyIdentifier::EGRESS_ACTIVE).await,
PropertyValue::Boolean(true)
);
advance(1_999).await;
assert_eq!(reports(&sent), []);
advance(1).await;
assert_eq!(
reports(&sent),
[(OBJECT, PV, real(0.0)), (TRACKING, TV, real(0.0))]
);
assert_eq!(
read(&server, oid, PropertyIdentifier::EGRESS_ACTIVE).await,
PropertyValue::Boolean(false)
);
server.stop().await.unwrap();
}
#[tokio::test(start_paused = true)]
async fn a_trim_change_fades_in_on_the_task_and_reports_trim_active() {
let (mut server, oid, sent) = start(|object| {
object.set_high_end_trim(Some(100.0)).unwrap();
object.set_trim_fade_time(300).unwrap();
})
.await;
subscribe(&server, oid, None).await;
write(&server, oid, PV, PropertyValue::Real(90.0), Some(8)).await;
assert_eq!(
reports(&sent),
[(OBJECT, PV, real(90.0)), (TRACKING, TV, real(90.0))]
);
write(
&server,
oid,
PropertyIdentifier::HIGH_END_TRIM,
PropertyValue::Real(60.0),
None,
)
.await;
assert_eq!(reports(&sent), []);
assert_eq!(
read(&server, oid, PropertyIdentifier::IN_PROGRESS).await,
PropertyValue::Enumerated(5)
);
let mut heard = Vec::new();
for _ in 0..3 {
advance(100).await;
heard.push(reports(&sent));
}
assert_eq!(
heard,
[
vec![(TRACKING, TV, real(100.0 - 40.0 / 3.0))],
vec![(TRACKING, TV, real(100.0 - 80.0 / 3.0))],
vec![(TRACKING, TV, real(60.0))],
]
);
assert_eq!(
read(&server, oid, PropertyIdentifier::IN_PROGRESS).await,
PropertyValue::Enumerated(5)
);
let deadline = server
.database()
.read()
.await
.get(&oid)
.unwrap()
.next_monotonic_deadline_internal();
assert_eq!(deadline, None);
server.stop().await.unwrap();
}
async fn subscribe_tracking(
server: &BACnetServer<TestTransport>,
oid: ObjectIdentifier,
process: u32,
increment: Option<f32>,
) -> crate::cov::CovSubscriptionKey {
server
.cov_table
.write()
.await
.subscribe(CovSubscription {
subscriber_mac: MacAddr::from_slice(&[127, 0, 0, 1, 0xBA, process as u8]),
subscriber_network: None,
subscriber_process_identifier: process,
monitored_object_identifier: oid,
issue_confirmed_notifications: false,
expires_at: None,
last_notified_observation: None,
monitored_property: Some(TV),
monitored_property_array_index: None,
cov_increment: increment,
notification_kind: CovNotificationKind::Single,
timestamped: false,
})
.unwrap()
.key()
.clone()
}
#[tokio::test(start_paused = true)]
async fn the_finest_tracking_value_subscriber_sets_the_sample_step() {
let (mut server, oid, sent) = start(|object| {
object
.write_property(
PropertyIdentifier::COV_INCREMENT,
None,
PropertyValue::Real(25.0),
None,
)
.unwrap();
})
.await;
for (process, increment) in [(3, 1.0), (4, 10.0), (5, 50.0)] {
subscribe_tracking(&server, oid, process, Some(increment)).await;
}
let ramp = [
0x09, 0x02, 0x1C, 0x42, 0xC8, 0x00, 0x00, 0x2C, 0x42, 0x48, 0x00, 0x00,
];
write(&server, oid, LC, command(&ramp), None).await;
let first: Vec<_> = [3, 4, 5].map(|p| (p, TV, real(0.0))).into();
assert_eq!(reports(&sent), first);
for tenth in 1..=20u8 {
advance(100).await;
let level = f32::from(tenth) * 5.0;
let mut expected = vec![(3, TV, real(level))];
if tenth % 2 == 0 {
expected.push((4, TV, real(level)));
}
if tenth % 10 == 0 {
expected.push((5, TV, real(level)));
}
assert_eq!(reports(&sent), expected, "at {} ms", u32::from(tenth) * 100);
}
server.stop().await.unwrap();
}
#[tokio::test(start_paused = true)]
async fn an_unsubscribe_gives_back_the_objects_own_sample_step() {
let (mut server, oid, sent) = start(|object| {
object
.write_property(
PropertyIdentifier::COV_INCREMENT,
None,
PropertyValue::Real(25.0),
None,
)
.unwrap();
})
.await;
subscribe_tracking(&server, oid, 2, None).await;
let fine = subscribe_tracking(&server, oid, 3, Some(10.0)).await;
let ramp = [
0x09, 0x02, 0x1C, 0x42, 0xC8, 0x00, 0x00, 0x2C, 0x42, 0x48, 0x00, 0x00,
];
write(&server, oid, LC, command(&ramp), None).await;
assert_eq!(reports(&sent), [(2, TV, real(0.0)), (3, TV, real(0.0))]);
let mut heard = Vec::new();
for _ in 0..4 {
advance(100).await;
heard.push(reports(&sent));
}
let both = |level| vec![(2, TV, real(level)), (3, TV, real(level))];
assert_eq!(heard, [vec![], both(10.0), vec![], both(20.0)]);
server.cov_table.write().await.unsubscribe(&fine);
for at in (500..=2_000).step_by(100) {
advance(100).await;
let expected = match at {
600 => vec![(2, TV, real(30.0))],
1_100 => vec![(2, TV, real(55.0))],
1_600 => vec![(2, TV, real(80.0))],
2_000 => vec![(2, TV, real(100.0))],
_ => vec![],
};
assert_eq!(reports(&sent), expected, "at {at} ms");
}
server.stop().await.unwrap();
}