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 std::ops::RangeInclusive;
50use std::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        self.advance_to(now);
247        let from = self.trims.at(now);
248        change(&mut self.trims);
249        // The trims set CONFIGURATION_ERROR, and clear only what they set.
250        match (self.trims.misconfigured(), self.trims.replaced) {
251            (true, None) => {
252                self.trims.replaced = Some(self.reliability);
253                self.reliability = Reliability::CONFIGURATION_ERROR;
254            }
255            (false, Some(replaced)) => {
256                self.trims.replaced = None;
257                if self.reliability == Reliability::CONFIGURATION_ERROR {
258                    self.reliability = replaced;
259                }
260            }
261            _ => {}
262        }
263        let fade_time = Duration::from_millis(u64::from(self.trims.fade_time));
264        let fade = Transition::fade(from, self.trims.configured(), now, fade_time);
265        self.trims.fade = fade.map(|fade| Run::start(fade, self.sample_step()));
266        if self.trims.fade.is_some() {
267            self.wake();
268        }
269    }
270
271    /// Whether the trims stand aside: Present_Value comes from slot 1 or 2
272    /// (Clauses 12.54.Y1 and 12.54.Y2).
273    fn trims_bypassed(&self) -> bool {
274        matches!(self.highest_priority(), Some(1 | 2))
275    }
276
277    /// `level` as Tracking_Value reports it at `now`: held in the operating
278    /// range there.
279    pub(super) fn trimmed(&self, level: f32, now: Duration) -> f32 {
280        if self.trims_bypassed() {
281            level
282        } else {
283            self.trims.at(now).hold(level)
284        }
285    }
286
287    /// Whether In_Progress reads TRIM_ACTIVE at `now`, where `untrimmed` is
288    /// Tracking_Value before the trims hold it: Present_Value lies outside
289    /// the trims, or the bounds of a trim change still moving hold
290    /// Tracking_Value back.
291    pub(super) fn trim_active(&self, untrimmed: f32, now: Duration) -> bool {
292        !self.trims_bypassed()
293            && (self.trims.configured().hold(self.present_value) != self.present_value
294                || (self.trims.moving(now) && self.trims.at(now).hold(untrimmed) != untrimmed))
295    }
296
297    /// The low bound in effect at `now`, or 0.0 when no low trim holds
298    /// anything.
299    fn low_bound(&self, now: Duration) -> f32 {
300        if self.trims_bypassed() {
301            0.0
302        } else {
303            self.trims.at(now).low
304        }
305    }
306
307    /// The dimmest on level a step knows at `now`: the low trim while one
308    /// applies, otherwise 1.0. STEP_OFF turns the light off from it.
309    pub(super) fn step_floor(&self, now: Duration) -> f32 {
310        self.low_bound(now).max(1.0)
311    }
312
313    /// The line a fade or ramp from `from` to `to` runs at `now`: one up from
314    /// off starts at the low trim, and one down to off ends its line there,
315    /// going off as it finishes. Unchanged with no low trim in effect, or
316    /// from the low trim itself, which holds there until the end.
317    pub(super) fn trimmed_line(&self, from: f32, to: f32, now: Duration) -> (f32, f32) {
318        let low = self.low_bound(now);
319        if low <= 0.0 {
320            (from, to)
321        } else if from == 0.0 && to > 0.0 {
322            (low, to)
323        } else if to == 0.0 && from > low {
324            (from, low)
325        } else {
326            (from, to)
327        }
328    }
329
330    /// Read High_End_Trim, Low_End_Trim or Trim_Fade_Time, if `property` is
331    /// one of them and present.
332    pub(super) fn read_trim(
333        &self,
334        property: bacnet_types::enums::PropertyIdentifier,
335    ) -> Option<Result<PropertyValue, Error>> {
336        use bacnet_types::enums::PropertyIdentifier as P;
337        let value = match property {
338            P::HIGH_END_TRIM => self.trims.high_end.map(PropertyValue::Real),
339            P::LOW_END_TRIM => self.trims.low_end.map(PropertyValue::Real),
340            P::TRIM_FADE_TIME => self
341                .trims
342                .has_fade_time()
343                .then(|| PropertyValue::Unsigned(u64::from(self.trims.fade_time))),
344            _ => return None,
345        };
346        Some(value.ok_or_else(common::unknown_property_error))
347    }
348
349    /// Write High_End_Trim, Low_End_Trim or Trim_Fade_Time, if `property` is
350    /// one of them and present.
351    pub(super) fn write_trim(
352        &mut self,
353        property: bacnet_types::enums::PropertyIdentifier,
354        value: &PropertyValue,
355    ) -> Option<Result<(), Error>> {
356        use bacnet_types::enums::PropertyIdentifier as P;
357        if let Err(error) = self.read_trim(property)? {
358            return Some(Err(error));
359        }
360        Some(match (property, value) {
361            (P::HIGH_END_TRIM, PropertyValue::Real(trim)) => self.set_high_end_trim(Some(*trim)),
362            (P::LOW_END_TRIM, PropertyValue::Real(trim)) => self.set_low_end_trim(Some(*trim)),
363            (P::TRIM_FADE_TIME, PropertyValue::Unsigned(milliseconds)) => {
364                common::u64_to_u32(*milliseconds).and_then(|ms| self.set_trim_fade_time(ms))
365            }
366            _ => Err(common::invalid_data_type_error()),
367        })
368    }
369}