Skip to main content

bacnet_objects/
loop_obj.rs

1//! Loop (type 12) object per ASHRAE 135-2020 Clause 12.19.
2//!
3//! PID control loop. The application is responsible for running the PID
4//! algorithm; this object stores configuration and current output.
5
6use bacnet_types::constructed::BACnetObjectPropertyReference;
7use bacnet_types::enums::{ErrorClass, ErrorCode, ObjectType, PropertyIdentifier};
8use bacnet_types::error::Error;
9use bacnet_types::primitives::{ObjectIdentifier, PropertyValue, StatusFlags};
10use std::borrow::Cow;
11
12use crate::common::{self, read_property_list_property};
13use crate::traits::BACnetObject;
14
15/// BACnet Loop object — PID control loop configuration and state.
16pub struct LoopObject {
17    oid: ObjectIdentifier,
18    name: String,
19    description: String,
20    present_value: f32,
21    setpoint: f32,
22    proportional_constant: f32,
23    integral_constant: f32,
24    derivative_constant: f32,
25    output_units: u32,
26    update_interval: u32,
27    out_of_service: bool,
28    reliability: u32,
29    status_flags: StatusFlags,
30    controlled_variable_reference: Option<BACnetObjectPropertyReference>,
31    manipulated_variable_reference: Option<BACnetObjectPropertyReference>,
32    setpoint_reference: Option<BACnetObjectPropertyReference>,
33}
34
35impl LoopObject {
36    pub fn new(instance: u32, name: impl Into<String>, output_units: u32) -> Result<Self, Error> {
37        let oid = ObjectIdentifier::new(ObjectType::LOOP, instance)?;
38        Ok(Self {
39            oid,
40            name: name.into(),
41            description: String::new(),
42            present_value: 0.0,
43            setpoint: 0.0,
44            proportional_constant: 1.0,
45            integral_constant: 0.0,
46            derivative_constant: 0.0,
47            output_units,
48            update_interval: 1000, // milliseconds
49            out_of_service: false,
50            reliability: 0,
51            status_flags: StatusFlags::empty(),
52            controlled_variable_reference: None,
53            manipulated_variable_reference: None,
54            setpoint_reference: None,
55        })
56    }
57
58    /// Application sets the current output value after PID computation.
59    pub fn set_present_value(&mut self, value: f32) {
60        self.present_value = value;
61    }
62
63    /// Set the description string.
64    pub fn set_description(&mut self, desc: impl Into<String>) {
65        self.description = desc.into();
66    }
67
68    /// Set the controlled variable reference (the object whose present value is
69    /// being controlled by this loop).
70    pub fn set_controlled_variable_reference(&mut self, r: BACnetObjectPropertyReference) {
71        self.controlled_variable_reference = Some(r);
72    }
73
74    /// Set the manipulated variable reference (the object that the loop output
75    /// drives to achieve the setpoint).
76    pub fn set_manipulated_variable_reference(&mut self, r: BACnetObjectPropertyReference) {
77        self.manipulated_variable_reference = Some(r);
78    }
79
80    /// Set the setpoint reference (an alternative way to supply the setpoint
81    /// from another object's property instead of the inline `SETPOINT` value).
82    pub fn set_setpoint_reference(&mut self, r: BACnetObjectPropertyReference) {
83        self.setpoint_reference = Some(r);
84    }
85}
86
87impl BACnetObject for LoopObject {
88    fn object_identifier(&self) -> ObjectIdentifier {
89        self.oid
90    }
91
92    fn object_name(&self) -> &str {
93        &self.name
94    }
95
96    fn read_property(
97        &self,
98        property: PropertyIdentifier,
99        array_index: Option<u32>,
100    ) -> Result<PropertyValue, Error> {
101        match property {
102            p if p == PropertyIdentifier::OBJECT_IDENTIFIER => {
103                Ok(PropertyValue::ObjectIdentifier(self.oid))
104            }
105            p if p == PropertyIdentifier::OBJECT_NAME => {
106                Ok(PropertyValue::CharacterString(self.name.clone()))
107            }
108            p if p == PropertyIdentifier::DESCRIPTION => {
109                Ok(PropertyValue::CharacterString(self.description.clone()))
110            }
111            p if p == PropertyIdentifier::OBJECT_TYPE => {
112                Ok(PropertyValue::Enumerated(ObjectType::LOOP.to_raw()))
113            }
114            p if p == PropertyIdentifier::PRESENT_VALUE => {
115                Ok(PropertyValue::Real(self.present_value))
116            }
117            p if p == PropertyIdentifier::SETPOINT => Ok(PropertyValue::Real(self.setpoint)),
118            p if p == PropertyIdentifier::PROPORTIONAL_CONSTANT => {
119                Ok(PropertyValue::Real(self.proportional_constant))
120            }
121            p if p == PropertyIdentifier::INTEGRAL_CONSTANT => {
122                Ok(PropertyValue::Real(self.integral_constant))
123            }
124            p if p == PropertyIdentifier::DERIVATIVE_CONSTANT => {
125                Ok(PropertyValue::Real(self.derivative_constant))
126            }
127            p if p == PropertyIdentifier::OUTPUT_UNITS => {
128                Ok(PropertyValue::Enumerated(self.output_units))
129            }
130            p if p == PropertyIdentifier::UPDATE_INTERVAL => {
131                Ok(PropertyValue::Unsigned(self.update_interval as u64))
132            }
133            p if p == PropertyIdentifier::STATUS_FLAGS => Ok(PropertyValue::BitString {
134                unused_bits: 4,
135                data: vec![self.status_flags.bits() << 4],
136            }),
137            p if p == PropertyIdentifier::EVENT_STATE => Ok(PropertyValue::Enumerated(0)),
138            p if p == PropertyIdentifier::RELIABILITY => {
139                Ok(PropertyValue::Enumerated(self.reliability))
140            }
141            p if p == PropertyIdentifier::OUT_OF_SERVICE => {
142                Ok(PropertyValue::Boolean(self.out_of_service))
143            }
144            p if p == PropertyIdentifier::CONTROLLED_VARIABLE_REFERENCE => {
145                match &self.controlled_variable_reference {
146                    Some(r) => Ok(PropertyValue::List(vec![
147                        PropertyValue::ObjectIdentifier(r.object_identifier),
148                        PropertyValue::Enumerated(r.property_identifier),
149                    ])),
150                    None => Ok(PropertyValue::Null),
151                }
152            }
153            p if p == PropertyIdentifier::MANIPULATED_VARIABLE_REFERENCE => {
154                match &self.manipulated_variable_reference {
155                    Some(r) => Ok(PropertyValue::List(vec![
156                        PropertyValue::ObjectIdentifier(r.object_identifier),
157                        PropertyValue::Enumerated(r.property_identifier),
158                    ])),
159                    None => Ok(PropertyValue::Null),
160                }
161            }
162            p if p == PropertyIdentifier::SETPOINT_REFERENCE => match &self.setpoint_reference {
163                Some(r) => Ok(PropertyValue::List(vec![
164                    PropertyValue::ObjectIdentifier(r.object_identifier),
165                    PropertyValue::Enumerated(r.property_identifier),
166                ])),
167                None => Ok(PropertyValue::Null),
168            },
169            p if p == PropertyIdentifier::PROPERTY_LIST => {
170                read_property_list_property(&self.property_list(), array_index)
171            }
172            _ => Err(Error::Protocol {
173                class: ErrorClass::PROPERTY.to_raw() as u32,
174                code: ErrorCode::UNKNOWN_PROPERTY.to_raw() as u32,
175            }),
176        }
177    }
178
179    fn write_property(
180        &mut self,
181        property: PropertyIdentifier,
182        _array_index: Option<u32>,
183        value: PropertyValue,
184        _priority: Option<u8>,
185    ) -> Result<(), Error> {
186        match property {
187            p if p == PropertyIdentifier::SETPOINT => {
188                if let PropertyValue::Real(v) = value {
189                    common::reject_non_finite(v)?;
190                    self.setpoint = v;
191                    return Ok(());
192                }
193                Err(Error::Protocol {
194                    class: ErrorClass::PROPERTY.to_raw() as u32,
195                    code: ErrorCode::INVALID_DATA_TYPE.to_raw() as u32,
196                })
197            }
198            p if p == PropertyIdentifier::PROPORTIONAL_CONSTANT => {
199                if let PropertyValue::Real(v) = value {
200                    common::reject_non_finite(v)?;
201                    self.proportional_constant = v;
202                    return Ok(());
203                }
204                Err(Error::Protocol {
205                    class: ErrorClass::PROPERTY.to_raw() as u32,
206                    code: ErrorCode::INVALID_DATA_TYPE.to_raw() as u32,
207                })
208            }
209            p if p == PropertyIdentifier::INTEGRAL_CONSTANT => {
210                if let PropertyValue::Real(v) = value {
211                    common::reject_non_finite(v)?;
212                    self.integral_constant = v;
213                    return Ok(());
214                }
215                Err(Error::Protocol {
216                    class: ErrorClass::PROPERTY.to_raw() as u32,
217                    code: ErrorCode::INVALID_DATA_TYPE.to_raw() as u32,
218                })
219            }
220            p if p == PropertyIdentifier::DERIVATIVE_CONSTANT => {
221                if let PropertyValue::Real(v) = value {
222                    common::reject_non_finite(v)?;
223                    self.derivative_constant = v;
224                    return Ok(());
225                }
226                Err(Error::Protocol {
227                    class: ErrorClass::PROPERTY.to_raw() as u32,
228                    code: ErrorCode::INVALID_DATA_TYPE.to_raw() as u32,
229                })
230            }
231            p if p == PropertyIdentifier::UPDATE_INTERVAL => {
232                if let PropertyValue::Unsigned(v) = value {
233                    self.update_interval = common::u64_to_u32(v)?;
234                    return Ok(());
235                }
236                Err(Error::Protocol {
237                    class: ErrorClass::PROPERTY.to_raw() as u32,
238                    code: ErrorCode::INVALID_DATA_TYPE.to_raw() as u32,
239                })
240            }
241            p if p == PropertyIdentifier::RELIABILITY => {
242                if let PropertyValue::Enumerated(v) = value {
243                    self.reliability = v;
244                    return Ok(());
245                }
246                Err(Error::Protocol {
247                    class: ErrorClass::PROPERTY.to_raw() as u32,
248                    code: ErrorCode::INVALID_DATA_TYPE.to_raw() as u32,
249                })
250            }
251            p if p == PropertyIdentifier::OUT_OF_SERVICE => {
252                if let PropertyValue::Boolean(v) = value {
253                    self.out_of_service = v;
254                    return Ok(());
255                }
256                Err(Error::Protocol {
257                    class: ErrorClass::PROPERTY.to_raw() as u32,
258                    code: ErrorCode::INVALID_DATA_TYPE.to_raw() as u32,
259                })
260            }
261            p if p == PropertyIdentifier::DESCRIPTION => {
262                if let PropertyValue::CharacterString(s) = value {
263                    self.description = s;
264                    return Ok(());
265                }
266                Err(Error::Protocol {
267                    class: ErrorClass::PROPERTY.to_raw() as u32,
268                    code: ErrorCode::INVALID_DATA_TYPE.to_raw() as u32,
269                })
270            }
271            p if p == PropertyIdentifier::CONTROLLED_VARIABLE_REFERENCE => match value {
272                PropertyValue::Null => {
273                    self.controlled_variable_reference = None;
274                    Ok(())
275                }
276                PropertyValue::List(ref items) if items.len() >= 2 => {
277                    if let (PropertyValue::ObjectIdentifier(oid), PropertyValue::Enumerated(prop)) =
278                        (&items[0], &items[1])
279                    {
280                        self.controlled_variable_reference =
281                            Some(BACnetObjectPropertyReference::new(*oid, *prop));
282                        return Ok(());
283                    }
284                    Err(Error::Protocol {
285                        class: ErrorClass::PROPERTY.to_raw() as u32,
286                        code: ErrorCode::INVALID_DATA_TYPE.to_raw() as u32,
287                    })
288                }
289                _ => Err(Error::Protocol {
290                    class: ErrorClass::PROPERTY.to_raw() as u32,
291                    code: ErrorCode::INVALID_DATA_TYPE.to_raw() as u32,
292                }),
293            },
294            p if p == PropertyIdentifier::MANIPULATED_VARIABLE_REFERENCE => match value {
295                PropertyValue::Null => {
296                    self.manipulated_variable_reference = None;
297                    Ok(())
298                }
299                PropertyValue::List(ref items) if items.len() >= 2 => {
300                    if let (PropertyValue::ObjectIdentifier(oid), PropertyValue::Enumerated(prop)) =
301                        (&items[0], &items[1])
302                    {
303                        self.manipulated_variable_reference =
304                            Some(BACnetObjectPropertyReference::new(*oid, *prop));
305                        return Ok(());
306                    }
307                    Err(Error::Protocol {
308                        class: ErrorClass::PROPERTY.to_raw() as u32,
309                        code: ErrorCode::INVALID_DATA_TYPE.to_raw() as u32,
310                    })
311                }
312                _ => Err(Error::Protocol {
313                    class: ErrorClass::PROPERTY.to_raw() as u32,
314                    code: ErrorCode::INVALID_DATA_TYPE.to_raw() as u32,
315                }),
316            },
317            p if p == PropertyIdentifier::SETPOINT_REFERENCE => match value {
318                PropertyValue::Null => {
319                    self.setpoint_reference = None;
320                    Ok(())
321                }
322                PropertyValue::List(ref items) if items.len() >= 2 => {
323                    if let (PropertyValue::ObjectIdentifier(oid), PropertyValue::Enumerated(prop)) =
324                        (&items[0], &items[1])
325                    {
326                        self.setpoint_reference =
327                            Some(BACnetObjectPropertyReference::new(*oid, *prop));
328                        return Ok(());
329                    }
330                    Err(Error::Protocol {
331                        class: ErrorClass::PROPERTY.to_raw() as u32,
332                        code: ErrorCode::INVALID_DATA_TYPE.to_raw() as u32,
333                    })
334                }
335                _ => Err(Error::Protocol {
336                    class: ErrorClass::PROPERTY.to_raw() as u32,
337                    code: ErrorCode::INVALID_DATA_TYPE.to_raw() as u32,
338                }),
339            },
340            _ => Err(Error::Protocol {
341                class: ErrorClass::PROPERTY.to_raw() as u32,
342                code: ErrorCode::WRITE_ACCESS_DENIED.to_raw() as u32,
343            }),
344        }
345    }
346
347    fn property_list(&self) -> Cow<'static, [PropertyIdentifier]> {
348        static PROPS: &[PropertyIdentifier] = &[
349            PropertyIdentifier::OBJECT_IDENTIFIER,
350            PropertyIdentifier::OBJECT_NAME,
351            PropertyIdentifier::DESCRIPTION,
352            PropertyIdentifier::OBJECT_TYPE,
353            PropertyIdentifier::PRESENT_VALUE,
354            PropertyIdentifier::SETPOINT,
355            PropertyIdentifier::PROPORTIONAL_CONSTANT,
356            PropertyIdentifier::INTEGRAL_CONSTANT,
357            PropertyIdentifier::DERIVATIVE_CONSTANT,
358            PropertyIdentifier::OUTPUT_UNITS,
359            PropertyIdentifier::UPDATE_INTERVAL,
360            PropertyIdentifier::STATUS_FLAGS,
361            PropertyIdentifier::EVENT_STATE,
362            PropertyIdentifier::RELIABILITY,
363            PropertyIdentifier::OUT_OF_SERVICE,
364            PropertyIdentifier::CONTROLLED_VARIABLE_REFERENCE,
365            PropertyIdentifier::MANIPULATED_VARIABLE_REFERENCE,
366            PropertyIdentifier::SETPOINT_REFERENCE,
367        ];
368        Cow::Borrowed(PROPS)
369    }
370
371    fn supports_cov(&self) -> bool {
372        true
373    }
374}
375
376#[cfg(test)]
377mod tests {
378    use super::*;
379
380    #[test]
381    fn loop_read_defaults() {
382        let lo = LoopObject::new(1, "LOOP-1", 62).unwrap();
383        assert_eq!(
384            lo.read_property(PropertyIdentifier::PRESENT_VALUE, None)
385                .unwrap(),
386            PropertyValue::Real(0.0)
387        );
388        assert_eq!(
389            lo.read_property(PropertyIdentifier::SETPOINT, None)
390                .unwrap(),
391            PropertyValue::Real(0.0)
392        );
393        assert_eq!(
394            lo.read_property(PropertyIdentifier::PROPORTIONAL_CONSTANT, None)
395                .unwrap(),
396            PropertyValue::Real(1.0)
397        );
398    }
399
400    #[test]
401    fn loop_write_pid_constants() {
402        let mut lo = LoopObject::new(1, "LOOP-1", 62).unwrap();
403        lo.write_property(
404            PropertyIdentifier::SETPOINT,
405            None,
406            PropertyValue::Real(72.0),
407            None,
408        )
409        .unwrap();
410        lo.write_property(
411            PropertyIdentifier::PROPORTIONAL_CONSTANT,
412            None,
413            PropertyValue::Real(2.5),
414            None,
415        )
416        .unwrap();
417        lo.write_property(
418            PropertyIdentifier::INTEGRAL_CONSTANT,
419            None,
420            PropertyValue::Real(0.1),
421            None,
422        )
423        .unwrap();
424        lo.write_property(
425            PropertyIdentifier::DERIVATIVE_CONSTANT,
426            None,
427            PropertyValue::Real(0.05),
428            None,
429        )
430        .unwrap();
431
432        assert_eq!(
433            lo.read_property(PropertyIdentifier::SETPOINT, None)
434                .unwrap(),
435            PropertyValue::Real(72.0)
436        );
437        assert_eq!(
438            lo.read_property(PropertyIdentifier::PROPORTIONAL_CONSTANT, None)
439                .unwrap(),
440            PropertyValue::Real(2.5)
441        );
442        assert_eq!(
443            lo.read_property(PropertyIdentifier::INTEGRAL_CONSTANT, None)
444                .unwrap(),
445            PropertyValue::Real(0.1)
446        );
447        assert_eq!(
448            lo.read_property(PropertyIdentifier::DERIVATIVE_CONSTANT, None)
449                .unwrap(),
450            PropertyValue::Real(0.05)
451        );
452    }
453
454    #[test]
455    fn loop_set_present_value() {
456        let mut lo = LoopObject::new(1, "LOOP-1", 62).unwrap();
457        lo.set_present_value(55.0);
458        assert_eq!(
459            lo.read_property(PropertyIdentifier::PRESENT_VALUE, None)
460                .unwrap(),
461            PropertyValue::Real(55.0)
462        );
463    }
464
465    #[test]
466    fn loop_read_object_type() {
467        let lo = LoopObject::new(1, "LOOP-1", 62).unwrap();
468        let val = lo
469            .read_property(PropertyIdentifier::OBJECT_TYPE, None)
470            .unwrap();
471        assert_eq!(val, PropertyValue::Enumerated(ObjectType::LOOP.to_raw()));
472    }
473
474    #[test]
475    fn loop_write_wrong_type_rejected() {
476        let mut lo = LoopObject::new(1, "LOOP-1", 62).unwrap();
477        let result = lo.write_property(
478            PropertyIdentifier::SETPOINT,
479            None,
480            PropertyValue::Unsigned(72),
481            None,
482        );
483        assert!(result.is_err());
484    }
485
486    // --- Property reference tests ---
487
488    #[test]
489    fn loop_references_default_to_null() {
490        let lo = LoopObject::new(1, "LOOP-1", 62).unwrap();
491        assert_eq!(
492            lo.read_property(PropertyIdentifier::CONTROLLED_VARIABLE_REFERENCE, None)
493                .unwrap(),
494            PropertyValue::Null
495        );
496        assert_eq!(
497            lo.read_property(PropertyIdentifier::MANIPULATED_VARIABLE_REFERENCE, None)
498                .unwrap(),
499            PropertyValue::Null
500        );
501        assert_eq!(
502            lo.read_property(PropertyIdentifier::SETPOINT_REFERENCE, None)
503                .unwrap(),
504            PropertyValue::Null
505        );
506    }
507
508    #[test]
509    fn loop_set_controlled_variable_reference_read_back() {
510        let mut lo = LoopObject::new(1, "LOOP-1", 62).unwrap();
511        let oid = ObjectIdentifier::new(ObjectType::ANALOG_INPUT, 5).unwrap();
512        let prop_raw = PropertyIdentifier::PRESENT_VALUE.to_raw();
513        lo.set_controlled_variable_reference(BACnetObjectPropertyReference::new(oid, prop_raw));
514
515        let val = lo
516            .read_property(PropertyIdentifier::CONTROLLED_VARIABLE_REFERENCE, None)
517            .unwrap();
518        assert_eq!(
519            val,
520            PropertyValue::List(vec![
521                PropertyValue::ObjectIdentifier(oid),
522                PropertyValue::Enumerated(prop_raw),
523            ])
524        );
525    }
526
527    #[test]
528    fn loop_set_manipulated_variable_reference_read_back() {
529        let mut lo = LoopObject::new(1, "LOOP-1", 62).unwrap();
530        let oid = ObjectIdentifier::new(ObjectType::ANALOG_OUTPUT, 3).unwrap();
531        let prop_raw = PropertyIdentifier::PRESENT_VALUE.to_raw();
532        lo.set_manipulated_variable_reference(BACnetObjectPropertyReference::new(oid, prop_raw));
533
534        let val = lo
535            .read_property(PropertyIdentifier::MANIPULATED_VARIABLE_REFERENCE, None)
536            .unwrap();
537        assert_eq!(
538            val,
539            PropertyValue::List(vec![
540                PropertyValue::ObjectIdentifier(oid),
541                PropertyValue::Enumerated(prop_raw),
542            ])
543        );
544    }
545
546    #[test]
547    fn loop_set_setpoint_reference_read_back() {
548        let mut lo = LoopObject::new(1, "LOOP-1", 62).unwrap();
549        let oid = ObjectIdentifier::new(ObjectType::ANALOG_VALUE, 10).unwrap();
550        let prop_raw = PropertyIdentifier::PRESENT_VALUE.to_raw();
551        lo.set_setpoint_reference(BACnetObjectPropertyReference::new(oid, prop_raw));
552
553        let val = lo
554            .read_property(PropertyIdentifier::SETPOINT_REFERENCE, None)
555            .unwrap();
556        assert_eq!(
557            val,
558            PropertyValue::List(vec![
559                PropertyValue::ObjectIdentifier(oid),
560                PropertyValue::Enumerated(prop_raw),
561            ])
562        );
563    }
564
565    #[test]
566    fn loop_references_in_property_list() {
567        let lo = LoopObject::new(1, "LOOP-1", 62).unwrap();
568        let list = lo.property_list();
569        assert!(list.contains(&PropertyIdentifier::CONTROLLED_VARIABLE_REFERENCE));
570        assert!(list.contains(&PropertyIdentifier::MANIPULATED_VARIABLE_REFERENCE));
571        assert!(list.contains(&PropertyIdentifier::SETPOINT_REFERENCE));
572    }
573
574    #[test]
575    fn loop_write_reference_via_write_property() {
576        let mut lo = LoopObject::new(1, "LOOP-1", 62).unwrap();
577        let oid = ObjectIdentifier::new(ObjectType::ANALOG_INPUT, 7).unwrap();
578        let prop_raw = PropertyIdentifier::PRESENT_VALUE.to_raw();
579
580        lo.write_property(
581            PropertyIdentifier::CONTROLLED_VARIABLE_REFERENCE,
582            None,
583            PropertyValue::List(vec![
584                PropertyValue::ObjectIdentifier(oid),
585                PropertyValue::Enumerated(prop_raw),
586            ]),
587            None,
588        )
589        .unwrap();
590
591        assert_eq!(
592            lo.read_property(PropertyIdentifier::CONTROLLED_VARIABLE_REFERENCE, None)
593                .unwrap(),
594            PropertyValue::List(vec![
595                PropertyValue::ObjectIdentifier(oid),
596                PropertyValue::Enumerated(prop_raw),
597            ])
598        );
599    }
600
601    #[test]
602    fn loop_write_null_clears_reference() {
603        let mut lo = LoopObject::new(1, "LOOP-1", 62).unwrap();
604        let oid = ObjectIdentifier::new(ObjectType::ANALOG_INPUT, 1).unwrap();
605        lo.set_controlled_variable_reference(BACnetObjectPropertyReference::new(
606            oid,
607            PropertyIdentifier::PRESENT_VALUE.to_raw(),
608        ));
609
610        // Verify it is set
611        assert_ne!(
612            lo.read_property(PropertyIdentifier::CONTROLLED_VARIABLE_REFERENCE, None)
613                .unwrap(),
614            PropertyValue::Null
615        );
616
617        // Write Null to clear
618        lo.write_property(
619            PropertyIdentifier::CONTROLLED_VARIABLE_REFERENCE,
620            None,
621            PropertyValue::Null,
622            None,
623        )
624        .unwrap();
625
626        assert_eq!(
627            lo.read_property(PropertyIdentifier::CONTROLLED_VARIABLE_REFERENCE, None)
628                .unwrap(),
629            PropertyValue::Null
630        );
631    }
632
633    #[test]
634    fn loop_write_reference_wrong_type_rejected() {
635        let mut lo = LoopObject::new(1, "LOOP-1", 62).unwrap();
636        let result = lo.write_property(
637            PropertyIdentifier::CONTROLLED_VARIABLE_REFERENCE,
638            None,
639            PropertyValue::Unsigned(42),
640            None,
641        );
642        assert!(result.is_err());
643    }
644}