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