Skip to main content

robot_control/
robot.rs

1//! High-level robot controller with smooth motor ramping.
2//!
3//! The [`Robot`] struct wraps a [`MotorDriver`] and provides:
4//! - Speed commands as normalized `f32` values in the range `[-1.0, 1.0]`
5//! - Smooth acceleration/deceleration via a background task
6//! - Configurable motor factors (e.g., to correct for motor polarity)
7//! - Graceful shutdown via a cancellation token
8
9use std::sync::Arc;
10use std::time::Duration;
11
12use tokio::sync::Mutex;
13use tokio_util::sync::CancellationToken;
14use tracing::{debug, info, warn};
15
16use crate::error::{MotorError, RobotError};
17use crate::motor::{Motor, MotorDriver};
18
19/// Configuration for a [`Robot`] instance.
20///
21/// Use the builder methods or [`Default`] for sensible defaults.
22#[derive(Debug, Clone)]
23pub struct RobotConfig {
24    /// Factor applied to the left motor speed (use `-1.0` to reverse polarity).
25    pub left_motor_factor: f32,
26    /// Factor applied to the right motor speed (use `-1.0` to reverse polarity).
27    pub right_motor_factor: f32,
28    /// Which motor is the left motor.
29    pub left_motor: Motor,
30    /// Which motor is the right motor.
31    pub right_motor: Motor,
32    /// Speed adjustment step per tick (controls ramp smoothness).
33    pub adjustment_step: f32,
34    /// Interval between motor adjustment ticks.
35    pub adjustment_interval: Duration,
36}
37
38impl Default for RobotConfig {
39    fn default() -> Self {
40        Self {
41            left_motor_factor: 1.0,
42            right_motor_factor: 1.0,
43            left_motor: Motor::Motor1,
44            right_motor: Motor::Motor2,
45            adjustment_step: 0.1,
46            adjustment_interval: Duration::from_millis(10),
47        }
48    }
49}
50
51/// A two-motor differential-drive robot controller.
52///
53/// The robot smoothly ramps motor speeds from the current value to the target
54/// in a background task. Call [`drive`](Robot::drive) to set target speeds and
55/// [`stop`](Robot::stop) to decelerate to zero.
56///
57/// # Shutdown
58///
59/// Call [`shutdown`](Robot::shutdown) to cancel the background task and stop all motors.
60/// The robot also stops motors when dropped, but explicit shutdown is preferred.
61pub struct Robot {
62    motor_board: Arc<Mutex<dyn MotorDriver>>,
63    target_left_speed: Mutex<f32>,
64    target_right_speed: Mutex<f32>,
65    config: RobotConfig,
66    cancel_token: CancellationToken,
67}
68
69impl Robot {
70    /// Create a new robot with the given motor driver and configuration.
71    ///
72    /// Spawns a background task for smooth motor ramping. The task runs until
73    /// [`shutdown`](Robot::shutdown) is called or the [`Robot`] is dropped.
74    ///
75    /// # Errors
76    ///
77    /// Returns [`RobotError`] if the configuration is invalid.
78    pub fn new(
79        motor_board: Arc<Mutex<dyn MotorDriver>>,
80        config: RobotConfig,
81    ) -> Result<Arc<Self>, RobotError> {
82        let cancel_token = CancellationToken::new();
83        let robot = Arc::new(Self {
84            motor_board,
85            target_left_speed: Mutex::new(0.0),
86            target_right_speed: Mutex::new(0.0),
87            config,
88            cancel_token,
89        });
90
91        let robot_clone = Arc::clone(&robot);
92        tokio::spawn(async move {
93            robot_clone.smooth_motor_adjustment().await;
94        });
95
96        info!("robot initialized");
97        Ok(robot)
98    }
99
100    /// Set target motor speeds.
101    ///
102    /// Values are clamped to `[-1.0, 1.0]`. Motor factors from [`RobotConfig`]
103    /// are applied automatically.
104    ///
105    /// # Errors
106    ///
107    /// Returns [`RobotError::Shutdown`] if the robot has been shut down.
108    pub async fn drive(&self, left: f32, right: f32) -> Result<(), RobotError> {
109        if self.cancel_token.is_cancelled() {
110            return Err(RobotError::Shutdown);
111        }
112
113        let left = left.clamp(-1.0, 1.0) * self.config.left_motor_factor;
114        let right = right.clamp(-1.0, 1.0) * self.config.right_motor_factor;
115
116        *self.target_left_speed.lock().await = left;
117        *self.target_right_speed.lock().await = right;
118
119        debug!(left, right, "drive target set");
120        Ok(())
121    }
122
123    /// Set target speeds to zero (motors will ramp down smoothly).
124    ///
125    /// # Errors
126    ///
127    /// Returns [`RobotError::Shutdown`] if the robot has been shut down.
128    pub async fn stop(&self) -> Result<(), RobotError> {
129        if self.cancel_token.is_cancelled() {
130            return Err(RobotError::Shutdown);
131        }
132
133        *self.target_left_speed.lock().await = 0.0;
134        *self.target_right_speed.lock().await = 0.0;
135
136        debug!("stop requested");
137        Ok(())
138    }
139
140    /// Cancel the background task and stop all motors immediately.
141    pub async fn shutdown(&self) {
142        info!("shutting down robot");
143        self.cancel_token.cancel();
144
145        if let Err(e) = self.motor_board.lock().await.stop_all() {
146            warn!("failed to stop motors during shutdown: {e}");
147        }
148    }
149
150    async fn smooth_motor_adjustment(self: Arc<Self>) {
151        let mut current_left_speed: f32 = 0.0;
152        let mut current_right_speed: f32 = 0.0;
153        let step = self.config.adjustment_step;
154        let interval = self.config.adjustment_interval;
155
156        loop {
157            tokio::select! {
158                () = self.cancel_token.cancelled() => {
159                    debug!("motor adjustment task cancelled");
160                    break;
161                }
162                () = tokio::time::sleep(interval) => {}
163            }
164
165            let target_left = *self.target_left_speed.lock().await;
166            let target_right = *self.target_right_speed.lock().await;
167
168            current_left_speed = ramp_toward(current_left_speed, target_left, step);
169            current_right_speed = ramp_toward(current_right_speed, target_right, step);
170
171            let left_pwm = speed_to_pwm(current_left_speed);
172            let right_pwm = speed_to_pwm(current_right_speed);
173
174            let motor_board = self.motor_board.lock().await;
175
176            if let Err(e) = apply_motor_speed(
177                &*motor_board,
178                self.config.left_motor,
179                left_pwm,
180                current_left_speed,
181            ) {
182                warn!("left motor error: {e}");
183            }
184
185            if let Err(e) = apply_motor_speed(
186                &*motor_board,
187                self.config.right_motor,
188                right_pwm,
189                current_right_speed,
190            ) {
191                warn!("right motor error: {e}");
192            }
193        }
194    }
195}
196
197/// Ramp `current` toward `target` by at most `step`.
198fn ramp_toward(current: f32, target: f32, step: f32) -> f32 {
199    if (current - target).abs() > step {
200        if current < target {
201            current + step
202        } else {
203            current - step
204        }
205    } else {
206        target
207    }
208}
209
210/// Convert a normalized speed `[-1.0, 1.0]` to a PWM duty cycle `[0, 4095]`.
211fn speed_to_pwm(speed: f32) -> u16 {
212    #[expect(
213        clippy::cast_possible_truncation,
214        clippy::cast_sign_loss,
215        reason = "speed is clamped to [0.0, 1.0] after abs(), result fits in u16"
216    )]
217    let pwm = (speed.abs() * 4095.0).round() as u16;
218    pwm
219}
220
221/// Apply the computed PWM value to a motor, choosing direction or stopping.
222fn apply_motor_speed(
223    board: &dyn MotorDriver,
224    motor: Motor,
225    pwm: u16,
226    speed: f32,
227) -> Result<(), MotorError> {
228    if speed > 0.0 {
229        board.set_motor_speed(motor, pwm, true)
230    } else if speed < 0.0 {
231        board.set_motor_speed(motor, pwm, false)
232    } else {
233        board.stop_motor(motor)
234    }
235}
236
237#[cfg(test)]
238mod tests {
239    use super::*;
240    use crate::mock::{MockMotorDriver, MotorCommand};
241
242    #[test]
243    fn ramp_toward_moves_by_step() {
244        assert!((ramp_toward(0.0, 1.0, 0.1) - 0.1).abs() < f32::EPSILON);
245        assert!((ramp_toward(1.0, 0.0, 0.1) - 0.9).abs() < f32::EPSILON);
246    }
247
248    #[test]
249    fn ramp_toward_snaps_when_close() {
250        assert!((ramp_toward(0.95, 1.0, 0.1) - 1.0).abs() < f32::EPSILON);
251    }
252
253    #[test]
254    fn speed_to_pwm_converts_correctly() {
255        assert_eq!(speed_to_pwm(0.0), 0);
256        assert_eq!(speed_to_pwm(1.0), 4095);
257        assert_eq!(speed_to_pwm(-1.0), 4095);
258        assert_eq!(speed_to_pwm(0.5), 2048);
259    }
260
261    #[test]
262    fn apply_motor_speed_forward() {
263        let driver = MockMotorDriver::new();
264        apply_motor_speed(&driver, Motor::Motor1, 2048, 0.5).unwrap();
265        assert_eq!(
266            driver.commands(),
267            vec![MotorCommand::SetSpeed {
268                motor: Motor::Motor1,
269                speed: 2048,
270                forward: true,
271            }]
272        );
273    }
274
275    #[test]
276    fn apply_motor_speed_reverse() {
277        let driver = MockMotorDriver::new();
278        apply_motor_speed(&driver, Motor::Motor1, 2048, -0.5).unwrap();
279        assert_eq!(
280            driver.commands(),
281            vec![MotorCommand::SetSpeed {
282                motor: Motor::Motor1,
283                speed: 2048,
284                forward: false,
285            }]
286        );
287    }
288
289    #[test]
290    fn apply_motor_speed_stop() {
291        let driver = MockMotorDriver::new();
292        apply_motor_speed(&driver, Motor::Motor1, 0, 0.0).unwrap();
293        assert_eq!(
294            driver.commands(),
295            vec![MotorCommand::Stop {
296                motor: Motor::Motor1
297            }]
298        );
299    }
300
301    #[tokio::test]
302    async fn drive_clamps_input() {
303        let driver = Arc::new(Mutex::new(MockMotorDriver::new()));
304        let robot = Robot::new(driver.clone(), RobotConfig::default()).unwrap();
305
306        robot.drive(2.0, -3.0).await.unwrap();
307
308        let left = *robot.target_left_speed.lock().await;
309        let right = *robot.target_right_speed.lock().await;
310
311        // With default factors (1.0, 1.0), clamped to [-1.0, 1.0]
312        assert!((left - 1.0).abs() < f32::EPSILON);
313        assert!((right - (-1.0)).abs() < f32::EPSILON);
314
315        robot.shutdown().await;
316    }
317
318    #[tokio::test]
319    async fn stop_sets_zero_targets() {
320        let driver = Arc::new(Mutex::new(MockMotorDriver::new()));
321        let robot = Robot::new(driver.clone(), RobotConfig::default()).unwrap();
322
323        robot.drive(0.5, 0.5).await.unwrap();
324        robot.stop().await.unwrap();
325
326        let left = *robot.target_left_speed.lock().await;
327        let right = *robot.target_right_speed.lock().await;
328
329        assert!((left).abs() < f32::EPSILON);
330        assert!((right).abs() < f32::EPSILON);
331
332        robot.shutdown().await;
333    }
334
335    #[tokio::test]
336    async fn shutdown_stops_motors() {
337        let driver = Arc::new(Mutex::new(MockMotorDriver::new()));
338        let robot = Robot::new(driver.clone(), RobotConfig::default()).unwrap();
339
340        robot.shutdown().await;
341
342        // After shutdown, drive should return error
343        assert!(robot.drive(0.5, 0.5).await.is_err());
344    }
345
346    #[tokio::test]
347    async fn motor_factors_applied() {
348        let driver = Arc::new(Mutex::new(MockMotorDriver::new()));
349        let config = RobotConfig {
350            left_motor_factor: -1.0,
351            right_motor_factor: 1.0,
352            ..RobotConfig::default()
353        };
354        let robot = Robot::new(driver.clone(), config).unwrap();
355
356        robot.drive(0.5, 0.5).await.unwrap();
357
358        let left = *robot.target_left_speed.lock().await;
359        let right = *robot.target_right_speed.lock().await;
360
361        // Left factor is -1.0, so 0.5 * -1.0 = -0.5
362        assert!((left - (-0.5)).abs() < f32::EPSILON);
363        assert!((right - 0.5).abs() < f32::EPSILON);
364
365        robot.shutdown().await;
366    }
367}