Skip to main content

bacnet_server/cov/
observation.rs

1//! One immutable delivered observation per accepted reference.
2use super::CovSample;
3use bacnet_types::{error::Error, primitives::PropertyValue};
4
5/// A bounded selected sample and its declared optional four-bit Status_Flags.
6/// Specialized Value_Source reports also retain their captured PV and, for
7/// commandable objects, command priority; command time is not a trigger.
8/// Whole-object reports also retain the Table 13-1 values whose changes
9/// trigger a notification (Staging's Present_Stage).
10/// No delivered observation is represented by the subscription's outer `None`;
11/// an observation with absent flags is a distinct successful delivery.
12#[derive(Debug, Clone, PartialEq, Eq)]
13pub struct CovObservation {
14    sample: CovSample,
15    flags: Option<u8>,
16    source_companions: Option<(CovSample, Option<CovSample>)>,
17    triggers: Box<[CovSample]>,
18}
19impl CovObservation {
20    /// Pair a validated selected sample with absent or canonical present flags.
21    /// Present flags must be a one-byte BitString with four unused zero low bits.
22    pub fn new(sample: CovSample, flags: Option<&PropertyValue>) -> Result<Self, Error> {
23        Ok(Self {
24            sample,
25            source_companions: None,
26            flags: flags.map(validate_flags).transpose()?,
27            triggers: Box::default(),
28        })
29    }
30    /// Attach the bounded trigger values captured with a whole-object report.
31    pub(crate) fn with_triggers(mut self, triggers: Box<[CovSample]>) -> Self {
32        self.triggers = triggers;
33        self
34    }
35    /// Whether a captured trigger value differs from the last delivered one.
36    /// An object with no trigger values never reports through this check.
37    pub(crate) fn triggers_changed(&self, previous: Option<&Self>) -> bool {
38        !self.triggers.is_empty() && previous.map(|p| &p.triggers) != Some(&self.triggers)
39    }
40    /// Attach the bounded PV and optional command priority captured with a source.
41    pub(crate) fn with_source_companions(
42        mut self,
43        pv: CovSample,
44        priority: Option<CovSample>,
45    ) -> Self {
46        self.source_companions = Some((pv, priority));
47        self
48    }
49    pub(crate) fn source_companions(&self) -> Option<&(CovSample, Option<CovSample>)> {
50        self.source_companions.as_ref()
51    }
52    /// The immutable bounded selected value.
53    pub fn sample(&self) -> &CovSample {
54        &self.sample
55    }
56    /// The four used status bits, or declared absence at delivery.
57    pub fn status_flags(&self) -> Option<u8> {
58        self.flags
59    }
60    /// Only a present changed value is a status trigger; disappearance is not.
61    pub(crate) fn flags_changed(&self, previous: Option<&Self>) -> bool {
62        self.flags.is_some() && previous.map(|p| p.flags) != Some(self.flags)
63    }
64}
65pub(crate) fn validate_flags(value: &PropertyValue) -> Result<u8, Error> {
66    match value {
67        PropertyValue::BitString {
68            unused_bits: 4,
69            data,
70        } if data.len() == 1 && data[0] & 0x0f == 0 => Ok(data[0] >> 4),
71        _ => Err(Error::Encoding(
72            "COV Status_Flags must be a canonical four-bit BitString".into(),
73        )),
74    }
75}
76
77#[cfg(test)]
78mod tests {
79    use super::*;
80    #[test]
81    fn cov_status_observation_validation_and_presence_semantics() {
82        let sample = CovSample::new(&PropertyValue::Real(1.0)).unwrap();
83        let absent = CovObservation::new(sample.clone(), None).unwrap();
84        let flags = PropertyValue::BitString {
85            unused_bits: 4,
86            data: vec![0x80],
87        };
88        let present = CovObservation::new(sample.clone(), Some(&flags)).unwrap();
89        assert!(present.flags_changed(None));
90        assert!(present.flags_changed(Some(&absent)));
91        assert!(!present.flags_changed(Some(&present)));
92        assert!(!absent.flags_changed(Some(&present)));
93        assert_eq!(present.status_flags(), Some(8));
94        assert_eq!(absent.status_flags(), None);
95        for invalid in [
96            PropertyValue::Null,
97            PropertyValue::BitString {
98                unused_bits: 0,
99                data: vec![0],
100            },
101            PropertyValue::BitString {
102                unused_bits: 4,
103                data: vec![],
104            },
105            PropertyValue::BitString {
106                unused_bits: 4,
107                data: vec![0, 0],
108            },
109            PropertyValue::BitString {
110                unused_bits: 4,
111                data: vec![0x81],
112            },
113        ] {
114            assert!(CovObservation::new(sample.clone(), Some(&invalid)).is_err());
115        }
116    }
117}