feat: add FFI layer, protocol tests, mock transport, README

- FFI: eskin_open/close/read_register/write_register for C/C++/Python
- Protocol: encode/decode tests with golden bytes verification
- Stream: implement PollingSampleCollector producing FingerSample
- Register: add parse_combined_forces/parse_module_errors
- Transport: add MockSerialTransport for testing
- Include: add C header file eskin_ffi.h
- Examples: C++ and Python usage examples
- README: full usage guide for Rust/C++/Python
- Exclude docs/ from repo (internal only)
This commit is contained in:
lenn
2026-05-06 00:54:44 +08:00
parent 60f9ad15e7
commit a7b7192341
13 changed files with 721 additions and 1915 deletions

View File

@@ -1,4 +1,8 @@
use crate::{config::DeviceConfig, error::SdkErrorCode};
use std::{ptr};
use std::ffi::{CStr, c_char};
use crate::device::EskinDevice;
use crate::transport::SerialPortTransport;
use crate::{config::DeviceConfig, device::EskinDeviceInner, error::SdkErrorCode};
pub type EskinDeviceHandle = *mut core::ffi::c_void;
@@ -10,10 +14,144 @@ pub struct EskinSdkVersion {
pub patch: u16,
}
pub trait CApi {
fn version() -> EskinSdkVersion;
fn open(path: *const libc::c_char, config: *const DeviceConfig) -> EskinDeviceHandle;
fn close(handle: EskinDeviceHandle) -> SdkErrorCode;
fn start_stream(handle: EskinDeviceHandle) -> SdkErrorCode;
fn stop_stream(handle: EskinDeviceHandle) -> SdkErrorCode;
#[repr(C)]
pub struct CFingerSample {
pub timestamp_us: u64,
pub sequence: u32,
pub combinded_force_raw: *const u8,
pub combinded_force_len: u32,
pub module_error_raw: *const u8,
pub module_error_len: u32,
}
#[allow(dead_code)]
struct DeviceWrapper {
device: EskinDeviceInner,
last_cf_raw: Vec<u8>,
last_me_raw: Vec<u8>
}
#[unsafe(no_mangle)]
pub extern "C" fn eskin_version() -> EskinSdkVersion {
EskinSdkVersion { major: 0, minor: 1, patch: 0 }
}
#[unsafe(no_mangle)]
pub unsafe extern "C" fn eskin_open(
path: *const c_char,
config: *const DeviceConfig,
) -> EskinDeviceHandle {
if path.is_null() {
return ptr::null_mut();
}
let path_str = match unsafe {
CStr::from_ptr(path)
}.to_str() {
Ok(s) => s.to_string(),
Err(_) => return ptr::null_mut()
};
let device_config = if config.is_null() {
DeviceConfig::default()
} else {
unsafe { (*config).clone() }
};
let transport = SerialPortTransport::new(path_str, 921600);
let mut device = EskinDeviceInner::new(device_config, Box::new(transport));
if device.open().is_err() {
return ptr::null_mut();
}
let wrapper = Box::new(DeviceWrapper {
device,
last_cf_raw: Vec::new(),
last_me_raw: Vec::new(),
});
Box::into_raw(wrapper) as EskinDeviceHandle
}
/// 关闭设备
#[unsafe(no_mangle)]
pub unsafe extern "C" fn eskin_close(handle: EskinDeviceHandle) -> SdkErrorCode {
if handle.is_null() {
return SdkErrorCode::InvalidPointer;
}
let wrapper = unsafe { &mut *(handle as *mut DeviceWrapper) };
match wrapper.device.close() {
Ok(()) => {
unsafe { drop(Box::from_raw(handle as *mut DeviceWrapper)) };
SdkErrorCode::Success
}
Err(_) => SdkErrorCode::IoError,
}
}
/// 读寄存器(原始字节)
#[unsafe(no_mangle)]
pub unsafe extern "C" fn eskin_read_register(
handle: EskinDeviceHandle,
addr: u32,
length: u16,
buf: *mut u8,
buf_len: u32,
actual_len: *mut u32,
) -> SdkErrorCode {
if handle.is_null() || buf.is_null() || actual_len.is_null() {
return SdkErrorCode::InvalidPointer;
}
let wrapper = unsafe { &mut *(handle as *mut DeviceWrapper) };
let data = match wrapper.device.read_register(addr, length) {
Ok(d) => d,
Err(crate::error::SdkError::Timeout) => return SdkErrorCode::Timeout,
Err(crate::error::SdkError::FrameError(_)) => return SdkErrorCode::FrameError,
Err(crate::error::SdkError::CrcError { .. }) => return SdkErrorCode::CrcError,
Err(crate::error::SdkError::DeviceError(_)) => return SdkErrorCode::DeviceError,
Err(_) => return SdkErrorCode::IoError,
};
let copy_len = std::cmp::min(data.len(), buf_len as usize);
unsafe {
ptr::copy_nonoverlapping(data.as_ptr(), buf, copy_len);
*actual_len = data.len() as u32;
}
SdkErrorCode::Success
}
/// 写寄存器(原始字节)
#[unsafe(no_mangle)]
pub unsafe extern "C" fn eskin_write_register(
handle: EskinDeviceHandle,
addr: u32,
data: *const u8,
data_len: u16,
return_count: *mut u16,
) -> SdkErrorCode {
if handle.is_null() || data.is_null() || return_count.is_null() {
return SdkErrorCode::InvalidPointer;
}
let wrapper = unsafe { &mut *(handle as *mut DeviceWrapper) };
let data_slice = unsafe { std::slice::from_raw_parts(data, data_len as usize) };
match wrapper.device.write_register(addr, data_slice) {
Ok(count) => {
unsafe { *return_count = count };
SdkErrorCode::Success
}
Err(crate::error::SdkError::Timeout) => SdkErrorCode::Timeout,
Err(crate::error::SdkError::FrameError(_)) => SdkErrorCode::FrameError,
Err(crate::error::SdkError::CrcError { .. }) => SdkErrorCode::CrcError,
Err(crate::error::SdkError::DeviceError(_)) => SdkErrorCode::DeviceError,
Err(_) => SdkErrorCode::IoError,
}
}

View File

@@ -2,7 +2,7 @@ use serde::{Deserialize, Serialize};
use crate::error::SdkError;
pub const FRAME_START_REQUEST: u16 = 0x55AA;
pub const FRAME_START_REQUEST: [u8; 2] = [0x55, 0xAA];
pub const FRAME_START_RESPONSE: u16 = 0xAA55;
pub const FUNC_READ: u8 = 0xFB;
@@ -144,7 +144,7 @@ impl ProtocolCodec for EskinProtocolCodec {
let data_len: u16 = 9;
let mut frame = Vec::with_capacity(14);
frame.extend_from_slice(&FRAME_START_REQUEST.to_le_bytes());
frame.extend_from_slice(&FRAME_START_REQUEST);
frame.extend_from_slice(&data_len.to_le_bytes());
frame.push(request.device_addr);
frame.push(0x00);
@@ -177,7 +177,7 @@ impl ProtocolCodec for EskinProtocolCodec {
.ok_or_else(|| SdkError::InvalidParameter("write frame too large".into()))?;
let mut frame = Vec::with_capacity(14 + request.data.len());
frame.extend_from_slice(&FRAME_START_REQUEST.to_le_bytes());
frame.extend_from_slice(&FRAME_START_REQUEST);
frame.extend_from_slice(&data_len.to_le_bytes());
frame.push(request.device_addr);
frame.push(0x00);
@@ -394,3 +394,46 @@ impl ProtocolCodec for EskinProtocolCodec {
X25.checksum(_data)
}
}
#[cfg(test)]
mod tests {
use super::*;
fn codec() -> EskinProtocolCodec {
EskinProtocolCodec
}
#[test]
fn encode_read_request_has_correct_structure() {
let req = ReadRequest {
device_addr: 0x34,
start_addr: 0x1C00,
read_byte_count: 168
};
let frame = codec().encode_read_request(&req).unwrap();
println!("begin eq frame");
assert_eq!(frame[0], 0x55);
assert_eq!(frame[1], 0xAA);
assert_eq!(frame[2], 0x09);
assert_eq!(frame[3], 0x00);
assert_eq!(frame[4], 0x34);
assert_eq!(frame[5], 0x00);
assert_eq!(frame[6], 0xFB);
assert_eq!(frame[7], 0x00);
assert_eq!(frame[8], 0x1C);
assert_eq!(frame[9], 0x00);
assert_eq!(frame[10], 0x00);
assert_eq!(frame[11], 0xA8);
assert_eq!(frame[12], 0x00);
let crc = codec().crc8(&frame[..frame.len() - 1]);
assert_eq!(frame[frame.len() - 1], crc);
assert_eq!(frame[13], 0x35);
assert_eq!(frame.len(), 14);
}
}

View File

@@ -1,7 +1,7 @@
use crate::{
config::DeviceInfo,
error::SdkError,
types::{DistributionForce, ForcePoint, SensorModule},
types::{CombinedForce, DistributionForce, Force3D, ForcePoint, ModuleError, SensorModule},
};
pub const REG_SERIAL_NUMBER: u32 = 0x0000;
@@ -145,3 +145,60 @@ impl RegisterMap for EskinRegisterMap {
})
}
}
pub fn parse_combined_forces(raw: &[u8]) -> Result<Vec<CombinedForce>, SdkError> {
const MODULE_COUNT: usize = 28;
const BYTES_PER_MODULE: usize = 6;
if raw.len() < MODULE_COUNT * BYTES_PER_MODULE {
return Err(SdkError::FrameError(format!(
"combined force raw too short: expected {} bytes, got {}",
MODULE_COUNT * BYTES_PER_MODULE,
raw.len()
)));
}
let mut forces = Vec::with_capacity(MODULE_COUNT);
for i in 0..MODULE_COUNT {
let offset = i * BYTES_PER_MODULE;
let fx = i16::from_le_bytes([raw[offset], raw[offset + 1]]);
let fy = i16::from_le_bytes([raw[offset + 2], raw[offset + 3]]);
let fz = i16::from_le_bytes([raw[offset + 4], raw[offset + 5]]);
forces.push(CombinedForce {
module: SensorModule::from_index(i as u8),
force: Force3D { fx, fy, fz },
});
}
Ok(forces)
}
pub fn parse_module_errors(raw: &[u8]) -> Result<Vec<ModuleError>, SdkError> {
const MODULE_COUNT: usize = 28;
const BYTES_PER_MODULE: usize = 2;
if raw.len() < MODULE_COUNT * BYTES_PER_MODULE {
return Err(SdkError::FrameError(format!(
"module error raw too short: expected {} bytes, got {}",
MODULE_COUNT * BYTES_PER_MODULE,
raw.len()
)));
}
let mut errors = Vec::new();
for i in 0..MODULE_COUNT {
let offset = i * BYTES_PER_MODULE;
let error_code = u16::from_le_bytes([raw[offset], raw[offset + 1]]);
if error_code != 0 {
errors.push(ModuleError {
module: i as u8,
error_code,
});
}
}
Ok(errors)
}

