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