Skip to main content

ev3dev_rs/robotics/
drive_base.rs

1use crate::pid::Pid;
2use crate::pupdevices::GyroSensor;
3use crate::robotics::GyroController;
4use crate::Ev3Error;
5use crate::{parameters::Stop, pupdevices::Motor, Ev3Result};
6use fixed::traits::{LossyInto, ToFixed};
7use fixed::types::I32F32;
8use scopeguard::defer;
9use std::cell::Cell;
10use std::time::Duration;
11use tokio::time::interval;
12
13/// A pybricks-like `DriveBase`.
14///
15/// Using gyroscope(s) is highly recommended in order to get the most accurate actions
16///
17/// # Examples
18///
19/// ``` no_run
20/// use ev3dev_rs::parameters::{Direction, MotorPort, SensorPort, Stop};
21/// use ev3dev_rs::pupdevices::{Motor, GyroSensor};
22/// use ev3dev_rs::robotics::DriveBase;
23///
24/// let left = Motor::new(MotorPort::OutA, Direction::CounterClockwise).await?;
25/// let right = Motor::new(MotorPort::OutD, Direction::CounterClockwise).await?;
26///
27/// // no gyro
28/// let drive = DriveBase::new(&left, &right, 62.4, 130.5).await?;
29///
30/// // with gyro
31/// let gyro = GyroSensor::new(SensorPort::In1)?;
32///
33/// let drive = DriveBase::new(&left, &right, 62.4, 130.5).await?.with_gyro(&gyro).await?;
34///
35/// // you have to explicitly enable the gyro
36/// drive.use_gyro(true)?;
37///
38/// // default is 500
39/// drive.set_straight_speed(600)?;
40///
41/// // default should be coast
42/// // unlike pybricks, the stop action doesn't affect whether the robot tracks it's position and heading
43/// drive.set_stop_action(Stop::Hold)?;
44///
45/// drive.straight(500).await?;
46/// drive.turn(90).await?;
47/// ```
48pub struct DriveBase<'a> {
49    left_motor: &'a Motor,
50    right_motor: &'a Motor,
51    left_start_angle: i32,
52    right_start_angle: i32,
53    min_speed: I32F32,
54    wheel_diameter: I32F32,
55    axle_track: I32F32,
56    straight_speed: Cell<I32F32>,
57    turn_speed: Cell<I32F32>,
58    prev_encoder_heading: Cell<I32F32>,
59    distance_pid: Pid,
60    heading_pid: Pid,
61    distance_target: Cell<I32F32>,
62    heading_target: Cell<I32F32>,
63    distance_tolerance: Cell<I32F32>,
64    heading_tolerance: Cell<I32F32>,
65    using_gyros: Cell<bool>,
66    gyros: Option<GyroController<'a>>,
67}
68
69impl<'a> DriveBase<'a> {
70    /// Creates a new `DriveBase` with the defined parameters.
71    ///
72    /// Wheel diameter and axle track are in mm.
73    ///
74    /// Using a gyroscope is highly recommended, see `with_gyro` or `with_gyros`.
75    pub async fn new<Number>(
76        left_motor: &'a Motor,
77        right_motor: &'a Motor,
78        wheel_diameter: Number,
79        axle_track: Number,
80    ) -> Ev3Result<Self>
81    where
82        Number: ToFixed,
83    {
84        left_motor.set_ramp_up_setpoint(2000)?;
85        right_motor.set_ramp_up_setpoint(2000)?;
86
87        left_motor.set_ramp_down_setpoint(1800)?;
88        right_motor.set_ramp_down_setpoint(1800)?;
89
90        Ok(Self {
91            left_motor,
92            right_motor,
93            left_start_angle: left_motor.angle().await?,
94            right_start_angle: right_motor.angle().await?,
95            min_speed: I32F32::from_num(100),
96            wheel_diameter: I32F32::from_num(wheel_diameter),
97            axle_track: I32F32::from_num(axle_track),
98            straight_speed: Cell::new(I32F32::from_num(500)),
99            turn_speed: Cell::new(I32F32::from_num(550)),
100            prev_encoder_heading: Cell::new(I32F32::ZERO),
101            distance_pid: Pid::new(10, 0, 8, 0, 0),
102            heading_pid: Pid::new(10, 0, 5, 0, 0),
103            distance_target: Cell::new(I32F32::ZERO),
104            heading_target: Cell::new(I32F32::ZERO),
105            distance_tolerance: Cell::new(I32F32::from_num(4)),
106            heading_tolerance: Cell::new(I32F32::from_num(0.75)),
107            using_gyros: Cell::new(false),
108            gyros: None,
109        })
110    }
111
112    /// Adds a single gyro sensor to the `DriveBase`.
113    ///
114    /// # Examples
115    ///
116    /// ``` no_run
117    ///
118    /// use ev3dev_rs::parameters::{Direction, MotorPort, SensorPort, Stop};
119    /// use ev3dev_rs::pupdevices::{Motor, GyroSensor};
120    /// use ev3dev_rs::robotics::DriveBase;
121    /// use ev3dev_rs::Ev3Result;
122    ///
123    /// #[tokio::main]
124    /// async fn main() -> Ev3Result<()> {
125    ///
126    ///     let left = Motor::new(MotorPort::OutA, Direction::CounterClockwise).await?;
127    ///
128    ///     let right = Motor::new(MotorPort::OutD, Direction::CounterClockwise).await?;
129    ///
130    ///     let gyro = GyroSensor::new(SensorPort::In1)?;
131    ///
132    ///     let drive = DriveBase::new(&left, &right, 62.4, 130.5).await?.with_gyro(&gyro).await?;
133    ///
134    ///      // you have to explicitly enable the gyro
135    ///
136    ///     drive.use_gyro(true)?;
137    ///
138    ///     Ok(())
139    /// }
140    /// ```
141    pub async fn with_gyro<'b>(mut self, gyro_sensor: &'b GyroSensor) -> Ev3Result<Self>
142    where
143        'b: 'a,
144    {
145        self.gyros = Some(GyroController::new(vec![gyro_sensor]).await?);
146        Ok(self)
147    }
148
149    /// Adds multiple gyro sensors to the `DriveBase`.
150    ///
151    /// # Examples
152    ///
153    /// ``` no_run
154    /// use ev3dev_rs::parameters::{Direction, MotorPort, SensorPort, Stop};
155    /// use ev3dev_rs::pupdevices::{Motor, GyroSensor};
156    /// use ev3dev_rs::robotics::DriveBase;
157    /// use ev3dev_rs::Ev3Result;
158    ///
159    /// #[tokio::main]
160    /// async fn main() -> Ev3Result<()> {
161    ///
162    ///     let left = Motor::new(MotorPort::OutA, Direction::CounterClockwise).await?;
163    ///
164    ///     let right = Motor::new(MotorPort::OutD, Direction::CounterClockwise).await?;
165    ///
166    ///     let left_gyro = GyroSensor::new(SensorPort::In1)?;
167    ///
168    ///     let right_gyro = GyroSensor::new(SensorPort::In4)?;
169    ///
170    ///     let drive = DriveBase::new(&left, &right, 62.4, 130.5).await?.with_gyros(vec![ &left_gyro, &right_gyro ]).await?;
171    ///
172    ///     // you have to explicitly enable the gyro
173    ///
174    ///     drive.use_gyro(true)?;
175    ///
176    ///     Ok(())
177    /// }
178    /// ```
179    pub async fn with_gyros<'b>(mut self, gyro_sensors: Vec<&'b GyroSensor>) -> Ev3Result<Self>
180    where
181        'b: 'a,
182    {
183        self.gyros = Some(GyroController::new(gyro_sensors).await?);
184        Ok(self)
185    }
186
187    /// True makes the `DriveBase` use the gyro, while false makes the `DriveBase` use the motor encoders.
188    ///
189    /// Using the gyro is highly recommended for accurate drive actions.
190    pub fn use_gyro(&self, use_gyro: bool) -> Ev3Result<()> {
191        if use_gyro && self.gyros.is_none() {
192            return Err(Ev3Error::NoSensorProvided);
193        }
194        self.using_gyros.set(use_gyro);
195        Ok(())
196    }
197
198    /// Sets the straight speed in motor degrees per second.
199    ///
200    /// The default is 500 and the max is 1000.
201    pub fn set_straight_speed<Number>(&self, straight_speed: Number)
202    where
203        Number: ToFixed,
204    {
205        self.straight_speed.set(I32F32::from_num(straight_speed));
206    }
207
208    /// Sets the max turn speed in motor degrees per second.
209    ///
210    /// The default is 500 and the max is 1000.
211    pub fn set_turn_speed<Number>(&self, turn_speed: Number)
212    where
213        Number: ToFixed,
214    {
215        self.turn_speed.set(I32F32::from_num(turn_speed));
216    }
217
218    /// Units are in milliseconds and must be positive.
219    ///
220    /// When set to a non-zero value, the motor speed will increase from 0 to 100% of max_speed over the span of this setpoint.
221    ///
222    /// This is especially useful for avoiding wheel slip.
223    ///
224    /// The default for `DriveBase` motors is 2000.
225    pub fn set_ramp_up_setpoint(&self, sp: u32) -> Ev3Result<()> {
226        self.left_motor.set_ramp_up_setpoint(sp)?;
227        self.right_motor.set_ramp_up_setpoint(sp)
228    }
229
230    /// Units are in milliseconds and must be positive.
231    ///
232    /// When set to a non-zero value, the motor speed will decrease from 0 to 100% of max_speed over the span of this setpoint.
233    ///
234    /// This is especially useful for avoiding wheel slip.
235    ///
236    /// The default for `DriveBase` motors is 1800.
237    pub fn set_ramp_down_setpoint(&self, sp: u32) -> Ev3Result<()> {
238        self.left_motor.set_ramp_down_setpoint(sp)?;
239        self.right_motor.set_ramp_down_setpoint(sp)
240    }
241    /// Sets the stop action of the `DriveBase`
242    ///
243    /// Unlike pybricks, this doesn't affect whether the `DriveBase` tracks heading and distance
244    pub fn set_stop_action(&self, action: Stop) -> Ev3Result<()> {
245        self.left_motor.set_stop_action(action)?;
246        self.right_motor.set_stop_action(action)
247    }
248
249    /// Sets the distance PID settings
250    ///
251    /// default: 10, 0, 8, 0, 0
252    pub fn distance_pid_settings<Number>(
253        &self,
254        kp: Number,
255        ki: Number,
256        kd: Number,
257        integral_deadzone: Number,
258        integral_rate: Number,
259    ) where
260        Number: ToFixed,
261    {
262        self.distance_pid
263            .settings(kp, ki, kd, integral_deadzone, integral_rate);
264    }
265
266    /// Sets the heading PID settings
267    ///
268    /// default: 10, 0, 5, 0, 0
269    pub fn heading_pid_settings<Number>(
270        &self,
271        kp: Number,
272        ki: Number,
273        kd: Number,
274        integral_deadzone: Number,
275        integral_rate: Number,
276    ) where
277        Number: ToFixed,
278    {
279        self.heading_pid
280            .settings(kp, ki, kd, integral_deadzone, integral_rate);
281    }
282
283    /// Stops the `DriveBase` with the selected stop action.
284    ///
285    /// Async driving functions automatically do this.
286    ///
287    /// See `set_stop_action` to select the stop action.
288    pub fn stop(&self) -> Ev3Result<()> {
289        self.left_motor.stop_prev_action()?;
290        self.right_motor.stop_prev_action()
291    }
292
293    async fn drive_relative(&self, distance_mm: I32F32, angle_deg: I32F32) -> Ev3Result<()> {
294        defer! {
295            _ = self.stop()
296        }
297
298        self.distance_pid.reset();
299        self.heading_pid.reset();
300
301        let target_distance = self.distance_target.get() + distance_mm;
302        let target_heading = self.heading_target.get() + angle_deg;
303
304        self.distance_target.set(target_distance);
305        self.heading_target.set(target_heading);
306
307        let mut timer = interval(Duration::from_millis(5));
308
309        // the first tick completes immediately
310        timer.tick().await;
311
312        let straight_speed = self.straight_speed.get();
313        let turn_speed = self.turn_speed.get();
314
315        loop {
316            let left_angle =
317                I32F32::from_num(self.left_motor.angle().await? - self.left_start_angle);
318            let right_angle =
319                I32F32::from_num(self.right_motor.angle().await? - self.right_start_angle);
320            let current_distance = self.encoders_to_distance(left_angle, right_angle);
321            let current_heading = if self.using_gyros.get()
322                && let Some(ref gyro) = self.gyros
323            {
324                let encoders = self.encoders_to_heading().await?;
325                I32F32::from_num(gyro.heading().await?) * I32F32::from_num(0.9)
326                    + encoders * I32F32::from_num(0.1)
327            } else {
328                self.encoders_to_heading().await?
329            };
330
331            let distance_error = target_distance - current_distance;
332            let heading_error = target_heading - current_heading;
333
334            if distance_error.abs() < self.distance_tolerance.get()
335                && heading_error.abs() < self.heading_tolerance.get()
336            {
337                break;
338            }
339
340            let dive_effort = self.distance_pid.next(distance_error);
341            let turn_effort = -self.heading_pid.next(heading_error);
342
343            let drive_speed_out = dive_effort * straight_speed;
344            let turn_speed_out = turn_effort * turn_speed;
345
346            let left_speed = (drive_speed_out - turn_speed_out)
347                .clamp(-self.right_motor.max_speed, self.left_motor.max_speed);
348
349            let right_speed = (drive_speed_out + turn_speed_out)
350                .clamp(-self.left_motor.max_speed, self.right_motor.max_speed);
351
352            self.left_motor.run(
353                (if left_speed.abs() < self.min_speed {
354                    self.min_speed * left_speed.signum()
355                } else {
356                    left_speed
357                })
358                .lossy_into(),
359            )?;
360            self.right_motor.run(
361                (if right_speed.abs() < self.min_speed {
362                    self.min_speed * right_speed.signum()
363                } else {
364                    right_speed
365                })
366                .lossy_into(),
367            )?;
368
369            timer.tick().await;
370        }
371
372        Ok(())
373    }
374
375    /// Drives straight by the given distance in mm.
376    pub async fn straight<Number>(&self, distance: Number) -> Ev3Result<()>
377    where
378        Number: ToFixed,
379    {
380        self.drive_relative(I32F32::from_num(distance), I32F32::from_num(0))
381            .await
382    }
383
384    /// Turns by the angle in degrees.
385    pub async fn turn<Number>(&self, angle: Number) -> Ev3Result<()>
386    where
387        Number: ToFixed,
388    {
389        self.drive_relative(I32F32::from_num(0), I32F32::from_num(angle))
390            .await
391    }
392
393    /// Curves with a given radius and a target angle.
394    pub async fn curve<Number>(&self, radius: Number, angle: Number) -> Ev3Result<()>
395    where
396        Number: ToFixed,
397    {
398        let fixed_angle = I32F32::from_num(angle);
399
400        let angle_rad = fixed_angle * I32F32::PI / 180;
401        let arc_length = I32F32::from_num(radius).abs() * I32F32::from_num(angle_rad).abs();
402
403        self.drive_relative(arc_length, I32F32::from_num(fixed_angle))
404            .await
405    }
406
407    /// Curves with a given radius and distance.
408    pub async fn veer<Number>(&self, radius: Number, distance: Number) -> Ev3Result<()>
409    where
410        Number: ToFixed,
411    {
412        let fixed_distance = I32F32::from_num(distance);
413
414        let angle_rad = fixed_distance / I32F32::from_num(radius);
415        let angle_deg = angle_rad * 180 / I32F32::PI;
416
417        self.drive_relative(fixed_distance, I32F32::from_num(angle_deg))
418            .await
419    }
420
421    /// Experimental function to find the best axle track for the robot
422    ///
423    /// This should print out the ideal axle track once it is finished testing.
424    ///
425    /// If you are having trouble with inaccurate heading readings due to wheel slipping, see `set_ramp_up_setpoint`.
426    ///
427    /// Note that the output value can vary wildly based on surface.
428    pub async fn find_calibrated_axle_track<Number>(
429        &mut self,
430        margin_of_error: Number,
431    ) -> Ev3Result<I32F32>
432    where
433        Number: ToFixed,
434    {
435        self.use_gyro(true)?;
436        let fixed_estimate = I32F32::from_num(self.axle_track);
437        let fixed_margin_of_error = I32F32::from_num(margin_of_error);
438        let resphi = I32F32::FRAC_1_PHI;
439
440        let mut a = fixed_estimate - fixed_margin_of_error;
441        let mut b = fixed_estimate + fixed_margin_of_error;
442        let tolerance = I32F32::from_num(0.5);
443
444        // Initial test points
445        let mut x1 = a + resphi * (b - a);
446        let mut x2 = b - resphi * (b - a);
447
448        let mut f1 = self.test_axle_track(x1).await?;
449        let mut f2 = self.test_axle_track(x2).await?;
450
451        // Golden section search loop
452        while (b - a).abs() > tolerance {
453            if f1 < f2 {
454                // Minimum is between a and x2
455                a = x2;
456                x2 = x1;
457                f2 = f1;
458                x1 = a + resphi * (b - a);
459                f1 = self.test_axle_track(x1).await?;
460            } else {
461                // Minimum is between x1 and b
462                b = x1;
463                x1 = x2;
464                f1 = f2;
465                x2 = b - resphi * (b - a);
466                f2 = self.test_axle_track(x2).await?;
467            }
468        }
469
470        let best = (a + b) / I32F32::from_num(2);
471        println!("Best axle track: {}", best);
472
473        Ok(best)
474    }
475
476    // Helper function to test a single axle track value
477    async fn test_axle_track(&mut self, candidate: I32F32) -> Ev3Result<I32F32> {
478        println!("testing {}", candidate);
479        self.axle_track = candidate;
480
481        // Reset to known position
482        if let Some(ref gyros) = self.gyros {
483            let start_encoder_heading = self.encoders_to_heading().await?;
484            gyros.reset().await?;
485            // Do a test turn (90 degrees)
486            self.turn(90).await?;
487
488            // Measure error
489            let gyro_turned = gyros.heading().await?;
490            let encoder_turned = self.encoders_to_heading().await? - start_encoder_heading;
491
492            let error = (gyro_turned - encoder_turned).abs();
493            println!("Error: {}", error);
494
495            Ok(error)
496        } else {
497            Err(Ev3Error::NoSensorProvided)
498        }
499    }
500
501    // Convert encoder positions to distance traveled (average of both wheels)
502    fn encoders_to_distance(&self, left_deg: I32F32, right_deg: I32F32) -> I32F32 {
503        let wheel_circ = I32F32::PI * self.wheel_diameter;
504        let left_mm = wheel_circ * left_deg / 360;
505        let right_mm = wheel_circ * right_deg / 360;
506        (left_mm + right_mm) / 2
507    }
508
509    // Convert encoder positions to heading (differential between wheels)
510    async fn encoders_to_heading(&self) -> Ev3Result<I32F32> {
511        let left_deg = I32F32::from_num(self.left_motor.angle().await? - self.left_start_angle);
512        let right_deg = I32F32::from_num(self.right_motor.angle().await? - self.right_start_angle);
513
514        let wheel_circ = I32F32::PI * self.wheel_diameter;
515        let left_mm = wheel_circ * left_deg / 360;
516        let right_mm = wheel_circ * right_deg / 360;
517        let arc_diff = left_mm - right_mm;
518        let turn_rad = arc_diff / self.axle_track;
519        let raw_heading = turn_rad * 180 / I32F32::PI;
520
521        let alpha = I32F32::from_num(0.7);
522        let filtered =
523            alpha * raw_heading + (I32F32::from_num(1) - alpha) * self.prev_encoder_heading.get();
524        self.prev_encoder_heading.set(filtered);
525        Ok(filtered)
526    }
527}