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