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