refactor: enforce layered crates and Rust audit gates

This commit is contained in:
Horldsence
2026-09-24 13:00:03 +08:00
parent af151525d1
commit 8a582fc102
67 changed files with 3918 additions and 581 deletions
+21
View File
@@ -0,0 +1,21 @@
[package]
name = "motor"
description = "Protocol-independent motor device contract and registry"
version.workspace = true
edition.workspace = true
rust-version.workspace = true
license.workspace = true
[lints]
workspace = true
[dependencies]
can = { workspace = true }
motor-protocol = { workspace = true }
thiserror = { workspace = true }
[dev-dependencies]
approx = { workspace = true }
[package.metadata.sentry-chassis]
layer = "device/motor"
+912
View File
@@ -0,0 +1,912 @@
#![cfg_attr(test, allow(clippy::unwrap_used, clippy::expect_used))]
//! Protocol-independent motor device contracts.
pub mod traits {
//! 能力面 trait:小而正交,按硬件事实拆分。
//!
//! 消费者只声明自己用到的能力(静态分发,无 `dyn`):
//! - 底盘轮 → [`VelocityDrive`] + [`MotorFeedback`]
//! - 云台 → [`PositionDrive`] + [`EffortDrive`](+ 上层 [`crate::HeadingSource`])
//! - 拨弹 → [`VelocityDrive`] + [`PositionDrive`]
//! - 台架 → [`VelocityDrive`] + [`MotorFeedback`] + [`Disable`]
use std::time::Duration;
use crate::{Command, MotorState};
/// 读反馈。所有可观察设备的最小面。
pub trait MotorFeedback: Send + Sync {
/// 控制器内的电机 ID(对 DJI 电调即电调 ID)。
fn id(&self) -> u8;
/// 最新反馈快照。
fn state(&self) -> MotorState;
/// 距最近一帧反馈的时长;`None` 表示从未收到。
///
/// 用于在线判定(反馈新鲜度),与 [`MotorState::has_feedback`]
/// (历史上是否收到过)区分:上电门控之后运行中掉线要看本方法。
fn feedback_age(&self) -> Option<Duration>;
}
/// 开环控制量(协议 raw:ESC=电流,GM6020=电压)。
pub trait EffortDrive: Send + Sync {
/// 直接下发开环 raw(协议量程,物理方向)。
fn set_effort_raw(&self, raw: i16);
}
/// 速度目标(转子 rpm,物理方向);由驱动层速度环跟踪。
pub trait VelocityDrive: Send + Sync {
/// 设定目标转子转速。
fn set_velocity(&self, rotor_rpm: f32);
}
/// 位置目标(转子多圈,圈;物理方向)。
pub trait PositionDrive: Send + Sync {
/// 设定目标转子多圈位置。
fn set_position(&self, rotor_rev: f32);
}
/// 停用(零输出)。
pub trait Disable: Send + Sync {
/// 停用电机。
fn disable(&self);
}
/// 便利别名(blanket impl,零成本)。实现方**不必**实现它。
pub trait Motor: MotorFeedback + VelocityDrive + EffortDrive + Disable {}
impl<T> Motor for T where T: MotorFeedback + VelocityDrive + EffortDrive + Disable {}
/// One motor's data for one control-policy evaluation.
#[derive(Debug, Clone, Copy)]
pub struct PolicySample {
/// ESC identifier.
pub id: u8,
/// Latest direction-corrected physical feedback.
pub state: MotorState,
/// Latest command lease value.
pub command: Command,
/// Whether fresh feedback is available.
pub online: bool,
}
/// Snapshot passed to an external effort policy once per bus tick.
#[derive(Debug, Clone, Copy)]
pub struct PolicyInput {
/// Samples indexed by ESC id minus one.
pub samples: [PolicySample; 8],
/// Measured control period in seconds.
pub dt: f32,
}
/// Computes raw physical effort from motor commands and feedback.
///
/// `esc-bus` owns transport and safety but deliberately has no control-law
/// dependency. Control crates implement this trait and the application composes
/// the two sides.
pub trait EffortPolicy: Send + 'static {
/// Return physical effort raw values indexed by ESC id minus one.
fn step(&mut self, input: &PolicyInput) -> [i16; 8];
/// Clear controller state after an offline event or command-mode change.
fn reset(&mut self, esc_id: u8);
}
}
pub mod state {
//! 电机反馈快照(协议无关,方向修正后的物理量)。
//!
//! 只含**转子轴**物理量——减速比 / 输出轴换算由 [`crate::gearbox::OutputShaft`]
//! 或上层完成,不在此泄漏底盘语义。
/// 电机反馈快照。
#[derive(Debug, Clone, Copy, PartialEq, Default)]
pub struct MotorState {
/// 最近一帧反馈的转子机械原始角(编码器计数,量程由具体协议决定)。
pub rotor_angle_raw: u16,
/// 连续累计转子角(多圈回绕处理,单位:圈;随时间单调于转动方向)。
pub rotor_revolutions: f32,
/// 转子转速(rpm,物理方向)。
pub rotor_speed_rpm: f32,
/// 开环控制量反馈 raw(ESC=转矩电流,GM6020=电流相关量;物理方向)。
pub effort_raw: i16,
/// 电机温度(℃)。
pub temperature_c: i8,
/// 已收到的反馈帧总数;`0` 表示尚未收到任何反馈(上电门控用)。
pub feedback_count: u64,
}
impl MotorState {
/// 是否已收到过反馈(上电后首帧门控)。
#[must_use]
pub fn has_feedback(&self) -> bool {
self.feedback_count > 0
}
}
}
pub mod command {
//! 驱动层命令寄存器(最后写入胜出)。
/// 发给驱动层的电机命令。
#[derive(Debug, Clone, Copy, PartialEq)]
pub enum Command {
/// 停用:目标清零,下发 0 输出。
Disabled,
/// 直接下发开环 raw(协议量程,物理方向),绕过闭环。
/// ESC = 电流 raw;GM6020 = 电压 raw。
EffortRaw(i16),
/// 速度环跟踪目标转子转速(rpm,物理方向)。
Velocity(f32),
/// 位置环跟踪目标转子多圈位置(圈,物理方向)。
Position(f32),
}
/// 命令模式标签(不含设定值)——用于切换时复位控制器。
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum CommandMode {
/// 停用。
Disabled,
/// 开环 raw。
EffortRaw,
/// 速度环。
Velocity,
/// 位置环。
Position,
}
impl Command {
/// 模式标签。
#[must_use]
pub const fn mode(self) -> CommandMode {
match self {
Self::Disabled => CommandMode::Disabled,
Self::EffortRaw(_) => CommandMode::EffortRaw,
Self::Velocity(_) => CommandMode::Velocity,
Self::Position(_) => CommandMode::Position,
}
}
}
}
pub mod gearbox {
//! 输出轴单位适配器:减速比换算。
//!
//! 把「输出轴单位」的驱动接口适配到「转子单位」的底层电机。
//! `gear_ratio = 转子 : 输出轴`(GM3508 ≈ 19.203)。
use std::time::Duration;
use crate::{Disable, EffortDrive, MotorFeedback, MotorState, PositionDrive, VelocityDrive};
/// 输出轴包装:对外接口使用输出轴单位,对内换算到转子单位。
pub struct OutputShaft<T> {
inner: T,
gear_ratio: f32,
}
impl<T> OutputShaft<T> {
/// 包装底层电机。`gear_ratio` = 转子 : 输出轴(> 0)。
///
/// # Panics
///
/// `gear_ratio` 非有限或 ≤ 0 时 panic。
#[must_use]
pub fn new(inner: T, gear_ratio: f32) -> Self {
assert!(
gear_ratio.is_finite() && gear_ratio > 0.0,
"gear_ratio must be > 0, got {gear_ratio}"
);
Self { inner, gear_ratio }
}
/// 减速比。
#[must_use]
pub fn gear_ratio(&self) -> f32 {
self.gear_ratio
}
/// 底层电机引用。
#[must_use]
pub fn inner(&self) -> &T {
&self.inner
}
/// 转子量 → 输出轴量。
#[must_use]
pub fn to_output(&self, rotor: f32) -> f32 {
rotor / self.gear_ratio
}
/// 输出轴量 → 转子量。
#[must_use]
pub fn to_rotor(&self, output: f32) -> f32 {
output * self.gear_ratio
}
/// 由反馈快照推算输出轴转速(rpm)。
#[must_use]
pub fn output_speed_rpm(&self, state: &MotorState) -> f32 {
state.rotor_speed_rpm / self.gear_ratio
}
}
impl<T: MotorFeedback> MotorFeedback for OutputShaft<T> {
fn id(&self) -> u8 {
self.inner.id()
}
fn state(&self) -> MotorState {
// 只透传转子量;不伪造输出轴字段。
self.inner.state()
}
fn feedback_age(&self) -> Option<Duration> {
self.inner.feedback_age()
}
}
impl<T: VelocityDrive> VelocityDrive for OutputShaft<T> {
/// `output_rpm` 为**输出轴**转速。
fn set_velocity(&self, output_rpm: f32) {
self.inner.set_velocity(self.to_rotor(output_rpm));
}
}
impl<T: EffortDrive> EffortDrive for OutputShaft<T> {
fn set_effort_raw(&self, raw: i16) {
self.inner.set_effort_raw(raw);
}
}
impl<T: PositionDrive> PositionDrive for OutputShaft<T> {
/// `output_rev` 为**输出轴**圈数。
fn set_position(&self, output_rev: f32) {
self.inner.set_position(self.to_rotor(output_rev));
}
}
impl<T: Disable> Disable for OutputShaft<T> {
fn disable(&self) {
self.inner.disable();
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::Command;
struct Stub {
cmd: std::sync::Mutex<Command>,
}
impl Stub {
fn new() -> Self {
Self { cmd: std::sync::Mutex::new(Command::Disabled) }
}
}
impl MotorFeedback for Stub {
fn id(&self) -> u8 {
1
}
fn state(&self) -> MotorState {
MotorState::default()
}
fn feedback_age(&self) -> Option<Duration> {
Some(Duration::ZERO)
}
}
impl VelocityDrive for Stub {
fn set_velocity(&self, rotor_rpm: f32) {
*self.cmd.lock().unwrap() = Command::Velocity(rotor_rpm);
}
}
impl EffortDrive for Stub {
fn set_effort_raw(&self, raw: i16) {
*self.cmd.lock().unwrap() = Command::EffortRaw(raw);
}
}
impl PositionDrive for Stub {
fn set_position(&self, rotor_rev: f32) {
*self.cmd.lock().unwrap() = Command::Position(rotor_rev);
}
}
impl Disable for Stub {
fn disable(&self) {
*self.cmd.lock().unwrap() = Command::Disabled;
}
}
#[test]
fn velocity_scales_by_gear() {
let shaft = OutputShaft::new(Stub::new(), 19.203);
shaft.set_velocity(100.0);
match *shaft.inner().cmd.lock().unwrap() {
Command::Velocity(rpm) => assert!((rpm - 1920.3).abs() < 0.1, "{rpm}"),
ref c => panic!("unexpected {c:?}"),
}
}
#[test]
fn position_scales_by_gear() {
let shaft = OutputShaft::new(Stub::new(), 36.0);
shaft.set_position(2.0);
match *shaft.inner().cmd.lock().unwrap() {
Command::Position(rev) => assert!((rev - 72.0).abs() < 1e-3, "{rev}"),
ref c => panic!("unexpected {c:?}"),
}
}
#[test]
fn effort_passes_through() {
let shaft = OutputShaft::new(Stub::new(), 10.0);
shaft.set_effort_raw(-1234);
assert_eq!(*shaft.inner().cmd.lock().unwrap(), Command::EffortRaw(-1234));
}
#[test]
#[should_panic = "gear_ratio"]
fn rejects_nonpositive_gear() {
let _ = OutputShaft::new(Stub::new(), 0.0);
}
}
}
pub mod heading {
//! 航向观测源(IMU / 磁力计 / 视觉等)——云台绝对角融合 seam。
//!
//! 电机抽象只保证 [`crate::MotorState::rotor_revolutions`] 连续(上电相对)。
//! 场地绝对角由上层(未来 `gimbal` crate)融合 IMU 与电机多圈得到。
use std::future::Future;
/// 绝对航向观测源。
pub trait HeadingSource: Send {
/// 机体航向角(rad,连续;与 chassis ISO 约定一致:x 前、逆时针为正)。
fn heading_rad(&self) -> impl Future<Output = f32> + Send;
}
/// 云台目标角的参考系。
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum HeadingFrame {
/// 电机转子多圈(上电相对)。
RotorMultiTurn,
/// 场地绝对角(经 [`HeadingSource`] 对齐到转子基准后再下发)。
AbsoluteField,
}
}
pub mod registry {
//! 电机注册表:**按总线 + 反馈帧 ID** 找回设备(ARCHITECTURE.md §5.4)。
//!
//! # 为什么需要它
//!
//! 单总线底盘上,`EscBus` 自己就能把反馈帧分发给对应的 `EscMotor`。但整机是
//! **双 CAN**(§2.1:底盘 `can0` + 功能总线 `can1`),云台 / 拨弹的帧与底盘的
//! 帧可能落在**相同的 ID** 上(GM6020 与 C620 都可能是 0x2xx)。此时"这帧是谁的"
//! 只能由 `(总线, 帧 ID)` 共同决定。
//!
//! [`MotorRegistry`] 就是这张映射表:注册时校验冲突,分发时 O(1) 命中。
//! 当前 `chassisd` 的单总线底盘路径不经过它;它是云台 / 拨弹接入功能总线时的
//! seam(先有接口与测试,避免将来把分发逻辑硬编码进每个任务)。
use can::CanFrame;
use motor_protocol::feedback_frame_id;
/// 物理总线标识(ARCHITECTURE.md §2.1 的双 CAN 拓扑)。
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, PartialOrd, Ord)]
pub enum BusId {
/// 底盘总线(C620/C610 驱动轮)。
Chassis,
/// 功能总线(云台 GM6020 + 拨弹 M2006/C610)。
Function,
}
impl BusId {
/// 文本名。
#[must_use]
pub const fn name(self) -> &'static str {
match self {
Self::Chassis => "chassis",
Self::Function => "function",
}
}
}
/// 注册表条目。
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct RegisteredMotor {
/// 所属总线。
pub bus: BusId,
/// 反馈帧 ID。
pub frame_id: u32,
/// 轮名 / 设备名(配置里的 `name`)。
pub name: String,
}
/// 注册错误。
///
/// 手写 `Display` / `Error` 而不是 `derive(thiserror::Error)`:注册表属于
/// **总是可用**的设备抽象(关掉 `bus` feature 也要能编译),不应为一个派生宏
/// 引入可选依赖。
#[derive(Debug, Clone, PartialEq, Eq)]
pub enum RegistryError {
/// 同一 `(总线, 帧 ID)` 被注册两次——几乎总是接线或配置写重了。
Duplicate {
/// 总线。
bus: &'static str,
/// 冲突的帧 ID。
frame_id: u32,
/// 先注册的名字。
existing: String,
/// 后注册的名字。
name: String,
},
/// 电调 ID 越界(1–8)。
BadEscId(u8),
}
impl std::fmt::Display for RegistryError {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {
Self::Duplicate { bus, frame_id, existing, name } => write!(
f,
"总线 {bus} 上的帧 ID 0x{frame_id:03X} 已被 '{existing}' 占用,无法再注册 '{name}'"
),
Self::BadEscId(id) => write!(f, "电调 ID {id} 越界(合法范围 1–8)"),
}
}
}
impl std::error::Error for RegistryError {}
/// 设备注册表。
#[derive(Debug, Default, Clone)]
pub struct MotorRegistry {
entries: Vec<RegisteredMotor>,
}
impl MotorRegistry {
/// 空表。
#[must_use]
pub fn new() -> Self {
Self::default()
}
/// 按电调 ID 注册(反馈帧 ID = `0x200 + 电调ID`,C620/C610 约定)。
///
/// # Errors
///
/// 电调 ID 越界或该帧 ID 已被占用时返回错误。
pub fn register_esc(
&mut self,
bus: BusId,
esc_id: u8,
name: impl Into<String>,
) -> Result<(), RegistryError> {
let frame_id = feedback_frame_id(esc_id).ok_or(RegistryError::BadEscId(esc_id))?;
self.register_frame(bus, frame_id, name)
}
/// 按反馈帧 ID 注册(GM6020 或非 1–8 的 ID 用它)。
///
/// # Errors
///
/// 该 `(总线, 帧 ID)` 已被占用时返回错误。
pub fn register_frame(
&mut self,
bus: BusId,
frame_id: u32,
name: impl Into<String>,
) -> Result<(), RegistryError> {
let name = name.into();
if let Some(existing) = self.resolve(bus, frame_id) {
return Err(RegistryError::Duplicate {
bus: bus.name(),
frame_id,
existing: existing.name.clone(),
name,
});
}
self.entries.push(RegisteredMotor { bus, frame_id, name });
Ok(())
}
/// 按 `(总线, 帧 ID)` 查找。
#[must_use]
pub fn resolve(&self, bus: BusId, frame_id: u32) -> Option<&RegisteredMotor> {
self.entries.iter().find(|entry| entry.bus == bus && entry.frame_id == frame_id)
}
/// 从一帧反馈找出它的归属设备(ARCHITECTURE.md §5.4 的 `update_from_frame`)。
///
/// 返回 `None` 表示这帧不属于任何已注册设备(例如 IMU 帧,或没配的电机)。
#[must_use]
pub fn dispatch(&self, bus: BusId, frame: &CanFrame) -> Option<&RegisteredMotor> {
self.resolve(bus, frame.id)
}
/// 已注册设备数。
#[must_use]
pub fn len(&self) -> usize {
self.entries.len()
}
/// 是否为空。
#[must_use]
pub fn is_empty(&self) -> bool {
self.entries.is_empty()
}
/// 全部条目(按注册顺序)。
#[must_use]
pub fn entries(&self) -> &[RegisteredMotor] {
&self.entries
}
/// 某条总线上的设备名。
#[must_use]
pub fn names_on(&self, bus: BusId) -> Vec<&str> {
self.entries
.iter()
.filter(|entry| entry.bus == bus)
.map(|entry| entry.name.as_str())
.collect()
}
}
/// 底盘四轮的注册顺序(与 `mecanum` 的 FL/FR/RL/RR 一致)。
pub const CHASSIS_WHEEL_NAMES: [&str; 4] =
["front_left", "front_right", "rear_left", "rear_right"];
/// 把底盘四轮按顺序注册进表(电调 ID 从 1 开始连续)。
///
/// # Errors
///
/// 任一电调 ID 越界或冲突时返回错误。
pub fn register_chassis_wheels(
registry: &mut MotorRegistry,
esc_ids: [u8; 4],
) -> Result<(), RegistryError> {
for (name, esc_id) in CHASSIS_WHEEL_NAMES.iter().zip(esc_ids) {
registry.register_esc(BusId::Chassis, esc_id, *name)?;
}
Ok(())
}
#[cfg(test)]
mod tests {
use super::*;
use motor_protocol::{MAX_ESC_ID, MIN_ESC_ID};
#[test]
fn registers_and_dispatches_chassis_wheels() {
let mut registry = MotorRegistry::new();
register_chassis_wheels(&mut registry, [1, 2, 3, 4]).unwrap();
assert_eq!(registry.len(), 4);
assert_eq!(registry.names_on(BusId::Chassis).len(), 4);
assert!(registry.names_on(BusId::Function).is_empty());
let frame = CanFrame::new(0x203, [0; 8]);
let found = registry.dispatch(BusId::Chassis, &frame).unwrap();
assert_eq!(found.name, "rear_left", "0x203 = 电调 3 = 后左");
}
#[test]
fn same_frame_id_on_another_bus_is_not_a_conflict() {
let mut registry = MotorRegistry::new();
registry.register_esc(BusId::Chassis, 1, "front_left").unwrap();
// 功能总线上的 GM6020 ID 1 反馈同样是 0x205 —— 不同总线,互不冲突。
registry.register_frame(BusId::Function, 0x205, "gimbal_yaw").unwrap();
assert_eq!(registry.len(), 2);
assert_eq!(registry.resolve(BusId::Chassis, 0x205), None);
assert_eq!(registry.resolve(BusId::Function, 0x205).unwrap().name, "gimbal_yaw");
}
#[test]
fn duplicates_on_the_same_bus_are_rejected_with_both_names() {
let mut registry = MotorRegistry::new();
registry.register_esc(BusId::Chassis, 2, "front_right").unwrap();
let error = registry.register_esc(BusId::Chassis, 2, "typo_front_right").unwrap_err();
let text = error.to_string();
assert!(text.contains("front_right"), "{text}");
assert!(text.contains("typo_front_right"), "{text}");
assert_eq!(registry.len(), 1, "失败的注册不得写入");
}
#[test]
fn out_of_range_esc_ids_are_rejected() {
let mut registry = MotorRegistry::new();
assert_eq!(
registry.register_esc(BusId::Chassis, 0, "ghost"),
Err(RegistryError::BadEscId(0))
);
assert_eq!(
registry.register_esc(BusId::Chassis, 9, "ghost"),
Err(RegistryError::BadEscId(9))
);
assert!(registry.is_empty());
assert!(feedback_frame_id(MIN_ESC_ID).is_some());
assert!(feedback_frame_id(MAX_ESC_ID).is_some());
}
#[test]
fn unknown_frames_dispatch_to_nothing() {
let mut registry = MotorRegistry::new();
register_chassis_wheels(&mut registry, [1, 2, 3, 4]).unwrap();
// DETA10 的 IMU 帧不是任何电机。
let imu = CanFrame::new(0x100, [0; 8]);
assert!(registry.dispatch(BusId::Chassis, &imu).is_none());
}
}
}
pub mod testing {
//! 测试替身:内存电机与航向源。
use std::future::Future;
use std::sync::{Arc, Mutex, MutexGuard};
use std::time::{Duration, Instant};
use crate::{
Command, Disable, EffortDrive, HeadingSource, MotorFeedback, MotorState, PositionDrive,
VelocityDrive,
};
/// 锁辅助:中毒锁直接恢复内部数据。
fn lock<T>(mutex: &Mutex<T>) -> MutexGuard<'_, T> {
mutex.lock().unwrap_or_else(std::sync::PoisonError::into_inner)
}
/// 内存电机:记录命令、可注入状态。供上层单测,无需总线。
///
/// 句柄可克隆(内部 `Arc` 共享),便于一边注入/断言一边把所有权交给控制器。
#[derive(Clone)]
pub struct MockMotor {
id: u8,
published: Arc<Mutex<(MotorState, Command)>>,
last_feedback: Arc<Mutex<Option<Instant>>>,
}
impl MockMotor {
/// 创建指定 ID 的空电机。
#[must_use]
pub fn new(id: u8) -> Self {
Self {
id,
published: Arc::new(Mutex::new((MotorState::default(), Command::Disabled))),
last_feedback: Arc::new(Mutex::new(None)),
}
}
/// 注入反馈快照(记为「刚刚收到」)。
pub fn inject_state(&self, state: MotorState) {
self.inject_state_with_age(state, Duration::ZERO);
}
/// 注入反馈快照并指定其“已过去”的时长(用于在线/超时测试)。
pub fn inject_state_with_age(&self, state: MotorState, age: Duration) {
lock(&self.published).0 = state;
let at = Instant::now().checked_sub(age);
*lock(&self.last_feedback) = Some(at.unwrap_or_else(Instant::now));
}
/// 最近一次命令。
#[must_use]
pub fn last_command(&self) -> Command {
lock(&self.published).1
}
}
impl MotorFeedback for MockMotor {
fn id(&self) -> u8 {
self.id
}
fn state(&self) -> MotorState {
lock(&self.published).0
}
fn feedback_age(&self) -> Option<Duration> {
let at = *lock(&self.last_feedback);
Some(at?.elapsed())
}
}
impl VelocityDrive for MockMotor {
fn set_velocity(&self, rotor_rpm: f32) {
lock(&self.published).1 = Command::Velocity(rotor_rpm);
}
}
impl EffortDrive for MockMotor {
fn set_effort_raw(&self, raw: i16) {
lock(&self.published).1 = Command::EffortRaw(raw);
}
}
impl PositionDrive for MockMotor {
fn set_position(&self, rotor_rev: f32) {
lock(&self.published).1 = Command::Position(rotor_rev);
}
}
impl Disable for MockMotor {
fn disable(&self) {
lock(&self.published).1 = Command::Disabled;
}
}
/// 恒定航向测试源。
pub struct MockHeading {
rad: f32,
}
impl MockHeading {
/// 创建固定航向源。
#[must_use]
pub const fn new(rad: f32) -> Self {
Self { rad }
}
}
impl HeadingSource for MockHeading {
fn heading_rad(&self) -> impl Future<Output = f32> + Send {
std::future::ready(self.rad)
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn mock_motor_records_commands() {
let m = MockMotor::new(3);
m.set_velocity(1000.0);
assert_eq!(m.last_command(), Command::Velocity(1000.0));
m.disable();
assert_eq!(m.last_command(), Command::Disabled);
}
#[test]
fn mock_motor_injects_state() {
let m = MockMotor::new(1);
assert!(!m.state().has_feedback());
assert!(m.feedback_age().is_none());
m.inject_state(MotorState { feedback_count: 5, ..MotorState::default() });
assert!(m.state().has_feedback());
assert!(m.feedback_age().is_some_and(|age| age < Duration::from_millis(50)));
}
#[test]
fn mock_motor_feedback_age_can_be_stale() {
let m = MockMotor::new(1);
m.inject_state_with_age(
MotorState { feedback_count: 1, ..MotorState::default() },
Duration::from_millis(250),
);
assert!(m.feedback_age().is_some_and(|age| age >= Duration::from_millis(200)));
}
}
}
pub use command::*;
pub use gearbox::*;
pub use heading::*;
pub use registry::*;
pub use state::*;
pub use testing::*;
pub use traits::*;
pub mod spec {
//! Generic motor identity and mechanical characteristics.
use motor_protocol::{CurrentC610, CurrentC620, CurrentEsc, Gm6020Voltage, MotorProtocol};
/// Control quantity represented by a motor's wire command.
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum ControlMode {
/// Current command.
Current,
/// Voltage command.
Voltage,
}
/// Mechanical and protocol identity for one motor model.
pub trait MotorSpec: Send + Sync + 'static {
type Protocol: MotorProtocol;
const NAME: &'static str;
const GEAR_RATIO: f32;
const ENCODER_TICKS: u16;
const CONTROL_MODE: ControlMode;
fn scale() -> <Self::Protocol as MotorProtocol>::Scale;
}
/// Marker for models driven by the shared C620/C610 current frame path.
pub trait CurrentEscDrive: MotorSpec {}
/// Marker for models driven by a voltage frame path.
pub trait VoltageEscDrive: MotorSpec {}
/// GM3508 + C620.
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub struct Gm3508;
/// M2006 + C610.
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub struct M2006;
/// GM6020 direct-drive voltage motor.
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub struct Gm6020;
impl MotorSpec for Gm3508 {
type Protocol = CurrentEsc<CurrentC620>;
const NAME: &'static str = "GM3508";
const GEAR_RATIO: f32 = 3591.0 / 187.0;
const ENCODER_TICKS: u16 = 8192;
const CONTROL_MODE: ControlMode = ControlMode::Current;
fn scale() -> CurrentC620 {
CurrentC620
}
}
impl MotorSpec for M2006 {
type Protocol = CurrentEsc<CurrentC610>;
const NAME: &'static str = "M2006";
const GEAR_RATIO: f32 = 36.0;
const ENCODER_TICKS: u16 = 8192;
const CONTROL_MODE: ControlMode = ControlMode::Current;
fn scale() -> CurrentC610 {
CurrentC610
}
}
impl MotorSpec for Gm6020 {
type Protocol = Gm6020Voltage;
const NAME: &'static str = "GM6020";
const GEAR_RATIO: f32 = 1.0;
const ENCODER_TICKS: u16 = 8192;
const CONTROL_MODE: ControlMode = ControlMode::Voltage;
fn scale() -> motor_protocol::Voltage30000 {
motor_protocol::Voltage30000
}
}
impl CurrentEscDrive for Gm3508 {}
impl CurrentEscDrive for M2006 {}
impl VoltageEscDrive for Gm6020 {}
/// Keep scale construction available to generic callers.
pub fn scale_of<S: MotorSpec>() -> <S::Protocol as MotorProtocol>::Scale {
S::scale()
}
/// Compile-time assertion helper for current-path models.
pub fn assert_current_drive<S: CurrentEscDrive>() {}
/// Compile-time assertion helper for voltage-path models.
pub fn assert_voltage_drive<S: VoltageEscDrive>() {}
}
pub use spec::{ControlMode, CurrentEscDrive, Gm3508, Gm6020, M2006, MotorSpec, VoltageEscDrive};
#[cfg(test)]
mod spec_tests {
use super::*;
#[test]
fn motor_markers_select_the_right_protocol_family() {
spec::assert_current_drive::<Gm3508>();
spec::assert_current_drive::<M2006>();
spec::assert_voltage_drive::<Gm6020>();
assert_eq!(Gm3508::NAME, "GM3508");
assert_eq!(M2006::NAME, "M2006");
assert_eq!(Gm6020::NAME, "GM6020");
assert_eq!(Gm3508::CONTROL_MODE, ControlMode::Current);
assert_eq!(Gm6020::CONTROL_MODE, ControlMode::Voltage);
}
}