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#[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: Reliability,
70 priority_array: [Option<u32>; 16],
71 relinquish_default: u32,
72 value_source: ValueSourceTracking,
74 color_link: Option<ColorLink>,
77 profile: ProfileState,
79}
80
81impl BinaryLightingOutputObject {
82 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 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 pub fn color_link(&self) -> Option<&ColorLink> {
117 self.color_link.as_ref()
118 }
119
120 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 pub fn wait_for_tag_saves(&self) {
136 self.profile.wait_for_saves();
137 }
138
139 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 pub fn set_description(&mut self, desc: impl Into<String>) {
152 self.description = desc.into();
153 }
154
155 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 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 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 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;