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:
152
src/ffi/mod.rs
152
src/ffi/mod.rs
@@ -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,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
@@ -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)
|
||||
}
|
||||
@@ -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))
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
37
src/types.rs
37
src/types.rs
@@ -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)]
|
||||
|
||||
Reference in New Issue
Block a user