Skip to main content

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;