feat: 初始化

This commit is contained in:
Horldsence
2026-09-22 20:05:16 +08:00
commit 7d87987e41
54 changed files with 6446 additions and 0 deletions
+384
View File
@@ -0,0 +1,384 @@
# 审计 + 电机抽象层改造 + 实车台架单轮测试
## 目标(用户已确认)
1. **审计**现有抽象层级与抽象功能是否正确。
2. **改造抽象层**(本次就做),为后续派生铺路:
- 云台 = **GM6020**(位置环,绝对/多圈角度)
- 拨弹轮 = **M2006 + C610**(速度环 + 定量拨弹位置环)
3. **小车轮子测试** = 实车台架单轮验证(现有 4× GM3508 轮子)。
4. **硬约束**:合适的 Rust 抽象层与 crate,**明确单一职责**。
5. **架构决策**(本轮确认):
- **双 CAN 总线**:底盘一条 CAN,云台/拨弹另一条 CAN(协议隔离,不用避让 ID)。
- **必须预留 IMU 融合口**:云台绝对角需要 `HeadingSource` 类 seam。
- **wheel-bench 分阶段自动跑**。
---
## 一、审计结论
### 1.1 分层现状(正确的部分,保留)
```
chassisd ──► mecanum ──► motor-core::Motor ◄── dji-esc (EscBus) ──► CanLink ──► dji-esc-socketcan
```
| 做对的事 | 证据 |
|---|---|
| 协议与 I/O 解耦 | `CanLink` = tokio mpsc 对;`motor-core`/`dji-esc` 零真实 I/O |
| 命令 / 反馈分离 | `Command` enum vs `MotorState` struct |
| 方向修正对称 | `EscConfig::direction=±1`,读写双向翻转(`bus.rs:163-203`) |
| 纯算法可测 | `Pid`(抗饱和、D-on-measurement、滤波)、`VelocityController` |
| 多圈累计 | `TurnTracker` 8192 编码器回绕(`turn.rs`) |
| 安全三件套 | 上电门控 / 反馈离线 0 电流 / 命令超时归零 |
| 仿真闭环 | `sim::spawn_plant` + `closed_loop.rs` + `--mock` 全平台可跑 |
| 运动学纯数学 | `mecanum` 正逆解 + proptest |
| 组合根 | `chassisd` 装配 + `TwistSource` seam |
**结论:轮子(C620 电流环 + 主机速度环)路径是通的。**
### 1.2 单一职责违规 / 抽象缺口(必须改)
| # | 问题 | 位置 | SRP 违规 |
|---|---|---|---|
| G1 | `Motor` 无位置 API | `motor-core/src/lib.rs:51-79` | 能力面不足 |
| G2 | `MotorState.wheel_speed_rpm` | `lib.rs:32`, `bus.rs:171` | **底盘/减速比泄漏进协议无关层** |
| G3 | 无 `PositionController` | `motor-core` | 云台/拨弹阻塞 |
| G4 | 主机速度环写死在 `EscBus::compute_command` | `bus.rs:181-203` | **控制律与协议组帧/安全耦合** |
| G5 | `EscModel` 无 GM6020 | `codec.rs:23-30` | 电压协议族缺失 |
| G6 | `tx_loop` 硬编码 0x200/0x1FF 电流槽 | `bus.rs:369-388` | 协议族绑定过死 |
| G7 | `ChassisController` 依赖 `EscMotor` + 自己乘 gear | `controller.rs:25,83` | 应用层依赖协议 crate |
| G8 | `Command` 切模式不复位 PID | `bus.rs:192-199` | 拨弹速度↔位置积分冲击 |
| G9 | 无 `MockMotor` | — | 上层无法脱离 EscBus 测 |
| G10 | 配置写死恰好 4 电机 | `config.rs:181` | 无法扩设备段 |
| G11 | `CanFrame`/`CanLink` 在 `dji-esc::frame` | `frame.rs` | **传输被单个协议族占有** |
| G12 | 无航向/绝对角抽象 | — | 云台场地绝对角需 IMU 融合(用户要求预留) |
### 1.3 CAN ID 问题 —— 由「双 CAN」架构决策消解
`0x1FF` / `0x205` 在 C620 与 GM6020 两族中含义不同(电流 vs 电压)。**用户选择双 CAN 总线**:
| 总线 | 设备 | 协议 | ID | 命令帧 |
|---|---|---|---|---|
| **can0 底盘** | FL/FR/RL/RR 轮 | C620+GM3508 | 1–4 | `0x200` |
| **can1 功能** | 拨弹轮 | C610+M2006 | 1–4(本总线内) | `0x200` |
| **can1 功能** | 云台 | GM6020 | 1–4(本总线内) | `0x1FF` |
- 每条 CAN 一个独立 `CanLink` + 一个协议族 bus;**物理隔离**后 ID 空间独立,无需避让。
- 仍建议配置层按「每总线一协议族」校验,避免同总线混接时踩坑。
- 组合根按接口名装配:`chassisd --interface can0 --gimbal-interface can1`(或配置 `[can]`/`[can.function]`)。
---
## 二、目标设计(单一职责)
### 2.1 目标 crate / 模块树
```
crates/
motor-core/ # 协议无关电机域:能力 trait、状态、命令寄存器、控制算法、mock(零依赖、无 tokio)
src/
lib.rs # 导出 + 单位/方向约定
state.rs # MotorState(转子轴物理量,无 gear_ratio)
command.rs # Command + CommandMode
traits.rs # MotorFeedback / EffortDrive / VelocityDrive / PositionDrive / Disable
heading.rs # HeadingSource(IMU/绝对角融合 seam,新增)
pid.rs # Pid(保持)
velocity.rs # VelocityController(输出泛化为 effort_raw)
position.rs # PositionController(新增:Direct | Cascade)
gearbox.rs # OutputShaft<T>:输出轴单位适配(新增)
testing.rs # MockMotor / MockHeading(新增)
can-link/ # CAN 传输:CanFrame + CanLink(新增;无协议知识)
dji-esc/ # C620/C610 电流协议族:编解码 + 总线调度/安全 + sim
dji-gm6020/ # GM6020 电压协议族(本次只留 seam 文档,实现=后续)
dji-esc-socketcan/ # SocketCAN 桥(只依赖 can-link;支持多接口)
examples/
spin.rs # 保留:最小抽象演示
wheel-bench.rs # 新增:实车台架单轮验证(分阶段自动)
mecanum/ # 纯运动学(不动)
chassisd/ # 组合根:配置 / 1kHz 底盘循环 / 安全 / --mock
docs/
architecture.md # 更新分层图(双 CAN)
motor-abstraction.md # trait 契约与单位(新增)
bench-single-wheel.md# 台架流程(新增)
```
**裁剪原则**:只新增 `can-link` + `dji-gm6020` seam + `heading.rs` 模块。不拆 `motor-controllers`、暂不建 `gimbal`/`feeder` crate。
### 2.2 motor-core:能力拆分(小 trait)
```rust
// traits.rs —— 能力面,可组合,静态分发
pub trait MotorFeedback: Send + Sync {
fn id(&self) -> u8;
fn state(&self) -> MotorState;
}
pub trait EffortDrive: Send + Sync {
/// 开环 raw(ESC=电流,GM6020=电压)。
fn set_effort_raw(&self, raw: i16);
}
pub trait VelocityDrive: Send + Sync {
/// 目标转子转速(rpm,物理方向)。
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: MotorFeedback + VelocityDrive + EffortDrive + Disable> Motor for T {}
```
**为什么不是胖 `Motor` trait**:能力是硬件事实。消费者只声明用到的能力:
- 底盘 → `VelocityDrive + MotorFeedback`
- 云台 → `PositionDrive + EffortDrive`(+ 上层 `HeadingSource`)
- 拨弹 → `VelocityDrive + PositionDrive`
- 台架 → `VelocityDrive + MotorFeedback + Disable`
**`EffortDrive` 统一命名**:两族都是 i16 开环命令,仅物理量不同(A vs V);协议 crate 文档化单位。
### 2.3 IMU / 绝对角融合 seam(用户要求)
```rust
// heading.rs —— 不属于电机,属于「航向观测」
/// 绝对航向观测源(IMU / 磁力计 / 视觉等)。
pub trait HeadingSource: Send {
/// 机体航向角(rad,连续;定义与 chassis ISO 约定一致)。
fn heading_rad(&self) -> impl Future<Output = f32> + Send;
}
/// 云台目标角的参考系。
pub enum HeadingFrame {
/// 电机转子多圈(上电相对)。
RotorMultiTurn,
/// 场地绝对角(经 HeadingSource 对齐)。
AbsoluteField,
}
```
- **motor 层只保证 `rotor_revolutions` 连续**;场地绝对角由未来 `gimbal` crate 融合 IMU(`HeadingSource`)与电机多圈。
- `PositionDrive::set_position` 仍是转子圈;绝对角 → 转子圈的映射在云台控制器,不在 motor-core。
- `MockHeading` 放 `testing.rs`。
### 2.4 `Command` / `MotorState`
```rust
pub enum Command {
Disabled,
EffortRaw(i16), // 原 CurrentRaw
Velocity(f32), // rotor rpm
Position(f32), // rotor revolutions(圈,连续)
}
pub enum CommandMode { Disabled, EffortRaw, Velocity, Position }
pub struct MotorState {
pub rotor_angle_raw: u16,
pub rotor_revolutions: f32,
pub rotor_speed_rpm: f32,
pub effort_raw: i16, // 原 torque_current_raw
pub temperature_c: i8,
pub feedback_count: u64,
}
// 删除 wheel_speed_rpm(G2)
```
**模式切换复位**(G8):`compute_effort` 在 `cmd.mode()` 变化时 reset 控制器。
### 2.5 motor-core:控制算法
| 类型 | 模式 | 输入 → 输出 |
|---|---|---|
| `Pid` | — | setpoint, meas, dt → f32 |
| `VelocityController` | — | target_rpm, meas_rpm, dt → effort_raw |
| `PositionController` **新增** | `Direct(Pid)` | pos_rev, meas_rev, dt → effort_raw(云台电压) |
| | `Cascade{pos, vel}` | 位置→限速→速度环→effort_raw(拨弹定量) |
控制律纯函数放 motor-core;**执行节拍/安全/方向/限幅**留在各协议族 bus。
### 2.6 gearbox:`OutputShaft<T>`
```rust
/// 输出轴单位 ↔ 转子单位。gear_ratio = 转子 : 输出轴。
pub struct OutputShaft<T> { inner: T, gear_ratio: f32 }
impl<T: VelocityDrive> VelocityDrive for OutputShaft<T> {
fn set_velocity(&self, output_rpm: f32) {
self.inner.set_velocity(output_rpm * self.gear_ratio);
}
}
// Position 同理;Feedback/Effort/Disable 透传
```
### 2.7 can-link(新 crate)
`CanFrame`、`CanLink`、`CanLink::pair`。**不含 DJI ID 常量**。`motor-core` 保持无 tokio。
### 2.8 dji-esc(电流协议族)
- `EscConfig::m2006(name, esc_id, direction)`(C610、36:1 待核)。
- 算法改调 motor-core;`compute_effort` 加 `Position` 分支 + 模式 reset。
- 电流槽分组保持(协议事实)。
### 2.9 dji-gm6020(设计 seam,不实机)
- 实现 5 个能力 trait。
- 命令 `0x1FF`(ID1–4)/`0x2FF`(ID5–7) 电压槽;反馈 `0x204+id`;`TurnTracker` 复用。
- 云台位置环:`PositionController` → voltage raw。
- 绝对角:未来 `gimbal` crate 用 `HeadingSource` + `HeadingFrame::AbsoluteField` 对齐到 `rotor_revolutions` 基准后再 `set_position`。
### 2.10 拨弹 M2006/C610 派生路径(设计,不实现)
- 复用 `dji-esc` + `EscConfig::m2006`;定量 = `Position` + Cascade;连续 = `Velocity`。
- FSM 在上层,不在 motor-core。
### 2.11 chassisd(组合根)
```rust
pub struct ChassisController<W> {
kinematics: Mecanum,
wheels: [W; 4],
limits: (f32, f32, f32),
command_timeout: Duration,
}
impl<W: VelocityDrive + MotorFeedback> ChassisController<W> { ... }
```
- **不 import `dji_esc`**。
- 配置:底盘 `[can]` + 四轮;预留 `[can.function]` / `[gimbal]` / `[feeder]` seam 注释(双 CAN 接口名)。
- 不写云台/拨弹逻辑。
---
## 三、实车台架单轮验证(小车轮子测试)
### 3.1 工具:`wheel-bench`(分阶段自动跑)
```sh
cargo run --release -p dji-esc-socketcan --example wheel-bench -- \
--interface can0 --esc-id 1 --direction 1 --gear-ratio 19.203 \
--current-limit 4000 --rpm 300 --secs 3
```
一条命令顺序执行 stage 0–6,输出 CSV + 结论;ctrl-c 任意点安全停机。
| Stage | 动作 | 观察 | 通过标准 |
|---|---|---|---|
| 0 Link | `disable()` | 反馈帧率 ~1kHz、温度 | 2s 内有反馈 |
| 1 Direction | `set_effort_raw(±limit/4)` 点动 | `rotor_speed_rpm` 符号 | 与 `direction`/前进约定一致 |
| 2 Vel step | `set_velocity(+target)` | 超调、稳态误差 | 1s 后 \|err\|<5% |
| 3 gear_ratio | 对比转子 rpm 与手测轮端 | 比值 | ≈19.203 ±3% |
| 4 Reverse | `set_velocity(-target)` | 对称性 | 对称 |
| 5 Disable | `disable()` | rpm→0 | 停转 |
| 6 Offline(可选) | 运行中断电调 | 离线强制 0 电流 | 无重启冲击 |
安全内建:默认低限流、总时限、退出路径 `disable()+50ms` 刷帧、无反馈拒启动、单电机、打印「架空禁止落地」。保留 `spin.rs`。
### 3.2 流程(`docs/bench-single-wheel.md`)
1. **架空车轮**,急停可及;`ip link set can0 up type can bitrate 1000000 restart-ms 100`。
2. 首次 `--current-limit 3000~5000`。
3. 一次只驱一台;顺序 FL→FR→RL→RR。
4. 跑完自动 stage;方向错**改配置 `direction`**。
5. 记录 `direction` 终值、实测 gear_ratio、PID 备注。
6. 填表归档后才考虑落地四轮联动(不在本次)。
**通过标准**:反馈无连续丢帧;方向一致;无持续振荡、稳态 <5%;减速比偏差 <3%;`disable`/离线停转、电流 0。
---
## 四、迁移步骤(每步可编译、测试绿)
| 步 | 内容 |
|---|---|
| M0 | 基线 `cargo test --workspace && just lint` |
| M1 | 新建 `can-link`;`dji-esc::frame` re-export 兼容 |
| M2 | `dji-esc-socketcan` 改依赖 `can-link`;收缩 `frame.rs` |
| M3 | motor-core 拆 `state/command/traits/heading`;**一次破坏性重命名** `CurrentRaw→EffortRaw`、`torque_current_raw→effort_raw`;删 `wheel_speed_rpm` |
| M4 | 能力 trait 落地;`EscMotor` 实现;`Motor` blanket 别名 |
| M5 | `OutputShaft<T>`;`ChassisController` 泛型化;daemon 测试 gear 换算 |
| M6 | `Command::Position` + `CommandMode` 模式 reset;`PositionController` + 单测;`dji-esc` Position 分支 |
| M7 | `MockMotor`/`MockHeading`;改写 1 个 chassis 单测证明 seam |
| M8 | `EscConfig::m2006()`;双 CAN 配置 seam + 每总线单协议族校验 |
| M9 | `wheel-bench`(分阶段自动)+ `docs/bench-single-wheel.md` |
| M10 | 文档:`architecture.md`(双 CAN 图)、`motor-abstraction.md`、README |
| M11(后续) | `dji-gm6020` codec+bus+sim |
| M12(后续) | `gimbal` crate:`HeadingSource` 融合 + 云台控制 |
---
## 五、关键文件
**新建**
- `crates/can-link/{Cargo.toml,src/lib.rs}`
- `crates/motor-core/src/{state,command,traits,heading,position,gearbox,testing}.rs`
- `crates/dji-esc-socketcan/examples/wheel-bench.rs`
- `docs/{motor-abstraction,bench-single-wheel}.md`
**修改**
- `Cargo.toml`(workspace)
- `crates/motor-core/src/{lib,velocity}.rs`
- `crates/dji-esc/src/{lib,bus,codec,sim}.rs`、`tests/closed_loop.rs`
- `crates/dji-esc-socketcan/src/bridge.rs`、`examples/spin.rs`、`Cargo.toml`
- `crates/chassisd/src/{controller,config,main}.rs`、`tests/daemon.rs`
- `config/chassis.toml.example`、`docs/architecture.md`、`README.md`、`justfile`
**删除 / 迁出**
- `crates/dji-esc/src/frame.rs`(类型迁 can-link)
- `MotorState.wheel_speed_rpm`、`Command::CurrentRaw`、`torque_current_raw`
---
## 六、验证
```sh
cargo test --workspace
just lint
cargo run -p chassisd -- --mock
just vcan-test
just build-armv7
cargo build --release -p dji-esc-socketcan --example wheel-bench
cargo build --release -p dji-esc-socketcan --example spin
# 实车台架见 §3
```
**Definition of Done**
1. `cargo test --workspace` + `just lint` 绿。
2. `chassisd --mock` 行为不变。
3. `rg 'wheel_speed_rpm|CurrentRaw|torque_current_raw'` 为空。
4. `rg 'dji_esc' crates/chassisd/src/controller.rs` 为空。
5. `motor-core` 无运行时依赖。
6. `wheel-bench` + 台架文档就位;实车四轮台架表归档。
7. `HeadingSource` seam + 双 CAN 配置 seam 就位;**`chassisd` 无云台/拨弹逻辑**。
---
## 七、风险
| 风险 | 缓解 |
|---|---|
| 双 CAN 板端接口(can1)是否可用 | 配置可单 CAN 起步(只填 `[can]`);功能段可选 |
| GM6020 电压/反馈标度未核手册 | 常量隔离在 `dji-gm6020::codec`;上电前台架核对 |
| C610/M2006 36:1?±10000↔±10A | `m2006()` 标注接入前核对 |
| `OutputShaft` 单位混淆 | 参数名 `output_rpm`/`output_rev` + 文档 |
| 删 `wheel_speed_rpm` 破坏日志/测试 | M3/M5 同步改 |
| 模式切换 reset 惊吓 | 文档 + `debug!` |
| `HeadingSource` 的异步形态是否合适 | 先定 trait 契约;M12 可按 IMU 驱动改 `async fn` / watch |
| 同总线误接混协议 | 配置校验「每总线一协议族」+ 文档警告 |
---
## 八、不在本次范围
- 云台 / 拨弹控制逻辑与整定
- GM6020 实机驱动完整实现(M11)
- IMU 驱动与航向融合实现(M12,仅 seam)
- 四轮落地联动测试
- 遥控器 / 导航 TwistSource