use std::sync::Arc;
use std::time::Duration;
use bacnet_types::enums::PropertyIdentifier;
use super::{Lifetime, POINT_PROPERTIES};
use crate::limits::TokenBucket;
pub const MIN_POLL: Duration = Duration::from_secs(10);
pub const POLL_FLOOR: Duration = Duration::from_secs(1);
pub const MAX_ROWS: usize = 32;
pub const MAX_SAMPLES: usize = 1024;
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub struct Backoff {
pub first: Duration,
pub longest: Duration,
}
impl Backoff {
pub const fn fixed(wait: Duration) -> Self {
Self {
first: wait,
longest: wait,
}
}
pub fn next(self, previous: Duration) -> Duration {
if previous.is_zero() {
self.first
} else {
(previous * 2).clamp(self.first, self.longest)
}
}
fn validate(self, what: &str) -> Result<(), String> {
if self.first.is_zero() || self.longest < self.first {
return Err(format!(
"{what}: the first wait must be above 0 and at most the longest"
));
}
Ok(())
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum MissingDevice {
Fail,
Poll,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum Fallback {
All,
Watched,
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct Reads {
pub object: Vec<PropertyIdentifier>,
pub property_flags: bool,
pub first: Vec<PropertyIdentifier>,
pub name: bool,
pub state_texts: bool,
pub fallback: Fallback,
}
impl Reads {
pub fn points() -> Self {
Self {
object: POINT_PROPERTIES.to_vec(),
property_flags: false,
first: Vec::new(),
name: false,
state_texts: false,
fallback: Fallback::All,
}
}
pub fn value() -> Self {
Self {
object: vec![
PropertyIdentifier::PRESENT_VALUE,
PropertyIdentifier::STATUS_FLAGS,
],
property_flags: true,
first: vec![PropertyIdentifier::UNITS],
name: true,
state_texts: true,
fallback: Fallback::Watched,
}
}
}
#[derive(Debug, Clone)]
pub struct WatchOptions {
pub lifetime: Lifetime,
pub renewal_margin: Duration,
pub poll_every: Duration,
pub poll_floor: Duration,
pub poll_backoff: Backoff,
pub retry_cov: Backoff,
pub missing: MissingDevice,
pub reads: Reads,
pub coalesce: Duration,
pub samples: usize,
pub max_points: usize,
pub bucket: Option<Arc<TokenBucket>>,
}
impl Default for WatchOptions {
fn default() -> Self {
Self {
lifetime: Lifetime::DEFAULT,
renewal_margin: Duration::from_secs(60),
poll_every: MIN_POLL,
poll_floor: MIN_POLL,
poll_backoff: Backoff {
first: MIN_POLL,
longest: Duration::from_secs(60),
},
retry_cov: Backoff::fixed(Duration::from_secs(300)),
missing: MissingDevice::Poll,
reads: Reads::points(),
coalesce: Duration::from_secs(1),
samples: 32,
max_points: 256,
bucket: None,
}
}
}
impl WatchOptions {
pub fn validate(&self) -> Result<(), String> {
if self.poll_floor < POLL_FLOOR {
return Err(format!(
"the poll floor is at least {} s",
POLL_FLOOR.as_secs()
));
}
if self.poll_every < self.poll_floor {
return Err(format!(
"polls go at most every {} s",
self.poll_floor.as_secs()
));
}
if self.renewal_margin.is_zero() || self.renewal_margin >= self.lifetime.duration() {
return Err("the renewal margin must be inside the lifetime".into());
}
self.poll_backoff.validate("poll_backoff")?;
self.retry_cov.validate("retry_cov")?;
if self.retry_cov.first < self.poll_floor {
return Err("retry_cov waits at least the poll floor".into());
}
if self.max_points == 0 {
return Err("max_points must be at least 1".into());
}
if self.coalesce > Duration::from_secs(60) {
return Err("coalesce must be a minute or less".into());
}
if !(1..=MAX_SAMPLES).contains(&self.samples) {
return Err(format!("samples must be 1 to {MAX_SAMPLES}"));
}
let reads = &self.reads;
if reads.object.is_empty() {
return Err("an object watch must read a property".into());
}
if reads.object.len() + reads.first.len() + 4 > MAX_ROWS {
return Err(format!("a point reads at most {MAX_ROWS} rows"));
}
Ok(())
}
}