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)]
24pub struct RobotConfig {
25 pub left_motor_factor: f32,
28 pub right_motor_factor: f32,
31 pub left_motor: Motor,
33 pub right_motor: Motor,
35 pub adjustment_step: f32,
37 pub adjustment_interval: Duration,
40}
41
42impl RobotConfig {
43 fn validate(&self) -> Result<(), RobotError> {
44 let invalid = |reason: &str| {
45 RobotError::Motor(MotorError::Init(format!("invalid RobotConfig: {reason}")))
46 };
47 if self.left_motor == self.right_motor {
48 return Err(invalid("left_motor and right_motor must differ"));
49 }
50 if self.adjustment_interval.is_zero() {
51 return Err(invalid("adjustment_interval must be positive"));
52 }
53 if !self.adjustment_step.is_finite() || self.adjustment_step <= 0.0 {
54 return Err(invalid("adjustment_step must be finite and positive"));
55 }
56 let rate = self.adjustment_step / self.adjustment_interval.as_secs_f32();
57 if !rate.is_finite() || rate <= 0.0 {
58 return Err(invalid(
59 "adjustment_step / adjustment_interval must yield a finite, positive f32 rate",
60 ));
61 }
62 if !self.left_motor_factor.is_finite() {
63 return Err(invalid("left_motor_factor must be finite"));
64 }
65 if !self.right_motor_factor.is_finite() {
66 return Err(invalid("right_motor_factor must be finite"));
67 }
68 Ok(())
69 }
70}
71
72impl Default for RobotConfig {
73 fn default() -> Self {
74 Self {
75 left_motor_factor: 1.0,
76 right_motor_factor: 1.0,
77 left_motor: Motor::Motor1,
78 right_motor: Motor::Motor2,
79 adjustment_step: 0.1,
80 adjustment_interval: Duration::from_millis(10),
81 }
82 }
83}
84
85pub struct Robot {
96 motor_board: Arc<Mutex<dyn MotorDriver>>,
97 target_speeds: Mutex<(f32, f32)>,
100 config: RobotConfig,
101 cancel_token: CancellationToken,
102}
103
104impl Robot {
105 pub fn new(
116 motor_board: Arc<Mutex<dyn MotorDriver>>,
117 config: RobotConfig,
118 ) -> Result<Arc<Self>, RobotError> {
119 config.validate()?;
120 let cancel_token = CancellationToken::new();
121 let robot = Arc::new(Self {
122 motor_board,
123 target_speeds: Mutex::new((0.0, 0.0)),
124 config,
125 cancel_token,
126 });
127
128 let robot_clone = Arc::clone(&robot);
129 tokio::spawn(async move {
130 robot_clone.smooth_motor_adjustment().await;
131 });
132
133 info!("robot initialized");
134 Ok(robot)
135 }
136
137 pub async fn drive(&self, left: f32, right: f32) -> Result<(), RobotError> {
152 let mut targets = self.target_speeds.lock().await;
153 if self.cancel_token.is_cancelled() {
154 return Err(RobotError::Shutdown);
155 }
156
157 if !left.is_finite() || !right.is_finite() {
158 return Err(MotorError::Pwm(
159 "invalid drive command: left and right must both be finite".into(),
160 )
161 .into());
162 }
163
164 let left = scaled_demand(left, self.config.left_motor_factor);
165 let right = scaled_demand(right, self.config.right_motor_factor);
166
167 *targets = (left, right);
168 drop(targets);
169
170 debug!(left, right, "drive target set");
171 Ok(())
172 }
173
174 pub async fn stop(&self) -> Result<(), RobotError> {
180 let mut targets = self.target_speeds.lock().await;
181 if self.cancel_token.is_cancelled() {
182 return Err(RobotError::Shutdown);
183 }
184
185 *targets = (0.0, 0.0);
186 drop(targets);
187
188 debug!("stop requested");
189 Ok(())
190 }
191
192 pub async fn shutdown(&self) {
194 info!("shutting down robot");
195 {
196 let _targets = self.target_speeds.lock().await;
199 self.cancel_token.cancel();
200 }
201
202 if let Err(e) = self.motor_board.lock().await.stop_all() {
203 warn!("failed to stop motors during shutdown: {e}");
204 }
205 }
206
207 async fn smooth_motor_adjustment(self: Arc<Self>) {
208 let mut current_left_speed: f32 = 0.0;
209 let mut current_right_speed: f32 = 0.0;
210 let step = self.config.adjustment_step;
211 let interval = self.config.adjustment_interval;
212
213 loop {
214 tokio::select! {
215 () = self.cancel_token.cancelled() => {
216 debug!("motor adjustment task cancelled");
217 break;
218 }
219 () = tokio::time::sleep(interval) => {}
220 }
221
222 let (target_left, target_right) = *self.target_speeds.lock().await;
223
224 current_left_speed = ramp_toward(current_left_speed, target_left, step);
225 current_right_speed = ramp_toward(current_right_speed, target_right, step);
226
227 let left_pwm = speed_to_pwm(current_left_speed);
228 let right_pwm = speed_to_pwm(current_right_speed);
229
230 let motor_board = self.motor_board.lock().await;
231
232 if let Err(e) = apply_motor_speed(
233 &*motor_board,
234 self.config.left_motor,
235 left_pwm,
236 current_left_speed,
237 ) {
238 warn!("left motor error: {e}");
239 }
240
241 if let Err(e) = apply_motor_speed(
242 &*motor_board,
243 self.config.right_motor,
244 right_pwm,
245 current_right_speed,
246 ) {
247 warn!("right motor error: {e}");
248 }
249 }
250 }
251}
252
253fn scaled_demand(input: f32, factor: f32) -> f32 {
255 let demand = f64::from(input.clamp(-1.0, 1.0)) * f64::from(factor);
256 #[expect(
257 clippy::cast_possible_truncation,
258 reason = "bounded demand fits in f32"
259 )]
260 let demand = demand.clamp(-1.0, 1.0) as f32;
261 demand
262}
263
264fn ramp_toward(current: f32, target: f32, step: f32) -> f32 {
266 if (current - target).abs() > step {
267 if current < target {
268 current + step
269 } else {
270 current - step
271 }
272 } else {
273 target
274 }
275}
276
277fn speed_to_pwm(speed: f32) -> u16 {
279 #[expect(
280 clippy::cast_possible_truncation,
281 clippy::cast_sign_loss,
282 reason = "speed is clamped to [0.0, 1.0] after abs(), result fits in u16"
283 )]
284 let pwm = (speed.abs() * 4095.0).round() as u16;
285 pwm
286}
287
288fn apply_motor_speed(
290 board: &dyn MotorDriver,
291 motor: Motor,
292 pwm: u16,
293 speed: f32,
294) -> Result<(), MotorError> {
295 if speed > 0.0 {
296 board.set_motor_speed(motor, pwm, true)
297 } else if speed < 0.0 {
298 board.set_motor_speed(motor, pwm, false)
299 } else {
300 board.stop_motor(motor)
301 }
302}
303
304#[cfg(test)]
305mod tests {
306 use super::*;
307 use crate::mock::{MockMotorDriver, MotorCommand};
308
309 #[test]
311 fn invalid_config_never_starts_controller() {
312 let mut cases = vec![
313 (
314 RobotConfig {
315 right_motor: Motor::Motor1,
316 ..RobotConfig::default()
317 },
318 "must differ",
319 ),
320 (
321 RobotConfig {
322 adjustment_interval: Duration::ZERO,
323 ..RobotConfig::default()
324 },
325 "adjustment_interval",
326 ),
327 (
328 RobotConfig {
329 adjustment_step: f32::MAX,
330 ..RobotConfig::default()
331 },
332 "rate",
333 ),
334 (
335 RobotConfig {
336 adjustment_step: f32::from_bits(1),
337 adjustment_interval: Duration::MAX,
338 ..RobotConfig::default()
339 },
340 "rate",
341 ),
342 ];
343 for step in [0.0, -0.1, f32::NAN, f32::INFINITY, f32::NEG_INFINITY] {
344 cases.push((
345 RobotConfig {
346 adjustment_step: step,
347 ..RobotConfig::default()
348 },
349 "adjustment_step",
350 ));
351 }
352 for factor in [f32::NAN, f32::INFINITY, f32::NEG_INFINITY] {
353 cases.push((
354 RobotConfig {
355 left_motor_factor: factor,
356 ..RobotConfig::default()
357 },
358 "left_motor_factor",
359 ));
360 cases.push((
361 RobotConfig {
362 right_motor_factor: factor,
363 ..RobotConfig::default()
364 },
365 "right_motor_factor",
366 ));
367 }
368 for (config, expected) in cases {
369 let driver = Arc::new(Mutex::new(MockMotorDriver::new()));
370 let Err(RobotError::Motor(MotorError::Init(message))) =
371 Robot::new(driver.clone(), config)
372 else {
373 panic!("expected configuration error for {expected}");
374 };
375 assert!(message.contains(expected), "{message}");
376 assert_eq!(Arc::strong_count(&driver), 1);
377 assert_eq!(driver.try_lock().unwrap().command_count(), 0);
378 }
379 }
380
381 #[tokio::test]
382 async fn invalid_drive_preserves_both_targets() {
383 let driver = Arc::new(Mutex::new(MockMotorDriver::new()));
384 let robot = Robot::new(driver.clone(), RobotConfig::default()).unwrap();
385 robot.drive(0.25, -0.5).await.unwrap();
386 for invalid in [f32::NAN, f32::INFINITY, f32::NEG_INFINITY] {
387 for (left, right) in [(invalid, 0.75), (0.75, invalid), (invalid, invalid)] {
388 assert!(matches!(
389 robot.drive(left, right).await,
390 Err(RobotError::Motor(MotorError::Pwm(_)))
391 ));
392 assert_eq!(*robot.target_speeds.lock().await, (0.25, -0.5));
393 }
394 }
395 robot.shutdown().await;
396 }
397
398 #[tokio::test]
399 async fn finite_factors_scale_then_saturate() {
400 for (factor, input, expected) in [
401 (2.0, 0.75, 1.0),
402 (-2.0, 0.75, -1.0),
403 (0.5, 2.0, 0.5), (0.0, f32::MAX, 0.0),
405 (f32::MAX, f32::MAX, 1.0),
406 (f32::MAX, -f32::MAX, -1.0),
407 (-f32::MAX, -f32::MAX, 1.0),
408 (f32::MAX, 0.0, 0.0),
409 ] {
410 let driver = Arc::new(Mutex::new(MockMotorDriver::new()));
411 let config = RobotConfig {
412 left_motor_factor: factor,
413 right_motor_factor: factor,
414 ..RobotConfig::default()
415 };
416 let robot = Robot::new(driver, config).unwrap();
417 robot.drive(input, input).await.unwrap();
418 assert_eq!(*robot.target_speeds.lock().await, (expected, expected));
419 robot.shutdown().await;
420 }
421 }
422
423 #[test]
424 fn ramp_toward_moves_by_step() {
425 assert!((ramp_toward(0.0, 1.0, 0.1) - 0.1).abs() < f32::EPSILON);
426 assert!((ramp_toward(1.0, 0.0, 0.1) - 0.9).abs() < f32::EPSILON);
427 }
428
429 #[test]
430 fn ramp_toward_snaps_when_close() {
431 assert!((ramp_toward(0.95, 1.0, 0.1) - 1.0).abs() < f32::EPSILON);
432 }
433
434 #[test]
435 fn speed_to_pwm_converts_correctly() {
436 assert_eq!(speed_to_pwm(0.0), 0);
437 assert_eq!(speed_to_pwm(1.0), 4095);
438 assert_eq!(speed_to_pwm(-1.0), 4095);
439 assert_eq!(speed_to_pwm(0.5), 2048);
440 }
441
442 #[test]
443 fn apply_motor_speed_forward() {
444 let driver = MockMotorDriver::new();
445 apply_motor_speed(&driver, Motor::Motor1, 2048, 0.5).unwrap();
446 assert_eq!(
447 driver.commands(),
448 vec![MotorCommand::SetSpeed {
449 motor: Motor::Motor1,
450 speed: 2048,
451 forward: true,
452 }]
453 );
454 }
455
456 #[test]
457 fn apply_motor_speed_reverse() {
458 let driver = MockMotorDriver::new();
459 apply_motor_speed(&driver, Motor::Motor1, 2048, -0.5).unwrap();
460 assert_eq!(
461 driver.commands(),
462 vec![MotorCommand::SetSpeed {
463 motor: Motor::Motor1,
464 speed: 2048,
465 forward: false,
466 }]
467 );
468 }
469
470 #[test]
471 fn apply_motor_speed_stop() {
472 let driver = MockMotorDriver::new();
473 apply_motor_speed(&driver, Motor::Motor1, 0, 0.0).unwrap();
474 assert_eq!(
475 driver.commands(),
476 vec![MotorCommand::Stop {
477 motor: Motor::Motor1
478 }]
479 );
480 }
481
482 #[tokio::test]
483 async fn drive_clamps_input() {
484 let driver = Arc::new(Mutex::new(MockMotorDriver::new()));
485 let robot = Robot::new(driver.clone(), RobotConfig::default()).unwrap();
486
487 robot.drive(2.0, -3.0).await.unwrap();
488
489 let (left, right) = *robot.target_speeds.lock().await;
490
491 assert!((left - 1.0).abs() < f32::EPSILON);
493 assert!((right - (-1.0)).abs() < f32::EPSILON);
494
495 robot.shutdown().await;
496 }
497
498 #[tokio::test]
499 async fn stop_sets_zero_targets() {
500 let driver = Arc::new(Mutex::new(MockMotorDriver::new()));
501 let robot = Robot::new(driver.clone(), RobotConfig::default()).unwrap();
502
503 robot.drive(0.5, 0.5).await.unwrap();
504 robot.stop().await.unwrap();
505
506 let (left, right) = *robot.target_speeds.lock().await;
507
508 assert!((left).abs() < f32::EPSILON);
509 assert!((right).abs() < f32::EPSILON);
510
511 robot.shutdown().await;
512 }
513
514 #[tokio::test(flavor = "multi_thread", worker_threads = 4)]
515 async fn concurrent_drive_stop_snapshots_are_complete_pairs() {
516 tokio::time::timeout(Duration::from_secs(5), async {
517 let driver = Arc::new(Mutex::new(MockMotorDriver::new()));
518 let robot = Robot::new(driver, RobotConfig::default()).unwrap();
519 let barrier = Arc::new(tokio::sync::Barrier::new(4));
520 let mut writers = tokio::task::JoinSet::new();
521 let commands = [Some((0.75, -0.25)), Some((-0.5, 1.0)), None];
522 let accepted_pairs = [(0.75, -0.25), (-0.5, 1.0), (0.0, 0.0)];
523
524 for command in commands {
525 let robot = robot.clone();
526 let barrier = barrier.clone();
527 writers.spawn(async move {
528 for _ in 0..128 {
529 barrier.wait().await;
530 match command {
531 Some((left, right)) => robot.drive(left, right).await.unwrap(),
532 None => robot.stop().await.unwrap(),
533 }
534 let snapshot = *robot.target_speeds.lock().await;
535 assert!(
536 accepted_pairs.contains(&snapshot),
537 "mixed pair: {snapshot:?}"
538 );
539 barrier.wait().await;
540 }
541 });
542 }
543
544 for _ in 0..128 {
547 barrier.wait().await;
548 let snapshot = *robot.target_speeds.lock().await;
549 assert!(
550 accepted_pairs.contains(&snapshot),
551 "mixed pair: {snapshot:?}"
552 );
553 barrier.wait().await;
554 assert!(accepted_pairs.contains(&*robot.target_speeds.lock().await));
555 }
556 while let Some(result) = writers.join_next().await {
557 result.unwrap();
558 }
559 robot.shutdown().await;
560 })
561 .await
562 .expect("concurrent commands deadlocked");
563 }
564
565 async fn assert_pending<F: std::future::Future>(mut future: std::pin::Pin<&mut F>) {
567 std::future::poll_fn(|cx| {
568 assert!(future.as_mut().poll(cx).is_pending());
569 std::task::Poll::Ready(())
570 })
571 .await;
572 }
573
574 #[tokio::test]
575 async fn waiting_commands_recheck_admission_after_shutdown() {
576 tokio::time::timeout(Duration::from_secs(5), async {
577 let driver = Arc::new(Mutex::new(MockMotorDriver::new()));
578 let robot = Robot::new(driver.clone(), RobotConfig::default()).unwrap();
579 robot.drive(0.25, -0.5).await.unwrap();
580 let targets = robot.target_speeds.lock().await;
581 let board = driver.lock().await;
584 let shutdown = robot.shutdown();
585 let drive = robot.drive(-0.75, 1.0);
586 let stop = robot.stop();
587 tokio::pin!(shutdown, drive, stop);
588
589 assert_pending(shutdown.as_mut()).await;
592 assert_pending(drive.as_mut()).await;
593 assert_pending(stop.as_mut()).await;
594 assert!(!robot.cancel_token.is_cancelled());
595 drop(targets);
596
597 assert_pending(shutdown.as_mut()).await;
598 assert!(robot.cancel_token.is_cancelled());
599 assert!(matches!(drive.await, Err(RobotError::Shutdown)));
600 assert!(matches!(stop.await, Err(RobotError::Shutdown)));
601 assert_eq!(*robot.target_speeds.lock().await, (0.25, -0.5));
602 drop(board);
603 shutdown.await;
604 })
605 .await
606 .expect("shutdown admission deadlocked");
607 }
608
609 #[tokio::test(start_paused = true)]
610 async fn adjustment_applies_complete_target_pairs() {
611 let driver = Arc::new(Mutex::new(MockMotorDriver::new()));
612 let robot = Robot::new(
613 driver.clone(),
614 RobotConfig {
615 adjustment_step: 2.0,
618 ..RobotConfig::default()
619 },
620 )
621 .unwrap();
622 for command in [Some((0.75, -0.25)), None, Some((-0.5, 1.0))] {
623 let (left, right) = if let Some((left, right)) = command {
624 robot.drive(left, right).await.unwrap();
625 (left, right)
626 } else {
627 robot.stop().await.unwrap();
628 (0.0, 0.0)
629 };
630 driver.lock().await.clear();
631 tokio::task::yield_now().await;
632 tokio::time::advance(Duration::from_millis(10)).await;
633 tokio::task::yield_now().await;
634
635 let expected = MockMotorDriver::new();
636 apply_motor_speed(&expected, Motor::Motor1, speed_to_pwm(left), left).unwrap();
637 apply_motor_speed(&expected, Motor::Motor2, speed_to_pwm(right), right).unwrap();
638 assert_eq!(driver.lock().await.commands(), expected.commands());
639 }
640 robot.shutdown().await;
641 }
642
643 #[tokio::test]
644 async fn shutdown_stops_motors() {
645 let driver = Arc::new(Mutex::new(MockMotorDriver::new()));
646 let robot = Robot::new(driver.clone(), RobotConfig::default()).unwrap();
647
648 robot.shutdown().await;
649
650 assert!(robot.drive(0.5, 0.5).await.is_err());
652 }
653
654 #[tokio::test]
655 async fn motor_factors_applied() {
656 let driver = Arc::new(Mutex::new(MockMotorDriver::new()));
657 let config = RobotConfig {
658 left_motor_factor: -1.0,
659 right_motor_factor: 1.0,
660 ..RobotConfig::default()
661 };
662 let robot = Robot::new(driver.clone(), config).unwrap();
663
664 robot.drive(0.5, 0.5).await.unwrap();
665
666 let (left, right) = *robot.target_speeds.lock().await;
667
668 assert!((left - (-0.5)).abs() < f32::EPSILON);
670 assert!((right - 0.5).abs() < f32::EPSILON);
671
672 robot.shutdown().await;
673 }
674}