Skip to main content

bacnet_objects/color/
xy.rs

1//! The Color object (type 63, Addendum 135-2020ca Clause 12.X): a CIE 1931
2//! xy colour.
3
4use std::borrow::Cow;
5use std::sync::Arc;
6use std::time::Duration;
7
8use bacnet_types::constructed::{BACnetColorCommand, BACnetXyColor};
9use bacnet_types::enums::{ColorOperation, ColorTransition, ObjectType, PropertyIdentifier};
10use bacnet_types::error::Error;
11use bacnet_types::primitives::{ObjectIdentifier, PropertyValue};
12
13use super::engine::Engine;
14use super::{
15    command, metadata, milliseconds, noncommandable_audit_policy, written_transition,
16    written_unsigned, written_xy, xy_value,
17};
18use crate::audit::{AuditPolicyAuthority, ObjectAuditPolicy};
19use crate::command_source::{CommandOrigin, SingleValueSource};
20use crate::common::{self, read_identity_properties};
21use crate::object_profile::{ObjectProfile, ProfileState, TagsPersistence};
22use crate::traits::{BACnetObject, DeadlineWaker, MonotonicClock};
23use crate::transition::Transition;
24
25/// D65 white, where a new Color object starts.
26const D65: BACnetXyColor = BACnetXyColor::new(0.3127, 0.3290);
27
28/// How far a fading colour moves between COV samples of Tracking_Value: a
29/// thousandth of the 0.0 to 1.0 coordinate range, measured along the fade's
30/// straight line on the xy diagram. That is about the size of the smallest
31/// colour differences an eye tells apart, and the 100 ms sample grid bounds
32/// how often a short fade is sampled.
33const XY_SAMPLE_STEP: f64 = 0.001;
34
35/// BACnet Color object (type 63).
36///
37/// Holds a colour as CIE 1931 xy coordinates, each 0.0 to 1.0. It isn't
38/// commandable: Present_Value takes a write with no priority array.
39///
40/// Color_Command takes a [`BACnetColorCommand`] whose operation is
41/// FADE_TO_COLOR or STOP (see [`set_color_command`](Self::set_color_command))
42/// and carries it out (#1474). FADE_TO_COLOR sets Present_Value to the target
43/// at once and fades Tracking_Value to it from where it stands, over the
44/// command's fade time or Default_Fade_Time, with In_Progress FADE_ACTIVE
45/// until it arrives. STOP ends the fade there, and Present_Value takes the
46/// colour it reached. A Present_Value write halts a fade too, then moves to
47/// the new colour as Transition says: at once for NONE, or a fade over
48/// Default_Fade_Time for FADE.
49///
50/// Fades run on the monotonic clock the database binds; the server's
51/// monotonic task samples Tracking_Value for COV as it moves and ends the
52/// fade on time. With no clock bound they wait on `advance_time_internal`.
53///
54/// Cloning copies served state into an in-memory object. Tags persistence and
55/// pending saves remain solely with the original; cloning or dropping the copy
56/// never saves, corrects, settles, or waits on the original writer.
57#[derive(Clone)]
58pub struct ColorObject {
59    oid: ObjectIdentifier,
60    name: String,
61    description: String,
62    /// Present_Value: the colour the output is at, or fading to.
63    present_value: BACnetXyColor,
64    /// The last command written; operation NONE until then.
65    color_command: BACnetColorCommand,
66    /// Default_Color: the colour for the output after a restart.
67    default_color: BACnetXyColor,
68    /// Default_Fade_Time in milliseconds, within 100..=86_400_000.
69    default_fade_time: u32,
70    /// Transition: NONE or FADE.
71    transition: ColorTransition,
72    /// Audit_Level and Auditable_Operations, once provisioned.
73    audit_policy: ObjectAuditPolicy,
74    /// Value_Source, once tracked (#1552).
75    value_source: SingleValueSource,
76    /// Tags, Profile_Location and Profile_Name, once provisioned (#1553).
77    profile: ProfileState,
78    engine: Engine<BACnetXyColor>,
79}
80
81impl ColorObject {
82    /// Create a Color object at D65 white (x 0.3127, y 0.3290), which is also
83    /// its Default_Color.
84    ///
85    /// Default_Fade_Time starts at 100 ms, the shortest the addendum allows,
86    /// as Lighting Output's does, and Transition at NONE.
87    pub fn new(instance: u32, name: impl Into<String>) -> Result<Self, Error> {
88        let oid = ObjectIdentifier::new(ObjectType::COLOR, instance)?;
89        Ok(Self {
90            oid,
91            name: name.into(),
92            description: String::new(),
93            present_value: D65,
94            color_command: BACnetColorCommand::new(ColorOperation::NONE),
95            default_color: D65,
96            default_fade_time: *command::FADE_TIME_MS.start(),
97            transition: ColorTransition::NONE,
98            audit_policy: ObjectAuditPolicy::default(),
99            value_source: SingleValueSource::default(),
100            profile: ProfileState::default(),
101            engine: Engine::new(XY_SAMPLE_STEP),
102        })
103    }
104
105    /// Track Value_Source (Clause 19.5, #1552), provisioned before
106    /// registration as the audit policy is.
107    ///
108    /// On, the object serves Value_Source: the writer of the last
109    /// Present_Value write, or of the last Color_Command that set
110    /// Present_Value, which alone may then correct it. Such a write must
111    /// name its writer (`write_property_from`, as the server's network and
112    /// local writes do); one without is refused with WRITE_ACCESS_DENIED.
113    /// The typed setters name none, so after one Value_Source reads NONE.
114    pub fn set_value_source_tracking(&mut self, enabled: bool) {
115        self.value_source.set_enabled(enabled);
116    }
117
118    pub(super) fn value_source(&self) -> &SingleValueSource {
119        &self.value_source
120    }
121
122    /// Build with application-owned Tags storage. Saved Tags override configured
123    /// Tags only while `set_profile` provisions the row. Loaded data must satisfy
124    /// the normal Tags rules and the opt-in 1 MiB snapshot limit.
125    pub fn with_tags_persistence(
126        instance: u32,
127        name: impl Into<String>,
128        persistence: Arc<dyn TagsPersistence>,
129    ) -> Result<Self, Error> {
130        let mut object = Self::new(instance, name)?;
131        object.profile = ProfileState::persistent(object.oid, persistence)?;
132        Ok(object)
133    }
134
135    /// Wait for queued save attempts to finish. This is not a success receipt;
136    /// writes report their own save outcomes. Unstaged local writes block.
137    pub fn wait_for_tag_saves(&self) {
138        self.profile.wait_for_saves();
139    }
140
141    /// Provision the optional Tags, Profile_Location and Profile_Name rows
142    /// before registration (#1553; see [`ObjectProfile`]). Tags takes
143    /// writes; the profile rows are read-only over the network. A profile
144    /// that fails [`ObjectProfile::check`] is refused and changes nothing.
145    pub fn set_profile(&mut self, profile: ObjectProfile) -> Result<(), Error> {
146        self.profile.provision(profile)
147    }
148
149    pub(super) fn profile(&self) -> &ObjectProfile {
150        self.profile.profile()
151    }
152
153    /// Provision the optional Audit_Level and Auditable_Operations rows
154    /// before registration, as an Analog or Binary Value's are (#1525).
155    ///
156    /// A field left `None` keeps its row out of the object; a present row is
157    /// writable, and a DEFAULT level inherits the selected Audit Reporter's.
158    /// The object has no commandable property for Audit_Priority_Filter to
159    /// filter, and Table 12-X doesn't list it, so a priority filter in
160    /// `policy` is left out. Provisioning doesn't install or enable a
161    /// Reporter.
162    pub fn set_audit_policy(&mut self, policy: ObjectAuditPolicy) {
163        self.audit_policy = noncommandable_audit_policy(policy);
164    }
165
166    pub(super) fn audit_policy(&self) -> &ObjectAuditPolicy {
167        &self.audit_policy
168    }
169
170    /// Set Present_Value, as a WriteProperty of the colour would.
171    ///
172    /// A coordinate outside 0.0 to 1.0, or NaN, is refused with
173    /// VALUE_OUT_OF_RANGE and changes nothing (Clause 12.X.4). Otherwise a
174    /// fade in progress is halted, and the output moves to `color` as
175    /// Transition says.
176    pub fn set_present_value(&mut self, color: BACnetXyColor) -> Result<(), Error> {
177        if !command::xy_in_range(color) {
178            return Err(common::value_out_of_range_error());
179        }
180        self.write_present_value(color);
181        self.value_source.forget();
182        Ok(())
183    }
184
185    /// The last command written to Color_Command, or operation NONE with no
186    /// other field before any write.
187    pub fn color_command(&self) -> BACnetColorCommand {
188        self.color_command
189    }
190
191    /// Set Color_Command and carry the command out, as a WriteProperty of
192    /// the encoded command would.
193    ///
194    /// The command is checked against the Color object's table of colour
195    /// commands (Addendum 135-2020ca, Table 12-X2): only FADE_TO_COLOR and
196    /// STOP are taken. FADE_TO_COLOR needs a target colour with both
197    /// coordinates 0.0 to 1.0, and a fade time, when it carries one, of 100
198    /// to 86,400,000 ms. Fields the operation doesn't use are kept unchecked.
199    /// A refusal is VALUE_OUT_OF_RANGE and leaves the object unchanged.
200    pub fn set_color_command(&mut self, command: BACnetColorCommand) -> Result<(), Error> {
201        command::check_color(&command)?;
202        if self.take_color_command(command) {
203            self.value_source.forget();
204        }
205        Ok(())
206    }
207
208    /// Store a checked command and carry it out now (Table 12-X2). A new
209    /// FADE_TO_COLOR or a STOP halts the fade in progress (Clause 12.X.6.1).
210    /// Whether it set Present_Value: a STOP with no fade running doesn't.
211    fn take_color_command(&mut self, command: BACnetColorCommand) -> bool {
212        self.color_command = command;
213        let now = self.engine.now();
214        match command.operation {
215            ColorOperation::FADE_TO_COLOR => {
216                let Some(target) = command.target_color else {
217                    return false;
218                };
219                let tracking = self.engine.tracking(self.present_value, now);
220                let fade_time = command.fade_time.unwrap_or(self.default_fade_time);
221                self.present_value = target;
222                self.engine.start(Transition::fade(
223                    tracking,
224                    target,
225                    now,
226                    milliseconds(fade_time),
227                ));
228                true
229            }
230            ColorOperation::STOP => match self.engine.halt(now) {
231                Some(reached) => {
232                    self.present_value = reached;
233                    true
234                }
235                None => false,
236            },
237            _ => false,
238        }
239    }
240
241    /// A Present_Value or Color_Command write, the two that can set
242    /// Present_Value, and whether it did.
243    fn write_output(
244        &mut self,
245        property: PropertyIdentifier,
246        value: PropertyValue,
247    ) -> Result<bool, Error> {
248        if property == PropertyIdentifier::PRESENT_VALUE {
249            self.write_present_value(written_xy(value)?);
250            return Ok(true);
251        }
252        let command = command::decode_write(value, command::check_color)?;
253        Ok(self.take_color_command(command))
254    }
255
256    /// Halt any fade and move to a written Present_Value as Transition says
257    /// (Clause 12.X.11): at once, or a fade over Default_Fade_Time.
258    fn write_present_value(&mut self, color: BACnetXyColor) {
259        let now = self.engine.now();
260        let tracking = self.engine.tracking(self.present_value, now);
261        self.present_value = color;
262        let fade = (self.transition == ColorTransition::FADE)
263            .then(|| Transition::fade(tracking, color, now, milliseconds(self.default_fade_time)));
264        self.engine.start(fade.flatten());
265    }
266}
267
268impl BACnetObject for ColorObject {
269    fn object_identifier(&self) -> ObjectIdentifier {
270        self.oid
271    }
272
273    fn object_name(&self) -> &str {
274        &self.name
275    }
276
277    fn read_property(
278        &self,
279        property: PropertyIdentifier,
280        array_index: Option<u32>,
281    ) -> Result<PropertyValue, Error> {
282        if let Some(result) = read_identity_properties!(self, property, array_index) {
283            return result;
284        }
285        if let Some(result) = self.audit_policy.read(property, array_index) {
286            return result;
287        }
288        if let Some(result) = self.value_source.read(property, array_index) {
289            return result;
290        }
291        if let Some(result) = self.profile.read(property, array_index) {
292            return result;
293        }
294        // Each read takes its own instant, so one ReadPropertyMultiple that
295        // straddles a fade's end may pair a Tracking_Value just short of the
296        // target with In_Progress IDLE.
297        match property {
298            p if p == PropertyIdentifier::OBJECT_TYPE => {
299                Ok(PropertyValue::Enumerated(ObjectType::COLOR.to_raw()))
300            }
301            p if p == PropertyIdentifier::PRESENT_VALUE => Ok(xy_value(self.present_value)),
302            p if p == PropertyIdentifier::TRACKING_VALUE => Ok(xy_value(
303                self.engine.tracking(self.present_value, self.engine.now()),
304            )),
305            p if p == PropertyIdentifier::COLOR_COMMAND => Ok(command::encode(&self.color_command)),
306            p if p == PropertyIdentifier::IN_PROGRESS => Ok(PropertyValue::Enumerated(
307                self.engine.in_progress(self.engine.now()).to_raw(),
308            )),
309            p if p == PropertyIdentifier::DEFAULT_COLOR => Ok(xy_value(self.default_color)),
310            p if p == PropertyIdentifier::DEFAULT_FADE_TIME => {
311                Ok(PropertyValue::Unsigned(u64::from(self.default_fade_time)))
312            }
313            p if p == PropertyIdentifier::TRANSITION => {
314                Ok(PropertyValue::Enumerated(self.transition.to_raw()))
315            }
316            _ => Err(common::unknown_property_error()),
317        }
318    }
319
320    fn write_property(
321        &mut self,
322        property: PropertyIdentifier,
323        array_index: Option<u32>,
324        value: PropertyValue,
325        priority: Option<u8>,
326    ) -> Result<(), Error> {
327        if let Some(result) = common::write_description(&mut self.description, property, &value) {
328            return result;
329        }
330        if let Some(result) = self
331            .audit_policy
332            .write(property, array_index, &value, priority)
333        {
334            return result;
335        }
336        if let Some(result) = self.profile.write(property, array_index, &value) {
337            return result;
338        }
339        match property {
340            p if p == PropertyIdentifier::PRESENT_VALUE
341                || p == PropertyIdentifier::COLOR_COMMAND =>
342            {
343                // A tracked source needs the writer (`write_property_from`).
344                self.value_source.unsourced()?;
345                self.write_output(property, value)?;
346            }
347            // (0, 0) is taken like any other colour: Clause 12.X.8 has it
348            // mean that a restart brings back the colour from before it.
349            p if p == PropertyIdentifier::DEFAULT_COLOR => {
350                self.default_color = written_xy(value)?;
351            }
352            p if p == PropertyIdentifier::DEFAULT_FADE_TIME => {
353                self.default_fade_time = written_unsigned(value, command::FADE_TIME_MS)?;
354            }
355            p if p == PropertyIdentifier::TRANSITION => {
356                self.transition = written_transition(value, ColorTransition::FADE)?;
357            }
358            _ => {
359                return Err(common::unhandled_write_error(
360                    self.property_metadata().as_ref(),
361                    property,
362                    array_index,
363                ))
364            }
365        }
366        Ok(())
367    }
368
369    fn write_property_from(
370        &mut self,
371        property: PropertyIdentifier,
372        array_index: Option<u32>,
373        value: PropertyValue,
374        priority: Option<u8>,
375        origin: &CommandOrigin,
376    ) -> Result<(), Error> {
377        match property {
378            PropertyIdentifier::VALUE_SOURCE => {
379                self.value_source.correct(array_index, value, origin)
380            }
381            PropertyIdentifier::PRESENT_VALUE | PropertyIdentifier::COLOR_COMMAND => {
382                self.value_source.admit(origin)?;
383                if self.write_output(property, value)? {
384                    self.value_source.record(origin);
385                }
386                Ok(())
387            }
388            _ => self.write_property(property, array_index, value, priority),
389        }
390    }
391
392    fn property_metadata(&self) -> Cow<'_, [crate::property_metadata::PropertyMetadata]> {
393        metadata::for_color_object(self)
394    }
395
396    fn durable_writes_internal(&mut self) -> Option<&mut dyn crate::durable::DurableWrites> {
397        self.profile.capability()
398    }
399
400    fn property_list(&self) -> Cow<'static, [PropertyIdentifier]> {
401        crate::property_metadata::property_list_from_metadata(self.property_metadata().as_ref())
402    }
403
404    fn supports_cov(&self) -> bool {
405        true
406    }
407
408    fn audit_object_policy_internal(&self) -> ObjectAuditPolicy {
409        self.audit_policy
410    }
411
412    fn audit_policy_authority_internal(&mut self) -> Option<AuditPolicyAuthority<'_>> {
413        Some(AuditPolicyAuthority::new(&mut self.audit_policy))
414    }
415
416    fn advance_time_internal(&mut self, elapsed: Duration) -> bool {
417        self.engine.advance_by(elapsed)
418    }
419
420    fn bind_monotonic_clock_internal(&mut self, clock: Option<Arc<MonotonicClock>>) {
421        self.engine.bind_clock(clock);
422    }
423
424    fn bind_deadline_waker_internal(&mut self, waker: Option<Arc<DeadlineWaker>>) {
425        self.engine.bind_waker(waker);
426    }
427
428    fn advance_monotonic_time_internal(&mut self, now: Duration) -> bool {
429        self.profile.expire(now);
430        self.engine.advance_to(now)
431    }
432
433    fn next_monotonic_deadline_internal(&self) -> Option<Duration> {
434        self.engine.deadline()
435    }
436
437    /// A copy that reads as the object does now: its clock stops at this
438    /// instant, so a fade's Tracking_Value in a COV report is the value at
439    /// the moment the report was taken.
440    fn cov_snapshot_internal(&self) -> Option<Box<dyn BACnetObject>> {
441        Some(Box::new(Self {
442            engine: self.engine.frozen(),
443            ..self.clone()
444        }))
445    }
446}