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