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}