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}