1use super::*;
2use bacnet_types::enums::PropertyIdentifier;
3use std::sync::Mutex as StdMutex;
4use tokio::sync::oneshot;
5use tokio::time::{sleep_until, Instant as TokioInstant};
6
7const DEFAULT_COV_RENEWAL_MARGIN: Duration = Duration::from_secs(30);
8const DEFAULT_COV_RENEWAL_EVENT_CAPACITY: usize = 16;
9
10#[derive(Debug, Clone)]
12#[non_exhaustive]
13pub struct ManagedCOVSubscriptionOptions {
14 pub renewal_margin: Duration,
20 pub event_channel_capacity: usize,
24}
25
26impl Default for ManagedCOVSubscriptionOptions {
27 fn default() -> Self {
28 Self {
29 renewal_margin: DEFAULT_COV_RENEWAL_MARGIN,
30 event_channel_capacity: DEFAULT_COV_RENEWAL_EVENT_CAPACITY,
31 }
32 }
33}
34
35impl ManagedCOVSubscriptionOptions {
36 pub fn with_renewal_margin(mut self, margin: Duration) -> Self {
38 self.renewal_margin = margin;
39 self
40 }
41
42 pub fn with_event_channel_capacity(mut self, capacity: usize) -> Self {
44 self.event_channel_capacity = capacity;
45 self
46 }
47
48 fn validate(&self, lifetime: u32) -> Result<(), Error> {
49 if !(1..=MAX_COV_CHANNEL_CAPACITY).contains(&self.event_channel_capacity) {
50 return Err(Error::Encoding(format!(
51 "invalid managed COV subscription event channel capacity {}; expected 1..={}",
52 self.event_channel_capacity, MAX_COV_CHANNEL_CAPACITY
53 )));
54 }
55 if self.renewal_margin >= Duration::from_secs(u64::from(lifetime)) {
56 return Err(Error::Encoding(
57 "managed COV subscription renewal margin must be shorter than lifetime".into(),
58 ));
59 }
60 Ok(())
61 }
62
63 fn renewal_delay(&self, time_remaining: u32) -> Duration {
64 Duration::from_secs(u64::from(time_remaining)).saturating_sub(self.renewal_margin)
65 }
66}
67
68#[derive(Debug, Clone, PartialEq, Eq)]
70#[non_exhaustive]
71pub enum ManagedCOVSubscriptionEvent {
72 NotificationObserved {
74 time_remaining: u32,
76 renew_after: Duration,
78 },
79 ImpendingExpiry {
81 time_remaining: u32,
83 },
84 Renewed {
86 requested_lifetime: u32,
88 renew_after: Duration,
90 },
91 RenewalFailed {
93 error: String,
95 },
96}
97
98pub struct ManagedCOVSubscription {
100 events_tx: broadcast::Sender<ManagedCOVSubscriptionEvent>,
101 last_event: Arc<StdMutex<Option<ManagedCOVSubscriptionEvent>>>,
102 stop_tx: Option<oneshot::Sender<()>>,
103 task: Option<JoinHandle<()>>,
104}
105
106impl ManagedCOVSubscription {
107 pub fn events(&self) -> broadcast::Receiver<ManagedCOVSubscriptionEvent> {
109 self.events_tx.subscribe()
110 }
111
112 pub fn last_event(&self) -> Option<ManagedCOVSubscriptionEvent> {
114 self.last_event.lock().ok().and_then(|event| event.clone())
115 }
116
117 pub async fn stop(mut self) {
119 if let Some(stop_tx) = self.stop_tx.take() {
120 let _ = stop_tx.send(());
121 }
122 if let Some(task) = self.task.take() {
123 let _ = task.await;
124 }
125 }
126
127 pub fn is_finished(&self) -> bool {
129 match &self.task {
130 Some(task) => task.is_finished(),
131 None => true,
132 }
133 }
134}
135
136impl Drop for ManagedCOVSubscription {
137 fn drop(&mut self) {
138 if let Some(stop_tx) = self.stop_tx.take() {
139 let _ = stop_tx.send(());
140 }
141 if let Some(task) = self.task.take() {
142 task.abort();
143 }
144 }
145}
146
147#[derive(Clone, Copy)]
149enum ManagedRequest {
150 Object {
152 subscriber_process_identifier: u32,
153 monitored_object_identifier: bacnet_types::primitives::ObjectIdentifier,
154 confirmed: bool,
155 lifetime: u32,
156 },
157 Property(CovPropertySubscription),
159}
160
161impl ManagedRequest {
162 fn lifetime(self) -> u32 {
163 match self {
164 Self::Object { lifetime, .. } => lifetime,
165 Self::Property(subscription) => subscription.lifetime.get(),
166 }
167 }
168
169 fn property(self) -> Option<PropertyIdentifier> {
171 match self {
172 Self::Object { .. } => None,
173 Self::Property(subscription) => Some(subscription.monitored_property_identifier),
174 }
175 }
176
177 fn identity(self) -> (u32, bacnet_types::primitives::ObjectIdentifier) {
178 match self {
179 Self::Object {
180 subscriber_process_identifier,
181 monitored_object_identifier,
182 ..
183 } => (subscriber_process_identifier, monitored_object_identifier),
184 Self::Property(subscription) => (
185 subscription.subscriber_process_identifier,
186 subscription.monitored_object_identifier,
187 ),
188 }
189 }
190
191 async fn send<T: TransportPort + 'static>(
192 self,
193 client: &BACnetClient<T>,
194 destination_mac: &[u8],
195 ) -> Result<(), Error> {
196 match self {
197 Self::Object {
198 subscriber_process_identifier,
199 monitored_object_identifier,
200 confirmed,
201 lifetime,
202 } => {
203 client
204 .subscribe_cov(
205 destination_mac,
206 subscriber_process_identifier,
207 monitored_object_identifier,
208 confirmed,
209 Some(lifetime),
210 )
211 .await
212 }
213 Self::Property(subscription) => {
214 client
215 .subscribe_cov_property(destination_mac, subscription)
216 .await
217 }
218 }
219 }
220}
221
222impl<T: TransportPort + 'static> BACnetClient<T> {
223 pub async fn manage_cov_subscription(
233 self: Arc<Self>,
234 destination_mac: &[u8],
235 subscriber_process_identifier: u32,
236 monitored_object_identifier: bacnet_types::primitives::ObjectIdentifier,
237 confirmed: bool,
238 lifetime: u32,
239 options: ManagedCOVSubscriptionOptions,
240 ) -> Result<ManagedCOVSubscription, Error> {
241 if lifetime == 0 {
242 return Err(Error::Encoding(
243 "managed COV subscriptions require a finite non-zero lifetime".into(),
244 ));
245 }
246 let request = ManagedRequest::Object {
247 subscriber_process_identifier,
248 monitored_object_identifier,
249 confirmed,
250 lifetime,
251 };
252 self.manage(destination_mac, request, options).await
253 }
254
255 pub async fn manage_cov_property_subscription(
262 self: Arc<Self>,
263 destination_mac: &[u8],
264 subscription: CovPropertySubscription,
265 options: ManagedCOVSubscriptionOptions,
266 ) -> Result<ManagedCOVSubscription, Error> {
267 self.manage(
268 destination_mac,
269 ManagedRequest::Property(subscription),
270 options,
271 )
272 .await
273 }
274
275 async fn manage(
276 self: Arc<Self>,
277 destination_mac: &[u8],
278 request: ManagedRequest,
279 options: ManagedCOVSubscriptionOptions,
280 ) -> Result<ManagedCOVSubscription, Error> {
281 let lifetime = request.lifetime();
282 options.validate(lifetime)?;
283 let (subscriber_process_identifier, monitored_object_identifier) = request.identity();
284
285 let destination_mac = MacAddr::from_slice(destination_mac);
286 let mut notifications = self.cov_notifications();
287
288 request.send(&self, &destination_mac).await?;
289
290 let (events_tx, _) = broadcast::channel(options.event_channel_capacity);
291 let events_tx_for_task = events_tx.clone();
292 let last_event = Arc::new(StdMutex::new(None));
293 let last_event_for_task = Arc::clone(&last_event);
294 let (stop_tx, mut stop_rx) = oneshot::channel();
295 let client = Arc::clone(&self);
296 let mut renew_at = TokioInstant::now() + options.renewal_delay(lifetime);
297
298 let task = tokio::spawn(async move {
299 loop {
300 let renew_sleep = sleep_until(renew_at);
301 tokio::pin!(renew_sleep);
302 tokio::select! {
303 _ = &mut stop_rx => break,
304 _ = &mut renew_sleep => {
305 let renewal = request.send(&client, &destination_mac);
306 tokio::pin!(renewal);
307 tokio::select! {
308 _ = &mut stop_rx => break,
309 result = &mut renewal => match result {
310 Ok(()) => {
311 let renew_after = options.renewal_delay(lifetime);
312 publish_managed_cov_event(
313 &events_tx_for_task,
314 &last_event_for_task,
315 ManagedCOVSubscriptionEvent::Renewed {
316 requested_lifetime: lifetime,
317 renew_after,
318 },
319 );
320 renew_at = TokioInstant::now() + renew_after;
321 }
322 Err(error) => {
323 publish_managed_cov_event(
324 &events_tx_for_task,
325 &last_event_for_task,
326 ManagedCOVSubscriptionEvent::RenewalFailed {
327 error: error.to_string(),
328 },
329 );
330 break;
331 }
332 },
333 }
334 }
335 received = notifications.recv() => {
336 let received = match received {
337 Ok(received) => received,
338 Err(broadcast::error::RecvError::Lagged(_)) => continue,
339 Err(broadcast::error::RecvError::Closed) => break,
340 };
341 if !matches_managed_cov_notification(
342 &received,
343 &destination_mac,
344 subscriber_process_identifier,
345 monitored_object_identifier,
346 request.property(),
347 ) {
348 continue;
349 }
350
351 let time_remaining = received.notification.time_remaining;
352 let renew_after = options.renewal_delay(time_remaining);
353 publish_managed_cov_event(
354 &events_tx_for_task,
355 &last_event_for_task,
356 ManagedCOVSubscriptionEvent::NotificationObserved {
357 time_remaining,
358 renew_after,
359 },
360 );
361
362 let observed_renew_at = TokioInstant::now() + renew_after;
363 if renew_after.is_zero() {
364 publish_managed_cov_event(
365 &events_tx_for_task,
366 &last_event_for_task,
367 ManagedCOVSubscriptionEvent::ImpendingExpiry {
368 time_remaining,
369 },
370 );
371 renew_at = observed_renew_at;
372 } else if observed_renew_at < renew_at {
373 renew_at = observed_renew_at;
374 }
375 }
376 }
377 }
378 });
379
380 Ok(ManagedCOVSubscription {
381 events_tx,
382 last_event,
383 stop_tx: Some(stop_tx),
384 task: Some(task),
385 })
386 }
387}
388
389fn publish_managed_cov_event(
390 events_tx: &broadcast::Sender<ManagedCOVSubscriptionEvent>,
391 last_event: &StdMutex<Option<ManagedCOVSubscriptionEvent>>,
392 event: ManagedCOVSubscriptionEvent,
393) {
394 if let Ok(mut last_event) = last_event.lock() {
395 *last_event = Some(event.clone());
396 }
397 let _ = events_tx.send(event);
398}
399
400fn matches_managed_cov_notification(
405 received: &ReceivedCOVNotification,
406 destination_mac: &[u8],
407 subscriber_process_identifier: u32,
408 monitored_object_identifier: bacnet_types::primitives::ObjectIdentifier,
409 property: Option<PropertyIdentifier>,
410) -> bool {
411 received.notification.subscriber_process_identifier == subscriber_process_identifier
412 && received.notification.monitored_object_identifier == monitored_object_identifier
413 && received.source_network.is_none()
414 && received.source_address.is_none()
415 && &received.source_mac[..] == destination_mac
416 && property.is_none_or(|property| {
417 received
418 .notification
419 .list_of_values
420 .iter()
421 .any(|value| value.property_identifier == property)
422 })
423}