1use std::sync::Arc;
10use std::time::Duration;
11
12use tokio::sync::Mutex;
13use tokio::task::JoinHandle;
14use tokio_util::sync::CancellationToken;
15use tracing::{debug, info, warn};
16
17use crate::error::{MotorError, RobotError};
18use crate::motor::{Motor, MotorDriver};
19
20#[derive(Debug, Clone)]
25pub struct RobotConfig {
26 pub left_motor_factor: f32,
29 pub right_motor_factor: f32,
32 pub left_motor: Motor,
34 pub right_motor: Motor,
36 pub adjustment_step: f32,
38 pub adjustment_interval: Duration,
41}
42
43impl RobotConfig {
44 fn validate(&self) -> Result<(), RobotError> {
45 let invalid = |reason: &str| {
46 RobotError::Motor(MotorError::Init(format!("invalid RobotConfig: {reason}")))
47 };
48 if self.left_motor == self.right_motor {
49 return Err(invalid("left_motor and right_motor must differ"));
50 }
51 if self.adjustment_interval.is_zero() {
52 return Err(invalid("adjustment_interval must be positive"));
53 }
54 if !self.adjustment_step.is_finite() || self.adjustment_step <= 0.0 {
55 return Err(invalid("adjustment_step must be finite and positive"));
56 }
57 let rate = self.adjustment_step / self.adjustment_interval.as_secs_f32();
58 if !rate.is_finite() || rate <= 0.0 {
59 return Err(invalid(
60 "adjustment_step / adjustment_interval must yield a finite, positive f32 rate",
61 ));
62 }
63 if !self.left_motor_factor.is_finite() {
64 return Err(invalid("left_motor_factor must be finite"));
65 }
66 if !self.right_motor_factor.is_finite() {
67 return Err(invalid("right_motor_factor must be finite"));
68 }
69 Ok(())
70 }
71}
72
73impl Default for RobotConfig {
74 fn default() -> Self {
75 Self {
76 left_motor_factor: 1.0,
77 right_motor_factor: 1.0,
78 left_motor: Motor::Motor1,
79 right_motor: Motor::Motor2,
80 adjustment_step: 0.1,
81 adjustment_interval: Duration::from_millis(10),
82 }
83 }
84}
85
86pub struct Robot {
99 state: Arc<RobotState>,
100 cleanup: Mutex<Option<JoinHandle<()>>>,
103}
104
105struct RobotState {
107 motor_board: Arc<Mutex<dyn MotorDriver>>,
108 target_speeds: Mutex<(f32, f32)>,
111 config: RobotConfig,
112 cancel_token: CancellationToken,
113}
114
115impl Robot {
116 pub fn new(
128 motor_board: Arc<Mutex<dyn MotorDriver>>,
129 config: RobotConfig,
130 ) -> Result<Arc<Self>, RobotError> {
131 config.validate()?;
132 let cancel_token = CancellationToken::new();
133 let state = Arc::new(RobotState {
134 motor_board,
135 target_speeds: Mutex::new((0.0, 0.0)),
136 config,
137 cancel_token,
138 });
139
140 let adjustment = tokio::spawn(state.clone().smooth_motor_adjustment());
141 let cleanup_state = state.clone();
142 let cleanup = tokio::spawn(async move {
143 if let Err(error) = adjustment.await {
144 warn!("motor adjustment task failed: {error}");
145 }
146 cleanup_state.close_admission().await;
148 if let Err(error) = cleanup_state.motor_board.lock().await.stop_all() {
149 warn!("failed to stop motors during shutdown: {error}");
150 }
151 });
152 let robot = Arc::new(Self {
153 state,
154 cleanup: Mutex::new(Some(cleanup)),
155 });
156
157 info!("robot initialized");
158 Ok(robot)
159 }
160
161 pub async fn drive(&self, left: f32, right: f32) -> Result<(), RobotError> {
176 let mut targets = self.state.target_speeds.lock().await;
177 if self.state.cancel_token.is_cancelled() {
178 return Err(RobotError::Shutdown);
179 }
180
181 if !left.is_finite() || !right.is_finite() {
182 return Err(MotorError::Pwm(
183 "invalid drive command: left and right must both be finite".into(),
184 )
185 .into());
186 }
187
188 let left = scaled_demand(left, self.state.config.left_motor_factor);
189 let right = scaled_demand(right, self.state.config.right_motor_factor);
190
191 *targets = (left, right);
192 drop(targets);
193
194 debug!(left, right, "drive target set");
195 Ok(())
196 }
197
198 pub async fn stop(&self) -> Result<(), RobotError> {
204 let mut targets = self.state.target_speeds.lock().await;
205 if self.state.cancel_token.is_cancelled() {
206 return Err(RobotError::Shutdown);
207 }
208
209 *targets = (0.0, 0.0);
210 drop(targets);
211
212 debug!("stop requested");
213 Ok(())
214 }
215
216 pub async fn shutdown(&self) {
227 info!("shutting down robot");
228 self.state.close_admission().await;
229
230 let mut cleanup = self.cleanup.lock().await;
231 if let Some(task) = cleanup.as_mut() {
232 if let Err(error) = task.await {
235 warn!("robot cleanup task failed: {error}");
236 }
237 *cleanup = None;
238 }
239 }
240}
241
242impl Drop for Robot {
243 fn drop(&mut self) {
244 self.state.cancel_token.cancel();
248 }
249}
250
251impl RobotState {
252 async fn close_admission(&self) {
253 let _targets = self.target_speeds.lock().await;
255 self.cancel_token.cancel();
256 }
257
258 async fn smooth_motor_adjustment(self: Arc<Self>) {
259 let mut current_left_speed: f32 = 0.0;
260 let mut current_right_speed: f32 = 0.0;
261 let step = self.config.adjustment_step;
262 let interval = self.config.adjustment_interval;
263
264 loop {
265 tokio::select! {
266 biased;
267 () = self.cancel_token.cancelled() => {
268 debug!("motor adjustment task cancelled");
269 break;
270 }
271 () = tokio::time::sleep(interval) => {}
272 }
273
274 let (target_left, target_right) = *self.target_speeds.lock().await;
275
276 current_left_speed = ramp_toward(current_left_speed, target_left, step);
277 current_right_speed = ramp_toward(current_right_speed, target_right, step);
278
279 let left_pwm = speed_to_pwm(current_left_speed);
280 let right_pwm = speed_to_pwm(current_right_speed);
281
282 let motor_board = tokio::select! {
283 biased;
284 () = self.cancel_token.cancelled() => break,
285 board = self.motor_board.lock() => board,
286 };
287
288 if let Err(e) = apply_motor_speed(
289 &*motor_board,
290 self.config.left_motor,
291 left_pwm,
292 current_left_speed,
293 ) {
294 warn!("left motor error: {e}");
295 }
296
297 if let Err(e) = apply_motor_speed(
298 &*motor_board,
299 self.config.right_motor,
300 right_pwm,
301 current_right_speed,
302 ) {
303 warn!("right motor error: {e}");
304 }
305 }
306 }
307}
308
309fn scaled_demand(input: f32, factor: f32) -> f32 {
311 let demand = f64::from(input.clamp(-1.0, 1.0)) * f64::from(factor);
312 #[expect(
313 clippy::cast_possible_truncation,
314 reason = "bounded demand fits in f32"
315 )]
316 let demand = demand.clamp(-1.0, 1.0) as f32;
317 demand
318}
319
320fn ramp_toward(current: f32, target: f32, step: f32) -> f32 {
322 if (current - target).abs() > step {
323 if current < target {
324 current + step
325 } else {
326 current - step
327 }
328 } else {
329 target
330 }
331}
332
333fn speed_to_pwm(speed: f32) -> u16 {
335 #[expect(
336 clippy::cast_possible_truncation,
337 clippy::cast_sign_loss,
338 reason = "speed is clamped to [0.0, 1.0] after abs(), result fits in u16"
339 )]
340 let pwm = (speed.abs() * 4095.0).round() as u16;
341 pwm
342}
343
344fn apply_motor_speed(
346 board: &dyn MotorDriver,
347 motor: Motor,
348 pwm: u16,
349 speed: f32,
350) -> Result<(), MotorError> {
351 if speed > 0.0 {
352 board.set_motor_speed(motor, pwm, true)
353 } else if speed < 0.0 {
354 board.set_motor_speed(motor, pwm, false)
355 } else {
356 board.stop_motor(motor)
357 }
358}
359
360#[cfg(test)]
361mod lifecycle_tests;
362
363#[cfg(test)]
364mod tests {
365 use super::*;
366 use crate::mock::{MockMotorDriver, MotorCommand};
367
368 #[test]
370 fn invalid_config_never_starts_controller() {
371 let mut cases = vec![
372 (
373 RobotConfig {
374 right_motor: Motor::Motor1,
375 ..RobotConfig::default()
376 },
377 "must differ",
378 ),
379 (
380 RobotConfig {
381 adjustment_interval: Duration::ZERO,
382 ..RobotConfig::default()
383 },
384 "adjustment_interval",
385 ),
386 (
387 RobotConfig {
388 adjustment_step: f32::MAX,
389 ..RobotConfig::default()
390 },
391 "rate",
392 ),
393 (
394 RobotConfig {
395 adjustment_step: f32::from_bits(1),
396 adjustment_interval: Duration::MAX,
397 ..RobotConfig::default()
398 },
399 "rate",
400 ),
401 ];
402 for step in [0.0, -0.1, f32::NAN, f32::INFINITY, f32::NEG_INFINITY] {
403 cases.push((
404 RobotConfig {
405 adjustment_step: step,
406 ..RobotConfig::default()
407 },
408 "adjustment_step",
409 ));
410 }
411 for factor in [f32::NAN, f32::INFINITY, f32::NEG_INFINITY] {
412 cases.push((
413 RobotConfig {
414 left_motor_factor: factor,
415 ..RobotConfig::default()
416 },
417 "left_motor_factor",
418 ));
419 cases.push((
420 RobotConfig {
421 right_motor_factor: factor,
422 ..RobotConfig::default()
423 },
424 "right_motor_factor",
425 ));
426 }
427 for (config, expected) in cases {
428 let driver = Arc::new(Mutex::new(MockMotorDriver::new()));
429 let Err(RobotError::Motor(MotorError::Init(message))) =
430 Robot::new(driver.clone(), config)
431 else {
432 panic!("expected configuration error for {expected}");
433 };
434 assert!(message.contains(expected), "{message}");
435 assert_eq!(Arc::strong_count(&driver), 1);
436 assert_eq!(driver.try_lock().unwrap().command_count(), 0);
437 }
438 }
439
440 #[tokio::test]
441 async fn invalid_drive_preserves_both_targets() {
442 let driver = Arc::new(Mutex::new(MockMotorDriver::new()));
443 let robot = Robot::new(driver.clone(), RobotConfig::default()).unwrap();
444 robot.drive(0.25, -0.5).await.unwrap();
445 for invalid in [f32::NAN, f32::INFINITY, f32::NEG_INFINITY] {
446 for (left, right) in [(invalid, 0.75), (0.75, invalid), (invalid, invalid)] {
447 assert!(matches!(
448 robot.drive(left, right).await,
449 Err(RobotError::Motor(MotorError::Pwm(_)))
450 ));
451 assert_eq!(*robot.state.target_speeds.lock().await, (0.25, -0.5));
452 }
453 }
454 robot.shutdown().await;
455 }
456
457 #[tokio::test]
458 async fn finite_factors_scale_then_saturate() {
459 for (factor, input, expected) in [
460 (2.0, 0.75, 1.0),
461 (-2.0, 0.75, -1.0),
462 (0.5, 2.0, 0.5), (0.0, f32::MAX, 0.0),
464 (f32::MAX, f32::MAX, 1.0),
465 (f32::MAX, -f32::MAX, -1.0),
466 (-f32::MAX, -f32::MAX, 1.0),
467 (f32::MAX, 0.0, 0.0),
468 ] {
469 let driver = Arc::new(Mutex::new(MockMotorDriver::new()));
470 let config = RobotConfig {
471 left_motor_factor: factor,
472 right_motor_factor: factor,
473 ..RobotConfig::default()
474 };
475 let robot = Robot::new(driver, config).unwrap();
476 robot.drive(input, input).await.unwrap();
477 assert_eq!(
478 *robot.state.target_speeds.lock().await,
479 (expected, expected)
480 );
481 robot.shutdown().await;
482 }
483 }
484
485 #[test]
486 fn ramp_toward_moves_by_step() {
487 assert!((ramp_toward(0.0, 1.0, 0.1) - 0.1).abs() < f32::EPSILON);
488 assert!((ramp_toward(1.0, 0.0, 0.1) - 0.9).abs() < f32::EPSILON);
489 }
490
491 #[test]
492 fn ramp_toward_snaps_when_close() {
493 assert!((ramp_toward(0.95, 1.0, 0.1) - 1.0).abs() < f32::EPSILON);
494 }
495
496 #[test]
497 fn speed_to_pwm_converts_correctly() {
498 assert_eq!(speed_to_pwm(0.0), 0);
499 assert_eq!(speed_to_pwm(1.0), 4095);
500 assert_eq!(speed_to_pwm(-1.0), 4095);
501 assert_eq!(speed_to_pwm(0.5), 2048);
502 }
503
504 #[test]
505 fn apply_motor_speed_forward() {
506 let driver = MockMotorDriver::new();
507 apply_motor_speed(&driver, Motor::Motor1, 2048, 0.5).unwrap();
508 assert_eq!(
509 driver.commands(),
510 vec![MotorCommand::SetSpeed {
511 motor: Motor::Motor1,
512 speed: 2048,
513 forward: true,
514 }]
515 );
516 }
517
518 #[test]
519 fn apply_motor_speed_reverse() {
520 let driver = MockMotorDriver::new();
521 apply_motor_speed(&driver, Motor::Motor1, 2048, -0.5).unwrap();
522 assert_eq!(
523 driver.commands(),
524 vec![MotorCommand::SetSpeed {
525 motor: Motor::Motor1,
526 speed: 2048,
527 forward: false,
528 }]
529 );
530 }
531
532 #[test]
533 fn apply_motor_speed_stop() {
534 let driver = MockMotorDriver::new();
535 apply_motor_speed(&driver, Motor::Motor1, 0, 0.0).unwrap();
536 assert_eq!(
537 driver.commands(),
538 vec![MotorCommand::Stop {
539 motor: Motor::Motor1
540 }]
541 );
542 }
543
544 #[tokio::test]
545 async fn drive_clamps_input() {
546 let driver = Arc::new(Mutex::new(MockMotorDriver::new()));
547 let robot = Robot::new(driver.clone(), RobotConfig::default()).unwrap();
548
549 robot.drive(2.0, -3.0).await.unwrap();
550
551 let (left, right) = *robot.state.target_speeds.lock().await;
552
553 assert!((left - 1.0).abs() < f32::EPSILON);
555 assert!((right - (-1.0)).abs() < f32::EPSILON);
556
557 robot.shutdown().await;
558 }
559
560 #[tokio::test]
561 async fn stop_sets_zero_targets() {
562 let driver = Arc::new(Mutex::new(MockMotorDriver::new()));
563 let robot = Robot::new(driver.clone(), RobotConfig::default()).unwrap();
564
565 robot.drive(0.5, 0.5).await.unwrap();
566 robot.stop().await.unwrap();
567
568 let (left, right) = *robot.state.target_speeds.lock().await;
569
570 assert!((left).abs() < f32::EPSILON);
571 assert!((right).abs() < f32::EPSILON);
572
573 robot.shutdown().await;
574 }
575
576 #[tokio::test(flavor = "multi_thread", worker_threads = 4)]
577 async fn concurrent_drive_stop_snapshots_are_complete_pairs() {
578 tokio::time::timeout(Duration::from_secs(5), async {
579 let driver = Arc::new(Mutex::new(MockMotorDriver::new()));
580 let robot = Robot::new(driver, RobotConfig::default()).unwrap();
581 let barrier = Arc::new(tokio::sync::Barrier::new(4));
582 let mut writers = tokio::task::JoinSet::new();
583 let commands = [Some((0.75, -0.25)), Some((-0.5, 1.0)), None];
584 let accepted_pairs = [(0.75, -0.25), (-0.5, 1.0), (0.0, 0.0)];
585
586 for command in commands {
587 let robot = robot.clone();
588 let barrier = barrier.clone();
589 writers.spawn(async move {
590 for _ in 0..128 {
591 barrier.wait().await;
592 match command {
593 Some((left, right)) => robot.drive(left, right).await.unwrap(),
594 None => robot.stop().await.unwrap(),
595 }
596 let snapshot = *robot.state.target_speeds.lock().await;
597 assert!(
598 accepted_pairs.contains(&snapshot),
599 "mixed pair: {snapshot:?}"
600 );
601 barrier.wait().await;
602 }
603 });
604 }
605
606 for _ in 0..128 {
609 barrier.wait().await;
610 let snapshot = *robot.state.target_speeds.lock().await;
611 assert!(
612 accepted_pairs.contains(&snapshot),
613 "mixed pair: {snapshot:?}"
614 );
615 barrier.wait().await;
616 assert!(accepted_pairs.contains(&*robot.state.target_speeds.lock().await));
617 }
618 while let Some(result) = writers.join_next().await {
619 result.unwrap();
620 }
621 robot.shutdown().await;
622 })
623 .await
624 .expect("concurrent commands deadlocked");
625 }
626
627 async fn assert_pending<F: std::future::Future>(mut future: std::pin::Pin<&mut F>) {
629 std::future::poll_fn(|cx| {
630 assert!(future.as_mut().poll(cx).is_pending());
631 std::task::Poll::Ready(())
632 })
633 .await;
634 }
635
636 #[tokio::test]
637 async fn waiting_commands_recheck_admission_after_shutdown() {
638 tokio::time::timeout(Duration::from_secs(5), async {
639 let driver = Arc::new(Mutex::new(MockMotorDriver::new()));
640 let robot = Robot::new(driver.clone(), RobotConfig::default()).unwrap();
641 robot.drive(0.25, -0.5).await.unwrap();
642 let targets = robot.state.target_speeds.lock().await;
643 let board = driver.lock().await;
646 let shutdown = robot.shutdown();
647 let drive = robot.drive(-0.75, 1.0);
648 let stop = robot.stop();
649 tokio::pin!(shutdown, drive, stop);
650
651 assert_pending(shutdown.as_mut()).await;
654 assert_pending(drive.as_mut()).await;
655 assert_pending(stop.as_mut()).await;
656 assert!(!robot.state.cancel_token.is_cancelled());
657 drop(targets);
658
659 assert_pending(shutdown.as_mut()).await;
660 assert!(robot.state.cancel_token.is_cancelled());
661 assert!(matches!(drive.await, Err(RobotError::Shutdown)));
662 assert!(matches!(stop.await, Err(RobotError::Shutdown)));
663 assert_eq!(*robot.state.target_speeds.lock().await, (0.25, -0.5));
664 drop(board);
665 shutdown.await;
666 })
667 .await
668 .expect("shutdown admission deadlocked");
669 }
670
671 #[tokio::test(start_paused = true)]
672 async fn adjustment_applies_complete_target_pairs() {
673 let driver = Arc::new(Mutex::new(MockMotorDriver::new()));
674 let robot = Robot::new(
675 driver.clone(),
676 RobotConfig {
677 adjustment_step: 2.0,
680 ..RobotConfig::default()
681 },
682 )
683 .unwrap();
684 for command in [Some((0.75, -0.25)), None, Some((-0.5, 1.0))] {
685 let (left, right) = if let Some((left, right)) = command {
686 robot.drive(left, right).await.unwrap();
687 (left, right)
688 } else {
689 robot.stop().await.unwrap();
690 (0.0, 0.0)
691 };
692 driver.lock().await.clear();
693 tokio::task::yield_now().await;
694 tokio::time::advance(Duration::from_millis(10)).await;
695 tokio::task::yield_now().await;
696
697 let expected = MockMotorDriver::new();
698 apply_motor_speed(&expected, Motor::Motor1, speed_to_pwm(left), left).unwrap();
699 apply_motor_speed(&expected, Motor::Motor2, speed_to_pwm(right), right).unwrap();
700 assert_eq!(driver.lock().await.commands(), expected.commands());
701 }
702 robot.shutdown().await;
703 }
704
705 #[tokio::test]
706 async fn shutdown_stops_motors() {
707 let driver = Arc::new(Mutex::new(MockMotorDriver::new()));
708 let robot = Robot::new(driver.clone(), RobotConfig::default()).unwrap();
709
710 robot.shutdown().await;
711
712 assert!(robot.drive(0.5, 0.5).await.is_err());
714 }
715
716 #[tokio::test]
717 async fn motor_factors_applied() {
718 let driver = Arc::new(Mutex::new(MockMotorDriver::new()));
719 let config = RobotConfig {
720 left_motor_factor: -1.0,
721 right_motor_factor: 1.0,
722 ..RobotConfig::default()
723 };
724 let robot = Robot::new(driver.clone(), config).unwrap();
725
726 robot.drive(0.5, 0.5).await.unwrap();
727
728 let (left, right) = *robot.state.target_speeds.lock().await;
729
730 assert!((left - (-0.5)).abs() < f32::EPSILON);
732 assert!((right - 0.5).abs() < f32::EPSILON);
733
734 robot.shutdown().await;
735 }
736}