Skip to main content

bacnet_objects/
staging.rs

1//! Local-target Staging object (type 60) per ASHRAE 135-2020 Clause 12.62.
2//!
3//! The object owns threshold/hysteresis evaluation only. It queues guarded
4//! local target-write plans for the server, which executes them after releasing
5//! the source-object database mutation lock.
6
7use std::borrow::Cow;
8
9use bacnet_encoding::constructed::{decode_stage_limit_value, encode_stage_limit_value};
10use bacnet_types::constructed::{BACnetDeviceObjectReference, BACnetStageLimitValue};
11use bacnet_types::enums::{
12    ErrorClass, ErrorCode, EventState, ObjectType, PropertyIdentifier, Reliability,
13};
14use bacnet_types::error::Error;
15use bacnet_types::primitives::{ObjectIdentifier, PropertyValue, StatusFlags};
16use bytes::BytesMut;
17
18use crate::common::{self, read_common_properties};
19use crate::device_reference;
20use crate::property_metadata::{
21    property_list_from_metadata, PropertyConformance, PropertyMetadata, PropertyWriteCapability,
22};
23use crate::traits::BACnetObject;
24
25mod metadata;
26use metadata::STAGING_PROPERTY_METADATA;
27
28/// Complete atomic configuration for a [`StagingObject`].
29///
30/// Target references are deliberately local-only. A reference carrying a
31/// `device_identifier` is rejected rather than initiating remote BACnet I/O.
32/// The bundled server drops a Device identifier naming its own Device from a
33/// written reference before the object sees it (#1136).
34#[derive(Debug, Clone, PartialEq)]
35pub struct StagingConfig {
36    /// Initial REAL `Present_Value`; finite values are clamped to the ladder.
37    pub present_value: f32,
38    /// `Min_Pres_Value`, strictly below the first stage's lower deadband edge.
39    pub min_present_value: f32,
40    /// Engineering-units enumeration stored by `Units`.
41    pub units: u32,
42    /// Command priority used for local target writes (1..=16).
43    pub priority_for_writing: u8,
44    /// At least two ordered stage-limit values.
45    pub stages: Vec<BACnetStageLimitValue>,
46    /// Local Binary Output, Binary Value, or Binary Lighting Output targets.
47    pub target_references: Vec<BACnetDeviceObjectReference>,
48    /// Optional names, exactly one per stage when present.
49    pub stage_names: Option<Vec<String>>,
50}
51
52/// One server-owned local target mutation selected by a Staging transition.
53#[doc(hidden)]
54#[derive(Debug, Clone, PartialEq, Eq)]
55pub struct StagingTargetWrite {
56    /// Local target object.
57    pub object_identifier: ObjectIdentifier,
58    /// Whether the target receives ACTIVE rather than INACTIVE.
59    pub active: bool,
60}
61
62/// Guarded work emitted after a Staging source mutation.
63#[doc(hidden)]
64#[derive(Debug, Clone, PartialEq, Eq)]
65pub struct StagingWritePlan {
66    /// Staging source whose generation guards completion.
67    pub source: ObjectIdentifier,
68    /// Source generation that selected this work.
69    pub generation: u64,
70    /// BACnet priority used for every target write.
71    pub priority: u8,
72    /// Initialized local targets in configured order.
73    pub writes: Vec<StagingTargetWrite>,
74}
75
76/// BACnet Staging object with an explicit, validated stage ladder.
77pub struct StagingObject {
78    oid: ObjectIdentifier,
79    name: String,
80    description: String,
81    present_value: f32,
82    present_stage: u64,
83    stages: Vec<BACnetStageLimitValue>,
84    stage_names: Option<Vec<String>>,
85    target_references: Vec<BACnetDeviceObjectReference>,
86    priority_for_writing: u8,
87    min_present_value: f32,
88    units: u32,
89    cov_increment: f32,
90    status_flags: StatusFlags,
91    out_of_service: bool,
92    reliability: Reliability,
93    generation: u64,
94    pending_plan: Option<StagingWritePlan>,
95}
96
97impl StagingObject {
98    /// Construct an atomically validated Staging object.
99    ///
100    /// The object begins with `Present_Stage` uninitialized (0). The bundled
101    /// server initializes and applies it during startup; a direct
102    /// `Present_Value` write also evaluates it. No default ladder or targets
103    /// are invented.
104    pub fn new(
105        instance: u32,
106        name: impl Into<String>,
107        config: StagingConfig,
108    ) -> Result<Self, Error> {
109        validate_config(&config)?;
110        let oid = ObjectIdentifier::new(ObjectType::STAGING, instance)?;
111        let present_value = config.present_value.clamp(
112            config.min_present_value,
113            config.stages.last().unwrap().limit,
114        );
115        Ok(Self {
116            oid,
117            name: name.into(),
118            description: String::new(),
119            present_value,
120            present_stage: 0,
121            stages: config.stages,
122            stage_names: config.stage_names,
123            target_references: config.target_references,
124            priority_for_writing: config.priority_for_writing,
125            min_present_value: config.min_present_value,
126            units: config.units,
127            cov_increment: 0.0,
128            status_flags: StatusFlags::empty(),
129            out_of_service: false,
130            reliability: Reliability::NO_FAULT_DETECTED,
131            generation: 0,
132            pending_plan: None,
133        })
134    }
135
136    fn max_present_value(&self) -> f32 {
137        self.stages
138            .last()
139            .expect("validated Staging configuration has at least two stages")
140            .limit
141    }
142
143    fn select_stage(&self, value: f32) -> u64 {
144        self.stages
145            .iter()
146            .position(|stage| stage.limit >= value)
147            .map_or(self.stages.len() as u64, |index| index as u64 + 1)
148    }
149
150    fn retained_by_hysteresis(&self, value: f32) -> bool {
151        let Some(index) = self
152            .present_stage
153            .checked_sub(1)
154            .map(|stage| stage as usize)
155        else {
156            return false;
157        };
158        let Some(stage) = self.stages.get(index) else {
159            return false;
160        };
161        let lower = if index == 0 {
162            self.min_present_value
163        } else {
164            let prior = &self.stages[index - 1];
165            prior.limit - prior.deadband
166        };
167        value >= lower && value <= stage.limit + stage.deadband
168    }
169
170    fn evaluate(&mut self, force_reapply: bool) {
171        let previous = self.present_stage;
172        if previous != 0 && self.retained_by_hysteresis(self.present_value) && !force_reapply {
173            return;
174        }
175        let selected = self.select_stage(self.present_value);
176        self.present_stage = selected;
177        if previous != selected || force_reapply {
178            self.queue_current_stage_plan();
179        }
180    }
181
182    fn queue_current_stage_plan(&mut self) {
183        self.generation = self.generation.wrapping_add(1);
184        if self.out_of_service || self.present_stage == 0 {
185            self.pending_plan = None;
186            return;
187        }
188        let values = &self.stages[self.present_stage as usize - 1].values;
189        let writes = self
190            .target_references
191            .iter()
192            .zip(values)
193            .filter(|(reference, _)| {
194                reference.object_identifier.instance_number() != ObjectIdentifier::MAX_INSTANCE
195            })
196            .map(|(reference, active)| StagingTargetWrite {
197                object_identifier: reference.object_identifier,
198                active: *active,
199            })
200            .collect();
201        self.pending_plan = Some(StagingWritePlan {
202            source: self.oid,
203            generation: self.generation,
204            priority: self.priority_for_writing,
205            writes,
206        });
207    }
208
209    fn reset_and_reevaluate(&mut self) {
210        self.present_stage = 0;
211        self.present_value = self
212            .present_value
213            .clamp(self.min_present_value, self.max_present_value());
214        self.evaluate(true);
215    }
216
217    fn replace_stages(
218        &mut self,
219        array_index: Option<u32>,
220        value: PropertyValue,
221    ) -> Result<(), Error> {
222        let mut candidate = self.stages.clone();
223        match array_index {
224            None => {
225                let PropertyValue::List(values) = value else {
226                    return Err(common::invalid_data_type_error());
227                };
228                if values.len() != candidate.len() {
229                    return Err(common::value_out_of_range_error());
230                }
231                candidate = values
232                    .into_iter()
233                    .map(decode_stage_property_value)
234                    .collect::<Result<_, _>>()?;
235            }
236            Some(0) => return Err(common::write_access_denied_error()),
237            Some(index) => {
238                let Some(stage) = candidate.get_mut((index - 1) as usize) else {
239                    return Err(common::invalid_array_index_error());
240                };
241                *stage = decode_stage_property_value(value)?;
242            }
243        }
244        validate_ladder(
245            &candidate,
246            self.min_present_value,
247            self.target_references.len(),
248        )?;
249        if candidate != self.stages {
250            self.stages = candidate;
251            self.reset_and_reevaluate();
252        }
253        Ok(())
254    }
255
256    fn replace_target_references(
257        &mut self,
258        array_index: Option<u32>,
259        value: PropertyValue,
260    ) -> Result<(), Error> {
261        let mut candidate = self.target_references.clone();
262        match array_index {
263            None => {
264                let references: Vec<BACnetDeviceObjectReference> =
265                    device_reference::decode_references(&value)?;
266                if references.len() != candidate.len() {
267                    return Err(common::value_out_of_range_error());
268                }
269                candidate = references;
270            }
271            Some(0) => return Err(common::write_access_denied_error()),
272            Some(index) => {
273                let Some(reference) = candidate.get_mut((index - 1) as usize) else {
274                    return Err(common::invalid_array_index_error());
275                };
276                *reference = device_reference::decode_reference(&value)?;
277            }
278        }
279        validate_target_references(&candidate)?;
280        if candidate != self.target_references {
281            self.target_references = candidate;
282            self.reset_and_reevaluate();
283        }
284        Ok(())
285    }
286
287    fn replace_stage_names(
288        &mut self,
289        array_index: Option<u32>,
290        value: PropertyValue,
291    ) -> Result<(), Error> {
292        let Some(names) = &self.stage_names else {
293            return Err(if array_index.is_none() {
294                common::unknown_property_error()
295            } else {
296                common::write_access_denied_error()
297            });
298        };
299        let mut candidate = names.clone();
300        match array_index {
301            None => {
302                let PropertyValue::List(values) = value else {
303                    return Err(common::invalid_data_type_error());
304                };
305                if values.len() != candidate.len() {
306                    return Err(common::value_out_of_range_error());
307                }
308                candidate = values
309                    .into_iter()
310                    .map(|value| match value {
311                        PropertyValue::CharacterString(name) => Ok(name),
312                        _ => Err(common::invalid_data_type_error()),
313                    })
314                    .collect::<Result<_, _>>()?;
315            }
316            Some(0) => return Err(common::write_access_denied_error()),
317            Some(index) => {
318                let Some(name) = candidate.get_mut((index - 1) as usize) else {
319                    return Err(common::invalid_array_index_error());
320                };
321                let PropertyValue::CharacterString(value) = value else {
322                    return Err(common::invalid_data_type_error());
323                };
324                *name = value;
325            }
326        }
327        self.stage_names = Some(candidate);
328        Ok(())
329    }
330
331    fn metadata(&self) -> Vec<PropertyMetadata> {
332        let mut rows = STAGING_PROPERTY_METADATA.to_vec();
333        if self.stage_names.is_some() {
334            let target_index = rows
335                .iter()
336                .position(|row| row.property_identifier == PropertyIdentifier::TARGET_REFERENCES)
337                .unwrap();
338            rows.insert(
339                target_index,
340                PropertyMetadata::new(
341                    PropertyIdentifier::STAGE_NAMES,
342                    PropertyConformance::Optional,
343                    None,
344                    PropertyWriteCapability::Always,
345                ),
346            );
347        }
348        rows
349    }
350}
351
352impl BACnetObject for StagingObject {
353    fn object_identifier(&self) -> ObjectIdentifier {
354        self.oid
355    }
356
357    fn object_name(&self) -> &str {
358        &self.name
359    }
360
361    fn read_property(
362        &self,
363        property: PropertyIdentifier,
364        array_index: Option<u32>,
365    ) -> Result<PropertyValue, Error> {
366        if let Some(result) = read_common_properties!(self, property, array_index) {
367            return result;
368        }
369        match property {
370            PropertyIdentifier::OBJECT_TYPE => {
371                Ok(PropertyValue::Enumerated(ObjectType::STAGING.to_raw()))
372            }
373            PropertyIdentifier::PRESENT_VALUE => Ok(PropertyValue::Real(self.present_value)),
374            PropertyIdentifier::PRESENT_STAGE if self.present_stage == 0 => Err(protocol_error(
375                ErrorClass::PROPERTY,
376                ErrorCode::VALUE_NOT_INITIALIZED,
377            )),
378            PropertyIdentifier::PRESENT_STAGE => Ok(PropertyValue::Unsigned(self.present_stage)),
379            PropertyIdentifier::STAGES => common::read_array(
380                self.stages
381                    .iter()
382                    .map(|stage| {
383                        let mut encoded = BytesMut::new();
384                        encode_stage_limit_value(&mut encoded, stage);
385                        PropertyValue::ApplicationData(encoded.to_vec())
386                    })
387                    .collect(),
388                array_index,
389            ),
390            PropertyIdentifier::STAGE_NAMES => {
391                let Some(names) = &self.stage_names else {
392                    return Err(common::unknown_property_error());
393                };
394                common::read_array(
395                    names
396                        .iter()
397                        .cloned()
398                        .map(PropertyValue::CharacterString)
399                        .collect(),
400                    array_index,
401                )
402            }
403            PropertyIdentifier::TARGET_REFERENCES => common::read_array(
404                device_reference::reference_elements(&self.target_references),
405                array_index,
406            ),
407            PropertyIdentifier::EVENT_STATE => {
408                Ok(PropertyValue::Enumerated(EventState::NORMAL.to_raw()))
409            }
410            PropertyIdentifier::UNITS => Ok(PropertyValue::Enumerated(self.units)),
411            PropertyIdentifier::PRIORITY_FOR_WRITING => {
412                Ok(PropertyValue::Unsigned(self.priority_for_writing.into()))
413            }
414            PropertyIdentifier::MIN_PRES_VALUE => Ok(PropertyValue::Real(self.min_present_value)),
415            PropertyIdentifier::MAX_PRES_VALUE => Ok(PropertyValue::Real(self.max_present_value())),
416            PropertyIdentifier::COV_INCREMENT => Ok(PropertyValue::Real(self.cov_increment)),
417            _ => Err(common::unknown_property_error()),
418        }
419    }
420
421    fn write_property(
422        &mut self,
423        property: PropertyIdentifier,
424        array_index: Option<u32>,
425        value: PropertyValue,
426        _priority: Option<u8>,
427    ) -> Result<(), Error> {
428        if property != PropertyIdentifier::STAGES
429            && property != PropertyIdentifier::STAGE_NAMES
430            && property != PropertyIdentifier::TARGET_REFERENCES
431            && array_index.is_some()
432        {
433            return Err(common::property_is_not_an_array_error());
434        }
435        if let Some(result) = common::write_object_name(&mut self.name, property, &value) {
436            return result;
437        }
438        if let Some(result) = common::write_description(&mut self.description, property, &value) {
439            return result;
440        }
441        if let Some(result) = common::write_cov_increment(&mut self.cov_increment, property, &value)
442        {
443            return result;
444        }
445        match property {
446            PropertyIdentifier::PRESENT_VALUE => {
447                let PropertyValue::Real(value) = value else {
448                    return Err(common::invalid_data_type_error());
449                };
450                common::reject_non_finite(value)?;
451                self.present_value = value.clamp(self.min_present_value, self.max_present_value());
452                self.evaluate(false);
453                Ok(())
454            }
455            PropertyIdentifier::OUT_OF_SERVICE => {
456                let PropertyValue::Boolean(value) = value else {
457                    return Err(common::invalid_data_type_error());
458                };
459                if value == self.out_of_service {
460                    return Ok(());
461                }
462                self.out_of_service = value;
463                if value {
464                    self.generation = self.generation.wrapping_add(1);
465                    self.pending_plan = None;
466                } else {
467                    self.evaluate(true);
468                }
469                Ok(())
470            }
471            PropertyIdentifier::RELIABILITY => {
472                if !self.out_of_service {
473                    return Err(common::write_access_denied_error());
474                }
475                let PropertyValue::Enumerated(raw) = value else {
476                    return Err(common::invalid_data_type_error());
477                };
478                let value = Reliability::from_raw(raw);
479                if !common::is_reliability_value_valid(value) {
480                    return Err(common::value_out_of_range_error());
481                }
482                self.reliability = value;
483                Ok(())
484            }
485            PropertyIdentifier::STAGES => self.replace_stages(array_index, value),
486            PropertyIdentifier::STAGE_NAMES => self.replace_stage_names(array_index, value),
487            PropertyIdentifier::TARGET_REFERENCES => {
488                self.replace_target_references(array_index, value)
489            }
490            PropertyIdentifier::PRIORITY_FOR_WRITING => {
491                let PropertyValue::Unsigned(value) = value else {
492                    return Err(common::invalid_data_type_error());
493                };
494                let priority = u8::try_from(value)
495                    .ok()
496                    .filter(|value| (1..=16).contains(value))
497                    .ok_or_else(common::value_out_of_range_error)?;
498                if priority != self.priority_for_writing {
499                    self.priority_for_writing = priority;
500                    self.reset_and_reevaluate();
501                }
502                Ok(())
503            }
504            PropertyIdentifier::MIN_PRES_VALUE => {
505                let PropertyValue::Real(value) = value else {
506                    return Err(common::invalid_data_type_error());
507                };
508                common::reject_non_finite(value)?;
509                validate_min_present_value(value, &self.stages)?;
510                if value != self.min_present_value {
511                    self.min_present_value = value;
512                    self.reset_and_reevaluate();
513                }
514                Ok(())
515            }
516            _ => Err(crate::common::unhandled_write_error(
517                self.property_metadata().as_ref(),
518                property,
519                array_index,
520            )),
521        }
522    }
523
524    fn property_metadata(&self) -> Cow<'_, [PropertyMetadata]> {
525        Cow::Owned(self.metadata())
526    }
527
528    fn property_list(&self) -> Cow<'static, [PropertyIdentifier]> {
529        property_list_from_metadata(&self.metadata())
530    }
531
532    // Table 13-1 Staging row: Present_Value by COV_Increment, Status_Flags and
533    // Present_Stage trigger, and Present_Stage is reported (the trait default).
534    fn supports_cov(&self) -> bool {
535        true
536    }
537
538    fn cov_increment(&self) -> Option<f64> {
539        Some(f64::from(self.cov_increment))
540    }
541
542    fn take_staging_write_plan_internal(&mut self) -> Option<StagingWritePlan> {
543        if self.present_stage == 0 {
544            self.evaluate(false);
545        }
546        self.pending_plan.take()
547    }
548
549    fn staging_generation_internal(&self) -> Option<u64> {
550        Some(self.generation)
551    }
552
553    fn complete_staging_write_plan_internal(&mut self, generation: u64, success: bool) -> bool {
554        if generation != self.generation || self.out_of_service {
555            return false;
556        }
557        let reliability = if success {
558            Reliability::NO_FAULT_DETECTED
559        } else {
560            Reliability::UNRELIABLE_OTHER
561        };
562        if reliability == self.reliability {
563            return false;
564        }
565        self.reliability = reliability;
566        true
567    }
568}
569
570fn decode_stage_property_value(value: PropertyValue) -> Result<BACnetStageLimitValue, Error> {
571    let PropertyValue::ApplicationData(bytes) = value else {
572        return Err(common::invalid_data_type_error());
573    };
574    let (stage, consumed) =
575        decode_stage_limit_value(&bytes, 0).map_err(|_| common::invalid_data_encoding_error())?;
576    if consumed != bytes.len() {
577        return Err(common::invalid_data_encoding_error());
578    }
579    Ok(stage)
580}
581
582fn validate_config(config: &StagingConfig) -> Result<(), Error> {
583    common::reject_non_finite(config.present_value)?;
584    if !(1..=16).contains(&config.priority_for_writing) {
585        return Err(common::value_out_of_range_error());
586    }
587    if let Some(names) = &config.stage_names {
588        if names.len() != config.stages.len() {
589            return Err(common::value_out_of_range_error());
590        }
591    }
592    validate_target_references(&config.target_references)?;
593    validate_ladder(
594        &config.stages,
595        config.min_present_value,
596        config.target_references.len(),
597    )
598}
599
600fn validate_target_references(references: &[BACnetDeviceObjectReference]) -> Result<(), Error> {
601    for reference in references {
602        // A device member that isn't a Device makes no reference (#1285).
603        // The object can't tell which Device holds it, so any other Device
604        // member is refused too; the server localizes one naming itself
605        // (#1136).
606        device_reference::check_local_member(reference.device_identifier)?;
607        if !matches!(
608            reference.object_identifier.object_type(),
609            ObjectType::BINARY_OUTPUT
610                | ObjectType::BINARY_VALUE
611                | ObjectType::BINARY_LIGHTING_OUTPUT
612        ) {
613            return Err(common::value_out_of_range_error());
614        }
615    }
616    Ok(())
617}
618
619fn validate_ladder(
620    stages: &[BACnetStageLimitValue],
621    min_present_value: f32,
622    target_count: usize,
623) -> Result<(), Error> {
624    if stages.len() <= 1 {
625        return Err(common::value_out_of_range_error());
626    }
627    for stage in stages {
628        if !stage.limit.is_finite() || !stage.deadband.is_finite() || stage.deadband < 0.0 {
629            return Err(common::value_out_of_range_error());
630        }
631        if stage.values.len() != target_count {
632            return Err(common::value_out_of_range_error());
633        }
634    }
635    for adjacent in stages.windows(2) {
636        if adjacent[0].limit >= adjacent[1].limit {
637            return Err(common::value_out_of_range_error());
638        }
639        if adjacent[0].limit + adjacent[0].deadband > adjacent[1].limit - adjacent[1].deadband {
640            return Err(common::value_out_of_range_error());
641        }
642    }
643    validate_min_present_value(min_present_value, stages)
644}
645
646fn validate_min_present_value(
647    min_present_value: f32,
648    stages: &[BACnetStageLimitValue],
649) -> Result<(), Error> {
650    if !min_present_value.is_finite() || min_present_value >= stages[0].limit - stages[0].deadband {
651        return Err(common::value_out_of_range_error());
652    }
653    Ok(())
654}
655
656fn protocol_error(class: ErrorClass, code: ErrorCode) -> Error {
657    Error::Protocol {
658        class: class.to_raw() as u32,
659        code: code.to_raw() as u32,
660    }
661}
662
663#[cfg(test)]
664mod tests;