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}