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