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::{Instant as TokioInstant, sleep_until};
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    /// A semaphore each renewal takes one permit of before it is sent and
25    /// holds until its answer, its failure or [`ManagedCOVSubscription::stop`]
26    /// (#1791). An application that limits its own requests to a device
27    /// (one outstanding at a time, say) passes that limit here, so renewals
28    /// keep to it too. `None` (the default) sends renewals ungated.
29    ///
30    /// The first request is not gated: it is sent from the caller's
31    /// context, which may already hold a permit for it. A closed semaphore
32    /// stops gating.
33    ///
34    /// A gated renewal goes only once the permit comes, so whatever holds
35    /// the gate delays it, by up to that request's whole time with retries
36    /// on a device that stops answering. Size
37    /// [`renewal_margin`](Self::renewal_margin) to cover that wait as well as
38    /// the renewal's own request, or the subscription can lapse first.
39    pub renewal_gate: Option<Arc<tokio::sync::Semaphore>>,
40}
41
42impl Default for ManagedCOVSubscriptionOptions {
43    fn default() -> Self {
44        Self {
45            renewal_margin: DEFAULT_COV_RENEWAL_MARGIN,
46            event_channel_capacity: DEFAULT_COV_RENEWAL_EVENT_CAPACITY,
47            renewal_gate: None,
48        }
49    }
50}
51
52impl ManagedCOVSubscriptionOptions {
53    /// Set the renewal margin.
54    pub fn with_renewal_margin(mut self, margin: Duration) -> Self {
55        self.renewal_margin = margin;
56        self
57    }
58
59    /// Set the renewal event broadcast channel capacity.
60    pub fn with_event_channel_capacity(mut self, capacity: usize) -> Self {
61        self.event_channel_capacity = capacity;
62        self
63    }
64
65    /// Gate every renewal on a permit of `gate` (see
66    /// [`renewal_gate`](Self::renewal_gate)).
67    pub fn with_renewal_gate(mut self, gate: Arc<tokio::sync::Semaphore>) -> Self {
68        self.renewal_gate = Some(gate);
69        self
70    }
71
72    fn validate(&self, lifetime: u32) -> Result<(), Error> {
73        if !(1..=MAX_COV_CHANNEL_CAPACITY).contains(&self.event_channel_capacity) {
74            return Err(Error::Encoding(format!(
75                "invalid managed COV subscription event channel capacity {}; expected 1..={}",
76                self.event_channel_capacity, MAX_COV_CHANNEL_CAPACITY
77            )));
78        }
79        if self.renewal_margin >= Duration::from_secs(u64::from(lifetime)) {
80            return Err(Error::Encoding(
81                "managed COV subscription renewal margin must be shorter than lifetime".into(),
82            ));
83        }
84        Ok(())
85    }
86
87    fn renewal_delay(&self, time_remaining: u32) -> Duration {
88        Duration::from_secs(u64::from(time_remaining)).saturating_sub(self.renewal_margin)
89    }
90}
91
92/// Events emitted by a managed finite COV subscription.
93#[derive(Debug, Clone, PartialEq, Eq)]
94#[non_exhaustive]
95pub enum ManagedCOVSubscriptionEvent {
96    /// A matching COV notification supplied a new lifetime observation.
97    NotificationObserved {
98        /// The `time_remaining` field from the inbound notification.
99        time_remaining: u32,
100        /// Delay until the next renewal after applying the configured margin.
101        renew_after: Duration,
102    },
103    /// The observed lifetime is inside the configured renewal margin.
104    ImpendingExpiry {
105        /// The `time_remaining` field from the inbound notification.
106        time_remaining: u32,
107    },
108    /// The helper successfully renewed the subscription.
109    Renewed {
110        /// Lifetime requested on the renewed SubscribeCOV request.
111        requested_lifetime: u32,
112        /// Delay until the next scheduled renewal after applying the margin.
113        renew_after: Duration,
114    },
115    /// A renewal SubscribeCOV request failed and the helper stopped.
116    RenewalFailed {
117        /// Display form of the protocol, reject, abort, transport, or timeout error.
118        error: String,
119    },
120}
121
122/// Handle for a managed finite COV subscription renewal task.
123pub struct ManagedCOVSubscription {
124    events_tx: broadcast::Sender<ManagedCOVSubscriptionEvent>,
125    last_event: Arc<StdMutex<Option<ManagedCOVSubscriptionEvent>>>,
126    stop_tx: Option<oneshot::Sender<()>>,
127    task: Option<JoinHandle<()>>,
128}
129
130impl ManagedCOVSubscription {
131    /// Subscribe to lifetime observations and renewal outcomes.
132    pub fn events(&self) -> broadcast::Receiver<ManagedCOVSubscriptionEvent> {
133        self.events_tx.subscribe()
134    }
135
136    /// Return the latest observed renewal event, if any.
137    pub fn last_event(&self) -> Option<ManagedCOVSubscriptionEvent> {
138        self.last_event.lock().ok().and_then(|event| event.clone())
139    }
140
141    /// Stop renewing and wait for the renewal task to exit.
142    pub async fn stop(mut self) {
143        if let Some(stop_tx) = self.stop_tx.take() {
144            let _ = stop_tx.send(());
145        }
146        if let Some(task) = self.task.take() {
147            let _ = task.await;
148        }
149    }
150
151    /// Return whether the renewal task has already finished.
152    pub fn is_finished(&self) -> bool {
153        match &self.task {
154            Some(task) => task.is_finished(),
155            None => true,
156        }
157    }
158}
159
160impl Drop for ManagedCOVSubscription {
161    fn drop(&mut self) {
162        if let Some(stop_tx) = self.stop_tx.take() {
163            let _ = stop_tx.send(());
164        }
165        if let Some(task) = self.task.take() {
166            task.abort();
167        }
168    }
169}
170
171/// The request a managed subscription sends, first and on every renewal.
172#[derive(Clone, Copy)]
173enum ManagedRequest {
174    /// SubscribeCOV on the whole object.
175    Object {
176        subscriber_process_identifier: u32,
177        monitored_object_identifier: bacnet_types::primitives::ObjectIdentifier,
178        confirmed: bool,
179        lifetime: u32,
180    },
181    /// SubscribeCOVProperty on one property.
182    Property(CovPropertySubscription),
183}
184
185impl ManagedRequest {
186    fn lifetime(self) -> u32 {
187        match self {
188            Self::Object { lifetime, .. } => lifetime,
189            Self::Property(subscription) => subscription.lifetime.get(),
190        }
191    }
192
193    /// The property a single-property subscription monitors.
194    fn property(self) -> Option<PropertyIdentifier> {
195        match self {
196            Self::Object { .. } => None,
197            Self::Property(subscription) => Some(subscription.monitored_property_identifier),
198        }
199    }
200
201    fn identity(self) -> (u32, bacnet_types::primitives::ObjectIdentifier) {
202        match self {
203            Self::Object {
204                subscriber_process_identifier,
205                monitored_object_identifier,
206                ..
207            } => (subscriber_process_identifier, monitored_object_identifier),
208            Self::Property(subscription) => (
209                subscription.subscriber_process_identifier,
210                subscription.monitored_object_identifier,
211            ),
212        }
213    }
214
215    async fn send<T: TransportPort + 'static>(
216        self,
217        client: &BACnetClient<T>,
218        target: &CovTarget,
219    ) -> Result<(), Error> {
220        let target = target.confirmed();
221        match self {
222            Self::Object {
223                subscriber_process_identifier,
224                monitored_object_identifier,
225                confirmed,
226                lifetime,
227            } => {
228                let request = BACnetClient::<T>::subscribe_cov_request(
229                    subscriber_process_identifier,
230                    monitored_object_identifier,
231                    Some(confirmed),
232                    Some(lifetime),
233                );
234                client.send_subscribe_cov_request(target, request).await
235            }
236            Self::Property(subscription) => {
237                client
238                    .send_subscribe_cov_property_request(target, subscription.into_request())
239                    .await
240            }
241        }
242    }
243}
244
245impl<T: TransportPort + 'static> BACnetClient<T> {
246    /// Start a managed finite SubscribeCOV renewal task for the device at
247    /// `target`, on this network or behind a router.
248    ///
249    /// The helper sends the initial SubscribeCOV request, then renews the same
250    /// subscription before expiry. It also listens to [`Self::cov_notifications`]
251    /// and moves the renewal earlier when matching notifications report a lower
252    /// `time_remaining` than the requested lifetime-based schedule. A
253    /// notification matches when it comes from `target`'s address (see
254    /// [`CovTarget`]) and names this subscription's process and object.
255    /// The helper only renews; after [`ManagedCOVSubscription::stop`], cancel
256    /// with [`Self::unsubscribe_cov_target`].
257    ///
258    /// This method takes `Arc<Self>` so the renewal task can keep the client alive.
259    /// Call it as `Arc::clone(&client).manage_cov_subscription(...)`.
260    pub async fn manage_cov_subscription(
261        self: Arc<Self>,
262        target: CovTarget,
263        subscriber_process_identifier: u32,
264        monitored_object_identifier: bacnet_types::primitives::ObjectIdentifier,
265        confirmed: bool,
266        lifetime: u32,
267        options: ManagedCOVSubscriptionOptions,
268    ) -> Result<ManagedCOVSubscription, Error> {
269        if lifetime == 0 {
270            return Err(Error::Encoding(
271                "managed COV subscriptions require a finite non-zero lifetime".into(),
272            ));
273        }
274        let request = ManagedRequest::Object {
275            subscriber_process_identifier,
276            monitored_object_identifier,
277            confirmed,
278            lifetime,
279        };
280        self.manage(target, request, options).await
281    }
282
283    /// Start a managed finite SubscribeCOVProperty renewal task for the
284    /// device at `target`: [`Self::manage_cov_subscription`] for one property.
285    ///
286    /// Every renewal repeats `subscription` as given, its lifetime included.
287    /// The helper only renews; after [`ManagedCOVSubscription::stop`], cancel
288    /// with [`Self::unsubscribe_cov_property_target`].
289    pub async fn manage_cov_property_subscription(
290        self: Arc<Self>,
291        target: CovTarget,
292        subscription: CovPropertySubscription,
293        options: ManagedCOVSubscriptionOptions,
294    ) -> Result<ManagedCOVSubscription, Error> {
295        self.manage(target, ManagedRequest::Property(subscription), options)
296            .await
297    }
298
299    async fn manage(
300        self: Arc<Self>,
301        target: CovTarget,
302        request: ManagedRequest,
303        options: ManagedCOVSubscriptionOptions,
304    ) -> Result<ManagedCOVSubscription, Error> {
305        let lifetime = request.lifetime();
306        options.validate(lifetime)?;
307        let (subscriber_process_identifier, monitored_object_identifier) = request.identity();
308
309        let mut notifications = self.cov_notifications();
310
311        request.send(&self, &target).await?;
312
313        let (events_tx, _) = broadcast::channel(options.event_channel_capacity);
314        let events_tx_for_task = events_tx.clone();
315        let last_event = Arc::new(StdMutex::new(None));
316        let last_event_for_task = Arc::clone(&last_event);
317        let (stop_tx, mut stop_rx) = oneshot::channel();
318        let client = Arc::clone(&self);
319        let mut renew_at = TokioInstant::now() + options.renewal_delay(lifetime);
320
321        let task = tokio::spawn(async move {
322            loop {
323                let renew_sleep = sleep_until(renew_at);
324                tokio::pin!(renew_sleep);
325                tokio::select! {
326                    _ = &mut stop_rx => break,
327                    _ = &mut renew_sleep => {
328                        // The permit lives as long as this future: until the
329                        // answer, or until a stop drops it mid-wait or
330                        // mid-request.
331                        let renewal = async {
332                            let _permit = match &options.renewal_gate {
333                                Some(gate) => gate.acquire().await.ok(),
334                                None => None,
335                            };
336                            request.send(&client, &target).await
337                        };
338                        tokio::pin!(renewal);
339                        tokio::select! {
340                            _ = &mut stop_rx => break,
341                            result = &mut renewal => match result {
342                                Ok(()) => {
343                                    let renew_after = options.renewal_delay(lifetime);
344                                    publish_managed_cov_event(
345                                        &events_tx_for_task,
346                                        &last_event_for_task,
347                                        ManagedCOVSubscriptionEvent::Renewed {
348                                            requested_lifetime: lifetime,
349                                            renew_after,
350                                        },
351                                    );
352                                    renew_at = TokioInstant::now() + renew_after;
353                                }
354                                Err(error) => {
355                                    publish_managed_cov_event(
356                                        &events_tx_for_task,
357                                        &last_event_for_task,
358                                        ManagedCOVSubscriptionEvent::RenewalFailed {
359                                            error: error.to_string(),
360                                        },
361                                    );
362                                    break;
363                                }
364                            },
365                        }
366                    }
367                    received = notifications.recv() => {
368                        let received = match received {
369                            Ok(received) => received,
370                            Err(broadcast::error::RecvError::Lagged(_)) => continue,
371                            Err(broadcast::error::RecvError::Closed) => break,
372                        };
373                        if !matches_managed_cov_notification(
374                            &received,
375                            &target,
376                            client.network.local_network_number().get(),
377                            subscriber_process_identifier,
378                            monitored_object_identifier,
379                            request.property(),
380                        ) {
381                            continue;
382                        }
383
384                        let time_remaining = received.notification.time_remaining;
385                        let renew_after = options.renewal_delay(time_remaining);
386                        publish_managed_cov_event(
387                            &events_tx_for_task,
388                            &last_event_for_task,
389                            ManagedCOVSubscriptionEvent::NotificationObserved {
390                                time_remaining,
391                                renew_after,
392                            },
393                        );
394
395                        let observed_renew_at = TokioInstant::now() + renew_after;
396                        if renew_after.is_zero() {
397                            publish_managed_cov_event(
398                                &events_tx_for_task,
399                                &last_event_for_task,
400                                ManagedCOVSubscriptionEvent::ImpendingExpiry {
401                                    time_remaining,
402                                },
403                            );
404                            renew_at = observed_renew_at;
405                        } else if observed_renew_at < renew_at {
406                            renew_at = observed_renew_at;
407                        }
408                    }
409                }
410            }
411        });
412
413        Ok(ManagedCOVSubscription {
414            events_tx,
415            last_event,
416            stop_tx: Some(stop_tx),
417            task: Some(task),
418        })
419    }
420}
421
422fn publish_managed_cov_event(
423    events_tx: &broadcast::Sender<ManagedCOVSubscriptionEvent>,
424    last_event: &StdMutex<Option<ManagedCOVSubscriptionEvent>>,
425    event: ManagedCOVSubscriptionEvent,
426) {
427    if let Ok(mut last_event) = last_event.lock() {
428        *last_event = Some(event.clone());
429    }
430    let _ = events_tx.send(event);
431}
432
433/// True when `received` answers this subscription: it comes from `target`
434/// and names the subscription's process and object. A single-property
435/// subscription also needs its property among the reported values, so two
436/// subscriptions to one object under one process identifier don't move
437/// each other's renewals.
438fn matches_managed_cov_notification(
439    received: &ReceivedCOVNotification,
440    target: &CovTarget,
441    local_network: Option<u16>,
442    subscriber_process_identifier: u32,
443    monitored_object_identifier: bacnet_types::primitives::ObjectIdentifier,
444    property: Option<PropertyIdentifier>,
445) -> bool {
446    received.notification.subscriber_process_identifier == subscriber_process_identifier
447        && received.notification.monitored_object_identifier == monitored_object_identifier
448        && target.sent(received, local_network)
449        && property.is_none_or(|property| {
450            received
451                .notification
452                .list_of_values
453                .iter()
454                .any(|value| value.property_identifier == property)
455        })
456}