use std::future::Future;
use std::pin::Pin;
use std::time::Duration;
use bacnet_client::client::BACnetClient;
use bacnet_services::who_is::WhoIsRequest;
use bacnet_transport::port::TransportPort;
use bacnet_types::enums::UnconfirmedServiceChoice;
use bacnet_types::error::Error;
use bytes::BytesMut;
use tokio::sync::mpsc;
use tokio::task::JoinHandle;
use tokio::time::Instant;
use super::{DeviceFeed, EventSink};
use crate::transport::{self, TransportArgs};
use crate::tui::message::{
AddressStyle, Command, DirectedTo, OpId, OpOutcome, WhoIsScope, WhoIsSpec, WorkerEvent,
};
const RESYNC_EVERY: Duration = Duration::from_secs(1);
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub(crate) enum TransportKind {
Bip,
Bip6,
Sc,
}
impl TransportKind {
pub(crate) fn label(self) -> &'static str {
match self {
Self::Bip => "BIP",
Self::Bip6 => "BIP6",
Self::Sc => "SC",
}
}
pub(crate) fn style(self) -> AddressStyle {
match self {
Self::Bip => AddressStyle::Bip,
Self::Bip6 => AddressStyle::Bip6,
Self::Sc => AddressStyle::Hex,
}
}
}
pub(crate) struct ConnectPlan {
pub(crate) kind: TransportKind,
pub(crate) args: TransportArgs,
pub(crate) await_interface: bool,
}
pub(crate) fn spawn(
plan: ConnectPlan,
sink: EventSink,
commands: mpsc::Receiver<Command>,
) -> JoinHandle<()> {
tokio::spawn(Box::pin(connect(plan, sink, commands)))
}
async fn connect(plan: ConnectPlan, sink: EventSink, mut commands: mpsc::Receiver<Command>) {
let mut args = plan.args;
if plan.await_interface {
loop {
match commands.recv().await {
Some(Command::Connect {
interface,
broadcast,
}) => {
args.interface = interface;
args.broadcast = broadcast;
break;
}
Some(_) => {}
None => return,
}
}
}
let style = plan.kind.style();
match plan.kind {
TransportKind::Bip => match Box::pin(transport::build_bip_client(&args)).await {
Ok(client) => {
if let Some(foreign) = &args.foreign_device {
let bbmd = std::net::SocketAddrV4::new(foreign.bbmd_ip, foreign.bbmd_port);
match crate::foreign::await_registration(&client, bbmd).await {
Ok(()) => tracing::info!(
"BBMD {bbmd} accepted the foreign-device registration; it renews automatically"
),
Err(error) => tracing::warn!("{error}; retrying in the background"),
}
}
Box::pin(serve(client, style, sink, commands)).await
}
Err(error) => failed(&sink, error).await,
},
TransportKind::Bip6 => match Box::pin(transport::build_bip6_client(&args)).await {
Ok(client) => Box::pin(serve(client, style, sink, commands)).await,
Err(error) => failed(&sink, error).await,
},
#[cfg(feature = "sc-tls")]
TransportKind::Sc => match Box::pin(transport::build_sc_client(&args)).await {
Ok(client) => Box::pin(serve(client, style, sink, commands)).await,
Err(error) => failed(&sink, error).await,
},
#[cfg(not(feature = "sc-tls"))]
TransportKind::Sc => {
let error = Error::Encoding("BACnet/SC requires the 'sc-tls' feature".into());
failed(&sink, error).await;
}
}
}
async fn failed(sink: &EventSink, error: Error) {
tracing::error!("connect failed: {error}");
sink.deliver(WorkerEvent::ConnectFailed {
error: error.to_string(),
})
.await;
}
struct Resolving<'a> {
op: OpId,
spec: WhoIsSpec,
vmac: Pin<Box<dyn Future<Output = Result<Vec<u8>, String>> + Send + 'a>>,
}
async fn resolved(resolving: &mut Option<Resolving<'_>>) -> Result<Vec<u8>, String> {
match resolving {
Some(lookup) => lookup.vmac.as_mut().await,
None => std::future::pending().await,
}
}
pub(crate) async fn serve<T: TransportPort + 'static>(
mut client: BACnetClient<T>,
style: AddressStyle,
sink: EventSink,
mut commands: mpsc::Receiver<Command>,
) {
let mut local = style.format(client.local_mac());
if let Some(bound) = crate::resolve::as_bip6(client.transport()).and_then(|t| t.local_address())
{
local = format!("{local} ({bound})");
}
tracing::info!("connected; local address {local}");
let mut devices = client.device_events();
let mut collisions = client.device_collision_events();
if sink.deliver(WorkerEvent::Connected { local }).await {
let mut feed = DeviceFeed::new(style);
let mut listening: Option<(OpId, Instant)> = None;
let mut resolving: Option<Resolving<'_>> = None;
let mut resync = tokio::time::interval(RESYNC_EVERY);
resync.set_missed_tick_behavior(tokio::time::MissedTickBehavior::Delay);
loop {
let deadline = listening.map_or_else(far_future, |(_, until)| until);
tokio::select! {
command = commands.recv() => match command {
Some(Command::WhoIs { op, spec }) => {
let superseded = listening.take().map(|(old, _)| old);
let superseded = superseded.or(resolving.take().map(|lookup| lookup.op));
if let Some(old) = superseded {
sink.deliver(WorkerEvent::OpFinished { op: old, outcome: OpOutcome::Cancelled }).await;
}
if let WhoIsScope::Directed { to: DirectedTo::Bip6(peer), .. } = &spec.scope {
let vmac = Box::pin(crate::resolve::bip6_vmac(client.transport(), *peer));
resolving = Some(Resolving { op, spec, vmac });
} else {
listening = who_is(&client, &sink, op, &spec).await;
}
}
Some(Command::Cancel { op }) => {
let running = listening.is_some_and(|(id, _)| id == op)
|| resolving.as_ref().is_some_and(|lookup| lookup.op == op);
if running {
listening = None;
resolving = None;
tracing::info!("Who-Is {op} cancelled");
sink.deliver(WorkerEvent::OpFinished { op, outcome: OpOutcome::Cancelled }).await;
}
}
Some(Command::Connect { .. }) => {}
None => break,
},
vmac = resolved(&mut resolving), if resolving.is_some() => {
if let Some(Resolving { op, spec, .. }) = resolving.take() {
listening = match vmac {
Ok(mac) => who_is(&client, &sink, op, &spec.with_vmac(mac)).await,
Err(error) => {
failed_who_is(&sink, op, error).await;
None
}
};
}
}
received = devices.recv(), if feed.devices_open => feed.on_device(&sink, received),
received = collisions.recv(), if feed.collisions_open => {
if let Some(event) = feed.on_collision(&sink, received) {
sink.deliver(event).await;
}
}
() = tokio::time::sleep_until(deadline), if listening.is_some() => {
if let Some((op, _)) = listening.take() {
sink.deliver(WorkerEvent::OpFinished { op, outcome: OpOutcome::Completed }).await;
}
}
_ = resync.tick(), if feed.needs_resync => {
let rows = client.discovered_devices().await.iter().map(|d| feed.row(d)).collect();
if sink.offer(WorkerEvent::Snapshot(rows)) == super::Offer::Sent {
feed.needs_resync = false;
}
}
}
}
}
if let Err(error) = client.stop().await {
tracing::warn!("client stop: {error}");
}
}
fn far_future() -> Instant {
Instant::now() + Duration::from_secs(86_400)
}
async fn who_is<T: TransportPort + 'static>(
client: &BACnetClient<T>,
sink: &EventSink,
op: OpId,
spec: &WhoIsSpec,
) -> Option<(OpId, Instant)> {
match send_who_is(client, spec).await {
Ok(()) => {
tracing::info!("Who-Is {op} sent ({})", spec.describe());
sink.deliver(WorkerEvent::WhoIsSent { op }).await;
Some((op, Instant::now() + spec.listen))
}
Err(error) => {
failed_who_is(sink, op, error).await;
None
}
}
}
async fn failed_who_is(sink: &EventSink, op: OpId, error: String) {
tracing::warn!("Who-Is {op} failed: {error}");
sink.deliver(WorkerEvent::OpFinished {
op,
outcome: OpOutcome::Failed(error),
})
.await;
}
async fn send_who_is<T: TransportPort + 'static>(
client: &BACnetClient<T>,
spec: &WhoIsSpec,
) -> Result<(), String> {
let range = spec.range;
let sent = match &spec.scope {
WhoIsScope::Local => {
let mut buf = BytesMut::new();
WhoIsRequest { range }.encode(&mut buf);
client
.broadcast_unconfirmed(UnconfirmedServiceChoice::WHO_IS, &buf)
.await
}
WhoIsScope::Global => client.who_is(range).await,
WhoIsScope::Directed {
to: DirectedTo::Mac(mac),
..
} => client.who_is_directed(mac, range).await,
WhoIsScope::Directed {
to: DirectedTo::Bip6(peer),
..
} => return Err(format!("the VMAC at {peer} has not been found")),
WhoIsScope::Network(dnet) => client.who_is_network(*dnet, range).await,
};
sent.map_err(|error| error.to_string())
}