use crate::traits::Scalar;
use core::time::Duration;
use pictorus_block_data::{BlockData as OldBlockData, FromPass};
use pictorus_traits::{Context, Matrix, Pass, PassBy, ProcessBlock};
pub struct DelayControlBlock<T: Apply> {
pub data: OldBlockData,
buffer: Option<T::Output>,
state: T::State,
}
impl<T: Apply> Default for DelayControlBlock<T>
where
OldBlockData: FromPass<T::Output>,
{
fn default() -> Self {
Self {
data: <OldBlockData as FromPass<T::Output>>::from_pass(T::Output::default().as_by()),
buffer: None,
state: T::init_state(),
}
}
}
impl<T: Apply> ProcessBlock for DelayControlBlock<T>
where
OldBlockData: FromPass<T::Output>,
{
type Inputs = T;
type Output = T::Output;
type Parameters = Parameters;
fn process<'b>(
&'b mut self,
parameters: &Self::Parameters,
context: &dyn Context,
inputs: PassBy<'_, Self::Inputs>,
) -> PassBy<'b, Self::Output> {
let buffer = self.buffer.get_or_insert(T::Output::default());
let output = T::apply(buffer, inputs, &mut self.state, parameters, context);
self.data = <OldBlockData as FromPass<T::Output>>::from_pass(output);
output
}
}
pub trait Apply: Pass {
type State;
type Output: Pass + Default;
fn init_state() -> Self::State;
fn apply<'s>(
store: &'s mut Self::Output,
input: PassBy<Self>,
state: &mut Self::State,
parameters: &Parameters,
context: &dyn Context,
) -> PassBy<'s, Self::Output>;
}
impl<S: Scalar> Apply for S {
type State = Option<Duration>;
type Output = f64;
fn init_state() -> Self::State {
None
}
fn apply<'s>(
store: &'s mut Self::Output,
input: PassBy<Self>,
state: &mut Option<Duration>,
parameters: &Parameters,
context: &dyn Context,
) -> PassBy<'s, Self::Output> {
let is_true = input.is_truthy();
match parameters.method {
DelayControlMethod::Debounce => {
*store = debounce(is_true, state, parameters.delay, context.time());
}
DelayControlMethod::Throttle => {
*store = throttle(is_true, state, parameters.delay, context.time());
}
}
store.as_by()
}
}
impl<S: Scalar, const NROWS: usize, const NCOLS: usize> Apply for Matrix<NROWS, NCOLS, S> {
type Output = Matrix<NROWS, NCOLS, f64>;
type State = [[Option<Duration>; NROWS]; NCOLS];
fn init_state() -> Self::State {
[[None; NROWS]; NCOLS]
}
fn apply<'s>(
store: &'s mut Self::Output,
input: PassBy<Self>,
state: &mut Self::State,
parameters: &Parameters,
context: &dyn Context,
) -> PassBy<'s, Self::Output> {
let input_flat = input.data.as_flattened();
let state_flat = state.as_flattened_mut();
let store_flat = store.data.as_flattened_mut();
for i in 0..input_flat.len() {
let is_true = input_flat[i].is_truthy();
match parameters.method {
DelayControlMethod::Debounce => {
store_flat[i] = debounce(
is_true,
&mut state_flat[i],
parameters.delay,
context.time(),
);
}
DelayControlMethod::Throttle => {
store_flat[i] = throttle(
is_true,
&mut state_flat[i],
parameters.delay,
context.time(),
);
}
}
}
store.as_by()
}
}
fn debounce(
input: bool,
state: &mut Option<Duration>,
delay: Duration,
curr_time: Duration,
) -> f64 {
let mut output = false;
if input {
*state = Some(curr_time);
} else if let Some(d) = state {
if curr_time - *d >= delay {
output = true;
*state = None;
}
}
if output {
1.0
} else {
0.0
}
}
fn throttle(
input: bool,
state: &mut Option<Duration>,
delay: Duration,
curr_time: Duration,
) -> f64 {
let mut output = false;
if let Some(d) = state {
if curr_time - *d >= delay {
*state = None;
}
}
if input && state.is_none() {
output = true;
*state = Some(curr_time);
}
if output {
1.0
} else {
0.0
}
}
#[derive(strum::EnumString, Clone, Copy, Debug)]
pub enum DelayControlMethod {
Debounce,
Throttle,
}
#[derive(Clone, Copy, Debug)]
pub struct Parameters {
delay: Duration,
method: DelayControlMethod,
}
impl Parameters {
pub fn new(delay: f64, method: &str) -> Self {
Self {
delay: Duration::from_secs_f64(delay),
method: method.parse().unwrap(),
}
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::testing::StubRuntime;
#[test]
fn test_scalar_throttle() {
let mut runtime = StubRuntime::default(); let mut block = DelayControlBlock::<f64>::default();
let parameters = Parameters::new(0.3, "Throttle");
let output = block.process(¶meters, &runtime.context(), 0.0);
assert!(!output.is_truthy());
assert_eq!(block.data, OldBlockData::scalar_from_bool(false));
runtime.tick(); let output = block.process(¶meters, &runtime.context(), 0.5);
assert!(output.is_truthy());
assert_eq!(block.data, OldBlockData::scalar_from_bool(true));
runtime.tick(); let output = block.process(¶meters, &runtime.context(), 1.0);
assert!(!output.is_truthy());
assert_eq!(block.data, OldBlockData::scalar_from_bool(false));
runtime.tick(); let output = block.process(¶meters, &runtime.context(), 1.5);
assert!(!output.is_truthy());
assert_eq!(block.data, OldBlockData::scalar_from_bool(false));
runtime.tick(); let output = block.process(¶meters, &runtime.context(), 2.0);
assert!(output.is_truthy());
assert_eq!(block.data, OldBlockData::scalar_from_bool(true));
runtime.tick(); let output = block.process(¶meters, &runtime.context(), 2.5);
assert!(!output.is_truthy());
assert_eq!(block.data, OldBlockData::scalar_from_bool(false));
}
#[test]
fn test_scalar_debounce() {
let mut runtime = StubRuntime::default(); let mut block = DelayControlBlock::<f64>::default();
let parameters = Parameters::new(0.3, "Debounce");
let output = block.process(¶meters, &runtime.context(), 0.0);
assert!(!output.is_truthy());
assert_eq!(block.data, OldBlockData::scalar_from_bool(false));
runtime.tick(); let output = block.process(¶meters, &runtime.context(), -2.0);
assert!(!output.is_truthy());
assert_eq!(block.data, OldBlockData::scalar_from_bool(false));
runtime.tick(); let output = block.process(¶meters, &runtime.context(), 0.0);
assert!(!output.is_truthy());
assert_eq!(block.data, OldBlockData::scalar_from_bool(false));
runtime.tick(); let output = block.process(¶meters, &runtime.context(), 0.0);
assert!(!output.is_truthy());
assert_eq!(block.data, OldBlockData::scalar_from_bool(false));
runtime.tick(); let output = block.process(¶meters, &runtime.context(), 0.0);
assert!(output.is_truthy());
assert_eq!(block.data, OldBlockData::scalar_from_bool(true));
runtime.tick(); let output = block.process(¶meters, &runtime.context(), 0.0);
assert!(!output.is_truthy());
assert_eq!(block.data, OldBlockData::scalar_from_bool(false));
runtime.tick(); let output = block.process(¶meters, &runtime.context(), 0.0);
assert!(!output.is_truthy());
assert_eq!(block.data, OldBlockData::scalar_from_bool(false));
runtime.tick(); let output = block.process(¶meters, &runtime.context(), 1.0);
assert!(!output.is_truthy());
assert_eq!(block.data, OldBlockData::scalar_from_bool(false));
runtime.tick(); let output = block.process(¶meters, &runtime.context(), 0.0);
assert!(!output.is_truthy());
assert_eq!(block.data, OldBlockData::scalar_from_bool(false));
runtime.context.time += Duration::from_secs_f64(0.3);
let output = block.process(¶meters, &runtime.context(), 0.0);
assert!(output.is_truthy());
assert_eq!(block.data, OldBlockData::scalar_from_bool(true));
runtime.tick();
let output = block.process(¶meters, &runtime.context(), 0.0);
assert!(!output.is_truthy());
assert_eq!(block.data, OldBlockData::scalar_from_bool(false));
}
}