bacnet_objects/database/local_device.rs
1//! Which Device a database speaks for, and which device-qualified references
2//! stay inside it (#1183, #1184).
3//!
4//! Many references carry an optional Device member: a Trend Log's
5//! Log_DeviceObjectProperty, an Event Enrollment's monitored and setpoint
6//! references, a Schedule's targets and more. Without the member, or with one
7//! naming this device, the reference points at an object in this database.
8//! With one naming any other device it points somewhere this database can't
9//! read, even when an object with the same identifier exists here. The object
10//! layer (the Trend Log poller) and the server (Event Enrollment evaluation,
11//! local reference rewrites) ask that question of the same objects, so the
12//! answer lives with the database rather than in each caller.
13
14use bacnet_types::enums::ObjectType;
15use bacnet_types::primitives::ObjectIdentifier;
16
17use super::ObjectDatabase;
18
19/// This database's own Device, as a device-qualified reference names it.
20///
21/// Taken from [`ObjectDatabase::local_device`] under the caller's database
22/// guard, so it holds only while that guard does.
23#[derive(Debug, Clone, Copy, PartialEq, Eq)]
24pub struct LocalDevice(Option<ObjectIdentifier>);
25
26impl LocalDevice {
27 /// The Device identifier that names this device. `None` when the
28 /// database holds no Device, or only the wildcard instance, which names
29 /// no particular device.
30 pub fn identifier(self) -> Option<ObjectIdentifier> {
31 self.0
32 }
33
34 /// Whether a reference whose optional Device member is `device` points
35 /// inside this device: it names no device, or it names this one.
36 pub fn is_local(self, device: Option<ObjectIdentifier>) -> bool {
37 device.is_none_or(|device| Some(device) == self.0)
38 }
39}
40
41impl ObjectDatabase {
42 /// The Device object this database represents, for wildcard reads,
43 /// discovery, notifications and reference resolution.
44 ///
45 /// Clause 12.11 expects one Device per BACnet device. When a database
46 /// holds several, the lowest instance wins: a deterministic local policy,
47 /// independent of hash or insertion order. A concrete instance always
48 /// precedes the wildcard instance, which is the highest; a database whose
49 /// only Device is the wildcard selects it. No Device yields `None`.
50 pub fn selected_device(&self) -> Option<ObjectIdentifier> {
51 select_device(
52 self.type_index
53 .get(&ObjectType::DEVICE)
54 .into_iter()
55 .flatten()
56 .copied(),
57 )
58 }
59
60 /// The Device a reference must name to stay inside this device: the
61 /// [selected Device](Self::selected_device) when its instance is
62 /// concrete.
63 pub fn local_device(&self) -> LocalDevice {
64 LocalDevice(
65 self.selected_device()
66 .filter(|device| device.instance_number() != ObjectIdentifier::WILDCARD_INSTANCE),
67 )
68 }
69}
70
71fn select_device(
72 candidates: impl IntoIterator<Item = ObjectIdentifier>,
73) -> Option<ObjectIdentifier> {
74 candidates
75 .into_iter()
76 .filter(|candidate| candidate.object_type() == ObjectType::DEVICE)
77 .min_by_key(|candidate| candidate.instance_number())
78}
79
80#[cfg(test)]
81#[path = "local_device_tests.rs"]
82mod tests;