Skip to main content

bacnet_client/client/
cov_renewal.rs

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/// Options for a managed finite COV subscription.
11#[derive(Debug, Clone)]
12#[non_exhaustive]
13pub struct ManagedCOVSubscriptionOptions {
14    /// How long before the known remaining lifetime the helper should renew.
15    ///
16    /// The helper schedules renewal at `time_remaining - renewal_margin`, using
17    /// the requested lifetime until inbound notifications provide a fresher
18    /// `time_remaining` value.
19    pub renewal_margin: Duration,
20    /// Capacity of the renewal event broadcast channel.
21    ///
22    /// Accepted range is `1..=MAX_COV_CHANNEL_CAPACITY`.
23    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    /// Set the renewal margin.
37    pub fn with_renewal_margin(mut self, margin: Duration) -> Self {
38        self.renewal_margin = margin;
39        self
40    }
41
42    /// Set the renewal event broadcast channel capacity.
43    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/// Events emitted by a managed finite COV subscription.
69#[derive(Debug, Clone, PartialEq, Eq)]
70#[non_exhaustive]
71pub enum ManagedCOVSubscriptionEvent {
72    /// A matching COV notification supplied a new lifetime observation.
73    NotificationObserved {
74        /// The `time_remaining` field from the inbound notification.
75        time_remaining: u32,
76        /// Delay until the next renewal after applying the configured margin.
77        renew_after: Duration,
78    },
79    /// The observed lifetime is inside the configured renewal margin.
80    ImpendingExpiry {
81        /// The `time_remaining` field from the inbound notification.
82        time_remaining: u32,
83    },
84    /// The helper successfully renewed the subscription.
85    Renewed {
86        /// Lifetime requested on the renewed SubscribeCOV request.
87        requested_lifetime: u32,
88        /// Delay until the next scheduled renewal after applying the margin.
89        renew_after: Duration,
90    },
91    /// A renewal SubscribeCOV request failed and the helper stopped.
92    RenewalFailed {
93        /// Display form of the protocol, reject, abort, transport, or timeout error.
94        error: String,
95    },
96}
97
98/// Handle for a managed finite COV subscription renewal task.
99pub 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    /// Subscribe to lifetime observations and renewal outcomes.
108    pub fn events(&self) -> broadcast::Receiver<ManagedCOVSubscriptionEvent> {
109        self.events_tx.subscribe()
110    }
111
112    /// Return the latest observed renewal event, if any.
113    pub fn last_event(&self) -> Option<ManagedCOVSubscriptionEvent> {
114        self.last_event.lock().ok().and_then(|event| event.clone())
115    }
116
117    /// Stop renewing and wait for the renewal task to exit.
118    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    /// Return whether the renewal task has already finished.
128    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/// The request a managed subscription sends, first and on every renewal.
148#[derive(Clone, Copy)]
149enum ManagedRequest {
150    /// SubscribeCOV on the whole object.
151    Object {
152        subscriber_process_identifier: u32,
153        monitored_object_identifier: bacnet_types::primitives::ObjectIdentifier,
154        confirmed: bool,
155        lifetime: u32,
156    },
157    /// SubscribeCOVProperty on one property.
158    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    /// The property a single-property subscription monitors.
170    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    /// Start a managed finite SubscribeCOV renewal task for a directly reachable device.
224    ///
225    /// The helper sends the initial SubscribeCOV request, then renews the same
226    /// subscription before expiry. It also listens to [`Self::cov_notifications`]
227    /// and moves the renewal earlier when matching notifications report a lower
228    /// `time_remaining` than the requested lifetime-based schedule.
229    ///
230    /// This method takes `Arc<Self>` so the renewal task can keep the client alive.
231    /// Call it as `Arc::clone(&client).manage_cov_subscription(...)`.
232    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    /// Start a managed finite SubscribeCOVProperty renewal task for a directly
256    /// reachable device: [`Self::manage_cov_subscription`] for one property.
257    ///
258    /// Every renewal repeats `subscription` as given, its lifetime included.
259    /// The helper only renews; after [`ManagedCOVSubscription::stop`], cancel
260    /// with [`Self::unsubscribe_cov_property`].
261    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
400/// True when `received` answers this subscription. A single-property
401/// subscription also needs its property among the reported values, so two
402/// subscriptions to one object under one process identifier don't move
403/// each other's renewals.
404fn 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}