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:
lenn
2026-05-06 00:54:44 +08:00
parent 60f9ad15e7
commit a7b7192341
13 changed files with 721 additions and 1915 deletions

3
.gitignore vendored
View File

@@ -4,6 +4,9 @@
debug/ debug/
target/ target/
# Internal docs (not for end users)
docs/
# These are backup files generated by rustfmt # These are backup files generated by rustfmt
**/*.rs.bk **/*.rs.bk

214
README.md
View File

@@ -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` — 序列化(可选)

File diff suppressed because it is too large Load Diff

View File

@@ -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
View 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;
}

View 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()

View File

67
include/eskin_ffi.h Normal file
View 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

View File

@@ -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; pub type EskinDeviceHandle = *mut core::ffi::c_void;
@@ -10,10 +14,144 @@ pub struct EskinSdkVersion {
pub patch: u16, pub patch: u16,
} }
pub trait CApi { #[repr(C)]
fn version() -> EskinSdkVersion; pub struct CFingerSample {
fn open(path: *const libc::c_char, config: *const DeviceConfig) -> EskinDeviceHandle; pub timestamp_us: u64,
fn close(handle: EskinDeviceHandle) -> SdkErrorCode; pub sequence: u32,
fn start_stream(handle: EskinDeviceHandle) -> SdkErrorCode; pub combinded_force_raw: *const u8,
fn stop_stream(handle: EskinDeviceHandle) -> SdkErrorCode; 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,
}
} }

View File

@@ -2,7 +2,7 @@ use serde::{Deserialize, Serialize};
use crate::error::SdkError; 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 FRAME_START_RESPONSE: u16 = 0xAA55;
pub const FUNC_READ: u8 = 0xFB; pub const FUNC_READ: u8 = 0xFB;
@@ -144,7 +144,7 @@ impl ProtocolCodec for EskinProtocolCodec {
let data_len: u16 = 9; let data_len: u16 = 9;
let mut frame = Vec::with_capacity(14); 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.extend_from_slice(&data_len.to_le_bytes());
frame.push(request.device_addr); frame.push(request.device_addr);
frame.push(0x00); frame.push(0x00);
@@ -177,7 +177,7 @@ impl ProtocolCodec for EskinProtocolCodec {
.ok_or_else(|| SdkError::InvalidParameter("write frame too large".into()))?; .ok_or_else(|| SdkError::InvalidParameter("write frame too large".into()))?;
let mut frame = Vec::with_capacity(14 + request.data.len()); 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.extend_from_slice(&data_len.to_le_bytes());
frame.push(request.device_addr); frame.push(request.device_addr);
frame.push(0x00); frame.push(0x00);
@@ -394,3 +394,46 @@ impl ProtocolCodec for EskinProtocolCodec {
X25.checksum(_data) 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);
}
}

View File

@@ -1,7 +1,7 @@
use crate::{ use crate::{
config::DeviceInfo, config::DeviceInfo,
error::SdkError, error::SdkError,
types::{DistributionForce, ForcePoint, SensorModule}, types::{CombinedForce, DistributionForce, Force3D, ForcePoint, ModuleError, SensorModule},
}; };
pub const REG_SERIAL_NUMBER: u32 = 0x0000; 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)
}

View File

@@ -10,7 +10,7 @@ use crate::{
channel::{ChannelManager, DeviceEvent}, channel::{ChannelManager, DeviceEvent},
error::SdkError, error::SdkError,
protocol::{EskinProtocolCodec, ProtocolCodec}, protocol::{EskinProtocolCodec, ProtocolCodec},
transport::{self, SerialTransport, SharedSerialTransport}, transport::{SerialTransport, SharedSerialTransport},
types::{FingerSample, SensorModule}, types::{FingerSample, SensorModule},
}; };
@@ -96,7 +96,6 @@ impl StreamController for StreamRuntime {
let worker = StreamWorker::new( let worker = StreamWorker::new(
Arc::clone(&self.running), Arc::clone(&self.running),
Arc::clone(&self.channels), Arc::clone(&self.channels),
Arc::clone(&self.transport),
config.clone(), config.clone(),
collector, collector,
) )
@@ -148,7 +147,6 @@ impl StreamWorker {
pub fn new( pub fn new(
running: Arc<AtomicBool>, running: Arc<AtomicBool>,
channels: Arc<ChannelManager>, channels: Arc<ChannelManager>,
transport: SharedSerialTransport,
config: StreamConfig, config: StreamConfig,
collector: Box<dyn SampleCollector>, collector: Box<dyn SampleCollector>,
) -> Self { ) -> Self {
@@ -307,17 +305,25 @@ impl PollingSampleCollector {
impl SampleCollector for PollingSampleCollector { impl SampleCollector for PollingSampleCollector {
fn collect_once(&mut self) -> Result<Option<FingerSample>, SdkError> { 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 combined_force_raw = self.read_register(REG_COMBINED_FORCE, 168)?;
let _module_error_raw = self.read_register(REG_MODULE_ERROR, 56)?; let module_error_raw = self.read_register(REG_MODULE_ERROR, 56)?;
// TODO: let combined_forces = crate::register::parse_combined_forces(&combined_force_raw)?;
// parse combined force let module_errors = crate::register::parse_module_errors(&module_error_raw)?;
// parse module error
// build FingerSample
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))
} }
} }

View File

@@ -54,6 +54,43 @@ pub enum SensorModule {
Palm8 = 27, 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; pub const SENSOR_MODULE_COUNT: usize = 28;
#[repr(C)] #[repr(C)]