1use std::borrow::Cow;
2use std::sync::Arc;
3use std::time::Duration;
4
5use bacnet_types::enums::{ObjectType, PropertyIdentifier};
6use bacnet_types::error::Error;
7use bacnet_types::primitives::{ObjectIdentifier, PropertyValue, StatusFlags};
8
9use crate::common::{self, read_common_properties, read_priority_array};
10use crate::traits::{BACnetObject, MonotonicClock, WritePropertyRollback};
11
12const OFF: u32 = 0;
13const ON: u32 = 1;
14
15#[derive(Debug, Clone, Copy, PartialEq, Eq)]
16enum OperationKind {
17 WarnOff,
18 WarnRelinquish,
19}
20
21#[derive(Debug, Clone, Copy, PartialEq, Eq)]
22struct ActiveOperation {
23 kind: OperationKind,
24 priority: u8,
25 deadline: Duration,
26}
27
28#[derive(Debug, Clone, Copy, PartialEq, Eq)]
29enum PresentValueCommand {
30 Set(Option<u32>),
31 Warn,
32 WarnOff,
33 WarnRelinquish,
34 Stop,
35}
36
37#[derive(Clone)]
38struct CommandRollback {
39 present_value: u32,
40 blink_warn_enable: bool,
41 egress_time: u32,
42 priority_array: [Option<u32>; 16],
43 relinquish_default: u32,
44 active_operation: Option<ActiveOperation>,
45 blink_request_count: u64,
46 logical_now: Duration,
47}
48
49#[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: u32,
69 priority_array: [Option<u32>; 16],
70 relinquish_default: u32,
71}
72
73impl BinaryLightingOutputObject {
74 pub fn new(instance: u32, name: impl Into<String>) -> Result<Self, Error> {
76 Ok(Self {
77 oid: ObjectIdentifier::new(ObjectType::BINARY_LIGHTING_OUTPUT, instance)?,
78 name: name.into(),
79 description: String::new(),
80 present_value: OFF,
81 blink_warn_enable: false,
82 egress_time: 0,
83 active_operation: None,
84 blink_request_count: 0,
85 monotonic_clock: None,
86 logical_now: Duration::ZERO,
87 out_of_service: false,
88 status_flags: StatusFlags::empty(),
89 reliability: 0,
90 priority_array: [None; 16],
91 relinquish_default: OFF,
92 })
93 }
94
95 pub fn set_description(&mut self, desc: impl Into<String>) {
97 self.description = desc.into();
98 }
99
100 pub fn set_relinquish_default(&mut self, value: u32) -> Result<(), Error> {
102 if !matches!(value, OFF | ON) {
103 return Err(common::value_out_of_range_error());
104 }
105 self.relinquish_default = value;
106 self.recalculate_present_value();
107 Ok(())
108 }
109
110 fn recalculate_present_value(&mut self) {
111 self.present_value =
112 common::recalculate_from_priority_array(&self.priority_array, self.relinquish_default);
113 }
114
115 fn validate_present_value_command(
116 value: PropertyValue,
117 priority: Option<u8>,
118 ) -> Result<(u8, PresentValueCommand), Error> {
119 let priority = priority.unwrap_or(16);
120 if !(1..=16).contains(&priority) {
121 return Err(common::value_out_of_range_error());
122 }
123 let command = match value {
124 PropertyValue::Null => PresentValueCommand::Set(None),
125 PropertyValue::Enumerated(OFF) => PresentValueCommand::Set(Some(OFF)),
126 PropertyValue::Enumerated(ON) => PresentValueCommand::Set(Some(ON)),
127 PropertyValue::Enumerated(2) => PresentValueCommand::Warn,
128 PropertyValue::Enumerated(3) => PresentValueCommand::WarnOff,
129 PropertyValue::Enumerated(4) => PresentValueCommand::WarnRelinquish,
130 PropertyValue::Enumerated(5) => PresentValueCommand::Stop,
131 PropertyValue::Enumerated(_) => return Err(common::value_out_of_range_error()),
132 _ => return Err(common::invalid_data_type_error()),
133 };
134 Ok((priority, command))
135 }
136
137 fn validate_priority_array_write(
138 array_index: Option<u32>,
139 value: PropertyValue,
140 ) -> Result<(usize, Option<u32>), Error> {
141 let index = match array_index {
142 Some(index) if (1..=16).contains(&index) => (index - 1) as usize,
143 Some(_) => return Err(common::invalid_array_index_error()),
144 None => return Err(common::write_access_denied_error()),
145 };
146 let value = match value {
147 PropertyValue::Null => None,
148 PropertyValue::Enumerated(value @ (OFF | ON)) => Some(value),
149 PropertyValue::Enumerated(_) => return Err(common::value_out_of_range_error()),
150 _ => return Err(common::invalid_data_type_error()),
151 };
152 Ok((index, value))
153 }
154
155 fn highest_active_priority(&self) -> Option<u8> {
156 self.priority_array
157 .iter()
158 .position(Option::is_some)
159 .map(|index| index as u8 + 1)
160 }
161
162 fn priority_is_highest_on(&self, priority: u8) -> bool {
163 self.highest_active_priority() == Some(priority)
164 && self.priority_array[(priority - 1) as usize] == Some(ON)
165 }
166
167 fn next_effective_value(&self, priority: u8) -> u32 {
168 self.priority_array[priority as usize..]
169 .iter()
170 .flatten()
171 .next()
172 .copied()
173 .unwrap_or(self.relinquish_default)
174 }
175
176 fn request_blink(&mut self) {
177 self.blink_request_count = self.blink_request_count.saturating_add(1);
178 }
179
180 fn apply_terminal(&mut self, operation: ActiveOperation) {
181 let slot = &mut self.priority_array[(operation.priority - 1) as usize];
182 match operation.kind {
183 OperationKind::WarnOff => *slot = Some(OFF),
184 OperationKind::WarnRelinquish => *slot = None,
185 }
186 self.recalculate_present_value();
187 }
188
189 fn complete_active_operation(&mut self) {
190 if let Some(operation) = self.active_operation.take() {
191 self.apply_terminal(operation);
192 }
193 }
194
195 fn complete_active_operation_for_write(&mut self, priority: u8) {
196 if self
197 .active_operation
198 .is_some_and(|active| priority <= active.priority)
199 {
200 self.complete_active_operation();
201 }
202 }
203
204 fn arm_operation(&mut self, kind: OperationKind, priority: u8) {
205 let duration = Duration::from_secs(self.egress_time as u64);
206 let operation = ActiveOperation {
207 kind,
208 priority,
209 deadline: self.monotonic_now().saturating_add(duration),
210 };
211 if duration.is_zero() {
212 self.apply_terminal(operation);
213 } else {
214 self.active_operation = Some(operation);
215 }
216 }
217
218 fn write_present_value(&mut self, priority: u8, command: PresentValueCommand) {
219 if command != PresentValueCommand::Stop {
220 self.complete_active_operation_for_write(priority);
221 }
222
223 let index = (priority - 1) as usize;
224 match command {
225 PresentValueCommand::Set(value) => {
226 self.priority_array[index] = value;
227 self.recalculate_present_value();
228 }
229 PresentValueCommand::Warn => {
230 if self.blink_warn_enable && self.priority_is_highest_on(priority) {
231 self.request_blink();
232 }
233 }
234 PresentValueCommand::WarnOff => {
235 if self.blink_warn_enable && self.priority_is_highest_on(priority) {
236 self.request_blink();
237 self.arm_operation(OperationKind::WarnOff, priority);
238 } else {
239 self.priority_array[index] = Some(OFF);
240 self.recalculate_present_value();
241 }
242 }
243 PresentValueCommand::WarnRelinquish => {
244 let eligible = self.blink_warn_enable
245 && self.priority_is_highest_on(priority)
246 && self.next_effective_value(priority) != ON;
247 if eligible {
248 self.request_blink();
249 self.arm_operation(OperationKind::WarnRelinquish, priority);
250 } else {
251 self.priority_array[index] = None;
252 self.recalculate_present_value();
253 }
254 }
255 PresentValueCommand::Stop => {
256 if self
257 .active_operation
258 .is_some_and(|active| active.priority == priority)
259 {
260 self.active_operation = None;
261 }
262 }
263 }
264 }
265
266 fn command_rollback(&self) -> CommandRollback {
267 CommandRollback {
268 present_value: self.present_value,
269 blink_warn_enable: self.blink_warn_enable,
270 egress_time: self.egress_time,
271 priority_array: self.priority_array,
272 relinquish_default: self.relinquish_default,
273 active_operation: self.active_operation,
274 blink_request_count: self.blink_request_count,
275 logical_now: self.logical_now,
276 }
277 }
278
279 fn monotonic_now(&self) -> Duration {
280 self.monotonic_clock
281 .as_ref()
282 .map_or(self.logical_now, |clock| clock())
283 }
284
285 fn expire_at(&mut self, now: Duration) -> bool {
286 let Some(operation) = self.active_operation else {
287 return false;
288 };
289 if now < operation.deadline {
290 return false;
291 }
292 self.active_operation = None;
293 self.apply_terminal(operation);
294 true
295 }
296}
297
298impl BACnetObject for BinaryLightingOutputObject {
299 fn object_identifier(&self) -> ObjectIdentifier {
300 self.oid
301 }
302
303 fn object_name(&self) -> &str {
304 &self.name
305 }
306
307 fn read_property(
308 &self,
309 property: PropertyIdentifier,
310 array_index: Option<u32>,
311 ) -> Result<PropertyValue, Error> {
312 if let Some(result) = read_common_properties!(self, property, array_index) {
313 return result;
314 }
315 match property {
316 PropertyIdentifier::OBJECT_TYPE => Ok(PropertyValue::Enumerated(
317 ObjectType::BINARY_LIGHTING_OUTPUT.to_raw(),
318 )),
319 PropertyIdentifier::PRESENT_VALUE => Ok(PropertyValue::Enumerated(self.present_value)),
320 PropertyIdentifier::BLINK_WARN_ENABLE => {
321 Ok(PropertyValue::Boolean(self.blink_warn_enable))
322 }
323 PropertyIdentifier::EGRESS_TIME => Ok(PropertyValue::Unsigned(self.egress_time as u64)),
324 PropertyIdentifier::EGRESS_ACTIVE => {
325 Ok(PropertyValue::Boolean(self.active_operation.is_some()))
326 }
327 PropertyIdentifier::PRIORITY_ARRAY => {
328 read_priority_array!(self, array_index, PropertyValue::Enumerated)
329 }
330 PropertyIdentifier::RELINQUISH_DEFAULT => {
331 Ok(PropertyValue::Enumerated(self.relinquish_default))
332 }
333 _ => Err(common::unknown_property_error()),
334 }
335 }
336
337 fn write_property(
338 &mut self,
339 property: PropertyIdentifier,
340 array_index: Option<u32>,
341 value: PropertyValue,
342 priority: Option<u8>,
343 ) -> Result<(), Error> {
344 if property == PropertyIdentifier::PRIORITY_ARRAY {
345 let (index, value) = Self::validate_priority_array_write(array_index, value)?;
346 self.complete_active_operation_for_write(index as u8 + 1);
347 self.priority_array[index] = value;
348 self.recalculate_present_value();
349 return Ok(());
350 }
351 if property == PropertyIdentifier::PRESENT_VALUE {
352 let (priority, command) = Self::validate_present_value_command(value, priority)?;
353 self.write_present_value(priority, command);
354 return Ok(());
355 }
356 if property == PropertyIdentifier::BLINK_WARN_ENABLE {
357 if let PropertyValue::Boolean(value) = value {
358 self.blink_warn_enable = value;
359 return Ok(());
360 }
361 return Err(common::invalid_data_type_error());
362 }
363 if property == PropertyIdentifier::EGRESS_TIME {
364 if let PropertyValue::Unsigned(value) = value {
365 self.egress_time = common::u64_to_u32(value)?;
366 return Ok(());
367 }
368 return Err(common::invalid_data_type_error());
369 }
370 if property == PropertyIdentifier::RELINQUISH_DEFAULT {
371 if let PropertyValue::Enumerated(value) = value {
372 return self.set_relinquish_default(value);
373 }
374 return Err(common::invalid_data_type_error());
375 }
376 if let Some(result) =
377 common::write_out_of_service(&mut self.out_of_service, property, &value)
378 {
379 return result;
380 }
381 if let Some(result) = common::write_description(&mut self.description, property, &value) {
382 return result;
383 }
384 Err(common::write_access_denied_error())
385 }
386
387 fn property_list(&self) -> Cow<'static, [PropertyIdentifier]> {
388 static PROPS: &[PropertyIdentifier] = &[
389 PropertyIdentifier::OBJECT_IDENTIFIER,
390 PropertyIdentifier::OBJECT_NAME,
391 PropertyIdentifier::DESCRIPTION,
392 PropertyIdentifier::OBJECT_TYPE,
393 PropertyIdentifier::PRESENT_VALUE,
394 PropertyIdentifier::BLINK_WARN_ENABLE,
395 PropertyIdentifier::EGRESS_TIME,
396 PropertyIdentifier::EGRESS_ACTIVE,
397 PropertyIdentifier::STATUS_FLAGS,
398 PropertyIdentifier::OUT_OF_SERVICE,
399 PropertyIdentifier::RELIABILITY,
400 PropertyIdentifier::PRIORITY_ARRAY,
401 PropertyIdentifier::RELINQUISH_DEFAULT,
402 ];
403 Cow::Borrowed(PROPS)
404 }
405
406 fn supports_cov(&self) -> bool {
407 true
408 }
409
410 fn is_writable_property(&self, property: PropertyIdentifier) -> bool {
411 matches!(
412 property,
413 PropertyIdentifier::PRIORITY_ARRAY
414 | PropertyIdentifier::PRESENT_VALUE
415 | PropertyIdentifier::RELINQUISH_DEFAULT
416 | PropertyIdentifier::BLINK_WARN_ENABLE
417 | PropertyIdentifier::EGRESS_TIME
418 | PropertyIdentifier::OUT_OF_SERVICE
419 | PropertyIdentifier::DESCRIPTION
420 )
421 }
422
423 fn capture_write_property_rollback(
424 &mut self,
425 property: PropertyIdentifier,
426 _value: &PropertyValue,
427 ) -> Option<WritePropertyRollback> {
428 matches!(
429 property,
430 PropertyIdentifier::PRESENT_VALUE
431 | PropertyIdentifier::PRIORITY_ARRAY
432 | PropertyIdentifier::RELINQUISH_DEFAULT
433 | PropertyIdentifier::BLINK_WARN_ENABLE
434 | PropertyIdentifier::EGRESS_TIME
435 )
436 .then(|| WritePropertyRollback::new(self.command_rollback()))
437 }
438
439 fn restore_write_property_rollback(
440 &mut self,
441 rollback: WritePropertyRollback,
442 ) -> Result<(), Error> {
443 let rollback = rollback.downcast::<CommandRollback>()?;
444 self.present_value = rollback.present_value;
445 self.blink_warn_enable = rollback.blink_warn_enable;
446 self.egress_time = rollback.egress_time;
447 self.priority_array = rollback.priority_array;
448 self.relinquish_default = rollback.relinquish_default;
449 self.active_operation = rollback.active_operation;
450 self.blink_request_count = rollback.blink_request_count;
451 self.logical_now = rollback.logical_now;
452 Ok(())
453 }
454
455 fn advance_time_internal(&mut self, elapsed: Duration) -> bool {
456 self.logical_now = self.logical_now.saturating_add(elapsed);
457 self.expire_at(self.logical_now)
458 }
459
460 fn bind_monotonic_clock_internal(&mut self, clock: Option<Arc<MonotonicClock>>) {
461 self.monotonic_clock = clock;
462 }
463
464 fn advance_monotonic_time_internal(&mut self, now: Duration) -> bool {
465 self.expire_at(now)
466 }
467
468 fn next_monotonic_deadline_internal(&self) -> Option<Duration> {
469 self.active_operation.map(|operation| operation.deadline)
470 }
471
472 fn cov_snapshot_internal(&self) -> Option<Box<dyn BACnetObject>> {
473 Some(Box::new(self.clone()))
474 }
475
476 fn binary_lighting_blink_count_internal(&self) -> u64 {
477 self.blink_request_count
478 }
479}
480
481#[cfg(test)]
482#[path = "binary_tests.rs"]
483mod tests;
484
485#[cfg(test)]
486#[path = "binary_direct_tests.rs"]
487mod direct_tests;