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