Skip to main content

bacnet_objects/binary/
output.rs

1use super::*;
2
3// ---------------------------------------------------------------------------
4// BinaryOutput (type 4)
5// ---------------------------------------------------------------------------
6
7/// BACnet Binary Output object.
8///
9/// Commandable binary output with 16-level priority array.
10/// Uses Enumerated values: 0 = inactive, 1 = active.
11pub struct BinaryOutputObject {
12    oid: ObjectIdentifier,
13    name: String,
14    description: String,
15    present_value: u32,
16    feedback_value: u32,
17    out_of_service: bool,
18    status_flags: StatusFlags,
19    priority_array: [Option<u32>; 16],
20    relinquish_default: u32,
21    /// Polarity: 0 = normal, 1 = reverse.
22    polarity: u32,
23    /// Reliability: 0 = NO_FAULT_DETECTED.
24    reliability: u32,
25    reliability_before_out_of_service: Option<u32>,
26    reliability_inhibit: common::ReliabilityInhibitState,
27    event_detection_enable: bool,
28    active_text: String,
29    inactive_text: String,
30    /// COMMAND_FAILURE event detector.
31    event_detector: CommandFailureDetector,
32    pub(crate) event_history: EventHistory,
33    /// Value source tracking (optional per spec — exposed via VALUE_SOURCE property).
34    value_source: common::ValueSourceTracking,
35}
36
37impl BinaryOutputObject {
38    pub fn new(instance: u32, name: impl Into<String>) -> Result<Self, Error> {
39        let oid = ObjectIdentifier::new(ObjectType::BINARY_OUTPUT, instance)?;
40        Ok(Self {
41            oid,
42            name: name.into(),
43            description: String::new(),
44            present_value: 0,
45            feedback_value: 0,
46            out_of_service: false,
47            status_flags: StatusFlags::empty(),
48            priority_array: [None; 16],
49            relinquish_default: 0,
50            polarity: 0,
51            reliability: 0,
52            reliability_before_out_of_service: None,
53            reliability_inhibit: common::ReliabilityInhibitState::default(),
54            event_detection_enable: false,
55            active_text: "Active".into(),
56            inactive_text: "Inactive".into(),
57            event_detector: CommandFailureDetector::default(),
58            event_history: EventHistory::default(),
59            value_source: common::ValueSourceTracking::default(),
60        })
61    }
62
63    /// Set the description string.
64    pub fn set_description(&mut self, desc: impl Into<String>) {
65        self.description = desc.into();
66    }
67
68    fn recalculate_present_value(&mut self) {
69        self.present_value =
70            common::recalculate_from_priority_array(&self.priority_array, self.relinquish_default);
71    }
72
73    /// Set the Relinquish_Default (#270).
74    ///
75    /// Validated the same way a commanded Present_Value is (BinaryPV 0 or 1);
76    /// after the store, Present_Value is resolved anew from the priority
77    /// array so an empty array falls back to the new default immediately.
78    pub fn set_relinquish_default(&mut self, value: u32) -> Result<(), Error> {
79        if value > 1 {
80            return Err(common::value_out_of_range_error());
81        }
82        self.relinquish_default = value;
83        self.recalculate_present_value();
84        Ok(())
85    }
86}
87
88impl BACnetObject for BinaryOutputObject {
89    fn object_identifier(&self) -> ObjectIdentifier {
90        self.oid
91    }
92
93    fn object_name(&self) -> &str {
94        &self.name
95    }
96
97    fn supports_cov(&self) -> bool {
98        true
99    }
100
101    crate::event::impl_builtin_intrinsic_reporting!(
102        event_detector,
103        event_history,
104        [present_value, feedback_value],
105        reliability,
106        event_detection_enable,
107        CommandFailureDetector::ALGORITHM
108    );
109    impl_intrinsic_write_rollback!(
110        event_detector,
111        event_detection_enable,
112        event_history,
113        reliability_inhibit,
114        reliability,
115        out_of_service,
116        reliability_before_out_of_service
117    );
118
119    fn acknowledge_alarm_correlated_internal(
120        &mut self,
121        event_state: EventState,
122        timestamp: &BACnetTimeStamp,
123    ) -> Result<(), Error> {
124        if !self.event_detection_enable {
125            return Err(Error::Protocol {
126                class: ErrorClass::OBJECT.to_raw() as u32,
127                code: ErrorCode::NO_ALARM_CONFIGURED.to_raw() as u32,
128            });
129        }
130        self.event_history.acknowledge_correlated(
131            &mut self.event_detector.acked_transitions,
132            event_state,
133            timestamp,
134        )
135    }
136
137    fn read_property(
138        &self,
139        property: PropertyIdentifier,
140        array_index: Option<u32>,
141    ) -> Result<PropertyValue, Error> {
142        if property == PropertyIdentifier::STATUS_FLAGS {
143            return Ok(common::compute_status_flags(
144                self.status_flags,
145                self.reliability,
146                self.out_of_service,
147                self.event_detector.event_state.to_raw(),
148            ));
149        }
150        if property == PropertyIdentifier::EVENT_DETECTION_ENABLE {
151            return Ok(PropertyValue::Boolean(self.event_detection_enable));
152        }
153        if let Some(value) = self.reliability_inhibit.read(property) {
154            return Ok(value);
155        }
156        if let Some(result) = read_common_properties!(self, property, array_index) {
157            return result;
158        }
159        if let Some(result) = read_generic_event_properties!(self, property) {
160            return result;
161        }
162        if let Some(result) = self.event_history.read(property, array_index) {
163            return result;
164        }
165        match property {
166            p if p == PropertyIdentifier::OBJECT_TYPE => Ok(PropertyValue::Enumerated(
167                ObjectType::BINARY_OUTPUT.to_raw(),
168            )),
169            p if p == PropertyIdentifier::PRESENT_VALUE => {
170                Ok(PropertyValue::Enumerated(self.present_value))
171            }
172            p if p == PropertyIdentifier::FEEDBACK_VALUE => {
173                Ok(PropertyValue::Enumerated(self.feedback_value))
174            }
175            p if p == PropertyIdentifier::PRIORITY_ARRAY => {
176                common::read_priority_array!(self, array_index, PropertyValue::Enumerated)
177            }
178            p if p == PropertyIdentifier::RELINQUISH_DEFAULT => {
179                Ok(PropertyValue::Enumerated(self.relinquish_default))
180            }
181            p if p == PropertyIdentifier::CURRENT_COMMAND_PRIORITY => {
182                Ok(common::current_command_priority(&self.priority_array))
183            }
184            p if p == PropertyIdentifier::VALUE_SOURCE => {
185                Ok(self.value_source.value_source.clone())
186            }
187            p if p == PropertyIdentifier::LAST_COMMAND_TIME => Ok(PropertyValue::Unsigned(
188                match self.value_source.last_command_time {
189                    BACnetTimeStamp::SequenceNumber(n) => u64::from(n),
190                    _ => 0,
191                },
192            )),
193            p if p == PropertyIdentifier::POLARITY => Ok(PropertyValue::Enumerated(self.polarity)),
194            p if p == PropertyIdentifier::ACTIVE_TEXT => {
195                Ok(PropertyValue::CharacterString(self.active_text.clone()))
196            }
197            p if p == PropertyIdentifier::INACTIVE_TEXT => {
198                Ok(PropertyValue::CharacterString(self.inactive_text.clone()))
199            }
200            _ => Err(common::unknown_property_error()),
201        }
202    }
203
204    fn write_property(
205        &mut self,
206        property: PropertyIdentifier,
207        array_index: Option<u32>,
208        value: PropertyValue,
209        priority: Option<u8>,
210    ) -> Result<(), Error> {
211        common::write_priority_array_direct!(self, property, array_index, value, |v| {
212            if let PropertyValue::Enumerated(e) = v {
213                if e > 1 {
214                    Err(common::value_out_of_range_error())
215                } else {
216                    Ok(e)
217                }
218            } else {
219                Err(common::invalid_data_type_error())
220            }
221        });
222        if property == PropertyIdentifier::PRESENT_VALUE {
223            return common::write_priority_array!(self, value, priority, |v| {
224                if let PropertyValue::Enumerated(e) = v {
225                    if e > 1 {
226                        Err(common::value_out_of_range_error())
227                    } else {
228                        Ok(e)
229                    }
230                } else {
231                    Err(common::invalid_data_type_error())
232                }
233            });
234        }
235        if property == PropertyIdentifier::FEEDBACK_VALUE {
236            if let PropertyValue::Enumerated(e) = value {
237                if e > 1 {
238                    return Err(common::value_out_of_range_error());
239                }
240                self.feedback_value = e;
241                return Ok(());
242            }
243            return Err(common::invalid_data_type_error());
244        }
245        if property == PropertyIdentifier::ACTIVE_TEXT {
246            if let PropertyValue::CharacterString(s) = value {
247                self.active_text = s;
248                return Ok(());
249            }
250            return Err(common::invalid_data_type_error());
251        }
252        if property == PropertyIdentifier::INACTIVE_TEXT {
253            if let PropertyValue::CharacterString(s) = value {
254                self.inactive_text = s;
255                return Ok(());
256            }
257            return Err(common::invalid_data_type_error());
258        }
259        if property == PropertyIdentifier::EVENT_DETECTION_ENABLE {
260            if let PropertyValue::Boolean(v) = value {
261                self.event_detection_enable = v;
262                if !v {
263                    self.event_detector.event_state = bacnet_types::enums::EventState::NORMAL;
264                    self.event_detector.acked_transitions = 0b111;
265                    self.event_detector.pending = None;
266                    self.event_detector.fault_reliability = None;
267                    self.event_history.reset();
268                }
269                return Ok(());
270            }
271            return Err(common::invalid_data_type_error());
272        }
273        if property == PropertyIdentifier::RELINQUISH_DEFAULT {
274            if let PropertyValue::Enumerated(e) = value {
275                return self.set_relinquish_default(e);
276            }
277            return Err(common::invalid_data_type_error());
278        }
279        if let Some(result) = write_generic_event_properties!(self, property, value) {
280            return result;
281        }
282        if let Some(result) = self.reliability_inhibit.write_inhibit(
283            &mut self.reliability,
284            self.out_of_service,
285            property,
286            &value,
287        ) {
288            return result;
289        }
290        if let Some(result) = self.reliability_inhibit.write_out_of_service(
291            &mut self.out_of_service,
292            &mut self.reliability,
293            &mut self.reliability_before_out_of_service,
294            property,
295            &value,
296        ) {
297            return result;
298        }
299        if let Some(result) = common::write_object_name(&mut self.name, property, &value) {
300            return result;
301        }
302        if let Some(result) = common::write_description(&mut self.description, property, &value) {
303            return result;
304        }
305        // Clause 12.7, while Out_Of_Service is TRUE: "the Present_Value property and
306        // the Reliability property, if present and capable of taking on values other
307        // than NO_FAULT_DETECTED, shall be writable to allow simulating specific
308        // conditions or for testing purposes".
309        // `is_writable_property` stays statically true because it describes capability.
310        if let Some(result) = self.reliability_inhibit.write_client_reliability(
311            self.out_of_service,
312            &mut self.reliability,
313            property,
314            &value,
315        ) {
316            return result;
317        }
318        Err(common::write_access_denied_error())
319    }
320
321    fn property_list(&self) -> Cow<'static, [PropertyIdentifier]> {
322        static PROPS: &[PropertyIdentifier] = &[
323            PropertyIdentifier::OBJECT_IDENTIFIER,
324            PropertyIdentifier::OBJECT_NAME,
325            PropertyIdentifier::DESCRIPTION,
326            PropertyIdentifier::OBJECT_TYPE,
327            PropertyIdentifier::PRESENT_VALUE,
328            PropertyIdentifier::FEEDBACK_VALUE,
329            PropertyIdentifier::STATUS_FLAGS,
330            PropertyIdentifier::EVENT_STATE,
331            PropertyIdentifier::EVENT_DETECTION_ENABLE,
332            PropertyIdentifier::EVENT_ENABLE,
333            PropertyIdentifier::TIME_DELAY,
334            PropertyIdentifier::TIME_DELAY_NORMAL,
335            PropertyIdentifier::NOTIFY_TYPE,
336            PropertyIdentifier::NOTIFICATION_CLASS,
337            PropertyIdentifier::ACKED_TRANSITIONS,
338            PropertyIdentifier::EVENT_TIME_STAMPS,
339            PropertyIdentifier::EVENT_MESSAGE_TEXTS,
340            PropertyIdentifier::OUT_OF_SERVICE,
341            PropertyIdentifier::PRIORITY_ARRAY,
342            PropertyIdentifier::RELINQUISH_DEFAULT,
343            PropertyIdentifier::CURRENT_COMMAND_PRIORITY,
344            PropertyIdentifier::POLARITY,
345            PropertyIdentifier::RELIABILITY,
346            PropertyIdentifier::RELIABILITY_EVALUATION_INHIBIT,
347            PropertyIdentifier::ACTIVE_TEXT,
348            PropertyIdentifier::INACTIVE_TEXT,
349        ];
350        Cow::Borrowed(PROPS)
351    }
352
353    fn is_createable(&self) -> bool {
354        true
355    }
356
357    fn set_reliability_internal(&mut self, reliability: u32) -> Result<(), Error> {
358        if self.out_of_service || self.reliability_inhibit.enabled() {
359            return Err(common::write_access_denied_error());
360        }
361        if !common::is_reliability_value_valid(reliability) {
362            return Err(common::value_out_of_range_error());
363        }
364        self.reliability = reliability;
365        Ok(())
366    }
367
368    fn reliability_evaluation_inhibited_internal(&self) -> bool {
369        self.reliability_inhibit.enabled()
370    }
371
372    fn is_writable_property(&self, property: PropertyIdentifier) -> bool {
373        // Mirrors the BinaryOutput `write_property` arms: commandable
374        // (PRIORITY_ARRAY + PRESENT_VALUE) + common + text properties.
375        common::is_commandable_property_writable(property)
376            || common::is_common_writable(property)
377            || common::is_generic_event_property_writable(property)
378            || property == PropertyIdentifier::FEEDBACK_VALUE
379            || property == PropertyIdentifier::ACTIVE_TEXT
380            || property == PropertyIdentifier::INACTIVE_TEXT
381            || property == PropertyIdentifier::RELIABILITY
382            || property == PropertyIdentifier::RELIABILITY_EVALUATION_INHIBIT
383            || property == PropertyIdentifier::EVENT_DETECTION_ENABLE
384    }
385}
386
387#[cfg(test)]
388#[path = "tests/command_failure.rs"]
389mod command_failure_tests;