View File

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

View File

@@ -54,6 +54,43 @@ pub enum SensorModule {
Palm8 = 27,
}
impl SensorModule {
pub fn from_index(index: u8) -> Self {
match index {
0 => Self::ThumbProximal,
1 => Self::ThumbMiddle,
2 => Self::ThumbTip,
3 => Self::ThumbNail,
4 => Self::IndexProximal,
5 => Self::IndexMiddle,
6 => Self::IndexTip,
7 => Self::IndexNail,
8 => Self::MiddleProximal,
9 => Self::MiddleMiddle,
10 => Self::MiddleTip,
11 => Self::MiddleNail,
12 => Self::RingProximal,
13 => Self::RingMiddle,
14 => Self::RingTip,
15 => Self::RingNail,
16 => Self::PinkyProximal,
17 => Self::PinkyMiddle,
18 => Self::PinkyTip,
19 => Self::PinkyNail,
20 => Self::Palm1,
21 => Self::Palm2,
22 => Self::Palm3,
23 => Self::Palm4,
24 => Self::Palm5,
25 => Self::Palm6,
26 => Self::Palm7,
27 => Self::Palm8,
_ => Self::ThumbProximal,
}
}
}
pub const SENSOR_MODULE_COUNT: usize = 28;
#[repr(C)]