use crate::core::scalar::ControlScalar;
#[derive(Debug, Clone, Copy, PartialEq)]
pub enum DiffDriveError {
InvalidParameter,
ZeroWheelbase,
}
impl core::fmt::Display for DiffDriveError {
fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
match self {
DiffDriveError::InvalidParameter => {
write!(f, "invalid differential drive parameter")
}
DiffDriveError::ZeroWheelbase => write!(f, "wheelbase must be positive"),
}
}
}
#[derive(Debug, Clone, Copy)]
pub struct DifferentialDrive<S: ControlScalar> {
pub state: [S; 3],
pub wheelbase: S,
pub wheel_radius: S,
pub dt: S,
encoder_left: S,
encoder_right: S,
}
impl<S: ControlScalar> DifferentialDrive<S> {
pub fn new(wheelbase: S, wheel_radius: S, dt: S) -> Result<Self, DiffDriveError> {
if wheelbase <= S::ZERO {
return Err(DiffDriveError::ZeroWheelbase);
}
if wheel_radius <= S::ZERO {
return Err(DiffDriveError::InvalidParameter);
}
if dt <= S::ZERO {
return Err(DiffDriveError::InvalidParameter);
}
Ok(Self {
state: [S::ZERO; 3],
wheelbase,
wheel_radius,
dt,
encoder_left: S::ZERO,
encoder_right: S::ZERO,
})
}
pub fn step(&mut self, v_l: S, v_r: S) -> Result<[S; 3], DiffDriveError> {
let dt = self.dt;
let v = (v_r + v_l) * S::HALF;
let omega = (v_r - v_l) / self.wheelbase;
let theta = self.state[2];
self.state[0] += dt * v * theta.cos();
self.state[1] += dt * v * theta.sin();
self.state[2] += dt * omega;
self.encoder_left += v_l * dt / self.wheel_radius;
self.encoder_right += v_r * dt / self.wheel_radius;
Ok(self.state)
}
pub fn step_angular(&mut self, v: S, omega: S) -> Result<[S; 3], DiffDriveError> {
let half_b = self.wheelbase * S::HALF;
let v_r = v + omega * half_b;
let v_l = v - omega * half_b;
self.step(v_l, v_r)
}
pub fn odometry(&self) -> [S; 3] {
self.state
}
pub fn reset(&mut self) {
self.state = [S::ZERO; 3];
self.encoder_left = S::ZERO;
self.encoder_right = S::ZERO;
}
pub fn encoder_readings(&self) -> [S; 2] {
[self.encoder_left, self.encoder_right]
}
}
#[cfg(test)]
mod tests {
use super::*;
fn make_robot() -> DifferentialDrive<f64> {
DifferentialDrive::<f64>::new(0.3, 0.05, 0.01).expect("valid params")
}
#[test]
fn straight_line() {
let mut robot = make_robot();
for _ in 0..100 {
robot.step(1.0, 1.0).expect("step ok");
}
let s = robot.state;
assert!(s[0] > 0.9, "x should increase: {}", s[0]);
assert!(s[1].abs() < 1e-10, "y should stay 0: {}", s[1]);
assert!(s[2].abs() < 1e-10, "theta should stay 0: {}", s[2]);
}
#[test]
fn pure_rotation() {
let mut robot = make_robot();
let v = 0.3_f64;
let omega = 2.0 * v / 0.3;
let steps = 100_usize;
let dt = 0.01_f64;
for _ in 0..steps {
robot.step(-v, v).expect("step ok");
}
let s = robot.state;
let dist = (s[0] * s[0] + s[1] * s[1]).sqrt();
assert!(
dist < 1e-9,
"robot should stay at origin during pure rotation: dist={}",
dist
);
let expected_theta = omega * (steps as f64) * dt;
assert!(
(s[2] - expected_theta).abs() < 1e-10,
"heading should match omega*t: expected={:.6}, got={:.6}",
expected_theta,
s[2]
);
}
#[test]
fn circle_arc() {
let wheelbase = 0.3_f64;
let mut robot =
DifferentialDrive::<f64>::new(wheelbase, 0.05, 0.001).expect("valid params");
let v_l = 0.8_f64;
let v_r = 1.2_f64;
let r_expected = wheelbase * (v_r + v_l) / (2.0 * (v_r - v_l));
let v_avg = (v_r + v_l) / 2.0;
let period = 2.0 * core::f64::consts::PI * r_expected / v_avg;
let steps = (period / 0.001).ceil() as usize;
for _ in 0..steps {
robot.step(v_l, v_r).expect("step ok");
}
let s = robot.state;
let dist = (s[0] * s[0] + s[1] * s[1]).sqrt();
assert!(
dist < r_expected * 0.05,
"after full arc dist={:.4} should be < {:.4}",
dist,
r_expected * 0.05
);
}
#[test]
fn invalid_params() {
assert!(DifferentialDrive::<f64>::new(0.0, 0.05, 0.01).is_err());
assert!(DifferentialDrive::<f64>::new(-0.3, 0.05, 0.01).is_err());
assert!(DifferentialDrive::<f64>::new(0.3, 0.0, 0.01).is_err());
assert!(DifferentialDrive::<f64>::new(0.3, 0.05, 0.0).is_err());
}
#[test]
fn step_angular_matches_step() {
let b = 0.3_f64;
let mut r1 = DifferentialDrive::<f64>::new(b, 0.05, 0.01).expect("ok");
let mut r2 = DifferentialDrive::<f64>::new(b, 0.05, 0.01).expect("ok");
let v = 1.0_f64;
let omega = 0.5_f64;
let v_r = v + omega * b / 2.0;
let v_l = v - omega * b / 2.0;
r1.step(v_l, v_r).expect("ok");
r2.step_angular(v, omega).expect("ok");
for i in 0..3 {
assert!(
(r1.state[i] - r2.state[i]).abs() < 1e-12,
"state[{}] mismatch: {} vs {}",
i,
r1.state[i],
r2.state[i]
);
}
}
#[test]
fn reset_clears_state_and_encoders() {
let mut robot = make_robot();
for _ in 0..100 {
robot.step(1.0, 0.8).expect("ok");
}
robot.reset();
let s = robot.state;
assert_eq!(s[0], 0.0);
assert_eq!(s[1], 0.0);
assert_eq!(s[2], 0.0);
let enc = robot.encoder_readings();
assert_eq!(enc[0], 0.0);
assert_eq!(enc[1], 0.0);
}
#[test]
fn encoders_accumulate_correctly() {
let mut robot = make_robot();
let v = 1.0_f64;
let n = 50_usize;
for _ in 0..n {
robot.step(v, v).expect("ok");
}
let enc = robot.encoder_readings();
let expected = v * (n as f64) * 0.01 / 0.05; assert!(
(enc[0] - expected).abs() < 1e-10,
"left encoder expected {:.4}, got {:.4}",
expected,
enc[0]
);
assert!(
(enc[1] - expected).abs() < 1e-10,
"right encoder expected {:.4}, got {:.4}",
expected,
enc[1]
);
}
}