1use 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#[derive(Debug, Clone)]
23pub struct RobotConfig {
24 pub left_motor_factor: f32,
26 pub right_motor_factor: f32,
28 pub left_motor: Motor,
30 pub right_motor: Motor,
32 pub adjustment_step: f32,
34 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
51pub 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 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 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 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 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
197fn 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
210fn 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
221fn 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 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 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 assert!((left - (-0.5)).abs() < f32::EPSILON);
363 assert!((right - 0.5).abs() < f32::EPSILON);
364
365 robot.shutdown().await;
366 }
367}