Skip to main content

bacnet_objects/
loop_obj.rs

1//! Loop (type 12) object per ASHRAE 135-2020 Clause 12.17.
2//!
3//! PID control loop. The application is responsible for running the PID
4//! algorithm; this object stores configuration and current output.
5//! Action and Priority_For_Writing are configuration for that algorithm:
6//! the object itself neither computes an output nor commands the property
7//! that Manipulated_Variable_Reference names, so nothing here changes with
8//! them. While the application runs the algorithm it also feeds
9//! Controlled_Variable_Value, the measurement it compares with Setpoint; the
10//! object doesn't follow Controlled_Variable_Reference itself.
11
12use crate::prelude::*;
13use alloc::borrow::Cow;
14use bacnet_types::constructed::BACnetObjectPropertyReference;
15use bacnet_types::enums::{
16    Action, EngineeringUnits, ErrorClass, ErrorCode, EventState, ObjectType, PropertyIdentifier,
17    Reliability,
18};
19use bacnet_types::error::Error;
20use bacnet_types::primitives::{ObjectIdentifier, PropertyValue, StatusFlags};
21
22use crate::common::{self, read_property_list_property};
23use crate::reference::{self, ReferenceFrame};
24use crate::traits::BACnetObject;
25
26mod metadata;
27
28/// BACnet Loop object — PID control loop configuration and state.
29pub struct LoopObject {
30    oid: ObjectIdentifier,
31    name: String,
32    description: String,
33    present_value: f32,
34    setpoint: f32,
35    controlled_variable_value: f32,
36    cov_increment: f32,
37    proportional_constant: f32,
38    integral_constant: f32,
39    derivative_constant: f32,
40    output_units: EngineeringUnits,
41    controlled_variable_units: EngineeringUnits,
42    proportional_constant_units: EngineeringUnits,
43    integral_constant_units: EngineeringUnits,
44    derivative_constant_units: EngineeringUnits,
45    action: Action,
46    priority_for_writing: u8,
47    update_interval: u32,
48    out_of_service: bool,
49    reliability: Reliability,
50    /// Evaluated Reliability saved while a client simulation owns the property
51    /// (Out_Of_Service TRUE); restored on the return to service.
52    reliability_before_out_of_service: Option<Reliability>,
53    status_flags: StatusFlags,
54    controlled_variable_reference: Option<BACnetObjectPropertyReference>,
55    manipulated_variable_reference: Option<BACnetObjectPropertyReference>,
56    setpoint_reference: Option<BACnetObjectPropertyReference>,
57}
58
59impl LoopObject {
60    /// Create a new Loop object; `output_units` is a raw BACnetEngineeringUnits value for the
61    /// output.
62    pub fn new(
63        instance: u32,
64        name: impl Into<String>,
65        output_units: EngineeringUnits,
66    ) -> Result<Self, Error> {
67        let oid = ObjectIdentifier::new(ObjectType::LOOP, instance)?;
68        Ok(Self {
69            oid,
70            name: name.into(),
71            description: String::new(),
72            present_value: 0.0,
73            setpoint: 0.0,
74            controlled_variable_value: 0.0,
75            cov_increment: 0.0,
76            proportional_constant: 1.0,
77            integral_constant: 0.0,
78            derivative_constant: 0.0,
79            output_units,
80            controlled_variable_units: EngineeringUnits::NO_UNITS,
81            proportional_constant_units: EngineeringUnits::NO_UNITS,
82            integral_constant_units: EngineeringUnits::NO_UNITS,
83            derivative_constant_units: EngineeringUnits::NO_UNITS,
84            action: Action::DIRECT,
85            priority_for_writing: 16, // the lowest command priority
86            update_interval: 1000,    // milliseconds
87            out_of_service: false,
88            reliability: Reliability::NO_FAULT_DETECTED,
89            reliability_before_out_of_service: None,
90            status_flags: StatusFlags::empty(),
91            controlled_variable_reference: None,
92            manipulated_variable_reference: None,
93            setpoint_reference: None,
94        })
95    }
96
97    /// Application sets the current output value after PID computation.
98    ///
99    /// This direct setter bypasses the server's COV fanout and the
100    /// Out_Of_Service guard; a running server's application uses
101    /// `BACnetServer::set_present_value_local` instead.
102    pub fn set_present_value(&mut self, value: f32) {
103        self.present_value = value;
104    }
105
106    /// Application sets the measured value of the property that
107    /// Controlled_Variable_Reference names (Clause 12.17.14).
108    ///
109    /// COV notifications carry the latest value, but a change of it alone
110    /// sends none (Table 13-1). Like [`Self::set_present_value`] this direct
111    /// setter bypasses the server's COV fanout and value checks; a running
112    /// server's application uses
113    /// `BACnetServer::set_controlled_variable_value_local` instead.
114    pub fn set_controlled_variable_value(&mut self, value: f32) {
115        self.controlled_variable_value = value;
116    }
117
118    /// Validate and store a Present_Value write, without any access check.
119    ///
120    /// Shared by the network and internal routes, which differ only in the
121    /// Out_Of_Service condition each requires.
122    fn apply_present_value(&mut self, value: PropertyValue) -> Result<(), Error> {
123        write_finite_real(&mut self.present_value, value)
124    }
125
126    /// Set the description string.
127    pub fn set_description(&mut self, desc: impl Into<String>) {
128        self.description = desc.into();
129    }
130
131    /// Set the controlled variable reference (the object whose present value is
132    /// being controlled by this loop). A reference to the reserved instance
133    /// 4194303 clears it, as a client's write of the unset form does (#1417).
134    pub fn set_controlled_variable_reference(&mut self, r: BACnetObjectPropertyReference) {
135        self.controlled_variable_reference = reference::set_or_unset(r);
136    }
137
138    /// Set the manipulated variable reference (the object that the loop output
139    /// drives to achieve the setpoint). A reference to the reserved instance
140    /// 4194303 clears it (#1417).
141    pub fn set_manipulated_variable_reference(&mut self, r: BACnetObjectPropertyReference) {
142        self.manipulated_variable_reference = reference::set_or_unset(r);
143    }
144
145    /// Set the setpoint reference (an alternative way to supply the setpoint
146    /// from another object's property instead of the inline `SETPOINT` value).
147    pub fn set_setpoint_reference(&mut self, r: BACnetObjectPropertyReference) {
148        self.setpoint_reference = Some(r);
149    }
150
151    /// Set the units of Controlled_Variable_Value and Setpoint, served as
152    /// Controlled_Variable_Units (Clause 12.17.15). A new Loop uses NO_UNITS.
153    ///
154    /// The property is read-only over the network. A value above 65535,
155    /// outside BACnetEngineeringUnits, is refused with VALUE_OUT_OF_RANGE and
156    /// the property is left unchanged.
157    pub fn set_controlled_variable_units(&mut self, units: EngineeringUnits) -> Result<(), Error> {
158        self.controlled_variable_units = checked_units(units)?;
159        Ok(())
160    }
161
162    /// Set Proportional_Constant_Units (Clause 12.17.20), the units the
163    /// algorithm gives its proportional gain. A new Loop uses NO_UNITS.
164    /// Read-only over the network; refuses a value above 65535 like
165    /// [`Self::set_controlled_variable_units`].
166    pub fn set_proportional_constant_units(
167        &mut self,
168        units: EngineeringUnits,
169    ) -> Result<(), Error> {
170        self.proportional_constant_units = checked_units(units)?;
171        Ok(())
172    }
173
174    /// Set Integral_Constant_Units (Clause 12.17.22). A new Loop uses
175    /// NO_UNITS. Read-only over the network; refuses a value above 65535.
176    pub fn set_integral_constant_units(&mut self, units: EngineeringUnits) -> Result<(), Error> {
177        self.integral_constant_units = checked_units(units)?;
178        Ok(())
179    }
180
181    /// Set Derivative_Constant_Units (Clause 12.17.24). A new Loop uses
182    /// NO_UNITS. Read-only over the network; refuses a value above 65535.
183    pub fn set_derivative_constant_units(&mut self, units: EngineeringUnits) -> Result<(), Error> {
184        self.derivative_constant_units = checked_units(units)?;
185        Ok(())
186    }
187
188    /// Set Priority_For_Writing (Clause 12.17.28), the command priority the
189    /// loop's output holds in the Priority_Array of the property that
190    /// Manipulated_Variable_Reference names. A new Loop uses 16, the lowest.
191    ///
192    /// The property is read-only over the network. A priority outside 1..=16
193    /// is refused with VALUE_OUT_OF_RANGE and the property is left unchanged.
194    pub fn set_priority_for_writing(&mut self, priority: u8) -> Result<(), Error> {
195        if !(1..=16).contains(&priority) {
196            return Err(common::value_out_of_range_error());
197        }
198        self.priority_for_writing = priority;
199        Ok(())
200    }
201}
202
203/// BACnetEngineeringUnits is an enumeration capped at 65535 (Clause 21).
204fn checked_units(units: EngineeringUnits) -> Result<EngineeringUnits, Error> {
205    if units.to_raw() > 65_535 {
206        return Err(common::value_out_of_range_error());
207    }
208    Ok(units)
209}
210
211/// Store a finite REAL write into `target`.
212fn write_finite_real(target: &mut f32, value: PropertyValue) -> Result<(), Error> {
213    let PropertyValue::Real(v) = value else {
214        return Err(common::invalid_data_type_error());
215    };
216    common::reject_non_finite(v)?;
217    *target = v;
218    Ok(())
219}
220
221impl BACnetObject for LoopObject {
222    fn object_identifier(&self) -> ObjectIdentifier {
223        self.oid
224    }
225
226    fn object_name(&self) -> &str {
227        &self.name
228    }
229
230    fn read_property(
231        &self,
232        property: PropertyIdentifier,
233        array_index: Option<u32>,
234    ) -> Result<PropertyValue, Error> {
235        match property {
236            p if p == PropertyIdentifier::OBJECT_IDENTIFIER => {
237                Ok(PropertyValue::ObjectIdentifier(self.oid))
238            }
239            p if p == PropertyIdentifier::OBJECT_NAME => {
240                Ok(PropertyValue::CharacterString(self.name.clone()))
241            }
242            p if p == PropertyIdentifier::DESCRIPTION => {
243                Ok(PropertyValue::CharacterString(self.description.clone()))
244            }
245            p if p == PropertyIdentifier::OBJECT_TYPE => {
246                Ok(PropertyValue::Enumerated(ObjectType::LOOP.to_raw()))
247            }
248            p if p == PropertyIdentifier::PRESENT_VALUE => {
249                Ok(PropertyValue::Real(self.present_value))
250            }
251            p if p == PropertyIdentifier::SETPOINT => Ok(PropertyValue::Real(self.setpoint)),
252            p if p == PropertyIdentifier::CONTROLLED_VARIABLE_VALUE => {
253                Ok(PropertyValue::Real(self.controlled_variable_value))
254            }
255            p if p == PropertyIdentifier::COV_INCREMENT => {
256                Ok(PropertyValue::Real(self.cov_increment))
257            }
258            p if p == PropertyIdentifier::PROPORTIONAL_CONSTANT => {
259                Ok(PropertyValue::Real(self.proportional_constant))
260            }
261            p if p == PropertyIdentifier::INTEGRAL_CONSTANT => {
262                Ok(PropertyValue::Real(self.integral_constant))
263            }
264            p if p == PropertyIdentifier::DERIVATIVE_CONSTANT => {
265                Ok(PropertyValue::Real(self.derivative_constant))
266            }
267            p if p == PropertyIdentifier::OUTPUT_UNITS => {
268                Ok(PropertyValue::Enumerated(self.output_units.to_raw()))
269            }
270            p if p == PropertyIdentifier::CONTROLLED_VARIABLE_UNITS => Ok(
271                PropertyValue::Enumerated(self.controlled_variable_units.to_raw()),
272            ),
273            p if p == PropertyIdentifier::PROPORTIONAL_CONSTANT_UNITS => Ok(
274                PropertyValue::Enumerated(self.proportional_constant_units.to_raw()),
275            ),
276            p if p == PropertyIdentifier::INTEGRAL_CONSTANT_UNITS => Ok(PropertyValue::Enumerated(
277                self.integral_constant_units.to_raw(),
278            )),
279            p if p == PropertyIdentifier::DERIVATIVE_CONSTANT_UNITS => Ok(
280                PropertyValue::Enumerated(self.derivative_constant_units.to_raw()),
281            ),
282            p if p == PropertyIdentifier::ACTION => {
283                Ok(PropertyValue::Enumerated(self.action.to_raw()))
284            }
285            p if p == PropertyIdentifier::PRIORITY_FOR_WRITING => {
286                Ok(PropertyValue::Unsigned(self.priority_for_writing.into()))
287            }
288            p if p == PropertyIdentifier::UPDATE_INTERVAL => {
289                Ok(PropertyValue::Unsigned(self.update_interval as u64))
290            }
291            // FAULT follows Reliability and OUT_OF_SERVICE follows
292            // Out_Of_Service; IN_ALARM follows the fixed NORMAL Event_State
293            // this object reports.
294            p if p == PropertyIdentifier::STATUS_FLAGS => Ok(common::compute_status_flags(
295                self.status_flags,
296                self.reliability,
297                self.out_of_service,
298                EventState::NORMAL,
299            )),
300            p if p == PropertyIdentifier::EVENT_STATE => {
301                Ok(PropertyValue::Enumerated(EventState::NORMAL.to_raw()))
302            }
303            p if p == PropertyIdentifier::RELIABILITY => {
304                Ok(PropertyValue::Enumerated(self.reliability.to_raw()))
305            }
306            p if p == PropertyIdentifier::OUT_OF_SERVICE => {
307                Ok(PropertyValue::Boolean(self.out_of_service))
308            }
309            // Each reference reads as its Clause 21 encoding (#1312); see
310            // reference.rs for the unset forms (#1417): the measured input,
311            // or the commanded output, at the reserved instance.
312            p if p == PropertyIdentifier::CONTROLLED_VARIABLE_REFERENCE => {
313                Ok(reference::object_property_reference_value(
314                    self.controlled_variable_reference.as_ref(),
315                    ObjectType::ANALOG_INPUT,
316                ))
317            }
318            p if p == PropertyIdentifier::MANIPULATED_VARIABLE_REFERENCE => {
319                Ok(reference::object_property_reference_value(
320                    self.manipulated_variable_reference.as_ref(),
321                    ObjectType::ANALOG_OUTPUT,
322                ))
323            }
324            p if p == PropertyIdentifier::SETPOINT_REFERENCE => Ok(
325                reference::setpoint_reference_value(self.setpoint_reference.as_ref()),
326            ),
327            p if p == PropertyIdentifier::PROPERTY_LIST => {
328                read_property_list_property(&self.property_list(), array_index)
329            }
330            _ => Err(Error::protocol(
331                ErrorClass::PROPERTY,
332                ErrorCode::UNKNOWN_PROPERTY,
333            )),
334        }
335    }
336
337    fn write_property(
338        &mut self,
339        property: PropertyIdentifier,
340        _array_index: Option<u32>,
341        value: PropertyValue,
342        _priority: Option<u8>,
343    ) -> Result<(), Error> {
344        // Clause 12.17.9 decouples Present_Value from the algorithm while
345        // Out_Of_Service is TRUE and Table 12-20 footnote 7 makes it writable
346        // then, for simulation and testing. In service the algorithm owns it;
347        // the application route is `set_present_value_internal`.
348        if property == PropertyIdentifier::PRESENT_VALUE {
349            if !self.out_of_service {
350                return Err(common::write_access_denied_error());
351            }
352            return self.apply_present_value(value);
353        }
354        if let Some(result) = common::write_out_of_service_with_reliability_restore(
355            &mut self.out_of_service,
356            &mut self.reliability,
357            &mut self.reliability_before_out_of_service,
358            property,
359            &value,
360        ) {
361            return result;
362        }
363        if let Some(result) = common::write_cov_increment(&mut self.cov_increment, property, &value)
364        {
365            return result;
366        }
367        match property {
368            p if p == PropertyIdentifier::SETPOINT => write_finite_real(&mut self.setpoint, value),
369            p if p == PropertyIdentifier::PROPORTIONAL_CONSTANT => {
370                write_finite_real(&mut self.proportional_constant, value)
371            }
372            p if p == PropertyIdentifier::INTEGRAL_CONSTANT => {
373                write_finite_real(&mut self.integral_constant, value)
374            }
375            p if p == PropertyIdentifier::DERIVATIVE_CONSTANT => {
376                write_finite_real(&mut self.derivative_constant, value)
377            }
378            // BACnetAction names only DIRECT and REVERSE (Clause 21).
379            p if p == PropertyIdentifier::ACTION => {
380                let PropertyValue::Enumerated(raw) = value else {
381                    return Err(common::invalid_data_type_error());
382                };
383                let action = Action::from_raw(raw);
384                if action != Action::DIRECT && action != Action::REVERSE {
385                    return Err(common::value_out_of_range_error());
386                }
387                self.action = action;
388                Ok(())
389            }
390            p if p == PropertyIdentifier::UPDATE_INTERVAL => {
391                if let PropertyValue::Unsigned(v) = value {
392                    self.update_interval = common::u64_to_u32(v)?;
393                    return Ok(());
394                }
395                Err(Error::protocol(
396                    ErrorClass::PROPERTY,
397                    ErrorCode::INVALID_DATA_TYPE,
398                ))
399            }
400            // Clause 12.17 Table 12-20 lists Reliability O7; that footnote requires
401            // writes to be supported while Out_Of_Service is TRUE. The property's
402            // text specifies simulation/test writes to Present_Value and to
403            // Reliability when present and capable of values beyond
404            // NO_FAULT_DETECTED. In service the property is owned
405            // by the algorithm, so a network write is refused here; the internal
406            // evaluator route is `set_reliability_internal` with the complementary
407            // guard, and Out_Of_Service saves/restores the evaluated value (handled
408            // above the match).
409            p if p == PropertyIdentifier::RELIABILITY => {
410                if !self.out_of_service {
411                    return Err(common::write_access_denied_error());
412                }
413                if let PropertyValue::Enumerated(raw) = value {
414                    let v = Reliability::from_raw(raw);
415                    if !common::is_reliability_value_valid(v) {
416                        return Err(common::value_out_of_range_error());
417                    }
418                    self.reliability = v;
419                    return Ok(());
420                }
421                Err(Error::protocol(
422                    ErrorClass::PROPERTY,
423                    ErrorCode::INVALID_DATA_TYPE,
424                ))
425            }
426            p if p == PropertyIdentifier::DESCRIPTION => {
427                if let PropertyValue::CharacterString(s) = value {
428                    self.description = s;
429                    return Ok(());
430                }
431                Err(Error::protocol(
432                    ErrorClass::PROPERTY,
433                    ErrorCode::INVALID_DATA_TYPE,
434                ))
435            }
436            // Table 12-20 types the two variable references
437            // BACnetObjectPropertyReference: a write takes the reference's
438            // context-tagged members, the unset form clearing it, and Null
439            // is a value of another datatype (reference.rs).
440            p if p == PropertyIdentifier::CONTROLLED_VARIABLE_REFERENCE => {
441                self.controlled_variable_reference =
442                    reference::decode_reference_write(&value, ReferenceFrame::Bare)?;
443                Ok(())
444            }
445            p if p == PropertyIdentifier::MANIPULATED_VARIABLE_REFERENCE => {
446                self.manipulated_variable_reference =
447                    reference::decode_reference_write(&value, ReferenceFrame::Bare)?;
448                Ok(())
449            }
450            // Setpoint_Reference is BACnetSetpointReference: the members
451            // framed in context tag 0, or the empty value for none.
452            p if p == PropertyIdentifier::SETPOINT_REFERENCE => {
453                self.setpoint_reference =
454                    reference::decode_reference_write(&value, ReferenceFrame::Setpoint)?;
455                Ok(())
456            }
457            _ => Err(crate::common::unhandled_write_error(
458                self.property_metadata().as_ref(),
459                property,
460                _array_index,
461            )),
462        }
463    }
464
465    fn property_metadata(&self) -> Cow<'_, [crate::property_metadata::PropertyMetadata]> {
466        metadata::for_object(self)
467    }
468
469    fn property_list(&self) -> Cow<'static, [PropertyIdentifier]> {
470        crate::property_metadata::property_list_from_metadata(self.property_metadata().as_ref())
471    }
472
473    fn supports_cov(&self) -> bool {
474        true
475    }
476
477    fn cov_increment(&self) -> Option<f64> {
478        Some(f64::from(self.cov_increment))
479    }
480
481    fn set_present_value_internal(&mut self, value: PropertyValue) -> Result<(), Error> {
482        // The complement of the network route: while Out_Of_Service is TRUE
483        // a client's simulated output must not be overwritten by the algorithm.
484        if self.out_of_service {
485            return Err(common::write_access_denied_error());
486        }
487        self.apply_present_value(value)
488    }
489
490    fn set_controlled_variable_value_internal(
491        &mut self,
492        value: PropertyValue,
493    ) -> Result<(), Error> {
494        // Out_Of_Service decouples only Present_Value and Reliability from the
495        // algorithm (Clause 12.17.9), so the measurement keeps flowing while a
496        // client simulates the output.
497        write_finite_real(&mut self.controlled_variable_value, value)
498    }
499
500    fn set_reliability_internal(&mut self, reliability: Reliability) -> Result<(), Error> {
501        // While Out_Of_Service is TRUE the client owns the simulated value;
502        // an internal write would clobber the simulation (Clause 12.17
503        // Out_Of_Service paragraph separates Reliability from algorithm output),
504        // so it is refused until the object returns to service.
505        if self.out_of_service {
506            return Err(common::write_access_denied_error());
507        }
508        if !common::is_reliability_value_valid(reliability) {
509            return Err(common::value_out_of_range_error());
510        }
511        self.reliability = reliability;
512        Ok(())
513    }
514}
515
516#[cfg(test)]
517mod tests;
518
519#[cfg(test)]
520mod property_set_tests;
521
522#[cfg(test)]
523mod reference_tests;