use crate::motion::{clamp, magnitude, wrap_pi, Pose};
use crate::DiffDrive;
use libm::{cosf, sinf};
#[derive(Clone, Copy, Debug)]
pub struct Odometry {
pose: Pose,
}
impl Odometry {
pub fn new(start: Pose) -> Self {
Self { pose: start }
}
pub fn at_origin() -> Self {
Self {
pose: Pose::origin(),
}
}
pub fn pose(&self) -> Pose {
self.pose
}
pub fn reset(&mut self, pose: Pose) {
self.pose = pose;
}
pub fn integrate(&mut self, linear: f32, angular: f32, dt: f32) -> Pose {
self.advance(linear * dt, angular * dt);
self.pose
}
pub fn integrate_wheels(&mut self, left: f32, right: f32, drive: &DiffDrive) -> Pose {
let (distance, heading_change) = drive.body_motion(left, right);
self.advance(distance, heading_change);
self.pose
}
pub fn fuse_heading(&mut self, measured: f32, weight: f32) {
let w = clamp(weight, 0.0, 1.0);
let error = wrap_pi(measured - self.pose.theta);
self.pose.theta = wrap_pi(self.pose.theta + w * error);
}
fn advance(&mut self, distance: f32, heading_change: f32) {
let theta = self.pose.theta;
if magnitude(heading_change) < 1e-6 {
self.pose.x += distance * cosf(theta);
self.pose.y += distance * sinf(theta);
self.pose.theta = wrap_pi(theta + heading_change);
} else {
let radius = distance / heading_change;
let new_theta = theta + heading_change;
self.pose.x += radius * (sinf(new_theta) - sinf(theta));
self.pose.y += radius * (cosf(theta) - cosf(new_theta));
self.pose.theta = wrap_pi(new_theta);
}
}
}
#[cfg(test)]
mod tests {
use super::*;
use core::f32::consts::{FRAC_PI_2, PI};
#[test]
fn driving_straight_moves_along_the_heading() {
let mut odom = Odometry::new(Pose::new(0.0, 0.0, FRAC_PI_2)); let pose = odom.integrate(2.0, 0.0, 1.0);
assert!(pose.x.abs() < 1e-5);
assert!((pose.y - 2.0).abs() < 1e-5);
assert!((pose.theta - FRAC_PI_2).abs() < 1e-6);
}
#[test]
fn a_quarter_circle_lands_at_the_arc_corner() {
let mut odom = Odometry::at_origin();
let pose = odom.integrate(1.0, 1.0, FRAC_PI_2);
assert!((pose.x - 1.0).abs() < 1e-5);
assert!((pose.y - 1.0).abs() < 1e-5);
assert!((pose.theta - FRAC_PI_2).abs() < 1e-5);
}
#[test]
fn wheel_deltas_match_a_spin_in_place() {
let drive = DiffDrive::new(0.5);
let mut odom = Odometry::at_origin();
let pose = odom.integrate_wheels(-0.25, 0.25, &drive);
assert!(pose.x.abs() < 1e-6 && pose.y.abs() < 1e-6);
assert!((pose.theta - 1.0).abs() < 1e-6); }
#[test]
fn fuse_heading_blends_along_the_shortest_arc() {
let mut odom = Odometry::new(Pose::new(0.0, 0.0, 0.1));
odom.fuse_heading(0.5, 0.5); assert!((odom.pose().theta - 0.3).abs() < 1e-6);
}
#[test]
fn fuse_heading_takes_the_short_way_across_pi() {
let mut odom = Odometry::new(Pose::new(0.0, 0.0, 3.0));
odom.fuse_heading(-3.0, 1.0);
assert!((odom.pose().theta - -3.0).abs() < 1e-6);
assert!(odom.pose().theta.abs() <= PI);
}
}