update
This commit is contained in:
203
Cargo.lock
generated
203
Cargo.lock
generated
@@ -17,6 +17,18 @@ version = "1.5.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "c08606f8c3cbf4ce6ec8e28fb0014a2c086708fe954eaa885384a6165172e7e8"
|
||||
|
||||
[[package]]
|
||||
name = "bitflags"
|
||||
version = "1.3.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "bef38d45163c2f1dde094a7dfd33ccf595c92905c8f8f4fdc18d06fb1037718a"
|
||||
|
||||
[[package]]
|
||||
name = "bitflags"
|
||||
version = "2.11.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "c4512299f36f043ab09a583e57bceb5a5aab7a73db1805848e8fef3c9e8c78b3"
|
||||
|
||||
[[package]]
|
||||
name = "bumpalo"
|
||||
version = "3.20.2"
|
||||
@@ -52,12 +64,37 @@ dependencies = [
|
||||
"windows-link",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "core-foundation"
|
||||
version = "0.10.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "b2a6cd9ae233e7f62ba4e9353e81a88df7fc8a5987b8d445b4d90c879bd156f6"
|
||||
dependencies = [
|
||||
"core-foundation-sys",
|
||||
"libc",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "core-foundation-sys"
|
||||
version = "0.8.7"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "773648b94d0e5d620f64f280777445740e61fe701025087ec8b57f45c791888b"
|
||||
|
||||
[[package]]
|
||||
name = "crc"
|
||||
version = "3.4.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "5eb8a2a1cd12ab0d987a5d5e825195d372001a4094a0376319d5a0ad71c1ba0d"
|
||||
dependencies = [
|
||||
"crc-catalog",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "crc-catalog"
|
||||
version = "2.5.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "217698eaf96b4a3f0bc4f3662aaa55bdf913cd54d7204591faa790070c6d0853"
|
||||
|
||||
[[package]]
|
||||
name = "crossbeam-channel"
|
||||
version = "0.5.15"
|
||||
@@ -78,12 +115,14 @@ name = "eskin-finger-sdk"
|
||||
version = "0.1.0"
|
||||
dependencies = [
|
||||
"chrono",
|
||||
"crc",
|
||||
"crossbeam-channel",
|
||||
"fern",
|
||||
"libc",
|
||||
"log",
|
||||
"serde",
|
||||
"serde_json",
|
||||
"serialport",
|
||||
"thiserror",
|
||||
"uuid",
|
||||
]
|
||||
@@ -151,6 +190,16 @@ dependencies = [
|
||||
"cc",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "io-kit-sys"
|
||||
version = "0.4.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "617ee6cf8e3f66f3b4ea67a4058564628cde41901316e19f559e14c7c72c5e7b"
|
||||
dependencies = [
|
||||
"core-foundation-sys",
|
||||
"mach2",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "itoa"
|
||||
version = "1.0.18"
|
||||
@@ -175,18 +224,58 @@ version = "0.2.186"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "68ab91017fe16c622486840e4c83c9a37afeff978bd239b5293d61ece587de66"
|
||||
|
||||
[[package]]
|
||||
name = "libudev"
|
||||
version = "0.3.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "78b324152da65df7bb95acfcaab55e3097ceaab02fb19b228a9eb74d55f135e0"
|
||||
dependencies = [
|
||||
"libc",
|
||||
"libudev-sys",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "libudev-sys"
|
||||
version = "0.1.4"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "3c8469b4a23b962c1396b9b451dda50ef5b283e8dd309d69033475fa9b334324"
|
||||
dependencies = [
|
||||
"libc",
|
||||
"pkg-config",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "log"
|
||||
version = "0.4.29"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "5e5032e24019045c762d3c0f28f5b6b8bbf38563a65908389bf7978758920897"
|
||||
|
||||
[[package]]
|
||||
name = "mach2"
|
||||
version = "0.4.3"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "d640282b302c0bb0a2a8e0233ead9035e3bed871f0b7e81fe4a1ec829765db44"
|
||||
dependencies = [
|
||||
"libc",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "memchr"
|
||||
version = "2.8.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "f8ca58f447f06ed17d5fc4043ce1b10dd205e060fb3ce5b979b8ed8e59ff3f79"
|
||||
|
||||
[[package]]
|
||||
name = "nix"
|
||||
version = "0.26.4"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "598beaf3cc6fdd9a5dfb1630c2800c7acd31df7aaf0f565796fba2b53ca1af1b"
|
||||
dependencies = [
|
||||
"bitflags 1.3.2",
|
||||
"cfg-if",
|
||||
"libc",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "num-traits"
|
||||
version = "0.2.19"
|
||||
@@ -208,6 +297,12 @@ version = "0.2.17"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "a89322df9ebe1c1578d689c92318e070967d1042b512afbe49518723f4e6d5cd"
|
||||
|
||||
[[package]]
|
||||
name = "pkg-config"
|
||||
version = "0.3.33"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "19f132c84eca552bf34cab8ec81f1c1dcc229b811638f9d283dceabe58c5569e"
|
||||
|
||||
[[package]]
|
||||
name = "proc-macro2"
|
||||
version = "1.0.106"
|
||||
@@ -232,6 +327,12 @@ version = "1.0.22"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "b39cdef0fa800fc44525c84ccb54a029961a8215f9619753635a9c0d2538d46d"
|
||||
|
||||
[[package]]
|
||||
name = "scopeguard"
|
||||
version = "1.2.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "94143f37725109f92c262ed2cf5e59bce7498c01bcc1502d7b9afe439a4e9f49"
|
||||
|
||||
[[package]]
|
||||
name = "serde"
|
||||
version = "1.0.228"
|
||||
@@ -239,6 +340,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "9a8e94ea7f378bd32cbbd37198a4a91436180c5bb472411e48b5ec2e2124ae9e"
|
||||
dependencies = [
|
||||
"serde_core",
|
||||
"serde_derive",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -274,6 +376,25 @@ dependencies = [
|
||||
"zmij",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "serialport"
|
||||
version = "4.9.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "a4d91116f97173694f1642263b2ff837f80d933aa837e2314969f6728f661df3"
|
||||
dependencies = [
|
||||
"bitflags 2.11.1",
|
||||
"cfg-if",
|
||||
"core-foundation",
|
||||
"core-foundation-sys",
|
||||
"io-kit-sys",
|
||||
"libudev",
|
||||
"mach2",
|
||||
"nix",
|
||||
"scopeguard",
|
||||
"unescaper",
|
||||
"windows-sys",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "shlex"
|
||||
version = "1.3.0"
|
||||
@@ -317,6 +438,15 @@ dependencies = [
|
||||
"syn",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "unescaper"
|
||||
version = "0.1.8"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "4064ed685c487dbc25bd3f0e9548f2e34bab9d18cefc700f9ec2dba74ba1138e"
|
||||
dependencies = [
|
||||
"thiserror",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "unicode-ident"
|
||||
version = "1.0.24"
|
||||
@@ -437,6 +567,79 @@ dependencies = [
|
||||
"windows-link",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "windows-sys"
|
||||
version = "0.52.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "282be5f36a8ce781fad8c8ae18fa3f9beff57ec1b52cb3de0789201425d9a33d"
|
||||
dependencies = [
|
||||
"windows-targets",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "windows-targets"
|
||||
version = "0.52.6"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "9b724f72796e036ab90c1021d4780d4d3d648aca59e491e6b98e725b84e99973"
|
||||
dependencies = [
|
||||
"windows_aarch64_gnullvm",
|
||||
"windows_aarch64_msvc",
|
||||
"windows_i686_gnu",
|
||||
"windows_i686_gnullvm",
|
||||
"windows_i686_msvc",
|
||||
"windows_x86_64_gnu",
|
||||
"windows_x86_64_gnullvm",
|
||||
"windows_x86_64_msvc",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "windows_aarch64_gnullvm"
|
||||
version = "0.52.6"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "32a4622180e7a0ec044bb555404c800bc9fd9ec262ec147edd5989ccd0c02cd3"
|
||||
|
||||
[[package]]
|
||||
name = "windows_aarch64_msvc"
|
||||
version = "0.52.6"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "09ec2a7bb152e2252b53fa7803150007879548bc709c039df7627cabbd05d469"
|
||||
|
||||
[[package]]
|
||||
name = "windows_i686_gnu"
|
||||
version = "0.52.6"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "8e9b5ad5ab802e97eb8e295ac6720e509ee4c243f69d781394014ebfe8bbfa0b"
|
||||
|
||||
[[package]]
|
||||
name = "windows_i686_gnullvm"
|
||||
version = "0.52.6"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "0eee52d38c090b3caa76c563b86c3a4bd71ef1a819287c19d586d7334ae8ed66"
|
||||
|
||||
[[package]]
|
||||
name = "windows_i686_msvc"
|
||||
version = "0.52.6"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "240948bc05c5e7c6dabba28bf89d89ffce3e303022809e73deaefe4f6ec56c66"
|
||||
|
||||
[[package]]
|
||||
name = "windows_x86_64_gnu"
|
||||
version = "0.52.6"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "147a5c80aabfbf0c7d901cb5895d1de30ef2907eb21fbbab29ca94c5b08b1a78"
|
||||
|
||||
[[package]]
|
||||
name = "windows_x86_64_gnullvm"
|
||||
version = "0.52.6"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "24d5b23dc417412679681396f2b49f3de8c1473deb516bd34410872eff51ed0d"
|
||||
|
||||
[[package]]
|
||||
name = "windows_x86_64_msvc"
|
||||
version = "0.52.6"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "589f6da84c646204747d1270a2a5661ea66ed1cced2631d546fdfb155959f9ec"
|
||||
|
||||
[[package]]
|
||||
name = "zmij"
|
||||
version = "1.0.21"
|
||||
|
||||
@@ -9,11 +9,13 @@ crate-type = ["lib", "cdylib", "staticlib"]
|
||||
|
||||
[dependencies]
|
||||
chrono = "0.4.44"
|
||||
crc = "3.4.0"
|
||||
crossbeam-channel = "0.5.15"
|
||||
fern = "0.7.1"
|
||||
libc = "0.2.186"
|
||||
log = "0.4.29"
|
||||
serde = "1.0.228"
|
||||
serde = { version = "1.0.228", features = ["derive"] }
|
||||
serde_json = "1.0.149"
|
||||
serialport = "4.7.3"
|
||||
thiserror = "2.0.18"
|
||||
uuid = "1.23.1"
|
||||
|
||||
@@ -272,6 +272,9 @@ chrono = "0.4"
|
||||
serde = { version = "1", features = ["derive"] }
|
||||
serde_json = "1"
|
||||
|
||||
# 串口传输 — UART/USB-Serial 通信
|
||||
serialport = "4"
|
||||
|
||||
# FFI 类型支持(C 类型兼容)
|
||||
libc = "0.2"
|
||||
|
||||
@@ -292,6 +295,7 @@ env_logger = "0.11"
|
||||
| `log` / `fern` | 日志抽象层 + 日志前端,方便调试 | 推荐 |
|
||||
| `chrono` | 高精度时间戳(微秒/纳秒级) | 推荐 |
|
||||
| `serde` / `serde_json` | 配置文件序列化、调试输出 | 推荐 |
|
||||
| `serialport` | 串口打开、读写、清空缓冲区,承载设备 UART 协议 | **核心** |
|
||||
| `libc` | FFI 类型兼容(`c_char`, `c_int` 等) | **FFI 必需** |
|
||||
| `uuid` | 设备唯一标识符生成 | 可选 |
|
||||
|
||||
@@ -1417,7 +1421,120 @@ make
|
||||
|
||||
---
|
||||
|
||||
## 20. 安全性考虑
|
||||
## 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<usize, SdkError>;
|
||||
fn read(&mut self, buf: &mut [u8], timeout: chrono::Duration) -> Result<usize, SdkError>;
|
||||
fn flush_rx(&mut self) -> Result<(), SdkError>;
|
||||
}
|
||||
|
||||
pub trait ProtocolCodec {
|
||||
fn encode_read_request(&self, request: &ReadRequest) -> Result<Vec<u8>, SdkError>;
|
||||
fn encode_write_request(&self, request: &WriteRequest) -> Result<Vec<u8>, SdkError>;
|
||||
fn decode_read_response(&self, frame: &[u8]) -> Result<ReadResponse, SdkError>;
|
||||
fn decode_write_response(&self, frame: &[u8]) -> Result<WriteResponse, SdkError>;
|
||||
fn decode_stream_frame(&self, frame: &[u8]) -> Result<ProtocolFrame, SdkError>;
|
||||
fn crc8(&self, data: &[u8]) -> u8;
|
||||
}
|
||||
|
||||
pub trait RegisterMap {
|
||||
fn device_info_registers(&self) -> &'static [RegisterSpec];
|
||||
fn distribution_register(&self, module: SensorModule) -> Result<RegisterSpec, SdkError>;
|
||||
fn parse_device_info(&self, raw: &[u8]) -> Result<DeviceInfo, SdkError>;
|
||||
fn parse_distribution_force(
|
||||
&self,
|
||||
module: SensorModule,
|
||||
raw: &[u8],
|
||||
) -> Result<DistributionForce, SdkError>;
|
||||
}
|
||||
|
||||
pub trait EskinDevice {
|
||||
fn open(&mut self) -> Result<(), SdkError>;
|
||||
fn close(&mut self) -> Result<(), SdkError>;
|
||||
fn state(&self) -> DeviceState;
|
||||
fn device_info(&self) -> Result<DeviceInfo, SdkError>;
|
||||
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<FingerSample, SdkError>;
|
||||
fn read_event(&self, timeout_ms: u32) -> Result<DeviceEvent, SdkError>;
|
||||
fn read_register(&mut self, addr: u32, length: u16) -> Result<Vec<u8>, SdkError>;
|
||||
fn write_register(&mut self, addr: u32, data: &[u8]) -> Result<u16, SdkError>;
|
||||
}
|
||||
```
|
||||
|
||||
### 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. **状态检查**:操作前检查设备状态(是否已连接、是否正在采集等)
|
||||
@@ -1426,4 +1543,4 @@ make
|
||||
5. **线程安全**:使用 `AtomicBool`、`AtomicU64`、`AtomicU32` 管理共享状态
|
||||
6. **资源泄漏**:C++ RAII 保证析构时释放;C API 提供 `eskin_close()`
|
||||
7. **panic 安全**:Rust FFI 函数不 panic,所有 panic 用 `catch_unwind` 捕获
|
||||
8. **超时保护**:所有串口读操作带超时,避免永久阻塞
|
||||
8. **超时保护**:所有串口读操作带超时,避免永久阻塞
|
||||
|
||||
144
src/channel.rs
Normal file
144
src/channel.rs
Normal file
@@ -0,0 +1,144 @@
|
||||
use crate::{
|
||||
config::{DeviceConfig, DropPolicy},
|
||||
error::SdkError,
|
||||
types::{FingerSample, SensorModule},
|
||||
};
|
||||
use crossbeam_channel::{Receiver, Sender, bounded};
|
||||
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<u8> },
|
||||
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<FingerSample>,
|
||||
pub sample_rx: Receiver<FingerSample>,
|
||||
|
||||
pub cmd_tx: Sender<DeviceCommand>,
|
||||
pub cmd_rx: Receiver<DeviceCommand>,
|
||||
|
||||
pub event_tx: Sender<DeviceEvent>,
|
||||
pub event_rx: Receiver<DeviceEvent>,
|
||||
|
||||
pub dropped_samples: AtomicU64,
|
||||
|
||||
pub drop_policy: DropPolicy,
|
||||
}
|
||||
|
||||
impl ChannelManager {
|
||||
pub fn new(
|
||||
sample_capacity: usize,
|
||||
cmd_capacity: usize,
|
||||
event_capacity: usize,
|
||||
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(captured)) =
|
||||
self.sample_tx.try_send(sample)
|
||||
{
|
||||
let _ = self.sample_rx.try_recv();
|
||||
if self.sample_tx.try_send(captured).is_err() {
|
||||
self.dropped_samples.fetch_add(1, Ordering::Relaxed);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub fn recv_sample(&self, timeout_ms: u32) -> Result<FingerSample, SdkError> {
|
||||
let timeout = std::time::Duration::from_millis(timeout_ms as u64);
|
||||
self.sample_rx
|
||||
.recv_timeout(timeout)
|
||||
.map_err(|_| SdkError::Timeout)
|
||||
}
|
||||
|
||||
pub fn send_cmd(&self, cmd: DeviceCommand) {
|
||||
match self.drop_policy {
|
||||
DropPolicy::DropNewest => {
|
||||
if self.cmd_tx.try_send(cmd).is_err() {
|
||||
self.dropped_samples.fetch_add(1, Ordering::Relaxed);
|
||||
}
|
||||
}
|
||||
DropPolicy::DropOldest => {
|
||||
if let Err(crossbeam_channel::TrySendError::Full(captured)) =
|
||||
self.cmd_tx.try_send(cmd)
|
||||
{
|
||||
let _ = self.cmd_rx.try_recv();
|
||||
if self.cmd_tx.try_send(captured).is_err() {
|
||||
self.dropped_samples.fetch_add(1, Ordering::Relaxed);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub fn recv_cmd(&self, timeout_ms: u32) -> Result<DeviceCommand, SdkError> {
|
||||
let timeout = std::time::Duration::from_millis(timeout_ms as u64);
|
||||
self.cmd_rx
|
||||
.recv_timeout(timeout)
|
||||
.map_err(|_| SdkError::Timeout)
|
||||
}
|
||||
|
||||
pub fn dropped_count(&self) -> u64 {
|
||||
self.dropped_samples.load(Ordering::Relaxed)
|
||||
}
|
||||
|
||||
pub fn send_event(&self, event: DeviceEvent) -> Result<(), SdkError> {
|
||||
self.event_tx
|
||||
.try_send(event)
|
||||
.map_err(|_| SdkError::ChannelClosed)
|
||||
}
|
||||
|
||||
pub fn recv_event(&self, timeout_ms: u32) -> Result<DeviceEvent, SdkError> {
|
||||
let timeout = std::time::Duration::from_millis(timeout_ms as u64);
|
||||
self.event_rx
|
||||
.recv_timeout(timeout)
|
||||
.map_err(|_| SdkError::Timeout)
|
||||
}
|
||||
}
|
||||
64
src/config.rs
Normal file
64
src/config.rs
Normal file
@@ -0,0 +1,64 @@
|
||||
use serde::{Deserialize, Serialize};
|
||||
|
||||
#[repr(C)]
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
pub struct DeviceConfig {
|
||||
pub device_addr: u8,
|
||||
pub auto_distribution: bool,
|
||||
pub read_distribution: bool,
|
||||
pub drop_policy: DropPolicy,
|
||||
pub sample_capacity: usize,
|
||||
pub command_capacity: usize,
|
||||
pub event_capacity: usize,
|
||||
pub read_timeout_ms: u32,
|
||||
}
|
||||
|
||||
impl Default for DeviceConfig {
|
||||
fn default() -> Self {
|
||||
Self {
|
||||
device_addr: 0x34,
|
||||
auto_distribution: false,
|
||||
read_distribution: true,
|
||||
drop_policy: DropPolicy::DropOldest,
|
||||
sample_capacity: 1024,
|
||||
command_capacity: 64,
|
||||
event_capacity: 128,
|
||||
read_timeout_ms: 100,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[repr(C)]
|
||||
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, Copy)]
|
||||
pub enum DropPolicy {
|
||||
DropNewest,
|
||||
DropOldest,
|
||||
}
|
||||
|
||||
#[repr(C)]
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
pub struct DeviceInfo {
|
||||
pub serial_number: u32,
|
||||
pub firmware_version: u16,
|
||||
pub calibration_group: u16,
|
||||
pub module_active_status: u16,
|
||||
pub l_line: u16,
|
||||
pub h_line: u16,
|
||||
pub product_config_1: u32,
|
||||
pub product_config_2: u32,
|
||||
}
|
||||
|
||||
impl Default for DeviceInfo {
|
||||
fn default() -> Self {
|
||||
Self {
|
||||
serial_number: 0x0001,
|
||||
firmware_version: 0x01,
|
||||
calibration_group: 0,
|
||||
module_active_status: 0,
|
||||
l_line: 7,
|
||||
h_line: 12,
|
||||
product_config_1: 0,
|
||||
product_config_2: 0,
|
||||
}
|
||||
}
|
||||
}
|
||||
89
src/device.rs
Normal file
89
src/device.rs
Normal file
@@ -0,0 +1,89 @@
|
||||
use std::time::Duration;
|
||||
|
||||
use chrono::Duration;
|
||||
|
||||
use crate::{
|
||||
channel::{ChannelManager, DeviceEvent},
|
||||
config::{DeviceConfig, DeviceInfo},
|
||||
error::SdkError,
|
||||
protocol::{EskinProtocolCodec, ProtocolCodec},
|
||||
transport::SerialTransport,
|
||||
types::FingerSample,
|
||||
};
|
||||
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
|
||||
pub enum DeviceState {
|
||||
Closed,
|
||||
Open,
|
||||
Streaming,
|
||||
Error,
|
||||
}
|
||||
|
||||
pub struct EskinDeviceInner {
|
||||
pub info: DeviceInfo,
|
||||
pub config: DeviceConfig,
|
||||
pub channels: ChannelManager,
|
||||
pub state: DeviceState,
|
||||
pub transport: Box<dyn SerialTransport>,
|
||||
pub codec: Box<dyn ProtocolCodec>,
|
||||
}
|
||||
|
||||
impl EskinDeviceInner {
|
||||
pub fn new(config: DeviceConfig, transport: Box<dyn SerialTransport>) -> Self {
|
||||
let channels = ChannelManager::new(
|
||||
config.sample_capacity,
|
||||
config.command_capacity,
|
||||
config.event_capacity,
|
||||
config.drop_policy,
|
||||
);
|
||||
|
||||
Self {
|
||||
info: DeviceInfo::default(),
|
||||
config,
|
||||
channels,
|
||||
state: DeviceState::Closed,
|
||||
transport,
|
||||
codec: Box::new(EskinProtocolCodec),
|
||||
}
|
||||
}
|
||||
|
||||
fn read_exact_from_transport(
|
||||
&mut self,
|
||||
buf: &mut [u8],
|
||||
timeout: Duration,
|
||||
) -> Result<(), SdkError> {
|
||||
let mut offset = 0;
|
||||
while offset < buf.len() {
|
||||
let n = self.transport.read(&mut buf[offset..], timeout)?;
|
||||
|
||||
if n == 0 {
|
||||
return Err(SdkError::Timeout);
|
||||
}
|
||||
|
||||
offset += n;
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn read_response_frame(&mut self) -> Result<Vec<u8>, SdkError> {
|
||||
let timeout = Duration::from_millis(self.config.read_timeout_ms as u64);
|
||||
let mut header = [0u8; 4];
|
||||
self.read_exact_from_transport(&mut header, timeout)?;
|
||||
}
|
||||
}
|
||||
|
||||
pub trait EskinDevice {
|
||||
fn open(&mut self) -> Result<(), SdkError>;
|
||||
fn close(&mut self) -> Result<(), SdkError>;
|
||||
fn state(&self) -> DeviceState;
|
||||
fn device_info(&self) -> Result<DeviceInfo, SdkError>;
|
||||
fn config(&self) -> &DeviceConfig;
|
||||
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<FingerSample, SdkError>;
|
||||
fn read_event(&self, timeout_ms: u32) -> Result<DeviceEvent, SdkError>;
|
||||
fn read_register(&mut self, addr: u32, length: u16) -> Result<Vec<u8>, SdkError>;
|
||||
fn write_register(&mut self, addr: u32, data: &[u8]) -> Result<u16, SdkError>;
|
||||
}
|
||||
72
src/error.rs
Normal file
72
src/error.rs
Normal file
@@ -0,0 +1,72 @@
|
||||
#[repr(C)]
|
||||
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,
|
||||
}
|
||||
|
||||
#[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),
|
||||
}
|
||||
19
src/ffi/mod.rs
Normal file
19
src/ffi/mod.rs
Normal file
@@ -0,0 +1,19 @@
|
||||
use crate::{config::DeviceConfig, error::SdkErrorCode};
|
||||
|
||||
pub type EskinDeviceHandle = *mut core::ffi::c_void;
|
||||
|
||||
#[repr(C)]
|
||||
#[derive(Debug, Clone, Copy)]
|
||||
pub struct EskinSdkVersion {
|
||||
pub major: u16,
|
||||
pub minor: u16,
|
||||
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;
|
||||
}
|
||||
11
src/lib.rs
11
src/lib.rs
@@ -1 +1,10 @@
|
||||
pub mod types;
|
||||
pub mod channel;
|
||||
pub mod config;
|
||||
pub mod device;
|
||||
pub mod error;
|
||||
pub mod ffi;
|
||||
pub mod protocol;
|
||||
pub mod register;
|
||||
pub mod stream;
|
||||
pub mod transport;
|
||||
pub mod types;
|
||||
|
||||
396
src/protocol.rs
Normal file
396
src/protocol.rs
Normal file
@@ -0,0 +1,396 @@
|
||||
use serde::{Deserialize, Serialize};
|
||||
|
||||
use crate::error::SdkError;
|
||||
|
||||
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;
|
||||
pub const FUNC_RESPONSE_WRITE: u8 = 0xF9;
|
||||
|
||||
pub const FRAME_HEADER_LEN: usize = 13;
|
||||
pub const FRAME_CRC_LEN: usize = 1;
|
||||
pub const FRAME_STATUS_LEN: usize = 1;
|
||||
pub const MIN_RESPONSE_FRAME_LEN: usize = FRAME_HEADER_LEN + FRAME_STATUS_LEN + FRAME_CRC_LEN;
|
||||
|
||||
#[repr(C)]
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
|
||||
pub enum DeviceStatus {
|
||||
Success = 0x00,
|
||||
ReadLenExceeded = 0x01,
|
||||
LengthError = 0x02,
|
||||
InvalidAddress = 0x03,
|
||||
ReadOnlyRegister = 0x04,
|
||||
}
|
||||
|
||||
impl DeviceStatus {
|
||||
pub fn to_error(&self) -> Option<SdkError> {
|
||||
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)),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
|
||||
pub enum FrameFunction {
|
||||
Read,
|
||||
Write,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
pub struct ProtocolFrame {
|
||||
pub start: u16,
|
||||
pub device_addr: u8,
|
||||
pub function: u8,
|
||||
pub start_addr: u32,
|
||||
pub payload: Vec<u8>,
|
||||
pub status: Option<DeviceStatus>,
|
||||
}
|
||||
|
||||
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<u8>,
|
||||
}
|
||||
|
||||
pub struct ReadResponse {
|
||||
pub device_addr: u8,
|
||||
pub start_addr: u32,
|
||||
pub data: Vec<u8>,
|
||||
pub status: DeviceStatus,
|
||||
}
|
||||
|
||||
pub struct WriteResponse {
|
||||
pub device_addr: u8,
|
||||
pub start_addr: u32,
|
||||
pub return_byte_count: u16,
|
||||
pub status: DeviceStatus,
|
||||
}
|
||||
|
||||
pub trait ProtocolCodec: Send + Sync {
|
||||
fn encode_read_request(&self, request: &ReadRequest) -> Result<Vec<u8>, SdkError>;
|
||||
fn encode_write_request(&self, request: &WriteRequest) -> Result<Vec<u8>, SdkError>;
|
||||
fn decode_read_response(&self, frame: &[u8]) -> Result<ReadResponse, SdkError>;
|
||||
fn decode_write_response(&self, frame: &[u8]) -> Result<WriteResponse, SdkError>;
|
||||
fn decode_stream_frame(&self, frame: &[u8]) -> Result<ProtocolFrame, SdkError>;
|
||||
fn crc8(&self, data: &[u8]) -> u8;
|
||||
}
|
||||
|
||||
#[derive(Debug, Default, Clone, Copy)]
|
||||
pub struct EskinProtocolCodec;
|
||||
|
||||
impl EskinProtocolCodec {
|
||||
fn status_from_u8(raw: u8) -> Result<DeviceStatus, SdkError> {
|
||||
match raw {
|
||||
0x00 => Ok(DeviceStatus::Success),
|
||||
0x01 => Ok(DeviceStatus::ReadLenExceeded),
|
||||
0x02 => Ok(DeviceStatus::LengthError),
|
||||
0x03 => Ok(DeviceStatus::InvalidAddress),
|
||||
0x04 => Ok(DeviceStatus::ReadOnlyRegister),
|
||||
other => Err(SdkError::DeviceError(other as u16)),
|
||||
}
|
||||
}
|
||||
|
||||
fn validate_crc(&self, frame: &[u8]) -> Result<(), SdkError> {
|
||||
if frame.len() < FRAME_CRC_LEN {
|
||||
return Err(SdkError::FrameError("frame too short for crc".into()));
|
||||
}
|
||||
|
||||
let expected = frame[frame.len() - 1];
|
||||
let actual = self.crc8(&frame[..frame.len() - 1]);
|
||||
if expected != actual {
|
||||
return Err(SdkError::CrcError { expected, actual });
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn read_u16_le(frame: &[u8], offset: usize) -> Result<u16, SdkError> {
|
||||
let bytes = frame
|
||||
.get(offset..offset + 2)
|
||||
.ok_or_else(|| SdkError::FrameError("missing u16 field".into()))?;
|
||||
|
||||
Ok(u16::from_le_bytes([bytes[0], bytes[1]]))
|
||||
}
|
||||
|
||||
fn read_u32_le(frame: &[u8], offset: usize) -> Result<u32, SdkError> {
|
||||
let bytes = frame
|
||||
.get(offset..offset + 4)
|
||||
.ok_or_else(|| SdkError::FrameError("missing u32 field".into()))?;
|
||||
|
||||
Ok(u32::from_le_bytes([bytes[0], bytes[1], bytes[2], bytes[3]]))
|
||||
}
|
||||
}
|
||||
|
||||
impl ProtocolCodec for EskinProtocolCodec {
|
||||
fn encode_read_request(&self, request: &ReadRequest) -> Result<Vec<u8>, SdkError> {
|
||||
if request.read_byte_count == 0 {
|
||||
return Err(SdkError::InvalidParameter(
|
||||
"read_byte_count must be greater than 0".into(),
|
||||
));
|
||||
}
|
||||
|
||||
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(&data_len.to_le_bytes());
|
||||
frame.push(request.device_addr);
|
||||
frame.push(0x00);
|
||||
frame.push(FUNC_READ);
|
||||
frame.extend_from_slice(&request.start_addr.to_le_bytes());
|
||||
frame.extend_from_slice(&request.read_byte_count.to_le_bytes());
|
||||
|
||||
let crc = self.crc8(&frame);
|
||||
frame.push(crc);
|
||||
|
||||
Ok(frame)
|
||||
}
|
||||
|
||||
fn encode_write_request(&self, request: &WriteRequest) -> Result<Vec<u8>, SdkError> {
|
||||
if request.data.is_empty() {
|
||||
return Err(SdkError::InvalidParameter(
|
||||
"write data must not be empty".into(),
|
||||
));
|
||||
}
|
||||
|
||||
if request.data.len() > u16::MAX as usize {
|
||||
return Err(SdkError::InvalidParameter(
|
||||
"write data length exceeds u16::MAX".into(),
|
||||
));
|
||||
}
|
||||
|
||||
let write_len = request.data.len() as u16;
|
||||
let data_len = 9u16
|
||||
.checked_add(write_len)
|
||||
.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(&data_len.to_le_bytes());
|
||||
frame.push(request.device_addr);
|
||||
frame.push(0x00);
|
||||
frame.push(FUNC_WRITE);
|
||||
frame.extend_from_slice(&request.start_addr.to_le_bytes());
|
||||
frame.extend_from_slice(&write_len.to_le_bytes());
|
||||
frame.extend_from_slice(&request.data);
|
||||
|
||||
let crc = self.crc8(&frame);
|
||||
frame.push(crc);
|
||||
|
||||
Ok(frame)
|
||||
}
|
||||
|
||||
fn decode_read_response(&self, frame: &[u8]) -> Result<ReadResponse, SdkError> {
|
||||
if frame.len() < MIN_RESPONSE_FRAME_LEN {
|
||||
return Err(SdkError::FrameError("read response too short".into()));
|
||||
}
|
||||
|
||||
let start = Self::read_u16_le(frame, 0)?;
|
||||
if start != FRAME_START_RESPONSE {
|
||||
return Err(SdkError::FrameError(format!(
|
||||
"invalid response start: 0x{start:04X}"
|
||||
)));
|
||||
}
|
||||
|
||||
let data_len = Self::read_u16_le(frame, 2)? as usize;
|
||||
let expected_len = 2 + 2 + data_len + FRAME_STATUS_LEN + FRAME_CRC_LEN;
|
||||
|
||||
if frame.len() != expected_len {
|
||||
return Err(SdkError::FrameError(format!(
|
||||
"read response length mismatch: expected {expected_len}, got {}",
|
||||
frame.len()
|
||||
)));
|
||||
}
|
||||
|
||||
self.validate_crc(frame)?;
|
||||
|
||||
let device_addr = frame[4];
|
||||
let reserved = frame[5];
|
||||
let function = frame[6];
|
||||
|
||||
if reserved != 0x00 {
|
||||
return Err(SdkError::FrameError(format!(
|
||||
"invalid reserved byte: 0x{reserved:02X}"
|
||||
)));
|
||||
}
|
||||
|
||||
if function != FUNC_RESPONSE_READ {
|
||||
return Err(SdkError::FrameError(format!(
|
||||
"invalid read response function: 0x{function:02X}"
|
||||
)));
|
||||
}
|
||||
|
||||
let start_addr = Self::read_u32_le(frame, 7)?;
|
||||
let read_len = Self::read_u16_le(frame, 11)? as usize;
|
||||
if data_len != 9 + read_len {
|
||||
return Err(SdkError::FrameError(format!(
|
||||
"read response data length mismatch: header data_len {data_len}, payload len {read_len}"
|
||||
)));
|
||||
}
|
||||
|
||||
let payload_start = 13;
|
||||
let payload_end = payload_start + read_len;
|
||||
|
||||
let data = frame
|
||||
.get(payload_start..payload_end)
|
||||
.ok_or_else(|| SdkError::FrameError("read response payload missing".into()))?
|
||||
.to_vec();
|
||||
|
||||
let status_offset = 4 + data_len;
|
||||
let status_raw = *frame
|
||||
.get(status_offset)
|
||||
.ok_or_else(|| SdkError::FrameError("read response status missing".into()))?;
|
||||
let status = Self::status_from_u8(status_raw)?;
|
||||
|
||||
if let Some(err) = status.to_error() {
|
||||
return Err(err);
|
||||
}
|
||||
|
||||
Ok(ReadResponse {
|
||||
device_addr,
|
||||
start_addr,
|
||||
data,
|
||||
status,
|
||||
})
|
||||
}
|
||||
|
||||
fn decode_write_response(&self, frame: &[u8]) -> Result<WriteResponse, SdkError> {
|
||||
if frame.len() < MIN_RESPONSE_FRAME_LEN {
|
||||
return Err(SdkError::FrameError("write response too short".into()));
|
||||
}
|
||||
|
||||
let start = Self::read_u16_le(frame, 0)?;
|
||||
if start != FRAME_START_RESPONSE {
|
||||
return Err(SdkError::FrameError(format!(
|
||||
"invalid response start: 0x{start:04X}"
|
||||
)));
|
||||
}
|
||||
|
||||
let data_len = Self::read_u16_le(frame, 2)? as usize;
|
||||
let expected_len = 2 + 2 + data_len + FRAME_STATUS_LEN + FRAME_CRC_LEN;
|
||||
|
||||
if frame.len() != expected_len {
|
||||
return Err(SdkError::FrameError(format!(
|
||||
"write response length mismatch: expected {expected_len}, got {}",
|
||||
frame.len()
|
||||
)));
|
||||
}
|
||||
|
||||
self.validate_crc(frame)?;
|
||||
let device_addr = frame[4];
|
||||
let reserved = frame[5];
|
||||
let function = frame[6];
|
||||
|
||||
if reserved != 0x00 {
|
||||
return Err(SdkError::FrameError(format!(
|
||||
"invalid reserved byte: 0x{reserved:02X}"
|
||||
)));
|
||||
}
|
||||
|
||||
if function != FUNC_RESPONSE_WRITE {
|
||||
return Err(SdkError::FrameError(format!(
|
||||
"invalid write response function: 0x{function:02X}"
|
||||
)));
|
||||
}
|
||||
|
||||
let start_addr = Self::read_u32_le(frame, 7)?;
|
||||
let return_byte_count = Self::read_u16_le(frame, 11)?;
|
||||
if data_len != 9 {
|
||||
return Err(SdkError::FrameError(format!(
|
||||
"write response data length mismatch: expected 9, got {data_len}"
|
||||
)));
|
||||
}
|
||||
|
||||
let status_offset = 4 + data_len;
|
||||
let status_raw = *frame
|
||||
.get(status_offset)
|
||||
.ok_or_else(|| SdkError::FrameError("write response status missing".into()))?;
|
||||
let status = Self::status_from_u8(status_raw)?;
|
||||
|
||||
if let Some(err) = status.to_error() {
|
||||
return Err(err);
|
||||
}
|
||||
|
||||
Ok(WriteResponse {
|
||||
device_addr,
|
||||
start_addr,
|
||||
return_byte_count,
|
||||
status,
|
||||
})
|
||||
}
|
||||
|
||||
fn decode_stream_frame(&self, frame: &[u8]) -> Result<ProtocolFrame, SdkError> {
|
||||
if frame.len() < MIN_RESPONSE_FRAME_LEN {
|
||||
return Err(SdkError::FrameError("stream frame too short".into()));
|
||||
}
|
||||
|
||||
let start = Self::read_u16_le(frame, 0)?;
|
||||
if start != FRAME_START_RESPONSE {
|
||||
return Err(SdkError::FrameError(format!(
|
||||
"invalid stream frame start: 0x{start:04X}"
|
||||
)));
|
||||
}
|
||||
|
||||
let data_len = Self::read_u16_le(frame, 2)? as usize;
|
||||
let expected_len = 2 + 2 + data_len + FRAME_STATUS_LEN + FRAME_CRC_LEN;
|
||||
|
||||
if frame.len() != expected_len {
|
||||
return Err(SdkError::FrameError(format!(
|
||||
"stream frame length mismatch: expected {expected_len}, got {}",
|
||||
frame.len()
|
||||
)));
|
||||
}
|
||||
|
||||
self.validate_crc(frame)?;
|
||||
|
||||
let device_addr = frame[4];
|
||||
let function = frame[6];
|
||||
let start_addr = Self::read_u32_le(frame, 7)?;
|
||||
let payload_len = Self::read_u16_le(frame, 11)? as usize;
|
||||
if data_len != 9 + payload_len {
|
||||
return Err(SdkError::FrameError(format!(
|
||||
"stream frame data length mismatch: header data_len {data_len}, payload len {payload_len}"
|
||||
)));
|
||||
}
|
||||
|
||||
let payload_start = 13;
|
||||
let payload_end = payload_start + payload_len;
|
||||
|
||||
let payload = frame
|
||||
.get(payload_start..payload_end)
|
||||
.ok_or_else(|| SdkError::FrameError("stream payload missing".into()))?
|
||||
.to_vec();
|
||||
|
||||
let status_offset = 4 + data_len;
|
||||
let status_raw = *frame
|
||||
.get(status_offset)
|
||||
.ok_or_else(|| SdkError::FrameError("stream status missing".into()))?;
|
||||
let status = Self::status_from_u8(status_raw)?;
|
||||
|
||||
Ok(ProtocolFrame {
|
||||
start,
|
||||
device_addr,
|
||||
function,
|
||||
start_addr,
|
||||
payload,
|
||||
status: Some(status),
|
||||
})
|
||||
}
|
||||
|
||||
fn crc8(&self, _data: &[u8]) -> u8 {
|
||||
const X25: crc::Crc<u8> = crc::Crc::<u8>::new(&crc::CRC_8_I_432_1);
|
||||
X25.checksum(_data)
|
||||
}
|
||||
}
|
||||
76
src/register.rs
Normal file
76
src/register.rs
Normal file
@@ -0,0 +1,76 @@
|
||||
use crate::{
|
||||
config::DeviceInfo,
|
||||
error::SdkError,
|
||||
types::{DistributionForce, SensorModule},
|
||||
};
|
||||
|
||||
pub const REG_SERIAL_NUMBER: u32 = 0x0000;
|
||||
pub const REG_FIRMWARE_VERSION: u32 = 0x000F;
|
||||
pub const REG_CALIBRATION_GROUP: u32 = 0x0010;
|
||||
pub const REG_MODULE_ACTIVE_STATUS: u32 = 0x0011;
|
||||
pub const REG_L_LINE: u32 = 0x0012;
|
||||
pub const REG_H_LINE: u32 = 0x0013;
|
||||
pub const REG_PRODUCT_CONFIG_1: u32 = 0x0030;
|
||||
pub const REG_PRODUCT_CONFIG_2: u32 = 0x0032;
|
||||
pub const REG_COMBINED_FORCE: u32 = 0x0500;
|
||||
pub const REG_MODULE_ERROR: u32 = 0x0700;
|
||||
pub const REG_DISTRIBUTION_FORCE_BASE: u32 = 0x1000;
|
||||
pub const REG_PROCESSED_VALUE_BASE: u32 = 0x2000;
|
||||
pub const REG_CALIBRATION_BASE: u32 = 0x8000;
|
||||
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
|
||||
pub enum RegisterAccess {
|
||||
ReadOnly,
|
||||
ReadWrite,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
|
||||
pub enum RegisterValueType {
|
||||
U16,
|
||||
U32,
|
||||
Bytes,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
|
||||
pub struct RegisterSpec {
|
||||
pub addr: u32,
|
||||
pub len: u16,
|
||||
pub access: RegisterAccess,
|
||||
pub value_type: RegisterValueType,
|
||||
}
|
||||
|
||||
pub trait RegisterMap {
|
||||
fn device_info_registers(&self) -> &'static [RegisterSpec];
|
||||
fn distribution_register(&self, module: SensorModule) -> Result<RegisterSpec, SdkError>;
|
||||
fn parse_device_info(&self, raw: &[u8]) -> Result<DeviceInfo, SdkError>;
|
||||
fn parse_distribution_force(
|
||||
&self,
|
||||
module: SensorModule,
|
||||
raw: &[u8],
|
||||
) -> Result<DistributionForce, SdkError>;
|
||||
}
|
||||
|
||||
#[derive(Debug, Default, Clone, Copy)]
|
||||
pub struct EskinRegisterMap;
|
||||
|
||||
impl RegisterMap for EskinRegisterMap {
|
||||
fn device_info_registers(&self) -> &'static [RegisterSpec] {
|
||||
todo!("device info register specs")
|
||||
}
|
||||
|
||||
fn distribution_register(&self, _module: SensorModule) -> Result<RegisterSpec, SdkError> {
|
||||
todo!("distribution register spec")
|
||||
}
|
||||
|
||||
fn parse_device_info(&self, _raw: &[u8]) -> Result<DeviceInfo, SdkError> {
|
||||
todo!("parse device info")
|
||||
}
|
||||
|
||||
fn parse_distribution_force(
|
||||
&self,
|
||||
_module: SensorModule,
|
||||
_raw: &[u8],
|
||||
) -> Result<DistributionForce, SdkError> {
|
||||
todo!("parse distribution force")
|
||||
}
|
||||
}
|
||||
38
src/stream.rs
Normal file
38
src/stream.rs
Normal file
@@ -0,0 +1,38 @@
|
||||
use crate::{
|
||||
channel::DeviceEvent,
|
||||
error::SdkError,
|
||||
types::{FingerSample, SensorModule},
|
||||
};
|
||||
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
|
||||
pub enum StreamMode {
|
||||
Polling,
|
||||
AutoDistribution,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct StreamConfig {
|
||||
pub mode: StreamMode,
|
||||
pub read_distribution: bool,
|
||||
pub modules: Vec<SensorModule>,
|
||||
pub poll_interval_ms: u32,
|
||||
}
|
||||
|
||||
impl Default for StreamConfig {
|
||||
fn default() -> Self {
|
||||
Self {
|
||||
mode: StreamMode::Polling,
|
||||
read_distribution: true,
|
||||
modules: Vec::new(),
|
||||
poll_interval_ms: 10,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub trait StreamController: Send {
|
||||
fn start(&mut self, config: StreamConfig) -> Result<(), SdkError>;
|
||||
fn stop(&mut self) -> Result<(), SdkError>;
|
||||
fn is_running(&self) -> bool;
|
||||
fn next_sample(&self, timeout_ms: u32) -> Result<FingerSample, SdkError>;
|
||||
fn next_event(&self, timeout_ms: u32) -> Result<DeviceEvent, SdkError>;
|
||||
}
|
||||
110
src/transport.rs
Normal file
110
src/transport.rs
Normal file
@@ -0,0 +1,110 @@
|
||||
use crate::error::SdkError;
|
||||
use chrono::Duration;
|
||||
use serialport::{ClearBuffer, DataBits, FlowControl, Parity, StopBits};
|
||||
use std::io::ErrorKind;
|
||||
|
||||
pub trait SerialTransport: Send {
|
||||
fn open(&mut self) -> Result<(), SdkError>;
|
||||
fn close(&mut self) -> Result<(), SdkError>;
|
||||
fn is_open(&self) -> bool;
|
||||
fn write(&mut self, data: &[u8]) -> Result<usize, SdkError>;
|
||||
fn read(&mut self, buf: &mut [u8], timeout: Duration) -> Result<usize, SdkError>;
|
||||
fn flush_rx(&mut self) -> Result<(), SdkError>;
|
||||
}
|
||||
|
||||
pub struct SerialPortTransport {
|
||||
pub path: String,
|
||||
pub baud_rate: u32,
|
||||
pub port: Option<Box<dyn serialport::SerialPort>>,
|
||||
}
|
||||
|
||||
impl SerialPortTransport {
|
||||
pub fn new(path: impl Into<String>, baud_rate: u32) -> Self {
|
||||
Self {
|
||||
path: path.into(),
|
||||
baud_rate,
|
||||
port: None,
|
||||
}
|
||||
}
|
||||
|
||||
fn port_mut(&mut self) -> Result<&mut Box<dyn serialport::SerialPort>, SdkError> {
|
||||
self.port
|
||||
.as_mut()
|
||||
.ok_or_else(|| SdkError::DeviceNotFound(self.path.clone()))
|
||||
}
|
||||
|
||||
fn timeout_to_std(timeout: Duration) -> Result<std::time::Duration, SdkError> {
|
||||
timeout
|
||||
.to_std()
|
||||
.map_err(|_| SdkError::InvalidParameter("timeout must be non-negative".into()))
|
||||
}
|
||||
|
||||
fn map_serial_error(error: serialport::Error) -> SdkError {
|
||||
SdkError::IoError(std::io::Error::new(ErrorKind::Other, error.to_string()))
|
||||
}
|
||||
|
||||
fn map_io_error(error: std::io::Error) -> SdkError {
|
||||
match error.kind() {
|
||||
ErrorKind::TimedOut | ErrorKind::WouldBlock => SdkError::Timeout,
|
||||
_ => SdkError::IoError(error),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl SerialTransport for SerialPortTransport {
|
||||
fn open(&mut self) -> Result<(), SdkError> {
|
||||
if self.port.is_some() {
|
||||
return Err(SdkError::DeviceAlreadyOpen);
|
||||
}
|
||||
|
||||
let port = serialport::new(&self.path, self.baud_rate)
|
||||
.data_bits(DataBits::Eight)
|
||||
.stop_bits(StopBits::One)
|
||||
.parity(Parity::None)
|
||||
.flow_control(FlowControl::None)
|
||||
.open()
|
||||
.map_err(Self::map_serial_error)?;
|
||||
|
||||
self.port = Some(port);
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn close(&mut self) -> Result<(), SdkError> {
|
||||
self.port.take();
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn is_open(&self) -> bool {
|
||||
self.port.is_some()
|
||||
}
|
||||
|
||||
fn write(&mut self, data: &[u8]) -> Result<usize, SdkError> {
|
||||
if data.is_empty() {
|
||||
return Ok(0);
|
||||
}
|
||||
|
||||
let port = self.port_mut()?;
|
||||
let written = port.write(data).map_err(Self::map_io_error)?;
|
||||
port.flush().map_err(Self::map_io_error)?;
|
||||
|
||||
Ok(written)
|
||||
}
|
||||
|
||||
fn read(&mut self, buf: &mut [u8], timeout: Duration) -> Result<usize, SdkError> {
|
||||
if buf.is_empty() {
|
||||
return Ok(0);
|
||||
}
|
||||
|
||||
let timeout = Self::timeout_to_std(timeout)?;
|
||||
let port = self.port_mut()?;
|
||||
|
||||
port.set_timeout(timeout).map_err(Self::map_serial_error)?;
|
||||
port.read(buf).map_err(Self::map_io_error)
|
||||
}
|
||||
|
||||
fn flush_rx(&mut self) -> Result<(), SdkError> {
|
||||
self.port_mut()?
|
||||
.clear(ClearBuffer::Input)
|
||||
.map_err(Self::map_serial_error)
|
||||
}
|
||||
}
|
||||
97
src/types.rs
97
src/types.rs
@@ -0,0 +1,97 @@
|
||||
use serde::{Deserialize, Serialize};
|
||||
|
||||
#[repr(C)]
|
||||
#[derive(Debug, Clone, Copy, Default, Serialize, Deserialize)]
|
||||
pub struct Force3D {
|
||||
pub fx: i16,
|
||||
pub fy: i16,
|
||||
pub fz: i16,
|
||||
}
|
||||
|
||||
#[repr(C)]
|
||||
#[derive(Debug, Clone, Copy, Default, Serialize, Deserialize)]
|
||||
pub struct Force3F {
|
||||
pub fx: f32,
|
||||
pub fy: f32,
|
||||
pub fz: f32,
|
||||
}
|
||||
|
||||
#[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,
|
||||
}
|
||||
|
||||
pub const SENSOR_MODULE_COUNT: usize = 28;
|
||||
|
||||
#[repr(C)]
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
pub struct DistributionForce {
|
||||
pub module: SensorModule,
|
||||
pub point_count: u16,
|
||||
pub points: Vec<ForcePoint>,
|
||||
}
|
||||
|
||||
#[repr(C)]
|
||||
#[derive(Debug, Clone, Copy, Serialize, Deserialize)]
|
||||
pub struct CombinedForce {
|
||||
pub module: SensorModule,
|
||||
pub force: Force3D,
|
||||
}
|
||||
|
||||
#[repr(C)]
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
pub struct FingerSample {
|
||||
pub timestamp_us: u64,
|
||||
pub sequence: u32,
|
||||
pub combined_forces: Vec<CombinedForce>,
|
||||
pub distribution_forces: Vec<DistributionForce>,
|
||||
pub module_errors: Vec<ModuleError>,
|
||||
}
|
||||
|
||||
#[repr(C)]
|
||||
#[derive(Debug, Clone, Copy, Default, Serialize, Deserialize)]
|
||||
pub struct ModuleError {
|
||||
pub module: u8,
|
||||
pub error_code: u16,
|
||||
}
|
||||
|
||||
#[repr(C)]
|
||||
#[derive(Debug, Clone, Copy, Default, Serialize, Deserialize)]
|
||||
pub struct ForcePoint {
|
||||
pub fx: i8,
|
||||
pub fy: i8,
|
||||
pub fz: i8,
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user