diff --git a/eskin-finger-sdk b/eskin-finger-sdk new file mode 160000 index 0000000..7053750 --- /dev/null +++ b/eskin-finger-sdk @@ -0,0 +1 @@ +Subproject commit 705375085f17c79a6fbba32c18fb7630da0b67a7 diff --git a/package-lock.json b/package-lock.json index 917ae2e..9db4a2c 100644 --- a/package-lock.json +++ b/package-lock.json @@ -6,7 +6,7 @@ "packages": { "": { "name": "JE-Skin", - "version": "0.3.0", + "version": "0.4.0", "license": "MIT", "dependencies": { "@tauri-apps/api": "^2", diff --git a/src-tauri/Cargo.lock b/src-tauri/Cargo.lock index 63a431a..7e7e1d3 100644 --- a/src-tauri/Cargo.lock +++ b/src-tauri/Cargo.lock @@ -8,17 +8,14 @@ version = "0.4.0" dependencies = [ "anyhow", "async-stream", - "async-trait", "axum 0.8.9", - "chrono", - "crc", "csv", "dirs", + "eskin-finger-sdk", "fern", "futures-util", "humantime", "log", - "ndarray", "prost", "prost-types", "protoc-bin-vendored", @@ -1152,6 +1149,23 @@ dependencies = [ "windows-sys 0.61.2", ] +[[package]] +name = "eskin-finger-sdk" +version = "0.1.0" +dependencies = [ + "chrono", + "crc", + "crossbeam-channel", + "fern", + "libc", + "log", + "serde", + "serde_json", + "serialport", + "thiserror 2.0.18", + "uuid", +] + [[package]] name = "event-listener" version = "5.4.1" @@ -2314,9 +2328,9 @@ dependencies = [ [[package]] name = "libc" -version = "0.2.183" +version = "0.2.186" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b5b646652bf6661599e1da8901b3b9522896f01e736bad5f723fe7a3a27f899d" +checksum = "68ab91017fe16c622486840e4c83c9a37afeff978bd239b5293d61ece587de66" [[package]] name = "libloading" @@ -2340,6 +2354,26 @@ dependencies = [ "redox_syscall 0.7.4", ] +[[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 = "linux-raw-sys" version = "0.12.1" @@ -2442,16 +2476,6 @@ version = "0.8.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "47e1ffaa40ddd1f3ed91f717a33c8c0ee23fff369e3aa8772b9605cc1d22f4c3" -[[package]] -name = "matrixmultiply" -version = "0.3.10" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a06de3016e9fae57a36fd14dba131fccf49f74b40b7fbdb472f96e361ec71a08" -dependencies = [ - "autocfg", - "rawpointer", -] - [[package]] name = "memchr" version = "2.8.0" @@ -2541,19 +2565,6 @@ version = "0.10.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1d87ecb2933e8aeadb3e3a02b828fed80a7528047e68b4f424523a0981a3a084" -[[package]] -name = "ndarray" -version = "0.15.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "adb12d4e967ec485a5f71c6311fe28158e9d6f4bc4a447b474184d0f91a8fa32" -dependencies = [ - "matrixmultiply", - "num-complex", - "num-integer", - "num-traits", - "rawpointer", -] - [[package]] name = "ndk" version = "0.9.0" @@ -2619,30 +2630,12 @@ version = "0.1.14" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "72ef4a56884ca558e5ddb05a1d1e7e1bfd9a68d9ed024c21704cc98872dae1bb" -[[package]] -name = "num-complex" -version = "0.4.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "73f88a1307638156682bada9d7604135552957b7818057dcef22705b4d509495" -dependencies = [ - "num-traits", -] - [[package]] name = "num-conv" version = "0.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "cf97ec579c3c42f953ef76dbf8d55ac91fb219dde70e49aa4a6b7d74e9919050" -[[package]] -name = "num-integer" -version = "0.1.46" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7969661fd2958a5cb096e56c8e1ad0444ac2bbcd0061bd28660485a44879858f" -dependencies = [ - "num-traits", -] - [[package]] name = "num-traits" version = "0.2.19" @@ -3647,12 +3640,6 @@ version = "0.6.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "20675572f6f24e9e76ef639bc5552774ed45f1c30e2951e1e99c59888861c539" -[[package]] -name = "rawpointer" -version = "0.2.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "60a357793950651c4ed0f3f52338f53b2f809f32d83a07f72909fa13e4c6c1e3" - [[package]] name = "redox_syscall" version = "0.5.18" @@ -4263,6 +4250,7 @@ dependencies = [ "core-foundation", "core-foundation-sys", "io-kit-sys", + "libudev", "mach2", "nix 0.26.4", "scopeguard", @@ -5565,9 +5553,9 @@ checksum = "b6c140620e7ffbb22c2dee59cafe6084a59b5ffc27a8859a5f0d494b5d52b6be" [[package]] name = "uuid" -version = "1.22.0" +version = "1.23.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a68d3c8f01c0cfa54a75291d83601161799e4a89a39e0929f4b0354d88757a37" +checksum = "ddd74a9687298c6858e9b88ec8935ec45d22e8fd5e6394fa1bd4e99a87789c76" dependencies = [ "getrandom 0.4.2", "js-sys", diff --git a/src-tauri/Cargo.toml b/src-tauri/Cargo.toml index 8a19afb..85febb7 100644 --- a/src-tauri/Cargo.toml +++ b/src-tauri/Cargo.toml @@ -17,7 +17,6 @@ crate-type = ["staticlib", "cdylib", "rlib"] [features] default = [] devkit = ["dep:tonic", "dep:prost", "dep:prost-types", "dep:async-stream", "dep:dirs"] -multi-dim = ["dep:ndarray"] [build-dependencies] tauri-build = { version = "2", features = [] } @@ -37,22 +36,19 @@ async-stream = { version = "0.3", optional = true } dirs = { version = "6", optional = true } tokio-serial = { version = "5.4.5" } tokio = { version = "1.50.0", features = ["full"] } -async-trait = "0.1.89" tokio-util = "0.7.18" serde_json = "1" fern = { version = "0.7.1", features = ["colored", "date-based"] } log = "0.4.29" humantime = "2.3.0" csv = "1.4.0" -chrono = "0.4.44" -crc = "3.4.0" axum = { version = "0.8", features = ["ws"] } tower-http = { version = "0.6", features = ["cors"] } futures-util = "0.3" uuid = { version = "1", features = ["v4", "serde"] } rand = "0.8" reqwest = { version = "0.12", default-features = false, features = ["json", "rustls-tls"] } -ndarray = { version = "0.15", optional = true } +eskin-finger-sdk = { path = "../eskin-finger-sdk" } [target.'cfg(not(any(target_os = "android", target_os = "ios")))'.dependencies] tauri-plugin-updater = "2" diff --git a/src-tauri/src/commands/serial.rs b/src-tauri/src/commands/serial.rs index 8cb5df6..dafd92e 100644 --- a/src-tauri/src/commands/serial.rs +++ b/src-tauri/src/commands/serial.rs @@ -1,27 +1,18 @@ -use crate::serial_core::codecs::tactile_a::{ - export_recording_csv, TactileACodec, TactileACsvImporter, TactileAHandler, -}; use crate::serial_core::error::SerialError; -use crate::serial_core::record::CsvImporter; -use crate::serial_core::serial::{PollMode, TactileAPollRequester}; -use crate::serial_core::{serial, TactileARecording}; +use crate::serial_core::record::{self, FingerRecording}; +use crate::serial_core::serial; +use eskin_finger_sdk::device::EskinDevice; use log::info; use serde::Serialize; -use std::fs::File; use std::io::Cursor; use std::path::{Path, PathBuf}; use std::sync::{Arc, Mutex}; -use std::time::{Duration, Instant, SystemTime, UNIX_EPOCH}; +use std::time::{SystemTime, UNIX_EPOCH}; use tauri::{async_runtime::JoinHandle, AppHandle, Manager, State}; -use tokio_serial::{available_ports, SerialPortBuilderExt}; +use tokio_serial::available_ports; use tokio_util::sync::CancellationToken; -const DEFAULT_TACTILE_COLS: usize = 7; -const DEFAULT_TACTILE_ROWS: usize = 12; -const DEFAULT_TACTILE_POLL_INTERVAL_MS: u64 = 10; -const DEFAULT_TACTILE_REPLY_TIMEOUT_MS: u64 = 140; - -type SharedTactileRecording = Arc>; +type SharedRecording = Arc>; #[derive(Serialize)] #[serde(rename_all = "camelCase")] @@ -67,18 +58,18 @@ struct SerialSession { port: String, cancel: CancellationToken, task: JoinHandle<()>, - current_record: SharedTactileRecording, + current_record: SharedRecording, } #[derive(Default)] pub struct SerialConnectionState { session: Mutex>, - last_record: Mutex>, + last_record: Mutex>, } pub async fn shutdown_active_session( state: &SerialConnectionState, -) -> Result, SerialError> { +) -> Result, SerialError> { let session = { let mut guard = state.session.lock().map_err(|_| SerialError::StateError)?; guard.take() @@ -148,62 +139,41 @@ pub async fn serial_connect( } let cancel = CancellationToken::new(); - let current_record = Arc::new(Mutex::new(TactileARecording::new())); + let current_record = Arc::new(Mutex::new(FingerRecording::new())); let task_record = current_record.clone(); let task_cancel = cancel.clone(); let task_app = app.clone(); let task_port_name = port_name.clone(); - let port = tokio_serial::new(&port_name, 921600) - .open_native_async() - .map_err(|_| SerialError::OpenError)?; - let session_started_at = Instant::now(); - let task = tauri::async_runtime::spawn(async move { - let codec = TactileACodec::new(DEFAULT_TACTILE_COLS, DEFAULT_TACTILE_ROWS); - let handler = TactileAHandler; - let poll_mode = PollMode::Enabled(Box::new(TactileAPollRequester::new( - Duration::from_millis(DEFAULT_TACTILE_POLL_INTERVAL_MS), - DEFAULT_TACTILE_COLS, - DEFAULT_TACTILE_ROWS, - Duration::from_millis(DEFAULT_TACTILE_REPLY_TIMEOUT_MS), - ))); + // Open device using SDK + let session = match serial::open_device(&task_port_name) { + Ok(s) => s, + Err(e) => { + eprintln!("Failed to open device: {e}"); + cleanup_session(&task_app, &task_port_name, task_record).await; + return; + } + }; - if let Err(error) = serial::run_serial_with_poll( + let mut device = session.device; + + // Run stream with recording + if let Err(error) = serial::run_stream_with_record( task_app.clone(), - port, - codec, - handler, - session_started_at, - task_record.clone(), + &mut device, task_cancel, - poll_mode, + task_record.clone(), ) .await { eprintln!("serial task exited with error: {error}"); } - let manager = task_app.state::(); - if let Ok(mut last_record) = manager.last_record.lock() { - *last_record = Some(task_record); - } + // Close device + let _ = device.close(); - let mut session = match manager.session.lock() { - Ok(session) => session, - Err(_) => return, - }; - - { - let should_clear = session - .as_ref() - .map(|current| current.port.as_str() == task_port_name.as_str()) - .unwrap_or(false); - - if should_clear { - session.take(); - } - } + cleanup_session(&task_app, &task_port_name, task_record).await; }); let mut session = state.session.lock().map_err(|_| SerialError::StateError)?; @@ -227,6 +197,31 @@ pub async fn serial_connect( }) } +async fn cleanup_session( + app: &AppHandle, + port_name: &str, + record: SharedRecording, +) { + let manager = app.state::(); + if let Ok(mut last_record) = manager.last_record.lock() { + *last_record = Some(record); + } + + let mut session = match manager.session.lock() { + Ok(session) => session, + Err(_) => return, + }; + + let should_clear = session + .as_ref() + .map(|current| current.port.as_str() == port_name) + .unwrap_or(false); + + if should_clear { + session.take(); + } +} + #[tauri::command] pub async fn serial_disconnect( state: State<'_, SerialConnectionState>, @@ -293,8 +288,8 @@ pub fn serial_export_csv_to_path( state: State<'_, SerialConnectionState>, ) -> Result { let output_path = resolve_export_path(file_path)?; - let record = resolve_record_for_export(&state)?; - let frame_count = write_record_to_csv(record, &output_path)?; + let rec = resolve_record_for_export(&state)?; + let frame_count = write_record_to_csv(rec, &output_path)?; let path = output_path.display().to_string(); info!("csv exported to {path}, frame_count={frame_count}"); @@ -311,22 +306,20 @@ pub fn serial_import_csv( file_name: String, csv_content: String, ) -> Result { - let mut importer = TactileACsvImporter::new(file_name.as_str()); - let packets = importer - .load(Cursor::new(csv_content.into_bytes())) + let packets = record::import_csv(Cursor::new(csv_content.into_bytes())) .map_err(|_| SerialError::ImportError)?; if packets.is_empty() { return Err(SerialError::NoRecordedData); } - let channel_count = packets.first().map(|item| item.data.len()).unwrap_or(0); + let channel_count = 1; // fz is a single value per sample let frame_count = packets.len(); let frames = packets .into_iter() .map(|packet| SerialImportFrame { - data: packet.data, - dts_ms: packet.dts_ms, + data: vec![packet.fz as i32], + dts_ms: packet.timestamp_us / 1000, }) .collect(); @@ -355,7 +348,7 @@ pub fn serial_import_csv_from_path(file_path: String) -> Result, -) -> Result { +) -> Result { let current_record = { let session = state.session.lock().map_err(|_| SerialError::StateError)?; session @@ -406,7 +399,7 @@ fn snapshot_record_frame_count( } fn write_record_to_csv( - record: SharedTactileRecording, + record: SharedRecording, output_path: &Path, ) -> Result { if let Some(parent) = output_path.parent() { @@ -415,14 +408,14 @@ fn write_record_to_csv( } } - let mut file = File::create(output_path).map_err(|_| SerialError::ExportError)?; + let file = std::fs::File::create(output_path).map_err(|_| SerialError::ExportError)?; let frame_count = { let recording = record.lock().map_err(|_| SerialError::StateError)?; if recording.frames.is_empty() { return Err(SerialError::NoRecordedData); } - export_recording_csv(&recording, &mut file).map_err(|_| SerialError::ExportError)?; + record::export_recording_csv(&recording, file).map_err(|_| SerialError::ExportError)?; recording.frames.len() }; @@ -468,4 +461,4 @@ fn resolve_absolute_path(raw_path: &str) -> std::io::Result { } else { Ok(std::env::current_dir()?.join(path)) } -} +} \ No newline at end of file diff --git a/src-tauri/src/serial_core/codec.rs b/src-tauri/src/serial_core/codec.rs deleted file mode 100644 index bf37bee..0000000 --- a/src-tauri/src/serial_core/codec.rs +++ /dev/null @@ -1,6 +0,0 @@ -use crate::serial_core::error::CodecError; -use std::time::Instant; -pub trait Codec { - fn decode(&mut self, input: &[u8], session_started_at: Instant) -> Result, CodecError>; - fn encode(&self, frame: &F) -> Result, CodecError>; -} diff --git a/src-tauri/src/serial_core/codecs/mod.rs b/src-tauri/src/serial_core/codecs/mod.rs deleted file mode 100644 index d4b0944..0000000 --- a/src-tauri/src/serial_core/codecs/mod.rs +++ /dev/null @@ -1,5 +0,0 @@ -use crate::serial_core::{frame::TestFrame, record::Recording}; - -pub mod test; -pub mod tactile_a; -pub type TestRecording = Recording; \ No newline at end of file diff --git a/src-tauri/src/serial_core/codecs/tactile_a.rs b/src-tauri/src/serial_core/codecs/tactile_a.rs deleted file mode 100644 index a8bcee0..0000000 --- a/src-tauri/src/serial_core/codecs/tactile_a.rs +++ /dev/null @@ -1,382 +0,0 @@ -use crate::serial_core::error::CodecError; -use crate::serial_core::frame::{ - FrameHandler, TactileAFrameMetaData, TactileARepFrame, TactileAReqFrame, -}; -use crate::serial_core::record::{write_csv, CsvExporter, CsvImporter, RecordedFrame, Recording}; -use crate::serial_core::utils::{calc_crc8_itu, elapsed_millis}; -use crate::serial_core::{ - codec::Codec, - frame::{TactileAFrame, TactileAFrameStatusCode}, -}; -use async_trait::async_trait; -use csv::StringRecord; -use anyhow::anyhow; -use std::io::Read; -use log::debug; - -const FRAME_BUFFER_MIN_LENGTH: usize = 15; - -pub struct TactileACodec { - buffer: Vec, - expected_data_len: usize, -} - -pub struct TactileACsvExporter { - channels: usize, -} - -pub struct TactileACsvImporter { - channels: usize, - data_row: usize, - packets: Vec, -} - -pub struct TactileAHandler; - -#[derive(Clone)] -pub struct TactileADataPacket { - pub data: Vec, - pub dts_ms: u64, -} - -impl From for TactileAFrameStatusCode { - fn from(value: u8) -> Self { - match value { - 0 => TactileAFrameStatusCode::Success, - _ => TactileAFrameStatusCode::Failure, - } - } -} - -impl TryFrom<&TactileARepFrame> for TactileADataPacket { - type Error = CodecError; - fn try_from(value: &TactileARepFrame) -> Result { - let data = TactileACodec::parse_data_frame(&value.payload)?; - let dts_ms = value.dts_ms; - Ok(TactileADataPacket { - data: data, - dts_ms: dts_ms, - }) - } -} - -impl TactileACodec { - pub fn new(cols: usize, rows: usize) -> TactileACodec { - Self { - buffer: Vec::new(), - expected_data_len: cols * rows * 2, - } - } - - pub fn parse_data_frame(data: &[u8]) -> Result, CodecError> { - if data.len() % 2 != 0 { - return Err(CodecError::InvalidLength); - } - - let vals: Vec = data - .chunks_exact(2) - .map(|chunk| { - let raw = u16::from_le_bytes([chunk[0], chunk[1]]) as i32; - if raw < 15 { - 0 - } else { - raw - } - }) - .collect::>(); - - Ok(vals) - } - - pub fn build_req_frame(cols: usize, rows: usize) -> anyhow::Result { - let header = [0x55, 0xAA]; - let payload_len: usize = 9; - let device_addr: u8 = 0x34; - let extend_code: u8 = 0x00; - let func_code: u8 = 0xFB; - let start_addr: u32 = 7168; - let except_data_len: usize = cols * rows * 2; - let checksum: u8 = 0; - Ok(TactileAFrame::Req(TactileAReqFrame { - meta: TactileAFrameMetaData { - header, - payload_len, - device_addr, - extend_code, - func_code, - start_addr, - except_data_len, - checksum, - }, - })) - } -} - -impl Codec for TactileACodec { - fn decode( - &mut self, - input: &[u8], - session_started_at: std::time::Instant, - ) -> Result, CodecError> { - self.buffer.extend_from_slice(input); - let mut frames: Vec = Vec::new(); - - loop { - if self.buffer.len() < FRAME_BUFFER_MIN_LENGTH { - break; - } - - let header_pos = self.buffer.windows(2).position(|w| w == [0xAA, 0x55]); - - let Some(pos) = header_pos else { - self.buffer.clear(); - break; - }; - if pos > 0 { - self.buffer.drain(0..pos); - } - - if self.buffer.len() < FRAME_BUFFER_MIN_LENGTH { - break; - } - - let header = [self.buffer[0], self.buffer[1]]; - let payload_len = u16::from_le_bytes([self.buffer[2], self.buffer[3]]) as usize; - let device_addr = self.buffer[4]; - let extend_code = self.buffer[5]; - let func_code = self.buffer[6]; - let start_addr = u32::from_le_bytes([ - self.buffer[7], - self.buffer[8], - self.buffer[9], - self.buffer[10], - ]); - let except_data_len = u16::from_le_bytes([self.buffer[11], self.buffer[12]]) as usize; - let status = TactileAFrameStatusCode::from(self.buffer[13]); - if except_data_len != self.expected_data_len { - debug!( - "unexpected payload length: expected {}, got {}, buffer_len={}", - self.expected_data_len, - except_data_len, - self.buffer.len() - ); - self.buffer.drain(0..1); - continue; - } - - let frame_length = except_data_len + FRAME_BUFFER_MIN_LENGTH; - if self.buffer.len() < frame_length { - break; - } - - let need_check_data = self.buffer[0..14 + except_data_len].to_vec(); - let payload = self.buffer[14..14 + except_data_len].to_vec(); - let crc8_itu_alg = crc::Crc::::new(&crc::CRC_8_I_432_1); - let checksum = crc8_itu_alg.checksum(&need_check_data.as_slice()); - if self.buffer[frame_length - 1] != checksum { - debug!( - "checksum mismatch: expected {:02X}, got {:02X}, frame_len={}", - checksum, - self.buffer[frame_length - 1], - frame_length - ); - self.buffer.drain(0..1); - continue; - } - let dts_ms = elapsed_millis(session_started_at); - let meta: TactileAFrameMetaData = TactileAFrameMetaData { - header, - payload_len, - device_addr, - extend_code, - func_code, - start_addr, - except_data_len, - checksum, - }; - frames.push(TactileAFrame::Rep({ - TactileARepFrame { - meta, - status, - payload, - dts_ms, - } - })); - - self.buffer.drain(0..frame_length); - } - - Ok(frames) - } - - fn encode( - &self, - frame: &TactileAFrame, - ) -> Result, crate::serial_core::error::CodecError> { - match frame { - TactileAFrame::Req(f) => { - let mut req_bytes: Vec = Vec::new(); - req_bytes.extend_from_slice(f.meta.header.as_slice()); - req_bytes.extend_from_slice((f.meta.payload_len as u16).to_le_bytes().as_slice()); - req_bytes.push(f.meta.device_addr); - req_bytes.push(f.meta.extend_code); - req_bytes.push(f.meta.func_code); - - req_bytes.extend_from_slice(f.meta.start_addr.to_le_bytes().as_slice()); - req_bytes.extend_from_slice((f.meta.except_data_len as u16).to_le_bytes().as_slice()); - let checksum = calc_crc8_itu(req_bytes.as_slice()); - req_bytes.push(checksum); - Ok(req_bytes) - } - _ => { - Err(CodecError::InvalidFrameType) - } - } - } -} - -#[async_trait] -impl FrameHandler for TactileAHandler { - async fn on_frame(&mut self, frame: &TactileAFrame) -> anyhow::Result>> { - match frame { - TactileAFrame::Rep(rep) => { - let vals = TactileACodec::parse_data_frame(&rep.payload)?; - Ok(Some(vals)) - } - _ => Ok(None), - } - } -} - -impl TactileACsvExporter { - fn new(channels: usize) -> Self { - TactileACsvExporter { channels } - } -} - -impl CsvExporter for TactileACsvExporter { - type Error = CodecError; - fn csv_header(&self, _recording: &Recording) -> Vec { - let mut header: Vec = Vec::new(); - for i in 0..self.channels { - header.push(format!("channel{}", i + 1)); - } - - header.push("dts".to_string()); - header.push("summary".to_string()); - header - } - - fn csv_row( - &self, - item: &RecordedFrame, - ) -> anyhow::Result> { - let packet = TactileADataPacket::try_from(&item.frame)?; - let summary: i32 = packet.data.iter().sum(); - let mut row: Vec = packet.data.iter().map(|x| x.to_string()).collect(); - row.push(packet.dts_ms.to_string()); - row.push(summary.to_string()); - Ok(row) - } -} - -impl CsvExporter for TactileACsvExporter { - type Error = CodecError; - - fn csv_header(&self, _recording: &Recording) -> Vec { - let mut header: Vec = Vec::new(); - for i in 0..self.channels { - header.push(format!("channel{}", i + 1)); - } - - header.push("dts".to_string()); - header - } - - fn csv_row( - &self, - item: &RecordedFrame, - ) -> anyhow::Result> { - let rep = match &item.frame { - TactileAFrame::Rep(rep) => rep, - TactileAFrame::Req(_) => return Err(anyhow!("request frame cannot be exported to csv row")), - }; - - let packet = TactileADataPacket::try_from(rep)?; - let mut row: Vec = packet.data.iter().map(|x| x.to_string()).collect(); - row.push(packet.dts_ms.to_string()); - Ok(row) - } -} - -impl TactileACsvImporter { - pub fn new(_path: &str) -> TactileACsvImporter { - Self { - channels: 0, - data_row: 0, - packets: Vec::new(), - } - } - - fn parse_record(&mut self, record: StringRecord) -> anyhow::Result { - if self.channels == 0 { - return Err(anyhow!("csv header is missing channel columns")); - } - - if record.len() < self.channels + 1 { - return Err(anyhow!("csv row has insufficient columns")); - } - - let mut data = Vec::with_capacity(self.channels); - for index in 0..self.channels { - let cell = record.get(index).ok_or_else(|| anyhow!("missing channel cell"))?; - data.push(cell.parse::()?); - } - - let dts_cell = record - .get(self.channels) - .ok_or_else(|| anyhow!("missing dts cell"))?; - let dts_ms = dts_cell.parse::()?; - - Ok(TactileADataPacket { - data: data, - dts_ms: dts_ms, - }) - } -} - -impl CsvImporter for TactileACsvImporter { - fn load(&mut self, reader: R) -> anyhow::Result> { - let mut rdr = csv::Reader::from_reader(reader); - let headers = rdr.headers()?.clone(); - self.channels = headers.len().saturating_sub(1); - self.data_row = 0; - self.packets.clear(); - - for record in rdr.records() { - let record = record?; - let packet = self.parse_record(record)?; - self.packets.push(packet); - self.data_row += 1; - } - - Ok(self.packets.clone()) - } -} - -pub fn export_recording_csv(recording: &Recording, writer: W) -> anyhow::Result<()> -where - W: std::io::Write, -{ - let channel_nb = recording - .frames - .iter() - .find_map(|frame| match &frame.frame { - TactileAFrame::Rep(rep) => Some(rep.payload.len() / 2), - TactileAFrame::Req(_) => None, - }) - .unwrap_or(0); - - let exporter = TactileACsvExporter::new(channel_nb); - write_csv(recording, &exporter, writer) -} diff --git a/src-tauri/src/serial_core/codecs/test.rs b/src-tauri/src/serial_core/codecs/test.rs deleted file mode 100644 index ad4fc60..0000000 --- a/src-tauri/src/serial_core/codecs/test.rs +++ /dev/null @@ -1,256 +0,0 @@ -use std::io::Read; -use std::time::Instant; -use crate::serial_core::frame::{FrameHandler}; -use crate::serial_core::{codec::Codec, error::CodecError, frame::TestFrame}; -use anyhow::anyhow; -use async_trait::async_trait; -use csv::StringRecord; -use crate::serial_core::record::{write_csv, CsvExporter, CsvImporter, RecordedFrame, Recording}; -use crate::serial_core::utils::{ - elapsed_millis, - usize_to_u16_be_bytes -}; -pub struct TestCodec { - buffer: Vec, -} - -pub struct TestHandler; - -impl TestCodec { - pub fn new() -> TestCodec { - Self { buffer: Vec::new() } - } -} - -impl Codec for TestCodec { - fn decode(&mut self, input: &[u8], session_started_at: Instant) -> Result, CodecError> { - self.buffer.extend_from_slice(input); - let mut frames = Vec::new(); - - loop { - if self.buffer.len() < 6 { - break; - } - - let header_pos = self.buffer.windows(2).position(|w| w == [0xAA, 0x55]); - - let Some(pos) = header_pos else { - self.buffer.clear(); - break; - }; - if pos > 0 { - self.buffer.drain(0..pos); - } - - if self.buffer.len() < 6 { - break; - } - - let cmd = self.buffer[2]; - let length_bytes = [self.buffer[3], self.buffer[4]]; - let length = u16::from_be_bytes(length_bytes) as usize; - let frame_length = (length + 6) as usize; - if self.buffer.len() < frame_length { - break; - } - let payload = self.buffer[5..5 + length].to_vec(); - // let checksum = crc8(payload.as_slice()); - let crc8_alg = crc::Crc::::new(&crc::CRC_8_SMBUS); - let checksum = crc8_alg.checksum(payload.as_slice()); - if self.buffer[frame_length - 1] != checksum { - self.buffer.drain(0..1); - continue; - } - let dts = elapsed_millis(session_started_at); - println!("dts_ms: {dts}"); - frames.push(TestFrame { - header: [0xAA, 0x55], - cmd: cmd, - length: length, - payload: payload, - checksum: checksum, - dts_ms: dts, - }); - - self.buffer.drain(0..frame_length); - } - - Ok(frames) - } - fn encode(&self, frame: &TestFrame) -> Result, CodecError> { - let _ = u16::try_from(frame.payload.len()).map_err(|_| CodecError::PayloadTooLarge)?; - let mut out = Vec::with_capacity(6 + frame.length); - out.extend_from_slice(&frame.header); - out.push(frame.cmd); - out.extend_from_slice(&usize_to_u16_be_bytes(frame.length)); - out.extend_from_slice(&frame.payload); - out.push(frame.checksum); - - Ok(out) - } -} - -#[async_trait] -impl FrameHandler for TestHandler { - async fn on_frame(&mut self, frame: &TestFrame) -> anyhow::Result>> { - match frame.cmd { - 0x01 => { - let vals = parse_data_frame(&frame.payload)?; - Ok(Some(vals)) - } - _ => Ok(None), - } - } -} - -fn parse_data_frame(data: &[u8]) -> Result, CodecError> { - if data.len() % 2 != 0 { - return Err(CodecError::InvalidLength); - } - - let vals: Vec = data - .chunks_exact(2) - .map(|chunk| u16::from_be_bytes([chunk[0], chunk[1]]) as i32) - .collect::>(); - - Ok(vals) -} - -pub struct TestCsvExporter; -pub struct TestCsvImporter { - channels: usize, - data_row: usize, - packets: Vec, -} - -#[derive(Clone)] -pub struct TestDataPacket { - pub data: Vec, - pub dts_ms: u64 -} - -impl TryFrom<&TestFrame> for TestDataPacket { - type Error = CodecError; - fn try_from(frame: &TestFrame) -> Result { - let data = parse_data_frame(&frame.payload)?; - let dts = frame.dts_ms; - Ok(TestDataPacket { data: data, dts_ms: dts }) - } -} -// impl From for TestDataPacket { -// fn from(frame: TestFrame) -> Self { -// let data = parse_data_frame(&frame.payload)?; -// let dts = frame.dts_ms; -// TestDataPacket { data: data, dts_ms: dts } -// } -// } - - -impl CsvExporter for TestCsvExporter { - type Error = CodecError; - fn csv_header(&self, recording: &Recording) -> Vec { - let channel_nb = recording - .frames - .iter() - .find_map(|frame| parse_data_frame(&frame.frame.payload).ok().map(|vals| vals.len())) - .unwrap_or(0); - let mut header: Vec = Vec::new(); - for i in 0..channel_nb { - header.push(format!("channel{}", i + 1)); - } - header.push("dts".to_string()); - - header - } - - fn csv_row(&self, item: &RecordedFrame) -> anyhow::Result> { - let packet: TestDataPacket = TestDataPacket::try_from(&item.frame)?; - let mut row: Vec = packet.data.iter().map(|&x| x.to_string()).collect(); - row.push(packet.dts_ms.to_string()); - Ok(row) - } -} - -impl TestCsvImporter { - pub fn new(_path: &str) -> TestCsvImporter { - Self { - channels: 0, - data_row: 0, - packets: Vec::new(), - } - } - - fn parse_record(&mut self, record: StringRecord) -> anyhow::Result{ - if self.channels == 0 { - return Err(anyhow!("csv header is missing channel columns")); - } - - if record.len() < self.channels + 1 { - return Err(anyhow!("csv row has insufficient columns")); - } - - let mut data = Vec::with_capacity(self.channels); - for index in 0..self.channels { - let cell = record.get(index).ok_or_else(|| anyhow!("missing channel cell"))?; - data.push(cell.parse::()?); - } - - let dts_cell = record - .get(self.channels) - .ok_or_else(|| anyhow!("missing dts cell"))?; - let dts_ms = dts_cell.parse::()?; - - Ok(TestDataPacket { - data: data, - dts_ms: dts_ms, - }) - } -} - -impl CsvImporter for TestCsvImporter { - fn load(&mut self, reader: R) -> anyhow::Result> { - let mut rdr = csv::Reader::from_reader(reader); - let headers = rdr.headers()?.clone(); - self.channels = headers.len().saturating_sub(1); - self.data_row = 0; - self.packets.clear(); - - for record in rdr.records() { - let record = record?; - let packet = self.parse_record(record)?; - self.packets.push(packet); - self.data_row += 1; - } - - Ok(self.packets.clone()) - } -} - - -pub fn export_recording_csv(recording: &Recording, writer: W) -> anyhow::Result<()> -where - W: std::io::Write, -{ - write_csv(recording, &TestCsvExporter, writer) -} - -#[cfg(test)] -mod tests { - use super::*; - use csv::Reader; - use std::io::Cursor; - - #[test] - fn test_read_csv_basic() -> anyhow::Result<()> { - let mut rdr = Reader::from_path("recording_20260329_125238.csv")?; - let headers = rdr.headers()?; - println!("headers: {:?}", headers); - - for result in rdr.records() { - let record = result?; - println!("record: {:?}", record); - } - - Ok(()) - } -} diff --git a/src-tauri/src/serial_core/frame.rs b/src-tauri/src/serial_core/frame.rs deleted file mode 100644 index 42d23a6..0000000 --- a/src-tauri/src/serial_core/frame.rs +++ /dev/null @@ -1,57 +0,0 @@ -use anyhow::Result; -use async_trait::async_trait; -#[derive(Debug, Clone, PartialEq, Eq)] -pub struct TestFrame { - pub header: [u8; 2], - pub cmd: u8, - pub length: usize, - pub payload: Vec, - pub checksum: u8, - pub dts_ms: u64 -} - -#[derive(Debug, Clone, PartialEq, Eq)] -pub struct TactileAFrameMetaData { - pub header: [u8; 2], - pub payload_len: usize, - pub device_addr: u8, - pub extend_code: u8, - pub func_code: u8, - pub start_addr: u32, - pub except_data_len: usize, - // pub status: u8, - // pub payload_data: Vec, - pub checksum: u8, - // pub dts_ms: u64, -} - -#[derive(Debug, Clone, PartialEq, Eq)] -pub struct TactileAReqFrame { - pub meta: TactileAFrameMetaData, -} - -#[derive(Debug, Clone, PartialEq, Eq)] -pub struct TactileARepFrame { - pub meta: TactileAFrameMetaData, - pub status: TactileAFrameStatusCode, - pub payload: Vec, - pub dts_ms: u64 -} - -#[derive(Debug, Clone, PartialEq, Eq)] -pub enum TactileAFrameStatusCode { - Success, - Failure -} - -#[derive(Debug, Clone, PartialEq, Eq)] -pub enum TactileAFrame { - Req(TactileAReqFrame), - Rep(TactileARepFrame) -} - -#[async_trait] -pub trait FrameHandler: Send { - async fn on_frame(&mut self, frame: &F) -> Result>>; -} - diff --git a/src-tauri/src/serial_core/mod.rs b/src-tauri/src/serial_core/mod.rs index 3a3e73c..cf62075 100644 --- a/src-tauri/src/serial_core/mod.rs +++ b/src-tauri/src/serial_core/mod.rs @@ -1,34 +1,4 @@ -use crate::serial_core::{ - frame::{TactileAFrame, TestFrame}, - record::Recording, -}; - -pub mod codec; -pub mod codecs; pub mod error; -pub mod frame; pub mod model; pub mod serial; -pub mod record; -pub mod utils; -#[cfg(feature = "multi-dim")] -pub mod multi_dim_force; - -pub type TestRecording = Recording; -pub type TactileARecording = Recording; - -pub struct SerialConnection { - pub port: String, -} - -pub fn connect(port: &str) -> Result { - let port = port.trim(); - - if port.is_empty() { - return Err("Serial port is required".to_string()); - } - - Ok(SerialConnection { - port: port.to_string(), - }) -} +pub mod record; \ No newline at end of file diff --git a/src-tauri/src/serial_core/model.rs b/src-tauri/src/serial_core/model.rs index ce5b9e9..a714a4a 100644 --- a/src-tauri/src/serial_core/model.rs +++ b/src-tauri/src/serial_core/model.rs @@ -1,8 +1,6 @@ -use crate::serial_core::frame::TestFrame; use std::collections::HashMap; use std::time::{Duration, Instant, SystemTime, UNIX_EPOCH}; -const MAX_POINTS: usize = 28; const MAX_SUMMARY_POINTS: usize = 42; const PANEL_STALE_AFTER: Duration = Duration::from_millis(2400); @@ -74,16 +72,6 @@ pub struct HudSignalIcon { pub tone: HudTone, } -struct HudPanelUpdate { - source_id: String, - values: Vec, -} - -struct PanelEntry { - panel: HudSignalPanel, - last_seen: Instant, -} - pub struct HudChartState { panels: HashMap, order: Vec, @@ -92,6 +80,11 @@ pub struct HudChartState { last_frame_seen: Option, } +struct PanelEntry { + panel: HudSignalPanel, + last_seen: Instant, +} + impl HudChartState { pub fn new() -> Self { Self { @@ -105,76 +98,21 @@ impl HudChartState { pub fn record_summary(&mut self, value: f32) { push_summary_point(&mut self.summary_points, value); + self.last_frame_seen = Some(Instant::now()); } - pub fn record_pressure_matrix(&mut self, values: &[i32]) { + pub fn record_pressure_matrix(&mut self, values: &[f32]) { if values.is_empty() { return; } - - self.pressure_matrix = Some(values.iter().map(|value| *value as f32).collect()); - } - - pub fn apply_frame(&mut self, frame: &TestFrame, decoded_values: Option<&[i32]>) -> HudPacket { - let now = Instant::now(); - self.last_frame_seen = Some(now); - - for update in expand_frame_updates(frame, decoded_values) { - self.apply_update(update, now); - } - - self.prune_stale_at(now); - self.snapshot() + self.pressure_matrix = Some(values.to_vec()); } pub fn prune_stale(&mut self) -> Option { + let now = Instant::now(); let before = self.panels.len(); - let summary_points_before = self.summary_points.len(); - self.prune_stale_at(Instant::now()); + let summary_before = self.summary_points.len(); - if before == self.panels.len() && summary_points_before == self.summary_points.len() { - return None; - } - - Some(self.snapshot()) - } - - fn apply_update(&mut self, update: HudPanelUpdate, now: Instant) { - if update.values.is_empty() { - return; - } - - if !self.panels.contains_key(&update.source_id) { - let next_side = side_for_index(self.order.len()); - self.order.push(update.source_id.clone()); - self.panels.insert( - update.source_id.clone(), - PanelEntry { - panel: build_panel(&update.source_id, next_side, update.values.len()), - last_seen: now, - }, - ); - } - - let entry = self - .panels - .get_mut(&update.source_id) - .expect("panel entry should exist after insertion"); - - entry.last_seen = now; - entry.panel.active = true; - ensure_panel_channels(&mut entry.panel, update.values.len()); - - for (index, value) in update.values.into_iter().enumerate() { - if let Some(series) = entry.panel.series.get_mut(index) { - push_point(&mut series.points, value); - } - } - - refresh_panel_stats(&mut entry.panel); - } - - fn prune_stale_at(&mut self, now: Instant) { self.panels .retain(|_, entry| now.duration_since(entry.last_seen) <= PANEL_STALE_AFTER); self.order.retain(|id| self.panels.contains_key(id)); @@ -189,6 +127,16 @@ impl HudChartState { self.pressure_matrix = None; self.last_frame_seen = None; } + + if before == self.panels.len() && summary_before == self.summary_points.len() { + return None; + } + + Some(self.snapshot()) + } + + pub fn build_snapshot(&mut self) -> HudPacket { + self.snapshot() } fn snapshot(&mut self) -> HudPacket { @@ -223,106 +171,6 @@ impl Default for HudChartState { } } -fn build_panel(source_id: &str, side: HudPanelSide, channel_count: usize) -> HudSignalPanel { - HudSignalPanel { - id: format!("panel-{source_id}"), - code: source_id.to_string(), - title: format!("Source {source_id}"), - side, - active: true, - series: build_panel_series(source_id, channel_count, &[]), - icons: build_panel_icons(source_id, channel_count), - latest: None, - min: None, - max: None, - } -} - -fn expand_frame_updates(frame: &TestFrame, decoded_values: Option<&[i32]>) -> Vec { - if let Some(values) = decoded_values { - if values.is_empty() { - return Vec::new(); - } - - return vec![HudPanelUpdate { - source_id: format_source_id(frame.cmd), - values: values.iter().map(|value| *value as f32).collect(), - }]; - } - - let chunks = frame.payload.chunks_exact(4); - - if !frame.payload.is_empty() && chunks.remainder().is_empty() { - return chunks.map(build_update_from_chunk).collect(); - } - - vec![HudPanelUpdate { - source_id: format_source_id(frame.cmd), - values: fallback_values(frame), - }] -} - -fn build_update_from_chunk(chunk: &[u8]) -> HudPanelUpdate { - HudPanelUpdate { - source_id: format_source_id(chunk[0]), - values: chunk[1..] - .iter() - .enumerate() - .map(|(index, byte)| normalize_value(*byte, tone_for_index(index))) - .collect(), - } -} - -fn fallback_values(frame: &TestFrame) -> Vec { - let mut bytes = frame.payload.clone(); - - if bytes.is_empty() { - bytes.extend([ - frame.cmd, - frame.length as u8, - frame.checksum, - frame.cmd.wrapping_add(frame.checksum), - ]); - } - - while bytes.len() < 3 { - let previous = *bytes.last().unwrap_or(&frame.cmd); - bytes.push( - previous - .wrapping_add(frame.cmd) - .wrapping_add(bytes.len() as u8), - ); - } - - bytes - .into_iter() - .enumerate() - .map(|(index, byte)| normalize_value(byte, tone_for_index(index))) - .collect() -} - -fn normalize_value(byte: u8, tone: HudTone) -> f32 { - let base = (byte as f32 / 255.0) * 100.0; - let offset = match tone { - HudTone::Cyan => 6.0, - HudTone::Lime => 0.0, - HudTone::Orange => -6.0, - HudTone::Violet => 10.0, - HudTone::Gold => -10.0, - HudTone::Rose => 3.0, - }; - - (base + offset).clamp(0.0, 100.0) -} - -fn format_source_id(byte: u8) -> String { - if byte.is_ascii_alphanumeric() { - (byte as char).to_ascii_uppercase().to_string() - } else { - format!("CH{:02X}", byte) - } -} - fn side_for_index(index: usize) -> HudPanelSide { if index % 2 == 0 { HudPanelSide::Left @@ -331,91 +179,6 @@ fn side_for_index(index: usize) -> HudPanelSide { } } -fn push_point(points: &mut Vec, value: f32) { - if points.len() >= MAX_POINTS { - points.remove(0); - } - - points.push((value * 10.0).round() / 10.0); -} - -fn build_panel_series( - source_id: &str, - channel_count: usize, - previous: &[HudSignalSeries], -) -> Vec { - (0..channel_count) - .map(|index| HudSignalSeries { - id: format!("{source_id}-series-{}", index + 1), - tone: tone_for_index(index), - points: previous - .get(index) - .map(|series| series.points.clone()) - .unwrap_or_default(), - }) - .collect() -} - -fn build_panel_icons(source_id: &str, channel_count: usize) -> Vec { - (0..channel_count) - .map(|index| HudSignalIcon { - id: format!("{source_id}-icon-{}", index + 1), - label: if channel_count == 1 { - "TOTAL".to_string() - } else { - format!("{source_id}-{}", index + 1) - }, - tone: tone_for_index(index), - }) - .collect() -} - -fn ensure_panel_channels(panel: &mut HudSignalPanel, channel_count: usize) { - if panel.series.len() == channel_count && panel.icons.len() == channel_count { - return; - } - - panel.series = build_panel_series(&panel.code, channel_count, &panel.series); - panel.icons = build_panel_icons(&panel.code, channel_count); -} - -fn refresh_panel_stats(panel: &mut HudSignalPanel) { - let latest_values: Vec = panel - .series - .iter() - .filter_map(|series| series.points.last().copied()) - .collect(); - - panel.latest = if latest_values.is_empty() { - None - } else { - Some(latest_values.iter().sum::() / latest_values.len() as f32) - }; - - panel.min = panel - .series - .iter() - .flat_map(|series| series.points.iter().copied()) - .reduce(f32::min); - - panel.max = panel - .series - .iter() - .flat_map(|series| series.points.iter().copied()) - .reduce(f32::max); -} - -fn tone_for_index(index: usize) -> HudTone { - match index % 6 { - 0 => HudTone::Cyan, - 1 => HudTone::Lime, - 2 => HudTone::Orange, - 3 => HudTone::Violet, - 4 => HudTone::Gold, - _ => HudTone::Rose, - } -} - fn push_summary_point(points: &mut Vec, value: f32) { if points.len() >= MAX_SUMMARY_POINTS { points.remove(0); @@ -439,62 +202,4 @@ fn now_millis() -> u64 { .duration_since(UNIX_EPOCH) .map(|duration| duration.as_millis() as u64) .unwrap_or_default() -} - -// #[cfg(test)] -// mod tests { -// use super::*; -// -// fn sample_frame() -> TestFrame { -// TestFrame { -// header: [0xAA, 0x55], -// cmd: 0x01, -// length: 4, -// payload: vec![0x00, 0x0A, 0x00, 0x14], -// checksum: 0, -// -// } -// } -// -// #[test] -// fn prune_stale_clears_panels_and_summary_after_timeout() { -// let mut state = HudChartState::new(); -// let frame = sample_frame(); -// -// state.record_summary(30.0); -// let _ = state.apply_frame(&frame, Some(&[10, 20])); -// -// let stale_now = Instant::now(); -// let stale_seen = stale_now - PANEL_STALE_AFTER - Duration::from_millis(1); -// -// state.last_frame_seen = Some(stale_seen); -// -// for entry in state.panels.values_mut() { -// entry.last_seen = stale_seen; -// } -// -// let packet = state -// .prune_stale() -// .expect("stale data should emit an update"); -// -// assert!(packet.panels.is_empty()); -// assert!(packet.summary.points.is_empty()); -// assert!(state.panels.is_empty()); -// assert!(state.summary_points.is_empty()); -// } -// -// #[test] -// fn prune_stale_keeps_recent_summary_points() { -// let mut state = HudChartState::new(); -// let frame = sample_frame(); -// -// state.record_summary(30.0); -// let _ = state.apply_frame(&frame, Some(&[10, 20])); -// -// state.last_frame_seen = Some(Instant::now()); -// -// assert!(state.prune_stale().is_none()); -// assert_eq!(state.summary_points, vec![30.0]); -// assert_eq!(state.panels.len(), 1); -// } -// } +} \ No newline at end of file diff --git a/src-tauri/src/serial_core/multi_dim_force.rs b/src-tauri/src/serial_core/multi_dim_force.rs deleted file mode 100644 index 379af89..0000000 --- a/src-tauri/src/serial_core/multi_dim_force.rs +++ /dev/null @@ -1,122 +0,0 @@ -use ndarray::Array2; - -const TOTAL_PRESSURE_LOW_THRESHOLD: usize = 500; -const COP_STABILITY_FRAMES_REQUIRED: usize = 5; -const SENSOR_ROWS: usize = 12; -const SENSOR_COLS: usize = 7; - -pub struct PztProcessor { - first_frame: Option>, - first_contact_cop_x: Option, - first_contact_cop_y: Option, - contact_initialized: bool, - total_pressure_low_counter: usize, -} - -impl PztProcessor { - pub fn new() -> Self { - Self { - first_frame: None, - first_contact_cop_x: None, - first_contact_cop_y: None, - contact_initialized: false, - total_pressure_low_counter: 0, - } - } - - fn subtract_baseline(&mut self, current_frame: &[f32]) -> Vec { - if self.first_frame.is_none() { - self.first_frame = Some(current_frame.to_vec()); - } - - let baseline = self.first_frame.as_ref().unwrap(); - current_frame - .iter() - .zip(baseline.iter()) - .map(|(c, b)| (c - b).max(0.0)) - .collect() - } - - fn reset_cop_state(&mut self) { - self.first_contact_cop_x = None; - self.first_contact_cop_y = None; - self.contact_initialized = false; - self.total_pressure_low_counter = 0; - } - - fn compute_pressure_direction(&mut self, frame: &[f32]) -> (f32, f32) { - let frame2d = Array2::from_shape_vec((SENSOR_ROWS, SENSOR_COLS), frame.to_vec()).unwrap(); - let total_pressure: f32 = frame2d.sum(); - if total_pressure < TOTAL_PRESSURE_LOW_THRESHOLD as f32 { - self.total_pressure_low_counter += 1; - } else { - self.total_pressure_low_counter = 0; - } - - if self.total_pressure_low_counter >= COP_STABILITY_FRAMES_REQUIRED { - self.reset_cop_state(); - return (0.0, 0.0); - } - - if total_pressure == 0.0 { - return (0.0, 0.0); - } - - let mut sum_x = 0.0; - let mut sum_y = 0.0; - - for r in 0..SENSOR_ROWS { - for c in 0..SENSOR_COLS { - let val = frame2d[(r, c)]; - sum_x += val * c as f32; - sum_y += val * r as f32; - } - } - - let cop_x = sum_x / total_pressure; - let cop_y = sum_y / total_pressure; - - if !self.contact_initialized { - self.first_contact_cop_x = Some(cop_x); - self.first_contact_cop_y = Some(cop_y); - self.contact_initialized = true; - return (0.0, 0.0); - } - - let dx = cop_x - self.first_contact_cop_x.unwrap(); - let dy = cop_y - self.first_contact_cop_y.unwrap(); - - (dx, dy) - } - - fn compute_vector_angle(x: f32, y: f32) -> (f32, f32) { - let epsilon = 1e-8; - let mag = (x * x + y * y).sqrt(); - let mut angle = (y).atan2(x + epsilon).to_degrees(); - if angle < 0.0 { - angle += 360.0; - } - (angle, mag) - } - - fn compute_pzt_angle(px: f32, py: f32) -> (f32, f32) { - Self::compute_vector_angle(px, -py) - } - - pub fn get_pzt_angle(&mut self, adc_data: &[f32]) -> Result { - if adc_data.len() != 84 { - return Err("ADC data length must be 84"); - } - - let baseline = self.subtract_baseline(adc_data); - let (dx, dy) = self.compute_pressure_direction(&baseline); - let (angle, _) = Self::compute_pzt_angle(dx, dy); - - Ok(angle) - } - - pub fn reset_baseline(&mut self) { - self.first_frame = None; - self.reset_cop_state(); - } -} diff --git a/src-tauri/src/serial_core/record.rs b/src-tauri/src/serial_core/record.rs index 7a20d35..f36c74f 100644 --- a/src-tauri/src/serial_core/record.rs +++ b/src-tauri/src/serial_core/record.rs @@ -1,3 +1,5 @@ +use eskin_finger_sdk::types::FingerSample; + #[derive(Clone)] pub struct FrameTiming { pub pts_ms: Option, @@ -7,50 +9,82 @@ pub struct FrameTiming { #[derive(Clone)] pub struct RecordedFrame { pub timing: FrameTiming, - pub frame: F + pub frame: F, } #[derive(Clone, Default)] pub struct Recording { - pub frames: Vec> + pub frames: Vec>, } impl Recording { - pub fn new() -> Recording { Self { frames: Vec::new() } } - pub fn push(&mut self, ite: RecordedFrame) { - self.frames.push(ite); + pub fn new() -> Recording { + Self { + frames: Vec::new(), + } + } + pub fn push(&mut self, item: RecordedFrame) { + self.frames.push(item); } } -pub trait CsvExporter { - type Error: std::error::Error + Send + Sync + 'static; - fn csv_header(&self, recording: &Recording) -> Vec; - fn csv_row(&self, item: &RecordedFrame) -> anyhow::Result>; -} +pub type FingerRecording = Recording; -// TODO: CsvImporter -pub trait CsvImporter

{ - fn load(&mut self, reader: R) -> anyhow::Result>; -} - -pub fn write_csv( - recording: &Recording, - exporter: &E, - writer: W, +pub fn export_recording_csv( + recording: &Recording, + mut writer: W, ) -> anyhow::Result<()> where - E: CsvExporter, W: std::io::Write, { - let header = exporter.csv_header(&recording); - let mut wrt = csv::Writer::from_writer(writer); - wrt.write_record(header)?; - for f in &recording.frames { - let row = exporter.csv_row(f)?; - wrt.write_record(&row)?; + // Infer channel count from the first sample's combined_forces (just fz) + // We write: timestamp_us, sequence, module, fx, fy, fz + let mut wrt = csv::Writer::from_writer(&mut writer); + wrt.write_record(["timestamp_us", "sequence", "module", "fx", "fy", "fz"])?; + + for frame in &recording.frames { + let s = &frame.frame; + wrt.write_record(&[ + s.timestamp_us.to_string(), + s.sequence.to_string(), + format!("{:?}", s.combined_forces.module), + s.combined_forces.force.fx.to_string(), + s.combined_forces.force.fy.to_string(), + s.combined_forces.force.fz.to_string(), + ])?; } wrt.flush()?; - Ok(()) } + +pub struct FingerSampleCsvPacket { + pub timestamp_us: u64, + pub sequence: u32, + pub fz: u32, +} + +pub fn import_csv( + reader: R, +) -> anyhow::Result> { + let mut rdr = csv::Reader::from_reader(reader); + let mut packets = Vec::new(); + + for result in rdr.records() { + let record = result?; + if record.len() < 6 { + continue; + } + let timestamp_us = record.get(0).unwrap_or("0").parse::().unwrap_or(0); + let sequence = record.get(1).unwrap_or("0").parse::().unwrap_or(0); + let fz = record.get(5).unwrap_or("0").parse::().unwrap_or(0); + + packets.push(FingerSampleCsvPacket { + timestamp_us, + sequence, + fz, + }); + } + + Ok(packets) +} \ No newline at end of file diff --git a/src-tauri/src/serial_core/serial.rs b/src-tauri/src/serial_core/serial.rs index fca733d..6cbeb60 100644 --- a/src-tauri/src/serial_core/serial.rs +++ b/src-tauri/src/serial_core/serial.rs @@ -1,431 +1,160 @@ -use crate::serial_core::codec::Codec; -use crate::serial_core::codecs::tactile_a::TactileACodec; -use crate::serial_core::frame::{FrameHandler, TactileAFrame, TestFrame}; -use crate::serial_core::model::{HudChartState, HudPacket}; -#[cfg(feature = "multi-dim")] -use crate::serial_core::multi_dim_force::PztProcessor; +use crate::serial_core::model::HudChartState; use crate::serial_core::record::Recording; -use crate::serial_core::record::{FrameTiming, RecordedFrame}; -#[cfg(feature = "devkit")] -use crate::devkit::{proto::SensorFrame, DevKitState}; -use anyhow::Result; -use log::debug; -use std::future::pending; -#[cfg(feature = "devkit")] -use std::sync::atomic::Ordering; -use std::sync::{Arc, Mutex}; -use std::time::Instant; +use eskin_finger_sdk::channel::DeviceEvent; +use eskin_finger_sdk::config::DeviceConfig; +use eskin_finger_sdk::device::{EskinDevice, EskinDeviceInner}; +use eskin_finger_sdk::transport::SerialPortTransport; +use eskin_finger_sdk::types::FingerSample; use tauri::{AppHandle, Emitter}; -#[cfg(feature = "devkit")] -use tauri::Manager; -use tokio::io::{AsyncReadExt, AsyncWriteExt}; -use tokio::time::{self, Duration, MissedTickBehavior}; -use tokio_serial::SerialStream; use tokio_util::sync::CancellationToken; -const AUTO_SUB_INTERVAL: Duration = Duration::from_nanos(16_666_667); +use super::model::HudPacket; -pub enum PollMode { - Disable, - Enabled(Box>), +pub struct SdkSession { + pub device: EskinDeviceInner, } -struct PendingSubFrame { - frame: F, - values: Vec, -} - -pub trait SerialFrame: Clone + Send + 'static { - fn dts_ms(&self) -> u64; - - fn to_hud_packet( - &self, - chart_state: &mut HudChartState, - display_values: Option<&[i32]>, - ) -> Option; -} - -impl SerialFrame for TestFrame { - fn dts_ms(&self) -> u64 { - self.dts_ms +pub fn open_device(port: &str) -> Result { + let port = port.trim(); + if port.is_empty() { + return Err("Serial port is required".to_string()); } - fn to_hud_packet( - &self, - chart_state: &mut HudChartState, - display_values: Option<&[i32]>, - ) -> Option { - Some(chart_state.apply_frame(self, display_values)) - } + let transport = SerialPortTransport::new(port, 921600); + let config = DeviceConfig::default(); + let mut device = EskinDeviceInner::new(config, Box::new(transport)); + device.open().map_err(|e| e.to_string())?; + + Ok(SdkSession { device }) } -impl SerialFrame for TactileAFrame { - fn dts_ms(&self) -> u64 { - match self { - TactileAFrame::Req(_) => 0, - TactileAFrame::Rep(rep) => rep.dts_ms, +pub async fn run_stream( + app: AppHandle, + device: &mut EskinDeviceInner, + cancel: CancellationToken, +) -> Result<(), String> { + device + .start_stream() + .map_err(|e| format!("start_stream failed: {e}"))?; + + let channels = device.channels(); + let mut chart_state = HudChartState::new(); + + let result = loop { + tokio::select! { + _ = cancel.cancelled() => { + break Ok(()); + } + _ = tokio::time::sleep(tokio::time::Duration::from_millis(1)) => {} } - } - fn to_hud_packet( - &self, - chart_state: &mut HudChartState, - display_values: Option<&[i32]>, - ) -> Option { - match self { - TactileAFrame::Req(_) => None, - TactileAFrame::Rep(rep) => { - let proxy = TestFrame { - header: rep.meta.header, - cmd: rep.meta.func_code, - length: rep.meta.except_data_len, - payload: rep.payload.clone(), - checksum: rep.meta.checksum, - dts_ms: rep.dts_ms, - }; - - Some(chart_state.apply_frame(&proxy, display_values)) + // Try to receive a sample (non-blocking-ish via small timeout) + match channels.recv_sample(5) { + Ok(sample) => { + if let Some(packet) = build_hud_packet_from_sample(&sample, &mut chart_state) { + let _ = app.emit("hud_stream", packet); + } + } + Err(eskin_finger_sdk::error::SdkError::Timeout) => { + // No sample yet, check for events + } + Err(e) => { + break Err(format!("sample recv error: {e}")); } } - } -} -pub trait PollRequester: Send { - fn poll_interval(&self) -> Option { - None - } - - fn should_request(&mut self) -> bool { - true - } - - fn next_request(&mut self) -> Result> { - Ok(None) - } - - fn on_rx_frame(&mut self, _frame: &F) {} -} - -#[derive(Default)] -pub struct NoopPollRequester; - -impl PollRequester for NoopPollRequester {} - -pub struct TactileAPollRequester { - period: Duration, - cols: usize, - rows: usize, - awaiting_reply: bool, - last_request_at: Option, - reply_timeout: Duration, -} - -impl TactileAPollRequester { - pub fn new(period: Duration, cols: usize, rows: usize, reply_timeout: Duration) -> Self { - Self { - period, - cols, - rows, - awaiting_reply: false, - last_request_at: None, - reply_timeout, + // Drain any events + if let Err(e) = drain_events(&channels) { + break Err(e); } - } -} - -impl PollRequester for TactileAPollRequester { - fn poll_interval(&self) -> Option { - Some(self.period) - } - - fn should_request(&mut self) -> bool { - if !self.awaiting_reply { - return true; - } - let timed_out = self - .last_request_at - .map(|t| t.elapsed() >= self.reply_timeout) - .unwrap_or(false); - - if timed_out { - self.awaiting_reply = false; - self.last_request_at = None; - return true; - } - - false - } - - fn next_request(&mut self) -> Result> { - let req = TactileACodec::build_req_frame(self.cols, self.rows)?; - self.awaiting_reply = true; - self.last_request_at = Some(Instant::now()); - Ok(Some(req)) - } - - fn on_rx_frame(&mut self, frame: &TactileAFrame) { - if matches!(frame, TactileAFrame::Rep(_)) { - self.awaiting_reply = false; - self.last_request_at = None - } - } -} - -pub async fn run_serial( - app: AppHandle, - port: SerialStream, - codec: C, - handler: H, - session_started_at: Instant, - recording: Arc>>, - cancel: CancellationToken, -) -> Result<()> -where - F: SerialFrame, - C: Codec + Send + 'static, - H: FrameHandler + Send + 'static, - T: Into, -{ - run_serial_with_poll( - app, - port, - codec, - handler, - session_started_at, - recording, - cancel, - PollMode::Disable, - ) - .await -} - -pub async fn run_serial_with_poll( - app: AppHandle, - mut port: SerialStream, - mut codec: C, - mut handler: H, - session_started_at: Instant, - recording: Arc>>, - cancel: CancellationToken, - poll_mode: PollMode, -) -> Result<()> -where - F: SerialFrame, - C: Codec + Send + 'static, - H: FrameHandler + Send + 'static, - T: Into, -{ - let mut requester = match poll_mode { - PollMode::Disable => None, - PollMode::Enabled(r) => Some(r), }; - let mut poll_interval = requester.as_ref().and_then(|r| r.poll_interval()).map(|d| { - let mut it = time::interval(d); - it.set_missed_tick_behavior(MissedTickBehavior::Skip); - it - }); - let mut poll_sub_interval = time::interval(AUTO_SUB_INTERVAL); - poll_sub_interval.set_missed_tick_behavior(MissedTickBehavior::Skip); + let _ = device.stop_stream(); + result +} +pub async fn run_stream_with_record( + app: AppHandle, + device: &mut EskinDeviceInner, + cancel: CancellationToken, + recording: std::sync::Arc>>, +) -> Result<(), String> { + device + .start_stream() + .map_err(|e| format!("start_stream failed: {e}"))?; + + let channels = device.channels(); let mut chart_state = HudChartState::new(); - let mut buffer = [0u8; 1024]; - let mut prune_interval = time::interval(Duration::from_millis(450)); - #[cfg(feature = "multi-dim")] - let mut pzt_processor = PztProcessor::new(); - let mut pending_sub_frame: Option> = None; - prune_interval.set_missed_tick_behavior(MissedTickBehavior::Delay); - loop { + let result = loop { tokio::select! { - _ = cancel.cancelled() => break, - _ = async { - match poll_interval.as_mut() { - Some(it) => { - it.tick().await; - } - None => pending::<()>().await, - } - } => { - if let Some(r) = requester.as_mut() { - if r.should_request() { - if let Some(req) = r.next_request()? { - let bytes = codec.encode(&req)?; - port.write_all(&bytes).await?; - } - } - } + _ = cancel.cancelled() => { + break Ok(()); } - _ = prune_interval.tick() => { - if let Some(packet) = chart_state.prune_stale() { - app.emit("hud_stream", packet)?; - } - } - _ = poll_sub_interval.tick() => { - if let Some(pending) = pending_sub_frame.take() { - let display_values = build_display_values( - &mut chart_state, - pending.values.as_slice(), - ); - - if let Some(packet) = pending - .frame - .to_hud_packet(&mut chart_state, display_values.as_deref()) - { - app.emit("hud_stream", packet)?; - } - } - } - read_result = port.read(&mut buffer) => { - let n = read_result?; - if n == 0 { - // Some serial drivers can resolve reads with 0 bytes repeatedly. - // Yield here so timer-driven poll requests are not starved by a busy loop. - tokio::task::yield_now().await; - continue; - } - - let frames = codec.decode(&buffer[..n], session_started_at)?; - for frame in frames { - if let Some(r) = requester.as_mut() { - r.on_rx_frame(&frame); - } - - let decode_res = handler - .on_frame(&frame) - .await? - .map(|vals| vals.into_iter().map(Into::into).collect::>()); + _ = tokio::time::sleep(tokio::time::Duration::from_millis(1)) => {} + } + match channels.recv_sample(5) { + Ok(sample) => { + // Record + { let mut record = recording .lock() - .map_err(|_| anyhow::anyhow!("recording state poisoned"))?; - record.push(RecordedFrame { - timing: FrameTiming { + .map_err(|_| "recording state poisoned".to_string())?; + record.push(crate::serial_core::record::RecordedFrame { + timing: crate::serial_core::record::FrameTiming { pts_ms: None, - dts_ms: frame.dts_ms(), + dts_ms: sample.timestamp_us / 1000, }, - frame: frame.clone(), + frame: sample.clone(), }); - drop(record); + } - if let Some(vals) = decode_res { - #[cfg(feature = "multi-dim")] - { - let pzt_values = vals.iter().map(|value| *value as f32).collect::>(); - if let Ok(angle) = pzt_processor.get_pzt_angle(&pzt_values) { - // debug!("pzt angle: {:.2}", angle); - } - } - #[cfg(feature = "devkit")] - { - let summary = vals.iter().copied().sum::(); - let force = raw_to_g1(summary as u32); - push_devkit_frame(&app, vals.as_slice(), frame.dts_ms(), force); - } - - pending_sub_frame = Some(PendingSubFrame { - frame: frame.clone(), - values: vals, - }); - } else if let Some(packet) = frame.to_hud_packet(&mut chart_state, None) { - app.emit("hud_stream", packet)?; - } + if let Some(packet) = build_hud_packet_from_sample(&sample, &mut chart_state) { + let _ = app.emit("hud_stream", packet); } } + Err(eskin_finger_sdk::error::SdkError::Timeout) => {} + Err(e) => { + break Err(format!("sample recv error: {e}")); + } + } + + if let Err(e) = drain_events(&channels) { + break Err(e); + } + }; + + let _ = device.stop_stream(); + result +} + +fn drain_events(channels: &std::sync::Arc) -> Result<(), String> { + loop { + match channels.recv_event(0) { + Ok(DeviceEvent::IoError(msg)) => { + eprintln!("SDK stream io error: {msg}"); + return Err(format!("stream io error: {msg}")); + } + Ok(_) => {} + Err(eskin_finger_sdk::error::SdkError::Timeout) => return Ok(()), + Err(eskin_finger_sdk::error::SdkError::ChannelClosed) => { + return Err("event channel closed".into()); + } + Err(_) => return Ok(()), } } - Ok(()) } -fn build_display_values(chart_state: &mut HudChartState, values: &[i32]) -> Option> { - let summary = values.iter().copied().sum::(); - let force = raw_to_g1(summary as u32); - chart_state.record_summary(force as f32); - chart_state.record_pressure_matrix(values); - Some(vec![summary]) -} - -#[cfg(feature = "devkit")] -fn push_devkit_frame(app: &AppHandle, values: &[i32], dts_ms: u64, resultant_force: f64) { - let devkit_state = app.state::(); - if !devkit_state.running.load(Ordering::Relaxed) { - return; - } - - let (rows, cols) = infer_matrix_shape(values.len()); - let timestamp_ms = std::time::SystemTime::now() - .duration_since(std::time::UNIX_EPOCH) - .unwrap_or_default() - .as_millis() as u64; - - let seq = timestamp_ms; - let matrix = values - .iter() - .map(|value| (*value).max(0) as u32) - .collect::>(); - - devkit_state.push_frame(SensorFrame { - seq, - timestamp_ms, - rows, - cols, - matrix, - resultant_force, - dts_ms: dts_ms as u32, - }); -} - -#[cfg(feature = "devkit")] -fn infer_matrix_shape(len: usize) -> (u32, u32) { - if len == 84 { - return (12, 7); - } - - if len == 0 { - return (0, 0); - } - - let mut best = (len, 1); - let mut factor = 1usize; - while factor * factor <= len { - if len % factor == 0 { - best = (len / factor, factor); - } - factor += 1; - } - - (best.0 as u32, best.1 as u32) -} - -fn raw_to_g1(raw: u32) -> f64 { - const X: [u32; 12] = [ - 0, 84402, 117218, 140176, 159126, 175812, 191484, 208758, 224703, 252448, 302361, 352703, - ]; - - const Y: [f64; 12] = [ - 0.0, 160.0, 260.0, 360.0, 460.0, 560.0, 660.0, 760.0, 860.0, 1060.0, 1560.0, 2060.0, - ]; - - let n = X.len(); - if raw <= X[0] { - return Y[0] / 100.0; - } - if raw >= X[n - 1] { - return Y[n - 1] / 100.0; - } - - let mut left = 0; - let mut right = n - 1; - - while left + 1 < right { - let mid = (left + right) / 2; - if raw < X[mid] { - right = mid; - } else { - left = mid; - } - } - - let ratio = (raw - X[left]) as f64 / (X[right] - X[left]) as f64; - Y[left] / 100.0 + ratio * (Y[right] - Y[left]) / 100.0 +fn build_hud_packet_from_sample( + sample: &FingerSample, + chart_state: &mut HudChartState, +) -> Option { + let fz = sample.combined_forces.force.fz as f32; + chart_state.record_summary(fz); + if !sample.raw_adcs.is_empty() { + let pressure: Vec = sample.raw_adcs.iter().map(|&v| v as f32).collect(); + chart_state.record_pressure_matrix(&pressure); + } + Some(chart_state.build_snapshot()) } diff --git a/src-tauri/src/serial_core/utils.rs b/src-tauri/src/serial_core/utils.rs deleted file mode 100644 index f5b2542..0000000 --- a/src-tauri/src/serial_core/utils.rs +++ /dev/null @@ -1,59 +0,0 @@ - -use std::time::Instant; - -pub fn usize_to_u16_be_bytes(n: usize) -> [u8; 2] { - (n as u16).to_be_bytes() -} - -pub fn usize_to_u16_le_bytes(n: usize) -> [u8; 2] { - (n as u16).to_be_bytes() -} - -pub fn u16_to_hex_be_bytes(n: u16) -> [u8; 2] { - (n as u16).to_be_bytes() -} - -pub fn u16_to_hex_le_bytes(n: u16) -> [u8; 2] { - (n as u16).to_le_bytes() -} - -pub fn calc_crc8_smbus(c: &[u8]) -> u8 { - let crc8_smbus = crc::Crc::::new(&crc::CRC_8_SMBUS); - let checksum = crc8_smbus.checksum(c); - return checksum; -} - -pub fn calc_crc8_itu(c: &[u8]) -> u8 { - let crc8_itu_alg = crc::Crc::::new(&crc::CRC_8_I_432_1); - let checksum = crc8_itu_alg.checksum(c); - return checksum; -} - -pub fn elapsed_millis(start_at: Instant) -> u64 { - start_at.elapsed().as_millis() as u64 -} - -#[cfg(test)] -mod test { - use anyhow::Ok; - - use crate::serial_core::utils::{calc_crc8_itu, calc_crc8_smbus}; - - #[test] - fn test_crc8_itu() -> anyhow::Result<()> { - let req_vec = vec![0x55, 0xAA, 0x09, 0x00, 0x34, 0x00, 0xFB, 0x00, 0x1C, 0x00, 0x00, 0x18, 0x00]; - let checksum = calc_crc8_itu(req_vec.as_slice()); - assert_eq!(checksum, 0x7A); - - Ok(()) - } - - #[test] - fn test_crc8_smbus() -> anyhow::Result<()> { - let req_vec = vec![0x55, 0xAA, 0x09, 0x00, 0x34, 0x00, 0xFB, 0x00, 0x1C, 0x00, 0x00, 0x18, 0x00]; - let checksum = calc_crc8_smbus(req_vec.as_slice()); - assert_eq!(checksum, 0x2F); - - Ok(()) - } -} \ No newline at end of file