use std::time::Duration;
use crate::error::{Error, Result};
#[derive(Debug, Clone, Copy, PartialEq)]
pub struct SlewLimiter {
max_change_per_second: f32,
current_setpoint: f32,
}
impl SlewLimiter {
pub const GENTLE: Self = Self {
max_change_per_second: 1.0,
current_setpoint: 0.0,
};
pub fn new(max_change_per_second: f32) -> Result<Self> {
if !max_change_per_second.is_finite() || max_change_per_second <= 0.0 {
return Err(Error::InvalidSlewRate(max_change_per_second));
}
Ok(Self {
max_change_per_second,
current_setpoint: 0.0,
})
}
pub fn step(&mut self, target_setpoint: f32, elapsed: Duration) -> f32 {
if !target_setpoint.is_finite() {
return self.current_setpoint;
}
let max_change = self.max_change_per_second * elapsed.as_secs_f32();
let change = (target_setpoint - self.current_setpoint).clamp(-max_change, max_change);
self.current_setpoint += change;
self.current_setpoint
}
pub fn reset_to(&mut self, value: f32) {
if value.is_finite() {
self.current_setpoint = value;
}
}
pub fn current_setpoint(&self) -> f32 {
self.current_setpoint
}
pub fn max_change_per_second(&self) -> f32 {
self.max_change_per_second
}
}
#[cfg(test)]
mod tests {
use super::*;
const CYCLE: Duration = Duration::from_millis(20);
fn limiter(rate: f32) -> SlewLimiter {
match SlewLimiter::new(rate) {
Ok(l) => l,
Err(e) => unreachable!("valid rate rejected: {e}"),
}
}
#[test]
fn new_rejects_rates_that_would_disable_limiting() {
for bad in [0.0, -1.0, f32::NAN, f32::INFINITY, f32::NEG_INFINITY] {
assert!(
SlewLimiter::new(bad).is_err(),
"rate {bad} must be refused, not sanitized"
);
}
assert!(SlewLimiter::new(f32::MIN_POSITIVE).is_ok());
}
#[test]
fn steps_are_capped_at_rate_times_elapsed() {
let mut l = limiter(300.0);
assert_eq!(l.step(250.0, CYCLE), 6.0);
assert_eq!(l.step(250.0, CYCLE), 12.0);
assert_eq!(l.current_setpoint(), 12.0);
}
#[test]
fn converges_to_the_target_and_then_holds_it() {
let mut l = limiter(300.0);
for _ in 0..100 {
l.step(60.0, CYCLE);
}
assert_eq!(
l.step(60.0, CYCLE),
60.0,
"must settle exactly, not oscillate"
);
assert_eq!(l.step(60.0, CYCLE), 60.0);
}
#[test]
fn never_overshoots_a_target_closer_than_one_step() {
let mut l = limiter(300.0);
assert_eq!(l.step(2.0, CYCLE), 2.0, "a 2 RPM target must not become 6");
}
#[test]
fn limiting_is_symmetric_on_the_way_down_and_negative() {
let mut l = limiter(300.0);
l.reset_to(100.0);
assert_eq!(l.step(0.0, CYCLE), 94.0);
l.reset_to(0.0);
assert_eq!(l.step(-250.0, CYCLE), -6.0, "reverse must ramp too");
}
#[test]
fn a_full_reversal_ramps_through_zero_rather_than_stepping() {
let mut l = limiter(300.0);
l.reset_to(250.0);
let after = l.step(-250.0, CYCLE);
assert_eq!(after, 244.0);
assert!(
after > 0.0,
"F->B must not cross to full reverse in one cycle"
);
}
#[test]
fn reset_to_bypasses_the_limit_for_stop_paths() {
let mut l = limiter(300.0);
l.reset_to(250.0);
assert_eq!(l.current_setpoint(), 250.0);
l.reset_to(0.0);
assert_eq!(l.current_setpoint(), 0.0, "a fail-safe must not ramp");
}
#[test]
fn zero_elapsed_holds_the_setpoint() {
let mut l = limiter(300.0);
l.reset_to(42.0);
assert_eq!(l.step(250.0, Duration::ZERO), 42.0);
}
#[test]
fn non_finite_target_holds_instead_of_poisoning_the_setpoint() {
let mut l = limiter(300.0);
l.reset_to(50.0);
for bad in [f32::NAN, f32::INFINITY, f32::NEG_INFINITY] {
assert_eq!(l.step(bad, CYCLE), 50.0);
assert!(l.current_setpoint().is_finite(), "{bad} leaked into state");
}
assert_eq!(l.step(56.0, CYCLE), 56.0);
}
#[test]
fn non_finite_reset_is_ignored() {
let mut l = limiter(300.0);
l.reset_to(7.0);
l.reset_to(f32::NAN);
assert_eq!(l.current_setpoint(), 7.0);
}
#[test]
fn an_absurd_elapsed_does_not_overshoot_or_go_non_finite() {
let mut l = limiter(f32::MAX);
let out = l.step(250.0, Duration::from_secs(u32::MAX.into()));
assert_eq!(out, 250.0, "a huge dt saturates at the target, not past it");
assert!(out.is_finite());
}
}