1use anyhow::{bail, Context};
2
3use crate::error::Result;
4use crate::math;
5use crate::movement::acceleration::TrapezoidalAcceleration;
6use crate::movement::pid::{PidConfig, PidController};
7use crate::robot::{AngleProvider, Command, MotorId, Robot, StopAction, TurnType};
8use crate::types::{Degrees, DegreesPerSecond, Heading, UnitsExt};
9use std::thread;
10use std::time::{Duration, Instant};
11
12pub struct MovementController {
14 pid_config: PidConfig,
15 pub target_direction: Heading,
16}
17
18impl MovementController {
19 pub fn new(pid_config: PidConfig) -> Self {
20 MovementController {
21 pid_config,
22 target_direction: Heading(0.0),
23 }
24 }
25
26 pub fn drive<R: Robot + AngleProvider>(
28 &mut self,
29 robot: &R,
30 distance: Degrees,
31 speed: DegreesPerSecond,
32 ) -> Result<()> {
33 self.arc(
34 robot,
35 distance,
36 speed,
37 (1.0, 1.0),
38 EndingCondition::Distance,
39 )
40 }
41
42 pub fn turn<R: Robot + AngleProvider>(
43 &mut self,
44 robot: &R,
45 angle: Heading,
46 speed: DegreesPerSecond,
47 ) -> Result<()> {
48 let difference = math::subtract_angles(angle, self.target_direction);
50
51 let turn_type = if difference.0 > 0.0 {
52 TurnType::Left
53 } else {
54 TurnType::Right
55 };
56
57 self.turn_named(robot, angle, speed, turn_type)
58 }
59
60 pub fn turn_named<R: Robot + AngleProvider>(
63 &mut self,
64 robot: &R,
65 angle: Heading,
66 speed: DegreesPerSecond,
67 turn: TurnType,
68 ) -> Result<()> {
69 let observered_heading = robot.angle().context("Read heading")?;
70 let angle_delta = math::subtract_angles(angle, observered_heading);
71 let distance = robot.spec().get_distance_for_turn(angle_delta);
72
73 let ratio = match turn {
74 TurnType::Left => (1.0, 0.0),
75 TurnType::Right => (0.0, -1.0),
76 TurnType::Center => (1.0, -1.0),
77 };
78
79 self.arc(robot, distance, speed, ratio, EndingCondition::Heading)
80 }
81
82 pub fn arc<R: Robot + AngleProvider>(
83 &mut self,
84 robot: &R,
85 distance: Degrees,
86 speed: DegreesPerSecond,
87 ratio: (f32, f32),
88 ending_condition: EndingCondition,
89 ) -> Result<()> {
90 if distance.0 == 0.0 {
91 return Ok(());
93 }
94
95 let direction = distance.0.signum();
97
98 let distance = distance.0.abs();
99 let speed = speed.0;
100
101 assert!(speed > 0.0, "Speed must be greater than 0");
102
103 let spec = robot.spec();
104
105 let current_max_speed = spec.max_speed().0 * robot.battery()?.0 - 100.0;
107 let speed = f32::clamp(speed, 0.0, current_max_speed);
108
109 let base = f32::max(ratio.0.abs(), ratio.1.abs());
111 let ratio_left = ratio.0 / base * direction;
112 let ratio_right = ratio.1 / base * direction;
113
114 let move_left = (ratio_left.ceil() as i32).signum().abs() as f32;
116 let move_right = (ratio_right.ceil() as i32).signum().abs() as f32;
117
118 let start_angle = self.target_direction;
120 let delta_angle = spec.get_approx_angle(
121 (distance * ratio_left).deg(),
122 (distance * ratio_right).deg(),
123 );
124 let end_angle = math::add_angles(start_angle, delta_angle);
125
126 let acceleration = TrapezoidalAcceleration::new(
128 distance.deg(),
129 speed.dps(),
130 spec.acceleration(),
131 spec.deceleration(),
132 );
133
134 let mut pid = PidController::new(self.pid_config);
136
137 let mut left = robot.motor(MotorId::DriveLeft).context("Left motor")?;
139 let mut right = robot.motor(MotorId::DriveRight).context("Right motor")?;
140
141 left.motor_reset(Some(StopAction::Hold))?;
142 right.motor_reset(Some(StopAction::Hold))?;
143
144 let start = Instant::now();
146
147 loop {
148 let iter_start = Instant::now();
149 let duration_since_start = iter_start - start;
150
151 let average_distance = {
153 let raw_left = left.motor_angle().context("Read left wheel angle")?;
154 let raw_right = right.motor_angle().context("Read right wheel angle")?;
155
156 let adjusted_left = raw_left.to_deg(spec).0 / ratio_left;
159 let adjusted_right = raw_right.to_deg(spec).0 / ratio_right;
160
161 let combined = match (
164 adjusted_left.is_normal() || adjusted_left == 0.0,
165 adjusted_right.is_normal() || adjusted_right == 0.0,
166 ) {
167 (true, true) => (adjusted_left.abs() + adjusted_right.abs()) / 2.0,
168 (true, false) => adjusted_left.abs(),
169 (false, true) => adjusted_right.abs(),
170 (false, false) => bail!("Nether wheel has a normal adjusted angle"),
171 };
172
173 combined
176 };
177
178 let progress = (acceleration.get_distance(duration_since_start) / distance).min(1.0);
180 let observered_heading = robot.angle().context("Read heading")?;
181 let target_heading = math::lerp_angles(progress, start_angle, end_angle);
182
183 let should_break = match ending_condition {
185 EndingCondition::Heading => {
186 math::subtract_angles(end_angle, observered_heading).0.abs() <= 2.0
187 }
188 EndingCondition::Distance => average_distance >= distance,
189 };
190 if should_break {
191 break;
192 }
193
194 let error = math::subtract_angles(target_heading, observered_heading).0;
196 let (pid, _internal) = pid.update(error);
197 let correction = pid / 100.0;
198
199 let speed_base = acceleration.get_speed(duration_since_start).0;
203
204 let speed_left = (speed_base * ratio_left + correction * current_max_speed) * move_left;
206 let speed_right =
207 (speed_base * ratio_right - correction * current_max_speed) * move_right;
208
209 let speed_left = f32::clamp(speed_left, -current_max_speed, current_max_speed);
211 let speed_right = f32::clamp(speed_right, -current_max_speed, current_max_speed);
212
213 {
215 left.raw(Command::Queue(Command::On(speed_left.dps().into()).into()))?;
216 right.raw(Command::Queue(Command::On(speed_right.dps().into()).into()))?;
217
218 left.raw(Command::Execute)?;
219 right.raw(Command::Execute)?;
220 }
221
222 robot.handle_interrupt()?;
224
225 thread::sleep(
227 Duration::checked_sub(Duration::from_millis(10), iter_start.elapsed())
228 .unwrap_or_default(),
229 )
230 }
231
232 self.target_direction = end_angle;
234
235 left.raw(Command::Stop(StopAction::Hold))?;
237 right.raw(Command::Stop(StopAction::Hold))?;
238
239 left.wait(None)?;
241 right.wait(None)?;
242
243 Ok(())
244 }
245}
246
247#[derive(Clone, Copy, Debug)]
248pub enum EndingCondition {
249 Heading,
250 Distance,
251}