ev3_drivebase/drivebase/
turn.rs

1use super::DriveBase;
2use crate::Direction;
3use ev3dev_lang_rust::Ev3Error;
4use std::f64::consts::PI;
5
6impl DriveBase {
7    /// Turns the robot by a specified angle, either in place or along an arc.
8    ///
9    /// This method provides two types of turns:
10    ///
11    /// - **In-place turn** (radius = `None`): The robot rotates around its center point
12    /// - **Arc turn** (radius = `Some(r)`): The robot follows a circular path with the given radius
13    ///
14    /// # Parameters
15    ///
16    /// - `speed`: The speed in degrees per second (motor rotation, not robot turning speed)
17    /// - `degree`: The angle to turn in degrees. Positive for counter-clockwise, negative for clockwise.
18    /// - `radius`: The turning radius in millimeters:
19    ///   - `None`: Turn in place around the robot's center
20    ///   - `Some(r)`: Turn along an arc with radius `r`
21    ///     - `r = 0`: Same as in-place turn
22    ///     - `r > 0`: Outer wheel follows arc at distance `r` from turn center
23    ///
24    /// # Errors
25    ///
26    /// Returns an error if the motors cannot be commanded or calculations fail.
27    ///
28    /// # Examples
29    ///
30    /// In-place turns:
31    ///
32    /// ```no_run
33    /// use ev3_drivebase::{DriveBase, Motor, Direction};
34    /// use ev3_drivebase::ev3dev_lang_rust::{Ev3Error, motors::MotorPort};
35    ///
36    /// fn main() -> Result<(), Ev3Error> {
37    ///     let left = Motor::new(MotorPort::OutA, Direction::Clockwise);
38    ///     let right = Motor::new(MotorPort::OutB, Direction::CounterClockwise);
39    ///     let mut drivebase = DriveBase::new(left, right, 43.2, 185.0)?;
40    ///
41    ///     // Turn 90 degrees counter-clockwise in place
42    ///     drivebase.turn(200, 90, None)?;
43    ///
44    ///     // Turn 180 degrees clockwise in place
45    ///     drivebase.turn(200, -180, None)?;
46    ///
47    ///     Ok(())
48    /// }
49    /// ```
50    ///
51    /// Arc turns with radius:
52    ///
53    /// ```no_run
54    /// use ev3_drivebase::{DriveBase, Motor, Direction};
55    /// use ev3_drivebase::ev3dev_lang_rust::{Ev3Error, motors::MotorPort};
56    ///
57    /// fn main() -> Result<(), Ev3Error> {
58    ///     let left = Motor::new(MotorPort::OutA, Direction::Clockwise);
59    ///     let right = Motor::new(MotorPort::OutB, Direction::CounterClockwise);
60    ///     let mut drivebase = DriveBase::new(left, right, 43.2, 185.0)?;
61    ///
62    ///     // Turn 90 degrees following a 200mm radius arc
63    ///     drivebase.turn(200, 90, Some(200.0))?;
64    ///
65    ///     // Turn 45 degrees with a tight 50mm radius
66    ///     drivebase.turn(150, 45, Some(50.0))?;
67    ///
68    ///     Ok(())
69    /// }
70    /// ```
71    ///
72    /// # Behavior
73    ///
74    /// - If `degree` is 0, the method returns immediately without moving
75    /// - Negative radius values are converted to positive (radius is always absolute)
76    /// - The method blocks until the turn is complete
77    /// - For arc turns, the inner wheel moves slower than the outer wheel
78    pub fn turn(
79        &mut self,
80        speed: i32,
81        degree: i32,
82        radius: impl Into<Option<f64>>,
83    ) -> Result<&mut Self, Ev3Error> {
84        let mut radius: Option<f64> = radius.into();
85        radius = radius.map(f64::abs);
86
87        if degree == 0 {
88            return Ok(self);
89        }
90
91        match radius {
92            None => self.turn_in_place(speed, degree),
93            Some(radius) => self.turn_with_radius(speed, degree, radius),
94        }
95    }
96
97    #[expect(clippy::cast_possible_truncation)]
98    fn turn_in_place(&mut self, speed: i32, degree: i32) -> Result<&mut Self, Ev3Error> {
99        let speed = speed.abs();
100        let axle_radius = self.axle_track / 2.;
101        let arc_length = (axle_radius * f64::from(degree) * (PI / 180.))
102            .round()
103            .clamp(f64::from(i32::MIN), f64::from(i32::MAX)) as i32;
104
105        let direction = if degree >= 0 {
106            Direction::CounterClockwise
107        } else {
108            Direction::Clockwise
109        };
110
111        let counts = self.calculate_counts(arc_length, arc_length)?;
112        let (left_counts, right_counts) = match direction {
113            Direction::CounterClockwise => (-counts.0, counts.1),
114            Direction::Clockwise => (counts.0, -counts.1),
115        };
116
117        self.set_speed(speed, speed)?;
118
119        self.run_to_rel_pos(left_counts, right_counts)?;
120
121        self.wait_until_not_moving(None);
122
123        Ok(self)
124    }
125
126    #[expect(clippy::cast_possible_truncation)]
127    fn turn_with_radius(
128        &mut self,
129        mut speed: i32,
130        degree: i32,
131        radius: f64,
132    ) -> Result<&mut Self, Ev3Error> {
133        speed = speed.clamp(
134            -self.left.get_max_speed()? / 2,
135            self.left.get_max_speed()? / 2,
136        );
137
138        let theta = (f64::from(degree)).to_radians();
139        let theta_abs = theta.abs();
140        let half_track = self.axle_track / 2.0;
141        let eps = 1e-9;
142
143        let sign_f64 = |v: f64| if v >= 0.0 { 1 } else { -1 };
144        let deg_is_left = degree > 0;
145        let s = f64::from(speed);
146
147        let (left_speed_f, right_speed_f, left_arc_mm, right_arc_mm) = if radius.abs() < eps {
148            let arc = half_track * theta_abs;
149            let deg_sign = if deg_is_left { 1.0 } else { -1.0 };
150            let left_speed = -s * deg_sign;
151            let right_speed = s * deg_sign;
152            (left_speed, right_speed, arc, arc)
153        } else {
154            let r = radius.abs();
155            let (r_left, r_right) = if deg_is_left {
156                (r - half_track, r + half_track)
157            } else {
158                (r + half_track, r - half_track)
159            };
160
161            let left_speed = s * (r_left / r);
162            let right_speed = s * (r_right / r);
163
164            let left_arc = theta_abs * r_left.abs();
165            let right_arc = theta_abs * r_right.abs();
166            (left_speed, right_speed, left_arc, right_arc)
167        };
168
169        let left_rot = left_arc_mm / self.circumference;
170        let right_rot = right_arc_mm / self.circumference;
171
172        let left_tacho_deg = (left_rot * 360.0).round();
173        let right_tacho_deg = (right_rot * 360.0).round();
174
175        let left_speed =
176            (left_speed_f.round() as i32).saturating_mul(self.left_meta.direction.sign());
177        let right_speed =
178            (right_speed_f.round() as i32).saturating_mul(self.right_meta.direction.sign());
179
180        let left_counts = (left_tacho_deg as i32)
181            .saturating_mul(sign_f64(left_speed_f) * self.left_meta.direction.sign());
182        let right_counts = (right_tacho_deg as i32)
183            .saturating_mul(sign_f64(right_speed_f) * self.right_meta.direction.sign());
184
185        self.set_speed(left_speed, right_speed)?;
186        self.run_to_rel_pos(left_counts, right_counts)?;
187        self.wait_until_not_moving(None);
188
189        Ok(self)
190    }
191}