use std::net::{Ipv4Addr, Ipv6Addr};
use std::time::{Duration, Instant};
use bacnet_services::who_is::DeviceInstanceRange;
use bacnet_types::enums::Segmentation;
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 {
mac: Vec<u8>,
label: String,
},
Network(u16),
}
#[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 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 {
Connect {
interface: Ipv4Addr,
broadcast: Ipv4Addr,
},
WhoIs {
op: OpId,
spec: WhoIsSpec,
},
Cancel {
op: OpId,
},
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub(crate) struct DeviceRow {
pub(crate) instance: u32,
pub(crate) address: 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,
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub(crate) enum OpOutcome {
Completed,
Cancelled,
Failed(String),
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub(crate) enum WorkerEvent {
Connected {
local: String,
},
ConnectFailed {
error: String,
},
Discovered(DeviceRow),
Updated(DeviceRow),
Lost(DeviceRow),
Collision {
retained: DeviceRow,
incoming: DeviceRow,
},
Snapshot(Vec<DeviceRow>),
WhoIsSent {
op: OpId,
},
OpFinished {
op: OpId,
outcome: OpOutcome,
},
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub(crate) enum AddressStyle {
Bip,
Bip6,
Hex,
}
impl AddressStyle {
pub(crate) fn format(self, mac: &[u8]) -> String {
match (self, mac.len()) {
(Self::Bip, 6) => crate::output::format_mac(mac),
(Self::Bip6, 18) => {
let mut ip = [0u8; 16];
ip.copy_from_slice(&mac[..16]);
let port = u16::from_be_bytes([mac[16], mac[17]]);
format!("[{}]:{port}", Ipv6Addr::from(ip))
}
_ => hex(mac),
}
}
}
pub(crate) fn hex(bytes: &[u8]) -> String {
let mut out = String::with_capacity(bytes.len() * 3);
for (i, b) in bytes.iter().enumerate() {
if i > 0 {
out.push(':');
}
out.push_str(&format!("{b:02x}"));
}
out
}
#[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");
}
#[test]
fn addresses_format_per_transport() {
assert_eq!(
AddressStyle::Bip.format(&[10, 0, 0, 1, 0xBA, 0xC0]),
"10.0.0.1:47808"
);
let mut v6 = Ipv6Addr::LOCALHOST.octets().to_vec();
v6.extend_from_slice(&47808u16.to_be_bytes());
assert_eq!(AddressStyle::Bip6.format(&v6), "[::1]:47808");
assert_eq!(
AddressStyle::Hex.format(&[2, 0, 0, 0, 0, 1]),
"02:00:00:00:00:01"
);
}
}