diff --git a/Cargo.lock b/Cargo.lock index 18b0e9c..67999c2 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -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" diff --git a/Cargo.toml b/Cargo.toml index d7abeaf..7a9fb91 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -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" diff --git a/docs/ARCHITECTURE.md b/docs/ARCHITECTURE.md index e5f41e2..993bfdc 100644 --- a/docs/ARCHITECTURE.md +++ b/docs/ARCHITECTURE.md @@ -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; + fn read(&mut self, buf: &mut [u8], timeout: chrono::Duration) -> Result; + fn flush_rx(&mut self) -> Result<(), SdkError>; +} + +pub trait ProtocolCodec { + fn encode_read_request(&self, request: &ReadRequest) -> Result, SdkError>; + fn encode_write_request(&self, request: &WriteRequest) -> Result, SdkError>; + fn decode_read_response(&self, frame: &[u8]) -> Result; + fn decode_write_response(&self, frame: &[u8]) -> Result; + fn decode_stream_frame(&self, frame: &[u8]) -> Result; + fn crc8(&self, data: &[u8]) -> u8; +} + +pub trait RegisterMap { + fn device_info_registers(&self) -> &'static [RegisterSpec]; + fn distribution_register(&self, module: SensorModule) -> Result; + fn parse_device_info(&self, raw: &[u8]) -> Result; + fn parse_distribution_force( + &self, + module: SensorModule, + raw: &[u8], + ) -> Result; +} + +pub trait EskinDevice { + fn open(&mut self) -> Result<(), SdkError>; + fn close(&mut self) -> Result<(), SdkError>; + fn state(&self) -> DeviceState; + fn device_info(&self) -> Result; + fn apply_config(&mut self, config: DeviceConfig) -> Result<(), SdkError>; + fn start_stream(&mut self) -> Result<(), SdkError>; + fn stop_stream(&mut self) -> Result<(), SdkError>; + fn read_sample(&self, timeout_ms: u32) -> Result; + fn read_event(&self, timeout_ms: u32) -> Result; + fn read_register(&mut self, addr: u32, length: u16) -> Result, SdkError>; + fn write_register(&mut self, addr: u32, data: &[u8]) -> Result; +} +``` + +### 20.3 数据流流程图 + +```mermaid +flowchart TD + App[Rust/C/C++ App] --> Device[EskinDevice] + Device --> Channel[ChannelManager] + Device --> Codec[ProtocolCodec] + Device --> Register[RegisterMap] + Device --> Transport[SerialTransport] + Transport --> Serial[UART / USB-Serial] + Serial --> Hardware[Eskin Finger Device] + + App -->|open| Device + Device -->|open port| Transport + Device -->|read device info registers| Register + Register -->|ReadRequest| Codec + Codec -->|frame bytes| Transport + Transport -->|response bytes| Codec + Codec -->|ReadResponse| Register + Register -->|DeviceInfo| Device + + App -->|start_stream| Device + Device -->|DeviceCommand::StartStream| Channel + Device -->|poll or auto receive| Transport + Transport -->|raw frame| Codec + Codec -->|ProtocolFrame| Register + Register -->|FingerSample| Channel + Channel -->|sample_rx| App + Channel -->|event_rx| App +``` + +### 20.4 设备生命周期 + +```mermaid +stateDiagram-v2 + [*] --> Closed + Closed --> Open: open() + Open --> Streaming: start_stream() + Streaming --> Open: stop_stream() + Open --> Closed: close() + Streaming --> Error: transport/protocol error + Open --> Error: transport/protocol error + Error --> Closed: close() +``` + +--- + +## 21. 安全性考虑 1. **空指针检查**:所有 C API 入口检查指针非空 2. **状态检查**:操作前检查设备状态(是否已连接、是否正在采集等) @@ -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. **超时保护**:所有串口读操作带超时,避免永久阻塞 \ No newline at end of file +8. **超时保护**:所有串口读操作带超时,避免永久阻塞 diff --git a/src/channel.rs b/src/channel.rs new file mode 100644 index 0000000..6531c56 --- /dev/null +++ b/src/channel.rs @@ -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 }, + Shutdown, +} + +#[derive(Debug, Clone)] +pub enum DeviceEvent { + Disconnected(String), + IoError(String), + ProtocolError(String), + ConfigApplied, + StreamStarted, + StreamStopped, + SampleDropped { + count: u64, + }, + ModuleError { + module: SensorModule, + error_code: u16, + }, +} + +pub struct ChannelManager { + pub sample_tx: Sender, + pub sample_rx: Receiver, + + pub cmd_tx: Sender, + pub cmd_rx: Receiver, + + pub event_tx: Sender, + pub event_rx: Receiver, + + pub dropped_samples: AtomicU64, + + pub drop_policy: DropPolicy, +} + +impl ChannelManager { + pub fn new( + sample_capacity: usize, + 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 { + 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 { + 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 { + let timeout = std::time::Duration::from_millis(timeout_ms as u64); + self.event_rx + .recv_timeout(timeout) + .map_err(|_| SdkError::Timeout) + } +} diff --git a/src/config.rs b/src/config.rs new file mode 100644 index 0000000..6596976 --- /dev/null +++ b/src/config.rs @@ -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, + } + } +} diff --git a/src/device.rs b/src/device.rs new file mode 100644 index 0000000..353fcd5 --- /dev/null +++ b/src/device.rs @@ -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, + pub codec: Box, +} + +impl EskinDeviceInner { + pub fn new(config: DeviceConfig, transport: Box) -> 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, 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; + 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; + fn read_event(&self, timeout_ms: u32) -> Result; + fn read_register(&mut self, addr: u32, length: u16) -> Result, SdkError>; + fn write_register(&mut self, addr: u32, data: &[u8]) -> Result; +} diff --git a/src/error.rs b/src/error.rs new file mode 100644 index 0000000..ea92c7d --- /dev/null +++ b/src/error.rs @@ -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), +} diff --git a/src/ffi/mod.rs b/src/ffi/mod.rs new file mode 100644 index 0000000..0dfe3b5 --- /dev/null +++ b/src/ffi/mod.rs @@ -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; +} diff --git a/src/lib.rs b/src/lib.rs index dd198c6..290f76e 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -1 +1,10 @@ -pub mod types; \ No newline at end of file +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; diff --git a/src/protocol.rs b/src/protocol.rs new file mode 100644 index 0000000..c5a14fc --- /dev/null +++ b/src/protocol.rs @@ -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 { + 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, + pub status: Option, +} + +pub struct ReadRequest { + pub device_addr: u8, + pub start_addr: u32, + pub read_byte_count: u16, +} + +pub struct WriteRequest { + pub device_addr: u8, + pub start_addr: u32, + pub data: Vec, +} + +pub struct ReadResponse { + pub device_addr: u8, + pub start_addr: u32, + pub data: Vec, + pub status: DeviceStatus, +} + +pub struct WriteResponse { + pub device_addr: u8, + pub start_addr: u32, + pub return_byte_count: u16, + pub status: DeviceStatus, +} + +pub trait ProtocolCodec: Send + Sync { + fn encode_read_request(&self, request: &ReadRequest) -> Result, SdkError>; + fn encode_write_request(&self, request: &WriteRequest) -> Result, SdkError>; + fn decode_read_response(&self, frame: &[u8]) -> Result; + fn decode_write_response(&self, frame: &[u8]) -> Result; + fn decode_stream_frame(&self, frame: &[u8]) -> Result; + fn crc8(&self, data: &[u8]) -> u8; +} + +#[derive(Debug, Default, Clone, Copy)] +pub struct EskinProtocolCodec; + +impl EskinProtocolCodec { + fn status_from_u8(raw: u8) -> Result { + 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 { + 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 { + 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, 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, 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 { + 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 { + 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 { + 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 = crc::Crc::::new(&crc::CRC_8_I_432_1); + X25.checksum(_data) + } +} diff --git a/src/register.rs b/src/register.rs new file mode 100644 index 0000000..5dc04cf --- /dev/null +++ b/src/register.rs @@ -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; + fn parse_device_info(&self, raw: &[u8]) -> Result; + fn parse_distribution_force( + &self, + module: SensorModule, + raw: &[u8], + ) -> Result; +} + +#[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 { + todo!("distribution register spec") + } + + fn parse_device_info(&self, _raw: &[u8]) -> Result { + todo!("parse device info") + } + + fn parse_distribution_force( + &self, + _module: SensorModule, + _raw: &[u8], + ) -> Result { + todo!("parse distribution force") + } +} diff --git a/src/stream.rs b/src/stream.rs new file mode 100644 index 0000000..614ceb3 --- /dev/null +++ b/src/stream.rs @@ -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, + 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; + fn next_event(&self, timeout_ms: u32) -> Result; +} diff --git a/src/transport.rs b/src/transport.rs new file mode 100644 index 0000000..0c6edc1 --- /dev/null +++ b/src/transport.rs @@ -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; + fn read(&mut self, buf: &mut [u8], timeout: Duration) -> Result; + fn flush_rx(&mut self) -> Result<(), SdkError>; +} + +pub struct SerialPortTransport { + pub path: String, + pub baud_rate: u32, + pub port: Option>, +} + +impl SerialPortTransport { + pub fn new(path: impl Into, baud_rate: u32) -> Self { + Self { + path: path.into(), + baud_rate, + port: None, + } + } + + fn port_mut(&mut self) -> Result<&mut Box, SdkError> { + self.port + .as_mut() + .ok_or_else(|| SdkError::DeviceNotFound(self.path.clone())) + } + + fn timeout_to_std(timeout: Duration) -> Result { + 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 { + 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 { + 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) + } +} diff --git a/src/types.rs b/src/types.rs index e69de29..c24ccb3 100644 --- a/src/types.rs +++ b/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, +} + +#[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, + pub distribution_forces: Vec, + pub module_errors: Vec, +} + +#[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, +}