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