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::{
10    decode_device_object_reference, decode_stage_limit_value, encode_device_object_reference,
11    encode_stage_limit_value,
12};
13use bacnet_types::constructed::{BACnetDeviceObjectReference, BACnetStageLimitValue};
14use bacnet_types::enums::{
15    ErrorClass, ErrorCode, EventState, ObjectType, PropertyIdentifier, Reliability,
16};
17use bacnet_types::error::Error;
18use bacnet_types::primitives::{ObjectIdentifier, PropertyValue, StatusFlags};
19use bytes::BytesMut;
20
21use crate::common::{self, read_common_properties};
22use crate::property_metadata::{
23    property_list_from_metadata, PropertyConformance, PropertyMetadata, PropertyWriteCapability,
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#[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    status_flags: StatusFlags,
90    out_of_service: bool,
91    reliability: u32,
92    generation: u64,
93    pending_plan: Option<StagingWritePlan>,
94}
95
96impl StagingObject {
97    /// Construct an atomically validated Staging object.
98    ///
99    /// The object begins with `Present_Stage` uninitialized (0). The bundled
100    /// server initializes and applies it during startup; a direct
101    /// `Present_Value` write also evaluates it. No default ladder or targets
102    /// are invented.
103    pub fn new(
104        instance: u32,
105        name: impl Into<String>,
106        config: StagingConfig,
107    ) -> Result<Self, Error> {
108        validate_config(&config)?;
109        let oid = ObjectIdentifier::new(ObjectType::STAGING, instance)?;
110        let present_value = config.present_value.clamp(
111            config.min_present_value,
112            config.stages.last().unwrap().limit,
113        );
114        Ok(Self {
115            oid,
116            name: name.into(),
117            description: String::new(),
118            present_value,
119            present_stage: 0,
120            stages: config.stages,
121            stage_names: config.stage_names,
122            target_references: config.target_references,
123            priority_for_writing: config.priority_for_writing,
124            min_present_value: config.min_present_value,
125            units: config.units,
126            status_flags: StatusFlags::empty(),
127            out_of_service: false,
128            reliability: Reliability::NO_FAULT_DETECTED.to_raw(),
129            generation: 0,
130            pending_plan: None,
131        })
132    }
133
134    fn max_present_value(&self) -> f32 {
135        self.stages
136            .last()
137            .expect("validated Staging configuration has at least two stages")
138            .limit
139    }
140
141    fn select_stage(&self, value: f32) -> u64 {
142        self.stages
143            .iter()
144            .position(|stage| stage.limit >= value)
145            .map_or(self.stages.len() as u64, |index| index as u64 + 1)
146    }
147
148    fn retained_by_hysteresis(&self, value: f32) -> bool {
149        let Some(index) = self
150            .present_stage
151            .checked_sub(1)
152            .map(|stage| stage as usize)
153        else {
154            return false;
155        };
156        let Some(stage) = self.stages.get(index) else {
157            return false;
158        };
159        let lower = if index == 0 {
160            self.min_present_value
161        } else {
162            let prior = &self.stages[index - 1];
163            prior.limit - prior.deadband
164        };
165        value >= lower && value <= stage.limit + stage.deadband
166    }
167
168    fn evaluate(&mut self, force_reapply: bool) {
169        let previous = self.present_stage;
170        if previous != 0 && self.retained_by_hysteresis(self.present_value) && !force_reapply {
171            return;
172        }
173        let selected = self.select_stage(self.present_value);
174        self.present_stage = selected;
175        if previous != selected || force_reapply {
176            self.queue_current_stage_plan();
177        }
178    }
179
180    fn queue_current_stage_plan(&mut self) {
181        self.generation = self.generation.wrapping_add(1);
182        if self.out_of_service || self.present_stage == 0 {
183            self.pending_plan = None;
184            return;
185        }
186        let values = &self.stages[self.present_stage as usize - 1].values;
187        let writes = self
188            .target_references
189            .iter()
190            .zip(values)
191            .filter(|(reference, _)| {
192                reference.object_identifier.instance_number() != ObjectIdentifier::MAX_INSTANCE
193            })
194            .map(|(reference, active)| StagingTargetWrite {
195                object_identifier: reference.object_identifier,
196                active: *active,
197            })
198            .collect();
199        self.pending_plan = Some(StagingWritePlan {
200            source: self.oid,
201            generation: self.generation,
202            priority: self.priority_for_writing,
203            writes,
204        });
205    }
206
207    fn reset_and_reevaluate(&mut self) {
208        self.present_stage = 0;
209        self.present_value = self
210            .present_value
211            .clamp(self.min_present_value, self.max_present_value());
212        self.evaluate(true);
213    }
214
215    fn replace_stages(
216        &mut self,
217        array_index: Option<u32>,
218        value: PropertyValue,
219    ) -> Result<(), Error> {
220        let mut candidate = self.stages.clone();
221        match array_index {
222            None => {
223                let PropertyValue::List(values) = value else {
224                    return Err(common::invalid_data_type_error());
225                };
226                if values.len() != candidate.len() {
227                    return Err(common::value_out_of_range_error());
228                }
229                candidate = values
230                    .into_iter()
231                    .map(decode_stage_property_value)
232                    .collect::<Result<_, _>>()?;
233            }
234            Some(0) => return Err(common::write_access_denied_error()),
235            Some(index) => {
236                let Some(stage) = candidate.get_mut((index - 1) as usize) else {
237                    return Err(common::invalid_array_index_error());
238                };
239                *stage = decode_stage_property_value(value)?;
240            }
241        }
242        validate_ladder(
243            &candidate,
244            self.min_present_value,
245            self.target_references.len(),
246        )?;
247        if candidate != self.stages {
248            self.stages = candidate;
249            self.reset_and_reevaluate();
250        }
251        Ok(())
252    }
253
254    fn replace_target_references(
255        &mut self,
256        array_index: Option<u32>,
257        value: PropertyValue,
258    ) -> Result<(), Error> {
259        let mut candidate = self.target_references.clone();
260        match array_index {
261            None => {
262                let PropertyValue::List(values) = value else {
263                    return Err(common::invalid_data_type_error());
264                };
265                if values.len() != candidate.len() {
266                    return Err(common::value_out_of_range_error());
267                }
268                candidate = values
269                    .into_iter()
270                    .map(decode_reference_property_value)
271                    .collect::<Result<_, _>>()?;
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 = decode_reference_property_value(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(common::write_access_denied_error());
296        };
297        let mut candidate = names.clone();
298        match array_index {
299            None => {
300                let PropertyValue::List(values) = value else {
301                    return Err(common::invalid_data_type_error());
302                };
303                if values.len() != candidate.len() {
304                    return Err(common::value_out_of_range_error());
305                }
306                candidate = values
307                    .into_iter()
308                    .map(|value| match value {
309                        PropertyValue::CharacterString(name) => Ok(name),
310                        _ => Err(common::invalid_data_type_error()),
311                    })
312                    .collect::<Result<_, _>>()?;
313            }
314            Some(0) => return Err(common::write_access_denied_error()),
315            Some(index) => {
316                let Some(name) = candidate.get_mut((index - 1) as usize) else {
317                    return Err(common::invalid_array_index_error());
318                };
319                let PropertyValue::CharacterString(value) = value else {
320                    return Err(common::invalid_data_type_error());
321                };
322                *name = value;
323            }
324        }
325        self.stage_names = Some(candidate);
326        Ok(())
327    }
328
329    fn metadata(&self) -> Vec<PropertyMetadata> {
330        let mut rows = STAGING_PROPERTY_METADATA.to_vec();
331        if self.stage_names.is_some() {
332            let target_index = rows
333                .iter()
334                .position(|row| row.property_identifier == PropertyIdentifier::TARGET_REFERENCES)
335                .unwrap();
336            rows.insert(
337                target_index,
338                PropertyMetadata::new(
339                    PropertyIdentifier::STAGE_NAMES,
340                    PropertyConformance::Optional,
341                    None,
342                    PropertyWriteCapability::Always,
343                ),
344            );
345        }
346        rows
347    }
348}
349
350impl BACnetObject for StagingObject {
351    fn object_identifier(&self) -> ObjectIdentifier {
352        self.oid
353    }
354
355    fn object_name(&self) -> &str {
356        &self.name
357    }
358
359    fn read_property(
360        &self,
361        property: PropertyIdentifier,
362        array_index: Option<u32>,
363    ) -> Result<PropertyValue, Error> {
364        if let Some(result) = read_common_properties!(self, property, array_index) {
365            return result;
366        }
367        match property {
368            PropertyIdentifier::OBJECT_TYPE => {
369                Ok(PropertyValue::Enumerated(ObjectType::STAGING.to_raw()))
370            }
371            PropertyIdentifier::PRESENT_VALUE => Ok(PropertyValue::Real(self.present_value)),
372            PropertyIdentifier::PRESENT_STAGE if self.present_stage == 0 => Err(protocol_error(
373                ErrorClass::PROPERTY,
374                ErrorCode::VALUE_NOT_INITIALIZED,
375            )),
376            PropertyIdentifier::PRESENT_STAGE => Ok(PropertyValue::Unsigned(self.present_stage)),
377            PropertyIdentifier::STAGES => read_encoded_array(
378                self.stages
379                    .iter()
380                    .map(|stage| {
381                        let mut encoded = BytesMut::new();
382                        encode_stage_limit_value(&mut encoded, stage);
383                        PropertyValue::ApplicationData(encoded.to_vec())
384                    })
385                    .collect(),
386                array_index,
387            ),
388            PropertyIdentifier::STAGE_NAMES => {
389                let Some(names) = &self.stage_names else {
390                    return Err(common::unknown_property_error());
391                };
392                read_encoded_array(
393                    names
394                        .iter()
395                        .cloned()
396                        .map(PropertyValue::CharacterString)
397                        .collect(),
398                    array_index,
399                )
400            }
401            PropertyIdentifier::TARGET_REFERENCES => read_encoded_array(
402                self.target_references
403                    .iter()
404                    .map(|reference| {
405                        let mut encoded = BytesMut::new();
406                        encode_device_object_reference(&mut encoded, reference);
407                        PropertyValue::ApplicationData(encoded.to_vec())
408                    })
409                    .collect(),
410                array_index,
411            ),
412            PropertyIdentifier::EVENT_STATE => {
413                Ok(PropertyValue::Enumerated(EventState::NORMAL.to_raw()))
414            }
415            PropertyIdentifier::UNITS => Ok(PropertyValue::Enumerated(self.units)),
416            PropertyIdentifier::PRIORITY_FOR_WRITING => {
417                Ok(PropertyValue::Unsigned(self.priority_for_writing.into()))
418            }
419            PropertyIdentifier::MIN_PRES_VALUE => Ok(PropertyValue::Real(self.min_present_value)),
420            PropertyIdentifier::MAX_PRES_VALUE => Ok(PropertyValue::Real(self.max_present_value())),
421            _ => Err(common::unknown_property_error()),
422        }
423    }
424
425    fn write_property(
426        &mut self,
427        property: PropertyIdentifier,
428        array_index: Option<u32>,
429        value: PropertyValue,
430        _priority: Option<u8>,
431    ) -> Result<(), Error> {
432        if property != PropertyIdentifier::STAGES
433            && property != PropertyIdentifier::STAGE_NAMES
434            && property != PropertyIdentifier::TARGET_REFERENCES
435            && array_index.is_some()
436        {
437            return Err(common::property_is_not_an_array_error());
438        }
439        if let Some(result) = common::write_object_name(&mut self.name, property, &value) {
440            return result;
441        }
442        if let Some(result) = common::write_description(&mut self.description, property, &value) {
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(value) = value else {
476                    return Err(common::invalid_data_type_error());
477                };
478                if !common::is_reliability_value_valid(value) {
479                    return Err(common::value_out_of_range_error());
480                }
481                self.reliability = value;
482                Ok(())
483            }
484            PropertyIdentifier::STAGES => self.replace_stages(array_index, value),
485            PropertyIdentifier::STAGE_NAMES => self.replace_stage_names(array_index, value),
486            PropertyIdentifier::TARGET_REFERENCES => {
487                self.replace_target_references(array_index, value)
488            }
489            PropertyIdentifier::PRIORITY_FOR_WRITING => {
490                let PropertyValue::Unsigned(value) = value else {
491                    return Err(common::invalid_data_type_error());
492                };
493                let priority = u8::try_from(value)
494                    .ok()
495                    .filter(|value| (1..=16).contains(value))
496                    .ok_or_else(common::value_out_of_range_error)?;
497                if priority != self.priority_for_writing {
498                    self.priority_for_writing = priority;
499                    self.reset_and_reevaluate();
500                }
501                Ok(())
502            }
503            PropertyIdentifier::MIN_PRES_VALUE => {
504                let PropertyValue::Real(value) = value else {
505                    return Err(common::invalid_data_type_error());
506                };
507                common::reject_non_finite(value)?;
508                validate_min_present_value(value, &self.stages)?;
509                if value != self.min_present_value {
510                    self.min_present_value = value;
511                    self.reset_and_reevaluate();
512                }
513                Ok(())
514            }
515            _ => Err(common::write_access_denied_error()),
516        }
517    }
518
519    fn property_metadata(&self) -> Cow<'_, [PropertyMetadata]> {
520        Cow::Owned(self.metadata())
521    }
522
523    fn property_list(&self) -> Cow<'static, [PropertyIdentifier]> {
524        property_list_from_metadata(&self.metadata())
525    }
526
527    fn take_staging_write_plan_internal(&mut self) -> Option<StagingWritePlan> {
528        if self.present_stage == 0 {
529            self.evaluate(false);
530        }
531        self.pending_plan.take()
532    }
533
534    fn staging_generation_internal(&self) -> Option<u64> {
535        Some(self.generation)
536    }
537
538    fn complete_staging_write_plan_internal(&mut self, generation: u64, success: bool) -> bool {
539        if generation != self.generation || self.out_of_service {
540            return false;
541        }
542        let reliability = if success {
543            Reliability::NO_FAULT_DETECTED.to_raw()
544        } else {
545            Reliability::UNRELIABLE_OTHER.to_raw()
546        };
547        if reliability == self.reliability {
548            return false;
549        }
550        self.reliability = reliability;
551        true
552    }
553}
554
555fn read_encoded_array(
556    values: Vec<PropertyValue>,
557    array_index: Option<u32>,
558) -> Result<PropertyValue, Error> {
559    match array_index {
560        None => Ok(PropertyValue::List(values)),
561        Some(0) => Ok(PropertyValue::Unsigned(values.len() as u64)),
562        Some(index) => values
563            .into_iter()
564            .nth((index - 1) as usize)
565            .ok_or_else(common::invalid_array_index_error),
566    }
567}
568
569fn decode_stage_property_value(value: PropertyValue) -> Result<BACnetStageLimitValue, Error> {
570    let PropertyValue::ApplicationData(bytes) = value else {
571        return Err(common::invalid_data_type_error());
572    };
573    let (stage, consumed) =
574        decode_stage_limit_value(&bytes, 0).map_err(|_| common::invalid_data_encoding_error())?;
575    if consumed != bytes.len() {
576        return Err(common::invalid_data_encoding_error());
577    }
578    Ok(stage)
579}
580
581fn decode_reference_property_value(
582    value: PropertyValue,
583) -> Result<BACnetDeviceObjectReference, Error> {
584    let PropertyValue::ApplicationData(bytes) = value else {
585        return Err(common::invalid_data_type_error());
586    };
587    let (reference, consumed) = decode_device_object_reference(&bytes, 0)
588        .map_err(|_| common::invalid_data_encoding_error())?;
589    if consumed != bytes.len() {
590        return Err(common::invalid_data_encoding_error());
591    }
592    Ok(reference)
593}
594
595fn validate_config(config: &StagingConfig) -> Result<(), Error> {
596    common::reject_non_finite(config.present_value)?;
597    if !(1..=16).contains(&config.priority_for_writing) {
598        return Err(common::value_out_of_range_error());
599    }
600    if let Some(names) = &config.stage_names {
601        if names.len() != config.stages.len() {
602            return Err(common::value_out_of_range_error());
603        }
604    }
605    validate_target_references(&config.target_references)?;
606    validate_ladder(
607        &config.stages,
608        config.min_present_value,
609        config.target_references.len(),
610    )
611}
612
613fn validate_target_references(references: &[BACnetDeviceObjectReference]) -> Result<(), Error> {
614    for reference in references {
615        if reference.device_identifier.is_some() {
616            return Err(protocol_error(
617                ErrorClass::PROPERTY,
618                ErrorCode::OPTIONAL_FUNCTIONALITY_NOT_SUPPORTED,
619            ));
620        }
621        if !matches!(
622            reference.object_identifier.object_type(),
623            ObjectType::BINARY_OUTPUT
624                | ObjectType::BINARY_VALUE
625                | ObjectType::BINARY_LIGHTING_OUTPUT
626        ) {
627            return Err(common::value_out_of_range_error());
628        }
629    }
630    Ok(())
631}
632
633fn validate_ladder(
634    stages: &[BACnetStageLimitValue],
635    min_present_value: f32,
636    target_count: usize,
637) -> Result<(), Error> {
638    if stages.len() <= 1 {
639        return Err(common::value_out_of_range_error());
640    }
641    for stage in stages {
642        if !stage.limit.is_finite() || !stage.deadband.is_finite() || stage.deadband < 0.0 {
643            return Err(common::value_out_of_range_error());
644        }
645        if stage.values.len() != target_count {
646            return Err(common::value_out_of_range_error());
647        }
648    }
649    for adjacent in stages.windows(2) {
650        if adjacent[0].limit >= adjacent[1].limit {
651            return Err(common::value_out_of_range_error());
652        }
653        if adjacent[0].limit + adjacent[0].deadband > adjacent[1].limit - adjacent[1].deadband {
654            return Err(common::value_out_of_range_error());
655        }
656    }
657    validate_min_present_value(min_present_value, stages)
658}
659
660fn validate_min_present_value(
661    min_present_value: f32,
662    stages: &[BACnetStageLimitValue],
663) -> Result<(), Error> {
664    if !min_present_value.is_finite() || min_present_value >= stages[0].limit - stages[0].deadband {
665        return Err(common::value_out_of_range_error());
666    }
667    Ok(())
668}
669
670fn protocol_error(class: ErrorClass, code: ErrorCode) -> Error {
671    Error::Protocol {
672        class: class.to_raw() as u32,
673        code: code.to_raw() as u32,
674    }
675}
676
677#[cfg(test)]
678mod tests;