use crate::core::scalar::ControlScalar;
#[derive(Debug, Clone, Copy)]
pub struct StuckMonitor<S: ControlScalar> {
pub threshold: S,
pub timeout: S,
prev_value: S,
stagnant_time: S,
stuck: bool,
initialized: bool,
}
impl<S: ControlScalar> StuckMonitor<S> {
pub fn new(threshold: S, timeout: S) -> Self {
Self {
threshold,
timeout,
prev_value: S::ZERO,
stagnant_time: S::ZERO,
stuck: false,
initialized: false,
}
}
pub fn check(&mut self, value: S, dt: S) -> bool {
if !self.initialized {
self.prev_value = value;
self.initialized = true;
return true;
}
let change = (value - self.prev_value).abs();
self.prev_value = value;
if change >= self.threshold {
self.stagnant_time = S::ZERO;
} else {
self.stagnant_time += dt;
if self.stagnant_time >= self.timeout {
self.stuck = true;
}
}
!self.stuck
}
pub fn is_stuck(&self) -> bool {
self.stuck
}
pub fn reset(&mut self) {
self.stagnant_time = S::ZERO;
self.stuck = false;
self.initialized = false;
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn moving_signal_does_not_trip() {
let mut mon = StuckMonitor::new(0.01_f64, 1.0);
let mut v = 0.0_f64;
for _ in 0..100 {
v += 0.1;
assert!(mon.check(v, 0.01));
}
assert!(!mon.is_stuck());
}
#[test]
fn stuck_signal_trips_after_timeout() {
let mut mon = StuckMonitor::new(0.01_f64, 1.0);
for _ in 0..200 {
mon.check(5.0, 0.01); }
assert!(mon.is_stuck());
}
#[test]
fn small_changes_below_threshold_count_as_stuck() {
let mut mon = StuckMonitor::new(0.1_f64, 0.5); for i in 0..200 {
mon.check(i as f64 * 0.001, 0.01);
}
assert!(mon.is_stuck());
}
#[test]
fn reset_clears_stuck_state() {
let mut mon = StuckMonitor::new(0.01_f64, 0.1);
for _ in 0..20 {
mon.check(1.0, 0.01);
}
assert!(mon.is_stuck());
mon.reset();
assert!(!mon.is_stuck());
assert!(mon.check(2.0, 0.01));
}
}