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