Skip to main content

pamoja_kit/
drivers.rs

1//! The small, exact conversions between robot actuators and sensors and real units.
2//!
3//! Driving a robot means turning intent into the pulse widths a servo or motor controller expects,
4//! and turning encoder edges back into how far a wheel has rolled. Each conversion is pure
5//! arithmetic with a classic off-by-one or sign trap, so it lives here as checked logic rather
6//! than scattered inline math: a [`ServoMap`] and [`Esc`] for hobby PWM outputs, and a
7//! [`Quadrature`] decoder with a [`QuadratureScale`] for incremental encoders. The PCA9685 driver
8//! in `pamoja-actuators` clocks the pulses, and a GPIO line from `pamoja-gpio` reads an encoder's
9//! channels; this is the math on either side of them.
10
11use crate::motion::{clamp, magnitude};
12use core::f32::consts::PI;
13
14/// Maps a servo angle to its RC pulse width in microseconds, and back.
15///
16/// A hobby servo is positioned by the width of a pulse repeated about every 20 ms: the standard
17/// range is 1000 to 2000 microseconds spanning the full travel, with 1500 at center.
18/// [`ServoMap::standard`] uses those defaults over 180 degrees; [`ServoMap::new`] covers servos
19/// with a different range or travel.
20///
21/// # Examples
22///
23/// ```
24/// use pamoja_kit::ServoMap;
25///
26/// let servo = ServoMap::standard();
27/// assert_eq!(servo.pulse(0.0), 1000);
28/// assert_eq!(servo.pulse(90.0), 1500); // center
29/// assert_eq!(servo.pulse(180.0), 2000);
30/// assert!((servo.angle(1500) - 90.0).abs() < 1e-3);
31/// ```
32#[derive(Clone, Copy, Debug)]
33pub struct ServoMap {
34    min_us: u16,
35    max_us: u16,
36    range_deg: f32,
37}
38
39impl ServoMap {
40    /// Returns the standard hobby-servo map: 1000 to 2000 microseconds over 180 degrees.
41    ///
42    /// # Returns
43    ///
44    /// The standard map.
45    pub fn standard() -> Self {
46        Self {
47            min_us: 1000,
48            max_us: 2000,
49            range_deg: 180.0,
50        }
51    }
52
53    /// Creates a map from explicit pulse and travel limits.
54    ///
55    /// # Arguments
56    ///
57    /// * `min_us` - the pulse width at zero degrees.
58    /// * `max_us` - the pulse width at full travel.
59    /// * `range_deg` - the full travel in degrees; its magnitude is used.
60    ///
61    /// # Returns
62    ///
63    /// The map.
64    pub fn new(min_us: u16, max_us: u16, range_deg: f32) -> Self {
65        Self {
66            min_us,
67            max_us,
68            range_deg: magnitude(range_deg),
69        }
70    }
71
72    /// Returns the pulse width at zero degrees, in microseconds.
73    pub fn min_us(&self) -> u16 {
74        self.min_us
75    }
76
77    /// Returns the pulse width at full travel, in microseconds.
78    pub fn max_us(&self) -> u16 {
79        self.max_us
80    }
81
82    /// Returns the full travel, in degrees.
83    pub fn range_deg(&self) -> f32 {
84        self.range_deg
85    }
86
87    /// Returns the pulse width for an angle.
88    ///
89    /// # Arguments
90    ///
91    /// * `angle_deg` - the desired angle in degrees, clamped to `[0, range]`.
92    ///
93    /// # Returns
94    ///
95    /// The pulse width in microseconds, or 0, no pulse at all, for an angle that is not a
96    /// number, so a driver that writes it stops commanding the servo rather than moving it.
97    pub fn pulse(&self, angle_deg: f32) -> u16 {
98        if angle_deg.is_nan() {
99            return 0;
100        }
101        let span_us = self.max_us as f32 - self.min_us as f32;
102        let fraction = if self.range_deg == 0.0 {
103            0.0
104        } else {
105            clamp(angle_deg, 0.0, self.range_deg) / self.range_deg
106        };
107        (self.min_us as f32 + fraction * span_us + 0.5) as u16
108    }
109
110    /// Returns the angle for a pulse width.
111    ///
112    /// # Arguments
113    ///
114    /// * `pulse_us` - the pulse width in microseconds, clamped to the configured range.
115    ///
116    /// # Returns
117    ///
118    /// The angle in degrees, zero when the pulse range is empty.
119    pub fn angle(&self, pulse_us: u16) -> f32 {
120        let span_us = self.max_us as f32 - self.min_us as f32;
121        if span_us == 0.0 {
122            return 0.0;
123        }
124        let (low, high) = if self.min_us <= self.max_us {
125            (self.min_us, self.max_us)
126        } else {
127            (self.max_us, self.min_us)
128        };
129        let p = clamp(pulse_us as f32, low as f32, high as f32);
130        (p - self.min_us as f32) / span_us * self.range_deg
131    }
132}
133
134/// Maps a normalized throttle to an electronic speed controller's RC pulse width.
135///
136/// An ESC reads the same RC pulse a servo does, with the pulse width setting motor output.
137/// [`Esc::bidirectional`] uses the common reversible scheme: 1000 microseconds full reverse, 1500
138/// neutral, 2000 full forward, so a throttle in `[-1, 1]` maps linearly across it.
139///
140/// # Examples
141///
142/// ```
143/// use pamoja_kit::Esc;
144///
145/// let esc = Esc::bidirectional();
146/// assert_eq!(esc.pulse(0.0), 1500); // neutral
147/// assert_eq!(esc.pulse(1.0), 2000); // full forward
148/// assert_eq!(esc.pulse(-1.0), 1000); // full reverse
149/// assert_eq!(esc.pulse(0.5), 1750);
150/// ```
151#[derive(Clone, Copy, Debug)]
152pub struct Esc {
153    min_us: u16,
154    neutral_us: u16,
155    max_us: u16,
156}
157
158impl Esc {
159    /// Returns the standard reversible ESC map: 1000 / 1500 / 2000 microseconds.
160    ///
161    /// # Returns
162    ///
163    /// The bidirectional map.
164    pub fn bidirectional() -> Self {
165        Self {
166            min_us: 1000,
167            neutral_us: 1500,
168            max_us: 2000,
169        }
170    }
171
172    /// Creates a map from explicit reverse, neutral, and forward pulse widths.
173    ///
174    /// # Arguments
175    ///
176    /// * `min_us` - the pulse width at full reverse.
177    /// * `neutral_us` - the pulse width at rest.
178    /// * `max_us` - the pulse width at full forward.
179    ///
180    /// # Returns
181    ///
182    /// The map.
183    pub fn new(min_us: u16, neutral_us: u16, max_us: u16) -> Self {
184        Self {
185            min_us,
186            neutral_us,
187            max_us,
188        }
189    }
190
191    /// Returns the pulse width at full reverse, in microseconds.
192    pub fn min_us(&self) -> u16 {
193        self.min_us
194    }
195
196    /// Returns the pulse width at rest, in microseconds.
197    pub fn neutral_us(&self) -> u16 {
198        self.neutral_us
199    }
200
201    /// Returns the pulse width at full forward, in microseconds.
202    pub fn max_us(&self) -> u16 {
203        self.max_us
204    }
205
206    /// Returns the pulse width for a throttle.
207    ///
208    /// # Arguments
209    ///
210    /// * `throttle` - the demand in `[-1, 1]`, clamped; negative reverses, positive drives forward.
211    ///
212    /// # Returns
213    ///
214    /// The pulse width in microseconds, or the neutral one, which stops the motor, for a
215    /// throttle that is not a number.
216    pub fn pulse(&self, throttle: f32) -> u16 {
217        let t = if throttle.is_nan() {
218            0.0
219        } else {
220            clamp(throttle, -1.0, 1.0)
221        };
222        let span = if t >= 0.0 {
223            self.max_us as f32 - self.neutral_us as f32
224        } else {
225            self.neutral_us as f32 - self.min_us as f32
226        };
227        (self.neutral_us as f32 + t * span + 0.5) as u16
228    }
229}
230
231// The quadrature transition table, indexed by `(previous << 2) | next` where each two-bit state
232// is `(A << 1) | B`. Valid Gray-code steps give +1 or -1; no change or an illegal jump gives 0.
233const QUADRATURE_TABLE: [i8; 16] = [0, 1, -1, 0, -1, 0, 0, 1, 1, 0, 0, -1, 0, -1, 1, 0];
234
235fn encode(a: bool, b: bool) -> u8 {
236    ((a as u8) << 1) | (b as u8)
237}
238
239/// Decodes a quadrature (A/B) encoder into a running tick count.
240///
241/// An incremental encoder reports motion as two square waves a quarter-cycle apart; their order of
242/// change tells direction. Feeding successive A/B readings to [`update`](Quadrature::update) returns
243/// the per-step direction and accumulates a signed count, the foundation for wheel odometry. Pair
244/// it with a [`QuadratureScale`] to turn that count into meters.
245///
246/// # Examples
247///
248/// ```
249/// use pamoja_kit::Quadrature;
250///
251/// let mut enc = Quadrature::new();
252/// // One full cycle forward: 00 -> 01 -> 11 -> 10 -> 00, one tick each.
253/// for &(a, b) in &[(false, true), (true, true), (true, false), (false, false)] {
254///     assert_eq!(enc.update(a, b), 1);
255/// }
256/// assert_eq!(enc.count(), 4);
257/// ```
258#[derive(Clone, Copy, Debug, Default)]
259pub struct Quadrature {
260    state: u8,
261    count: i64,
262}
263
264impl Quadrature {
265    /// Creates a decoder assuming both channels start low.
266    ///
267    /// # Returns
268    ///
269    /// A decoder with a zero count.
270    pub fn new() -> Self {
271        Self { state: 0, count: 0 }
272    }
273
274    /// Creates a decoder seeded with the encoder's current channel levels.
275    ///
276    /// Seeding the initial state avoids a spurious first tick when the encoder does not happen to
277    /// rest with both channels low.
278    ///
279    /// # Arguments
280    ///
281    /// * `a` - the current A channel level.
282    /// * `b` - the current B channel level.
283    ///
284    /// # Returns
285    ///
286    /// A decoder with a zero count and the given starting state.
287    pub fn starting(a: bool, b: bool) -> Self {
288        Self {
289            state: encode(a, b),
290            count: 0,
291        }
292    }
293
294    /// Feeds the latest channel levels and returns the tick delta.
295    ///
296    /// # Arguments
297    ///
298    /// * `a` - the latest A channel level.
299    /// * `b` - the latest B channel level.
300    ///
301    /// # Returns
302    ///
303    /// `+1` or `-1` for a step in either direction, or `0` for no change or an illegal jump.
304    pub fn update(&mut self, a: bool, b: bool) -> i8 {
305        let next = encode(a, b);
306        let delta = QUADRATURE_TABLE[((self.state << 2) | next) as usize];
307        self.state = next;
308        self.count += delta as i64;
309        delta
310    }
311
312    /// Returns the accumulated signed tick count.
313    ///
314    /// # Returns
315    ///
316    /// The running count.
317    pub fn count(&self) -> i64 {
318        self.count
319    }
320
321    /// Resets the count to zero, keeping the current channel state.
322    pub fn reset(&mut self) {
323        self.count = 0;
324    }
325}
326
327/// Converts encoder ticks into the distance and speed a wheel has traveled.
328///
329/// # Examples
330///
331/// ```
332/// use pamoja_kit::QuadratureScale;
333///
334/// // 360 ticks per revolution on a wheel of 0.05 m radius.
335/// let scale = QuadratureScale::new(360.0, 0.05);
336/// // One full revolution rolls out one circumference.
337/// assert!((scale.distance(360) - (2.0 * core::f32::consts::PI * 0.05)).abs() < 1e-6);
338/// ```
339#[derive(Clone, Copy, Debug)]
340pub struct QuadratureScale {
341    counts_per_rev: f32,
342    wheel_radius: f32,
343}
344
345impl QuadratureScale {
346    /// Creates a scale from the encoder resolution and wheel size.
347    ///
348    /// # Arguments
349    ///
350    /// * `counts_per_rev` - ticks per wheel revolution; its magnitude is used.
351    /// * `wheel_radius` - the wheel radius in meters; its magnitude is used.
352    ///
353    /// # Returns
354    ///
355    /// The scale.
356    pub fn new(counts_per_rev: f32, wheel_radius: f32) -> Self {
357        Self {
358            counts_per_rev: magnitude(counts_per_rev),
359            wheel_radius: magnitude(wheel_radius),
360        }
361    }
362
363    /// Returns the distance rolled for a tick count.
364    ///
365    /// # Arguments
366    ///
367    /// * `count` - the signed tick count.
368    ///
369    /// # Returns
370    ///
371    /// The distance in meters, zero when the resolution is zero.
372    pub fn distance(&self, count: i64) -> f32 {
373        if self.counts_per_rev == 0.0 {
374            return 0.0;
375        }
376        let circumference = 2.0 * PI * self.wheel_radius;
377        (count as f32 / self.counts_per_rev) * circumference
378    }
379
380    /// Returns the speed for a tick count accumulated over a time step.
381    ///
382    /// # Arguments
383    ///
384    /// * `delta_count` - the ticks counted during the step.
385    /// * `dt` - the length of the step.
386    ///
387    /// # Returns
388    ///
389    /// The speed in meters per second, zero when `dt` is zero.
390    pub fn velocity(&self, delta_count: i64, dt: f32) -> f32 {
391        if dt == 0.0 {
392            0.0
393        } else {
394            self.distance(delta_count) / dt
395        }
396    }
397}
398
399#[cfg(test)]
400mod tests {
401    use super::*;
402
403    #[test]
404    fn servo_maps_ends_and_center() {
405        let servo = ServoMap::standard();
406        assert_eq!(servo.pulse(0.0), 1000);
407        assert_eq!(servo.pulse(90.0), 1500);
408        assert_eq!(servo.pulse(180.0), 2000);
409        assert_eq!(servo.pulse(999.0), 2000); // clamped
410        assert!((servo.angle(1750) - 135.0).abs() < 1e-3);
411    }
412
413    #[test]
414    fn esc_maps_throttle_across_the_range() {
415        let esc = Esc::bidirectional();
416        assert_eq!(esc.pulse(0.0), 1500);
417        assert_eq!(esc.pulse(1.0), 2000);
418        assert_eq!(esc.pulse(-1.0), 1000);
419        assert_eq!(esc.pulse(0.5), 1750);
420        assert_eq!(esc.pulse(-2.0), 1000); // clamped
421    }
422
423    #[test]
424    fn a_throttle_that_is_not_a_number_is_neutral() {
425        assert_eq!(Esc::bidirectional().pulse(f32::NAN), 1500);
426    }
427
428    #[test]
429    fn an_angle_that_is_not_a_number_gives_no_pulse() {
430        assert_eq!(ServoMap::standard().pulse(f32::NAN), 0);
431    }
432
433    #[test]
434    fn a_reversed_servo_reads_its_angle_back() {
435        let reversed = ServoMap::new(2000, 1000, 180.0);
436        assert_eq!(reversed.pulse(0.0), 2000);
437        assert!((reversed.angle(1500) - 90.0).abs() < 1e-3);
438        assert!((reversed.angle(1250) - 135.0).abs() < 1e-3);
439    }
440
441    #[test]
442    fn quadrature_counts_forward_and_backward() {
443        let mut enc = Quadrature::new();
444        let forward = [(false, true), (true, true), (true, false), (false, false)];
445        for &(a, b) in &forward {
446            assert_eq!(enc.update(a, b), 1);
447        }
448        assert_eq!(enc.count(), 4);
449
450        // Same sequence reversed steps the count back down.
451        let backward = [(true, false), (true, true), (false, true), (false, false)];
452        for &(a, b) in &backward {
453            assert_eq!(enc.update(a, b), -1);
454        }
455        assert_eq!(enc.count(), 0);
456    }
457
458    #[test]
459    fn quadrature_ignores_no_change_and_illegal_jumps() {
460        let mut enc = Quadrature::new();
461        assert_eq!(enc.update(false, false), 0); // no change from 00
462        assert_eq!(enc.update(true, true), 0); // 00 -> 11 is a skipped step
463    }
464
465    #[test]
466    fn scale_turns_ticks_into_distance_and_speed() {
467        let scale = QuadratureScale::new(360.0, 0.05);
468        let one_rev = 2.0 * PI * 0.05;
469        assert!((scale.distance(360) - one_rev).abs() < 1e-6);
470        assert!((scale.velocity(360, 2.0) - one_rev / 2.0).abs() < 1e-6);
471        assert_eq!(scale.velocity(360, 0.0), 0.0);
472    }
473}