Skip to main content

bacnet_objects/lighting/
binary.rs

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