Compare commits
1 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
50b3741b1a |
1
eskin-finger-sdk
Submodule
1
eskin-finger-sdk
Submodule
Submodule eskin-finger-sdk added at 705375085f
2
package-lock.json
generated
2
package-lock.json
generated
@@ -6,7 +6,7 @@
|
|||||||
"packages": {
|
"packages": {
|
||||||
"": {
|
"": {
|
||||||
"name": "JE-Skin",
|
"name": "JE-Skin",
|
||||||
"version": "0.3.0",
|
"version": "0.4.0",
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@tauri-apps/api": "^2",
|
"@tauri-apps/api": "^2",
|
||||||
|
|||||||
98
src-tauri/Cargo.lock
generated
98
src-tauri/Cargo.lock
generated
@@ -8,17 +8,14 @@ version = "0.4.0"
|
|||||||
dependencies = [
|
dependencies = [
|
||||||
"anyhow",
|
"anyhow",
|
||||||
"async-stream",
|
"async-stream",
|
||||||
"async-trait",
|
|
||||||
"axum 0.8.9",
|
"axum 0.8.9",
|
||||||
"chrono",
|
|
||||||
"crc",
|
|
||||||
"csv",
|
"csv",
|
||||||
"dirs",
|
"dirs",
|
||||||
|
"eskin-finger-sdk",
|
||||||
"fern",
|
"fern",
|
||||||
"futures-util",
|
"futures-util",
|
||||||
"humantime",
|
"humantime",
|
||||||
"log",
|
"log",
|
||||||
"ndarray",
|
|
||||||
"prost",
|
"prost",
|
||||||
"prost-types",
|
"prost-types",
|
||||||
"protoc-bin-vendored",
|
"protoc-bin-vendored",
|
||||||
@@ -1152,6 +1149,23 @@ dependencies = [
|
|||||||
"windows-sys 0.61.2",
|
"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]]
|
[[package]]
|
||||||
name = "event-listener"
|
name = "event-listener"
|
||||||
version = "5.4.1"
|
version = "5.4.1"
|
||||||
@@ -2314,9 +2328,9 @@ dependencies = [
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "libc"
|
name = "libc"
|
||||||
version = "0.2.183"
|
version = "0.2.186"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "b5b646652bf6661599e1da8901b3b9522896f01e736bad5f723fe7a3a27f899d"
|
checksum = "68ab91017fe16c622486840e4c83c9a37afeff978bd239b5293d61ece587de66"
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "libloading"
|
name = "libloading"
|
||||||
@@ -2340,6 +2354,26 @@ dependencies = [
|
|||||||
"redox_syscall 0.7.4",
|
"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]]
|
[[package]]
|
||||||
name = "linux-raw-sys"
|
name = "linux-raw-sys"
|
||||||
version = "0.12.1"
|
version = "0.12.1"
|
||||||
@@ -2442,16 +2476,6 @@ version = "0.8.4"
|
|||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "47e1ffaa40ddd1f3ed91f717a33c8c0ee23fff369e3aa8772b9605cc1d22f4c3"
|
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]]
|
[[package]]
|
||||||
name = "memchr"
|
name = "memchr"
|
||||||
version = "2.8.0"
|
version = "2.8.0"
|
||||||
@@ -2541,19 +2565,6 @@ version = "0.10.1"
|
|||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "1d87ecb2933e8aeadb3e3a02b828fed80a7528047e68b4f424523a0981a3a084"
|
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]]
|
[[package]]
|
||||||
name = "ndk"
|
name = "ndk"
|
||||||
version = "0.9.0"
|
version = "0.9.0"
|
||||||
@@ -2619,30 +2630,12 @@ version = "0.1.14"
|
|||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "72ef4a56884ca558e5ddb05a1d1e7e1bfd9a68d9ed024c21704cc98872dae1bb"
|
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]]
|
[[package]]
|
||||||
name = "num-conv"
|
name = "num-conv"
|
||||||
version = "0.2.0"
|
version = "0.2.0"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "cf97ec579c3c42f953ef76dbf8d55ac91fb219dde70e49aa4a6b7d74e9919050"
|
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]]
|
[[package]]
|
||||||
name = "num-traits"
|
name = "num-traits"
|
||||||
version = "0.2.19"
|
version = "0.2.19"
|
||||||
@@ -3647,12 +3640,6 @@ version = "0.6.2"
|
|||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "20675572f6f24e9e76ef639bc5552774ed45f1c30e2951e1e99c59888861c539"
|
checksum = "20675572f6f24e9e76ef639bc5552774ed45f1c30e2951e1e99c59888861c539"
|
||||||
|
|
||||||
[[package]]
|
|
||||||
name = "rawpointer"
|
|
||||||
version = "0.2.1"
|
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
||||||
checksum = "60a357793950651c4ed0f3f52338f53b2f809f32d83a07f72909fa13e4c6c1e3"
|
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "redox_syscall"
|
name = "redox_syscall"
|
||||||
version = "0.5.18"
|
version = "0.5.18"
|
||||||
@@ -4263,6 +4250,7 @@ dependencies = [
|
|||||||
"core-foundation",
|
"core-foundation",
|
||||||
"core-foundation-sys",
|
"core-foundation-sys",
|
||||||
"io-kit-sys",
|
"io-kit-sys",
|
||||||
|
"libudev",
|
||||||
"mach2",
|
"mach2",
|
||||||
"nix 0.26.4",
|
"nix 0.26.4",
|
||||||
"scopeguard",
|
"scopeguard",
|
||||||
@@ -5565,9 +5553,9 @@ checksum = "b6c140620e7ffbb22c2dee59cafe6084a59b5ffc27a8859a5f0d494b5d52b6be"
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "uuid"
|
name = "uuid"
|
||||||
version = "1.22.0"
|
version = "1.23.1"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "a68d3c8f01c0cfa54a75291d83601161799e4a89a39e0929f4b0354d88757a37"
|
checksum = "ddd74a9687298c6858e9b88ec8935ec45d22e8fd5e6394fa1bd4e99a87789c76"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"getrandom 0.4.2",
|
"getrandom 0.4.2",
|
||||||
"js-sys",
|
"js-sys",
|
||||||
|
|||||||
@@ -17,7 +17,6 @@ crate-type = ["staticlib", "cdylib", "rlib"]
|
|||||||
[features]
|
[features]
|
||||||
default = []
|
default = []
|
||||||
devkit = ["dep:tonic", "dep:prost", "dep:prost-types", "dep:async-stream", "dep:dirs"]
|
devkit = ["dep:tonic", "dep:prost", "dep:prost-types", "dep:async-stream", "dep:dirs"]
|
||||||
multi-dim = ["dep:ndarray"]
|
|
||||||
|
|
||||||
[build-dependencies]
|
[build-dependencies]
|
||||||
tauri-build = { version = "2", features = [] }
|
tauri-build = { version = "2", features = [] }
|
||||||
@@ -37,22 +36,19 @@ async-stream = { version = "0.3", optional = true }
|
|||||||
dirs = { version = "6", optional = true }
|
dirs = { version = "6", optional = true }
|
||||||
tokio-serial = { version = "5.4.5" }
|
tokio-serial = { version = "5.4.5" }
|
||||||
tokio = { version = "1.50.0", features = ["full"] }
|
tokio = { version = "1.50.0", features = ["full"] }
|
||||||
async-trait = "0.1.89"
|
|
||||||
tokio-util = "0.7.18"
|
tokio-util = "0.7.18"
|
||||||
serde_json = "1"
|
serde_json = "1"
|
||||||
fern = { version = "0.7.1", features = ["colored", "date-based"] }
|
fern = { version = "0.7.1", features = ["colored", "date-based"] }
|
||||||
log = "0.4.29"
|
log = "0.4.29"
|
||||||
humantime = "2.3.0"
|
humantime = "2.3.0"
|
||||||
csv = "1.4.0"
|
csv = "1.4.0"
|
||||||
chrono = "0.4.44"
|
|
||||||
crc = "3.4.0"
|
|
||||||
axum = { version = "0.8", features = ["ws"] }
|
axum = { version = "0.8", features = ["ws"] }
|
||||||
tower-http = { version = "0.6", features = ["cors"] }
|
tower-http = { version = "0.6", features = ["cors"] }
|
||||||
futures-util = "0.3"
|
futures-util = "0.3"
|
||||||
uuid = { version = "1", features = ["v4", "serde"] }
|
uuid = { version = "1", features = ["v4", "serde"] }
|
||||||
rand = "0.8"
|
rand = "0.8"
|
||||||
reqwest = { version = "0.12", default-features = false, features = ["json", "rustls-tls"] }
|
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]
|
[target.'cfg(not(any(target_os = "android", target_os = "ios")))'.dependencies]
|
||||||
tauri-plugin-updater = "2"
|
tauri-plugin-updater = "2"
|
||||||
|
|||||||
@@ -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::error::SerialError;
|
||||||
use crate::serial_core::record::CsvImporter;
|
use crate::serial_core::record::{self, FingerRecording};
|
||||||
use crate::serial_core::serial::{PollMode, TactileAPollRequester};
|
use crate::serial_core::serial;
|
||||||
use crate::serial_core::{serial, TactileARecording};
|
use eskin_finger_sdk::device::EskinDevice;
|
||||||
use log::info;
|
use log::info;
|
||||||
use serde::Serialize;
|
use serde::Serialize;
|
||||||
use std::fs::File;
|
|
||||||
use std::io::Cursor;
|
use std::io::Cursor;
|
||||||
use std::path::{Path, PathBuf};
|
use std::path::{Path, PathBuf};
|
||||||
use std::sync::{Arc, Mutex};
|
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 tauri::{async_runtime::JoinHandle, AppHandle, Manager, State};
|
||||||
use tokio_serial::{available_ports, SerialPortBuilderExt};
|
use tokio_serial::available_ports;
|
||||||
use tokio_util::sync::CancellationToken;
|
use tokio_util::sync::CancellationToken;
|
||||||
|
|
||||||
const DEFAULT_TACTILE_COLS: usize = 7;
|
type SharedRecording = Arc<Mutex<FingerRecording>>;
|
||||||
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<Mutex<TactileARecording>>;
|
|
||||||
|
|
||||||
#[derive(Serialize)]
|
#[derive(Serialize)]
|
||||||
#[serde(rename_all = "camelCase")]
|
#[serde(rename_all = "camelCase")]
|
||||||
@@ -67,18 +58,18 @@ struct SerialSession {
|
|||||||
port: String,
|
port: String,
|
||||||
cancel: CancellationToken,
|
cancel: CancellationToken,
|
||||||
task: JoinHandle<()>,
|
task: JoinHandle<()>,
|
||||||
current_record: SharedTactileRecording,
|
current_record: SharedRecording,
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Default)]
|
#[derive(Default)]
|
||||||
pub struct SerialConnectionState {
|
pub struct SerialConnectionState {
|
||||||
session: Mutex<Option<SerialSession>>,
|
session: Mutex<Option<SerialSession>>,
|
||||||
last_record: Mutex<Option<SharedTactileRecording>>,
|
last_record: Mutex<Option<SharedRecording>>,
|
||||||
}
|
}
|
||||||
|
|
||||||
pub async fn shutdown_active_session(
|
pub async fn shutdown_active_session(
|
||||||
state: &SerialConnectionState,
|
state: &SerialConnectionState,
|
||||||
) -> Result<Option<(String, SharedTactileRecording)>, SerialError> {
|
) -> Result<Option<(String, SharedRecording)>, SerialError> {
|
||||||
let session = {
|
let session = {
|
||||||
let mut guard = state.session.lock().map_err(|_| SerialError::StateError)?;
|
let mut guard = state.session.lock().map_err(|_| SerialError::StateError)?;
|
||||||
guard.take()
|
guard.take()
|
||||||
@@ -148,62 +139,41 @@ pub async fn serial_connect(
|
|||||||
}
|
}
|
||||||
|
|
||||||
let cancel = CancellationToken::new();
|
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_record = current_record.clone();
|
||||||
let task_cancel = cancel.clone();
|
let task_cancel = cancel.clone();
|
||||||
let task_app = app.clone();
|
let task_app = app.clone();
|
||||||
let task_port_name = port_name.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 task = tauri::async_runtime::spawn(async move {
|
||||||
let codec = TactileACodec::new(DEFAULT_TACTILE_COLS, DEFAULT_TACTILE_ROWS);
|
// Open device using SDK
|
||||||
let handler = TactileAHandler;
|
let session = match serial::open_device(&task_port_name) {
|
||||||
let poll_mode = PollMode::Enabled(Box::new(TactileAPollRequester::new(
|
Ok(s) => s,
|
||||||
Duration::from_millis(DEFAULT_TACTILE_POLL_INTERVAL_MS),
|
Err(e) => {
|
||||||
DEFAULT_TACTILE_COLS,
|
eprintln!("Failed to open device: {e}");
|
||||||
DEFAULT_TACTILE_ROWS,
|
cleanup_session(&task_app, &task_port_name, task_record).await;
|
||||||
Duration::from_millis(DEFAULT_TACTILE_REPLY_TIMEOUT_MS),
|
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(),
|
task_app.clone(),
|
||||||
port,
|
&mut device,
|
||||||
codec,
|
|
||||||
handler,
|
|
||||||
session_started_at,
|
|
||||||
task_record.clone(),
|
|
||||||
task_cancel,
|
task_cancel,
|
||||||
poll_mode,
|
task_record.clone(),
|
||||||
)
|
)
|
||||||
.await
|
.await
|
||||||
{
|
{
|
||||||
eprintln!("serial task exited with error: {error}");
|
eprintln!("serial task exited with error: {error}");
|
||||||
}
|
}
|
||||||
|
|
||||||
let manager = task_app.state::<SerialConnectionState>();
|
// Close device
|
||||||
if let Ok(mut last_record) = manager.last_record.lock() {
|
let _ = device.close();
|
||||||
*last_record = Some(task_record);
|
|
||||||
}
|
|
||||||
|
|
||||||
let mut session = match manager.session.lock() {
|
cleanup_session(&task_app, &task_port_name, task_record).await;
|
||||||
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();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
});
|
});
|
||||||
|
|
||||||
let mut session = state.session.lock().map_err(|_| SerialError::StateError)?;
|
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::<SerialConnectionState>();
|
||||||
|
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]
|
#[tauri::command]
|
||||||
pub async fn serial_disconnect(
|
pub async fn serial_disconnect(
|
||||||
state: State<'_, SerialConnectionState>,
|
state: State<'_, SerialConnectionState>,
|
||||||
@@ -293,8 +288,8 @@ pub fn serial_export_csv_to_path(
|
|||||||
state: State<'_, SerialConnectionState>,
|
state: State<'_, SerialConnectionState>,
|
||||||
) -> Result<SerialExportResponse, SerialError> {
|
) -> Result<SerialExportResponse, SerialError> {
|
||||||
let output_path = resolve_export_path(file_path)?;
|
let output_path = resolve_export_path(file_path)?;
|
||||||
let record = resolve_record_for_export(&state)?;
|
let rec = resolve_record_for_export(&state)?;
|
||||||
let frame_count = write_record_to_csv(record, &output_path)?;
|
let frame_count = write_record_to_csv(rec, &output_path)?;
|
||||||
let path = output_path.display().to_string();
|
let path = output_path.display().to_string();
|
||||||
|
|
||||||
info!("csv exported to {path}, frame_count={frame_count}");
|
info!("csv exported to {path}, frame_count={frame_count}");
|
||||||
@@ -311,22 +306,20 @@ pub fn serial_import_csv(
|
|||||||
file_name: String,
|
file_name: String,
|
||||||
csv_content: String,
|
csv_content: String,
|
||||||
) -> Result<SerialImportResponse, SerialError> {
|
) -> Result<SerialImportResponse, SerialError> {
|
||||||
let mut importer = TactileACsvImporter::new(file_name.as_str());
|
let packets = record::import_csv(Cursor::new(csv_content.into_bytes()))
|
||||||
let packets = importer
|
|
||||||
.load(Cursor::new(csv_content.into_bytes()))
|
|
||||||
.map_err(|_| SerialError::ImportError)?;
|
.map_err(|_| SerialError::ImportError)?;
|
||||||
|
|
||||||
if packets.is_empty() {
|
if packets.is_empty() {
|
||||||
return Err(SerialError::NoRecordedData);
|
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 frame_count = packets.len();
|
||||||
let frames = packets
|
let frames = packets
|
||||||
.into_iter()
|
.into_iter()
|
||||||
.map(|packet| SerialImportFrame {
|
.map(|packet| SerialImportFrame {
|
||||||
data: packet.data,
|
data: vec![packet.fz as i32],
|
||||||
dts_ms: packet.dts_ms,
|
dts_ms: packet.timestamp_us / 1000,
|
||||||
})
|
})
|
||||||
.collect();
|
.collect();
|
||||||
|
|
||||||
@@ -355,7 +348,7 @@ pub fn serial_import_csv_from_path(file_path: String) -> Result<SerialImportResp
|
|||||||
|
|
||||||
fn resolve_record_for_export(
|
fn resolve_record_for_export(
|
||||||
state: &State<'_, SerialConnectionState>,
|
state: &State<'_, SerialConnectionState>,
|
||||||
) -> Result<SharedTactileRecording, SerialError> {
|
) -> Result<SharedRecording, SerialError> {
|
||||||
let current_record = {
|
let current_record = {
|
||||||
let session = state.session.lock().map_err(|_| SerialError::StateError)?;
|
let session = state.session.lock().map_err(|_| SerialError::StateError)?;
|
||||||
session
|
session
|
||||||
@@ -406,7 +399,7 @@ fn snapshot_record_frame_count(
|
|||||||
}
|
}
|
||||||
|
|
||||||
fn write_record_to_csv(
|
fn write_record_to_csv(
|
||||||
record: SharedTactileRecording,
|
record: SharedRecording,
|
||||||
output_path: &Path,
|
output_path: &Path,
|
||||||
) -> Result<usize, SerialError> {
|
) -> Result<usize, SerialError> {
|
||||||
if let Some(parent) = output_path.parent() {
|
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 frame_count = {
|
||||||
let recording = record.lock().map_err(|_| SerialError::StateError)?;
|
let recording = record.lock().map_err(|_| SerialError::StateError)?;
|
||||||
if recording.frames.is_empty() {
|
if recording.frames.is_empty() {
|
||||||
return Err(SerialError::NoRecordedData);
|
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()
|
recording.frames.len()
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
@@ -1,6 +0,0 @@
|
|||||||
use crate::serial_core::error::CodecError;
|
|
||||||
use std::time::Instant;
|
|
||||||
pub trait Codec<F> {
|
|
||||||
fn decode(&mut self, input: &[u8], session_started_at: Instant) -> Result<Vec<F>, CodecError>;
|
|
||||||
fn encode(&self, frame: &F) -> Result<Vec<u8>, CodecError>;
|
|
||||||
}
|
|
||||||
@@ -1,5 +0,0 @@
|
|||||||
use crate::serial_core::{frame::TestFrame, record::Recording};
|
|
||||||
|
|
||||||
pub mod test;
|
|
||||||
pub mod tactile_a;
|
|
||||||
pub type TestRecording = Recording<TestFrame>;
|
|
||||||
@@ -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<u8>,
|
|
||||||
expected_data_len: usize,
|
|
||||||
}
|
|
||||||
|
|
||||||
pub struct TactileACsvExporter {
|
|
||||||
channels: usize,
|
|
||||||
}
|
|
||||||
|
|
||||||
pub struct TactileACsvImporter {
|
|
||||||
channels: usize,
|
|
||||||
data_row: usize,
|
|
||||||
packets: Vec<TactileADataPacket>,
|
|
||||||
}
|
|
||||||
|
|
||||||
pub struct TactileAHandler;
|
|
||||||
|
|
||||||
#[derive(Clone)]
|
|
||||||
pub struct TactileADataPacket {
|
|
||||||
pub data: Vec<i32>,
|
|
||||||
pub dts_ms: u64,
|
|
||||||
}
|
|
||||||
|
|
||||||
impl From<u8> 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<TactileADataPacket, Self::Error> {
|
|
||||||
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<Vec<i32>, CodecError> {
|
|
||||||
if data.len() % 2 != 0 {
|
|
||||||
return Err(CodecError::InvalidLength);
|
|
||||||
}
|
|
||||||
|
|
||||||
let vals: Vec<i32> = 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::<Vec<i32>>();
|
|
||||||
|
|
||||||
Ok(vals)
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn build_req_frame(cols: usize, rows: usize) -> anyhow::Result<TactileAFrame> {
|
|
||||||
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<TactileAFrame> for TactileACodec {
|
|
||||||
fn decode(
|
|
||||||
&mut self,
|
|
||||||
input: &[u8],
|
|
||||||
session_started_at: std::time::Instant,
|
|
||||||
) -> Result<Vec<TactileAFrame>, CodecError> {
|
|
||||||
self.buffer.extend_from_slice(input);
|
|
||||||
let mut frames: Vec<TactileAFrame> = 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::<u8>::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<Vec<u8>, crate::serial_core::error::CodecError> {
|
|
||||||
match frame {
|
|
||||||
TactileAFrame::Req(f) => {
|
|
||||||
let mut req_bytes: Vec<u8> = 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<TactileAFrame, i32> for TactileAHandler {
|
|
||||||
async fn on_frame(&mut self, frame: &TactileAFrame) -> anyhow::Result<Option<Vec<i32>>> {
|
|
||||||
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<TactileARepFrame> for TactileACsvExporter {
|
|
||||||
type Error = CodecError;
|
|
||||||
fn csv_header(&self, _recording: &Recording<TactileARepFrame>) -> Vec<String> {
|
|
||||||
let mut header: Vec<String> = 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<TactileARepFrame>,
|
|
||||||
) -> anyhow::Result<Vec<String>> {
|
|
||||||
let packet = TactileADataPacket::try_from(&item.frame)?;
|
|
||||||
let summary: i32 = packet.data.iter().sum();
|
|
||||||
let mut row: Vec<String> = 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<TactileAFrame> for TactileACsvExporter {
|
|
||||||
type Error = CodecError;
|
|
||||||
|
|
||||||
fn csv_header(&self, _recording: &Recording<TactileAFrame>) -> Vec<String> {
|
|
||||||
let mut header: Vec<String> = 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<TactileAFrame>,
|
|
||||||
) -> anyhow::Result<Vec<String>> {
|
|
||||||
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<String> = 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<TactileADataPacket> {
|
|
||||||
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::<i32>()?);
|
|
||||||
}
|
|
||||||
|
|
||||||
let dts_cell = record
|
|
||||||
.get(self.channels)
|
|
||||||
.ok_or_else(|| anyhow!("missing dts cell"))?;
|
|
||||||
let dts_ms = dts_cell.parse::<u64>()?;
|
|
||||||
|
|
||||||
Ok(TactileADataPacket {
|
|
||||||
data: data,
|
|
||||||
dts_ms: dts_ms,
|
|
||||||
})
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl CsvImporter<TactileADataPacket> for TactileACsvImporter {
|
|
||||||
fn load<R: Read>(&mut self, reader: R) -> anyhow::Result<Vec<TactileADataPacket>> {
|
|
||||||
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<W>(recording: &Recording<TactileAFrame>, 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)
|
|
||||||
}
|
|
||||||
@@ -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<u8>,
|
|
||||||
}
|
|
||||||
|
|
||||||
pub struct TestHandler;
|
|
||||||
|
|
||||||
impl TestCodec {
|
|
||||||
pub fn new() -> TestCodec {
|
|
||||||
Self { buffer: Vec::new() }
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl Codec<TestFrame> for TestCodec {
|
|
||||||
fn decode(&mut self, input: &[u8], session_started_at: Instant) -> Result<Vec<TestFrame>, 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::<u8>::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<Vec<u8>, 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<TestFrame, i32> for TestHandler {
|
|
||||||
async fn on_frame(&mut self, frame: &TestFrame) -> anyhow::Result<Option<Vec<i32>>> {
|
|
||||||
match frame.cmd {
|
|
||||||
0x01 => {
|
|
||||||
let vals = parse_data_frame(&frame.payload)?;
|
|
||||||
Ok(Some(vals))
|
|
||||||
}
|
|
||||||
_ => Ok(None),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
fn parse_data_frame(data: &[u8]) -> Result<Vec<i32>, CodecError> {
|
|
||||||
if data.len() % 2 != 0 {
|
|
||||||
return Err(CodecError::InvalidLength);
|
|
||||||
}
|
|
||||||
|
|
||||||
let vals: Vec<i32> = data
|
|
||||||
.chunks_exact(2)
|
|
||||||
.map(|chunk| u16::from_be_bytes([chunk[0], chunk[1]]) as i32)
|
|
||||||
.collect::<Vec<i32>>();
|
|
||||||
|
|
||||||
Ok(vals)
|
|
||||||
}
|
|
||||||
|
|
||||||
pub struct TestCsvExporter;
|
|
||||||
pub struct TestCsvImporter {
|
|
||||||
channels: usize,
|
|
||||||
data_row: usize,
|
|
||||||
packets: Vec<TestDataPacket>,
|
|
||||||
}
|
|
||||||
|
|
||||||
#[derive(Clone)]
|
|
||||||
pub struct TestDataPacket {
|
|
||||||
pub data: Vec<i32>,
|
|
||||||
pub dts_ms: u64
|
|
||||||
}
|
|
||||||
|
|
||||||
impl TryFrom<&TestFrame> for TestDataPacket {
|
|
||||||
type Error = CodecError;
|
|
||||||
fn try_from(frame: &TestFrame) -> Result<TestDataPacket, Self::Error> {
|
|
||||||
let data = parse_data_frame(&frame.payload)?;
|
|
||||||
let dts = frame.dts_ms;
|
|
||||||
Ok(TestDataPacket { data: data, dts_ms: dts })
|
|
||||||
}
|
|
||||||
}
|
|
||||||
// impl From<TestFrame> 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<TestFrame> for TestCsvExporter {
|
|
||||||
type Error = CodecError;
|
|
||||||
fn csv_header(&self, recording: &Recording<TestFrame>) -> Vec<String> {
|
|
||||||
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<String> = 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<TestFrame>) -> anyhow::Result<Vec<String>> {
|
|
||||||
let packet: TestDataPacket = TestDataPacket::try_from(&item.frame)?;
|
|
||||||
let mut row: Vec<String> = 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<TestDataPacket>{
|
|
||||||
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::<i32>()?);
|
|
||||||
}
|
|
||||||
|
|
||||||
let dts_cell = record
|
|
||||||
.get(self.channels)
|
|
||||||
.ok_or_else(|| anyhow!("missing dts cell"))?;
|
|
||||||
let dts_ms = dts_cell.parse::<u64>()?;
|
|
||||||
|
|
||||||
Ok(TestDataPacket {
|
|
||||||
data: data,
|
|
||||||
dts_ms: dts_ms,
|
|
||||||
})
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl CsvImporter<TestDataPacket> for TestCsvImporter {
|
|
||||||
fn load<R: Read>(&mut self, reader: R) -> anyhow::Result<Vec<TestDataPacket>> {
|
|
||||||
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<W>(recording: &Recording<TestFrame>, 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(())
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -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<u8>,
|
|
||||||
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<u8>,
|
|
||||||
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<u8>,
|
|
||||||
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<F, T>: Send {
|
|
||||||
async fn on_frame(&mut self, frame: &F) -> Result<Option<Vec<T>>>;
|
|
||||||
}
|
|
||||||
|
|
||||||
@@ -1,34 +1,4 @@
|
|||||||
use crate::serial_core::{
|
|
||||||
frame::{TactileAFrame, TestFrame},
|
|
||||||
record::Recording,
|
|
||||||
};
|
|
||||||
|
|
||||||
pub mod codec;
|
|
||||||
pub mod codecs;
|
|
||||||
pub mod error;
|
pub mod error;
|
||||||
pub mod frame;
|
|
||||||
pub mod model;
|
pub mod model;
|
||||||
pub mod serial;
|
pub mod serial;
|
||||||
pub mod record;
|
pub mod record;
|
||||||
pub mod utils;
|
|
||||||
#[cfg(feature = "multi-dim")]
|
|
||||||
pub mod multi_dim_force;
|
|
||||||
|
|
||||||
pub type TestRecording = Recording<TestFrame>;
|
|
||||||
pub type TactileARecording = Recording<TactileAFrame>;
|
|
||||||
|
|
||||||
pub struct SerialConnection {
|
|
||||||
pub port: String,
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn connect(port: &str) -> Result<SerialConnection, String> {
|
|
||||||
let port = port.trim();
|
|
||||||
|
|
||||||
if port.is_empty() {
|
|
||||||
return Err("Serial port is required".to_string());
|
|
||||||
}
|
|
||||||
|
|
||||||
Ok(SerialConnection {
|
|
||||||
port: port.to_string(),
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|||||||
@@ -1,8 +1,6 @@
|
|||||||
use crate::serial_core::frame::TestFrame;
|
|
||||||
use std::collections::HashMap;
|
use std::collections::HashMap;
|
||||||
use std::time::{Duration, Instant, SystemTime, UNIX_EPOCH};
|
use std::time::{Duration, Instant, SystemTime, UNIX_EPOCH};
|
||||||
|
|
||||||
const MAX_POINTS: usize = 28;
|
|
||||||
const MAX_SUMMARY_POINTS: usize = 42;
|
const MAX_SUMMARY_POINTS: usize = 42;
|
||||||
const PANEL_STALE_AFTER: Duration = Duration::from_millis(2400);
|
const PANEL_STALE_AFTER: Duration = Duration::from_millis(2400);
|
||||||
|
|
||||||
@@ -74,16 +72,6 @@ pub struct HudSignalIcon {
|
|||||||
pub tone: HudTone,
|
pub tone: HudTone,
|
||||||
}
|
}
|
||||||
|
|
||||||
struct HudPanelUpdate {
|
|
||||||
source_id: String,
|
|
||||||
values: Vec<f32>,
|
|
||||||
}
|
|
||||||
|
|
||||||
struct PanelEntry {
|
|
||||||
panel: HudSignalPanel,
|
|
||||||
last_seen: Instant,
|
|
||||||
}
|
|
||||||
|
|
||||||
pub struct HudChartState {
|
pub struct HudChartState {
|
||||||
panels: HashMap<String, PanelEntry>,
|
panels: HashMap<String, PanelEntry>,
|
||||||
order: Vec<String>,
|
order: Vec<String>,
|
||||||
@@ -92,6 +80,11 @@ pub struct HudChartState {
|
|||||||
last_frame_seen: Option<Instant>,
|
last_frame_seen: Option<Instant>,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
struct PanelEntry {
|
||||||
|
panel: HudSignalPanel,
|
||||||
|
last_seen: Instant,
|
||||||
|
}
|
||||||
|
|
||||||
impl HudChartState {
|
impl HudChartState {
|
||||||
pub fn new() -> Self {
|
pub fn new() -> Self {
|
||||||
Self {
|
Self {
|
||||||
@@ -105,76 +98,21 @@ impl HudChartState {
|
|||||||
|
|
||||||
pub fn record_summary(&mut self, value: f32) {
|
pub fn record_summary(&mut self, value: f32) {
|
||||||
push_summary_point(&mut self.summary_points, value);
|
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() {
|
if values.is_empty() {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
self.pressure_matrix = Some(values.to_vec());
|
||||||
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()
|
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn prune_stale(&mut self) -> Option<HudPacket> {
|
pub fn prune_stale(&mut self) -> Option<HudPacket> {
|
||||||
|
let now = Instant::now();
|
||||||
let before = self.panels.len();
|
let before = self.panels.len();
|
||||||
let summary_points_before = self.summary_points.len();
|
let summary_before = self.summary_points.len();
|
||||||
self.prune_stale_at(Instant::now());
|
|
||||||
|
|
||||||
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
|
self.panels
|
||||||
.retain(|_, entry| now.duration_since(entry.last_seen) <= PANEL_STALE_AFTER);
|
.retain(|_, entry| now.duration_since(entry.last_seen) <= PANEL_STALE_AFTER);
|
||||||
self.order.retain(|id| self.panels.contains_key(id));
|
self.order.retain(|id| self.panels.contains_key(id));
|
||||||
@@ -189,6 +127,16 @@ impl HudChartState {
|
|||||||
self.pressure_matrix = None;
|
self.pressure_matrix = None;
|
||||||
self.last_frame_seen = 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 {
|
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<HudPanelUpdate> {
|
|
||||||
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<f32> {
|
|
||||||
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 {
|
fn side_for_index(index: usize) -> HudPanelSide {
|
||||||
if index % 2 == 0 {
|
if index % 2 == 0 {
|
||||||
HudPanelSide::Left
|
HudPanelSide::Left
|
||||||
@@ -331,91 +179,6 @@ fn side_for_index(index: usize) -> HudPanelSide {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fn push_point(points: &mut Vec<f32>, 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<HudSignalSeries> {
|
|
||||||
(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<HudSignalIcon> {
|
|
||||||
(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<f32> = 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::<f32>() / 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<f32>, value: f32) {
|
fn push_summary_point(points: &mut Vec<f32>, value: f32) {
|
||||||
if points.len() >= MAX_SUMMARY_POINTS {
|
if points.len() >= MAX_SUMMARY_POINTS {
|
||||||
points.remove(0);
|
points.remove(0);
|
||||||
@@ -440,61 +203,3 @@ fn now_millis() -> u64 {
|
|||||||
.map(|duration| duration.as_millis() as u64)
|
.map(|duration| duration.as_millis() as u64)
|
||||||
.unwrap_or_default()
|
.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);
|
|
||||||
// }
|
|
||||||
// }
|
|
||||||
|
|||||||
@@ -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<Vec<f32>>,
|
|
||||||
first_contact_cop_x: Option<f32>,
|
|
||||||
first_contact_cop_y: Option<f32>,
|
|
||||||
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<f32> {
|
|
||||||
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<f32, &'static str> {
|
|
||||||
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();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,3 +1,5 @@
|
|||||||
|
use eskin_finger_sdk::types::FingerSample;
|
||||||
|
|
||||||
#[derive(Clone)]
|
#[derive(Clone)]
|
||||||
pub struct FrameTiming {
|
pub struct FrameTiming {
|
||||||
pub pts_ms: Option<u64>,
|
pub pts_ms: Option<u64>,
|
||||||
@@ -7,50 +9,82 @@ pub struct FrameTiming {
|
|||||||
#[derive(Clone)]
|
#[derive(Clone)]
|
||||||
pub struct RecordedFrame<F> {
|
pub struct RecordedFrame<F> {
|
||||||
pub timing: FrameTiming,
|
pub timing: FrameTiming,
|
||||||
pub frame: F
|
pub frame: F,
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Clone, Default)]
|
#[derive(Clone, Default)]
|
||||||
pub struct Recording<F> {
|
pub struct Recording<F> {
|
||||||
pub frames: Vec<RecordedFrame<F>>
|
pub frames: Vec<RecordedFrame<F>>,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl<F> Recording<F> {
|
impl<F> Recording<F> {
|
||||||
pub fn new() -> Recording<F> { Self { frames: Vec::new() } }
|
pub fn new() -> Recording<F> {
|
||||||
pub fn push(&mut self, ite: RecordedFrame<F>) {
|
Self {
|
||||||
self.frames.push(ite);
|
frames: Vec::new(),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
pub fn push(&mut self, item: RecordedFrame<F>) {
|
||||||
|
self.frames.push(item);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
pub trait CsvExporter<F> {
|
pub type FingerRecording = Recording<FingerSample>;
|
||||||
type Error: std::error::Error + Send + Sync + 'static;
|
|
||||||
fn csv_header(&self, recording: &Recording<F>) -> Vec<String>;
|
|
||||||
fn csv_row(&self, item: &RecordedFrame<F>) -> anyhow::Result<Vec<String>>;
|
|
||||||
}
|
|
||||||
|
|
||||||
// TODO: CsvImporter
|
pub fn export_recording_csv<W>(
|
||||||
pub trait CsvImporter<P> {
|
recording: &Recording<FingerSample>,
|
||||||
fn load<R: std::io::Read>(&mut self, reader: R) -> anyhow::Result<Vec<P>>;
|
mut writer: W,
|
||||||
}
|
|
||||||
|
|
||||||
pub fn write_csv<F, E, W>(
|
|
||||||
recording: &Recording<F>,
|
|
||||||
exporter: &E,
|
|
||||||
writer: W,
|
|
||||||
) -> anyhow::Result<()>
|
) -> anyhow::Result<()>
|
||||||
where
|
where
|
||||||
E: CsvExporter<F>,
|
|
||||||
W: std::io::Write,
|
W: std::io::Write,
|
||||||
{
|
{
|
||||||
let header = exporter.csv_header(&recording);
|
// Infer channel count from the first sample's combined_forces (just fz)
|
||||||
let mut wrt = csv::Writer::from_writer(writer);
|
// We write: timestamp_us, sequence, module, fx, fy, fz
|
||||||
wrt.write_record(header)?;
|
let mut wrt = csv::Writer::from_writer(&mut writer);
|
||||||
for f in &recording.frames {
|
wrt.write_record(["timestamp_us", "sequence", "module", "fx", "fy", "fz"])?;
|
||||||
let row = exporter.csv_row(f)?;
|
|
||||||
wrt.write_record(&row)?;
|
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()?;
|
wrt.flush()?;
|
||||||
|
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub struct FingerSampleCsvPacket {
|
||||||
|
pub timestamp_us: u64,
|
||||||
|
pub sequence: u32,
|
||||||
|
pub fz: u32,
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn import_csv<R: std::io::Read>(
|
||||||
|
reader: R,
|
||||||
|
) -> anyhow::Result<Vec<FingerSampleCsvPacket>> {
|
||||||
|
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::<u64>().unwrap_or(0);
|
||||||
|
let sequence = record.get(1).unwrap_or("0").parse::<u32>().unwrap_or(0);
|
||||||
|
let fz = record.get(5).unwrap_or("0").parse::<u32>().unwrap_or(0);
|
||||||
|
|
||||||
|
packets.push(FingerSampleCsvPacket {
|
||||||
|
timestamp_us,
|
||||||
|
sequence,
|
||||||
|
fz,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
Ok(packets)
|
||||||
|
}
|
||||||
@@ -1,431 +1,160 @@
|
|||||||
use crate::serial_core::codec::Codec;
|
use crate::serial_core::model::HudChartState;
|
||||||
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::record::Recording;
|
use crate::serial_core::record::Recording;
|
||||||
use crate::serial_core::record::{FrameTiming, RecordedFrame};
|
use eskin_finger_sdk::channel::DeviceEvent;
|
||||||
#[cfg(feature = "devkit")]
|
use eskin_finger_sdk::config::DeviceConfig;
|
||||||
use crate::devkit::{proto::SensorFrame, DevKitState};
|
use eskin_finger_sdk::device::{EskinDevice, EskinDeviceInner};
|
||||||
use anyhow::Result;
|
use eskin_finger_sdk::transport::SerialPortTransport;
|
||||||
use log::debug;
|
use eskin_finger_sdk::types::FingerSample;
|
||||||
use std::future::pending;
|
|
||||||
#[cfg(feature = "devkit")]
|
|
||||||
use std::sync::atomic::Ordering;
|
|
||||||
use std::sync::{Arc, Mutex};
|
|
||||||
use std::time::Instant;
|
|
||||||
use tauri::{AppHandle, Emitter};
|
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;
|
use tokio_util::sync::CancellationToken;
|
||||||
|
|
||||||
const AUTO_SUB_INTERVAL: Duration = Duration::from_nanos(16_666_667);
|
use super::model::HudPacket;
|
||||||
|
|
||||||
pub enum PollMode<F> {
|
pub struct SdkSession {
|
||||||
Disable,
|
pub device: EskinDeviceInner,
|
||||||
Enabled(Box<dyn PollRequester<F>>),
|
|
||||||
}
|
}
|
||||||
|
|
||||||
struct PendingSubFrame<F> {
|
pub fn open_device(port: &str) -> Result<SdkSession, String> {
|
||||||
frame: F,
|
let port = port.trim();
|
||||||
values: Vec<i32>,
|
if port.is_empty() {
|
||||||
|
return Err("Serial port is required".to_string());
|
||||||
}
|
}
|
||||||
|
|
||||||
pub trait SerialFrame: Clone + Send + 'static {
|
let transport = SerialPortTransport::new(port, 921600);
|
||||||
fn dts_ms(&self) -> u64;
|
let config = DeviceConfig::default();
|
||||||
|
let mut device = EskinDeviceInner::new(config, Box::new(transport));
|
||||||
|
device.open().map_err(|e| e.to_string())?;
|
||||||
|
|
||||||
fn to_hud_packet(
|
Ok(SdkSession { device })
|
||||||
&self,
|
|
||||||
chart_state: &mut HudChartState,
|
|
||||||
display_values: Option<&[i32]>,
|
|
||||||
) -> Option<HudPacket>;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
impl SerialFrame for TestFrame {
|
pub async fn run_stream(
|
||||||
fn dts_ms(&self) -> u64 {
|
|
||||||
self.dts_ms
|
|
||||||
}
|
|
||||||
|
|
||||||
fn to_hud_packet(
|
|
||||||
&self,
|
|
||||||
chart_state: &mut HudChartState,
|
|
||||||
display_values: Option<&[i32]>,
|
|
||||||
) -> Option<HudPacket> {
|
|
||||||
Some(chart_state.apply_frame(self, display_values))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl SerialFrame for TactileAFrame {
|
|
||||||
fn dts_ms(&self) -> u64 {
|
|
||||||
match self {
|
|
||||||
TactileAFrame::Req(_) => 0,
|
|
||||||
TactileAFrame::Rep(rep) => rep.dts_ms,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
fn to_hud_packet(
|
|
||||||
&self,
|
|
||||||
chart_state: &mut HudChartState,
|
|
||||||
display_values: Option<&[i32]>,
|
|
||||||
) -> Option<HudPacket> {
|
|
||||||
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))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
pub trait PollRequester<F>: Send {
|
|
||||||
fn poll_interval(&self) -> Option<Duration> {
|
|
||||||
None
|
|
||||||
}
|
|
||||||
|
|
||||||
fn should_request(&mut self) -> bool {
|
|
||||||
true
|
|
||||||
}
|
|
||||||
|
|
||||||
fn next_request(&mut self) -> Result<Option<F>> {
|
|
||||||
Ok(None)
|
|
||||||
}
|
|
||||||
|
|
||||||
fn on_rx_frame(&mut self, _frame: &F) {}
|
|
||||||
}
|
|
||||||
|
|
||||||
#[derive(Default)]
|
|
||||||
pub struct NoopPollRequester;
|
|
||||||
|
|
||||||
impl<F> PollRequester<F> for NoopPollRequester {}
|
|
||||||
|
|
||||||
pub struct TactileAPollRequester {
|
|
||||||
period: Duration,
|
|
||||||
cols: usize,
|
|
||||||
rows: usize,
|
|
||||||
awaiting_reply: bool,
|
|
||||||
last_request_at: Option<Instant>,
|
|
||||||
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,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl PollRequester<TactileAFrame> for TactileAPollRequester {
|
|
||||||
fn poll_interval(&self) -> Option<Duration> {
|
|
||||||
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<Option<TactileAFrame>> {
|
|
||||||
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<C, H, T, F>(
|
|
||||||
app: AppHandle,
|
app: AppHandle,
|
||||||
port: SerialStream,
|
device: &mut EskinDeviceInner,
|
||||||
codec: C,
|
|
||||||
handler: H,
|
|
||||||
session_started_at: Instant,
|
|
||||||
recording: Arc<Mutex<Recording<F>>>,
|
|
||||||
cancel: CancellationToken,
|
cancel: CancellationToken,
|
||||||
) -> Result<()>
|
) -> Result<(), String> {
|
||||||
where
|
device
|
||||||
F: SerialFrame,
|
.start_stream()
|
||||||
C: Codec<F> + Send + 'static,
|
.map_err(|e| format!("start_stream failed: {e}"))?;
|
||||||
H: FrameHandler<F, T> + Send + 'static,
|
|
||||||
T: Into<i32>,
|
|
||||||
{
|
|
||||||
run_serial_with_poll(
|
|
||||||
app,
|
|
||||||
port,
|
|
||||||
codec,
|
|
||||||
handler,
|
|
||||||
session_started_at,
|
|
||||||
recording,
|
|
||||||
cancel,
|
|
||||||
PollMode::Disable,
|
|
||||||
)
|
|
||||||
.await
|
|
||||||
}
|
|
||||||
|
|
||||||
pub async fn run_serial_with_poll<C, H, T, F>(
|
|
||||||
app: AppHandle,
|
|
||||||
mut port: SerialStream,
|
|
||||||
mut codec: C,
|
|
||||||
mut handler: H,
|
|
||||||
session_started_at: Instant,
|
|
||||||
recording: Arc<Mutex<Recording<F>>>,
|
|
||||||
cancel: CancellationToken,
|
|
||||||
poll_mode: PollMode<F>,
|
|
||||||
) -> Result<()>
|
|
||||||
where
|
|
||||||
F: SerialFrame,
|
|
||||||
C: Codec<F> + Send + 'static,
|
|
||||||
H: FrameHandler<F, T> + Send + 'static,
|
|
||||||
T: Into<i32>,
|
|
||||||
{
|
|
||||||
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 channels = device.channels();
|
||||||
let mut chart_state = HudChartState::new();
|
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<PendingSubFrame<F>> = None;
|
|
||||||
prune_interval.set_missed_tick_behavior(MissedTickBehavior::Delay);
|
|
||||||
|
|
||||||
loop {
|
let result = loop {
|
||||||
tokio::select! {
|
tokio::select! {
|
||||||
_ = cancel.cancelled() => break,
|
_ = cancel.cancelled() => {
|
||||||
_ = async {
|
break Ok(());
|
||||||
match poll_interval.as_mut() {
|
|
||||||
Some(it) => {
|
|
||||||
it.tick().await;
|
|
||||||
}
|
}
|
||||||
None => pending::<()>().await,
|
_ = tokio::time::sleep(tokio::time::Duration::from_millis(1)) => {}
|
||||||
}
|
}
|
||||||
} => {
|
|
||||||
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?;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
_ = 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
|
// Try to receive a sample (non-blocking-ish via small timeout)
|
||||||
.frame
|
match channels.recv_sample(5) {
|
||||||
.to_hud_packet(&mut chart_state, display_values.as_deref())
|
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}"));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Drain any events
|
||||||
|
if let Err(e) = drain_events(&channels) {
|
||||||
|
break Err(e);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
let _ = device.stop_stream();
|
||||||
|
result
|
||||||
|
}
|
||||||
|
|
||||||
|
pub async fn run_stream_with_record(
|
||||||
|
app: AppHandle,
|
||||||
|
device: &mut EskinDeviceInner,
|
||||||
|
cancel: CancellationToken,
|
||||||
|
recording: std::sync::Arc<std::sync::Mutex<Recording<FingerSample>>>,
|
||||||
|
) -> 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)) => {}
|
||||||
|
}
|
||||||
|
|
||||||
|
match channels.recv_sample(5) {
|
||||||
|
Ok(sample) => {
|
||||||
|
// Record
|
||||||
{
|
{
|
||||||
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::<Vec<i32>>());
|
|
||||||
|
|
||||||
let mut record = recording
|
let mut record = recording
|
||||||
.lock()
|
.lock()
|
||||||
.map_err(|_| anyhow::anyhow!("recording state poisoned"))?;
|
.map_err(|_| "recording state poisoned".to_string())?;
|
||||||
record.push(RecordedFrame {
|
record.push(crate::serial_core::record::RecordedFrame {
|
||||||
timing: FrameTiming {
|
timing: crate::serial_core::record::FrameTiming {
|
||||||
pts_ms: None,
|
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::<Vec<f32>>();
|
|
||||||
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::<i32>();
|
|
||||||
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)?;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
Ok(())
|
|
||||||
}
|
|
||||||
|
|
||||||
fn build_display_values(chart_state: &mut HudChartState, values: &[i32]) -> Option<Vec<i32>> {
|
|
||||||
let summary = values.iter().copied().sum::<i32>();
|
|
||||||
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::<DevKitState>();
|
|
||||||
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::<Vec<_>>();
|
|
||||||
|
|
||||||
devkit_state.push_frame(SensorFrame {
|
|
||||||
seq,
|
|
||||||
timestamp_ms,
|
|
||||||
rows,
|
|
||||||
cols,
|
|
||||||
matrix,
|
|
||||||
resultant_force,
|
|
||||||
dts_ms: dts_ms as u32,
|
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
#[cfg(feature = "devkit")]
|
if let Some(packet) = build_hud_packet_from_sample(&sample, &mut chart_state) {
|
||||||
fn infer_matrix_shape(len: usize) -> (u32, u32) {
|
let _ = app.emit("hud_stream", packet);
|
||||||
if len == 84 {
|
|
||||||
return (12, 7);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if len == 0 {
|
|
||||||
return (0, 0);
|
|
||||||
}
|
}
|
||||||
|
Err(eskin_finger_sdk::error::SdkError::Timeout) => {}
|
||||||
let mut best = (len, 1);
|
Err(e) => {
|
||||||
let mut factor = 1usize;
|
break Err(format!("sample recv error: {e}"));
|
||||||
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;
|
if let Err(e) = drain_events(&channels) {
|
||||||
Y[left] / 100.0 + ratio * (Y[right] - Y[left]) / 100.0
|
break Err(e);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
let _ = device.stop_stream();
|
||||||
|
result
|
||||||
|
}
|
||||||
|
|
||||||
|
fn drain_events(channels: &std::sync::Arc<eskin_finger_sdk::channel::ChannelManager>) -> 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(()),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn build_hud_packet_from_sample(
|
||||||
|
sample: &FingerSample,
|
||||||
|
chart_state: &mut HudChartState,
|
||||||
|
) -> Option<HudPacket> {
|
||||||
|
let fz = sample.combined_forces.force.fz as f32;
|
||||||
|
chart_state.record_summary(fz);
|
||||||
|
if !sample.raw_adcs.is_empty() {
|
||||||
|
let pressure: Vec<f32> = sample.raw_adcs.iter().map(|&v| v as f32).collect();
|
||||||
|
chart_state.record_pressure_matrix(&pressure);
|
||||||
|
}
|
||||||
|
Some(chart_state.build_snapshot())
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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::<u8>::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::<u8>::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(())
|
|
||||||
}
|
|
||||||
}
|
|
||||||
Reference in New Issue
Block a user