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