use super::data_attributes::sc_hub_accept_with_limits;
use super::*;
use bacnet_npdu::NpduLimits;
use bacnet_transport::sc::{LoopbackWebSocket, ScReconnectConfig, ScTransport, WebSocketPort};
use bacnet_transport::sc_frame::{
ScFunction, ScMessage, ScOption, Vmac, decode_sc_message, encode_sc_message,
};
use tokio::time::{Instant, sleep, timeout};
const NODE: Vmac = [0x0A; 6];
const PEER: Vmac = [0x22; 6];
const WAIT: Duration = Duration::from_secs(5);
struct Hubs {
router: BACnetRouter,
hub_a: LoopbackWebSocket,
primary: LoopbackWebSocket,
failover: LoopbackWebSocket,
}
async fn start(primary: (u16, u16)) -> Hubs {
let (client_a, hub_a) = LoopbackWebSocket::pair();
let (client_primary, hub_primary) = LoopbackWebSocket::pair();
let (client_failover, hub_failover) = LoopbackWebSocket::pair();
let accept_a = tokio::spawn(async move {
sc_hub_accept_with_limits(&hub_a, [0xA0; 6], 1476, 1476).await;
hub_a
});
let accept_primary = tokio::spawn(async move {
sc_hub_accept_with_limits(&hub_primary, [0xB0; 6], primary.0, primary.1).await;
hub_primary
});
let ports = vec![
RouterPort {
transport: ScTransport::new(client_a, [0x01; 6]).with_device_uuid([1; 16]),
network_number: 1000,
},
RouterPort {
transport: ScTransport::new(client_primary, [0x02; 6])
.with_device_uuid([2; 16])
.with_reconnect(ScReconnectConfig {
initial_delay_ms: 25,
max_delay_ms: 25,
max_retries: 1,
})
.with_failover(client_failover),
network_number: 2000,
},
];
let StartedRouter { router, .. } = BACnetRouter::start(ports, RouterOptions::new())
.await
.unwrap();
Hubs {
router,
hub_a: accept_a.await.unwrap(),
primary: accept_primary.await.unwrap(),
failover: hub_failover,
}
}
impl Hubs {
async fn fail_over(
self,
limits: (u16, u16),
expected: u16,
) -> (BACnetRouter, LoopbackWebSocket, LoopbackWebSocket) {
let Self {
router,
hub_a,
primary,
failover,
} = self;
let accept = tokio::spawn(async move {
sc_hub_accept_with_limits(&failover, [0xC0; 6], limits.0, limits.1).await;
failover
});
drop(primary);
let failover = timeout(WAIT, accept)
.await
.expect("the port fails over")
.unwrap();
let deadline = Instant::now() + WAIT;
while router.shared.engine.lock().ports()[1].max_npdu != NpduLimits::both(expected) {
assert!(
Instant::now() < deadline,
"the router never routed by {expected}"
);
sleep(Duration::from_millis(10)).await;
}
(router, hub_a, failover)
}
}
async fn send_to_peer(hub_a: &LoopbackWebSocket, apdu: usize) {
send_with_options(hub_a, apdu, Vec::new()).await;
}
async fn send_with_options(hub_a: &LoopbackWebSocket, apdu: usize, data_options: Vec<ScOption>) {
let mut payload = vec![0x10, 0x08];
payload.resize(apdu, 0);
let npdu = Npdu {
destination: Some(NpduAddress {
network: 2000,
mac_address: MacAddr::from_slice(&PEER),
}),
hop_count: 255,
payload: Bytes::from(payload),
..Npdu::default()
};
let mut npdu_buf = BytesMut::new();
encode_npdu(&mut npdu_buf, &npdu).unwrap();
send_encoded_with_options(hub_a, npdu_buf.freeze(), data_options).await;
}
async fn send_encoded_with_options(
hub: &LoopbackWebSocket,
npdu: Bytes,
data_options: Vec<ScOption>,
) {
let message = ScMessage {
function: ScFunction::EncapsulatedNpdu,
message_id: 0x4455,
originating_vmac: Some(NODE),
destination_vmac: None,
dest_options: Vec::new(),
data_options,
payload: npdu,
};
let mut buf = BytesMut::new();
encode_sc_message(&mut buf, &message);
hub.send(&buf).await.unwrap();
}
async fn next_npdu_to(hub: &LoopbackWebSocket, to: Vmac) -> Bytes {
let wanted = async {
loop {
let message = decode_sc_message(&hub.recv().await.unwrap()).unwrap();
if message.function == ScFunction::EncapsulatedNpdu
&& message.destination_vmac == Some(to)
{
return message.payload;
}
}
};
timeout(WAIT, wanted).await.expect("the NPDU comes")
}
fn is_reason_4(npdu: Bytes) -> bool {
let npdu = decode_npdu(npdu).unwrap();
npdu.message_type == Some(NetworkMessageType::REJECT_MESSAGE_TO_NETWORK)
&& npdu.payload.as_ref() == [4, 0x07, 0xD0]
}
#[tokio::test]
async fn a_hub_that_carries_less_draws_reason_4() {
let hubs = start((1600, 1497)).await;
assert_eq!(
hubs.router.shared.engine.lock().ports()[1].max_npdu,
NpduLimits::both(1497)
);
let (mut router, hub_a, _failover) = hubs.fail_over((1200, 480), 480).await;
send_to_peer(&hub_a, 590).await;
assert!(is_reason_4(next_npdu_to(&hub_a, NODE).await));
assert_eq!(router.counters().egress_too_long, 1);
router.stop().await.unwrap();
}
#[tokio::test]
async fn a_hub_that_carries_more_takes_what_the_old_one_refused() {
let hubs = start((1200, 480)).await;
send_to_peer(&hubs.hub_a, 590).await;
assert!(is_reason_4(next_npdu_to(&hubs.hub_a, NODE).await));
let (mut router, hub_a, failover) = hubs.fail_over((1600, 1497), 1497).await;
send_to_peer(&hub_a, 590).await;
assert_eq!(next_npdu_to(&failover, PEER).await.len(), 601);
assert_eq!(router.counters().egress_too_long, 1);
router.stop().await.unwrap();
}
#[tokio::test]
async fn forwarded_attributes_beyond_max_bvlc_return_reason_4() {
let mut hubs = start((100, 1497)).await;
send_with_options(
&hubs.hub_a,
2,
vec![ScOption {
option_type: 31,
must_understand: false,
data: vec![0xAB; 100],
}],
)
.await;
send_to_peer(&hubs.hub_a, 2).await;
assert_eq!(next_npdu_to(&hubs.primary, PEER).await.len(), 13);
let counts = hubs.router.counters();
assert_eq!(
(counts.egress_too_long, counts.refused, counts.rejects),
(1, 1, 1)
);
assert!(is_reason_4(next_npdu_to(&hubs.hub_a, NODE).await));
hubs.router.stop().await.unwrap();
}
#[tokio::test]
async fn forwarded_reject_attributes_beyond_max_bvlc_do_not_make_feedback() {
let mut hubs = start((100, 1497)).await;
let mut reject = vec![1, 0xa0, 7, 0xd0, 6];
reject.extend_from_slice(&PEER);
reject.extend_from_slice(&[255, 3, 4, 0, 77]);
send_encoded_with_options(
&hubs.hub_a,
Bytes::from(reject),
vec![ScOption {
option_type: 31,
must_understand: false,
data: vec![0xab; 100],
}],
)
.await;
send_to_peer(&hubs.hub_a, 2).await;
assert_eq!(next_npdu_to(&hubs.primary, PEER).await.len(), 13);
let counts = hubs.router.counters();
assert_eq!(
(counts.egress_too_long, counts.refused, counts.rejects),
(1, 1, 0)
);
hubs.router.stop().await.unwrap();
}