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}