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}