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::traits::{BACnetObject, ReliabilityEvaluation};
17
18/// Resource cap consistent with bounded server tables such as
19/// `MAX_COV_SUBSCRIPTIONS`.
20pub(crate) const MAX_ALARM_VALUES: usize = 1024;
21
22/// Decode an Alarm_Values write on a Multi-state Input or Value. Each entry
23/// is an Unsigned naming one of the object's `number_of_states` states; one
24/// past them could never match Present_Value, so it is VALUE_OUT_OF_RANGE,
25/// naming the element, rather than stored for Reliability to report as
26/// CONFIGURATION_ERROR (#1429). The local `set_alarm_values` stays unchecked.
27fn decode_alarm_values_write(
28    array_index: Option<u32>,
29    value: PropertyValue,
30    number_of_states: u32,
31) -> Result<Vec<u32>, Error> {
32    if array_index.is_some() {
33        return Err(Error::Protocol {
34            class: ErrorClass::PROPERTY.to_raw() as u32,
35            code: ErrorCode::PROPERTY_IS_NOT_AN_ARRAY.to_raw() as u32,
36        });
37    }
38    let PropertyValue::List(values) = value else {
39        return Err(common::invalid_data_type_error());
40    };
41    // A refusal names its element (#1048); the first one past the cap is the
42    // element that does not fit.
43    if values.len() > MAX_ALARM_VALUES {
44        return Err(common::at_list_element(
45            Error::Protocol {
46                class: ErrorClass::RESOURCES.to_raw() as u32,
47                code: ErrorCode::NO_SPACE_TO_WRITE_PROPERTY.to_raw() as u32,
48            },
49            MAX_ALARM_VALUES,
50        ));
51    }
52    values
53        .into_iter()
54        .enumerate()
55        .map(|(index, value)| {
56            match value {
57                PropertyValue::Unsigned(state)
58                    if (1..=u64::from(number_of_states)).contains(&state) =>
59                {
60                    Ok(state as u32)
61                }
62                PropertyValue::Unsigned(_) => Err(common::value_out_of_range_error()),
63                _ => Err(common::invalid_data_type_error()),
64            }
65            .map_err(|error| common::at_list_element(error, index))
66        })
67        .collect()
68}
69
70/// Reject a `number_of_states` of zero.
71///
72/// Every multi-state object starts `PRESENT_VALUE` (and, for Output/Value, the
73/// relinquish default) at 1, and accepts writes only in `1..=number_of_states`.
74/// With zero states that initial value is out of range with no attainable value,
75/// so the constructors centralize this guard here.
76fn require_nonzero_states(number_of_states: u32) -> Result<(), Error> {
77    if number_of_states == 0 {
78        return Err(Error::OutOfRange(
79            "number_of_states must be at least 1".into(),
80        ));
81    }
82    Ok(())
83}
84
85/// Resize an object's State_Text with the repository's local count-change policy.
86///
87/// Validation precedes mutation. Shrink truncates the tail, while growth keeps
88/// the retained prefix and appends the same `State {n}` labels as construction.
89fn resize_state_text(
90    current_number_of_states: &mut u32,
91    state_text: &mut Vec<String>,
92    number_of_states: u32,
93) -> Result<(), Error> {
94    require_nonzero_states(number_of_states)?;
95    let new_len = number_of_states as usize;
96    if new_len < state_text.len() {
97        state_text.truncate(new_len);
98    } else {
99        state_text.extend((state_text.len() + 1..=new_len).map(|state| format!("State {state}")));
100    }
101    *current_number_of_states = number_of_states;
102    Ok(())
103}
104
105/// What a multi-state object takes only from a CreateObject initial value
106/// (#1429): Number_Of_States. WriteProperty refuses it; a whole State_Text
107/// write changes it instead (#1443).
108const CREATION_ONLY: &[PropertyIdentifier] = &[PropertyIdentifier::NUMBER_OF_STATES];
109
110/// The most states a client may give a multi-state object, by a CreateObject
111/// initial value or by State_Text written whole. State_Text keeps a label
112/// for every state, so the count a client picks is bounded like the other
113/// tables a client sizes.
114pub const MAX_NUMBER_OF_STATES: u32 = 1024;
115
116/// Check a state count a client asks for, given the `current` one and
117/// `held`, the values the object keeps that name a state (Present_Value,
118/// Relinquish_Default, the commands in Priority_Array, Alarm_Values).
119///
120/// The count runs from 1 to [`MAX_NUMBER_OF_STATES`]. Clause 12.20.10 and
121/// its Multi-state Input and Output counterparts leave it to the device
122/// whether a shrink repairs the values that name a state past it, or keeps
123/// them and reports a fault; this one refuses a count that would strand a
124/// held state that names one of the `current` states. Both refusals are
125/// VALUE_OUT_OF_RANGE, the code WriteProperty gives a value the property
126/// can't take (Clause 15.9.1.3.1). A held state already past the current
127/// count, which only a local shrink can leave, doesn't block a new count.
128fn check_state_count(
129    current: u32,
130    count: u64,
131    held: impl IntoIterator<Item = u32>,
132) -> Result<u32, Error> {
133    let count = u32::try_from(count)
134        .ok()
135        .filter(|count| (1..=MAX_NUMBER_OF_STATES).contains(count))
136        .ok_or_else(common::value_out_of_range_error)?;
137    let stranded = |state: u32| (1..=current).contains(&state) && state > count;
138    if held.into_iter().any(stranded) {
139        return Err(common::value_out_of_range_error());
140    }
141    Ok(count)
142}
143
144/// Apply a CreateObject initial value to Number_Of_States (#1429), checked
145/// before anything changes: [`check_state_count`], then State_Text resized
146/// as [`resize_state_text`] does (Clause 12.20.10 ties the two sizes
147/// together). Any other property is WRITE_ACCESS_DENIED, as the trait
148/// default answers.
149fn initialize_states(
150    number_of_states: &mut u32,
151    state_text: &mut Vec<String>,
152    held: impl IntoIterator<Item = u32>,
153    property: PropertyIdentifier,
154    value: PropertyValue,
155) -> Result<(), Error> {
156    if property != PropertyIdentifier::NUMBER_OF_STATES {
157        return Err(common::write_access_denied_error());
158    }
159    let PropertyValue::Unsigned(count) = value else {
160        return Err(common::invalid_data_type_error());
161    };
162    let count = check_state_count(*number_of_states, count, held)?;
163    resize_state_text(number_of_states, state_text, count)
164}
165
166/// Take State_Text written whole, by WriteProperty, WritePropertyMultiple or
167/// a CreateObject initial value (#1443): one CharacterString per state,
168/// where a single label arrives as that CharacterString, the way a
169/// one-element array value decodes. The labels replace the old ones, and
170/// their number becomes Number_Of_States: Clause 12.20.11 and its
171/// Multi-state Input and Output counterparts tie the two sizes together in
172/// both directions. A new count is checked as [`check_state_count`] says;
173/// nothing changes on a refusal.
174fn write_whole_state_text(
175    number_of_states: &mut u32,
176    state_text: &mut Vec<String>,
177    held: impl IntoIterator<Item = u32>,
178    value: PropertyValue,
179) -> Result<(), Error> {
180    let labels = match value {
181        PropertyValue::CharacterString(label) => vec![label],
182        PropertyValue::List(values) => values
183            .into_iter()
184            .map(|value| match value {
185                PropertyValue::CharacterString(label) => Ok(label),
186                _ => Err(common::invalid_data_type_error()),
187            })
188            .collect::<Result<Vec<_>, _>>()?,
189        _ => return Err(common::invalid_data_type_error()),
190    };
191    let count = if labels.len() == *number_of_states as usize {
192        *number_of_states
193    } else {
194        check_state_count(*number_of_states, labels.len() as u64, held)?
195    };
196    *state_text = labels;
197    *number_of_states = count;
198    Ok(())
199}
200
201/// Write one State_Text element, the whole array
202/// ([`write_whole_state_text`]), or its size at index 0; an index past the
203/// states is INVALID_ARRAY_INDEX.
204///
205/// A whole write can change the array's size, so Clause 12.1.5.1 has index
206/// 0 take a write too (#1443). The Unsigned written there is a new state
207/// count, checked as [`check_state_count`] says, and it resizes State_Text
208/// and Number_Of_States together as [`resize_state_text`] does: a shrink
209/// drops the last labels, and a grow, whose new labels the clause leaves to
210/// the device, appends the `State {n}` labels a new object starts with.
211fn write_state_text(
212    number_of_states: &mut u32,
213    state_text: &mut Vec<String>,
214    held: impl IntoIterator<Item = u32>,
215    array_index: Option<u32>,
216    value: PropertyValue,
217) -> Result<(), Error> {
218    match array_index {
219        None => write_whole_state_text(number_of_states, state_text, held, value),
220        Some(0) => {
221            let PropertyValue::Unsigned(count) = value else {
222                return Err(common::invalid_data_type_error());
223            };
224            let count = check_state_count(*number_of_states, count, held)?;
225            resize_state_text(number_of_states, state_text, count)
226        }
227        Some(index) if (index as usize) <= state_text.len() => {
228            let PropertyValue::CharacterString(label) = value else {
229                return Err(common::invalid_data_type_error());
230            };
231            state_text[(index - 1) as usize] = label;
232            Ok(())
233        }
234        Some(_) => Err(common::invalid_array_index_error()),
235    }
236}
237
238#[derive(Debug, Clone, Copy, PartialEq, Eq)]
239pub(crate) enum OwnedMultiStateFault {
240    ConfigurationError,
241    MultiStateOutOfRange,
242}
243
244impl OwnedMultiStateFault {
245    fn reliability(self) -> Reliability {
246        match self {
247            Self::ConfigurationError => Reliability::CONFIGURATION_ERROR,
248            Self::MultiStateOutOfRange => Reliability::MULTI_STATE_OUT_OF_RANGE,
249        }
250    }
251}
252
253/// Private first-stage Reliability ownership shared by MSI, MSO, and MSV.
254#[derive(Debug, Default)]
255struct MultiStateReliabilityState {
256    pub(crate) owned_fault: Option<OwnedMultiStateFault>,
257}
258
259impl MultiStateReliabilityState {
260    fn clear_ownership(&mut self) {
261        self.owned_fault = None;
262    }
263
264    fn evaluate(
265        &mut self,
266        configuration_invalid: bool,
267        present_value: u32,
268        number_of_states: u32,
269        reliability: &mut Reliability,
270    ) -> ReliabilityEvaluation {
271        let observed_fault = if configuration_invalid {
272            Some(OwnedMultiStateFault::ConfigurationError)
273        } else if !(1..=number_of_states).contains(&present_value) {
274            Some(OwnedMultiStateFault::MultiStateOutOfRange)
275        } else {
276            None
277        };
278
279        let (new_reliability, new_owner) = if self.owned_fault.is_some() {
280            (
281                observed_fault
282                    .map(OwnedMultiStateFault::reliability)
283                    .unwrap_or(Reliability::NO_FAULT_DETECTED),
284                observed_fault,
285            )
286        } else if *reliability == Reliability::NO_FAULT_DETECTED {
287            let Some(fault) = observed_fault else {
288                return ReliabilityEvaluation::Unchanged;
289            };
290            (fault.reliability(), Some(fault))
291        } else {
292            return ReliabilityEvaluation::Unchanged;
293        };
294
295        // Ownership can change while Reliability is already zero, notably when
296        // inhibit normalized an owned fault and its source recovered before the
297        // gate reopened. Keep the private state current even without a public
298        // Reliability transition.
299        self.owned_fault = new_owner;
300        if new_reliability == *reliability {
301            return ReliabilityEvaluation::Unchanged;
302        }
303        let old_reliability = *reliability;
304        *reliability = new_reliability;
305        ReliabilityEvaluation::Changed {
306            old_reliability,
307            new_reliability,
308        }
309    }
310}
311
312mod input;
313mod output;
314mod value;
315pub use input::*;
316pub use output::*;
317pub use value::*;
318#[cfg(test)]
319mod tests;