use super::*;
use bacnet_types::enums::PropertyIdentifier;
use std::sync::Mutex as StdMutex;
use tokio::sync::oneshot;
use tokio::time::{sleep_until, Instant as TokioInstant};
const DEFAULT_COV_RENEWAL_MARGIN: Duration = Duration::from_secs(30);
const DEFAULT_COV_RENEWAL_EVENT_CAPACITY: usize = 16;
#[derive(Debug, Clone)]
#[non_exhaustive]
pub struct ManagedCOVSubscriptionOptions {
pub renewal_margin: Duration,
pub event_channel_capacity: usize,
}
impl Default for ManagedCOVSubscriptionOptions {
fn default() -> Self {
Self {
renewal_margin: DEFAULT_COV_RENEWAL_MARGIN,
event_channel_capacity: DEFAULT_COV_RENEWAL_EVENT_CAPACITY,
}
}
}
impl ManagedCOVSubscriptionOptions {
pub fn with_renewal_margin(mut self, margin: Duration) -> Self {
self.renewal_margin = margin;
self
}
pub fn with_event_channel_capacity(mut self, capacity: usize) -> Self {
self.event_channel_capacity = capacity;
self
}
fn validate(&self, lifetime: u32) -> Result<(), Error> {
if !(1..=MAX_COV_CHANNEL_CAPACITY).contains(&self.event_channel_capacity) {
return Err(Error::Encoding(format!(
"invalid managed COV subscription event channel capacity {}; expected 1..={}",
self.event_channel_capacity, MAX_COV_CHANNEL_CAPACITY
)));
}
if self.renewal_margin >= Duration::from_secs(u64::from(lifetime)) {
return Err(Error::Encoding(
"managed COV subscription renewal margin must be shorter than lifetime".into(),
));
}
Ok(())
}
fn renewal_delay(&self, time_remaining: u32) -> Duration {
Duration::from_secs(u64::from(time_remaining)).saturating_sub(self.renewal_margin)
}
}
#[derive(Debug, Clone, PartialEq, Eq)]
#[non_exhaustive]
pub enum ManagedCOVSubscriptionEvent {
NotificationObserved {
time_remaining: u32,
renew_after: Duration,
},
ImpendingExpiry {
time_remaining: u32,
},
Renewed {
requested_lifetime: u32,
renew_after: Duration,
},
RenewalFailed {
error: String,
},
}
pub struct ManagedCOVSubscription {
events_tx: broadcast::Sender<ManagedCOVSubscriptionEvent>,
last_event: Arc<StdMutex<Option<ManagedCOVSubscriptionEvent>>>,
stop_tx: Option<oneshot::Sender<()>>,
task: Option<JoinHandle<()>>,
}
impl ManagedCOVSubscription {
pub fn events(&self) -> broadcast::Receiver<ManagedCOVSubscriptionEvent> {
self.events_tx.subscribe()
}
pub fn last_event(&self) -> Option<ManagedCOVSubscriptionEvent> {
self.last_event.lock().ok().and_then(|event| event.clone())
}
pub async fn stop(mut self) {
if let Some(stop_tx) = self.stop_tx.take() {
let _ = stop_tx.send(());
}
if let Some(task) = self.task.take() {
let _ = task.await;
}
}
pub fn is_finished(&self) -> bool {
match &self.task {
Some(task) => task.is_finished(),
None => true,
}
}
}
impl Drop for ManagedCOVSubscription {
fn drop(&mut self) {
if let Some(stop_tx) = self.stop_tx.take() {
let _ = stop_tx.send(());
}
if let Some(task) = self.task.take() {
task.abort();
}
}
}
#[derive(Clone, Copy)]
enum ManagedRequest {
Object {
subscriber_process_identifier: u32,
monitored_object_identifier: bacnet_types::primitives::ObjectIdentifier,
confirmed: bool,
lifetime: u32,
},
Property(CovPropertySubscription),
}
impl ManagedRequest {
fn lifetime(self) -> u32 {
match self {
Self::Object { lifetime, .. } => lifetime,
Self::Property(subscription) => subscription.lifetime.get(),
}
}
fn property(self) -> Option<PropertyIdentifier> {
match self {
Self::Object { .. } => None,
Self::Property(subscription) => Some(subscription.monitored_property_identifier),
}
}
fn identity(self) -> (u32, bacnet_types::primitives::ObjectIdentifier) {
match self {
Self::Object {
subscriber_process_identifier,
monitored_object_identifier,
..
} => (subscriber_process_identifier, monitored_object_identifier),
Self::Property(subscription) => (
subscription.subscriber_process_identifier,
subscription.monitored_object_identifier,
),
}
}
async fn send<T: TransportPort + 'static>(
self,
client: &BACnetClient<T>,
destination_mac: &[u8],
) -> Result<(), Error> {
match self {
Self::Object {
subscriber_process_identifier,
monitored_object_identifier,
confirmed,
lifetime,
} => {
client
.subscribe_cov(
destination_mac,
subscriber_process_identifier,
monitored_object_identifier,
confirmed,
Some(lifetime),
)
.await
}
Self::Property(subscription) => {
client
.subscribe_cov_property(destination_mac, subscription)
.await
}
}
}
}
impl<T: TransportPort + 'static> BACnetClient<T> {
pub async fn manage_cov_subscription(
self: Arc<Self>,
destination_mac: &[u8],
subscriber_process_identifier: u32,
monitored_object_identifier: bacnet_types::primitives::ObjectIdentifier,
confirmed: bool,
lifetime: u32,
options: ManagedCOVSubscriptionOptions,
) -> Result<ManagedCOVSubscription, Error> {
if lifetime == 0 {
return Err(Error::Encoding(
"managed COV subscriptions require a finite non-zero lifetime".into(),
));
}
let request = ManagedRequest::Object {
subscriber_process_identifier,
monitored_object_identifier,
confirmed,
lifetime,
};
self.manage(destination_mac, request, options).await
}
pub async fn manage_cov_property_subscription(
self: Arc<Self>,
destination_mac: &[u8],
subscription: CovPropertySubscription,
options: ManagedCOVSubscriptionOptions,
) -> Result<ManagedCOVSubscription, Error> {
self.manage(
destination_mac,
ManagedRequest::Property(subscription),
options,
)
.await
}
async fn manage(
self: Arc<Self>,
destination_mac: &[u8],
request: ManagedRequest,
options: ManagedCOVSubscriptionOptions,
) -> Result<ManagedCOVSubscription, Error> {
let lifetime = request.lifetime();
options.validate(lifetime)?;
let (subscriber_process_identifier, monitored_object_identifier) = request.identity();
let destination_mac = MacAddr::from_slice(destination_mac);
let mut notifications = self.cov_notifications();
request.send(&self, &destination_mac).await?;
let (events_tx, _) = broadcast::channel(options.event_channel_capacity);
let events_tx_for_task = events_tx.clone();
let last_event = Arc::new(StdMutex::new(None));
let last_event_for_task = Arc::clone(&last_event);
let (stop_tx, mut stop_rx) = oneshot::channel();
let client = Arc::clone(&self);
let mut renew_at = TokioInstant::now() + options.renewal_delay(lifetime);
let task = tokio::spawn(async move {
loop {
let renew_sleep = sleep_until(renew_at);
tokio::pin!(renew_sleep);
tokio::select! {
_ = &mut stop_rx => break,
_ = &mut renew_sleep => {
let renewal = request.send(&client, &destination_mac);
tokio::pin!(renewal);
tokio::select! {
_ = &mut stop_rx => break,
result = &mut renewal => match result {
Ok(()) => {
let renew_after = options.renewal_delay(lifetime);
publish_managed_cov_event(
&events_tx_for_task,
&last_event_for_task,
ManagedCOVSubscriptionEvent::Renewed {
requested_lifetime: lifetime,
renew_after,
},
);
renew_at = TokioInstant::now() + renew_after;
}
Err(error) => {
publish_managed_cov_event(
&events_tx_for_task,
&last_event_for_task,
ManagedCOVSubscriptionEvent::RenewalFailed {
error: error.to_string(),
},
);
break;
}
},
}
}
received = notifications.recv() => {
let received = match received {
Ok(received) => received,
Err(broadcast::error::RecvError::Lagged(_)) => continue,
Err(broadcast::error::RecvError::Closed) => break,
};
if !matches_managed_cov_notification(
&received,
&destination_mac,
subscriber_process_identifier,
monitored_object_identifier,
request.property(),
) {
continue;
}
let time_remaining = received.notification.time_remaining;
let renew_after = options.renewal_delay(time_remaining);
publish_managed_cov_event(
&events_tx_for_task,
&last_event_for_task,
ManagedCOVSubscriptionEvent::NotificationObserved {
time_remaining,
renew_after,
},
);
let observed_renew_at = TokioInstant::now() + renew_after;
if renew_after.is_zero() {
publish_managed_cov_event(
&events_tx_for_task,
&last_event_for_task,
ManagedCOVSubscriptionEvent::ImpendingExpiry {
time_remaining,
},
);
renew_at = observed_renew_at;
} else if observed_renew_at < renew_at {
renew_at = observed_renew_at;
}
}
}
}
});
Ok(ManagedCOVSubscription {
events_tx,
last_event,
stop_tx: Some(stop_tx),
task: Some(task),
})
}
}
fn publish_managed_cov_event(
events_tx: &broadcast::Sender<ManagedCOVSubscriptionEvent>,
last_event: &StdMutex<Option<ManagedCOVSubscriptionEvent>>,
event: ManagedCOVSubscriptionEvent,
) {
if let Ok(mut last_event) = last_event.lock() {
*last_event = Some(event.clone());
}
let _ = events_tx.send(event);
}
fn matches_managed_cov_notification(
received: &ReceivedCOVNotification,
destination_mac: &[u8],
subscriber_process_identifier: u32,
monitored_object_identifier: bacnet_types::primitives::ObjectIdentifier,
property: Option<PropertyIdentifier>,
) -> bool {
received.notification.subscriber_process_identifier == subscriber_process_identifier
&& received.notification.monitored_object_identifier == monitored_object_identifier
&& received.source_network.is_none()
&& received.source_address.is_none()
&& &received.source_mac[..] == destination_mac
&& property.is_none_or(|property| {
received
.notification
.list_of_values
.iter()
.any(|value| value.property_identifier == property)
})
}