This commit is contained in:
Lenn Louis
2026-05-04 22:42:00 +08:00
parent 985002c96d
commit 79f4055959
14 changed files with 1440 additions and 4 deletions

203
Cargo.lock generated
View File

@@ -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"

View File

@@ -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"

View File

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

View File

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

View File

@@ -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,
}