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