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;