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