use crate::motion::{clamp, magnitude, Twist};
use crate::Coordinate;
use core::f32::consts::PI;
use libm::cosf;
fn wrap_deg_180(angle: f32) -> f32 {
let mut a = angle % 360.0;
if a > 180.0 {
a -= 360.0;
} else if a <= -180.0 {
a += 360.0;
}
a
}
fn magnitude_f64(value: f64) -> f64 {
if value < 0.0 {
-value
} else {
value
}
}
#[derive(Clone, Copy, Debug, PartialEq)]
pub struct Guidance {
pub twist: Twist,
pub distance_m: f64,
pub heading_error_deg: f32,
pub arrived: bool,
}
#[derive(Clone, Copy, Debug)]
pub struct WaypointFollower {
cruise: f32,
arrival_m: f64,
heading_gain: f32,
max_angular: f32,
}
impl WaypointFollower {
pub fn new(cruise: f32, arrival_m: f64, heading_gain: f32, max_angular: f32) -> Self {
Self {
cruise: magnitude(cruise),
arrival_m: magnitude_f64(arrival_m),
heading_gain: magnitude(heading_gain),
max_angular: magnitude(max_angular),
}
}
pub fn guide(&self, here: Coordinate, heading_deg: f32, target: Coordinate) -> Guidance {
let distance_m = here.distance_to(target);
let bearing_deg = here.bearing_to(target) as f32;
let heading_error_deg = wrap_deg_180(bearing_deg - heading_deg);
if distance_m <= self.arrival_m {
return Guidance {
twist: Twist::zero(),
distance_m,
heading_error_deg,
arrived: true,
};
}
let error_rad = heading_error_deg * (PI / 180.0);
let angular = clamp(
self.heading_gain * error_rad,
-self.max_angular,
self.max_angular,
);
let facing = cosf(error_rad);
let forward = if facing > 0.0 {
self.cruise * facing
} else {
0.0
};
Guidance {
twist: Twist::planar(forward, angular),
distance_m,
heading_error_deg,
arrived: false,
}
}
}
pub fn obstacle_stop(twist: Twist, range_m: f32, stop_distance_m: f32) -> Twist {
if range_m <= magnitude(stop_distance_m) {
Twist::new(0.0, 0.0, twist.omega)
} else {
twist
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn it_drives_at_cruise_when_pointed_at_the_target() {
let follower = WaypointFollower::new(1.5, 3.0, 1.5, 1.0);
let here = Coordinate::new(0.0, 0.0);
let target = Coordinate::new(0.0, 0.01); let g = follower.guide(here, 90.0, target); assert!(g.heading_error_deg.abs() < 1e-3);
assert!((g.twist.vx - 1.5).abs() < 1e-3);
assert!(g.twist.omega.abs() < 1e-3);
assert!(!g.arrived);
}
#[test]
fn it_pivots_without_driving_when_the_target_is_behind() {
let follower = WaypointFollower::new(1.5, 3.0, 1.5, 1.0);
let here = Coordinate::new(0.0, 0.0);
let target = Coordinate::new(0.0, 0.01); let g = follower.guide(here, 270.0, target); assert!(g.twist.vx.abs() < 1e-6); assert!(g.twist.omega.abs() > 0.0); }
#[test]
fn it_reports_arrival_inside_the_radius() {
let follower = WaypointFollower::new(1.5, 50.0, 1.5, 1.0);
let here = Coordinate::new(0.0, 0.0);
let target = Coordinate::new(0.0, 0.0001); let g = follower.guide(here, 90.0, target);
assert!(g.arrived);
assert_eq!(g.twist, Twist::zero());
}
#[test]
fn the_angular_command_is_capped() {
let follower = WaypointFollower::new(1.0, 1.0, 10.0, 0.5); let here = Coordinate::new(0.0, 0.0);
let target = Coordinate::new(0.0001, 0.0); let g = follower.guide(here, 90.0, target); assert!((g.twist.omega.abs() - 0.5).abs() < 1e-6); }
}