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}