use std::ops::RangeInclusive;
use std::time::Duration;
use bacnet_types::enums::Reliability;
use bacnet_types::error::Error;
use bacnet_types::primitives::PropertyValue;
use super::LightingOutputObject;
use crate::common;
use crate::transition::{Progress, Run, Transition, TransitionLevel};
const TRIM_FADE_TIME_MS: RangeInclusive<u32> = 0..=86_400_000;
#[derive(Debug, Clone, Copy, PartialEq)]
pub(super) struct Bounds {
low: f32,
high: f32,
}
impl Bounds {
const FULL: Self = Self {
low: 0.0,
high: 100.0,
};
fn hold(self, level: f32) -> f32 {
if level > 0.0 {
level.clamp(self.low, self.high)
} else {
level
}
}
}
impl TransitionLevel for Bounds {
fn interpolate(from: Self, to: Self, fraction: f64) -> Self {
Self {
low: f32::interpolate(from.low, to.low, fraction),
high: f32::interpolate(from.high, to.high, fraction),
}
}
fn distance(from: Self, to: Self) -> f64 {
f32::distance(from.low, to.low).max(f32::distance(from.high, to.high))
}
}
#[derive(Debug, Clone, Copy, PartialEq)]
pub(super) struct Trims {
high_end: Option<f32>,
low_end: Option<f32>,
fade_time: u32,
fade: Option<Run<Bounds>>,
replaced: Option<Reliability>,
}
impl Trims {
pub(super) const NONE: Self = Self {
high_end: None,
low_end: None,
fade_time: 0,
fade: None,
replaced: None,
};
pub(super) fn high_end(&self) -> Option<f32> {
self.high_end
}
pub(super) fn low_end(&self) -> Option<f32> {
self.low_end
}
pub(super) fn has_fade_time(&self) -> bool {
self.high_end.is_some() || self.low_end.is_some()
}
fn misconfigured(&self) -> bool {
matches!((self.high_end, self.low_end), (Some(high), Some(low)) if high < low)
}
fn configured(&self) -> Bounds {
if self.misconfigured() {
return Bounds::FULL;
}
Bounds {
low: self.low_end.unwrap_or(Bounds::FULL.low),
high: self.high_end.unwrap_or(Bounds::FULL.high),
}
}
fn moving(&self, now: Duration) -> bool {
self.fade
.is_some_and(|run| !run.transition().is_finished(now))
}
fn at(&self, now: Duration) -> Bounds {
self.fade
.map_or_else(|| self.configured(), |run| run.transition().value_at(now))
}
pub(super) fn advance(&mut self, now: Duration, step: f64) -> bool {
let Some(run) = &mut self.fade else {
return false;
};
match run.advance(now, step) {
Progress::Finished => {
self.fade = None;
true
}
Progress::Sampled => true,
Progress::Pending => false,
}
}
pub(super) fn deadline(&self) -> Option<Duration> {
self.fade.map(|run| run.deadline())
}
}
fn trim_level(value: f32) -> Result<f32, Error> {
if (1.0..=100.0).contains(&value) {
Ok(value)
} else {
Err(common::value_out_of_range_error())
}
}
impl LightingOutputObject {
pub fn set_high_end_trim(&mut self, trim: Option<f32>) -> Result<(), Error> {
let trim = trim.map(trim_level).transpose()?;
self.change_trims(|trims| trims.high_end = trim);
Ok(())
}
pub fn set_low_end_trim(&mut self, trim: Option<f32>) -> Result<(), Error> {
let trim = trim.map(trim_level).transpose()?;
self.change_trims(|trims| trims.low_end = trim);
Ok(())
}
pub fn set_trim_fade_time(&mut self, milliseconds: u32) -> Result<(), Error> {
if !TRIM_FADE_TIME_MS.contains(&milliseconds) {
return Err(common::value_out_of_range_error());
}
self.trims.fade_time = milliseconds;
Ok(())
}
fn change_trims(&mut self, change: impl FnOnce(&mut Trims)) {
let now = self.now();
self.advance_to(now);
let from = self.trims.at(now);
change(&mut self.trims);
match (self.trims.misconfigured(), self.trims.replaced) {
(true, None) => {
self.trims.replaced = Some(self.reliability);
self.reliability = Reliability::CONFIGURATION_ERROR;
}
(false, Some(replaced)) => {
self.trims.replaced = None;
if self.reliability == Reliability::CONFIGURATION_ERROR {
self.reliability = replaced;
}
}
_ => {}
}
let fade_time = Duration::from_millis(u64::from(self.trims.fade_time));
let fade = Transition::fade(from, self.trims.configured(), now, fade_time);
self.trims.fade = fade.map(|fade| Run::start(fade, self.sample_step()));
if self.trims.fade.is_some() {
self.wake();
}
}
fn trims_bypassed(&self) -> bool {
matches!(self.highest_priority(), Some(1 | 2))
}
pub(super) fn trimmed(&self, level: f32, now: Duration) -> f32 {
if self.trims_bypassed() {
level
} else {
self.trims.at(now).hold(level)
}
}
pub(super) fn trim_active(&self, untrimmed: f32, now: Duration) -> bool {
!self.trims_bypassed()
&& (self.trims.configured().hold(self.present_value) != self.present_value
|| (self.trims.moving(now) && self.trims.at(now).hold(untrimmed) != untrimmed))
}
fn low_bound(&self, now: Duration) -> f32 {
if self.trims_bypassed() {
0.0
} else {
self.trims.at(now).low
}
}
pub(super) fn step_floor(&self, now: Duration) -> f32 {
self.low_bound(now).max(1.0)
}
pub(super) fn trimmed_line(&self, from: f32, to: f32, now: Duration) -> (f32, f32) {
let low = self.low_bound(now);
if low <= 0.0 {
(from, to)
} else if from == 0.0 && to > 0.0 {
(low, to)
} else if to == 0.0 && from > low {
(from, low)
} else {
(from, to)
}
}
pub(super) fn read_trim(
&self,
property: bacnet_types::enums::PropertyIdentifier,
) -> Option<Result<PropertyValue, Error>> {
use bacnet_types::enums::PropertyIdentifier as P;
let value = match property {
P::HIGH_END_TRIM => self.trims.high_end.map(PropertyValue::Real),
P::LOW_END_TRIM => self.trims.low_end.map(PropertyValue::Real),
P::TRIM_FADE_TIME => self
.trims
.has_fade_time()
.then(|| PropertyValue::Unsigned(u64::from(self.trims.fade_time))),
_ => return None,
};
Some(value.ok_or_else(common::unknown_property_error))
}
pub(super) fn write_trim(
&mut self,
property: bacnet_types::enums::PropertyIdentifier,
value: &PropertyValue,
) -> Option<Result<(), Error>> {
use bacnet_types::enums::PropertyIdentifier as P;
if let Err(error) = self.read_trim(property)? {
return Some(Err(error));
}
Some(match (property, value) {
(P::HIGH_END_TRIM, PropertyValue::Real(trim)) => self.set_high_end_trim(Some(*trim)),
(P::LOW_END_TRIM, PropertyValue::Real(trim)) => self.set_low_end_trim(Some(*trim)),
(P::TRIM_FADE_TIME, PropertyValue::Unsigned(milliseconds)) => {
common::u64_to_u32(*milliseconds).and_then(|ms| self.set_trim_fade_time(ms))
}
_ => Err(common::invalid_data_type_error()),
})
}
}