diff --git a/.gitignore b/.gitignore index 04db570..8c2ecd5 100644 --- a/.gitignore +++ b/.gitignore @@ -4,6 +4,9 @@ debug/ target/ +# Internal docs (not for end users) +docs/ + # These are backup files generated by rustfmt **/*.rs.bk diff --git a/README.md b/README.md index 04e8bab..6bc6890 100644 --- a/README.md +++ b/README.md @@ -1,2 +1,214 @@ -# eskin-finger-sdk +# Eskin Finger SDK +E-Skin 手指力传感器 Rust SDK,提供串口通信、寄存器读写、流式采集能力,支持 Rust / C/C++ / Python 调用。 + +## 目录结构 + +```text +src/ + lib.rs — Rust 库入口 + device.rs — 设备管理(open/close/read/write) + stream.rs — 流式采集(PollingSampleCollector) + protocol.rs — 协议帧编解码(CRC-8/X25) + register.rs — 寄存器地址定义与解析 + transport.rs — 串口传输抽象 + channel.rs — 线程间 Channel + config.rs — 配置与设备信息 + error.rs — 错误类型 + types.rs — 数据类型定义 + ffi/mod.rs — C FFI 导出函数 + +include/ + eskin_ffi.h — C/C++ 头文件 + +example/ + cpp/main.cpp — C++ 使用示例 + python/ — Python 使用示例 +``` + +--- + +## 快速开始(Rust) + +```rust +use eskin_finger_sdk::config::DeviceConfig; +use eskin_finger_sdk::device::{EskinDevice, EskinDeviceInner}; +use eskin_finger_sdk::transport::SerialPortTransport; + +let transport = SerialPortTransport::new("/dev/ttyUSB0", 921600); +let config = DeviceConfig::default(); +let mut device = EskinDeviceInner::new(config, Box::new(transport)); +device.open().unwrap(); + +// 读寄存器(原始字节) +let data = device.read_register(0x0000, 4).unwrap(); +println!("Serial: {:?}", data); + +// 写寄存器 +let count = device.write_register(0x0030, &[0x01, 0x00, 0x00, 0x00]).unwrap(); +println!("Wrote {} bytes", count); + +device.close().unwrap(); +``` + +--- + +## 使用 C/C++ + +### 构建动态库 + +```bash +# 安装依赖(Ubuntu) +sudo apt install pkg-config libudev-dev + +# 构建 +cargo build --release +# 输出: target/release/libeskin_finger_sdk.so +``` + +### 编译 C++ 示例 + +```bash +g++ example/cpp/main.cpp -I include -L target/release -leskin_finger_sdk -o example_cpp +LD_LIBRARY_PATH=target/release ./example_cpp +``` + +### C++ 代码示例 + +```cpp +#include "eskin_ffi.h" +#include + +int main() { + EskinDeviceHandle dev = eskin_open("/dev/ttyUSB0", nullptr); + if (!dev) return 1; + + uint8_t buf[256]; + uint32_t actual; + if (eskin_read_register(dev, 0x0000, 4, buf, sizeof(buf), &actual) == ESkinSuccess) { + printf("Serial: %02X %02X %02X %02X\n", buf[0], buf[1], buf[2], buf[3]); + } + + eskin_close(dev); + return 0; +} +``` + +--- + +## 使用 Python + +```bash +cargo build --release +cd example/python +LD_LIBRARY_PATH=../../target/release python3 example.py +``` + +```python +from eskin_ffi import EskinDevice + +with EskinDevice("target/release/libeskin_finger_sdk.so") as dev: + dev.open("/dev/ttyUSB0") + + # 读寄存器 + data = dev.read_register(0x0000, 4) + print(f"Serial: {data.hex()}") + + # 写寄存器 + dev.write_register(0x0030, bytes([0x01, 0x00, 0x00, 0x00])) +``` + +--- + +## FFI 接口一览 + +| 函数 | 说明 | +|------|------| +| `eskin_version()` | 获取 SDK 版本 | +| `eskin_open(path, config)` | 打开串口设备,返回 handle | +| `eskin_close(handle)` | 关闭设备,释放 handle | +| `eskin_read_register(handle, addr, length, buf, buf_len, actual_len)` | 读寄存器原始字节 | +| `eskin_write_register(handle, addr, data, data_len, return_count)` | 写寄存器原始字节 | + +--- + +## 协议格式 + +### Request 帧 + +```text +[start:2B] [data_len:2B LE] [dev_addr:1B] [reserved:1B] [func:1B] +[start_addr:4B LE] [read/write_len:2B LE] [payload:NB] [crc:1B] + +start = [0x55, 0xAA] +func: READ=0xFB, WRITE=0x79 +``` + +### Response 帧 + +```text +[start:2B] [data_len:2B LE] [dev_addr:1B] [reserved:1B] [func:1B] +[start_addr:4B LE] [read/write_len:2B LE] [payload:NB] [status:1B] [crc:1B] + +start = [0x55, 0xAA] (注: 0xAA55 LE) +func: RESPONSE_READ=0xFF, RESPONSE_WRITE=0xF9 +status: 0x00=Success +``` + +--- + +## 错误码 + +| 名称 | 值 | 说明 | +|------|----|------| +| `Success` | 0 | 成功 | +| `InvalidPointer` | 1 | 空指针 | +| `DeviceNotFound` | 2 | 设备未找到 | +| `DeviceAlreadyOpen` | 3 | 设备已打开 | +| `NotInitialized` | 4 | 未初始化 | +| `AlreadyStreaming` | 5 | 已在采集中 | +| `NotStreaming` | 6 | 未在采集 | +| `Timeout` | 9 | 读超时 | +| `ChannelClosed` | 10 | 通道断开 | +| `CrcError` | 14 | CRC 校验失败 | +| `FrameError` | 15 | 帧格式错误 | +| `DeviceError` | 17 | 设备返回错误 | + +--- + +## 测试 + +```bash +cargo test # 全部测试 +cargo test protocol::tests # 仅协议层 +``` + +--- + +## 架构 + +```text +User / C / Python + ↓ FFI + DeviceWrapper (handle) + ↓ + EskinDeviceInner + ↓ read_register / write_register + EskinProtocolCodec (encode/decode + CRC8) + ↓ + SerialTransport (write/read bytes) + ↓ + Hardware (UART) +``` + +详细设计见 `docs/PROGRESS.md` 和 `docs/ARCHITECTURE.md`。 + +--- + +## 依赖 + +- `serialport` — 串口访问(需要 `pkg-config` + `libudev-dev`) +- `crc` — CRC-8/X25 校验 +- `crossbeam-channel` — 高性能线程间通信 +- `chrono` — 时间戳 +- `serde` / `serde_json` — 序列化(可选) \ No newline at end of file diff --git a/docs/ARCHITECTURE.md b/docs/ARCHITECTURE.md deleted file mode 100644 index 993bfdc..0000000 --- a/docs/ARCHITECTURE.md +++ /dev/null @@ -1,1546 +0,0 @@ -# Eskin Finger SDK — 架构设计文档 - -## 1. 概述 - -**Eskin Finger SDK** 是一款高性能压敏薄膜传感器数据采集 SDK,用于采集手套式压力传感器阵列的分布力数据(五指 + 掌心共 28 个传感器模组)。采用三层架构设计: - -``` -┌─────────────────────────────────────────────────┐ -│ C++ Wrapper (RAII + Callback) │ -│ cpp/include/eskin_finger_sdk.hpp │ -│ cpp/src/eskin_finger_sdk.cpp │ -├─────────────────────────────────────────────────┤ -│ C ABI Layer (FFI) │ -│ include/eskin_finger_sdk.h │ -│ src/ffi/c_api.rs │ -├─────────────────────────────────────────────────┤ -│ Rust Core (Channel + Thread) │ -│ src/lib.rs, device.rs, stream.rs │ -│ src/channel.rs, types.rs, error.rs │ -│ src/protocol.rs, src/transport.rs │ -└─────────────────────────────────────────────────┘ -``` - -- **Rust Core**:UART 通信、协议解析、数据采集线程、Channel 通信、错误管理 -- **C ABI Layer**:通过 `#[repr(C)]` 和 `extern "C"` 提供稳定的 FFI 接口 -- **C++ Wrapper**:RAII 封装、异常处理、回调机制 - ---- - -## 2. 通信协议概述 - -### 2.1 UART 物理层 - -| 参数 | 值 | -|------|-----| -| 波特率 | 115200 | -| 数据位 | 8 bit | -| 停止位 | 1 位 | -| 校验位 | NONE | -| 通信模式 | 请求-应答(主从模式) | - -### 2.2 帧结构 - -#### 读请求帧 - -``` -┌────────┬──────────┬────────────┬────────┬────────┬──────────┬──────────────┬───────────┬────────┐ -│ 起始符 │ 数据长度 │ 设备地址 │ 预留 │功能码 │ 起始地址 │ 读取数据长度 │ CRC-8 │ │ -│ 0x55AA │ 2B(LE) │ 1B │ 0x00 │ 0xFB │ 4B(LE) │ 2B(LE) │ 1B │ │ -├────────┼──────────┼────────────┼────────┼────────┼──────────┼──────────────┼──────────┼────────┤ -│data[0-1]│data[2-3]│ data[4] │data[5] │data[6] │data[7-10]│ data[11-12] │ data[13] │ │ -└────────┴──────────┴────────────┴────────┴────────┴──────────┴──────────────┴──────────┘ -``` - -#### 读应答帧 - -``` -┌────────┬──────────┬────────────┬────────┬────────────┬──────────┬───────────┬──────────┬────────┬────────┐ -│ 起始符 │ 数据长度 │ 设备地址 │ 预留 │ 功能码 │ 起始地址 │ 读取数据 │ CRC-8 │ 状态 │ │ -│ 0xAA55 │ 2B(LE) │ 1B │ 0x00 │ 0x80+func │ 4B(LE) │ 2B(LE) │ 1B │ 1B │ │ -├────────┼──────────┼────────────┼────────┼────────────┼──────────┼───────────┼──────────┼────────┤ -│data[0-1]│data[2-3]│ data[4] │data[5] │ data[6] │data[7-10]│data[11-12]│data[N+13]│data[13]│ │ -└────────┴──────────┴────────────┴────────┴────────────┴──────────┴───────────┴──────────┴────────┘ -``` - -#### 写请求帧 - -``` -┌────────┬──────────┬────────────┬────────┬────────┬──────────┬──────────────┬───────────┬────────┐ -│ 起始符 │ 数据长度 │ 设备地址 │ 预留 │功能码 │ 起始地址 │ 写入数据长度 │ 写入数据 │ CRC-8 │ -│ 0x55AA │ 2B(LE) │ 1B │ 0x00 │ 0x79 │ 4B(LE) │ 2B(LE) │ NB │ 1B │ -├────────┼──────────┼────────────┼────────┼────────┼──────────┼──────────────┼──────────┼────────┤ -│data[0-1]│data[2-3]│ data[4] │data[5] │data[6] │data[7-10]│ data[11-12] │ data[] │data[N+13]│ -└────────┴──────────┴────────────┴────────┴────────┴──────────┴──────────────┴──────────┴────────┘ -``` - -#### 写应答帧 - -``` -┌────────┬──────────┬────────────┬────────┬────────────┬──────────┬───────────┬────────┬────────┐ -│ 起始符 │ 数据长度 │ 设备地址 │ 预留 │ 功能码 │ 起始地址 │ 返回字节数 │ CRC-8 │ 状态 │ -│ 0xAA55 │ 2B(LE) │ 1B │ 0x00 │ 0x80+func │ 4B(LE) │ 2B(LE) │ 1B │ 1B │ -├────────┼──────────┼────────────┼────────┼────────────┼──────────┼───────────┼────────┼────────┤ -│data[0-1]│data[2-3]│ data[4] │data[5] │ data[6] │data[7-10]│data[11-12]│data[14]│data[13]│ -└────────┴──────────┴────────────┴────────┴────────────┴──────────┴───────────┴────────┴────────┘ -``` - -### 2.3 状态码 - -| 值 | 含义 | -|----|------| -| `0x0000` | 正常(成功) | -| `0x0001` | 读取长度超限 | -| `0x0002` | 长度错误 | -| `0x0003` | 无效地址 | -| `0x0004` | 只读寄存器 | - -### 2.4 功能码 - -| 功能码 | 方向 | 说明 | -|--------|------|------| -| `0xFB` | 请求 → | 读请求 | -| `0x79` | 请求 → | 写请求 | -| `0x80 + func` | ← 应答 | 读/写应答(`0xFF` = 读应答,`0xF9` = 写应答) | - ---- - -## 3. 传感器模组布局 - -### 3.1 模组分布(28 个传感器模组) - -``` - ┌──────────────────────────────────────────────────────────────┐ - │ 传感器手套布局 │ - │ │ - │ 大拇指 食指 中指 无名指 小拇指 │ - │ ┌──┐ ┌──┐ ┌──┐ ┌──┐ ┌──┐ │ - │ │指甲│ │指甲│ │指甲│ │指甲│ │指甲│ │ - │ ├──┤ ├──┤ ├──┤ ├──┤ ├──┤ │ - │ │指尖│ │指尖│ │指尖│ │指尖│ │指尖│ │ - │ ├──┤ ├──┤ ├──┤ ├──┤ ├──┤ │ - │ │中节│ │中节│ │中节│ │中节│ │中节│ │ - │ ├──┤ ├──┤ ├──┤ ├──┤ ├──┤ │ - │ │近节│ │近节│ │近节│ │近节│ │近节│ │ - │ └──┘ └──┘ └──┘ └──┘ └──┘ │ - │ │ - │ ┌─────────────────┐ │ - │ │ 掌心 1-8 │ │ - │ │ (8个模组) │ │ - │ └─────────────────┘ │ - └──────────────────────────────────────────────────────────────┘ -``` - -### 3.2 模组编号(0-27) - -| 编号 | 模组 | 编号 | 模组 | -|------|------|------|------| -| 0 | 大拇指-近节 | 14 | 无名指-近节 | -| 1 | 大拇指-中节 | 15 | 无名指-中节 | -| 2 | 大拇指-指尖 | 16 | 无名指-指尖 | -| 3 | 大拇指-指甲 | 17 | 无名指-指甲 | -| 4 | 食指-近节 | 18 | 小拇指-近节 | -| 5 | 食指-中节 | 19 | 小拇指-中节 | -| 6 | 食指-指尖 | 20 | 小拇指-指尖 | -| 7 | 食指-指甲 | 21 | 小拇指-指甲 | -| 8 | 中指-近节 | 22 | 掌心 1 | -| 9 | 中指-中节 | 23 | 掌心 2 | -| 10 | 中指-指尖 | 24 | 掌心 3 | -| 11 | 中指-指甲 | 25 | 掌心 4 | -| 12 | 无名指-近节 | 26 | 掌心 5 | -| 13 | 无名指-中节 | 27 | 掌心 6 | - -> 注:掌心 7、掌心 8 为扩展编号 28-29(部分配置可能不包含)。 - ---- - -## 4. 寄存器地址映射 - -### 4.1 系统信息寄存器 - -| 地址 | 名称 | 权限 | 数据类型 | 说明 | -|------|------|------|---------|------| -| `0x0000-0x0001` | 系列号 | RO | `u32` | 设备唯一序列号 | -| `0x000F` | 固件版本 | RO | `u16` | `0x0000` = 系列号,`0x0001` = 软件版本号 | -| `0x0010` | 标定组 | RO | `u16` | 标定参数组,用于确认产品状态 | -| `0x0011` | 模组有效状态 | RO | `u16` | bit0-3: 大拇指近/中/指/甲,bit4-7: 食指近/中/指/甲 | -| `0x0012` | 传感器尺寸 L_LINE | RO | `u16` | 宽度阵列点数 | -| `0x0013` | 传感器尺寸 H_LINE | RO | `u16` | 长度阵列点数 | -| `0x0014` | 分布力点数(大拇指) | RO | `u16` | 分布力字节数 = 该值 × 3 | -| `0x0015` | 分布力点数(食指) | RO | `u16` | 同上 | -| `0x0016` | 分布力点数(中指) | RO | `u16` | 同上 | -| `0x0017` | 分布力点数(无名指) | RO | `u16` | 同上 | -| `0x0018` | 分布力点数(小拇指) | RO | `u16` | 同上 | -| `0x0023` | 当前模块位置 | RO | `u16` | 0-3: 大拇指,4-7: 食指,... 20-27: 掌心 | - -### 4.2 配置寄存器 - -| 地址 | 名称 | 权限 | 数据类型 | 说明 | -|------|------|------|---------|------| -| `0x0030-0x0031` | 产品配置 1 | RW | `u32` | bit0: 自动回传使能(1=使能,0=不使能,默认0) | -| `0x0032-0x0033` | 产品配置 2 | RW | `u32` | 保留 | -| `0x0034-0x003F` | 大拇指分布力点数 | RO | `u16[6]` | 近节、中节、指尖、指甲各模组的分布力点数 | -| `0x0040-0x004B` | 食指分布力点数 | RO | `u16[6]` | 同上 | -| `0x004C-0x0057` | 中指分布力点数 | RO | `u16[6]` | 同上 | -| `0x0058-0x0059` | 无名指分布力点数 | RO | `u16[2]` | 近节、中节 | -| `0x005A-0x005B` | 无名指分布力点数 | RO | `u16[2]` | 指尖、指甲 | -| `0x005C-0x0067` | 掌心分布力点数 | RO | `u16[6]` | 掌心8-1(倒序) | - -### 4.3 数据寄存器 - -| 地址 | 名称 | 权限 | 数据大小 | 说明 | -|------|------|------|---------|------| -| `0x0500-0x05A7` | 合力数据 | RO | 168B | 28个模组,每个 3×2B (fx,fy,fz 为 i16) | -| `0x0700-0x0737` | 模组错误码 | RO | 56B | 28个模组,每个 2B | -| `0x1000-0x11FF` | 大拇指分布力 | RO | 变长 | 三维分布力 (fx,fy,fz 每点 3B) | -| `0x1200-0x13FF` | 食指分布力 | RO | 变长 | 同上 | -| `0x1400-0x15FF` | 中指分布力 | RO | 变长 | 同上 | -| `0x1600-0x17FF` | 无名指分布力 | RO | 变长 | 同上 | -| `0x1800-0x19FF` | 小拇指分布力 | RO | 变长 | 同上 | -| `0x1A00-0x1BFF` | 掌心分布力(1-4) | RO | 变长 | 同上 | -| `0x1C00-0x1DFF` | 掌心分布力(5-8) | RO | 变长 | 同上 | -| `0x2000-0x2FFF` | 处理后数值 | RO | 变长 | 原始地址 + 0x3000 | -| `0x8000-0x8FFF` | 标定参数 | RW | 变长 | 每点 32 参数,最多 128 点 | - ---- - -## 5. 目录结构 - -``` -eskin-finger-sdk/ -├── Cargo.toml # Rust crate 配置 -├── CMakeLists.txt # C++ wrapper 构建配置 -├── docs/ -│ └── ARCHITECTURE.md # 本文档 -├── include/ -│ └── eskin_finger_sdk.h # C 头文件(公共 API) -├── cpp/ -│ ├── include/ -│ │ └── eskin_finger_sdk.hpp # C++ wrapper 头文件 -│ └── src/ -│ └── eskin_finger_sdk.cpp # C++ wrapper 实现 -├── src/ -│ ├── lib.rs # Rust 库入口 -│ ├── main.rs # 示例/测试入口 -│ ├── types.rs # 数据建模(传感器结构体、枚举) -│ ├── error.rs # 错误类型定义 -│ ├── protocol.rs # UART 协议解析(帧编解码、CRC-8-ITU) -│ ├── transport.rs # 串口传输层(读写抽象) -│ ├── register.rs # 寄存器地址映射与读写封装 -│ ├── device.rs # 设备管理核心 -│ ├── channel.rs # Channel 设计与管理 -│ ├── stream.rs # 数据流控制(分布力采集) -│ ├── config.rs # 设备配置管理 -│ └── ffi/ -│ ├── mod.rs # FFI 模块声明 -│ └── c_api.rs # C ABI 实现 -└── examples/ - └── basic_usage.rs # Rust 使用示例 -``` - ---- - -## 6. Crate 依赖 (Cargo.toml) - -```toml -[package] -name = "eskin-finger-sdk" -version = "0.1.0" -edition = "2024" - -[lib] -name = "eskin_finger_sdk" -crate-type = ["lib", "cdylib", "staticlib"] - -[dependencies] -# Channel 通信 — 高性能多生产者多消费者通道 -crossbeam-channel = "0.5" - -# 错误处理 — 结构化错误类型 -thiserror = "2" - -# 日志 — 可选的日志输出 -log = "0.4" - -# 日志前端 -fern = "0.7" - -# 时间处理 — 高精度时间戳 -chrono = "0.4" - -# 序列化 — 配置和数据的序列化支持 -serde = { version = "1", features = ["derive"] } -serde_json = "1" - -# 串口传输 — UART/USB-Serial 通信 -serialport = "4" - -# FFI 类型支持(C 类型兼容) -libc = "0.2" - -# UUID — 设备序列号等唯一标识 -uuid = { version = "1", features = ["v4"] } - -[dev-dependencies] -# 测试工具 -env_logger = "0.11" -``` - -### 依赖说明 - -| 依赖 | 用途 | 必要性 | -|------|------|--------| -| `crossbeam-channel` | 高性能 bounded/unbounded channel,支持 `try_send`、`recv_timeout` | **核心** | -| `thiserror` | 派生 `std::error::Error`,简化错误类型定义 | **核心** | -| `log` / `fern` | 日志抽象层 + 日志前端,方便调试 | 推荐 | -| `chrono` | 高精度时间戳(微秒/纳秒级) | 推荐 | -| `serde` / `serde_json` | 配置文件序列化、调试输出 | 推荐 | -| `serialport` | 串口打开、读写、清空缓冲区,承载设备 UART 协议 | **核心** | -| `libc` | FFI 类型兼容(`c_char`, `c_int` 等) | **FFI 必需** | -| `uuid` | 设备唯一标识符生成 | 可选 | - -### 编译产物 - -```toml -crate-type = ["lib", "cdylib", "staticlib"] -``` - -- `lib`:Rust 内部使用 -- `cdylib`:生成 `.dylib`/`.so`/`.dll`,供 C/C++ 动态链接 -- `staticlib`:生成 `.a`/`.lib`,供 C/C++ 静态链接 - ---- - -## 7. 数据建模 - -### 7.1 核心数据类型 - -```rust -// src/types.rs - -/// 三维力向量(分布力中的单个测力点) -#[repr(C)] -#[derive(Debug, Clone, Copy, Default, Serialize, Deserialize)] -pub struct Force3D { - pub fx: i16, // X 方向力 (原始值) - pub fy: i16, // Y 方向力 (原始值) - pub fz: i16, // Z 方向力 (原始值) -} - -/// 三维力向量(浮点版本,用于合力) -#[repr(C)] -#[derive(Debug, Clone, Copy, Default, Serialize, Deserialize)] -pub struct Force3F { - pub fx: f32, // X 方向力 (N) - pub fy: f32, // Y 方向力 (N) - pub fz: f32, // Z 方向力 (N) -} - -/// 传感器模组标识 -#[repr(C)] -#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)] -pub enum SensorModule { - // 大拇指 - ThumbProximal = 0, // 大拇指近节 - ThumbMiddle = 1, // 大拇指中节 - ThumbTip = 2, // 大拇指指尖 - ThumbNail = 3, // 大拇指指甲 - - // 食指 - IndexProximal = 4, - IndexMiddle = 5, - IndexTip = 6, - IndexNail = 7, - - // 中指 - MiddleProximal = 8, - MiddleMiddle = 9, - MiddleTip = 10, - MiddleNail = 11, - - // 无名指 - RingProximal = 12, - RingMiddle = 13, - RingTip = 14, - RingNail = 15, - - // 小拇指 - PinkyProximal = 16, - PinkyMiddle = 17, - PinkyTip = 18, - PinkyNail = 19, - - // 掌心 - Palm1 = 20, - Palm2 = 21, - Palm3 = 22, - Palm4 = 23, - Palm5 = 24, - Palm6 = 25, - Palm7 = 26, - Palm8 = 27, -} - -/// 单个传感器模组的分布力数据 -#[repr(C)] -#[derive(Debug, Clone, Serialize, Deserialize)] -pub struct DistributionForce { - pub module: SensorModule, - /// 分布力点数(每个点 3 字节: fx, fy, fz) - pub point_count: u16, - /// 分布力数据(原始 i8 值,长度 = point_count * 3) - pub points: Vec, -} - -/// 单个传感器模组的合力数据 -#[repr(C)] -#[derive(Debug, Clone, Copy, Serialize, Deserialize)] -pub struct CombinedForce { - pub module: SensorModule, - /// X 方向合力 (i16 原始值) - pub fx: i16, - /// Y 方向合力 (i16 原始值) - pub fy: i16, - /// Z 方向合力 (i16 原始值) - pub fz: i16, -} - -/// 一帧完整的传感器采样数据 -#[repr(C)] -#[derive(Debug, Clone, Serialize, Deserialize)] -pub struct FingerSample { - /// 时间戳(微秒,SDK 侧采集时间) - pub timestamp_us: u64, - /// 采样序列号(SDK 侧递增) - pub sequence: u32, - /// 28 个模组的合力数据 - pub combined_forces: [CombinedForce; 28], - /// 28 个模组的分布力数据(可选,根据需求拉取) - pub distribution_forces: Vec, - /// 28 个模组的错误码(每个模组 2 字节) - pub module_errors: [u16; 28], -} - -/// 单个测力点的简化数据(用于 C ABI 传递分布力) -#[repr(C)] -#[derive(Debug, Clone, Copy, Default, Serialize, Deserialize)] -pub struct ForcePoint { - pub fx: i8, - pub fy: i8, - pub fz: i8, -} -``` - -### 7.2 设备配置 - -```rust -// src/config.rs - -/// 设备配置 -#[repr(C)] -#[derive(Debug, Clone, Serialize, Deserialize)] -pub struct DeviceConfig { - /// 设备地址(默认 0x34) - pub device_addr: u8, - - /// 是否启用自动回传模式 - /// bit0: 1=使能, 0=不使能(默认0) - pub auto_transmit: bool, - - /// 是否读取分布力数据(合力数据始终读取) - pub read_distribution: bool, - - /// 样本丢弃策略 - pub drop_policy: DropPolicy, -} - -/// 样本丢弃策略 -#[repr(C)] -#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)] -pub enum DropPolicy { - /// 丢弃新数据 — 通道满时丢弃当前新样本,保留旧数据 - DropNewest, - /// 丢弃旧数据 — 通道满时丢弃队列头部的旧数据,保留最新样本 - DropOldest, -} -``` - -### 7.3 系统信息 - -```rust -/// 设备系统信息(从只读寄存器读取) -#[repr(C)] -#[derive(Debug, Clone, Serialize, Deserialize)] -pub struct DeviceInfo { - /// 系列号 - pub serial_number: u32, - /// 固件版本 - pub firmware_version: u16, - /// 标定组 - pub calibration_group: u16, - /// 模组有效状态(bit 掩码) - pub module_active_status: u16, - /// 传感器宽度阵列点数 (L_LINE) - pub l_line: u16, - /// 传感器长度阵列点数 (H_LINE) - pub h_line: u16, - /// 产品配置 1 - pub product_config_1: u32, - /// 产品配置 2 - pub product_config_2: u32, -} -``` - -### 7.4 协议帧类型 - -```rust -// src/protocol.rs - -/// 帧起始符 -pub const FRAME_START_REQUEST: u16 = 0x55AA; -pub const FRAME_START_RESPONSE: u16 = 0xAA55; - -/// 功能码 -pub const FUNC_READ: u8 = 0xFB; -pub const FUNC_WRITE: u8 = 0x79; -pub const FUNC_RESPONSE_READ: u8 = 0xFF; // 0x80 + 0xFB -pub const FUNC_RESPONSE_WRITE: u8 = 0xF9; // 0x80 + 0x79 - -/// 设备状态码(应答帧中的 status 字段) -#[repr(C)] -#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)] -pub enum DeviceStatus { - Success = 0x0000, // 正常 - ReadLenExceeded = 0x0001, // 读取长度超限 - LengthError = 0x0002, // 长度错误 - InvalidAddress = 0x0003, // 无效地址 - ReadOnlyRegister = 0x0004, // 只读寄存器 -} - -/// 读请求帧结构 -pub struct ReadRequest { - pub device_addr: u8, - pub start_addr: u32, - pub read_byte_count: u16, -} - -/// 写请求帧结构 -pub struct WriteRequest { - pub device_addr: u8, - pub start_addr: u32, - pub data: Vec, -} - -/// 读应答帧结构 -pub struct ReadResponse { - pub device_addr: u8, - pub start_addr: u32, - pub data: Vec, - pub status: DeviceStatus, -} - -/// 写应答帧结构 -pub struct WriteResponse { - pub device_addr: u8, - pub start_addr: u32, - pub return_byte_count: u16, - pub status: DeviceStatus, -} -``` - ---- - -## 8. 错误处理 - -### 8.1 SDK 错误码(C ABI) - -```rust -// src/error.rs - -/// C ABI 兼容的错误码 -#[repr(C)] -#[derive(Debug, Clone, Copy, PartialEq, Eq)] -pub enum SdkErrorCode { - Success = 0, - InvalidPointer = 1, - DeviceNotFound = 2, - DeviceAlreadyOpen = 3, - NotInitialized = 4, - AlreadyStreaming = 5, - NotStreaming = 6, - ConfigError = 7, - IoError = 8, - Timeout = 9, - ChannelClosed = 10, - InternalError = 11, - BufferOverflow = 12, - InvalidParameter = 13, - CrcError = 14, - FrameError = 15, - ProtocolError = 16, - DeviceError = 17, // 设备返回非零状态码 -} - -/// Rust 内部错误类型 -#[derive(Debug, thiserror::Error)] -pub enum SdkError { - #[error("Device not found: {0}")] - DeviceNotFound(String), - - #[error("Device already open")] - DeviceAlreadyOpen, - - #[error("SDK not initialized")] - NotInitialized, - - #[error("Already streaming")] - AlreadyStreaming, - - #[error("Not streaming")] - NotStreaming, - - #[error("Configuration error: {0}")] - ConfigError(String), - - #[error("I/O error: {0}")] - IoError(#[from] std::io::Error), - - #[error("Read timeout")] - Timeout, - - #[error("Channel closed")] - ChannelClosed, - - #[error("Internal error: {0}")] - InternalError(String), - - #[error("Buffer overflow — dropped {0} samples")] - BufferOverflow(u64), - - #[error("Invalid parameter: {0}")] - InvalidParameter(String), - - #[error("CRC error: expected 0x{expected:02X}, got 0x{actual:02X}")] - CrcError { expected: u8, actual: u8 }, - - #[error("Frame error: {0}")] - FrameError(String), - - #[error("Protocol error: {0}")] - ProtocolError(String), - - #[error("Device error: status 0x{0:04X}")] - DeviceError(u16), -} -``` - -### 8.2 设备侧错误码 - -```rust -/// 设备应答帧中的状态码 -impl DeviceStatus { - pub fn to_error(&self) -> Option { - match self { - DeviceStatus::Success => None, - DeviceStatus::ReadLenExceeded => Some(SdkError::DeviceError(0x0001)), - DeviceStatus::LengthError => Some(SdkError::DeviceError(0x0002)), - DeviceStatus::InvalidAddress => Some(SdkError::DeviceError(0x0003)), - DeviceStatus::ReadOnlyRegister => Some(SdkError::DeviceError(0x0004)), - } - } -} -``` - ---- - -## 9. CRC-8-ITU 校验 - -```rust -// src/protocol.rs - -/// CRC-8-ITU 多项式: x^8 + x^2 + x + 1 (0x07) -/// 初始值: 0x00 -pub fn crc8_itu(data: &[u8]) -> u8 { - let mut crc: u8 = 0x00; - for &byte in data { - crc ^= byte; - for _ in 0..8 { - if crc & 0x80 != 0 { - crc = (crc << 1) ^ 0x07; - } else { - crc <<= 1; - } - } - } - crc -} -``` - ---- - -## 10. Channel 设计 - -### 10.1 通道架构 - -``` - ┌──────────────────────┐ - │ UART I/O Thread │ - │ (串口数据采集线程) │ - └──────┬───────┬───────┘ - │ │ - sample_tx event_tx - │ │ - ┌────────────▼─┐ ┌──▼───────────┐ - │ Data Channel │ │ Event Channel │ - │ (bounded) │ │ (bounded) │ - │ capacity: 64 │ │ capacity: 64 │ - └────────────┬─┘ └──┬────────────┘ - │ │ - sample_rx event_rx - │ │ - ┌────────────▼───────▼────────────┐ - │ User-facing API │ - │ read_sample() / callback │ - └──────────────────────────────────┘ - - ┌──────────────────────┐ - │ User Thread │ - └──────┬───────────────┘ - │ - cmd_tx - │ - ┌──────▼──────────────┐ - │ Command Channel │ - │ (bounded) │ - │ capacity: 16 │ - └──────┬───────────────┘ - │ - cmd_rx - ┌──────▼──────────────┐ - │ UART I/O Thread │ - └──────────────────────┘ -``` - -### 10.2 通道定义 - -```rust -// src/channel.rs - -use crossbeam_channel::{bounded, Sender, Receiver}; -use std::sync::atomic::{AtomicU64, Ordering}; - -/// 设备命令 -#[derive(Debug, Clone)] -pub enum DeviceCommand { - StartStream, - StopStream, - SetConfig(DeviceConfig), - /// 读取指定寄存器 - ReadRegister { addr: u32, length: u16 }, - /// 写入指定寄存器 - WriteRegister { addr: u32, data: Vec }, - Shutdown, -} - -/// 设备事件 -#[derive(Debug, Clone)] -pub enum DeviceEvent { - Disconnected(String), - IoError(String), - ProtocolError(String), - ConfigApplied, - StreamStarted, - StreamStopped, - SampleDropped { count: u64 }, - ModuleError { module: SensorModule, error_code: u16 }, -} - -/// 通道管理器 -pub struct ChannelManager { - // 数据通道 — 传感器采样数据 - pub sample_tx: Sender, - pub sample_rx: Receiver, - - // 命令通道 — 控制命令 - pub cmd_tx: Sender, - pub cmd_rx: Receiver, - - // 事件通道 — 事件通知 - pub event_tx: Sender, - pub event_rx: Receiver, - - // 丢弃计数器 - pub dropped_samples: AtomicU64, - - // 丢弃策略 - pub drop_policy: DropPolicy, -} - -impl ChannelManager { - pub fn new( - sample_capacity: usize, // 建议 64(传感器帧率较低) - cmd_capacity: usize, // 建议 16 - event_capacity: usize, // 建议 64 - drop_policy: DropPolicy, - ) -> Self { - let (sample_tx, sample_rx) = bounded(sample_capacity); - let (cmd_tx, cmd_rx) = bounded(cmd_capacity); - let (event_tx, event_rx) = bounded(event_capacity); - - Self { - sample_tx, sample_rx, - cmd_tx, cmd_rx, - event_tx, event_rx, - dropped_samples: AtomicU64::new(0), - drop_policy, - } - } - - /// 非阻塞发送样本,根据策略处理溢出 - pub fn send_sample(&self, sample: FingerSample) { - match self.drop_policy { - DropPolicy::DropNewest => { - if self.sample_tx.try_send(sample).is_err() { - self.dropped_samples.fetch_add(1, Ordering::Relaxed); - } - } - DropPolicy::DropOldest => { - if let Err(crossbeam_channel::TrySendError::Full(_)) = - self.sample_tx.try_send(sample) - { - let _ = self.sample_rx.try_recv(); - if self.sample_tx.try_send(sample).is_err() { - self.dropped_samples.fetch_add(1, Ordering::Relaxed); - } - } - } - } - } - - /// 带超时的样本读取 - pub fn recv_sample(&self, timeout_ms: u32) -> Result { - let timeout = std::time::Duration::from_millis(timeout_ms as u64); - self.sample_rx - .recv_timeout(timeout) - .map_err(|_| SdkError::Timeout) - } - - /// 获取丢弃的样本数量 - pub fn dropped_count(&self) -> u64 { - self.dropped_samples.load(Ordering::Relaxed) - } -} -``` - -### 10.3 通道容量配置 - -| 通道 | 容量 | 频率 | 说明 | -|------|------|------|------| -| 数据通道 (sample) | 64 | 低频 (~10-100Hz) | bounded,传感器帧率决定 | -| 命令通道 (cmd) | 16 | 低频 (~1Hz) | bounded,固定 | -| 事件通道 (event) | 64 | 低频 (~1Hz) | bounded,固定 | - ---- - -## 11. 设备管理 - -### 11.1 设备状态机 - -``` - ┌──────────┐ init() ┌──────────────┐ - │ Idle │ ──────────────►│ Initialized │ - └──────────┘ └──────┬───────┘ - ▲ │ - │ open(port) - │ │ - │ ┌────▼───────┐ - │ │ Connected │ - │ └────┬───────┘ - │ │ - │ start_stream() - │ │ - │ ┌────▼───────┐ - │ │ Streaming │ - │ └────┬───────┘ - │ │ - │ stop_stream() - │ │ - │ ┌────▼───────┐ - │ close() │ Connected │ - │◄───────────────────────└────────────┘ - │ - shutdown() -``` - -### 11.2 核心设备结构 - -```rust -// src/device.rs - -pub struct EskinDeviceInner { - pub info: DeviceInfo, - pub config: DeviceConfig, - pub channels: ChannelManager, - pub state: DeviceState, - pub transport: Box, - pub stream_handle: Option>, - pub cmd_handle: Option>, - pub shutdown_flag: Arc, - pub sequence_counter: AtomicU32, -} - -#[derive(Debug, Clone, Copy, PartialEq, Eq)] -pub enum DeviceState { - Idle, - Initialized, - Connected, - Streaming, - Error, -} -``` - -### 11.3 传输层抽象 - -```rust -// src/transport.rs - -/// 串口传输抽象(方便测试时 mock) -pub trait SerialTransport: Send + Sync { - /// 发送原始字节 - fn write(&mut self, data: &[u8]) -> Result; - /// 接收原始字节(带超时) - fn read(&mut self, buf: &mut [u8], timeout: Duration) -> Result; - /// 清空接收缓冲区 - fn flush_rx(&mut self) -> Result<(), SdkError>; -} - -/// 真实串口实现 -pub struct SerialPortTransport { - port: Box, -} -``` - ---- - -## 12. 数据采集流程 - -### 12.1 合力数据采集(标准模式) - -``` -用户调用 start_stream() - │ - ▼ -┌──────────────────────────────────────────┐ -│ UART I/O Thread 启动 │ -│ │ -│ loop { │ -│ 1. 检查 shutdown_flag │ -│ 2. 检查 cmd_rx 是否有新命令 │ -│ 3. 发送读请求: 0x0500, 长度 168 字节 │ -│ (28 模组 × 6 字节合力) │ -│ 4. 接收读应答帧 │ -│ 5. 校验 CRC-8-ITU │ -│ 6. 解析 28 个 CombinedForce │ -│ 7. 组装 FingerSample │ -│ 8. sample_tx.try_send(sample) │ -│ 9. 如果配置了 read_distribution: │ -│ a. 依次发送读请求获取各指分布力 │ -│ b. 填充 distribution_forces │ -│ } │ -└──────────────────────────────────────────┘ -``` - -### 12.2 分布力数据采集 - -分布力数据按模组分段读取,每段地址范围不同: - -| 传感器 | 地址范围 | 每点字节数 | -|--------|---------|-----------| -| 大拇指 | `0x1000-0x11FF` | 3 (fx, fy, fz) | -| 食指 | `0x1200-0x13FF` | 3 | -| 中指 | `0x1400-0x15FF` | 3 | -| 无名指 | `0x1600-0x17FF` | 3 | -| 小拇指 | `0x1800-0x19FF` | 3 | -| 掌心 1-4 | `0x1A00-0x1BFF` | 3 | -| 掌心 5-8 | `0x1C00-0x1DFF` | 3 | - -> 分布力字节数 = point_count × 3,point_count 从 `0x0014-0x0018` 等寄存器读取。 - -### 12.3 命令处理流程 - -``` -cmd_rx 接收到命令 - │ - ▼ -match command { - StartStream → 设置 streaming 标志,开始采集循环 - StopStream → 清除 streaming 标志 - SetConfig(cfg) → 写入产品配置寄存器 (0x0030) - ReadRegister → 发送读请求,返回结果 - WriteRegister → 发送写请求,检查状态码 - Shutdown → 设置 shutdown_flag,退出线程 -} -``` - ---- - -## 13. C ABI 设计 - -### 13.1 Opaque Pointer 模式 - -```rust -// src/ffi/c_api.rs - -/// 不透明指针,C 端只能通过 API 函数操作 -#[repr(C)] -pub struct EskinDevice { - inner: Box, -} - -/// 全局 SDK 状态 -static SDK_STATE: OnceLock> = OnceLock::new(); - -struct SdkState { - initialized: bool, -} -``` - -### 13.2 C API 函数签名 - -```c -// include/eskin_finger_sdk.h - -#ifndef ESKIN_FINGER_SDK_H -#define ESKIN_FINGER_SDK_H - -#include -#include - -#ifdef __cplusplus -extern "C" { -#endif - -// ─── 错误码 ────────────────────────────────────────── -typedef enum { - ESKIN_SUCCESS = 0, - ESKIN_INVALID_POINTER = 1, - ESKIN_DEVICE_NOT_FOUND = 2, - ESKIN_DEVICE_ALREADY_OPEN = 3, - ESKIN_NOT_INITIALIZED = 4, - ESKIN_ALREADY_STREAMING = 5, - ESKIN_NOT_STREAMING = 6, - ESKIN_CONFIG_ERROR = 7, - ESKIN_IO_ERROR = 8, - ESKIN_TIMEOUT = 9, - ESKIN_CHANNEL_CLOSED = 10, - ESKIN_INTERNAL_ERROR = 11, - ESKIN_BUFFER_OVERFLOW = 12, - ESKIN_INVALID_PARAMETER = 13, - ESKIN_CRC_ERROR = 14, - ESKIN_FRAME_ERROR = 15, - ESKIN_PROTOCOL_ERROR = 16, - ESKIN_DEVICE_ERROR = 17, -} EskinErrorCode; - -// ─── 数据类型 ──────────────────────────────────────── -typedef struct { int16_t fx, fy, fz; } EskinForce3D; - -typedef struct { - uint8_t module_index; // 0-27 - int16_t fx, fy, fz; -} EskinCombinedForce; - -typedef struct { - uint8_t module_index; - uint16_t point_count; - EskinForce3D* points; // 长度 = point_count -} EskinDistributionForce; - -typedef struct { - uint64_t timestamp_us; - uint32_t sequence; - EskinCombinedForce combined_forces[28]; - EskinDistributionForce* distribution_forces; // 可选,NULL 表示不采集 - uint16_t distribution_count; - uint16_t module_errors[28]; -} EskinSample; - -typedef struct { - uint32_t serial_number; - uint16_t firmware_version; - uint16_t calibration_group; - uint16_t module_active_status; - uint16_t l_line; - uint16_t h_line; - uint32_t product_config_1; - uint32_t product_config_2; -} EskinDeviceInfo; - -typedef enum { - ESKIN_DROP_NEWEST = 0, - ESKIN_DROP_OLDEST = 1, -} EskinDropPolicy; - -typedef struct { - uint8_t device_addr; // 默认 0x34 - int auto_transmit; // 0=不使能, 1=使能 - int read_distribution; // 0=只读合力, 1=也读分布力 - EskinDropPolicy drop_policy; -} EskinConfig; - -// ─── 设备管理 API ──────────────────────────────────── -EskinErrorCode eskin_initialize(void); -EskinErrorCode eskin_shutdown(void); - -EskinErrorCode eskin_open(const char* port_name, EskinDevice** out_device); -EskinErrorCode eskin_open_by_serial(const char* serial, EskinDevice** out_device); -void eskin_close(EskinDevice* device); - -// ─── 设备信息 API ──────────────────────────────────── -EskinErrorCode eskin_get_device_info(EskinDevice* device, EskinDeviceInfo* out_info); - -// ─── 配置 API ──────────────────────────────────────── -EskinErrorCode eskin_get_config(EskinDevice* device, EskinConfig* config); -EskinErrorCode eskin_set_config(EskinDevice* device, const EskinConfig* config); - -// ─── 流控制 API ────────────────────────────────────── -EskinErrorCode eskin_start_stream(EskinDevice* device); -EskinErrorCode eskin_stop_stream(EskinDevice* device); - -// ─── 数据读取 API ──────────────────────────────────── -EskinErrorCode eskin_read_sample(EskinDevice* device, EskinSample* out_sample, - uint32_t timeout_ms); -uint64_t eskin_get_dropped_sample_count(EskinDevice* device); - -// ─── 寄存器直接访问 API ───────────────────────────── -EskinErrorCode eskin_read_register(EskinDevice* device, uint32_t addr, - uint16_t length, uint8_t* out_data); -EskinErrorCode eskin_write_register(EskinDevice* device, uint32_t addr, - uint16_t length, const uint8_t* data); - -// ─── 错误管理 API ──────────────────────────────────── -const char* eskin_error_string(EskinErrorCode code); -EskinErrorCode eskin_get_last_error(EskinDevice* device); - -// ─── 内存管理 API ──────────────────────────────────── -void eskin_free_sample(EskinSample* sample); - -#ifdef __cplusplus -} -#endif - -#endif // ESKIN_FINGER_SDK_H -``` - ---- - -## 14. C++ Wrapper 设计 - -### 14.1 RAII 封装 - -```cpp -// cpp/include/eskin_finger_sdk.hpp - -#pragma once - -#include "eskin_finger_sdk.h" -#include -#include -#include -#include -#include -#include -#include - -namespace eskin { - -// ─── 异常类型 ──────────────────────────────────────── -class EskinException : public std::runtime_error { -public: - EskinException(EskinErrorCode code, const std::string& msg) - : std::runtime_error(msg), code_(code) {} - EskinErrorCode code() const { return code_; } -private: - EskinErrorCode code_; -}; - -// ─── 数据类型 ──────────────────────────────────────── -struct Force3D { int16_t fx, fy, fz; }; - -struct CombinedForce { - uint8_t module_index; - int16_t fx, fy, fz; -}; - -struct DistributionForce { - uint8_t module_index; - std::vector points; -}; - -struct Sample { - uint64_t timestamp_us; - uint32_t sequence; - std::array combined_forces; - std::vector distribution_forces; - std::array module_errors; -}; - -struct DeviceInfo { - uint32_t serial_number; - uint16_t firmware_version; - uint16_t calibration_group; - uint16_t module_active_status; - uint16_t l_line; - uint16_t h_line; -}; - -// ─── 回调类型 ──────────────────────────────────────── -using SampleCallback = std::function; -using ErrorCallback = std::function; - -// ─── RAII Device 封装 ──────────────────────────────── -class Device { -public: - Device(const Device&) = delete; - Device& operator=(const Device&) = delete; - Device(Device&& other) noexcept; - Device& operator=(Device&& other) noexcept; - ~Device(); - - // 设备信息 - DeviceInfo getDeviceInfo(); - - // 配置 - void setConfig(const EskinConfig& config); - EskinConfig getConfig(); - - // 流控制 - void start(); - void stop(); - - // 同步数据读取 - Sample readSample(uint32_t timeout_ms = 1000); - uint64_t getDroppedSampleCount() const; - - // 寄存器直接访问 - std::vector readRegister(uint32_t addr, uint16_t length); - void writeRegister(uint32_t addr, const std::vector& data); - - // 回调模式 - void setCallback(SampleCallback on_sample); - void setErrorCallback(ErrorCallback on_error); - void startAsync(); - void stopAsync(); - -private: - friend class Sdk; - explicit Device(EskinDevice* handle); - - EskinDevice* handle_ = nullptr; - SampleCallback sample_callback_; - ErrorCallback error_callback_; - std::thread callback_thread_; - std::atomic callback_running_{false}; -}; - -// ─── SDK 全局管理 ──────────────────────────────────── -class Sdk { -public: - Sdk(); - ~Sdk(); - - Sdk(const Sdk&) = delete; - Sdk& operator=(const Sdk&) = delete; - - Device open(const std::string& port_name); - Device openBySerial(const std::string& serial); - - static std::string errorString(EskinErrorCode code); - -private: - bool initialized_ = false; -}; - -} // namespace eskin -``` - ---- - -## 15. 数据流示意 - -### 15.1 同步读取模式(Pull) - -``` -用户线程 SDK 内部 - │ │ - │ eskin_read_sample() │ - │────────────────────────────►│ sample_rx.recv_timeout() - │ │ - │ 内部循环: │ - │ 1. 发送 UART 读请求 │ - │ 2. 接收应答,校验 CRC │ - │ 3. 解析合力/分布力 │ - │ 4. 组装 FingerSample │ - │ 5. 通过 channel 传递 │ - │ │ - │◄────────────────────────────│ 返回 FingerSample 或 Timeout - │ │ - │ eskin_get_dropped_count() │ - │────────────────────────────►│ atomic load - │◄────────────────────────────│ 返回 u64 -``` - -### 15.2 回调模式(Push) - -``` -SDK 内部 用户回调 - │ │ - │ UART I/O 线程采集并解析 │ - │ sample_tx.try_send(sample) │ - │ │ - │ 回调线程 recv sample │ - │────────────────────────────────►│ on_sample(sample) - │ │ - │ 如果采集出错 │ - │────────────────────────────────►│ on_error(code, msg) -``` - ---- - -## 16. 线程模型 - -``` -┌─────────────────────────────────────────────────────┐ -│ Rust SDK 进程 │ -│ │ -│ ┌───────────────────┐ ┌───────────────────┐ │ -│ │ UART I/O Thread │ │ Command Handler │ │ -│ │ (per device) │ │ (same thread) │ │ -│ │ │ │ │ │ -│ │ - 串口读写 │ │ - 接收 cmd_rx │ │ -│ │ - CRC 校验 │ │ - 执行寄存器操作 │ │ -│ │ - 协议解析 │ │ - 写入设备 │ │ -│ │ - 组装样本 │ │ │ │ -│ │ - sample_tx │ │ │ │ -│ │ - event_tx │ │ │ │ -│ └───────┬───────────┘ └───────┬───────────┘ │ -│ │ │ │ -│ ▼ ▼ │ -│ ┌───────────────────────────────────────┐ │ -│ │ Channel Manager │ │ -│ │ - sample channel (bounded 64) │ │ -│ │ - cmd channel (bounded 16) │ │ -│ │ - event channel (bounded 64) │ │ -│ │ - dropped_samples (AtomicU64) │ │ -│ └───────────────────┬───────────────────┘ │ -│ │ │ -│ ┌───────────────────▼───────────────────┐ │ -│ │ User-facing API │ │ -│ │ - C ABI (extern "C") │ │ -│ │ - C++ Wrapper (RAII + callback) │ │ -│ └───────────────────────────────────────┘ │ -└─────────────────────────────────────────────────────┘ -``` - ---- - -## 17. 关键设计决策 - -| 决策 | 选择 | 理由 | -|------|------|------| -| Channel 库 | `crossbeam-channel` | 比 `std::sync::mpsc` 更高性能,支持 MPMC,有 `try_send`、`recv_timeout` | -| 数据通道容量 | 64 (bounded) | 传感器帧率较低 (~10-100Hz),64 帧缓冲足够 | -| 样本发送策略 | `try_send` (非阻塞) | 采集线程绝不阻塞,保证实时性 | -| 丢弃策略 | 可配置 (`DropNewest`/`DropOldest`) | 不同场景需求不同 | -| FFI 指针 | Opaque pointer (`EskinDevice*`) | 隐藏内部结构,ABI 稳定 | -| 内存管理 | Box + raw pointer | Rust 侧 Box 管理,C 侧只持有指针,`close()` 时归还 | -| 错误处理 | 错误码 + last_error | C ABI 用错误码,C++ wrapper 转为异常 | -| 时间戳 | `u64` 微秒 | SDK 侧采集时间,避免浮点精度问题 | -| CRC 校验 | CRC-8-ITU | 协议规定,多项式 0x07,初始值 0x00 | -| 传输抽象 | `SerialTransport` trait | 方便单元测试 mock,可扩展支持其他传输方式 | -| 分布力采集 | 可选配置 | 分布力数据量大,按需采集避免影响帧率 | - ---- - -## 18. 指令示例 - -### 18.1 读取固件版本信息 - -``` -请求: 55 AA 09 00 34 00 FB 00 00 00 00 10 00 76 - ├─起始─┤├─长度─┤├地址┤├─功能─┤├─地址──┤├─长度──┤├CRC┤ - -应答: AA 55 09 00 34 00 FF 00 00 00 00 02 00 xx xx CRC 00 -``` - -### 18.2 读取大拇指指尖分布力点数 - -``` -请求: 55 AA 09 00 34 00 FB 14 00 00 00 02 00 31 - 起始地址 = 0x0014, 读取 2 字节 -``` - -### 18.3 读取食指指尖合力 - -``` -请求: 55 AA 09 00 34 00 FB 00 1C 00 00 A8 00 35 - 起始地址 = 0x1C00, 读取 0xA8 = 168 字节 -``` - -### 18.4 修改食指指尖 L_LINE 值 - -``` -写请求: 55 AA 0A 00 34 00 79 14 00 00 00 01 00 03 A4 - 起始地址 = 0x0014, 写入 1 字节, 数据 = 0x03 -``` - ---- - -## 19. 构建方式 - -### Rust(生成 C 共享库) - -```bash -cargo build --release -# 产物: target/release/libeskin_finger_sdk.dylib (macOS) -# target/release/libeskin_finger_sdk.so (Linux) -# target/release/eskin_finger_sdk.dll (Windows) -``` - -### C++ Wrapper(CMake) - -```bash -mkdir build && cd build -cmake .. -make -``` - -### Python 绑定(未来扩展) - -可以通过 `cffi` 或 `pybind11` 直接加载 C 共享库,无需额外绑定层。 - ---- - -## 20. 当前 Rust Core 接口骨架 - -当前阶段只定义接口和数据边界,具体读写、CRC、解析、线程生命周期后续实现。 - -### 20.1 模块职责 - -| 模块 | 文件 | 职责 | -|------|------|------| -| 数据类型 | `src/types.rs` | 力向量、模组枚举、合力、分布力、采样帧、模组错误 | -| 配置 | `src/config.rs` | 设备地址、采样队列容量、超时、丢弃策略、设备信息 | -| 错误 | `src/error.rs` | SDK 错误枚举和 C ABI 错误码 | -| 传输层 | `src/transport.rs` | `SerialTransport` trait,屏蔽具体串口实现 | -| 协议层 | `src/protocol.rs` | 请求/应答帧结构、`ProtocolCodec` trait、CRC 接口 | -| 寄存器层 | `src/register.rs` | 地址常量、寄存器元信息、原始字节到业务类型的解析接口 | -| 通道层 | `src/channel.rs` | sample/command/event 三类 channel 和丢弃策略 | -| 数据流 | `src/stream.rs` | `StreamController` trait,定义轮询/自动回传模式 | -| 设备层 | `src/device.rs` | `EskinDevice` trait,聚合 transport、codec、channel | -| FFI 边界 | `src/ffi/mod.rs` | C ABI handle、版本和基础生命周期接口草案 | - -### 20.2 核心接口 - -```rust -pub trait SerialTransport { - fn open(&mut self) -> Result<(), SdkError>; - fn close(&mut self) -> Result<(), SdkError>; - fn is_open(&self) -> bool; - fn write(&mut self, data: &[u8]) -> Result; - fn read(&mut self, buf: &mut [u8], timeout: chrono::Duration) -> Result; - fn flush_rx(&mut self) -> Result<(), SdkError>; -} - -pub trait ProtocolCodec { - fn encode_read_request(&self, request: &ReadRequest) -> Result, SdkError>; - fn encode_write_request(&self, request: &WriteRequest) -> Result, SdkError>; - fn decode_read_response(&self, frame: &[u8]) -> Result; - fn decode_write_response(&self, frame: &[u8]) -> Result; - fn decode_stream_frame(&self, frame: &[u8]) -> Result; - fn crc8(&self, data: &[u8]) -> u8; -} - -pub trait RegisterMap { - fn device_info_registers(&self) -> &'static [RegisterSpec]; - fn distribution_register(&self, module: SensorModule) -> Result; - fn parse_device_info(&self, raw: &[u8]) -> Result; - fn parse_distribution_force( - &self, - module: SensorModule, - raw: &[u8], - ) -> Result; -} - -pub trait EskinDevice { - fn open(&mut self) -> Result<(), SdkError>; - fn close(&mut self) -> Result<(), SdkError>; - fn state(&self) -> DeviceState; - fn device_info(&self) -> Result; - fn apply_config(&mut self, config: DeviceConfig) -> Result<(), SdkError>; - fn start_stream(&mut self) -> Result<(), SdkError>; - fn stop_stream(&mut self) -> Result<(), SdkError>; - fn read_sample(&self, timeout_ms: u32) -> Result; - fn read_event(&self, timeout_ms: u32) -> Result; - fn read_register(&mut self, addr: u32, length: u16) -> Result, SdkError>; - fn write_register(&mut self, addr: u32, data: &[u8]) -> Result; -} -``` - -### 20.3 数据流流程图 - -```mermaid -flowchart TD - App[Rust/C/C++ App] --> Device[EskinDevice] - Device --> Channel[ChannelManager] - Device --> Codec[ProtocolCodec] - Device --> Register[RegisterMap] - Device --> Transport[SerialTransport] - Transport --> Serial[UART / USB-Serial] - Serial --> Hardware[Eskin Finger Device] - - App -->|open| Device - Device -->|open port| Transport - Device -->|read device info registers| Register - Register -->|ReadRequest| Codec - Codec -->|frame bytes| Transport - Transport -->|response bytes| Codec - Codec -->|ReadResponse| Register - Register -->|DeviceInfo| Device - - App -->|start_stream| Device - Device -->|DeviceCommand::StartStream| Channel - Device -->|poll or auto receive| Transport - Transport -->|raw frame| Codec - Codec -->|ProtocolFrame| Register - Register -->|FingerSample| Channel - Channel -->|sample_rx| App - Channel -->|event_rx| App -``` - -### 20.4 设备生命周期 - -```mermaid -stateDiagram-v2 - [*] --> Closed - Closed --> Open: open() - Open --> Streaming: start_stream() - Streaming --> Open: stop_stream() - Open --> Closed: close() - Streaming --> Error: transport/protocol error - Open --> Error: transport/protocol error - Error --> Closed: close() -``` - ---- - -## 21. 安全性考虑 - -1. **空指针检查**:所有 C API 入口检查指针非空 -2. **状态检查**:操作前检查设备状态(是否已连接、是否正在采集等) -3. **CRC 校验**:每帧数据必须通过 CRC-8-ITU 校验 -4. **帧完整性**:检查起始符、数据长度与实际数据一致性 -5. **线程安全**:使用 `AtomicBool`、`AtomicU64`、`AtomicU32` 管理共享状态 -6. **资源泄漏**:C++ RAII 保证析构时释放;C API 提供 `eskin_close()` -7. **panic 安全**:Rust FFI 函数不 panic,所有 panic 用 `catch_unwind` 捕获 -8. **超时保护**:所有串口读操作带超时,避免永久阻塞 diff --git a/docs/PROGRESS.md b/docs/PROGRESS.md deleted file mode 100644 index 5be375b..0000000 --- a/docs/PROGRESS.md +++ /dev/null @@ -1,346 +0,0 @@ -# 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>>` - -设计决策: - -- `SerialTransport: Send`,不要求 `Sync` -- 跨线程共享通过 `Arc>` 完成 -- 串口 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` -- 共享 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>> -``` - -原因: - -- 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, SdkError>` -- `parse_module_errors(raw: &[u8]) -> Result, 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` 事件存在重复来源,需要统一入口。 - diff --git a/example/cpp/main.cpp b/example/cpp/main.cpp new file mode 100644 index 0000000..5cf35f7 --- /dev/null +++ b/example/cpp/main.cpp @@ -0,0 +1,49 @@ +#include "../../include/eskin_ffi.h" +#include +#include +#include + +int main() { + printf("ESkin SDK version: %u.%u.%u\n", + eskin_version().major, eskin_version().minor, eskin_version().patch); + + EskinDeviceHandle dev = eskin_open("/dev/ttyUSB0", nullptr); + if (!dev) { + printf("Failed to open device\n"); + return 1; + } + printf("Device opened\n"); + + uint8_t buf[256]; + uint32_t actual = 0; + EskinSdkErrorCode err = eskin_read_register(dev, 0x0000, 4, buf, sizeof(buf), &actual); + if (err == ESkinSuccess) { + printf("Serial number (%u bytes): ", actual); + for (uint32_t i = 0; i < actual; i++) { + printf("%02X ", buf[i]); + } + + printf("\n"); + } + else { + printf("read_register failed: %d\n", err); + } + + err = eskin_read_register(dev, 0x000F, 2, buf, sizeof(buf), &actual); + if (err == ESkinSuccess) { + printf("Firmware version (%u bytes): ", actual); + for (uint32_t i = 0; i < actual; i++) { + printf("%02X", buf[i]); + } + printf("\n"); + } + + err = eskin_read_register(dev, 0x0500, 168, buf, sizeof(buf), &actual); + if (err == ESkinSuccess) { + printf("Combined force raw (%u bytes)\n"); + } + + eskin_close(dev); + printf("Device closed\n"); + return 0; +} \ No newline at end of file diff --git a/example/python/eskin_ffi.py b/example/python/eskin_ffi.py new file mode 100644 index 0000000..84fdc99 --- /dev/null +++ b/example/python/eskin_ffi.py @@ -0,0 +1,86 @@ +import ctypes +from ctypes import ( + Structure, POINTER, c_void_p, c_char_p, c_uint8, c_uint16, + c_uint32, c_uint64, c_int16, c_bool +) + + +class EskinSdkVersion(Structure): + _fields_ = [ + ("major", c_uint16), + ("minor", c_uint16), + ("patch", c_uint16), + ] + + +class EskinDevice: + def __init__(self, lib_path: str): + self._lib = ctypes.CDLL(lib_path) + self._setup_functions() + self._handle = None + + def _setup_functions(self): + lib = self._lib + + lib.eskin_version.restype = EskinSdkVersion + lib.eskin_version.argtypes = [] + + lib.eskin_open.restype = c_void_p + lib.eskin_open.argtypes = [c_char_p, c_void_p] + + lib.eskin_close.restype = c_uint32 + lib.eskin_close.argtypes = [c_void_p] + + lib.eskin_read_register.restype = c_uint32 + lib.eskin_read_register.argtypes = [ + c_void_p, c_uint32, c_uint16, + POINTER(c_uint8), c_uint32, POINTER(c_uint32) + ] + + lib.eskin_write_register.restype = c_uint32 + lib.eskin_write_register.argtypes = [ + c_void_p, c_uint32, POINTER(c_uint8), c_uint16, POINTER(c_uint16) + ] + + def version(self) -> tuple: + v = self._lib.eskin_version() + return (v.major, v.minor, v.patch) + + def open(self, path: str): + handle = self._lib.eskin_open(path.encode("utf-8"), None) + if not handle: + raise RuntimeError(f"Failed to open device: {path}") + self._handle = handle + + def close(self): + if self._handle: + self._lib.eskin_close(self._handle) + self._handle = None + + def read_register(self, addr: int, length: int) -> bytes: + """读寄存器,返回原始字节""" + buf = (c_uint8 * 256)() + actual = c_uint32(0) + err = self._lib.eskin_read_register( + self._handle, addr, length, buf, len(buf), ctypes.byref(actual) + ) + if err != 0: + raise RuntimeError(f"read_register failed: error={err}") + return bytes(buf[:actual.value]) + + def write_register(self, addr: int, data: bytes) -> int: + """写寄存器,返回设备确认的字节数""" + arr = (c_uint8 * len(data))(*data) + ret = c_uint16(0) + err = self._lib.eskin_write_register( + self._handle, addr, arr, len(data), ctypes.byref(ret) + ) + if err != 0: + raise RuntimeError(f"write_register failed: error={err}") + return ret.value + + def __enter__(self): + return self + + def __exit__(self, *args): + self.close() diff --git a/example/python/example.py b/example/python/example.py new file mode 100644 index 0000000..e69de29 diff --git a/include/eskin_ffi.h b/include/eskin_ffi.h new file mode 100644 index 0000000..9ef949c --- /dev/null +++ b/include/eskin_ffi.h @@ -0,0 +1,67 @@ +#ifndef ESkin_FFI_H +#define ESkin_FFI_H + +#include +#include +#include + +#ifdef __cplusplus +extern "C" { +#endif + +typedef void* EskinDeviceHandle; + +typedef struct { + uint16_t major; + uint16_t minor; + uint16_t patch; +} EskinSdkVersion; + +typedef enum { + ESkinSuccess = 0, + ESkinInvalidPointer = 1, + ESkinDeviceNotFound = 2, + ESkinDeviceAlreadyOpen = 3, + ESkinNotInitialized = 4, + ESkinAlreadyStreaming = 5, + ESkinNotStreaming = 6, + ESkinConfigError = 7, + ESkinIoError = 8, + ESkinTimeout = 9, + ESkinChannelClosed = 10, + ESkinInternalError = 11, + ESkinBufferOverflow = 12, + ESkinInvalidParameter = 13, + ESkinCrcError = 14, + ESkinFrameError = 15, + ESkinProtocolError = 16, + ESkinDeviceError = 17, +} EskinSdkErrorCode; + +EskinSdkVersion eskin_version(void); + +EskinDeviceHandle eskin_open(const char* path, const void* config); +EskinSdkErrorCode eskin_close(EskinDeviceHandle handle); + +EskinSdkErrorCode eskin_read_register( + EskinDeviceHandle handle, + uint32_t addr, + uint16_t length, + uint8_t* buf, + uint32_t buf_len, + uint32_t* actual_len +); + +EskinSdkErrorCode eskin_write_register( + EskinDeviceHandle handle, + uint32_t addr, + const uint8_t* data, + uint16_t data_len, + uint16_t* return_count +); + +#ifdef __cplusplus +} +#endif + +#endif \ No newline at end of file diff --git a/src/ffi/mod.rs b/src/ffi/mod.rs index 0dfe3b5..3c9897c 100644 --- a/src/ffi/mod.rs +++ b/src/ffi/mod.rs @@ -1,4 +1,8 @@ -use crate::{config::DeviceConfig, error::SdkErrorCode}; +use std::{ptr}; +use std::ffi::{CStr, c_char}; +use crate::device::EskinDevice; +use crate::transport::SerialPortTransport; +use crate::{config::DeviceConfig, device::EskinDeviceInner, error::SdkErrorCode}; pub type EskinDeviceHandle = *mut core::ffi::c_void; @@ -10,10 +14,144 @@ pub struct EskinSdkVersion { pub patch: u16, } -pub trait CApi { - fn version() -> EskinSdkVersion; - fn open(path: *const libc::c_char, config: *const DeviceConfig) -> EskinDeviceHandle; - fn close(handle: EskinDeviceHandle) -> SdkErrorCode; - fn start_stream(handle: EskinDeviceHandle) -> SdkErrorCode; - fn stop_stream(handle: EskinDeviceHandle) -> SdkErrorCode; +#[repr(C)] +pub struct CFingerSample { + pub timestamp_us: u64, + pub sequence: u32, + pub combinded_force_raw: *const u8, + pub combinded_force_len: u32, + pub module_error_raw: *const u8, + pub module_error_len: u32, +} + +#[allow(dead_code)] +struct DeviceWrapper { + device: EskinDeviceInner, + last_cf_raw: Vec, + last_me_raw: Vec +} + +#[unsafe(no_mangle)] +pub extern "C" fn eskin_version() -> EskinSdkVersion { + EskinSdkVersion { major: 0, minor: 1, patch: 0 } +} + + +#[unsafe(no_mangle)] +pub unsafe extern "C" fn eskin_open( + path: *const c_char, + config: *const DeviceConfig, +) -> EskinDeviceHandle { + if path.is_null() { + return ptr::null_mut(); + } + + let path_str = match unsafe { + CStr::from_ptr(path) + }.to_str() { + Ok(s) => s.to_string(), + Err(_) => return ptr::null_mut() + }; + + let device_config = if config.is_null() { + DeviceConfig::default() + } else { + unsafe { (*config).clone() } + }; + + let transport = SerialPortTransport::new(path_str, 921600); + let mut device = EskinDeviceInner::new(device_config, Box::new(transport)); + + if device.open().is_err() { + return ptr::null_mut(); + } + + let wrapper = Box::new(DeviceWrapper { + device, + last_cf_raw: Vec::new(), + last_me_raw: Vec::new(), + }); + + Box::into_raw(wrapper) as EskinDeviceHandle +} + +/// 关闭设备 +#[unsafe(no_mangle)] +pub unsafe extern "C" fn eskin_close(handle: EskinDeviceHandle) -> SdkErrorCode { + if handle.is_null() { + return SdkErrorCode::InvalidPointer; + } + + let wrapper = unsafe { &mut *(handle as *mut DeviceWrapper) }; + + match wrapper.device.close() { + Ok(()) => { + unsafe { drop(Box::from_raw(handle as *mut DeviceWrapper)) }; + SdkErrorCode::Success + } + Err(_) => SdkErrorCode::IoError, + } +} + +/// 读寄存器(原始字节) +#[unsafe(no_mangle)] +pub unsafe extern "C" fn eskin_read_register( + handle: EskinDeviceHandle, + addr: u32, + length: u16, + buf: *mut u8, + buf_len: u32, + actual_len: *mut u32, +) -> SdkErrorCode { + if handle.is_null() || buf.is_null() || actual_len.is_null() { + return SdkErrorCode::InvalidPointer; + } + + let wrapper = unsafe { &mut *(handle as *mut DeviceWrapper) }; + + let data = match wrapper.device.read_register(addr, length) { + Ok(d) => d, + Err(crate::error::SdkError::Timeout) => return SdkErrorCode::Timeout, + Err(crate::error::SdkError::FrameError(_)) => return SdkErrorCode::FrameError, + Err(crate::error::SdkError::CrcError { .. }) => return SdkErrorCode::CrcError, + Err(crate::error::SdkError::DeviceError(_)) => return SdkErrorCode::DeviceError, + Err(_) => return SdkErrorCode::IoError, + }; + + let copy_len = std::cmp::min(data.len(), buf_len as usize); + unsafe { + ptr::copy_nonoverlapping(data.as_ptr(), buf, copy_len); + *actual_len = data.len() as u32; + } + + SdkErrorCode::Success +} + +/// 写寄存器(原始字节) +#[unsafe(no_mangle)] +pub unsafe extern "C" fn eskin_write_register( + handle: EskinDeviceHandle, + addr: u32, + data: *const u8, + data_len: u16, + return_count: *mut u16, +) -> SdkErrorCode { + if handle.is_null() || data.is_null() || return_count.is_null() { + return SdkErrorCode::InvalidPointer; + } + + let wrapper = unsafe { &mut *(handle as *mut DeviceWrapper) }; + let data_slice = unsafe { std::slice::from_raw_parts(data, data_len as usize) }; + + match wrapper.device.write_register(addr, data_slice) { + Ok(count) => { + unsafe { *return_count = count }; + SdkErrorCode::Success + } + Err(crate::error::SdkError::Timeout) => SdkErrorCode::Timeout, + Err(crate::error::SdkError::FrameError(_)) => SdkErrorCode::FrameError, + Err(crate::error::SdkError::CrcError { .. }) => SdkErrorCode::CrcError, + Err(crate::error::SdkError::DeviceError(_)) => SdkErrorCode::DeviceError, + Err(_) => SdkErrorCode::IoError, + } } diff --git a/src/protocol.rs b/src/protocol.rs index c5a14fc..24a2bda 100644 --- a/src/protocol.rs +++ b/src/protocol.rs @@ -2,7 +2,7 @@ use serde::{Deserialize, Serialize}; use crate::error::SdkError; -pub const FRAME_START_REQUEST: u16 = 0x55AA; +pub const FRAME_START_REQUEST: [u8; 2] = [0x55, 0xAA]; pub const FRAME_START_RESPONSE: u16 = 0xAA55; pub const FUNC_READ: u8 = 0xFB; @@ -144,7 +144,7 @@ impl ProtocolCodec for EskinProtocolCodec { let data_len: u16 = 9; let mut frame = Vec::with_capacity(14); - frame.extend_from_slice(&FRAME_START_REQUEST.to_le_bytes()); + frame.extend_from_slice(&FRAME_START_REQUEST); frame.extend_from_slice(&data_len.to_le_bytes()); frame.push(request.device_addr); frame.push(0x00); @@ -177,7 +177,7 @@ impl ProtocolCodec for EskinProtocolCodec { .ok_or_else(|| SdkError::InvalidParameter("write frame too large".into()))?; let mut frame = Vec::with_capacity(14 + request.data.len()); - frame.extend_from_slice(&FRAME_START_REQUEST.to_le_bytes()); + frame.extend_from_slice(&FRAME_START_REQUEST); frame.extend_from_slice(&data_len.to_le_bytes()); frame.push(request.device_addr); frame.push(0x00); @@ -394,3 +394,46 @@ impl ProtocolCodec for EskinProtocolCodec { X25.checksum(_data) } } + +#[cfg(test)] +mod tests { + use super::*; + fn codec() -> EskinProtocolCodec { + EskinProtocolCodec + } + + #[test] + fn encode_read_request_has_correct_structure() { + let req = ReadRequest { + device_addr: 0x34, + start_addr: 0x1C00, + read_byte_count: 168 + }; + + let frame = codec().encode_read_request(&req).unwrap(); + println!("begin eq frame"); + assert_eq!(frame[0], 0x55); + assert_eq!(frame[1], 0xAA); + + assert_eq!(frame[2], 0x09); + assert_eq!(frame[3], 0x00); + assert_eq!(frame[4], 0x34); + assert_eq!(frame[5], 0x00); + + assert_eq!(frame[6], 0xFB); + + assert_eq!(frame[7], 0x00); + assert_eq!(frame[8], 0x1C); + assert_eq!(frame[9], 0x00); + assert_eq!(frame[10], 0x00); + + assert_eq!(frame[11], 0xA8); + assert_eq!(frame[12], 0x00); + + let crc = codec().crc8(&frame[..frame.len() - 1]); + assert_eq!(frame[frame.len() - 1], crc); + assert_eq!(frame[13], 0x35); + + assert_eq!(frame.len(), 14); + } +} \ No newline at end of file diff --git a/src/register.rs b/src/register.rs index a9569b9..ce145e1 100644 --- a/src/register.rs +++ b/src/register.rs @@ -1,7 +1,7 @@ use crate::{ config::DeviceInfo, error::SdkError, - types::{DistributionForce, ForcePoint, SensorModule}, + types::{CombinedForce, DistributionForce, Force3D, ForcePoint, ModuleError, SensorModule}, }; pub const REG_SERIAL_NUMBER: u32 = 0x0000; @@ -145,3 +145,60 @@ impl RegisterMap for EskinRegisterMap { }) } } + + +pub fn parse_combined_forces(raw: &[u8]) -> Result, SdkError> { + const MODULE_COUNT: usize = 28; + const BYTES_PER_MODULE: usize = 6; + + if raw.len() < MODULE_COUNT * BYTES_PER_MODULE { + return Err(SdkError::FrameError(format!( + "combined force raw too short: expected {} bytes, got {}", + MODULE_COUNT * BYTES_PER_MODULE, + raw.len() + ))); + } + + let mut forces = Vec::with_capacity(MODULE_COUNT); + for i in 0..MODULE_COUNT { + let offset = i * BYTES_PER_MODULE; + let fx = i16::from_le_bytes([raw[offset], raw[offset + 1]]); + let fy = i16::from_le_bytes([raw[offset + 2], raw[offset + 3]]); + let fz = i16::from_le_bytes([raw[offset + 4], raw[offset + 5]]); + + forces.push(CombinedForce { + module: SensorModule::from_index(i as u8), + force: Force3D { fx, fy, fz }, + }); + } + + Ok(forces) +} + +pub fn parse_module_errors(raw: &[u8]) -> Result, SdkError> { + const MODULE_COUNT: usize = 28; + const BYTES_PER_MODULE: usize = 2; + + if raw.len() < MODULE_COUNT * BYTES_PER_MODULE { + return Err(SdkError::FrameError(format!( + "module error raw too short: expected {} bytes, got {}", + MODULE_COUNT * BYTES_PER_MODULE, + raw.len() + ))); + } + + let mut errors = Vec::new(); + for i in 0..MODULE_COUNT { + let offset = i * BYTES_PER_MODULE; + let error_code = u16::from_le_bytes([raw[offset], raw[offset + 1]]); + + if error_code != 0 { + errors.push(ModuleError { + module: i as u8, + error_code, + }); + } + } + + Ok(errors) +} \ No newline at end of file diff --git a/src/stream.rs b/src/stream.rs index f4a4e94..e8c63d9 100644 --- a/src/stream.rs +++ b/src/stream.rs @@ -10,7 +10,7 @@ use crate::{ channel::{ChannelManager, DeviceEvent}, error::SdkError, protocol::{EskinProtocolCodec, ProtocolCodec}, - transport::{self, SerialTransport, SharedSerialTransport}, + transport::{SerialTransport, SharedSerialTransport}, types::{FingerSample, SensorModule}, }; @@ -96,7 +96,6 @@ impl StreamController for StreamRuntime { let worker = StreamWorker::new( Arc::clone(&self.running), Arc::clone(&self.channels), - Arc::clone(&self.transport), config.clone(), collector, ) @@ -148,7 +147,6 @@ impl StreamWorker { pub fn new( running: Arc, channels: Arc, - transport: SharedSerialTransport, config: StreamConfig, collector: Box, ) -> Self { @@ -307,17 +305,25 @@ impl PollingSampleCollector { impl SampleCollector for PollingSampleCollector { fn collect_once(&mut self) -> Result, SdkError> { - let _sequence = self.next_sequence(); + let sequence = self.next_sequence(); - let _combined_force_raw = self.read_register(REG_COMBINED_FORCE, 168)?; - let _module_error_raw = self.read_register(REG_MODULE_ERROR, 56)?; + let combined_force_raw = self.read_register(REG_COMBINED_FORCE, 168)?; + let module_error_raw = self.read_register(REG_MODULE_ERROR, 56)?; - // TODO: - // parse combined force - // parse module error - // build FingerSample + let combined_forces = crate::register::parse_combined_forces(&combined_force_raw)?; + let module_errors = crate::register::parse_module_errors(&module_error_raw)?; - Ok(None) + let now = chrono::Utc::now().timestamp_micros() as u64; + + let sample = FingerSample { + timestamp_us: now, + sequence, + combined_forces, + distribution_forces: Vec::new(), + module_errors + }; + + Ok(Some(sample)) } } diff --git a/src/types.rs b/src/types.rs index c24ccb3..b982296 100644 --- a/src/types.rs +++ b/src/types.rs @@ -54,6 +54,43 @@ pub enum SensorModule { Palm8 = 27, } +impl SensorModule { + pub fn from_index(index: u8) -> Self { + match index { + 0 => Self::ThumbProximal, + 1 => Self::ThumbMiddle, + 2 => Self::ThumbTip, + 3 => Self::ThumbNail, + 4 => Self::IndexProximal, + 5 => Self::IndexMiddle, + 6 => Self::IndexTip, + 7 => Self::IndexNail, + 8 => Self::MiddleProximal, + 9 => Self::MiddleMiddle, + 10 => Self::MiddleTip, + 11 => Self::MiddleNail, + 12 => Self::RingProximal, + 13 => Self::RingMiddle, + 14 => Self::RingTip, + 15 => Self::RingNail, + 16 => Self::PinkyProximal, + 17 => Self::PinkyMiddle, + 18 => Self::PinkyTip, + 19 => Self::PinkyNail, + 20 => Self::Palm1, + 21 => Self::Palm2, + 22 => Self::Palm3, + 23 => Self::Palm4, + 24 => Self::Palm5, + 25 => Self::Palm6, + 26 => Self::Palm7, + 27 => Self::Palm8, + _ => Self::ThumbProximal, + } + } +} + + pub const SENSOR_MODULE_COUNT: usize = 28; #[repr(C)]