fll_rs/movement/
controller.rs

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
12/// The standard implementation of movement
13pub 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    /// Implementation of the gyro drive algorithm
27    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        // Use target angle so behaivor is deterministic
49        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    // Has a lot in common with drive, could they be merged?
61    // todo implement pid turns??
62    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            // Requested movement of 0 deg
92            return Ok(());
93        }
94
95        // Determine the requested direction
96        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        // Clamp requested speed
106        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        // Calculate ratios
110        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        // Determine which wheels should move at all
115        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        // Calculate angles
119        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        // Generate the acceleration curve
127        let acceleration = TrapezoidalAcceleration::new(
128            distance.deg(),
129            speed.dps(),
130            spec.acceleration(),
131            spec.deceleration(),
132        );
133
134        // Setup the PID controller
135        let mut pid = PidController::new(self.pid_config);
136
137        // Setup motors
138        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        // Record the start time for acceleration
145        let start = Instant::now();
146
147        loop {
148            let iter_start = Instant::now();
149            let duration_since_start = iter_start - start;
150
151            // TODO check if this is correct
152            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                // Due to the ratios, the raw data needs to be corrected before it can be
157                // compared with the requested distance
158                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                // Is it possible that ratio_left or ratio_right could be 0
162                // Check that adjusted values are normal before including them
163                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                // eprintln!("raw_l: {raw_left:.3?}, raw_r: {raw_right:.3?}, combined: {combined:.3}, distance: {distance:.3}");
174
175                combined
176            };
177
178            // Calculate headings
179            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            // Determine if loop should break
184            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            // Run PID controller
195            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            // eprintln!("obs: {observered_heading:5.3?}, tar: {target_heading:5.3?}, pid: {internal:5.3?}, cor: {correction:5.3?}, err: {error:5.3?}");
200
201            // Get position on acceleration curve
202            let speed_base = acceleration.get_speed(duration_since_start).0;
203
204            // Merge corrections with speeds
205            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            // Re-clamp speeds
210            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            // Send motor commands
214            {
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            // Exit if requested
223            robot.handle_interrupt()?;
224
225            // TODO tune
226            thread::sleep(
227                Duration::checked_sub(Duration::from_millis(10), iter_start.elapsed())
228                    .unwrap_or_default(),
229            )
230        }
231
232        // Update state
233        self.target_direction = end_angle;
234
235        // Request motor stop
236        left.raw(Command::Stop(StopAction::Hold))?;
237        right.raw(Command::Stop(StopAction::Hold))?;
238
239        // Wait for motors to stop
240        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}