Skip to main content

bacnet_objects/lighting/
binary.rs

1use std::borrow::Cow;
2use std::sync::Arc;
3use std::time::Duration;
4
5use bacnet_types::enums::{ObjectType, PropertyIdentifier, Reliability};
6use bacnet_types::error::Error;
7use bacnet_types::primitives::{ObjectIdentifier, PropertyValue, StatusFlags};
8
9use crate::common::{self, read_common_properties, read_priority_array};
10use crate::traits::{BACnetObject, MonotonicClock};
11
12const OFF: u32 = 0;
13const ON: u32 = 1;
14
15#[derive(Debug, Clone, Copy, PartialEq, Eq)]
16enum OperationKind {
17    WarnOff,
18    WarnRelinquish,
19}
20
21#[derive(Debug, Clone, Copy, PartialEq, Eq)]
22struct ActiveOperation {
23    kind: OperationKind,
24    priority: u8,
25    deadline: Duration,
26}
27
28#[derive(Debug, Clone, Copy, PartialEq, Eq)]
29enum PresentValueCommand {
30    Set(Option<u32>),
31    Warn,
32    WarnOff,
33    WarnRelinquish,
34    Stop,
35}
36
37/// BACnet Binary Lighting Output object.
38///
39/// The priority array stores only steady OFF/ON values. WARN, WARN_OFF,
40/// WARN_RELINQUISH, and STOP are command operations interpreted at write time.
41#[derive(Clone)]
42pub struct BinaryLightingOutputObject {
43    oid: ObjectIdentifier,
44    name: String,
45    description: String,
46    present_value: u32,
47    blink_warn_enable: bool,
48    egress_time: u32,
49    active_operation: Option<ActiveOperation>,
50    blink_request_count: u64,
51    monotonic_clock: Option<Arc<MonotonicClock>>,
52    logical_now: Duration,
53    out_of_service: bool,
54    status_flags: StatusFlags,
55    /// Reliability; NO_FAULT_DETECTED until a fault is evaluated or simulated.
56    reliability: Reliability,
57    priority_array: [Option<u32>; 16],
58    relinquish_default: u32,
59}
60
61impl BinaryLightingOutputObject {
62    /// Create a new Binary Lighting Output object with no reconstructed egress.
63    pub fn new(instance: u32, name: impl Into<String>) -> Result<Self, Error> {
64        Ok(Self {
65            oid: ObjectIdentifier::new(ObjectType::BINARY_LIGHTING_OUTPUT, instance)?,
66            name: name.into(),
67            description: String::new(),
68            present_value: OFF,
69            blink_warn_enable: false,
70            egress_time: 0,
71            active_operation: None,
72            blink_request_count: 0,
73            monotonic_clock: None,
74            logical_now: Duration::ZERO,
75            out_of_service: false,
76            status_flags: StatusFlags::empty(),
77            reliability: Reliability::NO_FAULT_DETECTED,
78            priority_array: [None; 16],
79            relinquish_default: OFF,
80        })
81    }
82
83    /// Set the description string.
84    pub fn set_description(&mut self, desc: impl Into<String>) {
85        self.description = desc.into();
86    }
87
88    /// Set Relinquish_Default to OFF (0) or ON (1) (#270, #283).
89    pub fn set_relinquish_default(&mut self, value: u32) -> Result<(), Error> {
90        if !matches!(value, OFF | ON) {
91            return Err(common::value_out_of_range_error());
92        }
93        self.relinquish_default = value;
94        self.recalculate_present_value();
95        Ok(())
96    }
97
98    fn recalculate_present_value(&mut self) {
99        self.present_value =
100            common::recalculate_from_priority_array(&self.priority_array, self.relinquish_default);
101    }
102
103    fn validate_present_value_command(
104        value: PropertyValue,
105        priority: Option<u8>,
106    ) -> Result<(u8, PresentValueCommand), Error> {
107        let priority = priority.unwrap_or(16);
108        if !(1..=16).contains(&priority) {
109            return Err(common::value_out_of_range_error());
110        }
111        let command = match value {
112            PropertyValue::Null => PresentValueCommand::Set(None),
113            PropertyValue::Enumerated(OFF) => PresentValueCommand::Set(Some(OFF)),
114            PropertyValue::Enumerated(ON) => PresentValueCommand::Set(Some(ON)),
115            PropertyValue::Enumerated(2) => PresentValueCommand::Warn,
116            PropertyValue::Enumerated(3) => PresentValueCommand::WarnOff,
117            PropertyValue::Enumerated(4) => PresentValueCommand::WarnRelinquish,
118            PropertyValue::Enumerated(5) => PresentValueCommand::Stop,
119            PropertyValue::Enumerated(_) => return Err(common::value_out_of_range_error()),
120            _ => return Err(common::invalid_data_type_error()),
121        };
122        Ok((priority, command))
123    }
124
125    fn highest_active_priority(&self) -> Option<u8> {
126        self.priority_array
127            .iter()
128            .position(Option::is_some)
129            .map(|index| index as u8 + 1)
130    }
131
132    fn priority_is_highest_on(&self, priority: u8) -> bool {
133        self.highest_active_priority() == Some(priority)
134            && self.priority_array[(priority - 1) as usize] == Some(ON)
135    }
136
137    fn next_effective_value(&self, priority: u8) -> u32 {
138        self.priority_array[priority as usize..]
139            .iter()
140            .flatten()
141            .next()
142            .copied()
143            .unwrap_or(self.relinquish_default)
144    }
145
146    fn request_blink(&mut self) {
147        self.blink_request_count = self.blink_request_count.saturating_add(1);
148    }
149
150    fn apply_terminal(&mut self, operation: ActiveOperation) {
151        let slot = &mut self.priority_array[(operation.priority - 1) as usize];
152        match operation.kind {
153            OperationKind::WarnOff => *slot = Some(OFF),
154            OperationKind::WarnRelinquish => *slot = None,
155        }
156        self.recalculate_present_value();
157    }
158
159    fn complete_active_operation(&mut self) {
160        if let Some(operation) = self.active_operation.take() {
161            self.apply_terminal(operation);
162        }
163    }
164
165    fn complete_active_operation_for_write(&mut self, priority: u8) {
166        if self
167            .active_operation
168            .is_some_and(|active| priority <= active.priority)
169        {
170            self.complete_active_operation();
171        }
172    }
173
174    fn arm_operation(&mut self, kind: OperationKind, priority: u8) {
175        let duration = Duration::from_secs(self.egress_time as u64);
176        let operation = ActiveOperation {
177            kind,
178            priority,
179            deadline: self.monotonic_now().saturating_add(duration),
180        };
181        if duration.is_zero() {
182            self.apply_terminal(operation);
183        } else {
184            self.active_operation = Some(operation);
185        }
186    }
187
188    fn write_present_value(&mut self, priority: u8, command: PresentValueCommand) {
189        if command != PresentValueCommand::Stop {
190            self.complete_active_operation_for_write(priority);
191        }
192
193        let index = (priority - 1) as usize;
194        match command {
195            PresentValueCommand::Set(value) => {
196                self.priority_array[index] = value;
197                self.recalculate_present_value();
198            }
199            PresentValueCommand::Warn => {
200                if self.blink_warn_enable && self.priority_is_highest_on(priority) {
201                    self.request_blink();
202                }
203            }
204            PresentValueCommand::WarnOff => {
205                if self.blink_warn_enable && self.priority_is_highest_on(priority) {
206                    self.request_blink();
207                    self.arm_operation(OperationKind::WarnOff, priority);
208                } else {
209                    self.priority_array[index] = Some(OFF);
210                    self.recalculate_present_value();
211                }
212            }
213            PresentValueCommand::WarnRelinquish => {
214                let eligible = self.blink_warn_enable
215                    && self.priority_is_highest_on(priority)
216                    && self.next_effective_value(priority) != ON;
217                if eligible {
218                    self.request_blink();
219                    self.arm_operation(OperationKind::WarnRelinquish, priority);
220                } else {
221                    self.priority_array[index] = None;
222                    self.recalculate_present_value();
223                }
224            }
225            PresentValueCommand::Stop => {
226                if self
227                    .active_operation
228                    .is_some_and(|active| active.priority == priority)
229                {
230                    self.active_operation = None;
231                }
232            }
233        }
234    }
235
236    fn monotonic_now(&self) -> Duration {
237        self.monotonic_clock
238            .as_ref()
239            .map_or(self.logical_now, |clock| clock())
240    }
241
242    fn expire_at(&mut self, now: Duration) -> bool {
243        let Some(operation) = self.active_operation else {
244            return false;
245        };
246        if now < operation.deadline {
247            return false;
248        }
249        self.active_operation = None;
250        self.apply_terminal(operation);
251        true
252    }
253}
254
255impl BACnetObject for BinaryLightingOutputObject {
256    fn object_identifier(&self) -> ObjectIdentifier {
257        self.oid
258    }
259
260    fn object_name(&self) -> &str {
261        &self.name
262    }
263
264    fn read_property(
265        &self,
266        property: PropertyIdentifier,
267        array_index: Option<u32>,
268    ) -> Result<PropertyValue, Error> {
269        if let Some(result) = read_common_properties!(self, property, array_index) {
270            return result;
271        }
272        match property {
273            PropertyIdentifier::OBJECT_TYPE => Ok(PropertyValue::Enumerated(
274                ObjectType::BINARY_LIGHTING_OUTPUT.to_raw(),
275            )),
276            PropertyIdentifier::PRESENT_VALUE => Ok(PropertyValue::Enumerated(self.present_value)),
277            PropertyIdentifier::BLINK_WARN_ENABLE => {
278                Ok(PropertyValue::Boolean(self.blink_warn_enable))
279            }
280            PropertyIdentifier::EGRESS_TIME => Ok(PropertyValue::Unsigned(self.egress_time as u64)),
281            PropertyIdentifier::EGRESS_ACTIVE => {
282                Ok(PropertyValue::Boolean(self.active_operation.is_some()))
283            }
284            PropertyIdentifier::PRIORITY_ARRAY => {
285                read_priority_array!(self, array_index, PropertyValue::Enumerated)
286            }
287            PropertyIdentifier::RELINQUISH_DEFAULT => {
288                Ok(PropertyValue::Enumerated(self.relinquish_default))
289            }
290            PropertyIdentifier::CURRENT_COMMAND_PRIORITY => {
291                Ok(common::current_command_priority(&self.priority_array))
292            }
293            _ => Err(common::unknown_property_error()),
294        }
295    }
296
297    fn write_property(
298        &mut self,
299        property: PropertyIdentifier,
300        _array_index: Option<u32>,
301        value: PropertyValue,
302        priority: Option<u8>,
303    ) -> Result<(), Error> {
304        if property == PropertyIdentifier::PRESENT_VALUE {
305            let (priority, command) = Self::validate_present_value_command(value, priority)?;
306            self.write_present_value(priority, command);
307            return Ok(());
308        }
309        if property == PropertyIdentifier::BLINK_WARN_ENABLE {
310            if let PropertyValue::Boolean(value) = value {
311                self.blink_warn_enable = value;
312                return Ok(());
313            }
314            return Err(common::invalid_data_type_error());
315        }
316        if property == PropertyIdentifier::EGRESS_TIME {
317            if let PropertyValue::Unsigned(value) = value {
318                self.egress_time = common::u64_to_u32(value)?;
319                return Ok(());
320            }
321            return Err(common::invalid_data_type_error());
322        }
323        if property == PropertyIdentifier::RELINQUISH_DEFAULT {
324            if let PropertyValue::Enumerated(value) = value {
325                return self.set_relinquish_default(value);
326            }
327            return Err(common::invalid_data_type_error());
328        }
329        if let Some(result) =
330            common::write_out_of_service(&mut self.out_of_service, property, &value)
331        {
332            return result;
333        }
334        if let Some(result) = common::write_description(&mut self.description, property, &value) {
335            return result;
336        }
337        Err(crate::common::unhandled_write_error(
338            self.property_metadata().as_ref(),
339            property,
340            _array_index,
341        ))
342    }
343
344    fn property_list(&self) -> Cow<'static, [PropertyIdentifier]> {
345        crate::property_metadata::property_list_from_metadata(self.property_metadata().as_ref())
346    }
347
348    fn property_metadata(&self) -> Cow<'_, [crate::property_metadata::PropertyMetadata]> {
349        super::metadata::for_binary_lighting_output_object(self)
350    }
351
352    fn supports_cov(&self) -> bool {
353        true
354    }
355
356    fn advance_time_internal(&mut self, elapsed: Duration) -> bool {
357        self.logical_now = self.logical_now.saturating_add(elapsed);
358        self.expire_at(self.logical_now)
359    }
360
361    fn bind_monotonic_clock_internal(&mut self, clock: Option<Arc<MonotonicClock>>) {
362        self.monotonic_clock = clock;
363    }
364
365    fn advance_monotonic_time_internal(&mut self, now: Duration) -> bool {
366        self.expire_at(now)
367    }
368
369    fn next_monotonic_deadline_internal(&self) -> Option<Duration> {
370        self.active_operation.map(|operation| operation.deadline)
371    }
372
373    fn cov_snapshot_internal(&self) -> Option<Box<dyn BACnetObject>> {
374        Some(Box::new(self.clone()))
375    }
376
377    fn lighting_blink_count_internal(&self) -> u64 {
378        self.blink_request_count
379    }
380}
381
382#[cfg(test)]
383#[path = "binary_tests.rs"]
384mod tests;
385
386#[cfg(test)]
387#[path = "binary_direct_tests.rs"]
388mod direct_tests;