347 lines
8.0 KiB
Markdown
347 lines
8.0 KiB
Markdown
# Eskin Finger SDK Progress
|
||
|
||
本文件记录当前代码骨架进度、已完成设计决策、已知问题和后续实现顺序。
|
||
|
||
## 当前状态
|
||
|
||
当前 SDK 已经形成如下分层:
|
||
|
||
```text
|
||
Device API
|
||
-> Stream Runtime / Channel
|
||
-> Register Access
|
||
-> Protocol Codec
|
||
-> Serial Transport
|
||
-> Hardware
|
||
```
|
||
|
||
当前 `cargo check` 可以通过,但还有 `stream.rs` 中的两个 warning 需要清理。
|
||
|
||
## 已完成
|
||
|
||
### 1. Protocol Layer
|
||
|
||
文件:`src/protocol.rs`
|
||
|
||
已完成:
|
||
|
||
- 读请求编码:`encode_read_request`
|
||
- 写请求编码:`encode_write_request`
|
||
- 读应答解码:`decode_read_response`
|
||
- 写应答解码:`decode_write_response`
|
||
- stream frame 解码入口:`decode_stream_frame`
|
||
- CRC 校验
|
||
- 设备状态码转换
|
||
- response 帧长度校验
|
||
|
||
当前协议约定:
|
||
|
||
```text
|
||
response frame = header + payload/status data + status(1B) + crc(1B)
|
||
crc 是最后 1 字节
|
||
status 是 crc 前 1 字节
|
||
```
|
||
|
||
注意:
|
||
|
||
- `FRAME_START_RESPONSE = 0xAA55`
|
||
- 当前 `device.rs` 和 `stream.rs` 读取 response 起始符时使用 `u16::from_be_bytes([header[0], header[1]])`
|
||
- 如果真实设备返回字节序为 `55 AA`,这里需要改为小端;如果真实返回 `AA 55`,当前逻辑正确
|
||
|
||
### 2. Transport Layer
|
||
|
||
文件:`src/transport.rs`
|
||
|
||
已完成:
|
||
|
||
- `SerialTransport` trait
|
||
- `SerialPortTransport`
|
||
- 串口 open/close/is_open
|
||
- write/read/flush_rx
|
||
- timeout 转换
|
||
- serialport error 到 `SdkError` 的转换
|
||
- `SharedSerialTransport = Arc<Mutex<Box<dyn SerialTransport>>>`
|
||
|
||
设计决策:
|
||
|
||
- `SerialTransport: Send`,不要求 `Sync`
|
||
- 跨线程共享通过 `Arc<Mutex<...>>` 完成
|
||
- 串口 request/response 应在同一把 mutex lock 内完成,避免多线程串帧
|
||
|
||
### 3. Device Layer
|
||
|
||
文件:`src/device.rs`
|
||
|
||
已完成:
|
||
|
||
- `DeviceState`
|
||
- `EskinDeviceInner`
|
||
- `EskinDevice` trait
|
||
- `open/close`
|
||
- `start_stream/stop_stream`
|
||
- `read_sample/read_event`
|
||
- 同步 `read_register/write_register`
|
||
- `ensure_open`
|
||
- 共享 channel:`Arc<ChannelManager>`
|
||
- 共享 transport:`SharedSerialTransport`
|
||
- `create_stream_runtime`
|
||
- `shared_transport`
|
||
|
||
当前行为:
|
||
|
||
- `read_register/write_register` 会:
|
||
- 检查设备状态
|
||
- protocol encode request
|
||
- lock transport
|
||
- flush rx
|
||
- write request
|
||
- read full response frame
|
||
- protocol decode response
|
||
|
||
注意:
|
||
|
||
- `device.start_stream()` 和 `StreamRuntime::start()` 当前都会发送 `StreamStarted`
|
||
- 后续需要明确 stream 状态由谁统一管理,避免重复事件
|
||
|
||
### 4. Channel Layer
|
||
|
||
文件:`src/channel.rs`
|
||
|
||
已完成:
|
||
|
||
- `DeviceCommand`
|
||
- `DeviceEvent`
|
||
- `ChannelManager`
|
||
- sample/cmd/event 三类 channel
|
||
- `send_sample/recv_sample`
|
||
- `send_cmd/recv_cmd`
|
||
- `send_event/recv_event`
|
||
- `dropped_count/reset_dropped_count`
|
||
- sample drop policy:
|
||
- `DropNewest`
|
||
- `DropOldest`
|
||
|
||
设计决策:
|
||
|
||
- `dropped_samples` 只统计 sample drop,不统计 command/event
|
||
- channel timeout 和 disconnected 分别映射为:
|
||
- `SdkError::Timeout`
|
||
- `SdkError::ChannelClosed`
|
||
- sample channel 满时根据 drop policy 处理,不作为硬错误
|
||
|
||
### 5. Stream Layer
|
||
|
||
文件:`src/stream.rs`
|
||
|
||
已完成:
|
||
|
||
- `StreamMode`
|
||
- `StreamConfig`
|
||
- `StreamController`
|
||
- `StreamRuntime`
|
||
- `StreamWorker`
|
||
- worker thread 生命周期:
|
||
- `start()` spawn worker
|
||
- `stop()` stop flag + join
|
||
- `SampleCollector` trait
|
||
- `NoopSampleCollector`
|
||
- `PollingSampleCollector` 骨架
|
||
- polling collector 已具备同步 `read_register` 能力
|
||
- polling collector 当前会尝试读取:
|
||
- `REG_COMBINED_FORCE`
|
||
- `REG_MODULE_ERROR`
|
||
|
||
当前行为:
|
||
|
||
```text
|
||
StreamRuntime::start()
|
||
-> make_collector()
|
||
-> spawn StreamWorker
|
||
-> worker loop
|
||
-> collector.collect_once()
|
||
-> if Some(sample), send_sample(sample)
|
||
```
|
||
|
||
当前 `PollingSampleCollector::collect_once()` 只读取 raw bytes,尚未解析为 `FingerSample`,因此返回 `Ok(None)`。
|
||
|
||
已知 warning:
|
||
|
||
- `src/stream.rs` unused import: `transport::{self, ...}` 中的 `self`
|
||
- `StreamWorker::new` 参数 `transport` 未使用
|
||
|
||
### 6. Register Layer
|
||
|
||
文件:`src/register.rs`
|
||
|
||
已完成:
|
||
|
||
- 寄存器地址常量
|
||
- `RegisterSpec`
|
||
- `RegisterAccess`
|
||
- `RegisterValueType`
|
||
- `DEVICE_INFO_REGISTERS`
|
||
- `RegisterMap` trait
|
||
- `EskinRegisterMap`
|
||
- `parse_distribution_force`
|
||
|
||
暂未完成:
|
||
|
||
- `distribution_register`
|
||
- `parse_device_info`
|
||
- combined force 解析
|
||
- module error 解析
|
||
|
||
## 当前主要设计决策
|
||
|
||
### Transport 共享模型
|
||
|
||
当前使用:
|
||
|
||
```rust
|
||
Arc<Mutex<Box<dyn SerialTransport>>>
|
||
```
|
||
|
||
原因:
|
||
|
||
- device 和 stream worker 需要共享同一个串口
|
||
- 串口读写需要 `&mut self`
|
||
- mutex 保证一次 request/response 不被其他线程打断
|
||
|
||
长期建议:
|
||
|
||
- stream running 时,尽量由 worker 独占串口访问
|
||
- 主线程通过 command channel 请求 worker 操作设备
|
||
- 避免主线程同步 `read_register` 和 worker polling 同时抢 transport
|
||
|
||
### Stream 职责拆分
|
||
|
||
当前拆分:
|
||
|
||
```text
|
||
StreamRuntime
|
||
管理 start/stop、worker handle、对外读取 sample/event
|
||
|
||
StreamWorker
|
||
管理 loop、running flag、sleep、错误事件
|
||
|
||
SampleCollector
|
||
管理一次采集,后续负责协议读写和 sample 构建
|
||
```
|
||
|
||
这是推荐方向。worker 不应该直接塞满协议和寄存器解析逻辑。
|
||
|
||
## 明确下一步
|
||
|
||
### Step 1: 清理当前 warning
|
||
|
||
文件:`src/stream.rs`
|
||
|
||
处理:
|
||
|
||
- 删除 unused import 中的 `self`
|
||
|
||
```rust
|
||
transport::{SerialTransport, SharedSerialTransport}
|
||
```
|
||
|
||
- `StreamWorker::new` 当前参数 `transport` 未使用。二选一:
|
||
- 删除 `StreamWorker` 中的 transport 参数,因为 collector 已持有 transport
|
||
- 或者让 worker 持有 transport,但不推荐,职责重复
|
||
|
||
推荐:删除 `StreamWorker::new` 的 `transport` 参数。
|
||
|
||
### Step 2: 完善 Register 解析接口
|
||
|
||
文件:`src/register.rs`
|
||
|
||
新增解析函数:
|
||
|
||
- `parse_combined_forces(raw: &[u8]) -> Result<Vec<CombinedForce>, SdkError>`
|
||
- `parse_module_errors(raw: &[u8]) -> Result<Vec<ModuleError>, SdkError>`
|
||
|
||
依据当前寄存器表:
|
||
|
||
```text
|
||
REG_COMBINED_FORCE = 0x0500
|
||
长度 168B = 28 modules * 6B
|
||
每个 module = fx:i16 + fy:i16 + fz:i16
|
||
|
||
REG_MODULE_ERROR = 0x0700
|
||
长度 56B = 28 modules * 2B
|
||
每个 module = error_code:u16
|
||
```
|
||
|
||
### Step 3: 让 PollingSampleCollector 产出 FingerSample
|
||
|
||
文件:`src/stream.rs`
|
||
|
||
在 `PollingSampleCollector::collect_once()` 中:
|
||
|
||
```text
|
||
1. sequence = next_sequence()
|
||
2. read REG_COMBINED_FORCE
|
||
3. read REG_MODULE_ERROR
|
||
4. register parse raw bytes
|
||
5. build FingerSample
|
||
6. return Ok(Some(sample))
|
||
```
|
||
|
||
先不处理 distribution force。
|
||
|
||
### Step 4: 补 distribution force
|
||
|
||
文件:`src/register.rs`、`src/stream.rs`
|
||
|
||
前置条件:
|
||
|
||
- `distribution_register(module)` 能根据 `SensorModule` 返回地址和长度
|
||
- 需要确认每个 module 的分布力长度来源
|
||
|
||
实现策略:
|
||
|
||
- `StreamConfig.read_distribution == false` 时跳过
|
||
- `StreamConfig.modules` 为空时默认读所有模块,或者默认不读;需要明确语义
|
||
|
||
### Step 5: 统一 stream 状态入口
|
||
|
||
文件:`src/device.rs`、`src/stream.rs`
|
||
|
||
当前重复点:
|
||
|
||
- `device.start_stream()` 发 `StreamStarted`
|
||
- `StreamRuntime::start()` 也发 `StreamStarted`
|
||
|
||
需要选择一个主入口:
|
||
|
||
推荐:
|
||
|
||
```text
|
||
device.open()
|
||
let mut stream = device.create_stream_runtime()
|
||
stream.start(config)
|
||
stream.next_sample()
|
||
stream.stop()
|
||
```
|
||
|
||
如果最终 SDK 希望用户只调用 `device.start_stream()`,则 `EskinDeviceInner` 需要持有 `StreamRuntime` 或 worker handle。
|
||
|
||
### Step 6: 增加基础测试
|
||
|
||
建议先加这些测试:
|
||
|
||
- protocol encode read request golden bytes
|
||
- protocol encode write request golden bytes
|
||
- CRC 校验
|
||
- register parse combined force
|
||
- register parse module error
|
||
- ChannelManager drop policy
|
||
|
||
## 当前风险点
|
||
|
||
1. response 起始符字节序仍需真实设备帧确认。
|
||
2. stream worker 和 device 同步 read/write 共享同一 transport,虽然 mutex 安全,但业务上仍可能抢响应。
|
||
3. `PollingSampleCollector` 已读取 raw bytes,但还未构建 sample。
|
||
4. `register.rs` 的 `parse_device_info` 和 `distribution_register` 仍是 `todo!()`。
|
||
5. `StreamStarted/StreamStopped` 事件存在重复来源,需要统一入口。
|
||
|