first commit

This commit is contained in:
lennlouisgeek
2026-03-30 02:59:56 +08:00
commit eec9927ae6
60 changed files with 15953 additions and 0 deletions

View File

@@ -0,0 +1,289 @@
use crate::serial_core::codecs::test::{export_recording_csv, TestCodec, TestCsvImporter, TestHandler};
use crate::serial_core::error::SerialError;
use crate::serial_core::record::CsvImporter;
use crate::serial_core::{TestRecording, serial};
use log::info;
use serde::Serialize;
use std::fs::File;
use std::io::Cursor;
use std::sync::{Arc, Mutex};
use std::time::{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>>;
#[derive(Serialize)]
#[serde(rename_all = "camelCase")]
pub struct SerialConnectResponse {
pub port: String,
pub connected: bool,
pub message: String,
}
#[derive(Serialize)]
#[serde(rename_all = "camelCase")]
pub struct SerialExportResponse {
pub path: String,
pub frame_count: usize,
pub message: String,
}
#[derive(Serialize)]
#[serde(rename_all = "camelCase")]
pub struct SerialImportFrame {
pub data: Vec<i32>,
pub dts_ms: u64,
}
#[derive(Serialize)]
#[serde(rename_all = "camelCase")]
pub struct SerialImportResponse {
pub file_name: String,
pub frame_count: usize,
pub channel_count: usize,
pub frames: Vec<SerialImportFrame>,
pub message: String,
}
struct SerialSession {
port: String,
cancel: CancellationToken,
task: JoinHandle<()>,
current_record: SharedTestRecording,
}
#[derive(Default)]
pub struct SerialConnectionState {
session: Mutex<Option<SerialSession>>,
last_record: Mutex<Option<SharedTestRecording>>
}
#[tauri::command]
pub fn serial_enum() -> Result<Vec<String>, SerialError> {
let ports = available_ports()
.map_err(|_| SerialError::ScanError)?
.into_iter()
.map(|info| info.port_name)
.collect();
Ok(ports)
}
#[tauri::command]
pub async fn serial_connect(
app: AppHandle,
port: String,
state: State<'_, SerialConnectionState>,
) -> Result<SerialConnectResponse, SerialError> {
let port_name = port.trim().to_string();
if port_name.is_empty() {
return Err(SerialError::InvalidConfig);
}
{
let session = state.session.lock().map_err(|_| SerialError::StateError)?;
if session.is_some() {
return Err(SerialError::AlreadyConnected);
}
}
let cancel = CancellationToken::new();
let current_record = Arc::new(Mutex::new(TestRecording::new()));
let task_record = current_record.clone();
let task_cancel = cancel.clone();
let task_app = app.clone();
let task_port_name = port_name.clone();
let port = tokio_serial::new(&port_name, 115200)
.open_native_async()
.map_err(|_| SerialError::OpenError)?;
let session_started_at = Instant::now();
let task = tauri::async_runtime::spawn(async move {
let codec = TestCodec::new();
let handler = TestHandler;
if let Err(error) = serial::run_serial(
task_app.clone(),
port,
codec,
handler,
session_started_at,
task_record.clone(),
task_cancel,
)
.await
{
eprintln!("serial task exited with error: {error}");
}
let manager = task_app.state::<SerialConnectionState>();
if let Ok(mut last_record) = manager.last_record.lock() {
*last_record = Some(task_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() == task_port_name.as_str())
.unwrap_or(false);
if should_clear {
session.take();
}
}
});
let mut session = state.session.lock().map_err(|_| SerialError::StateError)?;
if session.is_some() {
cancel.cancel();
task.abort();
return Err(SerialError::AlreadyConnected);
}
*session = Some(SerialSession {
port: port_name.clone(),
cancel,
task,
current_record
});
Ok(SerialConnectResponse {
port: port_name,
connected: true,
message: "connected".to_string(),
})
}
#[tauri::command]
pub async fn serial_disconnect(
state: State<'_, SerialConnectionState>,
) -> Result<SerialConnectResponse, SerialError> {
let session = {
let mut guard = state.session.lock().map_err(|_| SerialError::StateError)?;
guard.take()
};
let Some(SerialSession {
port,
cancel,
task,
current_record,
}) = session
else {
return Ok(SerialConnectResponse {
port: String::new(),
connected: false,
message: "already disconnected".to_string(),
});
};
cancel.cancel();
let _ = task.await;
let frame_count = current_record.lock().map(|record| {
record.frames.len()
}).unwrap_or(0);
info!("last_record has {} frames", frame_count);
if let Ok(mut last_record) = state.last_record.lock() {
*last_record = Some(current_record);
}
Ok(SerialConnectResponse {
port,
connected: false,
message: "disconnected".to_string(),
})
}
#[tauri::command]
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)?,
};
let timestamp = SystemTime::now()
.duration_since(UNIX_EPOCH)
.map(|duration| duration.as_millis())
.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 path = output_dir.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 packets = importer
.load(Cursor::new(csv_content.into_bytes()))
.map_err(|_| SerialError::ImportError)?;
if packets.is_empty() {
return Err(SerialError::NoRecordedData);
}
let channel_count = packets.first().map(|item| item.data.len()).unwrap_or(0);
let frame_count = packets.len();
let frames = packets
.into_iter()
.map(|packet| SerialImportFrame {
data: packet.data,
dts_ms: packet.dts_ms,
})
.collect();
Ok(SerialImportResponse {
file_name,
frame_count,
channel_count,
frames,
message: "imported".to_string(),
})
}