Skip to main content

bacnet_objects/lighting/
trim.rs

1//! Lighting Output's trims (#1528, Addendum 135-2020ca part 5).
2//!
3//! High_End_Trim and Low_End_Trim bound an operating range inside the
4//! normalized 1.0 to 100.0 percent range, and Tracking_Value is held in it:
5//! a level above the high trim tracks at the high trim, an on level below
6//! the low trim at the low trim, and off stays off. Present_Value keeps the
7//! level as commanded. In_Progress reads TRIM_ACTIVE, whatever fade or ramp
8//! is running, while Present_Value lies outside the trims (Table 12-68 as
9//! the addendum changes it), and while moving bounds (below) hold
10//! Tracking_Value back; that second case is this implementation's, so that
11//! IDLE always means Tracking_Value equals Present_Value. The trims stand
12//! aside while Present_Value comes from slot 1 or 2 (Clauses 12.54.Y1 and
13//! 12.54.Y2).
14//!
15//! A trim change doesn't move Tracking_Value at once. The bounds themselves
16//! move, in a straight line from where they stood to the new trims over
17//! Trim_Fade_Time (Clause 12.54.Y3), on the shared `transition` module, and
18//! Tracking_Value follows them. The clause leaves the manner to the
19//! implementation; moving the bounds means a trim change during a fade or
20//! ramp needs no special case, as the fade runs on and is held by bounds
21//! that are themselves moving.
22//!
23//! A command works from Tracking_Value as the object reports it, so a step
24//! or a fade starts from the held level, and STOP leaves the held level in
25//! the slot. A fade to a level past a trim runs its course behind the trim:
26//! Tracking_Value reaches the trim early and stays there.
27//!
28//! Two choices go past the addendum, which leaves fades under a trim to the
29//! implementation (Clauses 12.54.Y1 and 12.54.Y2):
30//!
31//! - The levels between off and the low trim are outside the operating
32//!   range, so a fade or ramp doesn't crawl through them. One up from off
33//!   starts at the low trim and runs from there to its target over its whole
34//!   time; one down to off runs to the low trim over its whole time and goes
35//!   off as it ends. Either reads FADE_ACTIVE or RAMP_ACTIVE.
36//! - The dimmest on level a step knows is the low trim while one applies, not
37//!   1.0, so STEP_OFF turns the light off from the low trim. STEP_DOWN still
38//!   stops at 1.0, as Table 12-67 has it, which puts Present_Value below the
39//!   trim while Tracking_Value stays at it.
40//!
41//! Steps follow the table otherwise, from the held level. So a STEP_UP while
42//! Present_Value is above the high trim writes the trim plus the increment,
43//! which lowers Present_Value.
44//!
45//! A high trim below the low one is a configuration error: Reliability says
46//! so (Clauses 12.54.Y1 and 12.54.Y2), and the trims hold nothing until it's
47//! put right. Putting it right gives back the Reliability the trims replaced.
48
49use core::ops::RangeInclusive;
50use core::time::Duration;
51
52use bacnet_types::enums::Reliability;
53use bacnet_types::error::Error;
54use bacnet_types::primitives::PropertyValue;
55
56use super::LightingOutputObject;
57use crate::common;
58use crate::transition::{Progress, Run, Transition, TransitionLevel};
59
60/// Trim_Fade_Time's range in milliseconds, none to a day (Clause 12.54.Y3).
61const TRIM_FADE_TIME_MS: RangeInclusive<u32> = 0..=86_400_000;
62
63/// The bounds of the operating range, in percent.
64#[derive(Debug, Clone, Copy, PartialEq)]
65pub(super) struct Bounds {
66    low: f32,
67    high: f32,
68}
69
70impl Bounds {
71    /// No bounds at all. The low one is 0.0 rather than the dimmest on
72    /// level, 1.0, so that with no low trim a fade up from off still passes
73    /// through the levels below 1.0 as it always has.
74    const FULL: Self = Self {
75        low: 0.0,
76        high: 100.0,
77    };
78
79    /// `level` held within the bounds; off isn't an on level, so it stays.
80    fn hold(self, level: f32) -> f32 {
81        if level > 0.0 {
82            level.clamp(self.low, self.high)
83        } else {
84            level
85        }
86    }
87}
88
89/// Moving bounds: each moves on its own straight line, and they are as far
90/// apart as the bound that moves further, which spaces the COV samples of a
91/// Tracking_Value that follows it.
92impl TransitionLevel for Bounds {
93    fn interpolate(from: Self, to: Self, fraction: f64) -> Self {
94        Self {
95            low: f32::interpolate(from.low, to.low, fraction),
96            high: f32::interpolate(from.high, to.high, fraction),
97        }
98    }
99
100    fn distance(from: Self, to: Self) -> f64 {
101        f32::distance(from.low, to.low).max(f32::distance(from.high, to.high))
102    }
103}
104
105/// The trims as configured, and a change of them under way.
106#[derive(Debug, Clone, Copy, PartialEq)]
107pub(super) struct Trims {
108    /// High_End_Trim, present once set.
109    high_end: Option<f32>,
110    /// Low_End_Trim, present once set.
111    low_end: Option<f32>,
112    /// Trim_Fade_Time in milliseconds, present with either trim.
113    fade_time: u32,
114    /// The bounds moving to the configured ones after a change.
115    fade: Option<Run<Bounds>>,
116    /// The Reliability a configuration error of the trims replaced, while it
117    /// stands.
118    replaced: Option<Reliability>,
119}
120
121impl Trims {
122    pub(super) const NONE: Self = Self {
123        high_end: None,
124        low_end: None,
125        fade_time: 0,
126        fade: None,
127        replaced: None,
128    };
129
130    pub(super) fn high_end(&self) -> Option<f32> {
131        self.high_end
132    }
133
134    pub(super) fn low_end(&self) -> Option<f32> {
135        self.low_end
136    }
137
138    /// Whether Trim_Fade_Time is present: with either trim (the footnote to
139    /// Table 12-64 ties it to them).
140    pub(super) fn has_fade_time(&self) -> bool {
141        self.high_end.is_some() || self.low_end.is_some()
142    }
143
144    /// A high trim below the low one.
145    fn misconfigured(&self) -> bool {
146        matches!((self.high_end, self.low_end), (Some(high), Some(low)) if high < low)
147    }
148
149    /// The bounds the trims set, or the full range when they're
150    /// misconfigured.
151    fn configured(&self) -> Bounds {
152        if self.misconfigured() {
153            return Bounds::FULL;
154        }
155        Bounds {
156            low: self.low_end.unwrap_or(Bounds::FULL.low),
157            high: self.high_end.unwrap_or(Bounds::FULL.high),
158        }
159    }
160
161    /// Whether the bounds are still moving after a trim change at `now`.
162    fn moving(&self, now: Duration) -> bool {
163        self.fade
164            .is_some_and(|run| !run.transition().is_finished(now))
165    }
166
167    /// The bounds in effect at `now`.
168    fn at(&self, now: Duration) -> Bounds {
169        self.fade
170            .map_or_else(|| self.configured(), |run| run.transition().value_at(now))
171    }
172
173    /// Advance a trim change to `now`. `true` when Tracking_Value is due a
174    /// COV sample, or the change has arrived.
175    pub(super) fn advance(&mut self, now: Duration, step: f64) -> bool {
176        let Some(run) = &mut self.fade else {
177            return false;
178        };
179        match run.advance(now, step) {
180            Progress::Finished => {
181                self.fade = None;
182                true
183            }
184            Progress::Sampled => true,
185            Progress::Pending => false,
186        }
187    }
188
189    /// When a trim change next needs advancing, if one is under way.
190    pub(super) fn deadline(&self) -> Option<Duration> {
191        self.fade.map(|run| run.deadline())
192    }
193}
194
195/// Check a trim: 1.0 to 100.0 percent. Anything else, NaN included, is
196/// VALUE_OUT_OF_RANGE.
197fn trim_level(value: f32) -> Result<f32, Error> {
198    if (1.0..=100.0).contains(&value) {
199        Ok(value)
200    } else {
201        Err(common::value_out_of_range_error())
202    }
203}
204
205impl LightingOutputObject {
206    /// Set High_End_Trim, the top of the range Tracking_Value is held in, as
207    /// a WriteProperty of it would; `None` takes the property away.
208    ///
209    /// A trim is 1.0 to 100.0 percent; anything else is refused with
210    /// VALUE_OUT_OF_RANGE and changes nothing. Setting either trim makes
211    /// Trim_Fade_Time present too. Tracking_Value follows the change over
212    /// Trim_Fade_Time, and a high trim below the low one sets Reliability to
213    /// CONFIGURATION_ERROR and holds nothing until it's put right.
214    pub fn set_high_end_trim(&mut self, trim: Option<f32>) -> Result<(), Error> {
215        let trim = trim.map(trim_level).transpose()?;
216        self.change_trims(|trims| trims.high_end = trim);
217        Ok(())
218    }
219
220    /// Set Low_End_Trim, the bottom of the range Tracking_Value is held in
221    /// while the light is on; `None` takes the property away. Checked and
222    /// applied as [`set_high_end_trim`](Self::set_high_end_trim) is.
223    pub fn set_low_end_trim(&mut self, trim: Option<f32>) -> Result<(), Error> {
224        let trim = trim.map(trim_level).transpose()?;
225        self.change_trims(|trims| trims.low_end = trim);
226        Ok(())
227    }
228
229    /// Set Trim_Fade_Time, the milliseconds Tracking_Value takes to follow a
230    /// trim change: 0 (at once, the initial value) to 86,400,000 (a day).
231    /// Anything else is refused with VALUE_OUT_OF_RANGE (Clause 12.54.Y3).
232    /// The property is present only with a trim, but the setting is kept
233    /// either way. A change already under way keeps its own time.
234    pub fn set_trim_fade_time(&mut self, milliseconds: u32) -> Result<(), Error> {
235        if !TRIM_FADE_TIME_MS.contains(&milliseconds) {
236            return Err(common::value_out_of_range_error());
237        }
238        self.trims.fade_time = milliseconds;
239        Ok(())
240    }
241
242    /// Change the trims at the current instant: the bounds move from where
243    /// they stand to the new ones over Trim_Fade_Time.
244    fn change_trims(&mut self, change: impl FnOnce(&mut Trims)) {
245        let now = self.now();
246        // An egress that ran out before this change ends its command here.
247        self.advance_sourced(now);
248        let from = self.trims.at(now);
249        change(&mut self.trims);
250        // The trims set CONFIGURATION_ERROR, and clear only what they set.
251        match (self.trims.misconfigured(), self.trims.replaced) {
252            (true, None) => {
253                self.trims.replaced = Some(self.reliability);
254                self.reliability = Reliability::CONFIGURATION_ERROR;
255            }
256            (false, Some(replaced)) => {
257                self.trims.replaced = None;
258                if self.reliability == Reliability::CONFIGURATION_ERROR {
259                    self.reliability = replaced;
260                }
261            }
262            _ => {}
263        }
264        let fade_time = Duration::from_millis(u64::from(self.trims.fade_time));
265        let fade = Transition::fade(from, self.trims.configured(), now, fade_time);
266        self.trims.fade = fade.map(|fade| Run::start(fade, self.sample_step()));
267        if self.trims.fade.is_some() {
268            self.wake();
269        }
270    }
271
272    /// Whether the trims stand aside: Present_Value comes from slot 1 or 2
273    /// (Clauses 12.54.Y1 and 12.54.Y2).
274    fn trims_bypassed(&self) -> bool {
275        matches!(self.highest_priority(), Some(1 | 2))
276    }
277
278    /// `level` as Tracking_Value reports it at `now`: held in the operating
279    /// range there.
280    pub(super) fn trimmed(&self, level: f32, now: Duration) -> f32 {
281        if self.trims_bypassed() {
282            level
283        } else {
284            self.trims.at(now).hold(level)
285        }
286    }
287
288    /// Whether In_Progress reads TRIM_ACTIVE at `now`, where `untrimmed` is
289    /// Tracking_Value before the trims hold it: Present_Value lies outside
290    /// the trims, or the bounds of a trim change still moving hold
291    /// Tracking_Value back.
292    pub(super) fn trim_active(&self, untrimmed: f32, now: Duration) -> bool {
293        !self.trims_bypassed()
294            && (self.trims.configured().hold(self.present_value) != self.present_value
295                || (self.trims.moving(now) && self.trims.at(now).hold(untrimmed) != untrimmed))
296    }
297
298    /// The low bound in effect at `now`, or 0.0 when no low trim holds
299    /// anything.
300    fn low_bound(&self, now: Duration) -> f32 {
301        if self.trims_bypassed() {
302            0.0
303        } else {
304            self.trims.at(now).low
305        }
306    }
307
308    /// The dimmest on level a step knows at `now`: the low trim while one
309    /// applies, otherwise 1.0. STEP_OFF turns the light off from it.
310    pub(super) fn step_floor(&self, now: Duration) -> f32 {
311        self.low_bound(now).max(1.0)
312    }
313
314    /// The line a fade or ramp from `from` to `to` runs at `now`: one up from
315    /// off starts at the low trim, and one down to off ends its line there,
316    /// going off as it finishes. Unchanged with no low trim in effect, or
317    /// from the low trim itself, which holds there until the end.
318    pub(super) fn trimmed_line(&self, from: f32, to: f32, now: Duration) -> (f32, f32) {
319        let low = self.low_bound(now);
320        if low <= 0.0 {
321            (from, to)
322        } else if from == 0.0 && to > 0.0 {
323            (low, to)
324        } else if to == 0.0 && from > low {
325            (from, low)
326        } else {
327            (from, to)
328        }
329    }
330
331    /// Read High_End_Trim, Low_End_Trim or Trim_Fade_Time, if `property` is
332    /// one of them and present.
333    pub(super) fn read_trim(
334        &self,
335        property: bacnet_types::enums::PropertyIdentifier,
336    ) -> Option<Result<PropertyValue, Error>> {
337        use bacnet_types::enums::PropertyIdentifier as P;
338        let value = match property {
339            P::HIGH_END_TRIM => self.trims.high_end.map(PropertyValue::Real),
340            P::LOW_END_TRIM => self.trims.low_end.map(PropertyValue::Real),
341            P::TRIM_FADE_TIME => self
342                .trims
343                .has_fade_time()
344                .then(|| PropertyValue::Unsigned(u64::from(self.trims.fade_time))),
345            _ => return None,
346        };
347        Some(value.ok_or_else(common::unknown_property_error))
348    }
349
350    /// Write High_End_Trim, Low_End_Trim or Trim_Fade_Time, if `property` is
351    /// one of them and present.
352    pub(super) fn write_trim(
353        &mut self,
354        property: bacnet_types::enums::PropertyIdentifier,
355        value: &PropertyValue,
356    ) -> Option<Result<(), Error>> {
357        use bacnet_types::enums::PropertyIdentifier as P;
358        if let Err(error) = self.read_trim(property)? {
359            return Some(Err(error));
360        }
361        Some(match (property, value) {
362            (P::HIGH_END_TRIM, PropertyValue::Real(trim)) => self.set_high_end_trim(Some(*trim)),
363            (P::LOW_END_TRIM, PropertyValue::Real(trim)) => self.set_low_end_trim(Some(*trim)),
364            (P::TRIM_FADE_TIME, PropertyValue::Unsigned(milliseconds)) => {
365                common::u64_to_u32(*milliseconds).and_then(|ms| self.set_trim_fade_time(ms))
366            }
367            _ => Err(common::invalid_data_type_error()),
368        })
369    }
370}