Skip to main content

bacnet_objects/analog/
output.rs

1use super::*;
2use crate::common::{
3    read_analog_event_properties, read_generic_event_properties, write_analog_event_properties,
4    write_generic_event_properties,
5};
6use crate::object_profile::{ObjectProfile, ProfileState, TagsPersistence};
7use crate::property_metadata::PropertyMetadata;
8use bacnet_types::enums::EngineeringUnits;
9use std::sync::Arc;
10
11mod metadata;
12
13// ---------------------------------------------------------------------------
14// AnalogOutput (type 1)
15// ---------------------------------------------------------------------------
16
17/// BACnet Analog Output object.
18pub struct AnalogOutputObject {
19    profile: ProfileState,
20    oid: ObjectIdentifier,
21    name: String,
22    description: String,
23    present_value: f32,
24    units: EngineeringUnits,
25    out_of_service: bool,
26    status_flags: StatusFlags,
27    priority_array: [Option<f32>; 16],
28    relinquish_default: f32,
29    /// COV_Increment: minimum change threshold for COV notifications.
30    /// Default 0.0 means notify on any write (including no-change).
31    /// Set to a positive value for delta-based filtering.
32    cov_increment: f32,
33    event_detector: OutOfRangeDetector,
34    /// Event_Detection_Enable (Clause 12.3). A FALSE value suspends
35    /// event-state-machine evaluation under Clause 13.2.2.1.
36    event_detection_enable: bool,
37    reliability: Reliability,
38    reliability_before_out_of_service: Option<Reliability>,
39    reliability_inhibit: common::ReliabilityInhibitState,
40    min_pres_value: Option<f32>,
41    max_pres_value: Option<f32>,
42    pub(crate) event_history: EventHistory,
43    /// Value source tracking.
44    value_source: crate::command_source::ValueSourceTracking,
45}
46
47impl AnalogOutputObject {
48    /// Create a new Analog Output object.
49    pub fn new(
50        instance: u32,
51        name: impl Into<String>,
52        units: EngineeringUnits,
53    ) -> Result<Self, Error> {
54        let oid = ObjectIdentifier::new(ObjectType::ANALOG_OUTPUT, instance)?;
55        Ok(Self {
56            profile: ProfileState::default(),
57            oid,
58            name: name.into(),
59            description: String::new(),
60            present_value: 0.0,
61            units,
62            out_of_service: false,
63            status_flags: StatusFlags::empty(),
64            priority_array: [None; 16],
65            relinquish_default: 0.0,
66            cov_increment: 0.0,
67            event_detector: OutOfRangeDetector::default(),
68            event_detection_enable: true,
69            reliability: Reliability::NO_FAULT_DETECTED,
70            reliability_before_out_of_service: None,
71            reliability_inhibit: common::ReliabilityInhibitState::default(),
72            min_pres_value: None,
73            max_pres_value: None,
74            event_history: EventHistory::default(),
75            value_source: crate::command_source::ValueSourceTracking::default(),
76        })
77    }
78
79    /// Build with application-owned Tags storage. Saved Tags override configured
80    /// Tags only while [`Self::set_profile`] provisions the row. Loaded data must
81    /// satisfy the shared Tags rules and opt-in 1 MiB snapshot limit.
82    pub fn with_tags_persistence(
83        instance: u32,
84        name: impl Into<String>,
85        units: EngineeringUnits,
86        persistence: Arc<dyn TagsPersistence>,
87    ) -> Result<Self, Error> {
88        let mut object = Self::new(instance, name, units)?;
89        object.profile = ProfileState::persistent(object.oid, persistence)?;
90        Ok(object)
91    }
92
93    /// Provision independent optional Tags, Profile_Location and Profile_Name
94    /// rows before registration. Tags is writable independently of commands and
95    /// Out_Of_Service; the text rows are network read-only. Invalid configuration
96    /// leaves the previous profile and its attached storage unchanged.
97    pub fn set_profile(&mut self, profile: ObjectProfile) -> Result<(), Error> {
98        self.profile.provision(profile)
99    }
100
101    /// Wait for queued save attempts to finish; writes report their own outcomes.
102    /// This is not a success receipt. Unstaged synchronous writes block their caller.
103    pub fn wait_for_tag_saves(&self) {
104        self.profile.wait_for_saves();
105    }
106
107    /// Set the description string.
108    pub fn set_description(&mut self, desc: impl Into<String>) {
109        self.description = desc.into();
110    }
111
112    /// Set minimum engineering-bound metadata; this is not a reliability fault limit.
113    pub fn set_min_pres_value(&mut self, value: f32) {
114        self.min_pres_value = Some(value);
115    }
116
117    /// Set maximum engineering-bound metadata; this is not a reliability fault limit.
118    pub fn set_max_pres_value(&mut self, value: f32) {
119        self.max_pres_value = Some(value);
120    }
121
122    /// Recalculate present-value from the priority array.
123    fn recalculate_present_value(&mut self) {
124        self.present_value =
125            common::recalculate_from_priority_array(&self.priority_array, self.relinquish_default);
126    }
127
128    /// Set the Relinquish_Default (#270).
129    ///
130    /// Validated the same way a commanded Present_Value is (finite Real);
131    /// after the store, Present_Value is resolved anew from the priority
132    /// array so an empty array falls back to the new default immediately.
133    pub fn set_relinquish_default(&mut self, value: f32) -> Result<(), Error> {
134        common::reject_non_finite(value)?;
135        self.relinquish_default = value;
136        self.recalculate_present_value();
137        Ok(())
138    }
139}
140
141impl BACnetObject for AnalogOutputObject {
142    fn object_identifier(&self) -> ObjectIdentifier {
143        self.oid
144    }
145
146    fn object_name(&self) -> &str {
147        &self.name
148    }
149
150    fn read_property(
151        &self,
152        property: PropertyIdentifier,
153        array_index: Option<u32>,
154    ) -> Result<PropertyValue, Error> {
155        if let Some(result) = self
156            .value_source
157            .read(property, array_index, &self.priority_array)
158        {
159            return result;
160        }
161
162        if property == PropertyIdentifier::STATUS_FLAGS {
163            return Ok(common::compute_status_flags(
164                self.status_flags,
165                self.reliability,
166                self.out_of_service,
167                self.event_detector.event_state,
168            ));
169        }
170        if let Some(value) = self.reliability_inhibit.read(property) {
171            return Ok(value);
172        }
173        if let Some(result) = read_common_properties!(self, property, array_index) {
174            return result;
175        }
176        if let Some(result) = read_analog_event_properties!(self, property) {
177            return result;
178        }
179        if let Some(result) = self.event_history.read(property, array_index) {
180            return result;
181        }
182        if let Some(result) = read_generic_event_properties!(self, property) {
183            return result;
184        }
185        if property == PropertyIdentifier::EVENT_DETECTION_ENABLE {
186            return Ok(PropertyValue::Boolean(self.event_detection_enable));
187        }
188        if let Some(result) = self.profile.read(property, array_index) {
189            return result;
190        }
191        match property {
192            p if p == PropertyIdentifier::OBJECT_TYPE => Ok(PropertyValue::Enumerated(
193                ObjectType::ANALOG_OUTPUT.to_raw(),
194            )),
195            p if p == PropertyIdentifier::PRESENT_VALUE => {
196                Ok(PropertyValue::Real(self.present_value))
197            }
198            p if p == PropertyIdentifier::UNITS => {
199                Ok(PropertyValue::Enumerated(self.units.to_raw()))
200            }
201            p if p == PropertyIdentifier::PRIORITY_ARRAY => {
202                common::read_priority_array!(self, array_index, PropertyValue::Real)
203            }
204            p if p == PropertyIdentifier::RELINQUISH_DEFAULT => {
205                Ok(PropertyValue::Real(self.relinquish_default))
206            }
207            p if p == PropertyIdentifier::CURRENT_COMMAND_PRIORITY => {
208                Ok(common::current_command_priority(&self.priority_array))
209            }
210            p if p == PropertyIdentifier::COV_INCREMENT => {
211                Ok(PropertyValue::Real(self.cov_increment))
212            }
213            p if p == PropertyIdentifier::MIN_PRES_VALUE => match self.min_pres_value {
214                Some(v) => Ok(PropertyValue::Real(v)),
215                None => Err(common::unknown_property_error()),
216            },
217            p if p == PropertyIdentifier::MAX_PRES_VALUE => match self.max_pres_value {
218                Some(v) => Ok(PropertyValue::Real(v)),
219                None => Err(common::unknown_property_error()),
220            },
221            _ => Err(common::unknown_property_error()),
222        }
223    }
224
225    fn write_property_from(
226        &mut self,
227        property: PropertyIdentifier,
228        array_index: Option<u32>,
229        value: PropertyValue,
230        priority: Option<u8>,
231        origin: &crate::command_source::CommandOrigin,
232    ) -> Result<(), Error> {
233        if matches!(
234            property,
235            PropertyIdentifier::PRESENT_VALUE | PropertyIdentifier::VALUE_SOURCE
236        ) && array_index.is_some()
237        {
238            return Err(common::property_is_not_an_array_error());
239        }
240        if property == PropertyIdentifier::VALUE_SOURCE {
241            return self.value_source.correct(value, priority, origin);
242        }
243        if property == PropertyIdentifier::PRESENT_VALUE {
244            return crate::command_source::write_sourced_priority!(
245                self,
246                value,
247                priority,
248                origin,
249                |v| {
250                    if let PropertyValue::Real(f) = v {
251                        if !f.is_finite() {
252                            return Err(common::value_out_of_range_error());
253                        }
254                        Ok(f)
255                    } else {
256                        Err(common::invalid_data_type_error())
257                    }
258                }
259            );
260        }
261        self.write_property(property, array_index, value, priority)
262    }
263
264    fn write_property(
265        &mut self,
266        property: PropertyIdentifier,
267        array_index: Option<u32>,
268        value: PropertyValue,
269        _priority: Option<u8>,
270    ) -> Result<(), Error> {
271        if matches!(
272            property,
273            PropertyIdentifier::PRESENT_VALUE | PropertyIdentifier::VALUE_SOURCE
274        ) {
275            return Err(common::write_access_denied_error());
276        }
277        if let Some(result) = self.reliability_inhibit.write_inhibit(
278            &mut self.reliability,
279            self.out_of_service,
280            property,
281            &value,
282        ) {
283            return result;
284        }
285        if let Some(result) = self.reliability_inhibit.write_out_of_service(
286            &mut self.out_of_service,
287            &mut self.reliability,
288            &mut self.reliability_before_out_of_service,
289            property,
290            &value,
291        ) {
292            return result.map(|_| ());
293        }
294        if let Some(result) = common::write_object_name(&mut self.name, property, &value) {
295            return result;
296        }
297        if let Some(result) = common::write_description(&mut self.description, property, &value) {
298            return result;
299        }
300        // Clause 12.3 requires simulation/test writes while Out_Of_Service is TRUE:
301        // Present_Value is writable, as is Reliability when that property exists
302        // and supports values beyond NO_FAULT_DETECTED.
303        // `is_writable_property` stays statically true because it describes capability.
304        if let Some(result) = self.reliability_inhibit.write_client_reliability(
305            self.out_of_service,
306            &mut self.reliability,
307            property,
308            &value,
309        ) {
310            return result;
311        }
312        if property == PropertyIdentifier::RELINQUISH_DEFAULT {
313            if let PropertyValue::Real(v) = value {
314                return self.set_relinquish_default(v);
315            }
316            return Err(common::invalid_data_type_error());
317        }
318        if let Some(result) = common::write_cov_increment(&mut self.cov_increment, property, &value)
319        {
320            return result;
321        }
322        if property == PropertyIdentifier::EVENT_DETECTION_ENABLE {
323            if let PropertyValue::Boolean(v) = value {
324                self.event_detection_enable = v;
325                if !v {
326                    self.event_detector.event_state = bacnet_types::enums::EventState::NORMAL;
327                    self.event_detector.acked_transitions =
328                        bacnet_types::bitstring::EventTransitionBits::all();
329                    self.event_detector.pending = None;
330                    self.event_detector.fault_reliability = None;
331                    self.event_history.reset();
332                }
333                return Ok(());
334            }
335            return Err(common::invalid_data_type_error());
336        }
337        if let Some(result) = write_analog_event_properties!(self, property, value) {
338            return result;
339        }
340        // Event_Message_Texts_Config and the Event_Algorithm_Inhibit pair (#1329).
341        if let Some(result) =
342            self.event_history
343                .write(property, array_index, &value, self.event_detection_enable)
344        {
345            return result;
346        }
347        if let Some(result) = write_generic_event_properties!(self, property, value) {
348            return result;
349        }
350        if let Some(result) = self.profile.write(property, array_index, &value) {
351            return result;
352        }
353        Err(crate::common::unhandled_write_error(
354            self.property_metadata().as_ref(),
355            property,
356            array_index,
357        ))
358    }
359
360    fn durable_writes_internal(&mut self) -> Option<&mut dyn crate::durable::DurableWrites> {
361        self.profile.capability()
362    }
363
364    fn advance_monotonic_time_internal(&mut self, now: std::time::Duration) -> bool {
365        self.profile.expire(now);
366        false
367    }
368
369    fn property_metadata(&self) -> Cow<'_, [PropertyMetadata]> {
370        metadata::for_object(self)
371    }
372
373    fn property_list(&self) -> Cow<'static, [PropertyIdentifier]> {
374        crate::property_metadata::property_list_from_metadata(self.property_metadata().as_ref())
375    }
376
377    fn supports_cov(&self) -> bool {
378        true
379    }
380
381    fn cov_increment(&self) -> Option<f64> {
382        Some(f64::from(self.cov_increment))
383    }
384
385    crate::event::impl_builtin_intrinsic_reporting!(
386        event_detector,
387        event_history,
388        [present_value],
389        reliability,
390        event_detection_enable,
391        OutOfRangeDetector::ALGORITHM
392    );
393
394    fn acknowledge_alarm(
395        &mut self,
396        transition_bit: bacnet_types::bitstring::EventTransitionBits,
397    ) -> Result<(), bacnet_types::error::Error> {
398        self.event_detector.acked_transitions |=
399            transition_bit & bacnet_types::bitstring::EventTransitionBits::all();
400        Ok(())
401    }
402
403    fn acknowledge_alarm_correlated_internal(
404        &mut self,
405        event_state: EventState,
406        timestamp: &BACnetTimeStamp,
407    ) -> Result<(), Error> {
408        if !self.event_detection_enable {
409            return Err(Error::Protocol {
410                class: ErrorClass::OBJECT,
411                code: ErrorCode::NO_ALARM_CONFIGURED,
412            });
413        }
414        self.event_history.acknowledge_correlated(
415            &mut self.event_detector.acked_transitions,
416            event_state,
417            timestamp,
418        )
419    }
420
421    fn set_reliability_internal(&mut self, reliability: Reliability) -> Result<(), Error> {
422        if self.out_of_service || self.reliability_inhibit.enabled() {
423            return Err(common::write_access_denied_error());
424        }
425        if !common::is_reliability_value_valid(reliability) {
426            return Err(common::value_out_of_range_error());
427        }
428        self.reliability = reliability;
429        Ok(())
430    }
431
432    fn reliability_evaluation_inhibited_internal(&self) -> bool {
433        self.reliability_inhibit.enabled()
434    }
435
436    fn is_createable(&self) -> bool {
437        true
438    }
439
440    fn creation_only_properties(&self) -> &'static [PropertyIdentifier] {
441        super::CREATION_ONLY
442    }
443
444    fn initialize_property(
445        &mut self,
446        property: PropertyIdentifier,
447        value: PropertyValue,
448    ) -> Result<(), Error> {
449        super::initialize_units(&mut self.units, property, value)
450    }
451}
452
453#[cfg(test)]
454mod detection_enable_reset_tests {
455    use super::*;
456
457    /// Regression guard for issue #123: once transitions populate timestamps and messages,
458    /// disabling event detection must still restore their Clause 13.2.2.1 initial conditions.
459    #[test]
460    fn ao_disabling_detection_resets_event_history() {
461        let mut ao = AnalogOutputObject::new(1, "AO-1", EngineeringUnits::DEGREES_CELSIUS).unwrap();
462        assert_eq!(
463            ao.read_property(PropertyIdentifier::EVENT_DETECTION_ENABLE, None)
464                .unwrap(),
465            PropertyValue::Boolean(true)
466        );
467        ao.event_detector.event_state = bacnet_types::enums::EventState::HIGH_LIMIT;
468        ao.event_detector.acked_transitions = bacnet_types::bitstring::EventTransitionBits::empty();
469        ao.event_detector.pending = Some(crate::event::PendingTransition {
470            state: bacnet_types::enums::EventState::HIGH_LIMIT,
471            remaining: 2,
472        });
473        ao.event_detector.fault_reliability = Some(bacnet_types::enums::Reliability::NO_SENSOR);
474        ao.event_history.time_stamps = [
475            BACnetTimeStamp::SequenceNumber(1),
476            BACnetTimeStamp::SequenceNumber(2),
477            BACnetTimeStamp::SequenceNumber(3),
478        ];
479        ao.event_history.message_texts = ["offnormal".into(), "fault".into(), "normal".into()];
480
481        ao.write_property(
482            PropertyIdentifier::EVENT_DETECTION_ENABLE,
483            None,
484            PropertyValue::Boolean(false),
485            None,
486        )
487        .unwrap();
488
489        assert_eq!(
490            ao.read_property(PropertyIdentifier::EVENT_DETECTION_ENABLE, None)
491                .unwrap(),
492            PropertyValue::Boolean(false)
493        );
494        assert_eq!(
495            ao.event_detector.event_state,
496            bacnet_types::enums::EventState::NORMAL
497        );
498        assert_eq!(
499            ao.event_detector.acked_transitions,
500            bacnet_types::bitstring::EventTransitionBits::all()
501        );
502        assert!(ao.event_detector.pending.is_none());
503        assert!(ao.event_detector.fault_reliability.is_none());
504        assert_eq!(
505            ao.event_history.time_stamps,
506            [
507                BACnetTimeStamp::SequenceNumber(0),
508                BACnetTimeStamp::SequenceNumber(0),
509                BACnetTimeStamp::SequenceNumber(0),
510            ]
511        );
512        assert_eq!(
513            ao.event_history.message_texts,
514            [String::new(), String::new(), String::new()]
515        );
516    }
517}