use bacnet_client::client::{BACnetClient, ReceivedIHave};
use bacnet_network::layer::{NetworkReport, ReceivedNetworkReport};
use bacnet_services::who_has::WhoHasObject;
use bacnet_transport::port::TransportPort;
use tokio::sync::broadcast::error::RecvError;
use tokio::sync::mpsc;
use super::EventSink;
use crate::core::target::AddressStyle;
use crate::tui::message::WorkerEvent;
use crate::tui::network::discovery::{DiscoveryCommand, DiscoveryEvent, FoundRow};
fn found(reply: &ReceivedIHave, style: AddressStyle) -> FoundRow {
let routed = reply
.source_network
.as_ref()
.map(|source| (source.network, source.mac_address.as_slice()));
let (address, via) = style.locate(&reply.source_mac, routed);
FoundRow {
device: reply.i_have.device_identifier.instance_number(),
object: reply.i_have.object_identifier,
name: reply.i_have.object_name.clone(),
address,
via,
network: reply.source_network.as_ref().map(|source| source.network),
}
}
fn report(received: ReceivedNetworkReport, style: AddressStyle) -> Option<DiscoveryEvent> {
let from = style.format(&received.source_mac);
Some(match received.report {
NetworkReport::IAmRouterToNetwork(networks) => DiscoveryEvent::Router {
router: from,
networks,
},
NetworkReport::NetworkNumberIs {
network,
configured,
} => DiscoveryEvent::NetworkNumber {
network,
configured,
},
NetworkReport::RejectMessageToNetwork(reject) => DiscoveryEvent::Rejected {
router: from,
network: reject.network,
reason: crate::decode::frame::kebab(reject.reason),
},
_ => return None,
})
}
pub(crate) async fn run<T: TransportPort + 'static>(
client: &BACnetClient<T>,
sink: &EventSink,
mut commands: mpsc::Receiver<DiscoveryCommand>,
style: AddressStyle,
) {
let mut i_haves = client.i_have_events();
let mut reports = client.network_reports();
loop {
tokio::select! {
biased;
() = sink.cancelled() => break,
command = commands.recv() => {
let Some(command) = command else { break };
let sent = match &command {
DiscoveryCommand::WhoHas { name } => {
client.who_has(WhoHasObject::Name(name.clone()), None).await
}
DiscoveryCommand::WhoIsRouter => client.who_is_router_to_network(None, None).await,
};
if let Err(error) = sent {
let reason = error.to_string();
let event = DiscoveryEvent::Refused { command, reason };
sink.deliver(WorkerEvent::Discovery(event)).await;
}
}
received = i_haves.recv() => match received {
Ok(reply) => {
sink.offer(WorkerEvent::Discovery(DiscoveryEvent::IHave(found(&reply, style))));
}
Err(RecvError::Lagged(missed)) => sink.add_dropped(missed),
Err(RecvError::Closed) => break,
},
received = reports.recv() => match received {
Ok(received) => {
if let Some(event) = report(received, style) {
sink.offer(WorkerEvent::Discovery(event));
}
}
Err(RecvError::Lagged(missed)) => sink.add_dropped(missed),
Err(RecvError::Closed) => break,
},
}
}
}
#[cfg(test)]
mod tests {
use bacnet_encoding::npdu::NpduAddress;
use bacnet_npdu::{RejectMessageReason, RejectMessageToNetwork};
use bacnet_services::who_has::IHaveRequest;
use bacnet_types::enums::ObjectType;
use bacnet_types::primitives::ObjectIdentifier;
use bacnet_types::MacAddr;
use super::*;
const ROUTER: [u8; 6] = [10, 0, 1, 1, 0xBA, 0xC0];
fn i_have(source_network: Option<NpduAddress>) -> ReceivedIHave {
let id = |kind, instance| ObjectIdentifier::new(kind, instance).unwrap();
ReceivedIHave {
i_have: IHaveRequest {
device_identifier: id(ObjectType::DEVICE, 3005),
object_identifier: id(ObjectType::ANALOG_INPUT, 1),
object_name: "Supply Air Temp".into(),
},
source_mac: MacAddr::from_slice(&ROUTER),
source_network,
}
}
#[test]
fn found_objects_are_located_like_the_device_table() {
let routed = NpduAddress {
network: 3,
mac_address: MacAddr::from_slice(&[5]),
};
let row = found(&i_have(Some(routed)), AddressStyle::Bip);
assert_eq!(row.address, "3:0x05");
assert_eq!(row.via.as_deref(), Some("10.0.1.1:47808"));
assert_eq!(row.network, Some(3));
let row = found(&i_have(None), AddressStyle::Bip);
assert_eq!((row.address.as_str(), row.via), ("10.0.1.1:47808", None));
}
#[test]
fn a_reject_reason_is_kebab_case() {
let received = ReceivedNetworkReport {
source_mac: MacAddr::from_slice(&ROUTER),
source_network: None,
report: NetworkReport::RejectMessageToNetwork(RejectMessageToNetwork {
reason: RejectMessageReason::NOT_DIRECTLY_CONNECTED,
network: 9,
}),
};
let Some(DiscoveryEvent::Rejected { reason, .. }) = report(received, AddressStyle::Bip)
else {
panic!("expected a reject");
};
assert_eq!(reason, "not-directly-connected");
}
}