use std::time::Duration;
use uuid::Uuid;
use crate::base::output::props::OutputProps;
use crate::base::{
block::{Block, BlockDesc, BlockProps, BlockState},
input::{InputProps, input_reader::InputReader},
output::Output,
};
use crate::blocks::utils::{input_as_number, input_as_number_matching, input_to_millis_or_default};
use crate::tokio_impl::sleep::current_time_millis;
use libhaystack::units::Unit;
use libhaystack::val::{Number, kind::HaystackKind};
use crate::{blocks::InputImpl, blocks::OutputImpl};
#[block]
#[derive(BlockProps, Debug)]
#[category = "control"]
pub struct TrimRespond {
#[input(kind = "Number")]
pub requests: InputImpl,
#[input(kind = "Number")]
pub period: InputImpl,
#[input(kind = "Number")]
pub ignore: InputImpl,
#[input(kind = "Number")]
pub trim: InputImpl,
#[input(kind = "Number")]
pub respond: InputImpl,
#[input(name = "maxChange", kind = "Number")]
pub max_change: InputImpl,
#[input(kind = "Number")]
pub min: InputImpl,
#[input(kind = "Number")]
pub max: InputImpl,
#[input(kind = "Number")]
pub initial: InputImpl,
#[output(kind = "Number")]
pub out: OutputImpl,
sp: f64,
last_update_ms: u64,
initialized: bool,
sp_unit: Option<&'static Unit>,
}
impl Block for TrimRespond {
async fn execute(&mut self) {
let period_ms = input_to_millis_or_default(&self.period.val);
let poll = period_ms.clamp(50, 1000);
self.wait_on_inputs(Duration::from_millis(poll)).await;
if !self.out.is_connected() {
return;
}
let initial_n = input_as_number(&self.initial);
let sp_unit = initial_n
.and_then(|n| n.unit)
.or_else(|| input_as_number(&self.min).and_then(|n| n.unit))
.or_else(|| input_as_number(&self.max).and_then(|n| n.unit));
let lo = input_as_number_matching(&self.min, sp_unit).unwrap_or(f64::NEG_INFINITY);
let hi = input_as_number_matching(&self.max, sp_unit).unwrap_or(f64::INFINITY);
let initial = input_as_number_matching(&self.initial, sp_unit)
.unwrap_or_else(|| if lo.is_finite() { lo } else { 0.0 });
let emit = |sp: f64| -> libhaystack::val::Value {
match sp_unit {
Some(u) => Number::make_with_unit(sp, u).into(),
None => sp.into(),
}
};
if !self.initialized {
self.initialized = true;
self.sp = initial.clamp(lo.min(hi), lo.max(hi));
self.sp_unit = sp_unit;
self.last_update_ms = current_time_millis();
self.out.set(emit(self.sp));
return;
}
let now = current_time_millis();
let elapsed = now.saturating_sub(self.last_update_ms);
if elapsed < period_ms {
self.out.set(emit(self.sp));
return;
}
self.last_update_ms = now;
let requests = input_as_number(&self.requests)
.map(|n| n.value)
.unwrap_or(0.0);
let ignore = input_as_number(&self.ignore)
.map(|n| n.value)
.unwrap_or(2.0);
let trim = input_as_number_matching(&self.trim, sp_unit).unwrap_or(0.0);
let respond = input_as_number_matching(&self.respond, sp_unit).unwrap_or(0.0);
let max_change = input_as_number_matching(&self.max_change, sp_unit)
.map(|v| v.abs())
.unwrap_or(f64::INFINITY);
let raw_change = if requests <= ignore {
trim
} else {
respond * (requests - ignore)
};
let bounded = raw_change.clamp(-max_change, max_change);
let (clamp_lo, clamp_hi) = if lo <= hi { (lo, hi) } else { (hi, lo) };
self.sp = (self.sp + bounded).clamp(clamp_lo, clamp_hi);
self.sp_unit = sp_unit;
self.out.set(emit(self.sp));
}
}
#[cfg(test)]
mod test {
use libhaystack::val::Value;
use crate::{
base::block::test_utils::write_block_inputs,
base::{block::Block, link::BaseLink},
blocks::control::TrimRespond,
};
fn link_out(block: &mut TrimRespond) {
block
.out
.links
.push(BaseLink::new(uuid::Uuid::new_v4(), "test".to_string()));
}
#[allow(clippy::too_many_arguments)]
async fn drive(
block: &mut TrimRespond,
requests: f64,
ignore: f64,
trim: f64,
respond: f64,
max_change: f64,
min: f64,
max: f64,
initial: f64,
) {
write_block_inputs([
(&mut block.requests, requests),
(&mut block.period, 0.0),
(&mut block.ignore, ignore),
(&mut block.trim, trim),
(&mut block.respond, respond),
(&mut block.max_change, max_change),
(&mut block.min, min),
(&mut block.max, max),
(&mut block.initial, initial),
])
.await;
}
#[tokio::test]
async fn test_trim_respond_initial_sp() {
let mut block = TrimRespond::new();
link_out(&mut block);
drive(&mut block, 0.0, 2.0, -0.05, 0.05, 0.5, 0.0, 2.0, 0.5).await;
block.execute().await;
assert_eq!(block.out.value, 0.5.into());
}
#[tokio::test]
async fn test_trim_respond_responds_to_requests() {
let mut block = TrimRespond::new();
link_out(&mut block);
drive(&mut block, 0.0, 2.0, -0.05, 0.05, 0.5, 0.0, 2.0, 0.5).await;
block.execute().await;
drive(&mut block, 5.0, 2.0, -0.05, 0.05, 0.5, 0.0, 2.0, 0.5).await;
block.execute().await;
if let Value::Number(n) = block.out.value {
assert!((n.value - 0.65).abs() < 1e-9, "got {}", n.value);
} else {
panic!("expected Number");
}
}
#[tokio::test]
async fn test_trim_respond_trims_when_quiet() {
let mut block = TrimRespond::new();
link_out(&mut block);
drive(&mut block, 0.0, 2.0, -0.05, 0.05, 0.5, 0.0, 2.0, 1.0).await;
block.execute().await;
drive(&mut block, 0.0, 2.0, -0.05, 0.05, 0.5, 0.0, 2.0, 1.0).await;
block.execute().await;
if let Value::Number(n) = block.out.value {
assert!((n.value - 0.95).abs() < 1e-9, "got {}", n.value);
} else {
panic!("expected Number");
}
}
#[tokio::test]
async fn test_trim_respond_clamps_to_max_change() {
let mut block = TrimRespond::new();
link_out(&mut block);
drive(&mut block, 0.0, 2.0, -0.05, 0.05, 0.1, 0.0, 5.0, 1.0).await;
block.execute().await;
drive(&mut block, 100.0, 0.0, -0.05, 0.05, 0.1, 0.0, 5.0, 1.0).await;
block.execute().await;
if let Value::Number(n) = block.out.value {
assert!((n.value - 1.1).abs() < 1e-9, "got {}", n.value);
} else {
panic!("expected Number");
}
}
}