feat: add FFI layer, protocol tests, mock transport, README
- FFI: eskin_open/close/read_register/write_register for C/C++/Python - Protocol: encode/decode tests with golden bytes verification - Stream: implement PollingSampleCollector producing FingerSample - Register: add parse_combined_forces/parse_module_errors - Transport: add MockSerialTransport for testing - Include: add C header file eskin_ffi.h - Examples: C++ and Python usage examples - README: full usage guide for Rust/C++/Python - Exclude docs/ from repo (internal only)
This commit is contained in:
3
.gitignore
vendored
3
.gitignore
vendored
@@ -4,6 +4,9 @@
|
||||
debug/
|
||||
target/
|
||||
|
||||
# Internal docs (not for end users)
|
||||
docs/
|
||||
|
||||
# These are backup files generated by rustfmt
|
||||
**/*.rs.bk
|
||||
|
||||
|
||||
214
README.md
214
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 <cstdio>
|
||||
|
||||
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` — 序列化(可选)
|
||||
1546
docs/ARCHITECTURE.md
1546
docs/ARCHITECTURE.md
File diff suppressed because it is too large
Load Diff
346
docs/PROGRESS.md
346
docs/PROGRESS.md
@@ -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<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` 事件存在重复来源,需要统一入口。
|
||||
|
||||
49
example/cpp/main.cpp
Normal file
49
example/cpp/main.cpp
Normal file
@@ -0,0 +1,49 @@
|
||||
#include "../../include/eskin_ffi.h"
|
||||
#include <cstdint>
|
||||
#include <cstdio>
|
||||
#include <cstring>
|
||||
|
||||
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;
|
||||
}
|
||||
86
example/python/eskin_ffi.py
Normal file
86
example/python/eskin_ffi.py
Normal file
@@ -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()
|
||||
0
example/python/example.py
Normal file
0
example/python/example.py
Normal file
67
include/eskin_ffi.h
Normal file
67
include/eskin_ffi.h
Normal file
@@ -0,0 +1,67 @@
|
||||
#ifndef ESkin_FFI_H
|
||||
#define ESkin_FFI_H
|
||||
|
||||
#include <cstdint>
|
||||
#include <stdint.h>
|
||||
#include <stdbool.h>
|
||||
|
||||
#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
|
||||
152
src/ffi/mod.rs
152
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<u8>,
|
||||
last_me_raw: Vec<u8>
|
||||
}
|
||||
|
||||
#[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,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
@@ -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<Vec<CombinedForce>, 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<Vec<ModuleError>, 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)
|
||||
}
|
||||
@@ -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<AtomicBool>,
|
||||
channels: Arc<ChannelManager>,
|
||||
transport: SharedSerialTransport,
|
||||
config: StreamConfig,
|
||||
collector: Box<dyn SampleCollector>,
|
||||
) -> Self {
|
||||
@@ -307,17 +305,25 @@ impl PollingSampleCollector {
|
||||
|
||||
impl SampleCollector for PollingSampleCollector {
|
||||
fn collect_once(&mut self) -> Result<Option<FingerSample>, 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))
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
37
src/types.rs
37
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)]
|
||||
|
||||
Reference in New Issue
Block a user