use std::net::{Ipv4Addr, SocketAddrV6};
use std::time::{Duration, Instant};
use bacnet_services::who_is::DeviceInstanceRange;
use bacnet_types::enums::Segmentation;
use super::point::{BrowseRead, BrowseResult, ReqId};
pub(crate) type OpId = u64;
pub(crate) const MAX_INSTANCE: u32 = 4_194_303;
#[derive(Debug, Clone, PartialEq, Eq)]
pub(crate) enum WhoIsScope {
Local,
Global,
Directed {
to: DirectedTo,
label: String,
},
Network(u16),
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub(crate) enum DirectedTo {
Mac(Vec<u8>),
Bip6(SocketAddrV6),
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub(crate) struct WhoIsSpec {
pub(crate) scope: WhoIsScope,
pub(crate) range: Option<DeviceInstanceRange>,
pub(crate) listen: Duration,
}
impl WhoIsSpec {
pub(crate) fn is_full_range(&self) -> bool {
self.range
.is_none_or(|range| (range.low(), range.high()) == (0, MAX_INSTANCE))
}
pub(crate) fn with_vmac(mut self, vmac: Vec<u8>) -> Self {
if let WhoIsScope::Directed { to, .. } = &mut self.scope {
*to = DirectedTo::Mac(vmac);
}
self
}
pub(crate) fn is_global(&self) -> bool {
self.scope == WhoIsScope::Global
}
pub(crate) fn wants(&self, instance: u32) -> bool {
self.range.is_none_or(|range| range.contains(instance))
}
pub(crate) fn describe(&self) -> String {
let scope = match &self.scope {
WhoIsScope::Local => "local".to_string(),
WhoIsScope::Global => "global".to_string(),
WhoIsScope::Directed { label, .. } => format!("to {label}"),
WhoIsScope::Network(dnet) => format!("network {dnet}"),
};
match self.range {
Some(range) if range.low() == range.high() => format!("{scope} {}", range.low()),
Some(range) if !self.is_full_range() => {
format!("{scope} {}-{}", range.low(), range.high())
}
_ => format!("{scope}, all"),
}
}
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub(crate) enum Command {
Profile(super::profile::service::Request),
Reconnect,
Disconnect,
Connect {
interface: Ipv4Addr,
broadcast: Ipv4Addr,
},
JoinTrunk {
settings: crate::mstp::MstpArgs,
},
WhoIs {
op: OpId,
spec: WhoIsSpec,
},
Cancel {
op: OpId,
},
Browse {
req: ReqId,
device: u32,
read: BrowseRead,
},
Names {
req: ReqId,
asks: Vec<super::point::NameAsk>,
},
StopBrowse {
device: u32,
},
Watch(super::watched::WatchCommand),
Safety(super::safety::SafetyCommand),
Network(super::network::NetworkCommand),
Discovery(super::network::discovery::DiscoveryCommand),
Capture(super::capture::CaptureCommand),
CaptureExport(Box<super::capture::ExportJob>),
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub(crate) struct DeviceRow {
pub(crate) instance: u32,
pub(crate) address: String,
pub(crate) via: Option<String>,
pub(crate) network: Option<u16>,
pub(crate) vendor_id: u16,
pub(crate) max_apdu: u32,
pub(crate) segmentation: Segmentation,
pub(crate) last_seen: Instant,
}
impl DeviceRow {
pub(crate) fn located(&self) -> String {
match &self.via {
Some(router) => format!("{} via {router}", self.address),
None => self.address.clone(),
}
}
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub(crate) struct Failure {
pub(crate) summary: String,
pub(crate) detail: String,
}
impl Failure {
pub(crate) fn of(error: &bacnet_types::error::Error) -> Self {
use bacnet_types::error::Error;
let summary = match error {
Error::Transport(io) => io_summary(io),
Error::Timeout(_) => "timed out".to_string(),
other => other.to_string(),
};
Self {
summary,
detail: error.to_string(),
}
}
pub(crate) fn within(context: &str, error: &bacnet_types::error::Error) -> Self {
let Self { summary, detail } = Self::of(error);
Self {
summary: format!("{context}: {summary}"),
detail: format!("{context}: {detail}"),
}
}
pub(crate) fn text(detail: impl Into<String>) -> Self {
let detail = detail.into();
Self {
summary: strip_os_error(&detail).to_string(),
detail,
}
}
}
#[cfg(test)]
impl From<&str> for Failure {
fn from(detail: &str) -> Self {
Self::text(detail)
}
}
impl std::fmt::Display for Failure {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.write_str(&self.detail)
}
}
fn io_summary(error: &std::io::Error) -> String {
match error.raw_os_error() {
Some(_) => error.kind().to_string(),
None => strip_os_error(&error.to_string()).to_string(),
}
}
fn strip_os_error(text: &str) -> &str {
match text.rfind(" (os error ") {
Some(at) if text.ends_with(')') => &text[..at],
_ => text,
}
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub(crate) enum OpOutcome {
Completed,
Cancelled,
Failed(String),
}
#[derive(Debug, Clone, PartialEq)]
pub(crate) enum WorkerEvent {
Fatal { error: String },
Profile(Result<super::profile::service::Reply, String>),
Rejected { error: String },
SessionStarted { generation: u64 },
Disconnected { generation: u64 },
Session {
generation: u64,
event: Box<WorkerEvent>,
},
Connected {
local: String,
},
ConnectFailed {
error: Failure,
},
Discovered(DeviceRow),
Updated(DeviceRow),
Lost(DeviceRow),
Collision {
retained: DeviceRow,
incoming: DeviceRow,
},
Snapshot(Vec<DeviceRow>),
WhoIsSent {
op: OpId,
},
OpRejected {
op: OpId,
cancel: bool,
error: String,
},
OpFinished {
op: OpId,
outcome: OpOutcome,
},
Browse {
req: ReqId,
result: BrowseResult,
},
Watch(super::watched::WatchEvent),
Safety(super::safety::SafetyEvent),
Passive,
ForeignDevice(super::network::Foreign),
Network(super::network::NetworkEvent),
Discovery(super::network::discovery::DiscoveryEvent),
#[cfg_attr(not(feature = "sc-tls"), allow(dead_code))]
Sc(super::sc::ScReport),
#[cfg_attr(not(feature = "serial"), allow(dead_code))]
Mstp(super::mstp::MstpReport),
Monitor(super::monitor::MonitorReport),
Capture(super::capture::CaptureEvent),
}
#[cfg(test)]
mod tests {
use super::*;
fn spec(scope: WhoIsScope, range: Option<(u32, u32)>) -> WhoIsSpec {
WhoIsSpec {
scope,
range: range.map(|(low, high)| DeviceInstanceRange::new(low, high).unwrap()),
listen: Duration::from_secs(3),
}
}
#[test]
fn full_range_and_wants_follow_the_limits() {
assert!(spec(WhoIsScope::Local, None).is_full_range());
assert!(spec(WhoIsScope::Local, Some((0, MAX_INSTANCE))).is_full_range());
let bounded = spec(WhoIsScope::Network(5), Some((101, 103)));
assert!(!bounded.is_full_range());
assert!(bounded.wants(101) && bounded.wants(103));
assert!(!bounded.wants(100) && !bounded.wants(104));
assert_eq!(bounded.describe(), "network 5 101-103");
assert_eq!(spec(WhoIsScope::Global, None).describe(), "global, all");
}
}