use super::*;
use bacnet_encoding::apdu::{ComplexAck, decode_apdu, encode_apdu};
use bacnet_encoding::npdu::{Npdu, decode_npdu, encode_npdu};
use bacnet_services::read_property::{ReadPropertyACK, ReadPropertyRequest};
use bacnet_transport::port::{ReceivedNpdu, TransportProvenance};
use bacnet_types::MacAddr;
use bytes::{Bytes, BytesMut};
use std::sync::Mutex as StdMutex;
use tokio::time::{Duration, Instant};
type Sends = Arc<StdMutex<Vec<(Instant, MacAddr)>>>;
const A: &[u8] = &[0x0A];
const B: &[u8] = &[0x0B];
struct Devices {
inbound_tx: mpsc::Sender<ReceivedNpdu>,
inbound_rx: Option<mpsc::Receiver<ReceivedNpdu>>,
sends: Sends,
unanswered: StdMutex<usize>,
reply_after: Duration,
}
impl Devices {
fn answer(&self, request: &[u8], mac: &[u8]) -> Result<Option<ReceivedNpdu>, Error> {
let npdu = decode_npdu(Bytes::copy_from_slice(request))?;
let Apdu::ConfirmedRequest(request) = decode_apdu(npdu.payload)? else {
return Ok(None);
};
{
let mut unanswered = self.unanswered.lock().unwrap();
if *unanswered > 0 {
*unanswered -= 1;
return Ok(None);
}
}
let read = ReadPropertyRequest::decode(&request.service_request)?;
let mut service_ack = BytesMut::new();
ReadPropertyACK {
object_identifier: read.object_identifier,
property_identifier: read.property_identifier,
property_array_index: read.property_array_index,
property_value: vec![0x21, 42],
}
.encode(&mut service_ack);
let mut apdu = BytesMut::new();
encode_apdu(
&mut apdu,
&Apdu::ComplexAck(ComplexAck {
segmented: false,
more_follows: false,
invoke_id: request.invoke_id,
sequence_number: None,
proposed_window_size: None,
service_choice: request.service_choice,
service_ack: service_ack.freeze(),
}),
)?;
let mut reply = BytesMut::new();
encode_npdu(
&mut reply,
&Npdu {
payload: apdu.freeze(),
..Npdu::default()
},
)?;
Ok(Some(ReceivedNpdu {
direct_response: None,
npdu: reply.freeze(),
source_mac: MacAddr::from_slice(mac),
link_layer_group: false,
data_attributes: Vec::new(),
provenance: TransportProvenance::unverified(),
reply_tx: None,
}))
}
}
impl TransportPort for Devices {
async fn start(&mut self) -> Result<mpsc::Receiver<ReceivedNpdu>, Error> {
Ok(self.inbound_rx.take().expect("started once"))
}
async fn stop(&mut self) -> Result<(), Error> {
Ok(())
}
async fn send_unicast(&self, npdu: &[u8], mac: &[u8]) -> Result<(), Error> {
self.sends
.lock()
.unwrap()
.push((Instant::now(), MacAddr::from_slice(mac)));
if let Some(reply) = self.answer(npdu, mac)? {
let inbound = self.inbound_tx.clone();
let delay = self.reply_after;
tokio::spawn(async move {
tokio::time::sleep(delay).await;
let _ = inbound.send(reply).await;
});
}
Ok(())
}
async fn send_broadcast(&self, _npdu: &[u8]) -> Result<(), Error> {
Ok(())
}
fn local_receive_apdu_capacity(&self) -> u16 {
1476
}
fn local_mac(&self) -> &[u8] {
&[0x01]
}
}
fn devices(sends: &Sends, unanswered: usize, reply_after: Duration) -> Devices {
let (inbound_tx, inbound_rx) = mpsc::channel(16);
Devices {
inbound_tx,
inbound_rx: Some(inbound_rx),
sends: Arc::clone(sends),
unanswered: StdMutex::new(unanswered),
reply_after,
}
}
async fn paced(
interval_ms: u64,
retries: u8,
unanswered: usize,
reply_after: Duration,
) -> (EndpointSession<Devices>, Sends) {
let sends = Sends::default();
let devices = devices(&sends, unanswered, reply_after);
let config = SessionConfig {
apdu_timeout_ms: 100,
apdu_retries: retries,
min_request_interval_ms: interval_ms,
..SessionConfig::default()
};
let mut session = EndpointSession::new(devices, SessionRole::ClientOnly, config).unwrap();
session.start().await.unwrap();
(session, sends)
}
async fn try_read(client: &ClientRoleHandle, mac: &[u8]) -> Result<(), Error> {
let analog = ObjectIdentifier::new(ObjectType::ANALOG_INPUT, 1).unwrap();
client
.read_property(mac, analog, PropertyIdentifier::PRESENT_VALUE, None)
.await
.map(drop)
}
async fn read(session: &EndpointSession<Devices>, mac: &[u8]) {
try_read(session.client().unwrap(), mac).await.unwrap();
}
fn sent_to(sends: &Sends, mac: &[u8], start: Instant) -> Vec<Duration> {
sends
.lock()
.unwrap()
.iter()
.filter(|(_, to)| to.as_slice() == mac)
.map(|(at, _)| *at - start)
.collect()
}
fn ms(values: &[u64]) -> Vec<Duration> {
values.iter().copied().map(Duration::from_millis).collect()
}
#[tokio::test(start_paused = true)]
async fn endpoint_requests_to_one_destination_are_spaced_by_the_interval() {
let (mut session, sends) = paced(50, 0, 0, Duration::ZERO).await;
let start = Instant::now();
for _ in 0..3 {
read(&session, A).await;
}
assert_eq!(sent_to(&sends, A, start), ms(&[0, 50, 100]));
session.stop().await.unwrap();
}
#[tokio::test(start_paused = true)]
async fn an_endpoint_request_waits_the_interval_from_a_slow_answer() {
let (mut session, sends) = paced(50, 0, 0, Duration::from_millis(30)).await;
let start = Instant::now();
for _ in 0..3 {
read(&session, A).await;
}
assert_eq!(sent_to(&sends, A, start), ms(&[0, 80, 160]));
session.stop().await.unwrap();
}
#[tokio::test(start_paused = true)]
async fn endpoint_requests_to_two_destinations_do_not_wait_on_each_other() {
let (mut session, sends) = paced(50, 0, 0, Duration::ZERO).await;
let start = Instant::now();
tokio::join!(read(&session, A), read(&session, B));
tokio::join!(read(&session, A), read(&session, B));
assert_eq!(sent_to(&sends, A, start), ms(&[0, 50]));
assert_eq!(sent_to(&sends, B, start), ms(&[0, 50]));
session.stop().await.unwrap();
}
#[tokio::test(start_paused = true)]
async fn an_endpoint_interval_of_zero_sends_at_once() {
let (mut session, sends) = paced(0, 0, 0, Duration::ZERO).await;
let start = Instant::now();
for _ in 0..3 {
read(&session, A).await;
}
assert_eq!(sent_to(&sends, A, start), [Duration::ZERO; 3]);
session.stop().await.unwrap();
}
#[tokio::test(start_paused = true)]
async fn an_endpoint_retry_is_not_held_back_by_the_interval() {
let (mut session, sends) = paced(500, 1, 1, Duration::ZERO).await;
let start = Instant::now();
read(&session, A).await;
read(&session, A).await;
assert_eq!(sent_to(&sends, A, start), ms(&[0, 100, 600]));
session.stop().await.unwrap();
}
#[tokio::test(start_paused = true)]
async fn an_endpoint_request_given_up_counts_as_finished_when_given_up() {
let (mut session, sends) = paced(50, 0, 1, Duration::ZERO).await;
let client = session.cloned_client_handle().unwrap();
let start = Instant::now();
let abandoned = tokio::time::timeout(Duration::from_millis(20), try_read(&client, A)).await;
assert!(abandoned.is_err());
let waiting = tokio::time::timeout(Duration::from_millis(10), try_read(&client, A)).await;
assert!(waiting.is_err());
read(&session, A).await;
assert_eq!(sent_to(&sends, A, start), ms(&[0, 70]));
session.stop().await.unwrap();
}
#[tokio::test(start_paused = true)]
async fn a_waiting_endpoint_request_fails_at_once_when_the_session_stops() {
let (mut session, sends) = paced(300_000, 0, 0, Duration::ZERO).await;
let client = session.cloned_client_handle().unwrap();
read(&session, A).await;
let waiting = tokio::spawn(async move { try_read(&client, A).await });
tokio::time::sleep(Duration::from_secs(1)).await;
session.stop().await.unwrap();
let ended = tokio::time::timeout(Duration::from_secs(1), waiting).await;
let error = ended.expect("ended at once").unwrap().unwrap_err();
assert!(
matches!(&error, Error::Encoding(message) if message == "endpoint shutdown"),
"{error:?}"
);
assert_eq!(sends.lock().unwrap().len(), 1);
}
#[test]
fn an_endpoint_interval_past_an_hour_is_refused() {
let new = |interval_ms| {
let config = SessionConfig {
min_request_interval_ms: interval_ms,
..SessionConfig::default()
};
let devices = devices(&Sends::default(), 0, Duration::ZERO);
EndpointSession::new(devices, SessionRole::ClientOnly, config).map(drop)
};
let hour = bacnet_client::client::MAX_MIN_REQUEST_INTERVAL_MS;
assert!(new(hour).is_ok());
assert!(matches!(new(hour + 1), Err(Error::Encoding(_))));
}