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_left_speed: Mutex<f32>,
98 target_right_speed: Mutex<f32>,
99 config: RobotConfig,
100 cancel_token: CancellationToken,
101}
102
103impl Robot {
104 pub fn new(
115 motor_board: Arc<Mutex<dyn MotorDriver>>,
116 config: RobotConfig,
117 ) -> Result<Arc<Self>, RobotError> {
118 config.validate()?;
119 let cancel_token = CancellationToken::new();
120 let robot = Arc::new(Self {
121 motor_board,
122 target_left_speed: Mutex::new(0.0),
123 target_right_speed: Mutex::new(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> {
149 if self.cancel_token.is_cancelled() {
150 return Err(RobotError::Shutdown);
151 }
152
153 if !left.is_finite() || !right.is_finite() {
154 return Err(MotorError::Pwm(
155 "invalid drive command: left and right must both be finite".into(),
156 )
157 .into());
158 }
159
160 let left = scaled_demand(left, self.config.left_motor_factor);
161 let right = scaled_demand(right, self.config.right_motor_factor);
162
163 *self.target_left_speed.lock().await = left;
164 *self.target_right_speed.lock().await = right;
165
166 debug!(left, right, "drive target set");
167 Ok(())
168 }
169
170 pub async fn stop(&self) -> Result<(), RobotError> {
176 if self.cancel_token.is_cancelled() {
177 return Err(RobotError::Shutdown);
178 }
179
180 *self.target_left_speed.lock().await = 0.0;
181 *self.target_right_speed.lock().await = 0.0;
182
183 debug!("stop requested");
184 Ok(())
185 }
186
187 pub async fn shutdown(&self) {
189 info!("shutting down robot");
190 self.cancel_token.cancel();
191
192 if let Err(e) = self.motor_board.lock().await.stop_all() {
193 warn!("failed to stop motors during shutdown: {e}");
194 }
195 }
196
197 async fn smooth_motor_adjustment(self: Arc<Self>) {
198 let mut current_left_speed: f32 = 0.0;
199 let mut current_right_speed: f32 = 0.0;
200 let step = self.config.adjustment_step;
201 let interval = self.config.adjustment_interval;
202
203 loop {
204 tokio::select! {
205 () = self.cancel_token.cancelled() => {
206 debug!("motor adjustment task cancelled");
207 break;
208 }
209 () = tokio::time::sleep(interval) => {}
210 }
211
212 let target_left = *self.target_left_speed.lock().await;
213 let target_right = *self.target_right_speed.lock().await;
214
215 current_left_speed = ramp_toward(current_left_speed, target_left, step);
216 current_right_speed = ramp_toward(current_right_speed, target_right, step);
217
218 let left_pwm = speed_to_pwm(current_left_speed);
219 let right_pwm = speed_to_pwm(current_right_speed);
220
221 let motor_board = self.motor_board.lock().await;
222
223 if let Err(e) = apply_motor_speed(
224 &*motor_board,
225 self.config.left_motor,
226 left_pwm,
227 current_left_speed,
228 ) {
229 warn!("left motor error: {e}");
230 }
231
232 if let Err(e) = apply_motor_speed(
233 &*motor_board,
234 self.config.right_motor,
235 right_pwm,
236 current_right_speed,
237 ) {
238 warn!("right motor error: {e}");
239 }
240 }
241 }
242}
243
244fn scaled_demand(input: f32, factor: f32) -> f32 {
246 let demand = f64::from(input.clamp(-1.0, 1.0)) * f64::from(factor);
247 #[expect(
248 clippy::cast_possible_truncation,
249 reason = "bounded demand fits in f32"
250 )]
251 let demand = demand.clamp(-1.0, 1.0) as f32;
252 demand
253}
254
255fn ramp_toward(current: f32, target: f32, step: f32) -> f32 {
257 if (current - target).abs() > step {
258 if current < target {
259 current + step
260 } else {
261 current - step
262 }
263 } else {
264 target
265 }
266}
267
268fn speed_to_pwm(speed: f32) -> u16 {
270 #[expect(
271 clippy::cast_possible_truncation,
272 clippy::cast_sign_loss,
273 reason = "speed is clamped to [0.0, 1.0] after abs(), result fits in u16"
274 )]
275 let pwm = (speed.abs() * 4095.0).round() as u16;
276 pwm
277}
278
279fn apply_motor_speed(
281 board: &dyn MotorDriver,
282 motor: Motor,
283 pwm: u16,
284 speed: f32,
285) -> Result<(), MotorError> {
286 if speed > 0.0 {
287 board.set_motor_speed(motor, pwm, true)
288 } else if speed < 0.0 {
289 board.set_motor_speed(motor, pwm, false)
290 } else {
291 board.stop_motor(motor)
292 }
293}
294
295#[cfg(test)]
296mod tests {
297 use super::*;
298 use crate::mock::{MockMotorDriver, MotorCommand};
299
300 #[test]
302 fn invalid_config_never_starts_controller() {
303 let mut cases = vec![
304 (
305 RobotConfig {
306 right_motor: Motor::Motor1,
307 ..RobotConfig::default()
308 },
309 "must differ",
310 ),
311 (
312 RobotConfig {
313 adjustment_interval: Duration::ZERO,
314 ..RobotConfig::default()
315 },
316 "adjustment_interval",
317 ),
318 (
319 RobotConfig {
320 adjustment_step: f32::MAX,
321 ..RobotConfig::default()
322 },
323 "rate",
324 ),
325 (
326 RobotConfig {
327 adjustment_step: f32::from_bits(1),
328 adjustment_interval: Duration::MAX,
329 ..RobotConfig::default()
330 },
331 "rate",
332 ),
333 ];
334 for step in [0.0, -0.1, f32::NAN, f32::INFINITY, f32::NEG_INFINITY] {
335 cases.push((
336 RobotConfig {
337 adjustment_step: step,
338 ..RobotConfig::default()
339 },
340 "adjustment_step",
341 ));
342 }
343 for factor in [f32::NAN, f32::INFINITY, f32::NEG_INFINITY] {
344 cases.push((
345 RobotConfig {
346 left_motor_factor: factor,
347 ..RobotConfig::default()
348 },
349 "left_motor_factor",
350 ));
351 cases.push((
352 RobotConfig {
353 right_motor_factor: factor,
354 ..RobotConfig::default()
355 },
356 "right_motor_factor",
357 ));
358 }
359 for (config, expected) in cases {
360 let driver = Arc::new(Mutex::new(MockMotorDriver::new()));
361 let Err(RobotError::Motor(MotorError::Init(message))) =
362 Robot::new(driver.clone(), config)
363 else {
364 panic!("expected configuration error for {expected}");
365 };
366 assert!(message.contains(expected), "{message}");
367 assert_eq!(Arc::strong_count(&driver), 1);
368 assert_eq!(driver.try_lock().unwrap().command_count(), 0);
369 }
370 }
371
372 #[tokio::test]
373 async fn invalid_drive_preserves_both_targets() {
374 let driver = Arc::new(Mutex::new(MockMotorDriver::new()));
375 let robot = Robot::new(driver.clone(), RobotConfig::default()).unwrap();
376 robot.drive(0.25, -0.5).await.unwrap();
377 for invalid in [f32::NAN, f32::INFINITY, f32::NEG_INFINITY] {
378 for (left, right) in [(invalid, 0.75), (0.75, invalid), (invalid, invalid)] {
379 assert!(matches!(
380 robot.drive(left, right).await,
381 Err(RobotError::Motor(MotorError::Pwm(_)))
382 ));
383 assert_eq!(*robot.target_left_speed.lock().await, 0.25);
384 assert_eq!(*robot.target_right_speed.lock().await, -0.5);
385 }
386 }
387 robot.shutdown().await;
388 }
389
390 #[tokio::test]
391 async fn finite_factors_scale_then_saturate() {
392 for (factor, input, expected) in [
393 (2.0, 0.75, 1.0),
394 (-2.0, 0.75, -1.0),
395 (0.5, 2.0, 0.5), (0.0, f32::MAX, 0.0),
397 (f32::MAX, f32::MAX, 1.0),
398 (f32::MAX, -f32::MAX, -1.0),
399 (-f32::MAX, -f32::MAX, 1.0),
400 (f32::MAX, 0.0, 0.0),
401 ] {
402 let driver = Arc::new(Mutex::new(MockMotorDriver::new()));
403 let config = RobotConfig {
404 left_motor_factor: factor,
405 right_motor_factor: factor,
406 ..RobotConfig::default()
407 };
408 let robot = Robot::new(driver, config).unwrap();
409 robot.drive(input, input).await.unwrap();
410 assert_eq!(*robot.target_left_speed.lock().await, expected);
411 assert_eq!(*robot.target_right_speed.lock().await, expected);
412 robot.shutdown().await;
413 }
414 }
415
416 #[test]
417 fn ramp_toward_moves_by_step() {
418 assert!((ramp_toward(0.0, 1.0, 0.1) - 0.1).abs() < f32::EPSILON);
419 assert!((ramp_toward(1.0, 0.0, 0.1) - 0.9).abs() < f32::EPSILON);
420 }
421
422 #[test]
423 fn ramp_toward_snaps_when_close() {
424 assert!((ramp_toward(0.95, 1.0, 0.1) - 1.0).abs() < f32::EPSILON);
425 }
426
427 #[test]
428 fn speed_to_pwm_converts_correctly() {
429 assert_eq!(speed_to_pwm(0.0), 0);
430 assert_eq!(speed_to_pwm(1.0), 4095);
431 assert_eq!(speed_to_pwm(-1.0), 4095);
432 assert_eq!(speed_to_pwm(0.5), 2048);
433 }
434
435 #[test]
436 fn apply_motor_speed_forward() {
437 let driver = MockMotorDriver::new();
438 apply_motor_speed(&driver, Motor::Motor1, 2048, 0.5).unwrap();
439 assert_eq!(
440 driver.commands(),
441 vec![MotorCommand::SetSpeed {
442 motor: Motor::Motor1,
443 speed: 2048,
444 forward: true,
445 }]
446 );
447 }
448
449 #[test]
450 fn apply_motor_speed_reverse() {
451 let driver = MockMotorDriver::new();
452 apply_motor_speed(&driver, Motor::Motor1, 2048, -0.5).unwrap();
453 assert_eq!(
454 driver.commands(),
455 vec![MotorCommand::SetSpeed {
456 motor: Motor::Motor1,
457 speed: 2048,
458 forward: false,
459 }]
460 );
461 }
462
463 #[test]
464 fn apply_motor_speed_stop() {
465 let driver = MockMotorDriver::new();
466 apply_motor_speed(&driver, Motor::Motor1, 0, 0.0).unwrap();
467 assert_eq!(
468 driver.commands(),
469 vec![MotorCommand::Stop {
470 motor: Motor::Motor1
471 }]
472 );
473 }
474
475 #[tokio::test]
476 async fn drive_clamps_input() {
477 let driver = Arc::new(Mutex::new(MockMotorDriver::new()));
478 let robot = Robot::new(driver.clone(), RobotConfig::default()).unwrap();
479
480 robot.drive(2.0, -3.0).await.unwrap();
481
482 let left = *robot.target_left_speed.lock().await;
483 let right = *robot.target_right_speed.lock().await;
484
485 assert!((left - 1.0).abs() < f32::EPSILON);
487 assert!((right - (-1.0)).abs() < f32::EPSILON);
488
489 robot.shutdown().await;
490 }
491
492 #[tokio::test]
493 async fn stop_sets_zero_targets() {
494 let driver = Arc::new(Mutex::new(MockMotorDriver::new()));
495 let robot = Robot::new(driver.clone(), RobotConfig::default()).unwrap();
496
497 robot.drive(0.5, 0.5).await.unwrap();
498 robot.stop().await.unwrap();
499
500 let left = *robot.target_left_speed.lock().await;
501 let right = *robot.target_right_speed.lock().await;
502
503 assert!((left).abs() < f32::EPSILON);
504 assert!((right).abs() < f32::EPSILON);
505
506 robot.shutdown().await;
507 }
508
509 #[tokio::test]
510 async fn shutdown_stops_motors() {
511 let driver = Arc::new(Mutex::new(MockMotorDriver::new()));
512 let robot = Robot::new(driver.clone(), RobotConfig::default()).unwrap();
513
514 robot.shutdown().await;
515
516 assert!(robot.drive(0.5, 0.5).await.is_err());
518 }
519
520 #[tokio::test]
521 async fn motor_factors_applied() {
522 let driver = Arc::new(Mutex::new(MockMotorDriver::new()));
523 let config = RobotConfig {
524 left_motor_factor: -1.0,
525 right_motor_factor: 1.0,
526 ..RobotConfig::default()
527 };
528 let robot = Robot::new(driver.clone(), config).unwrap();
529
530 robot.drive(0.5, 0.5).await.unwrap();
531
532 let left = *robot.target_left_speed.lock().await;
533 let right = *robot.target_right_speed.lock().await;
534
535 assert!((left - (-0.5)).abs() < f32::EPSILON);
537 assert!((right - 0.5).abs() < f32::EPSILON);
538
539 robot.shutdown().await;
540 }
541}