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};
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, WritePropertyRollback};
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#[derive(Clone)]
38struct CommandRollback {
39    present_value: u32,
40    blink_warn_enable: bool,
41    egress_time: u32,
42    priority_array: [Option<u32>; 16],
43    relinquish_default: u32,
44    active_operation: Option<ActiveOperation>,
45    blink_request_count: u64,
46    logical_now: Duration,
47}
48
49/// BACnet Binary Lighting Output object.
50///
51/// The priority array stores only steady OFF/ON values. WARN, WARN_OFF,
52/// WARN_RELINQUISH, and STOP are command operations interpreted at write time.
53#[derive(Clone)]
54pub struct BinaryLightingOutputObject {
55    oid: ObjectIdentifier,
56    name: String,
57    description: String,
58    present_value: u32,
59    blink_warn_enable: bool,
60    egress_time: u32,
61    active_operation: Option<ActiveOperation>,
62    blink_request_count: u64,
63    monotonic_clock: Option<Arc<MonotonicClock>>,
64    logical_now: Duration,
65    out_of_service: bool,
66    status_flags: StatusFlags,
67    /// Reliability: 0 = NO_FAULT_DETECTED.
68    reliability: u32,
69    priority_array: [Option<u32>; 16],
70    relinquish_default: u32,
71}
72
73impl BinaryLightingOutputObject {
74    /// Create a new Binary Lighting Output object with no reconstructed egress.
75    pub fn new(instance: u32, name: impl Into<String>) -> Result<Self, Error> {
76        Ok(Self {
77            oid: ObjectIdentifier::new(ObjectType::BINARY_LIGHTING_OUTPUT, instance)?,
78            name: name.into(),
79            description: String::new(),
80            present_value: OFF,
81            blink_warn_enable: false,
82            egress_time: 0,
83            active_operation: None,
84            blink_request_count: 0,
85            monotonic_clock: None,
86            logical_now: Duration::ZERO,
87            out_of_service: false,
88            status_flags: StatusFlags::empty(),
89            reliability: 0,
90            priority_array: [None; 16],
91            relinquish_default: OFF,
92        })
93    }
94
95    /// Set the description string.
96    pub fn set_description(&mut self, desc: impl Into<String>) {
97        self.description = desc.into();
98    }
99
100    /// Set Relinquish_Default to OFF (0) or ON (1) (#270, #283).
101    pub fn set_relinquish_default(&mut self, value: u32) -> Result<(), Error> {
102        if !matches!(value, OFF | ON) {
103            return Err(common::value_out_of_range_error());
104        }
105        self.relinquish_default = value;
106        self.recalculate_present_value();
107        Ok(())
108    }
109
110    fn recalculate_present_value(&mut self) {
111        self.present_value =
112            common::recalculate_from_priority_array(&self.priority_array, self.relinquish_default);
113    }
114
115    fn validate_present_value_command(
116        value: PropertyValue,
117        priority: Option<u8>,
118    ) -> Result<(u8, PresentValueCommand), Error> {
119        let priority = priority.unwrap_or(16);
120        if !(1..=16).contains(&priority) {
121            return Err(common::value_out_of_range_error());
122        }
123        let command = match value {
124            PropertyValue::Null => PresentValueCommand::Set(None),
125            PropertyValue::Enumerated(OFF) => PresentValueCommand::Set(Some(OFF)),
126            PropertyValue::Enumerated(ON) => PresentValueCommand::Set(Some(ON)),
127            PropertyValue::Enumerated(2) => PresentValueCommand::Warn,
128            PropertyValue::Enumerated(3) => PresentValueCommand::WarnOff,
129            PropertyValue::Enumerated(4) => PresentValueCommand::WarnRelinquish,
130            PropertyValue::Enumerated(5) => PresentValueCommand::Stop,
131            PropertyValue::Enumerated(_) => return Err(common::value_out_of_range_error()),
132            _ => return Err(common::invalid_data_type_error()),
133        };
134        Ok((priority, command))
135    }
136
137    fn validate_priority_array_write(
138        array_index: Option<u32>,
139        value: PropertyValue,
140    ) -> Result<(usize, Option<u32>), Error> {
141        let index = match array_index {
142            Some(index) if (1..=16).contains(&index) => (index - 1) as usize,
143            Some(_) => return Err(common::invalid_array_index_error()),
144            None => return Err(common::write_access_denied_error()),
145        };
146        let value = match value {
147            PropertyValue::Null => None,
148            PropertyValue::Enumerated(value @ (OFF | ON)) => Some(value),
149            PropertyValue::Enumerated(_) => return Err(common::value_out_of_range_error()),
150            _ => return Err(common::invalid_data_type_error()),
151        };
152        Ok((index, value))
153    }
154
155    fn highest_active_priority(&self) -> Option<u8> {
156        self.priority_array
157            .iter()
158            .position(Option::is_some)
159            .map(|index| index as u8 + 1)
160    }
161
162    fn priority_is_highest_on(&self, priority: u8) -> bool {
163        self.highest_active_priority() == Some(priority)
164            && self.priority_array[(priority - 1) as usize] == Some(ON)
165    }
166
167    fn next_effective_value(&self, priority: u8) -> u32 {
168        self.priority_array[priority as usize..]
169            .iter()
170            .flatten()
171            .next()
172            .copied()
173            .unwrap_or(self.relinquish_default)
174    }
175
176    fn request_blink(&mut self) {
177        self.blink_request_count = self.blink_request_count.saturating_add(1);
178    }
179
180    fn apply_terminal(&mut self, operation: ActiveOperation) {
181        let slot = &mut self.priority_array[(operation.priority - 1) as usize];
182        match operation.kind {
183            OperationKind::WarnOff => *slot = Some(OFF),
184            OperationKind::WarnRelinquish => *slot = None,
185        }
186        self.recalculate_present_value();
187    }
188
189    fn complete_active_operation(&mut self) {
190        if let Some(operation) = self.active_operation.take() {
191            self.apply_terminal(operation);
192        }
193    }
194
195    fn complete_active_operation_for_write(&mut self, priority: u8) {
196        if self
197            .active_operation
198            .is_some_and(|active| priority <= active.priority)
199        {
200            self.complete_active_operation();
201        }
202    }
203
204    fn arm_operation(&mut self, kind: OperationKind, priority: u8) {
205        let duration = Duration::from_secs(self.egress_time as u64);
206        let operation = ActiveOperation {
207            kind,
208            priority,
209            deadline: self.monotonic_now().saturating_add(duration),
210        };
211        if duration.is_zero() {
212            self.apply_terminal(operation);
213        } else {
214            self.active_operation = Some(operation);
215        }
216    }
217
218    fn write_present_value(&mut self, priority: u8, command: PresentValueCommand) {
219        if command != PresentValueCommand::Stop {
220            self.complete_active_operation_for_write(priority);
221        }
222
223        let index = (priority - 1) as usize;
224        match command {
225            PresentValueCommand::Set(value) => {
226                self.priority_array[index] = value;
227                self.recalculate_present_value();
228            }
229            PresentValueCommand::Warn => {
230                if self.blink_warn_enable && self.priority_is_highest_on(priority) {
231                    self.request_blink();
232                }
233            }
234            PresentValueCommand::WarnOff => {
235                if self.blink_warn_enable && self.priority_is_highest_on(priority) {
236                    self.request_blink();
237                    self.arm_operation(OperationKind::WarnOff, priority);
238                } else {
239                    self.priority_array[index] = Some(OFF);
240                    self.recalculate_present_value();
241                }
242            }
243            PresentValueCommand::WarnRelinquish => {
244                let eligible = self.blink_warn_enable
245                    && self.priority_is_highest_on(priority)
246                    && self.next_effective_value(priority) != ON;
247                if eligible {
248                    self.request_blink();
249                    self.arm_operation(OperationKind::WarnRelinquish, priority);
250                } else {
251                    self.priority_array[index] = None;
252                    self.recalculate_present_value();
253                }
254            }
255            PresentValueCommand::Stop => {
256                if self
257                    .active_operation
258                    .is_some_and(|active| active.priority == priority)
259                {
260                    self.active_operation = None;
261                }
262            }
263        }
264    }
265
266    fn command_rollback(&self) -> CommandRollback {
267        CommandRollback {
268            present_value: self.present_value,
269            blink_warn_enable: self.blink_warn_enable,
270            egress_time: self.egress_time,
271            priority_array: self.priority_array,
272            relinquish_default: self.relinquish_default,
273            active_operation: self.active_operation,
274            blink_request_count: self.blink_request_count,
275            logical_now: self.logical_now,
276        }
277    }
278
279    fn monotonic_now(&self) -> Duration {
280        self.monotonic_clock
281            .as_ref()
282            .map_or(self.logical_now, |clock| clock())
283    }
284
285    fn expire_at(&mut self, now: Duration) -> bool {
286        let Some(operation) = self.active_operation else {
287            return false;
288        };
289        if now < operation.deadline {
290            return false;
291        }
292        self.active_operation = None;
293        self.apply_terminal(operation);
294        true
295    }
296}
297
298impl BACnetObject for BinaryLightingOutputObject {
299    fn object_identifier(&self) -> ObjectIdentifier {
300        self.oid
301    }
302
303    fn object_name(&self) -> &str {
304        &self.name
305    }
306
307    fn read_property(
308        &self,
309        property: PropertyIdentifier,
310        array_index: Option<u32>,
311    ) -> Result<PropertyValue, Error> {
312        if let Some(result) = read_common_properties!(self, property, array_index) {
313            return result;
314        }
315        match property {
316            PropertyIdentifier::OBJECT_TYPE => Ok(PropertyValue::Enumerated(
317                ObjectType::BINARY_LIGHTING_OUTPUT.to_raw(),
318            )),
319            PropertyIdentifier::PRESENT_VALUE => Ok(PropertyValue::Enumerated(self.present_value)),
320            PropertyIdentifier::BLINK_WARN_ENABLE => {
321                Ok(PropertyValue::Boolean(self.blink_warn_enable))
322            }
323            PropertyIdentifier::EGRESS_TIME => Ok(PropertyValue::Unsigned(self.egress_time as u64)),
324            PropertyIdentifier::EGRESS_ACTIVE => {
325                Ok(PropertyValue::Boolean(self.active_operation.is_some()))
326            }
327            PropertyIdentifier::PRIORITY_ARRAY => {
328                read_priority_array!(self, array_index, PropertyValue::Enumerated)
329            }
330            PropertyIdentifier::RELINQUISH_DEFAULT => {
331                Ok(PropertyValue::Enumerated(self.relinquish_default))
332            }
333            _ => Err(common::unknown_property_error()),
334        }
335    }
336
337    fn write_property(
338        &mut self,
339        property: PropertyIdentifier,
340        array_index: Option<u32>,
341        value: PropertyValue,
342        priority: Option<u8>,
343    ) -> Result<(), Error> {
344        if property == PropertyIdentifier::PRIORITY_ARRAY {
345            let (index, value) = Self::validate_priority_array_write(array_index, value)?;
346            self.complete_active_operation_for_write(index as u8 + 1);
347            self.priority_array[index] = value;
348            self.recalculate_present_value();
349            return Ok(());
350        }
351        if property == PropertyIdentifier::PRESENT_VALUE {
352            let (priority, command) = Self::validate_present_value_command(value, priority)?;
353            self.write_present_value(priority, command);
354            return Ok(());
355        }
356        if property == PropertyIdentifier::BLINK_WARN_ENABLE {
357            if let PropertyValue::Boolean(value) = value {
358                self.blink_warn_enable = value;
359                return Ok(());
360            }
361            return Err(common::invalid_data_type_error());
362        }
363        if property == PropertyIdentifier::EGRESS_TIME {
364            if let PropertyValue::Unsigned(value) = value {
365                self.egress_time = common::u64_to_u32(value)?;
366                return Ok(());
367            }
368            return Err(common::invalid_data_type_error());
369        }
370        if property == PropertyIdentifier::RELINQUISH_DEFAULT {
371            if let PropertyValue::Enumerated(value) = value {
372                return self.set_relinquish_default(value);
373            }
374            return Err(common::invalid_data_type_error());
375        }
376        if let Some(result) =
377            common::write_out_of_service(&mut self.out_of_service, property, &value)
378        {
379            return result;
380        }
381        if let Some(result) = common::write_description(&mut self.description, property, &value) {
382            return result;
383        }
384        Err(common::write_access_denied_error())
385    }
386
387    fn property_list(&self) -> Cow<'static, [PropertyIdentifier]> {
388        static PROPS: &[PropertyIdentifier] = &[
389            PropertyIdentifier::OBJECT_IDENTIFIER,
390            PropertyIdentifier::OBJECT_NAME,
391            PropertyIdentifier::DESCRIPTION,
392            PropertyIdentifier::OBJECT_TYPE,
393            PropertyIdentifier::PRESENT_VALUE,
394            PropertyIdentifier::BLINK_WARN_ENABLE,
395            PropertyIdentifier::EGRESS_TIME,
396            PropertyIdentifier::EGRESS_ACTIVE,
397            PropertyIdentifier::STATUS_FLAGS,
398            PropertyIdentifier::OUT_OF_SERVICE,
399            PropertyIdentifier::RELIABILITY,
400            PropertyIdentifier::PRIORITY_ARRAY,
401            PropertyIdentifier::RELINQUISH_DEFAULT,
402        ];
403        Cow::Borrowed(PROPS)
404    }
405
406    fn supports_cov(&self) -> bool {
407        true
408    }
409
410    fn is_writable_property(&self, property: PropertyIdentifier) -> bool {
411        matches!(
412            property,
413            PropertyIdentifier::PRIORITY_ARRAY
414                | PropertyIdentifier::PRESENT_VALUE
415                | PropertyIdentifier::RELINQUISH_DEFAULT
416                | PropertyIdentifier::BLINK_WARN_ENABLE
417                | PropertyIdentifier::EGRESS_TIME
418                | PropertyIdentifier::OUT_OF_SERVICE
419                | PropertyIdentifier::DESCRIPTION
420        )
421    }
422
423    fn capture_write_property_rollback(
424        &mut self,
425        property: PropertyIdentifier,
426        _value: &PropertyValue,
427    ) -> Option<WritePropertyRollback> {
428        matches!(
429            property,
430            PropertyIdentifier::PRESENT_VALUE
431                | PropertyIdentifier::PRIORITY_ARRAY
432                | PropertyIdentifier::RELINQUISH_DEFAULT
433                | PropertyIdentifier::BLINK_WARN_ENABLE
434                | PropertyIdentifier::EGRESS_TIME
435        )
436        .then(|| WritePropertyRollback::new(self.command_rollback()))
437    }
438
439    fn restore_write_property_rollback(
440        &mut self,
441        rollback: WritePropertyRollback,
442    ) -> Result<(), Error> {
443        let rollback = rollback.downcast::<CommandRollback>()?;
444        self.present_value = rollback.present_value;
445        self.blink_warn_enable = rollback.blink_warn_enable;
446        self.egress_time = rollback.egress_time;
447        self.priority_array = rollback.priority_array;
448        self.relinquish_default = rollback.relinquish_default;
449        self.active_operation = rollback.active_operation;
450        self.blink_request_count = rollback.blink_request_count;
451        self.logical_now = rollback.logical_now;
452        Ok(())
453    }
454
455    fn advance_time_internal(&mut self, elapsed: Duration) -> bool {
456        self.logical_now = self.logical_now.saturating_add(elapsed);
457        self.expire_at(self.logical_now)
458    }
459
460    fn bind_monotonic_clock_internal(&mut self, clock: Option<Arc<MonotonicClock>>) {
461        self.monotonic_clock = clock;
462    }
463
464    fn advance_monotonic_time_internal(&mut self, now: Duration) -> bool {
465        self.expire_at(now)
466    }
467
468    fn next_monotonic_deadline_internal(&self) -> Option<Duration> {
469        self.active_operation.map(|operation| operation.deadline)
470    }
471
472    fn cov_snapshot_internal(&self) -> Option<Box<dyn BACnetObject>> {
473        Some(Box::new(self.clone()))
474    }
475
476    fn binary_lighting_blink_count_internal(&self) -> u64 {
477        self.blink_request_count
478    }
479}
480
481#[cfg(test)]
482#[path = "binary_tests.rs"]
483mod tests;
484
485#[cfg(test)]
486#[path = "binary_direct_tests.rs"]
487mod direct_tests;