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 super::color_link::{self, ColorLink};
10use crate::command_source::{CommandOrigin, ValueSourceTracking};
11use crate::common::{self, read_common_properties, read_priority_array};
12use crate::object_profile::{ObjectProfile, ProfileState, TagsPersistence};
13use crate::traits::{BACnetObject, MonotonicClock};
14
15const OFF: u32 = 0;
16const ON: u32 = 1;
17
18#[derive(Debug, Clone, Copy, PartialEq, Eq)]
19enum OperationKind {
20    WarnOff,
21    WarnRelinquish,
22}
23
24#[derive(Debug, Clone, Copy, PartialEq, Eq)]
25struct ActiveOperation {
26    kind: OperationKind,
27    priority: u8,
28    deadline: Duration,
29}
30
31#[derive(Debug, Clone, Copy, PartialEq, Eq)]
32enum PresentValueCommand {
33    Set(Option<u32>),
34    Warn,
35    WarnOff,
36    WarnRelinquish,
37    Stop,
38}
39
40/// BACnet Binary Lighting Output object.
41///
42/// The priority array stores only steady OFF/ON values. WARN, WARN_OFF,
43/// WARN_RELINQUISH, and STOP are command operations interpreted at write time.
44///
45/// Value_Source, Value_Source_Array and Last_Command_Time follow each
46/// command's writer per priority (Clause 19.5, #1866), as on the other
47/// commandable objects. A Present_Value write must name its writer
48/// (`write_property_from`); one without is WRITE_ACCESS_DENIED.
49///
50/// Cloning copies served state into an in-memory object. Tags persistence and
51/// pending saves remain solely with the original; cloning or dropping the copy
52/// never saves, corrects, settles, or waits on the original writer.
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; NO_FAULT_DETECTED until a fault is evaluated or simulated.
68    reliability: Reliability,
69    priority_array: [Option<u32>; 16],
70    relinquish_default: u32,
71    /// Value_Source, Value_Source_Array and Last_Command_Time (#1866).
72    value_source: ValueSourceTracking,
73    /// Color_Reference, with Color_Override and Override_Color_Reference
74    /// when overridable; absent until set.
75    color_link: Option<ColorLink>,
76    /// Tags, Profile_Location and Profile_Name, once provisioned (#1553).
77    profile: ProfileState,
78}
79
80impl BinaryLightingOutputObject {
81    /// Create a new Binary Lighting Output object with no reconstructed egress.
82    pub fn new(instance: u32, name: impl Into<String>) -> Result<Self, Error> {
83        Ok(Self {
84            oid: ObjectIdentifier::new(ObjectType::BINARY_LIGHTING_OUTPUT, instance)?,
85            name: name.into(),
86            description: String::new(),
87            present_value: OFF,
88            blink_warn_enable: false,
89            egress_time: 0,
90            active_operation: None,
91            blink_request_count: 0,
92            monotonic_clock: None,
93            logical_now: Duration::ZERO,
94            out_of_service: false,
95            status_flags: StatusFlags::empty(),
96            reliability: Reliability::NO_FAULT_DETECTED,
97            priority_array: [None; 16],
98            relinquish_default: OFF,
99            value_source: ValueSourceTracking::default(),
100            color_link: None,
101            profile: ProfileState::default(),
102        })
103    }
104
105    /// Link the output to the colour objects that set its colour, or take
106    /// the link away with `None`, as a Lighting Output's
107    /// [`set_color_link`](super::LightingOutputObject::set_color_link) does
108    /// (#1527).
109    pub fn set_color_link(&mut self, link: Option<ColorLink>) -> Result<(), Error> {
110        self.color_link = link.map(ColorLink::checked).transpose()?;
111        Ok(())
112    }
113
114    /// The colour link, if one is set.
115    pub fn color_link(&self) -> Option<&ColorLink> {
116        self.color_link.as_ref()
117    }
118
119    /// Build with application-owned Tags storage. Saved Tags override configured
120    /// Tags only while `set_profile` provisions the row. Loaded data must satisfy
121    /// the normal Tags rules and the opt-in 1 MiB snapshot limit.
122    pub fn with_tags_persistence(
123        instance: u32,
124        name: impl Into<String>,
125        persistence: Arc<dyn TagsPersistence>,
126    ) -> Result<Self, Error> {
127        let mut object = Self::new(instance, name)?;
128        object.profile = ProfileState::persistent(object.oid, persistence)?;
129        Ok(object)
130    }
131
132    /// Wait for queued save attempts to finish. This is not a success receipt;
133    /// writes report their own save outcomes. Unstaged local writes block.
134    pub fn wait_for_tag_saves(&self) {
135        self.profile.wait_for_saves();
136    }
137
138    /// Provision the optional Tags, Profile_Location and Profile_Name rows
139    /// before registration (#1553), as a Color object's
140    /// [`set_profile`](crate::color::ColorObject::set_profile) does.
141    pub fn set_profile(&mut self, profile: ObjectProfile) -> Result<(), Error> {
142        self.profile.provision(profile)
143    }
144
145    pub(super) fn profile(&self) -> &ObjectProfile {
146        self.profile.profile()
147    }
148
149    /// Set the description string.
150    pub fn set_description(&mut self, desc: impl Into<String>) {
151        self.description = desc.into();
152    }
153
154    /// Set Relinquish_Default to OFF (0) or ON (1) (#270, #283).
155    pub fn set_relinquish_default(&mut self, value: u32) -> Result<(), Error> {
156        if !matches!(value, OFF | ON) {
157            return Err(common::value_out_of_range_error());
158        }
159        self.relinquish_default = value;
160        self.recalculate_present_value();
161        Ok(())
162    }
163
164    fn recalculate_present_value(&mut self) {
165        self.present_value =
166            common::recalculate_from_priority_array(&self.priority_array, self.relinquish_default);
167    }
168
169    fn validate_present_value_command(
170        value: PropertyValue,
171        priority: Option<u8>,
172    ) -> Result<(u8, PresentValueCommand), Error> {
173        let priority = priority.unwrap_or(16);
174        if !(1..=16).contains(&priority) {
175            return Err(common::value_out_of_range_error());
176        }
177        let command = match value {
178            PropertyValue::Null => PresentValueCommand::Set(None),
179            PropertyValue::Enumerated(OFF) => PresentValueCommand::Set(Some(OFF)),
180            PropertyValue::Enumerated(ON) => PresentValueCommand::Set(Some(ON)),
181            PropertyValue::Enumerated(2) => PresentValueCommand::Warn,
182            PropertyValue::Enumerated(3) => PresentValueCommand::WarnOff,
183            PropertyValue::Enumerated(4) => PresentValueCommand::WarnRelinquish,
184            PropertyValue::Enumerated(5) => PresentValueCommand::Stop,
185            PropertyValue::Enumerated(_) => return Err(common::value_out_of_range_error()),
186            _ => return Err(common::invalid_data_type_error()),
187        };
188        Ok((priority, command))
189    }
190
191    fn highest_active_priority(&self) -> Option<u8> {
192        self.priority_array
193            .iter()
194            .position(Option::is_some)
195            .map(|index| index as u8 + 1)
196    }
197
198    fn priority_is_highest_on(&self, priority: u8) -> bool {
199        self.highest_active_priority() == Some(priority)
200            && self.priority_array[(priority - 1) as usize] == Some(ON)
201    }
202
203    fn next_effective_value(&self, priority: u8) -> u32 {
204        self.priority_array[priority as usize..]
205            .iter()
206            .flatten()
207            .next()
208            .copied()
209            .unwrap_or(self.relinquish_default)
210    }
211
212    fn request_blink(&mut self) {
213        self.blink_request_count = self.blink_request_count.saturating_add(1);
214    }
215
216    fn apply_terminal(&mut self, operation: ActiveOperation) {
217        let slot = &mut self.priority_array[(operation.priority - 1) as usize];
218        match operation.kind {
219            OperationKind::WarnOff => *slot = Some(OFF),
220            OperationKind::WarnRelinquish => *slot = None,
221        }
222        self.recalculate_present_value();
223    }
224
225    fn complete_active_operation(&mut self) {
226        if let Some(operation) = self.active_operation.take() {
227            self.apply_terminal(operation);
228        }
229    }
230
231    fn complete_active_operation_for_write(&mut self, priority: u8) {
232        if self
233            .active_operation
234            .is_some_and(|active| priority <= active.priority)
235        {
236            self.complete_active_operation();
237        }
238    }
239
240    fn arm_operation(&mut self, kind: OperationKind, priority: u8) {
241        let duration = Duration::from_secs(self.egress_time as u64);
242        let operation = ActiveOperation {
243            kind,
244            priority,
245            deadline: self.monotonic_now().saturating_add(duration),
246        };
247        if duration.is_zero() {
248            self.apply_terminal(operation);
249        } else {
250            self.active_operation = Some(operation);
251        }
252    }
253
254    /// Carry out a Present_Value command at `priority`; `true` when it acted
255    /// on that slot: wrote it, armed an egress there or stopped one. That
256    /// command's writer becomes the slot's Value_Source. WARN and a STOP
257    /// with nothing to stop leave the slot, and its source, as they were.
258    fn write_present_value(&mut self, priority: u8, command: PresentValueCommand) -> bool {
259        if command != PresentValueCommand::Stop {
260            self.complete_active_operation_for_write(priority);
261        }
262
263        let index = (priority - 1) as usize;
264        match command {
265            PresentValueCommand::Set(value) => {
266                self.priority_array[index] = value;
267                self.recalculate_present_value();
268            }
269            PresentValueCommand::Warn => {
270                if self.blink_warn_enable && self.priority_is_highest_on(priority) {
271                    self.request_blink();
272                }
273                return false;
274            }
275            PresentValueCommand::WarnOff => {
276                if self.blink_warn_enable && self.priority_is_highest_on(priority) {
277                    self.request_blink();
278                    self.arm_operation(OperationKind::WarnOff, priority);
279                } else {
280                    self.priority_array[index] = Some(OFF);
281                    self.recalculate_present_value();
282                }
283            }
284            PresentValueCommand::WarnRelinquish => {
285                let eligible = self.blink_warn_enable
286                    && self.priority_is_highest_on(priority)
287                    && self.next_effective_value(priority) != ON;
288                if eligible {
289                    self.request_blink();
290                    self.arm_operation(OperationKind::WarnRelinquish, priority);
291                } else {
292                    self.priority_array[index] = None;
293                    self.recalculate_present_value();
294                }
295            }
296            PresentValueCommand::Stop => {
297                if !self
298                    .active_operation
299                    .is_some_and(|active| active.priority == priority)
300                {
301                    return false;
302                }
303                self.active_operation = None;
304            }
305        }
306        true
307    }
308
309    fn monotonic_now(&self) -> Duration {
310        self.monotonic_clock
311            .as_ref()
312            .map_or(self.logical_now, |clock| clock())
313    }
314
315    /// Finish an egress that has run out by `now`. That ends its command,
316    /// so Last_Command_Time moves on if the value changed.
317    fn expire_at(&mut self, now: Duration) -> bool {
318        let Some(operation) = self.active_operation else {
319            return false;
320        };
321        if now < operation.deadline {
322            return false;
323        }
324        let before = self
325            .value_source
326            .effect(&self.priority_array, self.relinquish_default);
327        self.active_operation = None;
328        self.apply_terminal(operation);
329        let after = self
330            .value_source
331            .effect(&self.priority_array, self.relinquish_default);
332        self.value_source.settle(before, after);
333        true
334    }
335}
336
337impl BACnetObject for BinaryLightingOutputObject {
338    fn object_identifier(&self) -> ObjectIdentifier {
339        self.oid
340    }
341
342    fn object_name(&self) -> &str {
343        &self.name
344    }
345
346    fn read_property(
347        &self,
348        property: PropertyIdentifier,
349        array_index: Option<u32>,
350    ) -> Result<PropertyValue, Error> {
351        if let Some(result) = self
352            .value_source
353            .read(property, array_index, &self.priority_array)
354        {
355            return result;
356        }
357        if let Some(result) = read_common_properties!(self, property, array_index) {
358            return result;
359        }
360        match property {
361            PropertyIdentifier::OBJECT_TYPE => Ok(PropertyValue::Enumerated(
362                ObjectType::BINARY_LIGHTING_OUTPUT.to_raw(),
363            )),
364            PropertyIdentifier::PRESENT_VALUE => Ok(PropertyValue::Enumerated(self.present_value)),
365            PropertyIdentifier::BLINK_WARN_ENABLE => {
366                Ok(PropertyValue::Boolean(self.blink_warn_enable))
367            }
368            PropertyIdentifier::EGRESS_TIME => Ok(PropertyValue::Unsigned(self.egress_time as u64)),
369            PropertyIdentifier::EGRESS_ACTIVE => {
370                Ok(PropertyValue::Boolean(self.active_operation.is_some()))
371            }
372            PropertyIdentifier::PRIORITY_ARRAY => {
373                read_priority_array!(self, array_index, PropertyValue::Enumerated)
374            }
375            PropertyIdentifier::RELINQUISH_DEFAULT => {
376                Ok(PropertyValue::Enumerated(self.relinquish_default))
377            }
378            PropertyIdentifier::CURRENT_COMMAND_PRIORITY => {
379                Ok(common::current_command_priority(&self.priority_array))
380            }
381            p => color_link::read(self.color_link.as_ref(), p)
382                .or_else(|| self.profile.read(p, array_index))
383                .unwrap_or_else(|| Err(common::unknown_property_error())),
384        }
385    }
386
387    fn write_property_from(
388        &mut self,
389        property: PropertyIdentifier,
390        array_index: Option<u32>,
391        value: PropertyValue,
392        priority: Option<u8>,
393        origin: &CommandOrigin,
394    ) -> Result<(), Error> {
395        if matches!(
396            property,
397            PropertyIdentifier::PRESENT_VALUE | PropertyIdentifier::VALUE_SOURCE
398        ) && array_index.is_some()
399        {
400            return Err(common::property_is_not_an_array_error());
401        }
402        if property == PropertyIdentifier::VALUE_SOURCE {
403            return self.value_source.correct(value, priority, origin);
404        }
405        if property == PropertyIdentifier::PRESENT_VALUE {
406            let (priority, command) = Self::validate_present_value_command(value, priority)?;
407            let index = ValueSourceTracking::admit(Some(priority), origin)?;
408            let before = self
409                .value_source
410                .effect(&self.priority_array, self.relinquish_default);
411            if self.write_present_value(priority, command) {
412                self.value_source.record(index, origin);
413            }
414            let after = self
415                .value_source
416                .effect(&self.priority_array, self.relinquish_default);
417            self.value_source.settle(before, after);
418            return Ok(());
419        }
420        self.write_property(property, array_index, value, priority)
421    }
422
423    fn write_property(
424        &mut self,
425        property: PropertyIdentifier,
426        array_index: Option<u32>,
427        value: PropertyValue,
428        _priority: Option<u8>,
429    ) -> Result<(), Error> {
430        // A command needs its writer (`write_property_from`, #1866).
431        if matches!(
432            property,
433            PropertyIdentifier::PRESENT_VALUE | PropertyIdentifier::VALUE_SOURCE
434        ) {
435            return Err(common::write_access_denied_error());
436        }
437        if property == PropertyIdentifier::BLINK_WARN_ENABLE {
438            if let PropertyValue::Boolean(value) = value {
439                self.blink_warn_enable = value;
440                return Ok(());
441            }
442            return Err(common::invalid_data_type_error());
443        }
444        if property == PropertyIdentifier::EGRESS_TIME {
445            if let PropertyValue::Unsigned(value) = value {
446                self.egress_time = common::u64_to_u32(value)?;
447                return Ok(());
448            }
449            return Err(common::invalid_data_type_error());
450        }
451        if property == PropertyIdentifier::RELINQUISH_DEFAULT {
452            if let PropertyValue::Enumerated(value) = value {
453                return self.set_relinquish_default(value);
454            }
455            return Err(common::invalid_data_type_error());
456        }
457        if let Some(result) = color_link::write(&mut self.color_link, property, &value) {
458            return result;
459        }
460        if let Some(result) = self.profile.write(property, array_index, &value) {
461            return result;
462        }
463        if let Some(result) =
464            common::write_out_of_service(&mut self.out_of_service, property, &value)
465        {
466            return result;
467        }
468        if let Some(result) = common::write_description(&mut self.description, property, &value) {
469            return result;
470        }
471        Err(crate::common::unhandled_write_error(
472            self.property_metadata().as_ref(),
473            property,
474            array_index,
475        ))
476    }
477
478    fn durable_writes_internal(&mut self) -> Option<&mut dyn crate::durable::DurableWrites> {
479        self.profile.capability()
480    }
481
482    fn property_list(&self) -> Cow<'static, [PropertyIdentifier]> {
483        crate::property_metadata::property_list_from_metadata(self.property_metadata().as_ref())
484    }
485
486    fn property_metadata(&self) -> Cow<'_, [crate::property_metadata::PropertyMetadata]> {
487        super::metadata::for_binary_lighting_output_object(self)
488    }
489
490    fn supports_cov(&self) -> bool {
491        true
492    }
493
494    fn advance_time_internal(&mut self, elapsed: Duration) -> bool {
495        self.logical_now = self.logical_now.saturating_add(elapsed);
496        self.expire_at(self.logical_now)
497    }
498
499    fn bind_monotonic_clock_internal(&mut self, clock: Option<Arc<MonotonicClock>>) {
500        self.monotonic_clock = clock;
501    }
502
503    fn advance_monotonic_time_internal(&mut self, now: Duration) -> bool {
504        self.profile.expire(now);
505        self.expire_at(now)
506    }
507
508    fn next_monotonic_deadline_internal(&self) -> Option<Duration> {
509        self.active_operation.map(|operation| operation.deadline)
510    }
511
512    fn cov_snapshot_internal(&self) -> Option<Box<dyn BACnetObject>> {
513        Some(Box::new(self.clone()))
514    }
515
516    fn lighting_blink_count_internal(&self) -> u64 {
517        self.blink_request_count
518    }
519}
520
521#[cfg(test)]
522#[path = "binary_tests.rs"]
523mod tests;
524
525#[cfg(test)]
526#[path = "binary_direct_tests.rs"]
527mod direct_tests;