use super::channel_wire_tests::{channel, member, settled, write_channel};
use super::command_action_wire_tests::{ao, idle, outputs, write_pv};
use super::command_remote_write_tests::{ack, deliver, device, remote_write, start_unbound};
use super::command_run_stop_tests::db_flags;
use super::cov_wire_test_support::*;
use super::*;
use bacnet_objects::channel::MemberDatatype;
use bacnet_objects::traits::BACnetObject;
use bacnet_types::constructed::BACnetDeviceObjectPropertyReference;
use bacnet_types::enums::WriteStatus;
use bacnet_types::network_number::NetworkNumber;
const THIS_NETWORK: u16 = 7;
const REMOTE_NETWORK: u16 = 5;
const ROUTER: [u8; 6] = [10, 0, 0, 9, 0xBA, 0xC0];
#[derive(Clone, Copy, Debug)]
enum Writer {
Command,
Channel,
}
#[derive(Debug, PartialEq, Eq)]
struct Sent {
link: MacAddr,
dnet: Option<u16>,
}
fn local() -> Sent {
Sent {
link: MacAddr::from_slice(&PEER),
dnet: None,
}
}
fn routed(network: u16) -> Sent {
Sent {
link: MacAddr::from_slice(&ROUTER),
dnet: Some(network),
}
}
async fn start(writer: Writer) -> Harness {
match writer {
Writer::Command => start_unbound().await,
Writer::Channel => {
Harness::start_with(ServerConfig::default(), |db| {
outputs(db);
let remote = BACnetDeviceObjectPropertyReference {
device_identifier: Some(device(9)),
..member(ao(1), PropertyIdentifier::PRESENT_VALUE)
};
let mut channel = channel(5, 21, vec![(remote.clone(), 0)]);
channel.remember_member_datatype_internal(0, &remote, Some(MemberDatatype::Real));
db.add(Box::new(channel)).unwrap();
})
.await
}
}
}
#[derive(Clone, Copy, Debug)]
enum Answer {
AsSent,
Relayed,
}
#[derive(Clone, Copy, Debug)]
enum Bound {
Routed(u16),
Local,
}
struct Write {
h: Harness,
writer: Writer,
invoke_id: u8,
sent: Sent,
}
fn publish(h: &Harness, number: u16) {
h.server
.test_network()
.local_network_number()
.publish(NetworkNumber::configured(number).unwrap());
}
async fn send_write(writer: Writer, this_network: Option<u16>, bound: Bound) -> Write {
let mut h = start(writer).await;
let binding = match bound {
Bound::Routed(network) => DeviceBinding::routed(device(9), network, PEER, ROUTER),
Bound::Local => DeviceBinding::local(device(9), PEER),
};
h.server
.device_bindings
.write()
.await
.insert_configured(binding.unwrap(), |_| false)
.unwrap();
if let Some(number) = this_network {
publish(&h, number);
}
match writer {
Writer::Command => write_pv(&mut h, 1, 1).await.unwrap(),
Writer::Channel => write_channel(&mut h, 5, &PropertyValue::Real(80.0), Some(8))
.await
.unwrap(),
}
let invoke_id = remote_write(&h).await;
let frame = h
.server
.test_network()
.transport()
.sent()
.unicasts()
.into_iter()
.find(|frame| matches!(frame.apdu(), Apdu::ConfirmedRequest(_)))
.expect("the WriteProperty frame");
let destination = frame.decode_npdu().destination;
if let Some(to) = &destination {
assert_eq!(to.mac_address.as_slice(), PEER, "{writer:?}: the DADR");
}
let sent = Sent {
link: frame.mac.clone(),
dnet: destination.map(|to| to.network),
};
Write {
h,
writer,
invoke_id,
sent,
}
}
impl Write {
async fn deliver(&self, answer: &Apdu, link: &[u8], snet: Option<u16>) {
let source = snet.map(|network| NpduAddress {
network,
mac_address: MacAddr::from_slice(&PEER),
});
deliver(&self.h, answer, link, source).await;
}
fn reject(&self) -> Apdu {
Apdu::Reject(bacnet_encoding::apdu::RejectPdu {
invoke_id: self.invoke_id,
reject_reason: bacnet_types::enums::RejectReason::OTHER,
})
}
async fn succeeded(mut self, network: u16) -> Option<MacAddr> {
match self.writer {
Writer::Command => {
idle(&self.h, 1).await;
assert_eq!(
db_flags(&self.h, 1).await,
[true, true],
"{:?}",
self.writer
);
}
Writer::Channel => {
assert_eq!(settled(&mut self.h, 5).await, WriteStatus::SUCCESSFUL);
}
}
assert_eq!(self.h.server.notification_transactions.active_count(), 0);
let learned = self.h.server.learned_routers.lock().await;
learned.cached_router(network)
}
}
async fn write_through(
writer: Writer,
this_network: Option<u16>,
bound_on: u16,
answer: Answer,
) -> Sent {
let write = send_write(writer, this_network, Bound::Routed(bound_on)).await;
let (link, snet) = match answer {
Answer::AsSent => (write.sent.link.clone(), write.sent.dnet),
Answer::Relayed => (MacAddr::from_slice(&ROUTER), this_network),
};
write.deliver(&ack(write.invoke_id), &link, snet).await;
let sent = Sent {
link: write.sent.link.clone(),
dnet: write.sent.dnet,
};
let learned = write.succeeded(this_network.unwrap_or(bound_on)).await;
if this_network.is_some() {
assert_eq!(learned, None, "{writer:?}");
}
sent
}
#[tokio::test(start_paused = true)]
async fn command_write_bound_through_this_network_goes_without_a_dnet() {
let writer = Writer::Command;
assert_eq!(
write_through(writer, Some(THIS_NETWORK), THIS_NETWORK, Answer::AsSent).await,
local()
);
assert_eq!(
write_through(writer, Some(THIS_NETWORK), REMOTE_NETWORK, Answer::AsSent).await,
routed(REMOTE_NETWORK)
);
assert_eq!(
write_through(writer, None, THIS_NETWORK, Answer::AsSent).await,
routed(THIS_NETWORK)
);
}
#[tokio::test(start_paused = true)]
async fn channel_member_bound_through_this_network_is_written_without_a_dnet() {
let writer = Writer::Channel;
assert_eq!(
write_through(writer, Some(THIS_NETWORK), THIS_NETWORK, Answer::AsSent).await,
local()
);
assert_eq!(
write_through(writer, Some(THIS_NETWORK), REMOTE_NETWORK, Answer::AsSent).await,
routed(REMOTE_NETWORK)
);
assert_eq!(
write_through(writer, None, THIS_NETWORK, Answer::AsSent).await,
routed(THIS_NETWORK)
);
}
#[tokio::test(start_paused = true)]
async fn a_local_write_is_completed_by_its_answer_relayed_with_this_networks_snet() {
for writer in [Writer::Command, Writer::Channel] {
assert_eq!(
write_through(writer, Some(THIS_NETWORK), THIS_NETWORK, Answer::Relayed).await,
local()
);
}
}
#[tokio::test(start_paused = true)]
async fn a_relayed_answer_from_another_network_or_an_unknown_number_leaves_a_local_write_open() {
for writer in [Writer::Command, Writer::Channel] {
for (this_network, bound, snet) in [
(
Some(THIS_NETWORK),
Bound::Routed(THIS_NETWORK),
REMOTE_NETWORK,
),
(None, Bound::Local, THIS_NETWORK),
] {
let write = send_write(writer, this_network, bound).await;
assert_eq!(write.sent, local(), "{writer:?}, {bound:?}");
write.deliver(&write.reject(), &ROUTER, Some(snet)).await;
write.deliver(&ack(write.invoke_id), &PEER, None).await;
assert_eq!(write.succeeded(snet).await, None, "{writer:?}, {bound:?}");
}
}
}
#[tokio::test(start_paused = true)]
async fn a_write_routed_before_the_number_was_learned_completes_on_its_relayed_answer() {
for writer in [Writer::Command, Writer::Channel] {
let write = send_write(writer, None, Bound::Routed(THIS_NETWORK)).await;
assert_eq!(write.sent, routed(THIS_NETWORK), "{writer:?}");
publish(&write.h, THIS_NETWORK);
write
.deliver(&ack(write.invoke_id), &ROUTER, Some(THIS_NETWORK))
.await;
assert_eq!(write.succeeded(THIS_NETWORK).await, None, "{writer:?}");
}
}
#[tokio::test(start_paused = true)]
async fn an_answer_to_a_request_routed_from_this_network_goes_back_by_its_route() {
let h = start_unbound().await;
h.server
.test_network()
.local_network_number()
.publish(NetworkNumber::configured(THIS_NETWORK).unwrap());
let mut service = BytesMut::new();
bacnet_services::read_property::ReadPropertyRequest {
object_identifier: device(856),
property_identifier: PropertyIdentifier::OBJECT_NAME,
property_array_index: None,
}
.encode(&mut service);
let request = Apdu::ConfirmedRequest(ConfirmedRequestPdu {
segmented: false,
more_follows: false,
segmented_response_accepted: false,
max_segments: None,
max_apdu_length: 1476,
invoke_id: 42,
sequence_number: None,
proposed_window_size: None,
service_choice: ConfirmedServiceChoice::READ_PROPERTY,
service_request: service.freeze(),
});
let requester = NpduAddress {
network: THIS_NETWORK,
mac_address: MacAddr::from_slice(&PEER),
};
deliver(&h, &request, &ROUTER, Some(requester.clone())).await;
let log = h.server.test_network().transport().sent();
let answer = tokio::time::timeout(Duration::from_secs(1), async {
loop {
let unicasts = log.unicasts();
if let Some(frame) = unicasts
.into_iter()
.find(|frame| matches!(frame.apdu(), Apdu::ComplexAck(_)))
{
return frame;
}
tokio::time::sleep(Duration::from_millis(1)).await;
}
})
.await
.expect("the ComplexACK");
assert_eq!(answer.mac.as_slice(), ROUTER);
assert_eq!(answer.decode_npdu().destination, Some(requester));
}