pamoja_kit/drive.rs
1//! Differential-drive wheel kinematics.
2
3/// Converts between a robot's motion and its two wheel speeds (differential drive).
4///
5/// A differential-drive robot steers by spinning its left and right wheels at different
6/// speeds. This converts both ways: [`wheel_speeds`](DiffDrive::wheel_speeds) turns a desired
7/// forward speed and turn rate into the wheel speeds to command (inverse kinematics), and
8/// [`body_motion`](DiffDrive::body_motion) turns measured wheel speeds back into the robot's
9/// forward speed and turn rate (forward kinematics). The one parameter is the track: the
10/// distance between the wheels.
11///
12/// # Examples
13///
14/// ```
15/// use pamoja_kit::DiffDrive;
16///
17/// let drive = DiffDrive::new(0.5); // wheels 0.5 apart
18/// // Drive straight: both wheels turn at the forward speed.
19/// assert_eq!(drive.wheel_speeds(1.0, 0.0), (1.0, 1.0));
20/// // Spin in place: the wheels turn opposite, each at turn rate times half the track.
21/// assert_eq!(drive.wheel_speeds(0.0, 2.0), (-0.5, 0.5));
22/// ```
23#[derive(Clone, Copy, Debug)]
24pub struct DiffDrive {
25 track: f32,
26}
27
28impl DiffDrive {
29 /// Creates a model for wheels `track` apart.
30 ///
31 /// # Arguments
32 ///
33 /// * `track` - the distance between the left and right wheels; its magnitude is used.
34 ///
35 /// # Returns
36 ///
37 /// The kinematics model.
38 pub fn new(track: f32) -> Self {
39 Self {
40 track: magnitude(track),
41 }
42 }
43
44 /// Returns the `(left, right)` wheel speeds for a desired body motion.
45 ///
46 /// # Arguments
47 ///
48 /// * `linear` - the forward speed.
49 /// * `angular` - the turn rate, positive turning toward the left (counter-clockwise).
50 ///
51 /// # Returns
52 ///
53 /// `(left, right)`, where `left = linear - angular * track / 2` and
54 /// `right = linear + angular * track / 2`.
55 pub fn wheel_speeds(&self, linear: f32, angular: f32) -> (f32, f32) {
56 let half = angular * self.track / 2.0;
57 (linear - half, linear + half)
58 }
59
60 /// Returns the body `(linear, angular)` motion for measured wheel speeds.
61 ///
62 /// # Arguments
63 ///
64 /// * `left` - the left wheel speed.
65 /// * `right` - the right wheel speed.
66 ///
67 /// # Returns
68 ///
69 /// `(linear, angular)`, where `linear = (right + left) / 2` and
70 /// `angular = (right - left) / track`. Angular is zero when the track is zero.
71 pub fn body_motion(&self, left: f32, right: f32) -> (f32, f32) {
72 let linear = (right + left) / 2.0;
73 let angular = if self.track == 0.0 {
74 0.0
75 } else {
76 (right - left) / self.track
77 };
78 (linear, angular)
79 }
80}
81
82// `f32::abs` lives in `std`, so this `no_std` crate takes the magnitude by hand.
83fn magnitude(value: f32) -> f32 {
84 if value < 0.0 {
85 -value
86 } else {
87 value
88 }
89}
90
91#[cfg(test)]
92mod tests {
93 use super::*;
94
95 #[test]
96 fn driving_straight_turns_both_wheels_equally() {
97 let drive = DiffDrive::new(0.5);
98 assert_eq!(drive.wheel_speeds(1.0, 0.0), (1.0, 1.0));
99 }
100
101 #[test]
102 fn spinning_in_place_turns_the_wheels_opposite() {
103 let drive = DiffDrive::new(0.5);
104 assert_eq!(drive.wheel_speeds(0.0, 2.0), (-0.5, 0.5));
105 }
106
107 #[test]
108 fn body_motion_inverts_wheel_speeds() {
109 let drive = DiffDrive::new(0.5);
110 assert_eq!(drive.body_motion(1.0, 1.0), (1.0, 0.0)); // straight
111 assert_eq!(drive.body_motion(-0.5, 0.5), (0.0, 2.0)); // spinning
112 }
113
114 #[test]
115 fn the_two_directions_round_trip() {
116 let drive = DiffDrive::new(0.42);
117 let (left, right) = drive.wheel_speeds(1.3, -0.7);
118 let (linear, angular) = drive.body_motion(left, right);
119 assert!((linear - 1.3).abs() < 1e-6);
120 assert!((angular + 0.7).abs() < 1e-6);
121 }
122
123 #[test]
124 fn a_zero_track_reports_no_rotation() {
125 let drive = DiffDrive::new(0.0);
126 assert_eq!(drive.body_motion(1.0, 2.0), (1.5, 0.0));
127 }
128}