Build SDK core streaming skeleton

This commit is contained in:
lenn
2026-05-05 16:48:16 +08:00
parent 79f4055959
commit 60f9ad15e7
6 changed files with 963 additions and 57 deletions

346
docs/PROGRESS.md Normal file
View File

@@ -0,0 +1,346 @@
# 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` 事件存在重复来源,需要统一入口。