use crate::motion::{clamp, magnitude, Twist};
use crate::Ramp;
use libm::sqrtf;
#[derive(Clone, Copy, Debug, Default)]
pub struct EStop {
engaged: bool,
}
impl EStop {
pub fn new() -> Self {
Self { engaged: false }
}
pub fn engage(&mut self) {
self.engaged = true;
}
pub fn reset(&mut self) {
self.engaged = false;
}
pub fn is_engaged(&self) -> bool {
self.engaged
}
pub fn gate(&self, desired: Twist) -> Twist {
if self.engaged {
Twist::zero()
} else {
desired
}
}
}
#[derive(Clone, Copy, Debug)]
pub struct Watchdog {
timeout: f32,
elapsed: f32,
}
impl Watchdog {
pub fn new(timeout: f32) -> Self {
Self {
timeout: magnitude(timeout),
elapsed: 0.0,
}
}
pub fn feed(&mut self) {
self.elapsed = 0.0;
}
pub fn update(&mut self, dt: f32) -> bool {
self.elapsed += magnitude(dt);
self.is_expired()
}
pub fn is_expired(&self) -> bool {
self.elapsed > self.timeout
}
}
#[derive(Clone, Copy, Debug)]
pub struct Limits {
max_linear: f32,
max_angular: f32,
max_linear_accel: f32,
max_angular_accel: f32,
vx: Ramp,
vy: Ramp,
omega: Ramp,
}
impl Limits {
pub fn new(
max_linear: f32,
max_angular: f32,
max_linear_accel: f32,
max_angular_accel: f32,
) -> Self {
Self {
max_linear: magnitude(max_linear),
max_angular: magnitude(max_angular),
max_linear_accel: magnitude(max_linear_accel),
max_angular_accel: magnitude(max_angular_accel),
vx: Ramp::new(0.0, 0.0),
vy: Ramp::new(0.0, 0.0),
omega: Ramp::new(0.0, 0.0),
}
}
pub fn reset(&mut self) {
self.vx.set(0.0);
self.vy.set(0.0);
self.omega.set(0.0);
}
pub fn apply(&mut self, desired: Twist, dt: f32) -> Twist {
let bounded = self.clamp_speed(desired);
let linear_step = self.max_linear_accel * magnitude(dt);
let angular_step = self.max_angular_accel * magnitude(dt);
Twist::new(
self.vx.update_capped(bounded.vx, linear_step),
self.vy.update_capped(bounded.vy, linear_step),
self.omega.update_capped(bounded.omega, angular_step),
)
}
fn clamp_speed(&self, twist: Twist) -> Twist {
let speed = sqrtf(twist.vx * twist.vx + twist.vy * twist.vy);
let (vx, vy) = if speed > self.max_linear {
let scale = self.max_linear / speed;
(twist.vx * scale, twist.vy * scale)
} else {
(twist.vx, twist.vy)
};
Twist::new(
vx,
vy,
clamp(twist.omega, -self.max_angular, self.max_angular),
)
}
}
#[derive(Clone, Copy, Debug)]
pub struct SafetyGate {
estop: EStop,
watchdog: Watchdog,
limits: Limits,
}
impl SafetyGate {
pub fn new(limits: Limits, watchdog_timeout: f32) -> Self {
Self {
estop: EStop::new(),
watchdog: Watchdog::new(watchdog_timeout),
limits,
}
}
pub fn feed(&mut self) {
self.watchdog.feed();
}
pub fn engage_estop(&mut self) {
self.estop.engage();
}
pub fn reset_estop(&mut self) {
self.estop.reset();
}
pub fn is_stopped(&self) -> bool {
self.estop.is_engaged() || self.watchdog.is_expired()
}
pub fn command(&mut self, desired: Twist, dt: f32) -> Twist {
self.watchdog.update(dt);
if self.is_stopped() {
self.limits.reset();
return Twist::zero();
}
self.limits.apply(desired, dt)
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn estop_latches_until_reset() {
let mut estop = EStop::new();
let cmd = Twist::planar(1.0, 0.5);
assert_eq!(estop.gate(cmd), cmd);
estop.engage();
assert!(estop.is_engaged());
assert_eq!(estop.gate(cmd), Twist::zero());
estop.reset();
assert_eq!(estop.gate(cmd), cmd);
}
#[test]
fn watchdog_expires_on_silence_and_revives_on_feeding() {
let mut dog = Watchdog::new(0.5);
dog.feed();
assert!(!dog.update(0.3));
assert!(dog.update(0.3)); dog.feed();
assert!(!dog.is_expired());
}
#[test]
fn limits_cap_planar_speed_by_scaling() {
let mut limits = Limits::new(1.0, 10.0, 100.0, 100.0); let cmd = limits.apply(Twist::new(3.0, 4.0, 0.0), 1.0);
assert!((cmd.vx - 0.6).abs() < 1e-5);
assert!((cmd.vy - 0.8).abs() < 1e-5);
}
#[test]
fn limits_ease_in_at_the_acceleration_bound() {
let mut limits = Limits::new(1.0, 2.0, 0.5, 4.0);
assert!((limits.apply(Twist::planar(1.0, 0.0), 0.1).vx - 0.05).abs() < 1e-6);
assert!((limits.apply(Twist::planar(1.0, 0.0), 0.1).vx - 0.10).abs() < 1e-6);
}
#[test]
fn gate_stops_on_estop_and_on_watchdog_expiry() {
let mut gate = SafetyGate::new(Limits::new(1.0, 2.0, 100.0, 100.0), 0.2);
gate.feed();
assert!(gate.command(Twist::planar(1.0, 0.0), 0.1).vx > 0.0);
assert_eq!(gate.command(Twist::planar(1.0, 0.0), 0.5), Twist::zero());
assert!(gate.is_stopped());
gate.feed();
assert!(gate.command(Twist::planar(1.0, 0.0), 0.1).vx > 0.0);
gate.feed();
gate.engage_estop();
assert_eq!(gate.command(Twist::planar(1.0, 0.0), 0.1), Twist::zero());
}
}