use super::*;
use crate::server::device_bindings::BindingFreshness;
use crate::server::event_delivery::EventDelivery;
use crate::server::event_recipient_route::{ConfirmedRouteRefusal, RecipientRoute};
use bacnet_objects::analog::AnalogInputObject;
use bacnet_objects::event::EventStateChange;
use bacnet_objects::traits::BACnetObject;
use bacnet_types::enums::{EngineeringUnits, EventState, EventType, NotifyType};
fn is_link_broadcast(mac: &[u8]) -> bool {
mac == LITERAL_BROADCAST_MAC
}
const MULTICAST: [u8; 6] = [224, 0, 0, 1, 0xBA, 0xC0];
fn is_group(mac: &[u8]) -> bool {
is_link_broadcast(mac) || mac == MULTICAST
}
fn mac(octets: &[u8]) -> MacAddr {
MacAddr::from_slice(octets)
}
fn remote_unicast(network: u16, octets: &[u8]) -> RecipientRoute {
RecipientRoute::RemoteUnicast {
network,
mac: mac(octets),
}
}
fn bound_routed(network: u16, octets: &[u8]) -> RecipientRoute {
RecipientRoute::BoundRoutedUnicast {
network,
mac: mac(octets),
router: mac(&ROUTER),
freshness: BindingFreshness::Configured,
}
}
#[test]
fn localize_leaves_every_route_alone_while_the_number_is_unknown() {
for route in [
|| RecipientRoute::RemoteBroadcast(THIS_NETWORK),
|| remote_unicast(THIS_NETWORK, &PEER_A),
|| remote_unicast(THIS_NETWORK, LITERAL_BROADCAST_MAC),
|| bound_routed(THIS_NETWORK, &PEER_C),
|| bound_routed(THIS_NETWORK, LITERAL_BROADCAST_MAC),
|| bound_routed(THIS_NETWORK, &MULTICAST),
] {
assert_eq!(route().localize(None, is_link_broadcast, is_group), route());
}
}
#[test]
fn localize_takes_only_routes_naming_this_network_as_local() {
let local = Some(THIS_NETWORK);
let contradictory = RecipientRoute::resolve_address(
&bacnet_types::constructed::BACnetAddress {
network_number: 0xFFFF,
mac_address: mac(&PEER_A),
},
is_link_broadcast,
);
assert_eq!(contradictory, RecipientRoute::ContradictoryGlobal);
for route in [
|| RecipientRoute::GlobalBroadcast,
|| RecipientRoute::ContradictoryGlobal,
|| RecipientRoute::LocalBroadcast,
|| RecipientRoute::LocalUnicast(mac(&PEER_A)),
|| RecipientRoute::RemoteBroadcast(REMOTE_NETWORK),
|| remote_unicast(REMOTE_NETWORK, &REMOTE_MAC),
|| bound_routed(REMOTE_NETWORK, &PEER_C),
] {
assert_eq!(
route().localize(local, is_link_broadcast, is_group),
route()
);
}
for (route, expected) in [
(
RecipientRoute::RemoteBroadcast(THIS_NETWORK),
RecipientRoute::LocalBroadcast,
),
(
remote_unicast(THIS_NETWORK, &PEER_A),
RecipientRoute::LocalUnicast(mac(&PEER_A)),
),
(
remote_unicast(THIS_NETWORK, LITERAL_BROADCAST_MAC),
RecipientRoute::LocalBroadcast,
),
(
bound_routed(THIS_NETWORK, &PEER_C),
RecipientRoute::BoundLocalUnicast {
mac: mac(&PEER_C),
freshness: BindingFreshness::Configured,
},
),
(
bound_routed(THIS_NETWORK, LITERAL_BROADCAST_MAC),
RecipientRoute::InvalidDevice,
),
(
bound_routed(THIS_NETWORK, &MULTICAST),
RecipientRoute::InvalidDevice,
),
(
remote_unicast(THIS_NETWORK, &MULTICAST),
RecipientRoute::LocalUnicast(mac(&MULTICAST)),
),
] {
assert_eq!(route.localize(local, is_link_broadcast, is_group), expected);
}
}
#[test]
fn into_confirmed_refuses_a_group_next_hop() {
let refusal = |route: RecipientRoute| route.into_confirmed(is_group).err();
let group = Some(ConfirmedRouteRefusal::GroupNextHop);
let bound_local = |octets: &[u8]| RecipientRoute::BoundLocalUnicast {
mac: mac(octets),
freshness: BindingFreshness::Configured,
};
let routed_via = |router: &[u8]| RecipientRoute::BoundRoutedUnicast {
network: REMOTE_NETWORK,
mac: mac(&PEER_C),
router: mac(router),
freshness: BindingFreshness::Configured,
};
for (case, route, expected) in [
(
"local station",
RecipientRoute::LocalUnicast(mac(&PEER_C)),
None,
),
(
"local multicast",
RecipientRoute::LocalUnicast(mac(&MULTICAST)),
group,
),
(
"local broadcast MAC",
RecipientRoute::LocalUnicast(mac(LITERAL_BROADCAST_MAC)),
group,
),
("bound local station", bound_local(&PEER_C), None),
("bound local multicast", bound_local(&MULTICAST), group),
("routed via a station", routed_via(&ROUTER), None),
(
"routed via a multicast router",
routed_via(&MULTICAST),
group,
),
(
"remote with no router",
remote_unicast(REMOTE_NETWORK, &MULTICAST),
None,
),
(
"local broadcast",
RecipientRoute::LocalBroadcast,
Some(ConfirmedRouteRefusal::NotOneDevice),
),
] {
assert_eq!(refusal(route), expected, "{case}");
}
}
fn input_oid() -> ObjectIdentifier {
ObjectIdentifier::new(ObjectType::ANALOG_INPUT, 1).unwrap()
}
fn alarm_input() -> AnalogInputObject {
let mut ai = AnalogInputObject::new(1, "AI-1", EngineeringUnits::SQUARE_METERS).unwrap();
ai.write_property(
PropertyIdentifier::NOTIFY_TYPE,
None,
PropertyValue::Enumerated(NotifyType::ALARM.to_raw()),
None,
)
.unwrap();
ai
}
#[tokio::test(start_paused = true)]
async fn a_confirmed_notification_to_this_network_is_completed_by_the_direct_ack() {
let mut db = database(Vec::new());
let mut class = NotificationClass::new(0, "NC-0").unwrap();
class
.add_destination(destination(
address_recipient(THIS_NETWORK, &PEER_B),
4,
true,
))
.unwrap();
db.add(Box::new(class)).unwrap();
db.add(Box::new(alarm_input())).unwrap();
let started = Started::new(db, None).await;
assert_eq!(started.announce(THIS_NETWORK, 0).await, THIS_NETWORK);
let server = &started.server;
BACnetServer::<TestTransport>::build_and_send_event_notification_with_bindings(
&EventDelivery {
db: &server.db,
network: server.test_network(),
comm_state: &server.comm_state,
learned_routers: &server.learned_routers,
notification_transactions: &server.notification_transactions,
device_bindings: &server.device_bindings,
suppressions: &server.event_suppressions,
retry_timeout_ms: 1000,
local_apdu_capacity: 1476,
},
&input_oid(),
(
EventStateChange {
from: EventState::NORMAL,
to: EventState::HIGH_LIMIT,
},
EventType::OUT_OF_RANGE,
),
)
.await;
bounded(started.sent.wait_for_len(1)).await;
let request = started.sent.take();
let [frame] = &request[..] else {
panic!("expected one confirmed request, got {request:?}");
};
assert!(!frame.broadcast);
assert_eq!(frame.mac[..], PEER_B);
assert_eq!(frame.decode_npdu().destination, None);
let Apdu::ConfirmedRequest(sent) = frame.apdu() else {
panic!("expected a confirmed request");
};
assert_eq!(server.notification_transactions.active_count(), 1);
let mut apdu = BytesMut::new();
encode_apdu(
&mut apdu,
&Apdu::SimpleAck(SimpleAck {
invoke_id: sent.invoke_id,
service_choice: ConfirmedServiceChoice::CONFIRMED_EVENT_NOTIFICATION,
}),
)
.unwrap();
let mut npdu = BytesMut::new();
encode_npdu(
&mut npdu,
&Npdu {
payload: apdu.freeze(),
..Default::default()
},
)
.unwrap();
started.feed(&npdu, false).await;
for _ in 0..1_000 {
if server.notification_transactions.active_count() == 0 {
break;
}
tokio::task::yield_now().await;
}
assert_eq!(server.notification_transactions.active_count(), 0);
assert!(started.sent.is_empty(), "the request is not sent again");
assert_eq!(
server.event_suppressions.snapshot(),
EventNotificationCounters::default()
);
started.stop().await;
}
#[tokio::test(start_paused = true)]
async fn a_device_bound_at_the_broadcast_mac_on_this_network_is_unroutable() {
let started = Started::new(database(Vec::new()), None).await;
assert_eq!(started.announce(THIS_NETWORK, 0).await, THIS_NETWORK);
let bound = ObjectIdentifier::new(ObjectType::DEVICE, 78).unwrap();
let mut bindings = DeviceBindingTable::new();
bindings
.insert_configured(
DeviceBinding::routed(bound, THIS_NETWORK, LITERAL_BROADCAST_MAC, ROUTER).unwrap(),
|_| false,
)
.unwrap();
let mut class = NotificationClass::new(0, "NC-0").unwrap();
for (process, recipient) in [
(1, BACnetRecipient::Device(bound)),
(2, address_recipient(REMOTE_NETWORK, &REMOTE_MAC)),
] {
class
.add_destination(destination(recipient, process, false))
.unwrap();
}
let mut db = ObjectDatabase::new();
db.add(Box::new(class)).unwrap();
let (broadcasts, unicasts, counters) = distribute_counted_through(
started.server.test_network(),
&started.sent,
db,
Arc::new(RwLock::new(bindings)),
DccState::Enable,
)
.await;
assert_eq!(
routes(broadcasts, unicasts),
[(2, To::Remote(REMOTE_NETWORK, REMOTE_MAC.to_vec()), false)]
);
assert_eq!(
counters,
EventNotificationCounters {
recipient_unroutable: 1,
..Default::default()
}
);
started.stop().await;
}