bacnet_ops/watch/options.rs
1//! The hub's settings: lifetimes, polling, backoffs, what a point reads,
2//! and what a front end gets per change.
3
4use std::sync::Arc;
5use std::time::Duration;
6
7use bacnet_types::enums::PropertyIdentifier;
8
9use super::{Lifetime, POINT_PROPERTIES};
10use crate::limits::TokenBucket;
11
12/// The default shortest time between two polls of a device, the MCP
13/// server's floor.
14pub const MIN_POLL: Duration = Duration::from_secs(10);
15
16/// The lowest [`WatchOptions::poll_floor`] a hub takes.
17pub const POLL_FLOOR: Duration = Duration::from_secs(1);
18
19/// Rows a point's snapshot holds at most: what it reads, the first read's
20/// extras, and room for a device's spares.
21pub const MAX_ROWS: usize = 32;
22
23/// Samples a lease's stream holds at most.
24pub const MAX_SAMPLES: usize = 1024;
25
26/// A wait that doubles after each failure, from `first` up to `longest`.
27#[derive(Debug, Clone, Copy, PartialEq, Eq)]
28pub struct Backoff {
29 /// The first wait.
30 pub first: Duration,
31 /// The longest wait.
32 pub longest: Duration,
33}
34
35impl Backoff {
36 /// A wait that never grows.
37 pub const fn fixed(wait: Duration) -> Self {
38 Self {
39 first: wait,
40 longest: wait,
41 }
42 }
43
44 /// The wait after `previous` (zero for the first failure).
45 pub fn next(self, previous: Duration) -> Duration {
46 if previous.is_zero() {
47 self.first
48 } else {
49 (previous * 2).clamp(self.first, self.longest)
50 }
51 }
52
53 fn validate(self, what: &str) -> Result<(), String> {
54 if self.first.is_zero() || self.longest < self.first {
55 return Err(format!(
56 "{what}: the first wait must be above 0 and at most the longest"
57 ));
58 }
59 Ok(())
60 }
61}
62
63/// What a watch does when its device isn't in the discovery table.
64#[derive(Debug, Clone, Copy, PartialEq, Eq)]
65pub enum MissingDevice {
66 /// Give the point up: [`State::Gone`](super::State::Gone).
67 Fail,
68 /// Report it unreachable and look again after the poll backoff.
69 Poll,
70}
71
72/// Which properties a poll reads one by one once the device refused RPM
73/// or failed a batch.
74#[derive(Debug, Clone, Copy, PartialEq, Eq)]
75pub enum Fallback {
76 /// Every property the point reads.
77 All,
78 /// The watched one only.
79 Watched,
80}
81
82/// What a point reads.
83#[derive(Debug, Clone, PartialEq, Eq)]
84pub struct Reads {
85 /// The properties an object watch reads and polls; the first is the
86 /// one its state texts name.
87 pub object: Vec<PropertyIdentifier>,
88 /// A property watch also reads Status_Flags.
89 pub property_flags: bool,
90 /// Properties only the first read adds, such as Units.
91 pub first: Vec<PropertyIdentifier>,
92 /// The first read asks for Object_Name too, and again without it when
93 /// the name may have failed the read.
94 pub name: bool,
95 /// A binary or multi-state object's state texts follow the first read.
96 pub state_texts: bool,
97 /// What a poll reads without RPM.
98 pub fallback: Fallback,
99}
100
101impl Reads {
102 /// The MCP server's: an object's [`POINT_PROPERTIES`], a property
103 /// alone, nothing more on the first read.
104 pub fn points() -> Self {
105 Self {
106 object: POINT_PROPERTIES.to_vec(),
107 property_flags: false,
108 first: Vec::new(),
109 name: false,
110 state_texts: false,
111 fallback: Fallback::All,
112 }
113 }
114
115 /// A value with its Status_Flags, named, with its Units and state
116 /// texts from the first read; polled for the value alone without RPM.
117 /// An object watch follows Present_Value. The TUI's watch list reads
118 /// this way.
119 pub fn value() -> Self {
120 Self {
121 object: vec![
122 PropertyIdentifier::PRESENT_VALUE,
123 PropertyIdentifier::STATUS_FLAGS,
124 ],
125 property_flags: true,
126 first: vec![PropertyIdentifier::UNITS],
127 name: true,
128 state_texts: true,
129 fallback: Fallback::Watched,
130 }
131 }
132}
133
134/// The hub's settings.
135#[derive(Debug, Clone)]
136pub struct WatchOptions {
137 /// Each subscription's lifetime.
138 pub lifetime: Lifetime,
139 /// How long before a lifetime ends the renewal goes. It covers a wait
140 /// for the device's gate and the renewal's own request with retries
141 /// (see [`renewal_margin`](super::renewal_margin)).
142 pub renewal_margin: Duration,
143 /// Least time between two polls of a device, all its polled points
144 /// read together; at least [`poll_floor`](Self::poll_floor).
145 pub poll_every: Duration,
146 /// The shortest [`poll_every`](Self::poll_every) taken; at least
147 /// [`POLL_FLOOR`].
148 pub poll_floor: Duration,
149 /// How long a device that stopped answering polls waits for the next.
150 pub poll_backoff: Backoff,
151 /// After a subscription failed for want of an answer (rather than a
152 /// refusal), or a renewal failed, how long the point is polled before
153 /// it is read and subscribed again; never sooner than
154 /// [`poll_floor`](Self::poll_floor).
155 pub retry_cov: Backoff,
156 /// What a point does whose device isn't in the discovery table.
157 pub missing: MissingDevice,
158 /// What a point reads.
159 pub reads: Reads,
160 /// Changes to one point within this window make one signal; zero for
161 /// a signal per change.
162 pub coalesce: Duration,
163 /// Samples a lease's stream ([`Lease::samples`](super::Lease::samples))
164 /// holds at least before the oldest go (the channel rounds it up to a
165 /// power of two).
166 pub samples: usize,
167 /// Points watched at once, those still cancelling included.
168 pub max_points: usize,
169 /// The hub's own rate, taken by each request it sends but cancels;
170 /// `None` for none beyond the engine's limits.
171 pub bucket: Option<Arc<TokenBucket>>,
172}
173
174impl Default for WatchOptions {
175 fn default() -> Self {
176 Self {
177 lifetime: Lifetime::DEFAULT,
178 renewal_margin: Duration::from_secs(60),
179 poll_every: MIN_POLL,
180 poll_floor: MIN_POLL,
181 poll_backoff: Backoff {
182 first: MIN_POLL,
183 longest: Duration::from_secs(60),
184 },
185 retry_cov: Backoff::fixed(Duration::from_secs(300)),
186 missing: MissingDevice::Poll,
187 reads: Reads::points(),
188 coalesce: Duration::from_secs(1),
189 samples: 32,
190 max_points: 256,
191 bucket: None,
192 }
193 }
194}
195
196impl WatchOptions {
197 /// Check the settings: polls no faster than the floor, a renewal
198 /// margin inside the lifetime, backoffs that start above zero, room
199 /// for one point and its rows.
200 pub fn validate(&self) -> Result<(), String> {
201 if self.poll_floor < POLL_FLOOR {
202 return Err(format!(
203 "the poll floor is at least {} s",
204 POLL_FLOOR.as_secs()
205 ));
206 }
207 if self.poll_every < self.poll_floor {
208 return Err(format!(
209 "polls go at most every {} s",
210 self.poll_floor.as_secs()
211 ));
212 }
213 if self.renewal_margin.is_zero() || self.renewal_margin >= self.lifetime.duration() {
214 return Err("the renewal margin must be inside the lifetime".into());
215 }
216 self.poll_backoff.validate("poll_backoff")?;
217 self.retry_cov.validate("retry_cov")?;
218 if self.retry_cov.first < self.poll_floor {
219 return Err("retry_cov waits at least the poll floor".into());
220 }
221 if self.max_points == 0 {
222 return Err("max_points must be at least 1".into());
223 }
224 if self.coalesce > Duration::from_secs(60) {
225 return Err("coalesce must be a minute or less".into());
226 }
227 if !(1..=MAX_SAMPLES).contains(&self.samples) {
228 return Err(format!("samples must be 1 to {MAX_SAMPLES}"));
229 }
230 let reads = &self.reads;
231 if reads.object.is_empty() {
232 return Err("an object watch must read a property".into());
233 }
234 // The extras: Object_Name, Status_Flags and two state texts.
235 if reads.object.len() + reads.first.len() + 4 > MAX_ROWS {
236 return Err(format!("a point reads at most {MAX_ROWS} rows"));
237 }
238 Ok(())
239 }
240}