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#[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: Reliability,
63 priority_array: [Option<u32>; 16],
64 relinquish_default: u32,
65 color_link: Option<ColorLink>,
68 profile: ProfileState,
70}
71
72impl BinaryLightingOutputObject {
73 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 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 pub fn color_link(&self) -> Option<&ColorLink> {
107 self.color_link.as_ref()
108 }
109
110 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 pub fn wait_for_tag_saves(&self) {
126 self.profile.wait_for_saves();
127 }
128
129 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 pub fn set_description(&mut self, desc: impl Into<String>) {
142 self.description = desc.into();
143 }
144
145 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;