Skip to main content

bacnet_objects/
catalogue.rs

1//! The object catalogue: one object of each standard type this crate builds.
2//!
3//! [`representatives`] constructs one object per [`ObjectType`] that has an
4//! implementation here, with fixed small configurations (instance 1, a short
5//! name, minimal sizes). It answers "which object types can this crate
6//! construct", for audits, generated documentation and sample databases. It
7//! says nothing about how well each type behaves on a network: that is what the
8//! object and server tests cover.
9//!
10//! The types without an implementation are listed in [`EXCLUDED_TYPES`]:
11//! Network Security (type 38), whose clause was deleted from the standard. A
12//! catalogue entry is a starting point to configure, not a recommended
13//! deployment: the Audit Log keeps its records in memory only, and every
14//! object uses placeholder units and names.
15//!
16//! The catalogue builds without the standard library, like the crate.
17
18use crate::prelude::*;
19use crate::sync::Mutex;
20use alloc::sync::Arc;
21
22use bacnet_types::constructed::{BACnetDeviceObjectReference, BACnetStageLimitValue};
23use bacnet_types::enums::{EngineeringUnits, EventType, NetworkType, ObjectType};
24use bacnet_types::error::Error;
25use bacnet_types::primitives::{ObjectIdentifier, PropertyValue};
26
27use crate::access_control::{
28    AccessCredentialObject, AccessDoorObject, AccessPointObject, AccessRightsObject,
29    AccessUserObject, AccessZoneObject, CredentialDataInputObject,
30};
31use crate::accumulator::{AccumulatorObject, PulseConverterObject};
32use crate::analog::{AnalogInputObject, AnalogOutputObject, AnalogValueObject};
33use crate::audit::{AuditLogObject, AuditLogPersistence, AuditLogSnapshot, AuditReporterObject};
34use crate::averaging::AveragingObject;
35use crate::binary::{BinaryInputObject, BinaryOutputObject, BinaryValueObject};
36use crate::channel::ChannelObject;
37use crate::color::{ColorObject, ColorTemperatureObject};
38use crate::command::CommandObject;
39use crate::device::DeviceObject;
40use crate::elevator::{ElevatorGroupObject, EscalatorObject, LiftObject};
41use crate::event_enrollment::{AlertEnrollmentObject, EventEnrollmentObject};
42use crate::event_log::EventLogObject;
43use crate::file::FileObject;
44use crate::group::{GlobalGroupObject, GroupObject, StructuredViewObject};
45use crate::life_safety::{LifeSafetyPointObject, LifeSafetyZoneObject};
46use crate::lighting::{BinaryLightingOutputObject, LightingOutputObject};
47use crate::load_control::LoadControlObject;
48use crate::loop_obj::LoopObject;
49use crate::multistate::{MultiStateInputObject, MultiStateOutputObject, MultiStateValueObject};
50use crate::network_port::NetworkPortObject;
51use crate::notification_class::NotificationClass;
52use crate::notification_forwarder::NotificationForwarderObject;
53use crate::program::ProgramObject;
54use crate::schedule::{CalendarObject, ScheduleObject};
55use crate::staging::{StagingConfig, StagingObject};
56use crate::timer::TimerObject;
57use crate::traits::BACnetObject;
58use crate::trend::{TrendLogMultipleObject, TrendLogObject};
59use crate::value_types::{
60    BitStringValueObject, CharacterStringValueObject, DatePatternValueObject,
61    DateTimePatternValueObject, DateTimeValueObject, DateValueObject, IntegerValueObject,
62    LargeAnalogValueObject, OctetStringValueObject, PositiveIntegerValueObject,
63    TimePatternValueObject, TimeValueObject,
64};
65
66/// Standard object types with no implementation in this crate, so no
67/// catalogue entry: Network Security (38), whose clause the standard deleted.
68pub const EXCLUDED_TYPES: &[ObjectType] = &[ObjectType::NETWORK_SECURITY];
69
70/// Audit Log storage that keeps the last committed snapshot in memory, so the
71/// catalogue's Audit Log needs no file system.
72#[derive(Default)]
73struct MemoryAuditLogPersistence(Mutex<Option<AuditLogSnapshot>>);
74
75impl AuditLogPersistence for MemoryAuditLogPersistence {
76    fn load(&self, _expected_object: ObjectIdentifier) -> Result<Option<AuditLogSnapshot>, Error> {
77        Ok(self.0.lock().clone())
78    }
79
80    fn commit(&self, snapshot: &AuditLogSnapshot) -> Result<(), Error> {
81        *self.0.lock() = Some(snapshot.clone());
82        Ok(())
83    }
84}
85
86/// A three-stage Staging configuration over two local outputs.
87pub(crate) fn staging_config() -> StagingConfig {
88    let output = |object_type, instance| BACnetDeviceObjectReference {
89        device_identifier: None,
90        object_identifier: ObjectIdentifier::new(object_type, instance)
91            .expect("a valid catalogue reference"),
92    };
93    StagingConfig {
94        present_value: 5.0,
95        min_present_value: -1.0,
96        units: EngineeringUnits::DEGREES_CELSIUS,
97        priority_for_writing: 8,
98        stages: vec![
99            BACnetStageLimitValue {
100                limit: 10.0,
101                values: vec![false, true],
102                deadband: 1.0,
103            },
104            BACnetStageLimitValue {
105                limit: 20.0,
106                values: vec![true, false],
107                deadband: 2.0,
108            },
109            BACnetStageLimitValue {
110                limit: 30.0,
111                values: vec![true, true],
112                deadband: 1.0,
113            },
114        ],
115        target_references: vec![
116            output(ObjectType::BINARY_OUTPUT, 1),
117            output(ObjectType::BINARY_VALUE, 2),
118        ],
119        stage_names: Some(vec!["Low".into(), "Middle".into(), "High".into()]),
120    }
121}
122
123/// One object of every standard type this crate implements, in object type
124/// order: every type from Analog Input (0) to Color Temperature (64) except
125/// those in [`EXCLUDED_TYPES`].
126///
127/// Each object has instance 1 and a name unique within the list, so the whole
128/// list fits one [`ObjectDatabase`](crate::database::ObjectDatabase). The
129/// Device is built from its default configuration. The Staging object starts
130/// with Present_Stage uninitialized, as a new one does; a server initializes it
131/// at startup.
132///
133/// The list counts constructible types. It is not a statement about
134/// interoperability or about which services reach each type.
135///
136/// # Panics
137///
138/// Never in practice: each construction uses fixed arguments that the crate's
139/// tests build. A panic here is a bug in this function.
140pub fn representatives() -> Vec<Box<dyn BACnetObject>> {
141    let celsius = EngineeringUnits::DEGREES_CELSIUS;
142    let alert_source =
143        ObjectIdentifier::new(ObjectType::ANALOG_INPUT, 1).expect("a valid catalogue reference");
144    let audit_storage: Arc<dyn AuditLogPersistence> =
145        Arc::new(MemoryAuditLogPersistence::default());
146    let objects: Vec<Result<Box<dyn BACnetObject>, Error>> = vec![
147        boxed(AnalogInputObject::new(1, "AI-1", celsius)),
148        boxed(AnalogOutputObject::new(1, "AO-1", celsius)),
149        boxed(AnalogValueObject::new(1, "AV-1", celsius)),
150        boxed(BinaryInputObject::new(1, "BI-1")),
151        boxed(BinaryOutputObject::new(1, "BO-1")),
152        boxed(BinaryValueObject::new(1, "BV-1")),
153        boxed(CalendarObject::new(1, "CAL-1")),
154        boxed(CommandObject::new(1, "CMD-1")),
155        boxed(DeviceObject::new(Default::default())),
156        boxed(EventEnrollmentObject::new(
157            1,
158            "EE-1",
159            EventType::CHANGE_OF_BITSTRING,
160        )),
161        boxed(FileObject::new(1, "FILE-1", "raw")),
162        boxed(GroupObject::new(1, "GRP-1")),
163        boxed(LoopObject::new(1, "LOOP-1", celsius)),
164        boxed(MultiStateInputObject::new(1, "MSI-1", 3)),
165        boxed(MultiStateOutputObject::new(1, "MSO-1", 3)),
166        boxed(NotificationClass::new(1, "NC-1")),
167        boxed(ProgramObject::new(1, "PRG-1")),
168        boxed(ScheduleObject::new(1, "SCH-1", PropertyValue::Null)),
169        boxed(AveragingObject::new(1, "AVG-1")),
170        boxed(MultiStateValueObject::new(1, "MSV-1", 3)),
171        boxed(TrendLogObject::new(1, "TL-1", 3)),
172        boxed(LifeSafetyPointObject::new(1, "LSP-1")),
173        boxed(LifeSafetyZoneObject::new(1, "LSZ-1")),
174        boxed(AccumulatorObject::new(
175            1,
176            "ACC-1",
177            EngineeringUnits::NO_UNITS,
178        )),
179        boxed(PulseConverterObject::new(1, "PC-1", celsius)),
180        boxed(EventLogObject::new(1, "EL-1", 3)),
181        boxed(GlobalGroupObject::new(1, "GG-1")),
182        boxed(TrendLogMultipleObject::new(1, "TLM-1", 3)),
183        boxed(LoadControlObject::new(1, "LC-1")),
184        boxed(StructuredViewObject::new(1, "SV-1")),
185        boxed(AccessDoorObject::new(1, "DOOR-1")),
186        boxed(TimerObject::new(1, "TMR-1")),
187        boxed(AccessCredentialObject::new(1, "CRED-1")),
188        boxed(AccessPointObject::new(1, "AP-1")),
189        boxed(AccessRightsObject::new(1, "AR-1")),
190        boxed(AccessUserObject::new(1, "USER-1")),
191        boxed(AccessZoneObject::new(1, "ZONE-1")),
192        boxed(CredentialDataInputObject::new(1, "CDI-1")),
193        // 38, Network Security: in EXCLUDED_TYPES.
194        boxed(BitStringValueObject::new(1, "BSV-1")),
195        boxed(CharacterStringValueObject::new(1, "CSV-1")),
196        boxed(DatePatternValueObject::new(1, "DPV-1")),
197        boxed(DateValueObject::new(1, "DV-1")),
198        boxed(DateTimePatternValueObject::new(1, "DTPV-1")),
199        boxed(DateTimeValueObject::new(1, "DTV-1")),
200        boxed(IntegerValueObject::new(1, "IV-1")),
201        boxed(LargeAnalogValueObject::new(1, "LAV-1")),
202        boxed(OctetStringValueObject::new(1, "OSV-1")),
203        boxed(PositiveIntegerValueObject::new(1, "PIV-1")),
204        boxed(TimePatternValueObject::new(1, "TPV-1")),
205        boxed(TimeValueObject::new(1, "TV-1")),
206        boxed(NotificationForwarderObject::new(1, "NF-1")),
207        boxed(AlertEnrollmentObject::new(1, "AE-1", alert_source)),
208        boxed(ChannelObject::new(1, "CH-1", 1)),
209        boxed(LightingOutputObject::new(1, "LO-1")),
210        boxed(BinaryLightingOutputObject::new(1, "BLO-1")),
211        boxed(NetworkPortObject::new_non_bip(
212            1,
213            "NP-1",
214            NetworkType::from_raw(0),
215            0,
216            Default::default(),
217            1476,
218        )),
219        boxed(ElevatorGroupObject::new(1, "EG-1")),
220        boxed(EscalatorObject::new(1, "ESC-1")),
221        boxed(LiftObject::new(1, "LIFT-1", 3)),
222        boxed(StagingObject::new(1, "STG-1", staging_config())),
223        boxed(AuditLogObject::new(1, "AL-1", 3, audit_storage)),
224        boxed(AuditReporterObject::new(1, "ARP-1")),
225        boxed(ColorObject::new(1, "CLR-1")),
226        boxed(ColorTemperatureObject::new(1, "CT-1")),
227    ];
228    objects
229        .into_iter()
230        .map(|object| object.expect("a catalogue object builds from its fixed arguments"))
231        .collect()
232}
233
234fn boxed<O: BACnetObject + 'static>(
235    object: Result<O, Error>,
236) -> Result<Box<dyn BACnetObject>, Error> {
237    object.map(|object| Box::new(object) as Box<dyn BACnetObject>)
238}
239
240#[cfg(test)]
241mod tests {
242    use super::*;
243
244    #[test]
245    fn one_object_per_type_except_the_excluded() {
246        let objects = representatives();
247        let types: Vec<u32> = objects
248            .iter()
249            .map(|object| object.object_identifier().object_type().to_raw())
250            .collect();
251        let excluded: Vec<u32> = EXCLUDED_TYPES.iter().map(|t| t.to_raw()).collect();
252        let expected: Vec<u32> = (0..=64).filter(|raw| !excluded.contains(raw)).collect();
253        assert_eq!(types, expected, "one object per type, in type order");
254    }
255
256    #[test]
257    fn the_whole_catalogue_fits_one_database() {
258        let objects = representatives();
259        let count = objects.len();
260        let mut database = crate::database::ObjectDatabase::new();
261        for object in objects {
262            let oid = object.object_identifier();
263            database
264                .add(object)
265                .unwrap_or_else(|error| panic!("{oid:?} must fit beside the others: {error:?}"));
266        }
267        assert_eq!(database.len(), count);
268    }
269}