Skip to main content

bacnet_objects/multistate/
mod.rs

1//! Multi-State Input (type 13), Multi-State Output (type 14), and
2//! Multi-State Value (type 19) objects per ASHRAE 135-2020 Clauses 12.18,
3//! 12.19, and 12.20.
4
5use bacnet_types::enums::{
6    ErrorClass, ErrorCode, EventState, ObjectType, PropertyIdentifier, Reliability,
7};
8use bacnet_types::error::Error;
9use bacnet_types::primitives::{BACnetTimeStamp, ObjectIdentifier, PropertyValue, StatusFlags};
10use std::borrow::Cow;
11
12use crate::common::{
13    self, read_common_properties, read_generic_event_properties, write_generic_event_properties,
14};
15use crate::event::{history::EventHistory, ChangeOfStateDetector};
16use crate::rollback::impl_intrinsic_write_rollback;
17use crate::traits::{BACnetObject, ReliabilityEvaluation};
18
19/// Resource cap consistent with bounded server tables such as
20/// `MAX_COV_SUBSCRIPTIONS`. Recipient_List has the same pre-existing unbounded
21/// growth gap, which is outside issue #228.
22pub(crate) const MAX_ALARM_VALUES: usize = 1024;
23
24fn decode_alarm_values_write(
25    array_index: Option<u32>,
26    value: PropertyValue,
27) -> Result<Vec<u32>, Error> {
28    if array_index.is_some() {
29        return Err(Error::Protocol {
30            class: ErrorClass::PROPERTY.to_raw() as u32,
31            code: ErrorCode::PROPERTY_IS_NOT_AN_ARRAY.to_raw() as u32,
32        });
33    }
34    let PropertyValue::List(values) = value else {
35        return Err(common::invalid_data_type_error());
36    };
37    if values.len() > MAX_ALARM_VALUES {
38        return Err(Error::Protocol {
39            class: ErrorClass::RESOURCES.to_raw() as u32,
40            code: ErrorCode::NO_SPACE_TO_WRITE_PROPERTY.to_raw() as u32,
41        });
42    }
43    values
44        .into_iter()
45        .map(|value| match value {
46            PropertyValue::Unsigned(value) => common::u64_to_u32(value),
47            _ => Err(common::invalid_data_type_error()),
48        })
49        .collect()
50}
51
52/// Reject a `number_of_states` of zero.
53///
54/// Every multi-state object starts `PRESENT_VALUE` (and, for Output/Value, the
55/// relinquish default) at 1, and accepts writes only in `1..=number_of_states`.
56/// With zero states that initial value is out of range with no attainable value,
57/// so the constructors centralize this guard here.
58fn require_nonzero_states(number_of_states: u32) -> Result<(), Error> {
59    if number_of_states == 0 {
60        return Err(Error::OutOfRange(
61            "number_of_states must be at least 1".into(),
62        ));
63    }
64    Ok(())
65}
66
67/// Resize an object's State_Text with the repository's local count-change policy.
68///
69/// Validation precedes mutation. Shrink truncates the tail, while growth keeps
70/// the retained prefix and appends the same `State {n}` labels as construction.
71fn resize_state_text(
72    current_number_of_states: &mut u32,
73    state_text: &mut Vec<String>,
74    number_of_states: u32,
75) -> Result<(), Error> {
76    require_nonzero_states(number_of_states)?;
77    let new_len = number_of_states as usize;
78    if new_len < state_text.len() {
79        state_text.truncate(new_len);
80    } else {
81        state_text.extend((state_text.len() + 1..=new_len).map(|state| format!("State {state}")));
82    }
83    *current_number_of_states = number_of_states;
84    Ok(())
85}
86
87#[derive(Debug, Clone, Copy, PartialEq, Eq)]
88pub(crate) enum OwnedMultiStateFault {
89    ConfigurationError,
90    MultiStateOutOfRange,
91}
92
93impl OwnedMultiStateFault {
94    fn reliability(self) -> u32 {
95        match self {
96            Self::ConfigurationError => Reliability::CONFIGURATION_ERROR.to_raw(),
97            Self::MultiStateOutOfRange => Reliability::MULTI_STATE_OUT_OF_RANGE.to_raw(),
98        }
99    }
100}
101
102/// Private first-stage Reliability ownership shared by MSI, MSO, and MSV.
103#[derive(Debug, Default)]
104struct MultiStateReliabilityState {
105    pub(crate) owned_fault: Option<OwnedMultiStateFault>,
106}
107
108impl MultiStateReliabilityState {
109    fn clear_ownership(&mut self) {
110        self.owned_fault = None;
111    }
112
113    fn evaluate(
114        &mut self,
115        configuration_invalid: bool,
116        present_value: u32,
117        number_of_states: u32,
118        reliability: &mut u32,
119    ) -> ReliabilityEvaluation {
120        let observed_fault = if configuration_invalid {
121            Some(OwnedMultiStateFault::ConfigurationError)
122        } else if !(1..=number_of_states).contains(&present_value) {
123            Some(OwnedMultiStateFault::MultiStateOutOfRange)
124        } else {
125            None
126        };
127
128        let (new_reliability, new_owner) = if self.owned_fault.is_some() {
129            (
130                observed_fault
131                    .map(OwnedMultiStateFault::reliability)
132                    .unwrap_or_else(|| Reliability::NO_FAULT_DETECTED.to_raw()),
133                observed_fault,
134            )
135        } else if *reliability == Reliability::NO_FAULT_DETECTED.to_raw() {
136            let Some(fault) = observed_fault else {
137                return ReliabilityEvaluation::Unchanged;
138            };
139            (fault.reliability(), Some(fault))
140        } else {
141            return ReliabilityEvaluation::Unchanged;
142        };
143
144        // Ownership can change while Reliability is already zero, notably when
145        // inhibit normalized an owned fault and its source recovered before the
146        // gate reopened. Keep the private state current even without a public
147        // Reliability transition.
148        self.owned_fault = new_owner;
149        if new_reliability == *reliability {
150            return ReliabilityEvaluation::Unchanged;
151        }
152        let old_reliability = *reliability;
153        *reliability = new_reliability;
154        ReliabilityEvaluation::Changed {
155            old_reliability,
156            new_reliability,
157        }
158    }
159}
160
161mod input;
162mod output;
163mod value;
164pub use input::*;
165pub use output::*;
166pub use value::*;
167#[cfg(test)]
168mod tests;