Skip to main content

bacnet_objects/color/
persistence.rs

1//! Keeping a colour object's Present_Value across a restart (#1526).
2//!
3//! Addendum 135-2020ca lets a zero default, Default_Color (0, 0) or a
4//! Default_Color_Temperature of 0, mean that the output comes back at the
5//! value it held before the restart (Clauses 12.X.8 and 12.Y.4). That needs
6//! Present_Value kept somewhere that outlives the process; as for the
7//! Notification Class ([`NotificationClassPersistence`]), the storage
8//! belongs to the application. The object loads it once, as it takes the
9//! storage, and saves each new Present_Value on its own writer thread
10//! ([`crate::durable`]), so no file I/O happens under the database guard.
11//!
12//! Unlike a Notification Class's Recipient_List, a Present_Value change
13//! doesn't wait for its save, and a save that fails doesn't refuse it: the
14//! value is a live output, which a command, a fade's STOP or the start of
15//! the device can move, and a light shouldn't refuse a colour because a
16//! disk is full. Saves coalesce, so a burst of changes costs one save of the
17//! newest value. A save that fails is logged, and tried again with the next
18//! change, as the server stops, and as the object is dropped.
19//!
20//! [`NotificationClassPersistence`]: crate::notification_class::NotificationClassPersistence
21
22#[cfg(feature = "std")]
23use std::path::Path;
24
25use bacnet_types::constructed::BACnetXyColor;
26use bacnet_types::error::Error;
27use bacnet_types::primitives::ObjectIdentifier;
28
29use super::command;
30#[cfg(feature = "std")]
31use crate::durable::file::ObjectFile;
32
33/// A colour output's Present_Value, as storage keeps it.
34#[derive(Clone, Copy, Debug, PartialEq)]
35pub enum ColorOutputValue {
36    /// A Color object's CIE 1931 xy colour.
37    Xy(BACnetXyColor),
38    /// A Color Temperature object's temperature in kelvin.
39    Kelvin(u32),
40}
41
42impl ColorOutputValue {
43    /// Whether the value is one the object could hold: both xy
44    /// coordinates 0.0 to 1.0, or a temperature of 1000 to 30000 K.
45    pub fn is_valid(&self) -> bool {
46        match *self {
47            Self::Xy(color) => command::xy_in_range(color),
48            Self::Kelvin(kelvin) => command::KELVIN.contains(&kelvin),
49        }
50    }
51}
52
53/// What a Color or Color Temperature object keeps across a restart.
54#[derive(Clone, Copy, Debug, PartialEq)]
55pub struct ColorOutputSnapshot {
56    /// The last Present_Value the object held.
57    pub present_value: ColorOutputValue,
58}
59
60/// Application-owned storage for one colour object's Present_Value.
61///
62/// The object calls `load` once, as it takes the storage, and `save` from
63/// its writer thread, one call at a time. A backend that refuses a save
64/// returns an error and keeps what it held.
65pub trait ColorOutputPersistence: Send + Sync {
66    /// What was last saved for `object`, or `None` when nothing was saved.
67    fn load(&self, object: ObjectIdentifier) -> Result<Option<ColorOutputSnapshot>, Error>;
68
69    /// Durably replace what is saved for `object` with `snapshot`.
70    fn save(&self, object: ObjectIdentifier, snapshot: &ColorOutputSnapshot) -> Result<(), Error>;
71}
72
73/// The format's tag.
74#[cfg(feature = "std")]
75const MAGIC: &[u8; 8] = b"RBNCLR01";
76/// The body is at most a kind octet and two REALs; anything much larger is
77/// not a file this backend wrote.
78#[cfg(feature = "std")]
79const MAX_FILE_BYTES: u64 = 64;
80/// The kind octet of an xy colour, followed by x and y as big-endian IEEE
81/// 754 single-precision values.
82#[cfg(feature = "std")]
83const KIND_XY: u8 = 1;
84/// The kind octet of a colour temperature, followed by the kelvin as a
85/// big-endian 32-bit unsigned value.
86#[cfg(feature = "std")]
87const KIND_KELVIN: u8 = 2;
88
89/// A file holding one colour object's Present_Value, replaced whole on each
90/// save the same way as [`FileNotificationClassPersistence`]: a failed save
91/// leaves the previous value in place, and a completed one survives a power
92/// loss.
93///
94/// The file holds a magic tag, the object's identifier, one octet naming the
95/// kind of value, and the value: two big-endian REALs for an xy colour, a
96/// big-endian 32-bit kelvin for a temperature. A file of the wrong size, of
97/// the other kind, or with a value outside the object's range, does not
98/// load. It does not coordinate between processes.
99///
100/// [`FileNotificationClassPersistence`]: crate::notification_class::FileNotificationClassPersistence
101#[derive(Clone, Debug)]
102#[cfg(feature = "std")]
103pub struct FileColorOutputPersistence {
104    file: ObjectFile,
105}
106
107#[cfg(feature = "std")]
108impl FileColorOutputPersistence {
109    /// Keep the Present_Value in the file at `path`. Refuses an empty path.
110    pub fn new(path: impl AsRef<Path>) -> Result<Self, Error> {
111        Ok(Self {
112            file: ObjectFile::new(path.as_ref(), MAGIC, MAX_FILE_BYTES, "Color output")?,
113        })
114    }
115
116    /// The file the Present_Value is kept in.
117    pub fn path(&self) -> &Path {
118        self.file.path()
119    }
120}
121
122#[cfg(feature = "std")]
123impl ColorOutputPersistence for FileColorOutputPersistence {
124    fn load(&self, object: ObjectIdentifier) -> Result<Option<ColorOutputSnapshot>, Error> {
125        let Some(body) = self.file.load(object)? else {
126            return Ok(None);
127        };
128        let present_value = match body.as_slice() {
129            [KIND_XY, x0, x1, x2, x3, y0, y1, y2, y3] => ColorOutputValue::Xy(BACnetXyColor::new(
130                f32::from_be_bytes([*x0, *x1, *x2, *x3]),
131                f32::from_be_bytes([*y0, *y1, *y2, *y3]),
132            )),
133            [KIND_KELVIN, k0, k1, k2, k3] => {
134                ColorOutputValue::Kelvin(u32::from_be_bytes([*k0, *k1, *k2, *k3]))
135            }
136            _ => return Err(self.file.corrupt("holds no valid value")),
137        };
138        if !present_value.is_valid() {
139            return Err(self.file.corrupt("holds a value out of range"));
140        }
141        Ok(Some(ColorOutputSnapshot { present_value }))
142    }
143
144    fn save(&self, object: ObjectIdentifier, snapshot: &ColorOutputSnapshot) -> Result<(), Error> {
145        let mut body = Vec::with_capacity(9);
146        match snapshot.present_value {
147            ColorOutputValue::Xy(color) => {
148                body.push(KIND_XY);
149                body.extend_from_slice(&color.x.to_be_bytes());
150                body.extend_from_slice(&color.y.to_be_bytes());
151            }
152            ColorOutputValue::Kelvin(kelvin) => {
153                body.push(KIND_KELVIN);
154                body.extend_from_slice(&kelvin.to_be_bytes());
155            }
156        }
157        self.file.save(object, &body)
158    }
159}