Skip to main content

bacnet_objects/
subscribed_recipients.rs

1//! A Notification Forwarder's Subscribed_Recipients (Clause 12.51.9, #1049):
2//! a bounded BACnetLIST of BACnetEventNotificationSubscription whose entries
3//! lapse when their time runs out.
4//!
5//! The bundled [`NotificationForwarderObject`] keeps its list in a
6//! [`SubscribedRecipients`]. An application that implements its own type-51
7//! object can do the same, routing to the store the property's reads and
8//! writes ([`read`](SubscribedRecipients::read) and
9//! [`write`](SubscribedRecipients::write)) and the object's monotonic clock
10//! hooks ([`bind_monotonic_clock`](SubscribedRecipients::bind_monotonic_clock),
11//! [`advance_to`](SubscribedRecipients::advance_to) and
12//! [`next_deadline`](SubscribedRecipients::next_deadline)). The server then
13//! serves the list to ReadProperty, ReadPropertyMultiple and ReadRange, and
14//! its AddListElement and RemoveListElement handlers edit it the way the
15//! clause asks: an entry is named by its recipient and process identifier, and
16//! adding one already present renews it with the other members given.
17//!
18//! [`NotificationForwarderObject`]: crate::notification_forwarder::NotificationForwarderObject
19//!
20//! # Lifetimes
21//!
22//! Time Remaining travels in minutes. The store keeps each entry's deadline on
23//! the database's monotonic clock, the one the server's operation task uses
24//! for the Access Door's pulse relock, and serves the whole minutes left,
25//! rounded up, so a live entry never reads as zero. An entry leaves the list
26//! once its deadline passes: reads and writes skip it from then on, and the
27//! server's operation task drops it at its deadline (through
28//! `advance_monotonic_time_internal`). With no clock bound the store counts
29//! the time [`advance_by`](SubscribedRecipients::advance_by) gives it, and
30//! binding a clock later keeps the time each entry has left.
31//!
32//! Clause 12.51.9 asks for the list to survive a restart. The store itself
33//! lives in memory; the forwarder object saves it through a
34//! [`NotificationForwarderPersistence`] and restores it with
35//! [`write`](SubscribedRecipients::write).
36//!
37//! [`NotificationForwarderPersistence`]: crate::notification_forwarder::NotificationForwarderPersistence
38
39use std::fmt;
40use std::sync::Arc;
41use std::time::Duration;
42
43use bacnet_encoding::constructed::{
44    decode_event_notification_subscription, encode_event_notification_subscription_list,
45};
46use bacnet_types::constructed::{BACnetEventNotificationSubscription, BACnetRecipient};
47use bacnet_types::enums::{ErrorClass, ErrorCode};
48use bacnet_types::error::Error;
49use bacnet_types::primitives::PropertyValue;
50use bytes::BytesMut;
51
52use crate::common;
53use crate::traits::MonotonicClock;
54
55/// The most entries a [`SubscribedRecipients`] holds.
56///
57/// An entry naming a device encodes in at most 17 octets, and one naming an
58/// address with the longest MAC this stack carries
59/// ([`BACnetAddress::MAX_MAC_LEN`], 18 octets) in at most 37. A full list
60/// therefore takes at most 1,184 octets, and a ReadProperty of it fits B/IP's
61/// 1476-octet APDU unsegmented. It matches the Notification Class
62/// Recipient_List cap ([`MAX_RECIPIENT_LIST_DESTINATIONS`]).
63///
64/// A write that would leave more entries fails with RESOURCES /
65/// NO_SPACE_TO_WRITE_PROPERTY naming the first entry past the cap, which
66/// AddListElement reports as NO_SPACE_TO_ADD_LIST_ELEMENT at the request
67/// element that brought it (Clause 15.1.1.3). A renewal never counts against
68/// the cap.
69///
70/// [`BACnetAddress::MAX_MAC_LEN`]: bacnet_types::constructed::BACnetAddress::MAX_MAC_LEN
71/// [`MAX_RECIPIENT_LIST_DESTINATIONS`]: crate::notification_class::MAX_RECIPIENT_LIST_DESTINATIONS
72pub const MAX_SUBSCRIBED_RECIPIENTS: usize = 32;
73
74/// The longest Time Remaining, in minutes, the store accepts: 1,440 (a day).
75///
76/// Clause 12.51.9 requires a forwarder to take 1 through 1,440 and leaves
77/// longer subscriptions to the implementation. This one refuses them, and
78/// refuses 0, which no entry may hold, with PROPERTY / VALUE_OUT_OF_RANGE
79/// naming the entry.
80pub const MAX_SUBSCRIPTION_MINUTES: u32 = 1440;
81
82const NANOS_PER_MINUTE: u128 = 60_000_000_000;
83
84/// One held entry, its lifetime as a monotonic deadline.
85#[derive(Clone)]
86struct Entry {
87    recipient: BACnetRecipient,
88    process_identifier: u32,
89    issue_confirmed_notifications: bool,
90    expires_at: Duration,
91}
92
93impl Entry {
94    /// Whether `other` is the same entry: the same recipient and process
95    /// identifier, whatever its other members.
96    fn names_the_same(&self, other: &Entry) -> bool {
97        self.recipient == other.recipient && self.process_identifier == other.process_identifier
98    }
99
100    fn is_live(&self, now: Duration) -> bool {
101        self.expires_at > now
102    }
103
104    /// The entry as served at `now`, with the whole minutes left rounded up.
105    fn served(&self, now: Duration) -> BACnetEventNotificationSubscription {
106        let minutes = self
107            .expires_at
108            .saturating_sub(now)
109            .as_nanos()
110            .div_ceil(NANOS_PER_MINUTE);
111        BACnetEventNotificationSubscription {
112            recipient: self.recipient.clone(),
113            process_identifier: self.process_identifier,
114            issue_confirmed_notifications: self.issue_confirmed_notifications,
115            time_remaining: u32::try_from(minutes).unwrap_or(u32::MAX),
116        }
117    }
118}
119
120/// A Notification Forwarder's Subscribed_Recipients: at most
121/// [`MAX_SUBSCRIBED_RECIPIENTS`] entries, each lapsing when its Time
122/// Remaining runs out. See the [module documentation](self).
123#[derive(Clone, Default)]
124pub struct SubscribedRecipients {
125    entries: Vec<Entry>,
126    monotonic_clock: Option<Arc<MonotonicClock>>,
127    /// Elapsed time for a store with no bound clock, advanced by
128    /// [`Self::advance_by`].
129    logical_now: Duration,
130}
131
132impl fmt::Debug for SubscribedRecipients {
133    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
134        f.debug_struct("SubscribedRecipients")
135            .field("subscriptions", &self.subscriptions())
136            .finish_non_exhaustive()
137    }
138}
139
140impl SubscribedRecipients {
141    /// An empty list.
142    pub fn new() -> Self {
143        Self::default()
144    }
145
146    fn now(&self) -> Duration {
147        self.monotonic_clock
148            .as_ref()
149            .map_or(self.logical_now, |clock| clock())
150    }
151
152    /// The store's current time: the bound clock's reading, or the time
153    /// [`advance_by`](Self::advance_by) has counted.
154    pub(crate) fn current_time(&self) -> Duration {
155        self.now()
156    }
157
158    /// The live entries in list order, each with the whole minutes it has
159    /// left, rounded up.
160    pub fn subscriptions(&self) -> Vec<BACnetEventNotificationSubscription> {
161        let now = self.now();
162        self.entries
163            .iter()
164            .filter(|entry| entry.is_live(now))
165            .map(|entry| entry.served(now))
166            .collect()
167    }
168
169    /// The property value: the live entries, encoded back to back in
170    /// `PropertyValue::ApplicationData`.
171    pub fn read(&self) -> PropertyValue {
172        let mut buf = BytesMut::new();
173        // Every entry arrived through `write`, whose decoder holds recipients
174        // to BACnetAddress::MAX_MAC_LEN (#1156), so each one encodes.
175        encode_event_notification_subscription_list(&mut buf, &self.subscriptions())
176            .expect("stored recipients fit BACnetAddress::MAX_MAC_LEN");
177        PropertyValue::ApplicationData(buf.to_vec())
178    }
179
180    /// Replace the list with a written one, all or nothing.
181    ///
182    /// Only the framed form in `PropertyValue::ApplicationData` is a value;
183    /// anything else, or an entry that does not decode, fails with PROPERTY /
184    /// INVALID_DATA_TYPE. An entry whose Time Remaining is 0 or past
185    /// [`MAX_SUBSCRIPTION_MINUTES`] fails with PROPERTY / VALUE_OUT_OF_RANGE,
186    /// and the first entry past [`MAX_SUBSCRIBED_RECIPIENTS`] with RESOURCES /
187    /// NO_SPACE_TO_WRITE_PROPERTY; both name the entry's position in the
188    /// written list.
189    ///
190    /// Each entry's Time Remaining starts its lifetime over, except that an
191    /// entry written as it reads now keeps its deadline, and so does one
192    /// written with a minute more, which is how it read if a minute boundary
193    /// fell between the read and this write. A list read and written back, or
194    /// edited by the list services, so leaves the entries it doesn't change
195    /// alone, rather than stretching each to its rounded-up minute. Two
196    /// entries naming the same recipient and process are one entry, which
197    /// takes the later one's members.
198    pub fn write(&mut self, value: PropertyValue) -> Result<(), Error> {
199        let PropertyValue::ApplicationData(bytes) = value else {
200            return Err(common::invalid_data_type_error());
201        };
202        let now = self.now();
203        let mut written: Vec<Entry> = Vec::new();
204        let mut offset = 0;
205        let mut index = 0;
206        while offset < bytes.len() {
207            let (subscription, next) = decode_event_notification_subscription(&bytes, offset)
208                .map_err(|_| common::invalid_data_type_error())?;
209            offset = next;
210            if !(1..=MAX_SUBSCRIPTION_MINUTES).contains(&subscription.time_remaining) {
211                return Err(common::at_list_element(
212                    common::value_out_of_range_error(),
213                    index,
214                ));
215            }
216            let entry = self.entry_for(subscription, now);
217            match written.iter().position(|held| held.names_the_same(&entry)) {
218                Some(held) => written[held] = entry,
219                None if written.len() == MAX_SUBSCRIBED_RECIPIENTS => {
220                    return Err(common::at_list_element(
221                        common::protocol_error(
222                            ErrorClass::RESOURCES,
223                            ErrorCode::NO_SPACE_TO_WRITE_PROPERTY,
224                        ),
225                        index,
226                    ));
227                }
228                None => written.push(entry),
229            }
230            index += 1;
231        }
232        self.entries = written;
233        Ok(())
234    }
235
236    /// The entry a written `subscription` becomes at `now`. A live entry with
237    /// the same recipient, process and confirmation flag keeps its deadline
238    /// when the written minutes equal what it serves now or one more.
239    fn entry_for(&self, subscription: BACnetEventNotificationSubscription, now: Duration) -> Entry {
240        let lifetime = Duration::from_secs(u64::from(subscription.time_remaining) * 60);
241        let expires_at = self
242            .entries
243            .iter()
244            .find(|entry| {
245                if !entry.is_live(now) {
246                    return false;
247                }
248                let served = entry.served(now);
249                served.recipient == subscription.recipient
250                    && served.process_identifier == subscription.process_identifier
251                    && served.issue_confirmed_notifications
252                        == subscription.issue_confirmed_notifications
253                    && (served.time_remaining..=served.time_remaining.saturating_add(1))
254                        .contains(&subscription.time_remaining)
255            })
256            .map_or_else(|| now.saturating_add(lifetime), |entry| entry.expires_at);
257        Entry {
258            recipient: subscription.recipient,
259            process_identifier: subscription.process_identifier,
260            issue_confirmed_notifications: subscription.issue_confirmed_notifications,
261            expires_at,
262        }
263    }
264
265    /// Bind the database's monotonic clock, from the object's
266    /// `bind_monotonic_clock_internal`. Each held entry keeps the time it has
267    /// left: its deadline moves from the old time base to the new one, so a
268    /// list written or restored before the object joins a database lapses on
269    /// time.
270    pub fn bind_monotonic_clock(&mut self, clock: Option<Arc<MonotonicClock>>) {
271        let before = self.now();
272        self.monotonic_clock = clock;
273        let after = self.now();
274        for entry in &mut self.entries {
275            let left = entry.expires_at.saturating_sub(before);
276            entry.expires_at = after.saturating_add(left);
277        }
278    }
279
280    /// Drop every entry whose deadline is at or before `now`, a monotonic
281    /// instant, from the object's `advance_monotonic_time_internal`. `true`
282    /// when one was dropped.
283    pub fn advance_to(&mut self, now: Duration) -> bool {
284        let held = self.entries.len();
285        self.entries.retain(|entry| entry.is_live(now));
286        self.entries.len() != held
287    }
288
289    /// Count `elapsed` on a store with no bound clock, then drop what has
290    /// lapsed, as [`Self::advance_to`] does. `true` when an entry was dropped.
291    pub fn advance_by(&mut self, elapsed: Duration) -> bool {
292        self.logical_now = self.logical_now.saturating_add(elapsed);
293        self.advance_to(self.now())
294    }
295
296    /// The earliest deadline among the held entries, for the object's
297    /// `next_monotonic_deadline_internal`.
298    pub fn next_deadline(&self) -> Option<Duration> {
299        self.entries.iter().map(|entry| entry.expires_at).min()
300    }
301}
302
303#[cfg(test)]
304#[path = "subscribed_recipients_tests.rs"]
305mod tests;