//! 底盘控制循环:麦轮逆解 → 四电机速度指令,含安全逻辑。 use std::time::Duration; use mecanum::{Mecanum, Twist}; use motor_core::{Disable, MotorFeedback, OutputShaft, VelocityDrive}; use tokio::sync::watch; use tokio::task::JoinHandle; use tokio::time::{Instant, MissedTickBehavior, interval}; use tracing::{info, warn}; /// 底盘控制器。 /// /// 职责(1kHz 节拍): /// 1. 读取最新 twist 指令,**指令超时则归零**; /// 2. 速度限幅(保持方向按比例收缩); /// 3. **上电门控**:四台电机都收到过反馈才下发速度,否则全部停用; /// 4. 麦轮逆解 → 各轮目标转速(rpm,轮端)→ 经 [`OutputShaft`] 换算到转子。 /// /// 电机反馈离线安全在驱动层完成(强制 0 电流)。 pub struct ChassisController { kinematics: Mecanum, /// 顺序固定:FL、FR、RL、RR。轮端单位(经 `OutputShaft` 包装)。 wheels: [W; 4], /// (max_vx, max_vy, max_omega)。 limits: (f32, f32, f32), command_timeout: Duration, } impl ChassisController where W: VelocityDrive + MotorFeedback + Disable + 'static, { /// 组装控制器。`wheels` 顺序必须为 FL、FR、RL、RR。 #[must_use] pub fn new( kinematics: Mecanum, wheels: [W; 4], limits: (f32, f32, f32), command_timeout: Duration, ) -> Self { Self { kinematics, wheels, limits, command_timeout } } /// 启动控制循环任务。 pub fn spawn(self, commands: watch::Receiver, period: Duration) -> JoinHandle<()> { tokio::spawn(self.run(commands, period)) } async fn run(self, mut commands: watch::Receiver, period: Duration) { let mut ticker = interval(period); ticker.set_missed_tick_behavior(MissedTickBehavior::Skip); let mut latest = Twist::ZERO; let mut last_command_at: Option = None; let mut ticks: u64 = 0; loop { ticker.tick().await; let now = Instant::now(); if commands.has_changed().unwrap_or(false) { latest = *commands.borrow_and_update(); last_command_at = Some(now); } let timed_out = match last_command_at { Some(t) => now.duration_since(t) > self.command_timeout, // 尚未收到任何指令:保持静止。 None => true, }; let twist = if timed_out { Twist::ZERO } else { latest.clamped(self.limits.0, self.limits.1, self.limits.2) }; let all_seen_feedback = self.wheels.iter().all(|m| m.state().has_feedback()); if all_seen_feedback { let wheel_rpms = self.kinematics.wheel_rpms(&twist); for (motor, wheel_rpm) in self.wheels.iter().zip(wheel_rpms) { motor.set_velocity(wheel_rpm); } } else { for motor in &self.wheels { motor.disable(); } if ticks.is_multiple_of(1000) { warn!("waiting for first feedback from all motors"); } } ticks += 1; if ticks.is_multiple_of(1000) { self.log_status(); } } } fn log_status(&self) { let (rotor_rpm, effort_raw): (Vec, Vec) = self .wheels .iter() .map(|m| { let st = m.state(); (st.rotor_speed_rpm, i32::from(st.effort_raw)) }) .unzip(); info!(?rotor_rpm, ?effort_raw, "chassis status"); } } /// 由 `EscMotor` 列表组装轮端包装(`gear_ratio` = 转子 : 输出轴)。 #[must_use] pub fn wrap_wheels( motors: [T; N], gear_ratios: [f32; N], ) -> [OutputShaft; N] { let mut out = Vec::with_capacity(N); let mut gears = gear_ratios.into_iter(); for m in motors { let g = gears.next().unwrap_or(1.0); out.push(OutputShaft::new(m, g)); } out.try_into().unwrap_or_else(|_| unreachable!()) } #[cfg(test)] mod tests { use super::*; use motor_core::{Command, MockMotor, MotorState}; const GEAR: f32 = 19.203; fn mock_wheel(id: u8) -> MockMotor { let m = MockMotor::new(id); m.inject_state(MotorState { feedback_count: 1, ..MotorState::default() }); m } /// 证明 `ChassisController` 只依赖能力 trait:换 `MockMotor` 原样工作。 #[tokio::test(start_paused = true)] async fn controller_drives_wheels_through_capability_traits() { let kinematics = Mecanum::new(0.2, 0.25, 0.0762); let mocks = [mock_wheel(1), mock_wheel(2), mock_wheel(3), mock_wheel(4)]; let ctrl = ChassisController::new( kinematics, wrap_wheels(mocks.clone(), [GEAR; 4]), (3.0, 3.0, 6.0), Duration::from_millis(200), ); let (tx, rx) = watch::channel(Twist::ZERO); let handle = ctrl.spawn(rx, Duration::from_millis(1)); let twist = Twist { vx: 1.0, ..Twist::ZERO }; tx.send_replace(twist); tokio::time::sleep(Duration::from_millis(5)).await; let expected = kinematics.wheel_rpms(&twist); for (i, mock) in mocks.iter().enumerate() { match mock.last_command() { Command::Velocity(rotor_rpm) => { let want = expected[i] * GEAR; assert!( (rotor_rpm - want).abs() < 1e-2, "wheel {i}: rotor_rpm {rotor_rpm} != {want}" ); } ref cmd => panic!("wheel {i} expected Velocity, got {cmd:?}"), } } handle.abort(); } /// 反馈未到齐时必须全部停用,不得下发速度。 #[tokio::test(start_paused = true)] async fn disables_wheels_until_all_feedback_seen() { let kinematics = Mecanum::new(0.2, 0.25, 0.0762); let ready = mock_wheel(1); let missing = MockMotor::new(2); // 无反馈 let mocks_ready = [ready.clone(), mock_wheel(3), mock_wheel(4)]; let _ = mocks_ready; let wheels = wrap_wheels( [ready.clone(), missing.clone(), mock_wheel(3), mock_wheel(4)], [GEAR; 4], ); let ctrl = ChassisController::new( kinematics, wheels, (3.0, 3.0, 6.0), Duration::from_millis(200), ); let (tx, rx) = watch::channel(Twist::ZERO); let handle = ctrl.spawn(rx, Duration::from_millis(1)); tx.send_replace(Twist { vx: 1.0, ..Twist::ZERO }); tokio::time::sleep(Duration::from_millis(5)).await; assert_eq!(ready.last_command(), Command::Disabled); assert_eq!(missing.last_command(), Command::Disabled); handle.abort(); } }