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 crate::common::{self, read_common_properties, read_priority_array};
10use crate::traits::{BACnetObject, MonotonicClock};
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)]
42pub struct BinaryLightingOutputObject {
43 oid: ObjectIdentifier,
44 name: String,
45 description: String,
46 present_value: u32,
47 blink_warn_enable: bool,
48 egress_time: u32,
49 active_operation: Option<ActiveOperation>,
50 blink_request_count: u64,
51 monotonic_clock: Option<Arc<MonotonicClock>>,
52 logical_now: Duration,
53 out_of_service: bool,
54 status_flags: StatusFlags,
55 reliability: Reliability,
57 priority_array: [Option<u32>; 16],
58 relinquish_default: u32,
59}
60
61impl BinaryLightingOutputObject {
62 pub fn new(instance: u32, name: impl Into<String>) -> Result<Self, Error> {
64 Ok(Self {
65 oid: ObjectIdentifier::new(ObjectType::BINARY_LIGHTING_OUTPUT, instance)?,
66 name: name.into(),
67 description: String::new(),
68 present_value: OFF,
69 blink_warn_enable: false,
70 egress_time: 0,
71 active_operation: None,
72 blink_request_count: 0,
73 monotonic_clock: None,
74 logical_now: Duration::ZERO,
75 out_of_service: false,
76 status_flags: StatusFlags::empty(),
77 reliability: Reliability::NO_FAULT_DETECTED,
78 priority_array: [None; 16],
79 relinquish_default: OFF,
80 })
81 }
82
83 pub fn set_description(&mut self, desc: impl Into<String>) {
85 self.description = desc.into();
86 }
87
88 pub fn set_relinquish_default(&mut self, value: u32) -> Result<(), Error> {
90 if !matches!(value, OFF | ON) {
91 return Err(common::value_out_of_range_error());
92 }
93 self.relinquish_default = value;
94 self.recalculate_present_value();
95 Ok(())
96 }
97
98 fn recalculate_present_value(&mut self) {
99 self.present_value =
100 common::recalculate_from_priority_array(&self.priority_array, self.relinquish_default);
101 }
102
103 fn validate_present_value_command(
104 value: PropertyValue,
105 priority: Option<u8>,
106 ) -> Result<(u8, PresentValueCommand), Error> {
107 let priority = priority.unwrap_or(16);
108 if !(1..=16).contains(&priority) {
109 return Err(common::value_out_of_range_error());
110 }
111 let command = match value {
112 PropertyValue::Null => PresentValueCommand::Set(None),
113 PropertyValue::Enumerated(OFF) => PresentValueCommand::Set(Some(OFF)),
114 PropertyValue::Enumerated(ON) => PresentValueCommand::Set(Some(ON)),
115 PropertyValue::Enumerated(2) => PresentValueCommand::Warn,
116 PropertyValue::Enumerated(3) => PresentValueCommand::WarnOff,
117 PropertyValue::Enumerated(4) => PresentValueCommand::WarnRelinquish,
118 PropertyValue::Enumerated(5) => PresentValueCommand::Stop,
119 PropertyValue::Enumerated(_) => return Err(common::value_out_of_range_error()),
120 _ => return Err(common::invalid_data_type_error()),
121 };
122 Ok((priority, command))
123 }
124
125 fn highest_active_priority(&self) -> Option<u8> {
126 self.priority_array
127 .iter()
128 .position(Option::is_some)
129 .map(|index| index as u8 + 1)
130 }
131
132 fn priority_is_highest_on(&self, priority: u8) -> bool {
133 self.highest_active_priority() == Some(priority)
134 && self.priority_array[(priority - 1) as usize] == Some(ON)
135 }
136
137 fn next_effective_value(&self, priority: u8) -> u32 {
138 self.priority_array[priority as usize..]
139 .iter()
140 .flatten()
141 .next()
142 .copied()
143 .unwrap_or(self.relinquish_default)
144 }
145
146 fn request_blink(&mut self) {
147 self.blink_request_count = self.blink_request_count.saturating_add(1);
148 }
149
150 fn apply_terminal(&mut self, operation: ActiveOperation) {
151 let slot = &mut self.priority_array[(operation.priority - 1) as usize];
152 match operation.kind {
153 OperationKind::WarnOff => *slot = Some(OFF),
154 OperationKind::WarnRelinquish => *slot = None,
155 }
156 self.recalculate_present_value();
157 }
158
159 fn complete_active_operation(&mut self) {
160 if let Some(operation) = self.active_operation.take() {
161 self.apply_terminal(operation);
162 }
163 }
164
165 fn complete_active_operation_for_write(&mut self, priority: u8) {
166 if self
167 .active_operation
168 .is_some_and(|active| priority <= active.priority)
169 {
170 self.complete_active_operation();
171 }
172 }
173
174 fn arm_operation(&mut self, kind: OperationKind, priority: u8) {
175 let duration = Duration::from_secs(self.egress_time as u64);
176 let operation = ActiveOperation {
177 kind,
178 priority,
179 deadline: self.monotonic_now().saturating_add(duration),
180 };
181 if duration.is_zero() {
182 self.apply_terminal(operation);
183 } else {
184 self.active_operation = Some(operation);
185 }
186 }
187
188 fn write_present_value(&mut self, priority: u8, command: PresentValueCommand) {
189 if command != PresentValueCommand::Stop {
190 self.complete_active_operation_for_write(priority);
191 }
192
193 let index = (priority - 1) as usize;
194 match command {
195 PresentValueCommand::Set(value) => {
196 self.priority_array[index] = value;
197 self.recalculate_present_value();
198 }
199 PresentValueCommand::Warn => {
200 if self.blink_warn_enable && self.priority_is_highest_on(priority) {
201 self.request_blink();
202 }
203 }
204 PresentValueCommand::WarnOff => {
205 if self.blink_warn_enable && self.priority_is_highest_on(priority) {
206 self.request_blink();
207 self.arm_operation(OperationKind::WarnOff, priority);
208 } else {
209 self.priority_array[index] = Some(OFF);
210 self.recalculate_present_value();
211 }
212 }
213 PresentValueCommand::WarnRelinquish => {
214 let eligible = self.blink_warn_enable
215 && self.priority_is_highest_on(priority)
216 && self.next_effective_value(priority) != ON;
217 if eligible {
218 self.request_blink();
219 self.arm_operation(OperationKind::WarnRelinquish, priority);
220 } else {
221 self.priority_array[index] = None;
222 self.recalculate_present_value();
223 }
224 }
225 PresentValueCommand::Stop => {
226 if self
227 .active_operation
228 .is_some_and(|active| active.priority == priority)
229 {
230 self.active_operation = None;
231 }
232 }
233 }
234 }
235
236 fn monotonic_now(&self) -> Duration {
237 self.monotonic_clock
238 .as_ref()
239 .map_or(self.logical_now, |clock| clock())
240 }
241
242 fn expire_at(&mut self, now: Duration) -> bool {
243 let Some(operation) = self.active_operation else {
244 return false;
245 };
246 if now < operation.deadline {
247 return false;
248 }
249 self.active_operation = None;
250 self.apply_terminal(operation);
251 true
252 }
253}
254
255impl BACnetObject for BinaryLightingOutputObject {
256 fn object_identifier(&self) -> ObjectIdentifier {
257 self.oid
258 }
259
260 fn object_name(&self) -> &str {
261 &self.name
262 }
263
264 fn read_property(
265 &self,
266 property: PropertyIdentifier,
267 array_index: Option<u32>,
268 ) -> Result<PropertyValue, Error> {
269 if let Some(result) = read_common_properties!(self, property, array_index) {
270 return result;
271 }
272 match property {
273 PropertyIdentifier::OBJECT_TYPE => Ok(PropertyValue::Enumerated(
274 ObjectType::BINARY_LIGHTING_OUTPUT.to_raw(),
275 )),
276 PropertyIdentifier::PRESENT_VALUE => Ok(PropertyValue::Enumerated(self.present_value)),
277 PropertyIdentifier::BLINK_WARN_ENABLE => {
278 Ok(PropertyValue::Boolean(self.blink_warn_enable))
279 }
280 PropertyIdentifier::EGRESS_TIME => Ok(PropertyValue::Unsigned(self.egress_time as u64)),
281 PropertyIdentifier::EGRESS_ACTIVE => {
282 Ok(PropertyValue::Boolean(self.active_operation.is_some()))
283 }
284 PropertyIdentifier::PRIORITY_ARRAY => {
285 read_priority_array!(self, array_index, PropertyValue::Enumerated)
286 }
287 PropertyIdentifier::RELINQUISH_DEFAULT => {
288 Ok(PropertyValue::Enumerated(self.relinquish_default))
289 }
290 PropertyIdentifier::CURRENT_COMMAND_PRIORITY => {
291 Ok(common::current_command_priority(&self.priority_array))
292 }
293 _ => Err(common::unknown_property_error()),
294 }
295 }
296
297 fn write_property(
298 &mut self,
299 property: PropertyIdentifier,
300 _array_index: Option<u32>,
301 value: PropertyValue,
302 priority: Option<u8>,
303 ) -> Result<(), Error> {
304 if property == PropertyIdentifier::PRESENT_VALUE {
305 let (priority, command) = Self::validate_present_value_command(value, priority)?;
306 self.write_present_value(priority, command);
307 return Ok(());
308 }
309 if property == PropertyIdentifier::BLINK_WARN_ENABLE {
310 if let PropertyValue::Boolean(value) = value {
311 self.blink_warn_enable = value;
312 return Ok(());
313 }
314 return Err(common::invalid_data_type_error());
315 }
316 if property == PropertyIdentifier::EGRESS_TIME {
317 if let PropertyValue::Unsigned(value) = value {
318 self.egress_time = common::u64_to_u32(value)?;
319 return Ok(());
320 }
321 return Err(common::invalid_data_type_error());
322 }
323 if property == PropertyIdentifier::RELINQUISH_DEFAULT {
324 if let PropertyValue::Enumerated(value) = value {
325 return self.set_relinquish_default(value);
326 }
327 return Err(common::invalid_data_type_error());
328 }
329 if let Some(result) =
330 common::write_out_of_service(&mut self.out_of_service, property, &value)
331 {
332 return result;
333 }
334 if let Some(result) = common::write_description(&mut self.description, property, &value) {
335 return result;
336 }
337 Err(crate::common::unhandled_write_error(
338 self.property_metadata().as_ref(),
339 property,
340 _array_index,
341 ))
342 }
343
344 fn property_list(&self) -> Cow<'static, [PropertyIdentifier]> {
345 crate::property_metadata::property_list_from_metadata(self.property_metadata().as_ref())
346 }
347
348 fn property_metadata(&self) -> Cow<'_, [crate::property_metadata::PropertyMetadata]> {
349 super::metadata::for_binary_lighting_output_object(self)
350 }
351
352 fn supports_cov(&self) -> bool {
353 true
354 }
355
356 fn advance_time_internal(&mut self, elapsed: Duration) -> bool {
357 self.logical_now = self.logical_now.saturating_add(elapsed);
358 self.expire_at(self.logical_now)
359 }
360
361 fn bind_monotonic_clock_internal(&mut self, clock: Option<Arc<MonotonicClock>>) {
362 self.monotonic_clock = clock;
363 }
364
365 fn advance_monotonic_time_internal(&mut self, now: Duration) -> bool {
366 self.expire_at(now)
367 }
368
369 fn next_monotonic_deadline_internal(&self) -> Option<Duration> {
370 self.active_operation.map(|operation| operation.deadline)
371 }
372
373 fn cov_snapshot_internal(&self) -> Option<Box<dyn BACnetObject>> {
374 Some(Box::new(self.clone()))
375 }
376
377 fn lighting_blink_count_internal(&self) -> u64 {
378 self.blink_request_count
379 }
380}
381
382#[cfg(test)]
383#[path = "binary_tests.rs"]
384mod tests;
385
386#[cfg(test)]
387#[path = "binary_direct_tests.rs"]
388mod direct_tests;