use std::{
sync::Arc,
time::{Duration, Instant},
};
use clap::{Parser, ValueEnum};
use robot_control::{
Motor, MotorDriver, MotorError, Pca9685MotorBoard, Robot, RobotConfig, RobotError,
};
use tokio::sync::Mutex;
#[derive(Parser)]
#[command(
version,
about = "Check controller validation and a bounded two-motor sequence"
)]
struct Cli {
#[arg(long, default_value = "/dev/i2c-1")]
i2c_bus: String,
#[arg(long, default_value_t = 96)]
i2c_addr: u8,
#[arg(long, default_value_t = 4, value_parser = clap::value_parser!(u8).range(1..=4))]
left_motor: u8,
#[arg(long, default_value_t = 1, value_parser = clap::value_parser!(u8).range(1..=4))]
right_motor: u8,
#[arg(long, default_value_t = 1.0, allow_hyphen_values = true)]
left_factor: f32,
#[arg(long, default_value_t = 1.0, allow_hyphen_values = true)]
right_factor: f32,
#[arg(long, default_value_t = 10)]
interval_ms: u64,
#[arg(long, default_value_t = 0.1, allow_hyphen_values = true)]
adjustment_step: f32,
#[arg(long)]
trace_output: bool,
#[arg(long, default_value_t = 0.25)]
power: f32,
#[arg(long)]
paired_targets: bool,
#[arg(long, value_enum)]
lifecycle_check: Option<LifecycleCheck>,
}
#[derive(Clone, Copy, ValueEnum)]
enum LifecycleCheck {
Forward,
Reverse,
Drop,
}
struct TracedBoard {
board: Pca9685MotorBoard,
started: Instant,
}
impl MotorDriver for TracedBoard {
fn set_motor_speed(&self, motor: Motor, speed: u16, forward: bool) -> Result<(), MotorError> {
let result = self.board.set_motor_speed(motor, speed, forward);
println!(
"output t={:.6}s motor={motor:?} pwm={speed} forward={forward} ok={}",
self.started.elapsed().as_secs_f64(),
result.is_ok()
);
result
}
fn stop_motor(&self, motor: Motor) -> Result<(), MotorError> {
let result = self.board.stop_motor(motor);
println!(
"output t={:.6}s motor={motor:?} pwm=0 stop ok={}",
self.started.elapsed().as_secs_f64(),
result.is_ok()
);
result
}
}
fn motor(number: u8) -> Motor {
match number {
1 => Motor::Motor1,
2 => Motor::Motor2,
3 => Motor::Motor3,
_ => Motor::Motor4, }
}
async fn sequence(
robot: &Robot,
power: f32,
paired_targets: bool,
started: Instant,
) -> Result<(), RobotError> {
for (name, left, right) in [
("zero", 0.0, 0.0),
("left only", power, 0.0),
("zero", 0.0, 0.0),
("right only", 0.0, power),
("forward", power, power),
("zero", 0.0, 0.0),
("reverse", -power, -power),
("zero", 0.0, 0.0),
] {
println!(
"{name}: left={left}, right={right} at t={:.6}s (one second including ramp)",
started.elapsed().as_secs_f64()
);
robot.drive(left, right).await?;
tokio::time::sleep(Duration::from_secs(1)).await;
println!("Controller status: {:?}", robot.status().await);
}
if paired_targets {
for (name, left, right) in [
("forward", power, power),
("pivot right", power, -power),
("forward", power, power),
("pivot left", -power, power),
("forward", power, power),
("zero", 0.0, 0.0),
] {
println!(
"paired targets / {name}: left={left}, right={right} at t={:.6}s (one second including ramp)",
started.elapsed().as_secs_f64()
);
robot.drive(left, right).await?;
tokio::time::sleep(Duration::from_secs(1)).await;
}
}
match robot.drive(power, f32::NAN).await {
Err(error) => println!("Expected invalid-input rejection: {error}"),
Ok(()) => {
return Err(
robot_control::MotorError::Pwm("invalid command was accepted".into()).into(),
);
}
}
tokio::time::sleep(Duration::from_secs(1)).await;
robot.stop().await
}
async fn interrupted() -> std::io::Result<()> {
#[cfg(unix)]
{
let mut terminate =
tokio::signal::unix::signal(tokio::signal::unix::SignalKind::terminate())?;
tokio::select! {
result = tokio::signal::ctrl_c() => result,
_ = terminate.recv() => Ok(()),
}
}
#[cfg(not(unix))]
tokio::signal::ctrl_c().await
}
async fn lifecycle_check(
robot: Arc<Robot>,
power: f32,
check: LifecycleCheck,
started: Instant,
) -> Result<(), Box<dyn std::error::Error>> {
let demand = if matches!(check, LifecycleCheck::Reverse) {
-power
} else {
power
};
println!(
"Lifecycle check: left={demand}, right={demand} at t={:.6}s for one second including ramp",
started.elapsed().as_secs_f64()
);
let result: Result<bool, Box<dyn std::error::Error>> = tokio::select! {
result = async {
robot.drive(demand, demand).await?;
tokio::time::sleep(Duration::from_secs(1)).await;
Ok::<_, RobotError>(true)
} => result.map_err(Into::into),
result = interrupted() => result.map(|()| false).map_err(Into::into),
};
if matches!(result, Ok(true)) && matches!(check, LifecycleCheck::Drop) {
drop(robot);
println!(
"Last Robot handle dropped; observing best-effort cleanup for two seconds with the runtime alive."
);
} else {
robot.shutdown().await;
robot.shutdown().await;
println!("Final controller status: {:?}", robot.status().await);
println!(
"Shutdown completed twice; observing outputs for two seconds. Check driver warnings."
);
}
tokio::time::sleep(Duration::from_secs(2)).await;
println!("Observation window finished; confirm outputs stayed off.");
result.map(|_| ())
}
#[tokio::main]
async fn main() -> Result<(), Box<dyn std::error::Error>> {
let cli = Cli::parse();
if !cli.power.is_finite() || cli.power <= 0.0 || cli.power > 1.0 {
return Err("--power must be finite and in (0, 1]".into());
}
if !(0.01..=0.1).contains(&cli.adjustment_step) {
return Err("--adjustment-step must be finite and in [0.01, 0.1] for this example".into());
}
if !(10..=20).contains(&cli.interval_ms) {
return Err("--interval-ms must be in [10, 20] for this example".into());
}
tracing_subscriber::fmt::init();
let board = Pca9685MotorBoard::new(&cli.i2c_bus, cli.i2c_addr)?;
board.stop_all()?;
let started = Instant::now();
let driver: Arc<Mutex<dyn MotorDriver>> = if cli.trace_output {
Arc::new(Mutex::new(TracedBoard { board, started }))
} else {
Arc::new(Mutex::new(board))
};
let config = RobotConfig {
left_motor: motor(cli.left_motor),
right_motor: motor(cli.right_motor),
left_motor_factor: cli.left_factor,
right_motor_factor: cli.right_factor,
adjustment_interval: Duration::from_millis(cli.interval_ms),
adjustment_step: cli.adjustment_step,
};
let robot = match Robot::new(driver.clone(), config) {
Ok(robot) => robot,
Err(error) => {
eprintln!("Controller rejected configuration: {error}");
driver.lock().await.stop_all()?;
return Err(error.into());
}
};
println!("Initial controller status: {:?}", robot.status().await);
let result: Result<bool, Box<dyn std::error::Error>> = tokio::select! {
result = sequence(&robot, cli.power, cli.paired_targets, started) => result.map(|()| true).map_err(Into::into),
result = interrupted() => result.map(|()| false).map_err(Into::into),
};
if matches!(result, Ok(true)) {
if let Some(check) = cli.lifecycle_check {
return lifecycle_check(robot, cli.power, check, started).await;
}
}
robot.shutdown().await;
println!("Final controller status: {:?}", robot.status().await);
println!("Shutdown completed; inspect the robot and any driver warnings.");
result.map(|_| ())
}