Skip to main content

bacnet_objects/multistate/
input.rs

1use super::*;
2
3// ---------------------------------------------------------------------------
4// MultiStateInput (type 13)
5// ---------------------------------------------------------------------------
6
7/// BACnet Multi-State Input object.
8///
9/// Read-only multi-state point. Present_Value is writable only when out-of-service.
10/// Present_Value is Unsigned, range 1..=number_of_states.
11pub struct MultiStateInputObject {
12    oid: ObjectIdentifier,
13    name: String,
14    description: String,
15    present_value: u32,
16    number_of_states: u32,
17    out_of_service: bool,
18    status_flags: StatusFlags,
19    /// Reliability: 0 = NO_FAULT_DETECTED.
20    reliability: u32,
21    reliability_before_out_of_service: Option<u32>,
22    reliability_inhibit: common::ReliabilityInhibitState,
23    reliability_evaluator: MultiStateReliabilityState,
24    state_text: Vec<String>,
25    /// CHANGE_OF_STATE event detector.
26    event_detector: ChangeOfStateDetector,
27    /// Event_Detection_Enable (Clause 12.18). Clause 13.2.2.1: "If the
28    /// Event_Detection_Enable property is FALSE, then this state machine is not evaluated."
29    event_detection_enable: bool,
30    pub(crate) event_history: EventHistory,
31}
32
33impl MultiStateInputObject {
34    pub fn new(
35        instance: u32,
36        name: impl Into<String>,
37        number_of_states: u32,
38    ) -> Result<Self, Error> {
39        let oid = ObjectIdentifier::new(ObjectType::MULTI_STATE_INPUT, instance)?;
40        require_nonzero_states(number_of_states)?;
41        Ok(Self {
42            oid,
43            name: name.into(),
44            description: String::new(),
45            present_value: 1,
46            number_of_states,
47            out_of_service: false,
48            status_flags: StatusFlags::empty(),
49            reliability: 0,
50            reliability_before_out_of_service: None,
51            reliability_inhibit: common::ReliabilityInhibitState::default(),
52            reliability_evaluator: MultiStateReliabilityState::default(),
53            state_text: (1..=number_of_states)
54                .map(|i| format!("State {i}"))
55                .collect(),
56            event_detector: ChangeOfStateDetector::default(),
57            event_detection_enable: true,
58            event_history: EventHistory::default(),
59        })
60    }
61
62    /// Set the alarm values (states that trigger OFFNORMAL).
63    pub fn set_alarm_values(&mut self, values: Vec<u32>) {
64        self.event_detector.alarm_values = values;
65    }
66
67    /// Mutate `Present_Value` on an unattached or otherwise application-owned object.
68    ///
69    /// This low-level helper bypasses running-server `Out_Of_Service` ownership,
70    /// range validation, intrinsic-event processing, and COV processing, while
71    /// retaining this object's reliability recomputation. Applications updating
72    /// a live object should use `BACnetServer::set_present_value_local`.
73    pub fn set_present_value(&mut self, value: u32) {
74        self.present_value = value;
75        let _ = self.recompute_reliability();
76    }
77
78    /// Validate and store a `Present_Value` write, without any access check.
79    ///
80    /// Shared by the network and internal routes, which differ only in the
81    /// `Out_Of_Service` condition each requires.
82    fn apply_present_value(&mut self, value: PropertyValue) -> Result<(), Error> {
83        let PropertyValue::Unsigned(v) = value else {
84            return Err(common::invalid_data_type_error());
85        };
86        if v < 1 || v > self.number_of_states as u64 {
87            return Err(common::value_out_of_range_error());
88        }
89        self.present_value = v as u32;
90        let _ = self.recompute_reliability();
91        Ok(())
92    }
93
94    /// Change the locally configured number of states.
95    ///
96    /// The BACnet `Number_Of_States` property remains read-only. A successful
97    /// local change retains the State_Text prefix, truncates its tail on
98    /// shrink, appends constructor-style labels on growth, and immediately
99    /// re-evaluates Reliability without repairing Present_Value.
100    pub fn set_number_of_states(&mut self, number_of_states: u32) -> Result<(), Error> {
101        resize_state_text(
102            &mut self.number_of_states,
103            &mut self.state_text,
104            number_of_states,
105        )?;
106        let _ = self.recompute_reliability();
107        Ok(())
108    }
109
110    /// Set the description string.
111    pub fn set_description(&mut self, desc: impl Into<String>) {
112        self.description = desc.into();
113    }
114
115    fn recompute_reliability(&mut self) -> ReliabilityEvaluation {
116        if self.out_of_service || self.reliability_inhibit.enabled() {
117            return ReliabilityEvaluation::Unchanged;
118        }
119        self.reliability_evaluator.evaluate(
120            false,
121            self.present_value,
122            self.number_of_states,
123            &mut self.reliability,
124        )
125    }
126}
127
128impl BACnetObject for MultiStateInputObject {
129    fn object_identifier(&self) -> ObjectIdentifier {
130        self.oid
131    }
132
133    fn object_name(&self) -> &str {
134        &self.name
135    }
136
137    fn supports_cov(&self) -> bool {
138        true
139    }
140
141    crate::event::impl_builtin_intrinsic_reporting!(
142        event_detector,
143        event_history,
144        [present_value],
145        reliability,
146        event_detection_enable,
147        ChangeOfStateDetector::ALGORITHM
148    );
149    impl_intrinsic_write_rollback!(
150        event_detector,
151        event_detection_enable,
152        event_history,
153        reliability_inhibit,
154        reliability,
155        out_of_service,
156        reliability_before_out_of_service;
157        reliability_evaluator
158    );
159
160    fn acknowledge_alarm_correlated_internal(
161        &mut self,
162        event_state: EventState,
163        timestamp: &BACnetTimeStamp,
164    ) -> Result<(), Error> {
165        if !self.event_detection_enable {
166            return Err(Error::Protocol {
167                class: ErrorClass::OBJECT.to_raw() as u32,
168                code: ErrorCode::NO_ALARM_CONFIGURED.to_raw() as u32,
169            });
170        }
171        self.event_history.acknowledge_correlated(
172            &mut self.event_detector.acked_transitions,
173            event_state,
174            timestamp,
175        )
176    }
177
178    fn read_property(
179        &self,
180        property: PropertyIdentifier,
181        array_index: Option<u32>,
182    ) -> Result<PropertyValue, Error> {
183        if property == PropertyIdentifier::STATUS_FLAGS {
184            return Ok(common::compute_status_flags(
185                self.status_flags,
186                self.reliability,
187                self.out_of_service,
188                self.event_detector.event_state.to_raw(),
189            ));
190        }
191        if let Some(value) = self.reliability_inhibit.read(property) {
192            return Ok(value);
193        }
194        if let Some(result) = read_common_properties!(self, property, array_index) {
195            return result;
196        }
197        if property == PropertyIdentifier::EVENT_DETECTION_ENABLE {
198            return Ok(PropertyValue::Boolean(self.event_detection_enable));
199        }
200        if let Some(result) = read_generic_event_properties!(self, property) {
201            return result;
202        }
203        if let Some(result) = self.event_history.read(property, array_index) {
204            return result;
205        }
206        match property {
207            p if p == PropertyIdentifier::OBJECT_TYPE => Ok(PropertyValue::Enumerated(
208                ObjectType::MULTI_STATE_INPUT.to_raw(),
209            )),
210            p if p == PropertyIdentifier::PRESENT_VALUE => {
211                Ok(PropertyValue::Unsigned(self.present_value as u64))
212            }
213            p if p == PropertyIdentifier::NUMBER_OF_STATES => {
214                Ok(PropertyValue::Unsigned(self.number_of_states as u64))
215            }
216            p if p == PropertyIdentifier::STATE_TEXT => match array_index {
217                None => Ok(PropertyValue::List(
218                    self.state_text
219                        .iter()
220                        .map(|s| PropertyValue::CharacterString(s.clone()))
221                        .collect(),
222                )),
223                Some(0) => Ok(PropertyValue::Unsigned(self.state_text.len() as u64)),
224                Some(idx) if idx >= 1 && (idx as usize) <= self.state_text.len() => Ok(
225                    PropertyValue::CharacterString(self.state_text[(idx - 1) as usize].clone()),
226                ),
227                _ => Err(common::invalid_array_index_error()),
228            },
229            p if p == PropertyIdentifier::ALARM_VALUES => Ok(PropertyValue::List(
230                self.event_detector
231                    .alarm_values
232                    .iter()
233                    .map(|v| PropertyValue::Unsigned(*v as u64))
234                    .collect(),
235            )),
236            _ => Err(common::unknown_property_error()),
237        }
238    }
239
240    fn write_property(
241        &mut self,
242        property: PropertyIdentifier,
243        array_index: Option<u32>,
244        value: PropertyValue,
245        _priority: Option<u8>,
246    ) -> Result<(), Error> {
247        if property == PropertyIdentifier::PRESENT_VALUE {
248            if !self.out_of_service {
249                return Err(common::write_access_denied_error());
250            }
251            return self.apply_present_value(value);
252        }
253        if property == PropertyIdentifier::STATE_TEXT {
254            match array_index {
255                Some(idx) if idx >= 1 && (idx as usize) <= self.state_text.len() => {
256                    if let PropertyValue::CharacterString(s) = value {
257                        self.state_text[(idx - 1) as usize] = s;
258                        return Ok(());
259                    }
260                    return Err(common::invalid_data_type_error());
261                }
262                None => return Err(common::write_access_denied_error()),
263                _ => return Err(common::invalid_array_index_error()),
264            }
265        }
266        if property == PropertyIdentifier::ALARM_VALUES {
267            let values = decode_alarm_values_write(array_index, value)?;
268            self.event_detector.alarm_values = values;
269            return Ok(());
270        }
271        if property == PropertyIdentifier::EVENT_DETECTION_ENABLE {
272            if let PropertyValue::Boolean(v) = value {
273                self.event_detection_enable = v;
274                if !v {
275                    self.event_detector.event_state = bacnet_types::enums::EventState::NORMAL;
276                    self.event_detector.acked_transitions = 0b111;
277                    self.event_detector.pending = None;
278                    self.event_detector.fault_reliability = None;
279                    self.event_history.reset();
280                }
281                return Ok(());
282            }
283            return Err(common::invalid_data_type_error());
284        }
285        if let Some(result) = write_generic_event_properties!(self, property, value) {
286            return result;
287        }
288        if let Some(result) = self.reliability_inhibit.write_inhibit(
289            &mut self.reliability,
290            self.out_of_service,
291            property,
292            &value,
293        ) {
294            result?;
295            let _ = self.recompute_reliability();
296            return Ok(());
297        }
298        if let Some(result) = self.reliability_inhibit.write_out_of_service(
299            &mut self.out_of_service,
300            &mut self.reliability,
301            &mut self.reliability_before_out_of_service,
302            property,
303            &value,
304        ) {
305            result?;
306            let _ = self.recompute_reliability();
307            return Ok(());
308        }
309        if let Some(result) = common::write_object_name(&mut self.name, property, &value) {
310            return result;
311        }
312        if let Some(result) = common::write_description(&mut self.description, property, &value) {
313            return result;
314        }
315        // Clause 12.18, while Out_Of_Service is TRUE: "the Present_Value property and
316        // the Reliability property, if present and capable of taking on values other
317        // than NO_FAULT_DETECTED, shall be writable to allow simulating specific
318        // conditions or for testing purposes".
319        // `is_writable_property` stays statically true because it describes capability.
320        if let Some(result) = self.reliability_inhibit.write_client_reliability(
321            self.out_of_service,
322            &mut self.reliability,
323            property,
324            &value,
325        ) {
326            return result;
327        }
328        Err(common::write_access_denied_error())
329    }
330
331    fn property_list(&self) -> Cow<'static, [PropertyIdentifier]> {
332        static PROPS: &[PropertyIdentifier] = &[
333            PropertyIdentifier::OBJECT_IDENTIFIER,
334            PropertyIdentifier::OBJECT_NAME,
335            PropertyIdentifier::DESCRIPTION,
336            PropertyIdentifier::OBJECT_TYPE,
337            PropertyIdentifier::PRESENT_VALUE,
338            PropertyIdentifier::STATUS_FLAGS,
339            PropertyIdentifier::EVENT_STATE,
340            PropertyIdentifier::EVENT_DETECTION_ENABLE,
341            PropertyIdentifier::EVENT_ENABLE,
342            PropertyIdentifier::TIME_DELAY,
343            PropertyIdentifier::TIME_DELAY_NORMAL,
344            PropertyIdentifier::NOTIFY_TYPE,
345            PropertyIdentifier::NOTIFICATION_CLASS,
346            PropertyIdentifier::ACKED_TRANSITIONS,
347            PropertyIdentifier::EVENT_TIME_STAMPS,
348            PropertyIdentifier::EVENT_MESSAGE_TEXTS,
349            PropertyIdentifier::OUT_OF_SERVICE,
350            PropertyIdentifier::NUMBER_OF_STATES,
351            PropertyIdentifier::RELIABILITY,
352            PropertyIdentifier::RELIABILITY_EVALUATION_INHIBIT,
353            PropertyIdentifier::STATE_TEXT,
354            PropertyIdentifier::ALARM_VALUES,
355        ];
356        Cow::Borrowed(PROPS)
357    }
358
359    fn is_createable(&self) -> bool {
360        true
361    }
362    fn set_reliability_internal(&mut self, reliability: u32) -> Result<(), Error> {
363        if self.out_of_service || self.reliability_inhibit.enabled() {
364            return Err(common::write_access_denied_error());
365        }
366        if !common::is_reliability_value_valid(reliability) {
367            return Err(common::value_out_of_range_error());
368        }
369        self.reliability = reliability;
370        self.reliability_evaluator.clear_ownership();
371        Ok(())
372    }
373
374    fn set_present_value_internal(&mut self, value: PropertyValue) -> Result<(), Error> {
375        // Local safe-ownership policy: preserve the client's OOS simulation.
376        if self.out_of_service {
377            return Err(common::write_access_denied_error());
378        }
379        self.apply_present_value(value)
380    }
381
382    fn evaluate_reliability_internal(&mut self) -> Result<ReliabilityEvaluation, Error> {
383        Ok(self.recompute_reliability())
384    }
385
386    fn reliability_evaluation_inhibited_internal(&self) -> bool {
387        self.reliability_inhibit.enabled()
388    }
389
390    fn is_writable_property(&self, property: PropertyIdentifier) -> bool {
391        // Mirrors the MultiStateInput `write_property` arms. The generic event
392        // set became writable with #229: Clause 12.18 requires the supported
393        // Event_Enable value set to include (T, T, T), and these detectors
394        // default to (F, F, F) with, previously, no commissioning path at all.
395        common::is_multistate_input_writable(property)
396            || common::is_generic_event_property_writable(property)
397            || property == PropertyIdentifier::RELIABILITY
398            || property == PropertyIdentifier::RELIABILITY_EVALUATION_INHIBIT
399            || property == PropertyIdentifier::EVENT_DETECTION_ENABLE
400            || property == PropertyIdentifier::ALARM_VALUES
401    }
402}
403
404#[cfg(test)]
405mod detection_enable_tests {
406    use super::*;
407
408    #[test]
409    fn msi_detection_enable_resets_and_gates_intrinsic_reporting() {
410        let mut msi = MultiStateInputObject::new(1, "MSI-1", 3).unwrap();
411        assert_eq!(
412            msi.read_property(PropertyIdentifier::EVENT_DETECTION_ENABLE, None)
413                .unwrap(),
414            PropertyValue::Boolean(true)
415        );
416        msi.set_alarm_values(vec![2]);
417        msi.event_detector.time_delay = 2;
418        msi.set_present_value(2);
419        assert_eq!(msi.evaluate_intrinsic_reporting(), None);
420        assert!(msi.event_detector.pending.is_some());
421
422        msi.event_detector.event_state = bacnet_types::enums::EventState::OFFNORMAL;
423        msi.event_detector.acked_transitions = 0;
424        msi.event_detector.fault_reliability = Some(1);
425        msi.write_property(
426            PropertyIdentifier::EVENT_DETECTION_ENABLE,
427            None,
428            PropertyValue::Boolean(false),
429            None,
430        )
431        .unwrap();
432
433        assert_eq!(
434            msi.read_property(PropertyIdentifier::EVENT_DETECTION_ENABLE, None)
435                .unwrap(),
436            PropertyValue::Boolean(false)
437        );
438        assert_eq!(
439            msi.read_property(PropertyIdentifier::EVENT_STATE, None)
440                .unwrap(),
441            PropertyValue::Enumerated(bacnet_types::enums::EventState::NORMAL.to_raw())
442        );
443        assert_eq!(
444            msi.read_property(PropertyIdentifier::ACKED_TRANSITIONS, None)
445                .unwrap(),
446            PropertyValue::BitString {
447                unused_bits: 5,
448                data: vec![0xe0],
449            }
450        );
451        assert!(msi.event_detector.pending.is_none());
452        assert!(msi.event_detector.fault_reliability.is_none());
453        assert_eq!(msi.evaluate_intrinsic_reporting(), None);
454        assert_eq!(msi.tick_intrinsic_reporting(), None);
455        assert!(
456            msi.event_detector.pending.is_none(),
457            "evaluate/tick re-armed a countdown while detection is disabled"
458        );
459        assert!(msi
460            .property_list()
461            .contains(&PropertyIdentifier::EVENT_DETECTION_ENABLE));
462        assert!(msi.is_writable_property(PropertyIdentifier::EVENT_DETECTION_ENABLE));
463    }
464}
465
466#[cfg(test)]
467mod reliability_safety_net_tests {
468    use super::*;
469    use crate::traits::ReliabilityEvaluation;
470    use bacnet_types::enums::Reliability;
471
472    #[test]
473    fn periodic_hook_repairs_one_stale_bypassed_state_then_is_unchanged() {
474        let mut msi = MultiStateInputObject::new(1, "MSI-stale", 2).unwrap();
475        msi.present_value = 3;
476
477        assert_eq!(
478            msi.evaluate_reliability_internal().unwrap(),
479            ReliabilityEvaluation::Changed {
480                old_reliability: Reliability::NO_FAULT_DETECTED.to_raw(),
481                new_reliability: Reliability::MULTI_STATE_OUT_OF_RANGE.to_raw(),
482            }
483        );
484        assert_eq!(
485            msi.evaluate_reliability_internal().unwrap(),
486            ReliabilityEvaluation::Unchanged
487        );
488    }
489}