use std::cell::Cell;
use std::ops::ControlFlow;
use std::time::Duration;
use bacnet_client::client::{BACnetClient, DeviceEventKind};
use bacnet_client::discovery::DiscoveredDevice;
use bacnet_client::log_reader::{LogCursor, LogPage};
use bacnet_services::alarm_event::GetEventInformationAck;
use bacnet_services::alarm_summary::GetAlarmSummaryAck;
use bacnet_services::enrollment_summary::{GetEnrollmentSummaryAck, GetEnrollmentSummaryRequest};
use bacnet_services::file::{AtomicReadFileAck, FileAccessMethod, FileWriteAccessMethod};
use bacnet_services::read_property::ReadPropertyACK;
use bacnet_services::read_range::{ReadRangeReply, ReadRangeRequest, ReadRangeValidation};
use bacnet_services::rpm::ReadPropertyMultipleACK;
use bacnet_services::who_is::DeviceInstanceRange;
use bacnet_services::wpm::WriteAccessSpecification;
use bacnet_transport::port::TransportPort;
use bacnet_types::constructed::{PropertyReference, ReadAccessSpecification};
use bacnet_types::enums::PropertyIdentifier;
use bacnet_types::error::Error;
use bacnet_types::primitives::ObjectIdentifier;
use tokio::sync::broadcast::error::RecvError;
use super::target::{Target, bip6_vmac, parse_target_on};
use crate::engine::{RpmFailure, rpm_failure, rpm_rows, unreachable};
use crate::format::{LogColumns, RawRow, RowError, StateTexts};
#[derive(Debug, Clone, PartialEq, Eq)]
pub enum Destination {
Mac(Vec<u8>),
Device(u32),
}
fn text(error: Error) -> String {
error.to_string()
}
fn not_found(instance: u32) -> String {
format!("Device {instance} not found. Run 'discover' first or use an address.")
}
pub async fn resolve<T: TransportPort + 'static>(
client: &BACnetClient<T>,
text: &str,
wait: Duration,
) -> Result<Destination, String> {
match parse_target_on(client.transport(), text)? {
Target::Mac(mac) => Ok(Destination::Mac(mac)),
Target::Bip6(peer) => Ok(Destination::Mac(bip6_vmac(client.transport(), peer).await?)),
Target::Instance(instance) => match client.get_device(instance).await {
Some(_) => Ok(Destination::Device(instance)),
None => Err(not_found(instance)),
},
Target::Routed { dnet, instance } => {
find_routed(client, dnet, &Station::Instance(instance), wait).await
}
Target::RoutedMac { dnet, mac } => {
find_routed(client, dnet, &Station::Mac(mac), wait).await
}
}
}
pub async fn resolve_mac<T: TransportPort + 'static>(
client: &BACnetClient<T>,
text: &str,
) -> Result<Vec<u8>, String> {
match parse_target_on(client.transport(), text)? {
Target::Mac(mac) => Ok(mac),
Target::Bip6(peer) => bip6_vmac(client.transport(), peer).await,
Target::Instance(instance) => match client.get_device(instance).await {
Some(device) if device.source_network.is_some() => Err(format!(
"Device {instance} is behind a router; only read, readm, read-range, alarms, \
alarm-summary, enrollment-summary, file-read, file-write, write, writem, subscribe, control, reinit, time-sync, create-object, delete-object and ack-alarm reach routed devices"
)),
Some(device) => Ok(device.mac_address.to_vec()),
None => Err(not_found(instance)),
},
Target::Routed { .. } | Target::RoutedMac { .. } => Err(format!(
"'{text}' is a routed target; only read, readm, read-range, alarms, \
alarm-summary, enrollment-summary, file-read, file-write, write, writem, subscribe, control, reinit, time-sync, create-object, delete-object and ack-alarm reach routed devices"
)),
}
}
enum Station {
Instance(u32),
Mac(Vec<u8>),
}
impl Station {
fn matches(&self, dnet: u16, device: &DiscoveredDevice) -> bool {
device.source_network == Some(dnet)
&& match self {
Self::Instance(instance) => device.object_identifier.instance_number() == *instance,
Self::Mac(mac) => device
.source_address
.as_ref()
.is_some_and(|address| address.as_slice() == mac.as_slice()),
}
}
}
async fn find_routed<T: TransportPort + 'static>(
client: &BACnetClient<T>,
dnet: u16,
station: &Station,
wait: Duration,
) -> Result<Destination, String> {
let known = |devices: Vec<DiscoveredDevice>| {
devices
.iter()
.find(|device| station.matches(dnet, device))
.map(|device| Destination::Device(device.object_identifier.instance_number()))
};
if let Some(found) = known(client.discovered_devices().await) {
return Ok(found);
}
let mut events = client.device_events();
let range = match station {
Station::Instance(instance) => {
Some(DeviceInstanceRange::new(*instance, *instance).map_err(text)?)
}
Station::Mac(_) => None,
};
client.who_is_network(dnet, range).await.map_err(text)?;
let deadline = tokio::time::Instant::now() + wait;
loop {
match tokio::time::timeout_at(deadline, events.recv()).await {
Err(_) => {
let name = match station {
Station::Instance(instance) => format!("{dnet}:{instance}"),
Station::Mac(mac) => super::target::routed_station(dnet, mac)
.unwrap_or_else(|| format!("{dnet}:*")),
};
return Err(format!(
"device {name} did not answer a Who-Is on network {dnet} within {} ms",
wait.as_millis()
));
}
Ok(Ok(event)) => {
if event.kind != DeviceEventKind::Lost && station.matches(dnet, &event.device) {
return Ok(Destination::Device(
event.device.object_identifier.instance_number(),
));
}
}
Ok(Err(RecvError::Lagged(_))) => {
if let Some(found) = known(client.discovered_devices().await) {
return Ok(found);
}
}
Ok(Err(RecvError::Closed)) => return Err("the client stopped".into()),
}
}
}
impl Destination {
pub async fn atomic_read_file<T: TransportPort + 'static>(
&self,
client: &BACnetClient<T>,
file: ObjectIdentifier,
access: FileAccessMethod,
) -> Result<AtomicReadFileAck, Error> {
match self {
Self::Mac(mac) => client.atomic_read_file_decoded(mac, file, access).await,
Self::Device(instance) => {
client
.atomic_read_file_decoded_from_device(*instance, file, access)
.await
}
}
}
pub async fn atomic_write_file<T: TransportPort + 'static>(
&self,
client: &BACnetClient<T>,
file: ObjectIdentifier,
access: FileWriteAccessMethod,
) -> Result<bytes::Bytes, Error> {
match self {
Self::Mac(mac) => client.atomic_write_file(mac, file, access).await,
Self::Device(instance) => {
client
.atomic_write_file_from_device(*instance, file, access)
.await
}
}
}
pub async fn create_object<T: TransportPort + 'static>(
&self,
client: &BACnetClient<T>,
specifier: bacnet_services::object_mgmt::ObjectSpecifier,
initial_values: Vec<bacnet_services::common::BACnetPropertyValue>,
) -> Result<bacnet_types::primitives::ObjectIdentifier, Error> {
match self {
Self::Mac(mac) => client.create_object(mac, specifier, initial_values).await,
Self::Device(instance) => {
client
.create_object_from_device(*instance, specifier, initial_values)
.await
}
}
}
pub async fn delete_object<T: TransportPort + 'static>(
&self,
client: &BACnetClient<T>,
object: ObjectIdentifier,
) -> Result<(), Error> {
match self {
Self::Mac(mac) => client.delete_object(mac, object).await,
Self::Device(instance) => client.delete_object_from_device(*instance, object).await,
}
}
pub async fn acknowledge_alarm_request<T: TransportPort + 'static>(
&self,
client: &BACnetClient<T>,
request: &bacnet_services::alarm_event::AcknowledgeAlarmRequest,
) -> Result<(), Error> {
match self {
Self::Mac(mac) => client.acknowledge_alarm_request(mac, request).await,
Self::Device(instance) => {
client
.acknowledge_alarm_request_from_device(*instance, request)
.await
}
}
}
pub async fn device_communication_control<T: TransportPort + 'static>(
&self,
client: &BACnetClient<T>,
state: bacnet_types::enums::EnableDisable,
duration: Option<u16>,
password: Option<String>,
) -> Result<(), Error> {
match self {
Self::Mac(mac) => {
client
.device_communication_control(mac, state, duration, password)
.await
}
Self::Device(instance) => {
client
.device_communication_control_from_device(*instance, state, duration, password)
.await
}
}
}
pub async fn reinitialize_device<T: TransportPort + 'static>(
&self,
client: &BACnetClient<T>,
state: bacnet_types::enums::ReinitializedState,
password: Option<String>,
) -> Result<(), Error> {
match self {
Self::Mac(mac) => client.reinitialize_device(mac, state, password).await,
Self::Device(instance) => {
client
.reinitialize_device_from_device(*instance, state, password)
.await
}
}
}
pub async fn time_synchronization<T: TransportPort + 'static>(
&self,
client: &BACnetClient<T>,
date: bacnet_types::primitives::Date,
time: bacnet_types::primitives::Time,
utc: bool,
) -> Result<(), Error> {
match (self, utc) {
(Self::Mac(mac), false) => client.time_synchronization(mac, date, time).await,
(Self::Mac(mac), true) => client.utc_time_synchronization(mac, date, time).await,
(Self::Device(instance), false) => {
client
.time_synchronization_from_device(*instance, date, time)
.await
}
(Self::Device(instance), true) => {
client
.utc_time_synchronization_from_device(*instance, date, time)
.await
}
}
}
pub async fn get_event_information<T: TransportPort + 'static>(
&self,
client: &BACnetClient<T>,
last: Option<ObjectIdentifier>,
) -> Result<GetEventInformationAck, Error> {
match self {
Self::Mac(mac) => client.get_event_information(mac, last).await,
Self::Device(instance) => {
client
.get_event_information_from_device(*instance, last)
.await
}
}
}
pub async fn get_alarm_summary<T: TransportPort + 'static>(
&self,
client: &BACnetClient<T>,
) -> Result<GetAlarmSummaryAck, Error> {
match self {
Self::Mac(mac) => client.get_alarm_summary(mac).await,
Self::Device(instance) => client.get_alarm_summary_from_device(*instance).await,
}
}
pub async fn get_enrollment_summary<T: TransportPort + 'static>(
&self,
client: &BACnetClient<T>,
request: &GetEnrollmentSummaryRequest,
) -> Result<GetEnrollmentSummaryAck, Error> {
match self {
Self::Mac(mac) => client.get_enrollment_summary(mac, request).await,
Self::Device(instance) => {
client
.get_enrollment_summary_from_device(*instance, request)
.await
}
}
}
pub async fn read_range<T: TransportPort + 'static>(
&self,
client: &BACnetClient<T>,
request: &ReadRangeRequest,
validation: ReadRangeValidation,
) -> Result<ReadRangeReply, Error> {
match self {
Self::Mac(mac) => client.read_range_with(mac, request, validation).await,
Self::Device(instance) => {
client
.read_range_from_device_with(*instance, request, validation)
.await
}
}
}
pub async fn read_log_page<T: TransportPort + 'static>(
&self,
client: &BACnetClient<T>,
log: ObjectIdentifier,
cursor: LogCursor,
page_size: u16,
) -> Result<LogPage, Error> {
match self {
Self::Mac(mac) => client.read_log_page(mac, log, cursor, page_size).await,
Self::Device(instance) => {
client
.read_log_page_from_device(*instance, log, cursor, page_size)
.await
}
}
}
pub async fn log_columns<T: TransportPort + 'static>(
&self,
client: &BACnetClient<T>,
log: ObjectIdentifier,
) -> LogColumns {
if !LogColumns::applies(log.object_type()) {
return LogColumns::default();
}
self.read_property(
client,
log,
PropertyIdentifier::LOG_DEVICE_OBJECT_PROPERTY,
None,
)
.await
.map(|ack| LogColumns::decode(&ack.property_value))
.unwrap_or_default()
}
pub async fn log_state_texts<T: TransportPort + 'static>(
&self,
client: &BACnetClient<T>,
columns: &mut LogColumns,
) {
let log_device = match self {
Self::Device(instance) => Some(*instance),
Self::Mac(mac) => {
let devices = client.discovered_devices().await;
let mut at_mac = devices.iter().filter(|device| {
device.source_network.is_none()
&& device.mac_address.as_slice() == mac.as_slice()
});
match (at_mac.next(), at_mac.next()) {
(Some(device), None) => Some(device.object_identifier.instance_number()),
_ => None,
}
}
};
let no_rpm = Cell::new(false);
let no_rpm = &no_rpm;
columns
.read_state_texts(log_device, |object| async move {
match self.state_text_rows(client, object, no_rpm).await {
Ok(rows) => ControlFlow::Continue(StateTexts::from_rows(&rows)),
Err(error) if unreachable(&error) => ControlFlow::Break(()),
Err(_) => ControlFlow::Continue(StateTexts::default()),
}
})
.await;
}
pub async fn state_texts<T: TransportPort + 'static>(
&self,
client: &BACnetClient<T>,
object: ObjectIdentifier,
no_rpm: &Cell<bool>,
) -> Result<StateTexts, Error> {
if StateTexts::properties(object.object_type()).is_empty() {
return Ok(StateTexts::default());
}
match self.state_text_rows(client, object, no_rpm).await {
Ok(rows) => Ok(StateTexts::from_rows(&rows)),
Err(error) if unreachable(&error) => Err(error),
Err(_) => Ok(StateTexts::default()),
}
}
async fn state_text_rows<T: TransportPort + 'static>(
&self,
client: &BACnetClient<T>,
object: ObjectIdentifier,
no_rpm: &Cell<bool>,
) -> Result<Vec<RawRow>, Error> {
let properties = StateTexts::properties(object.object_type());
let read_each = async || {
let mut rows = Vec::with_capacity(properties.len());
for property in properties {
match self.read_property(client, object, *property, None).await {
Ok(ack) => rows.push((*property, Ok(ack.property_value))),
Err(error) if unreachable(&error) => return Err(error),
Err(error) => rows.push((*property, Err(RowError::from(error)))),
}
}
Ok(rows)
};
if no_rpm.get() {
return read_each().await;
}
if let [property] = properties {
let ack = self.read_property(client, object, *property, None).await?;
return Ok(vec![(*property, Ok(ack.property_value))]);
}
let spec = ReadAccessSpecification {
object_identifier: object,
list_of_property_references: properties
.iter()
.map(|property| PropertyReference {
property_identifier: *property,
property_array_index: None,
})
.collect(),
};
match self.read_property_multiple(client, vec![spec]).await {
Ok(ack) => Ok(rpm_rows(ack)),
Err(error) => match rpm_failure(&error) {
RpmFailure::Other => Err(error),
RpmFailure::Refused => {
no_rpm.set(true);
read_each().await
}
RpmFailure::TooLarge => read_each().await,
},
}
}
pub async fn read_property<T: TransportPort + 'static>(
&self,
client: &BACnetClient<T>,
object: ObjectIdentifier,
property: PropertyIdentifier,
index: Option<u32>,
) -> Result<ReadPropertyACK, Error> {
match self {
Self::Mac(mac) => client.read_property(mac, object, property, index).await,
Self::Device(instance) => {
client
.read_property_from_device(*instance, object, property, index)
.await
}
}
}
pub async fn read_property_multiple<T: TransportPort + 'static>(
&self,
client: &BACnetClient<T>,
specs: Vec<ReadAccessSpecification>,
) -> Result<ReadPropertyMultipleACK, Error> {
match self {
Self::Mac(mac) => client.read_property_multiple(mac, specs).await,
Self::Device(instance) => {
client
.read_property_multiple_from_device(*instance, specs)
.await
}
}
}
pub async fn write_property<T: TransportPort + 'static>(
&self,
client: &BACnetClient<T>,
object: ObjectIdentifier,
property: PropertyIdentifier,
index: Option<u32>,
value: Vec<u8>,
priority: Option<u8>,
) -> Result<(), Error> {
match self {
Self::Mac(mac) => {
client
.write_property(mac, object, property, index, value, priority)
.await
}
Self::Device(instance) => {
client
.write_property_to_device(*instance, object, property, index, value, priority)
.await
}
}
}
pub async fn write_property_multiple<T: TransportPort + 'static>(
&self,
client: &BACnetClient<T>,
specs: Vec<WriteAccessSpecification>,
) -> Result<(), Error> {
match self {
Self::Mac(mac) => client.write_property_multiple(mac, specs).await,
Self::Device(instance) => {
client
.write_property_multiple_to_device(*instance, specs)
.await
}
}
}
pub async fn subscribe_cov<T: TransportPort + 'static>(
&self,
client: &BACnetClient<T>,
process: u32,
object: ObjectIdentifier,
confirmed: bool,
lifetime: Option<u32>,
) -> Result<(), Error> {
match self {
Self::Mac(mac) => {
client
.subscribe_cov(mac, process, object, confirmed, lifetime)
.await
}
Self::Device(instance) => {
client
.subscribe_cov_to_device(*instance, process, object, confirmed, lifetime)
.await
}
}
}
pub async fn unsubscribe_cov<T: TransportPort + 'static>(
&self,
client: &BACnetClient<T>,
process: u32,
object: ObjectIdentifier,
) -> Result<(), Error> {
match self {
Self::Mac(mac) => client.unsubscribe_cov(mac, process, object).await,
Self::Device(instance) => {
client
.unsubscribe_cov_to_device(*instance, process, object)
.await
}
}
}
}