Skip to main content

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}