exchange tast to tactilea

This commit is contained in:
lennlouisgeek
2026-04-03 00:47:36 +08:00
parent a686d19e61
commit 7688986ad7
15 changed files with 1842 additions and 147 deletions

View File

@@ -1,19 +1,27 @@
use crate::serial_core::codecs::test::{export_recording_csv, TestCodec, TestCsvImporter, TestHandler};
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;
use crate::serial_core::{TestRecording, serial};
use crate::serial_core::serial::{PollMode, TactileAPollRequester};
use crate::serial_core::{serial, TactileARecording};
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::{Instant, SystemTime, UNIX_EPOCH};
use std::time::{Duration, Instant, SystemTime, UNIX_EPOCH};
use tauri::{async_runtime::JoinHandle, AppHandle, Manager, State};
use tokio_serial::{available_ports, SerialPortBuilderExt};
use tokio_util::sync::CancellationToken;
type SharedTestRecording = Arc<Mutex<TestRecording>>;
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<Mutex<TactileARecording>>;
#[derive(Serialize)]
@@ -49,17 +57,24 @@ pub struct SerialImportResponse {
pub message: String,
}
#[derive(Serialize)]
#[serde(rename_all = "camelCase")]
pub struct SerialRecordStateResponse {
pub has_data: bool,
pub frame_count: usize,
}
struct SerialSession {
port: String,
cancel: CancellationToken,
task: JoinHandle<()>,
current_record: SharedTestRecording,
current_record: SharedTactileRecording,
}
#[derive(Default)]
pub struct SerialConnectionState {
session: Mutex<Option<SerialSession>>,
last_record: Mutex<Option<SharedTestRecording>>
last_record: Mutex<Option<SharedTactileRecording>>
}
#[tauri::command]
@@ -92,7 +107,7 @@ pub async fn serial_connect(
}
let cancel = CancellationToken::new();
let current_record = Arc::new(Mutex::new(TestRecording::new()));
let current_record = Arc::new(Mutex::new(TactileARecording::new()));
let task_record = current_record.clone();
let task_cancel = cancel.clone();
let task_app = app.clone();
@@ -104,10 +119,16 @@ pub async fn serial_connect(
let session_started_at = Instant::now();
let task = tauri::async_runtime::spawn(async move {
let codec = TestCodec::new();
let handler = TestHandler;
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),
)));
if let Err(error) = serial::run_serial(
if let Err(error) = serial::run_serial_with_poll(
task_app.clone(),
port,
codec,
@@ -115,6 +136,7 @@ pub async fn serial_connect(
session_started_at,
task_record.clone(),
task_cancel,
poll_mode,
)
.await
{
@@ -212,20 +234,6 @@ pub fn serial_export_csv(
app: AppHandle,
state: State<'_, SerialConnectionState>,
) -> Result<SerialExportResponse, SerialError> {
let current_record = {
let session = state.session.lock().map_err(|_| SerialError::StateError)?;
session
.as_ref()
.map(|current_session| current_session.current_record.clone())
};
let record = if let Some(recording) = current_record {
recording
} else {
let last_record = state.last_record.lock().map_err(|_| SerialError::StateError)?;
last_record.clone().ok_or(SerialError::NoRecordedData)?
};
let mut output_dir = match app.path().desktop_dir() {
Ok(path) => path,
Err(_) => std::env::current_dir().map_err(|_| SerialError::ExportError)?,
@@ -237,17 +245,8 @@ pub fn serial_export_csv(
.unwrap_or_default();
output_dir.push(format!("joyson_export_{timestamp}.csv"));
let mut file = File::create(&output_dir).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)?;
recording.frames.len()
};
let record = resolve_record_for_export(&state)?;
let frame_count = write_record_to_csv(record, &output_dir)?;
let path = output_dir.display().to_string();
info!("csv exported to {path}, frame_count={frame_count}");
@@ -259,9 +258,40 @@ pub fn serial_export_csv(
})
}
#[tauri::command]
pub fn serial_has_record_data(
state: State<'_, SerialConnectionState>,
) -> Result<SerialRecordStateResponse, SerialError> {
let frame_count = snapshot_record_frame_count(&state)?;
Ok(SerialRecordStateResponse {
has_data: frame_count > 0,
frame_count,
})
}
#[tauri::command]
pub fn serial_export_csv_to_path(
file_path: String,
state: State<'_, SerialConnectionState>,
) -> Result<SerialExportResponse, SerialError> {
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 path = output_path.display().to_string();
info!("csv exported to {path}, frame_count={frame_count}");
Ok(SerialExportResponse {
path,
frame_count,
message: "exported".to_string(),
})
}
#[tauri::command]
pub fn serial_import_csv(file_name: String, csv_content: String) -> Result<SerialImportResponse, SerialError> {
let mut importer = TestCsvImporter::new(file_name.as_str());
let mut importer = TactileACsvImporter::new(file_name.as_str());
let packets = importer
.load(Cursor::new(csv_content.into_bytes()))
.map_err(|_| SerialError::ImportError)?;
@@ -288,3 +318,128 @@ pub fn serial_import_csv(file_name: String, csv_content: String) -> Result<Seria
message: "imported".to_string(),
})
}
#[tauri::command]
pub fn serial_import_csv_from_path(file_path: String) -> Result<SerialImportResponse, SerialError> {
let path = resolve_import_path(file_path)?;
let file_name = path
.file_name()
.and_then(|value| value.to_str())
.map(ToOwned::to_owned)
.unwrap_or_else(|| "import.csv".to_string());
let bytes = std::fs::read(&path).map_err(|_| SerialError::ImportError)?;
let csv_content = String::from_utf8_lossy(&bytes).to_string();
serial_import_csv(file_name, csv_content)
}
fn resolve_record_for_export(
state: &State<'_, SerialConnectionState>,
) -> Result<SharedTactileRecording, SerialError> {
let current_record = {
let session = state.session.lock().map_err(|_| SerialError::StateError)?;
session
.as_ref()
.map(|current_session| current_session.current_record.clone())
};
if let Some(recording) = current_record {
return Ok(recording);
}
let last_record = state.last_record.lock().map_err(|_| SerialError::StateError)?;
last_record.clone().ok_or(SerialError::NoRecordedData)
}
fn snapshot_record_frame_count(
state: &State<'_, SerialConnectionState>,
) -> Result<usize, SerialError> {
let current_record = {
let session = state.session.lock().map_err(|_| SerialError::StateError)?;
session
.as_ref()
.map(|current_session| current_session.current_record.clone())
};
if let Some(record) = current_record {
return record
.lock()
.map(|recording| recording.frames.len())
.map_err(|_| SerialError::StateError);
}
let last_record = state.last_record.lock().map_err(|_| SerialError::StateError)?;
let Some(record) = last_record.as_ref() else {
return Ok(0);
};
record
.lock()
.map(|recording| recording.frames.len())
.map_err(|_| SerialError::StateError)
}
fn write_record_to_csv(
record: SharedTactileRecording,
output_path: &Path,
) -> Result<usize, SerialError> {
if let Some(parent) = output_path.parent() {
if !parent.exists() {
return Err(SerialError::ExportError);
}
}
let mut file = 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)?;
recording.frames.len()
};
Ok(frame_count)
}
fn resolve_export_path(raw_path: String) -> Result<PathBuf, SerialError> {
let trimmed = raw_path.trim();
if trimmed.is_empty() {
return Err(SerialError::ExportError);
}
let mut path = resolve_absolute_path(trimmed).map_err(|_| SerialError::ExportError)?;
if path.extension().is_none() {
path.set_extension("csv");
}
if path.file_name().is_none() {
return Err(SerialError::ExportError);
}
Ok(path)
}
fn resolve_import_path(raw_path: String) -> Result<PathBuf, SerialError> {
let trimmed = raw_path.trim();
if trimmed.is_empty() {
return Err(SerialError::ImportError);
}
let path = resolve_absolute_path(trimmed).map_err(|_| SerialError::ImportError)?;
if !path.exists() || !path.is_file() {
return Err(SerialError::ImportError);
}
Ok(path)
}
fn resolve_absolute_path(raw_path: &str) -> std::io::Result<PathBuf> {
let path = PathBuf::from(raw_path);
if path.is_absolute() {
Ok(path)
} else {
Ok(std::env::current_dir()?.join(path))
}
}