use std::any::Any;
use std::collections::{BTreeMap, HashSet, VecDeque};
use std::future::Future;
use std::net::{Ipv4Addr, SocketAddrV4};
use std::time::Duration;
use bacnet_client::client::BACnetClient;
use bacnet_transport::bip::BipTransport;
use bacnet_transport::port::TransportPort;
use futures_util::stream::{FuturesUnordered, StreamExt};
use tokio::sync::mpsc;
use super::EventSink;
use crate::tui::message::WorkerEvent;
use crate::tui::network::matrix::Probe;
use crate::tui::network::{
address_of, mac, probe_error, read_error, BdtRow, FdtRow, NetworkCommand, NetworkEvent,
};
const STATUS_EVERY: Duration = Duration::from_secs(1);
pub(crate) const MAX_SURVEY: usize = 16;
pub(crate) const SURVEY_AT_ONCE: usize = 4;
const MAX_QUEUED: usize = 8;
fn as_bip<T: TransportPort + 'static>(transport: &T) -> Option<&BipTransport> {
(transport as &dyn Any).downcast_ref()
}
async fn finished<F: Future<Output = NetworkEvent> + Unpin>(job: &mut Option<F>) -> NetworkEvent {
match job {
Some(job) => job.await,
None => std::future::pending().await,
}
}
pub(crate) async fn run<T: TransportPort + 'static>(
client: &BACnetClient<T>,
sink: &EventSink,
mut commands: mpsc::Receiver<NetworkCommand>,
) {
let bip = as_bip(client.transport());
let local =
address_of(client.local_mac()).unwrap_or(SocketAddrV4::new(Ipv4Addr::UNSPECIFIED, 0));
let mut status = tokio::time::interval(STATUS_EVERY);
status.set_missed_tick_behavior(tokio::time::MissedTickBehavior::Delay);
let mut queue: VecDeque<NetworkCommand> = VecDeque::new();
let mut running: Option<NetworkCommand> = None;
let mut job = None;
let mut registered = false;
loop {
if job.is_none() {
if let (Some(bip), Some(next)) = (bip, queue.pop_front()) {
running = Some(next);
job = Some(Box::pin(execute(bip, next, local)));
}
}
tokio::select! {
biased;
() = sink.cancelled() => break,
command = commands.recv() => match command {
None => break,
Some(command) if bip.is_none() => {
let reason = "the Network screen needs BACnet/IP".to_string();
sink.deliver(WorkerEvent::Network(NetworkEvent::Refused { command, reason })).await;
}
Some(command) if running == Some(command) || queue.contains(&command) => {}
Some(command) if queue.len() >= MAX_QUEUED => {
let reason = "the network worker is busy; try again".to_string();
sink.deliver(WorkerEvent::Network(NetworkEvent::Refused { command, reason })).await;
}
Some(command) => queue.push_back(command),
},
event = finished(&mut job), if job.is_some() => {
job = None;
running = None;
sink.deliver(WorkerEvent::Network(event)).await;
}
_ = status.tick(), if bip.is_some() => {
let Some(bip) = bip else { continue };
let snapshot = bip.bvlc_client_snapshot().foreign_registration;
if let Some(bbmd) = snapshot.last_bbmd {
let tables = NetworkCommand::Tables { bbmd };
if snapshot.registered && !registered && running != Some(tables) && !queue.contains(&tables) {
queue.push_back(tables);
}
registered = snapshot.registered;
sink.offer(WorkerEvent::Network(NetworkEvent::Registration(snapshot)));
}
}
}
}
}
async fn execute(bip: &BipTransport, command: NetworkCommand, local: SocketAddrV4) -> NetworkEvent {
match command {
NetworkCommand::Tables { bbmd } => {
let target = mac(bbmd);
let bdt = bip
.read_bdt(&target)
.await
.map(|rows| rows.iter().map(BdtRow::from).collect())
.map_err(|error| read_error(&error));
let fdt = bip
.read_fdt(&target)
.await
.map(|rows| rows.iter().map(FdtRow::from).collect())
.map_err(|error| read_error(&error));
NetworkEvent::Tables {
bbmd,
local,
bdt,
fdt,
}
}
NetworkCommand::Survey { seed } => NetworkEvent::Survey {
seed,
probes: survey(bip, seed).await,
},
}
}
async fn survey(bip: &BipTransport, seed: SocketAddrV4) -> Vec<(SocketAddrV4, Probe)> {
survey_with(seed, |bbmd| async move {
match bip.read_bdt(&mac(bbmd)).await {
Ok(rows) => Probe::Table(rows.iter().map(BdtRow::from).collect()),
Err(error) => probe_error(&error),
}
})
.await
}
async fn survey_with<F, R>(seed: SocketAddrV4, read: F) -> Vec<(SocketAddrV4, Probe)>
where
F: Fn(SocketAddrV4) -> R,
R: Future<Output = Probe>,
{
let read = &read;
let mut seen = HashSet::from([seed]);
let mut found = vec![seed];
let mut probes = Vec::new();
let mut early = BTreeMap::new();
let mut started = 0;
let mut reads = FuturesUnordered::new();
loop {
while reads.len() < SURVEY_AT_ONCE && started < found.len().min(MAX_SURVEY) {
let (index, bbmd) = (started, found[started]);
reads.push(async move { (index, read(bbmd).await) });
started += 1;
}
let Some((index, probe)) = reads.next().await else {
break;
};
early.insert(index, probe);
while let Some(probe) = early.remove(&probes.len()) {
if let Probe::Table(rows) = &probe {
for row in rows {
if seen.insert(row.address) {
found.push(row.address);
}
}
}
probes.push((found[probes.len()], probe));
}
}
let unread = found[probes.len()..].iter();
probes.extend(unread.map(|&bbmd| (bbmd, Probe::Skipped)));
probes
}
#[cfg(test)]
#[path = "network_tests.rs"]
mod tests;