pamoja_kit/safety.rs
1//! Keeping a moving robot safe: stop on command, stop on silence, and never lurch.
2//!
3//! A robot that drives itself is dangerous when something goes wrong, so safety here is a real
4//! feature rather than an afterthought. Three pieces compose into a [`SafetyGate`] that every
5//! motion command passes through: an [`EStop`] that latches the machine stopped until a person
6//! clears it, a [`Watchdog`] that stops the machine if the commands stop arriving (a crashed
7//! controller or a dropped link), and [`Limits`] that bound how fast and how abruptly the robot
8//! may move. The gate fails safe: when stopped it commands zero, and it eases back up through the
9//! limits rather than jumping.
10
11use crate::motion::{clamp, magnitude, Twist};
12use crate::Ramp;
13use libm::sqrtf;
14
15/// An emergency stop that latches: once engaged it holds until explicitly reset.
16///
17/// Unlike a transient condition, an e-stop must stay tripped after the event that caused it, so a
18/// person decides when it is safe to move again. While engaged, [`gate`](EStop::gate) forces any
19/// command to a full stop.
20///
21/// # Examples
22///
23/// ```
24/// use pamoja_kit::{EStop, Twist};
25///
26/// let mut estop = EStop::new();
27/// let cmd = Twist::planar(1.0, 0.0);
28/// assert_eq!(estop.gate(cmd), cmd); // clear: passes through
29///
30/// estop.engage();
31/// assert_eq!(estop.gate(cmd), Twist::zero()); // latched: stopped
32/// estop.reset();
33/// assert_eq!(estop.gate(cmd), cmd); // cleared by a person
34/// ```
35#[derive(Clone, Copy, Debug, Default)]
36pub struct EStop {
37 engaged: bool,
38}
39
40impl EStop {
41 /// Creates a cleared e-stop.
42 ///
43 /// # Returns
44 ///
45 /// An e-stop that is not engaged.
46 pub fn new() -> Self {
47 Self { engaged: false }
48 }
49
50 /// Engages the stop; it latches until [`reset`](EStop::reset).
51 pub fn engage(&mut self) {
52 self.engaged = true;
53 }
54
55 /// Clears the stop, allowing motion again.
56 pub fn reset(&mut self) {
57 self.engaged = false;
58 }
59
60 /// Returns whether the stop is currently engaged.
61 ///
62 /// # Returns
63 ///
64 /// `true` while latched.
65 pub fn is_engaged(&self) -> bool {
66 self.engaged
67 }
68
69 /// Returns the command to apply: the input when clear, a full stop when engaged.
70 ///
71 /// # Arguments
72 ///
73 /// * `desired` - the command that would be applied if clear.
74 ///
75 /// # Returns
76 ///
77 /// `desired` when clear, or [`Twist::zero`] when engaged.
78 pub fn gate(&self, desired: Twist) -> Twist {
79 if self.engaged {
80 Twist::zero()
81 } else {
82 desired
83 }
84 }
85}
86
87/// A deadman timer: it expires unless fed often enough, catching a stalled controller or link.
88///
89/// Autonomy assumes a stream of fresh commands. If that stream stops, because the controller hung
90/// or the radio dropped, the last command must not run forever. A watchdog counts the time since it
91/// was last fed and reports expiry once that exceeds its timeout; the caller feeds it each time a
92/// fresh command arrives. It accumulates a supplied `dt` rather than reading a clock, so it works
93/// the same on a microcontroller with no wall time.
94///
95/// # Examples
96///
97/// ```
98/// use pamoja_kit::Watchdog;
99///
100/// let mut dog = Watchdog::new(0.5); // expire after 0.5 s of silence
101/// dog.feed();
102/// assert!(!dog.update(0.3)); // 0.3 s since feeding: still alive
103/// assert!(dog.update(0.3)); // 0.6 s total: expired
104/// dog.feed(); // a fresh command revives it
105/// assert!(!dog.is_expired());
106/// ```
107#[derive(Clone, Copy, Debug)]
108pub struct Watchdog {
109 timeout: f32,
110 elapsed: f32,
111}
112
113impl Watchdog {
114 /// Creates a watchdog that expires after `timeout` without being fed.
115 ///
116 /// # Arguments
117 ///
118 /// * `timeout` - the allowed silence before expiry; its magnitude is used.
119 ///
120 /// # Returns
121 ///
122 /// A freshly fed watchdog.
123 pub fn new(timeout: f32) -> Self {
124 Self {
125 timeout: magnitude(timeout),
126 elapsed: 0.0,
127 }
128 }
129
130 /// Feeds the watchdog, resetting the silence timer.
131 pub fn feed(&mut self) {
132 self.elapsed = 0.0;
133 }
134
135 /// Advances the timer by `dt` and returns whether it has expired.
136 ///
137 /// # Arguments
138 ///
139 /// * `dt` - the time since the previous update; its magnitude is used.
140 ///
141 /// # Returns
142 ///
143 /// `true` if the watchdog is now expired.
144 pub fn update(&mut self, dt: f32) -> bool {
145 self.elapsed += magnitude(dt);
146 self.is_expired()
147 }
148
149 /// Returns whether the watchdog is currently expired.
150 ///
151 /// # Returns
152 ///
153 /// `true` if the time since feeding exceeds the timeout.
154 pub fn is_expired(&self) -> bool {
155 self.elapsed > self.timeout
156 }
157}
158
159/// Bounds a robot's speed and acceleration so commands stay within what the machine can do safely.
160///
161/// A raw command can be too fast or too sudden: a full-speed setpoint snaps the wheels, a hard
162/// reverse strips traction. [`Limits`] caps the planar speed and yaw rate, then eases each toward
163/// the capped target at a bounded acceleration, so motion is smooth and within envelope. The
164/// easing reuses [`Ramp`] as its slew-rate limiter, with the step set to `accel * dt` each update.
165///
166/// # Examples
167///
168/// ```
169/// use pamoja_kit::{Limits, Twist};
170///
171/// // Up to 1 m/s and 2 rad/s, easing on at 0.5 m/s^2 and 4 rad/s^2.
172/// let mut limits = Limits::new(1.0, 2.0, 0.5, 4.0);
173///
174/// // Asking for full speed from rest: the first 0.1 s step is acceleration-limited.
175/// let cmd = limits.apply(Twist::planar(1.0, 0.0), 0.1);
176/// assert!((cmd.vx - 0.05).abs() < 1e-6); // 0.5 m/s^2 * 0.1 s
177/// ```
178#[derive(Clone, Copy, Debug)]
179pub struct Limits {
180 max_linear: f32,
181 max_angular: f32,
182 max_linear_accel: f32,
183 max_angular_accel: f32,
184 vx: Ramp,
185 vy: Ramp,
186 omega: Ramp,
187}
188
189impl Limits {
190 /// Creates limits from the speed and acceleration ceilings.
191 ///
192 /// # Arguments
193 ///
194 /// * `max_linear` - the largest planar speed; its magnitude is used.
195 /// * `max_angular` - the largest yaw rate; its magnitude is used.
196 /// * `max_linear_accel` - the largest change in linear speed per second; its magnitude is used.
197 /// * `max_angular_accel` - the largest change in yaw rate per second; its magnitude is used.
198 ///
199 /// # Returns
200 ///
201 /// Limits starting from rest.
202 pub fn new(
203 max_linear: f32,
204 max_angular: f32,
205 max_linear_accel: f32,
206 max_angular_accel: f32,
207 ) -> Self {
208 Self {
209 max_linear: magnitude(max_linear),
210 max_angular: magnitude(max_angular),
211 max_linear_accel: magnitude(max_linear_accel),
212 max_angular_accel: magnitude(max_angular_accel),
213 vx: Ramp::new(0.0, 0.0),
214 vy: Ramp::new(0.0, 0.0),
215 omega: Ramp::new(0.0, 0.0),
216 }
217 }
218
219 /// Resets the remembered motion to rest, so the next command eases up from zero.
220 pub fn reset(&mut self) {
221 self.vx.set(0.0);
222 self.vy.set(0.0);
223 self.omega.set(0.0);
224 }
225
226 /// Bounds a desired command in speed and acceleration and returns the safe command.
227 ///
228 /// # Arguments
229 ///
230 /// * `desired` - the requested body motion.
231 /// * `dt` - the time since the previous call, setting the acceleration step.
232 ///
233 /// # Returns
234 ///
235 /// The command after capping speed and easing toward it within the acceleration limit.
236 pub fn apply(&mut self, desired: Twist, dt: f32) -> Twist {
237 let bounded = self.clamp_speed(desired);
238 let linear_step = self.max_linear_accel * magnitude(dt);
239 let angular_step = self.max_angular_accel * magnitude(dt);
240 Twist::new(
241 self.vx.update_capped(bounded.vx, linear_step),
242 self.vy.update_capped(bounded.vy, linear_step),
243 self.omega.update_capped(bounded.omega, angular_step),
244 )
245 }
246
247 // Caps the planar speed (scaling vx and vy together so direction is kept) and the yaw rate.
248 fn clamp_speed(&self, twist: Twist) -> Twist {
249 let speed = sqrtf(twist.vx * twist.vx + twist.vy * twist.vy);
250 let (vx, vy) = if speed > self.max_linear {
251 let scale = self.max_linear / speed;
252 (twist.vx * scale, twist.vy * scale)
253 } else {
254 (twist.vx, twist.vy)
255 };
256 Twist::new(
257 vx,
258 vy,
259 clamp(twist.omega, -self.max_angular, self.max_angular),
260 )
261 }
262}
263
264/// The single gate every motion command passes through, composing e-stop, watchdog, and limits.
265///
266/// This is the one call a control loop makes to drive safely: feed it the desired motion and the
267/// time step, and it returns what is actually safe to command. It stops hard (commands zero and
268/// forgets its motion history, so resuming eases from rest) whenever the e-stop is engaged or the
269/// watchdog has expired; otherwise it returns the desired motion bounded by the [`Limits`]. Call
270/// [`feed`](SafetyGate::feed) whenever a fresh command arrives to keep the watchdog satisfied.
271///
272/// # Examples
273///
274/// ```
275/// use pamoja_kit::{Limits, SafetyGate, Twist};
276///
277/// let limits = Limits::new(1.0, 2.0, 0.5, 4.0);
278/// let mut gate = SafetyGate::new(limits, 0.2); // stop if unfed for 0.2 s
279///
280/// gate.feed();
281/// let cmd = gate.command(Twist::planar(1.0, 0.0), 0.1);
282/// assert!((cmd.vx - 0.05).abs() < 1e-6); // eased on, acceleration-limited
283///
284/// gate.engage_estop();
285/// assert_eq!(gate.command(Twist::planar(1.0, 0.0), 0.1), Twist::zero());
286/// ```
287#[derive(Clone, Copy, Debug)]
288pub struct SafetyGate {
289 estop: EStop,
290 watchdog: Watchdog,
291 limits: Limits,
292}
293
294impl SafetyGate {
295 /// Creates a gate from motion limits and a watchdog timeout.
296 ///
297 /// # Arguments
298 ///
299 /// * `limits` - the speed and acceleration bounds for normal motion.
300 /// * `watchdog_timeout` - the allowed silence before the gate stops the robot.
301 ///
302 /// # Returns
303 ///
304 /// The gate, cleared and freshly fed.
305 pub fn new(limits: Limits, watchdog_timeout: f32) -> Self {
306 Self {
307 estop: EStop::new(),
308 watchdog: Watchdog::new(watchdog_timeout),
309 limits,
310 }
311 }
312
313 /// Feeds the watchdog; call this whenever a fresh command arrives.
314 pub fn feed(&mut self) {
315 self.watchdog.feed();
316 }
317
318 /// Engages the latching emergency stop.
319 pub fn engage_estop(&mut self) {
320 self.estop.engage();
321 }
322
323 /// Clears the emergency stop.
324 pub fn reset_estop(&mut self) {
325 self.estop.reset();
326 }
327
328 /// Returns whether the gate is currently forcing a stop.
329 ///
330 /// # Returns
331 ///
332 /// `true` if the e-stop is engaged or the watchdog has expired.
333 pub fn is_stopped(&self) -> bool {
334 self.estop.is_engaged() || self.watchdog.is_expired()
335 }
336
337 /// Returns the safe command for a desired motion over a time step.
338 ///
339 /// # Arguments
340 ///
341 /// * `desired` - the requested body motion.
342 /// * `dt` - the time since the previous call.
343 ///
344 /// # Returns
345 ///
346 /// [`Twist::zero`] when stopped, otherwise the desired motion bounded by the limits.
347 pub fn command(&mut self, desired: Twist, dt: f32) -> Twist {
348 self.watchdog.update(dt);
349 if self.is_stopped() {
350 self.limits.reset();
351 return Twist::zero();
352 }
353 self.limits.apply(desired, dt)
354 }
355}
356
357#[cfg(test)]
358mod tests {
359 use super::*;
360
361 #[test]
362 fn estop_latches_until_reset() {
363 let mut estop = EStop::new();
364 let cmd = Twist::planar(1.0, 0.5);
365 assert_eq!(estop.gate(cmd), cmd);
366 estop.engage();
367 assert!(estop.is_engaged());
368 assert_eq!(estop.gate(cmd), Twist::zero());
369 estop.reset();
370 assert_eq!(estop.gate(cmd), cmd);
371 }
372
373 #[test]
374 fn watchdog_expires_on_silence_and_revives_on_feeding() {
375 let mut dog = Watchdog::new(0.5);
376 dog.feed();
377 assert!(!dog.update(0.3));
378 assert!(dog.update(0.3)); // 0.6 > 0.5
379 dog.feed();
380 assert!(!dog.is_expired());
381 }
382
383 #[test]
384 fn limits_cap_planar_speed_by_scaling() {
385 let mut limits = Limits::new(1.0, 10.0, 100.0, 100.0); // accel high so slew is not the cap
386 // A 3-4-5 triangle at speed 5 scales down to length 1, keeping direction.
387 let cmd = limits.apply(Twist::new(3.0, 4.0, 0.0), 1.0);
388 assert!((cmd.vx - 0.6).abs() < 1e-5);
389 assert!((cmd.vy - 0.8).abs() < 1e-5);
390 }
391
392 #[test]
393 fn limits_ease_in_at_the_acceleration_bound() {
394 let mut limits = Limits::new(1.0, 2.0, 0.5, 4.0);
395 assert!((limits.apply(Twist::planar(1.0, 0.0), 0.1).vx - 0.05).abs() < 1e-6);
396 assert!((limits.apply(Twist::planar(1.0, 0.0), 0.1).vx - 0.10).abs() < 1e-6);
397 }
398
399 #[test]
400 fn gate_stops_on_estop_and_on_watchdog_expiry() {
401 let mut gate = SafetyGate::new(Limits::new(1.0, 2.0, 100.0, 100.0), 0.2);
402
403 gate.feed();
404 assert!(gate.command(Twist::planar(1.0, 0.0), 0.1).vx > 0.0);
405
406 // Stop feeding: after enough time the watchdog trips and the gate zeroes the command.
407 assert_eq!(gate.command(Twist::planar(1.0, 0.0), 0.5), Twist::zero());
408 assert!(gate.is_stopped());
409
410 // Feeding revives it.
411 gate.feed();
412 assert!(gate.command(Twist::planar(1.0, 0.0), 0.1).vx > 0.0);
413
414 // The e-stop overrides even a fed watchdog.
415 gate.feed();
416 gate.engage_estop();
417 assert_eq!(gate.command(Twist::planar(1.0, 0.0), 0.1), Twist::zero());
418 }
419}