first commit
This commit is contained in:
2
src-tauri/src/commands/mod.rs
Normal file
2
src-tauri/src/commands/mod.rs
Normal file
@@ -0,0 +1,2 @@
|
||||
pub mod serial;
|
||||
pub mod window;
|
||||
289
src-tauri/src/commands/serial.rs
Normal file
289
src-tauri/src/commands/serial.rs
Normal 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(),
|
||||
})
|
||||
}
|
||||
32
src-tauri/src/commands/window.rs
Normal file
32
src-tauri/src/commands/window.rs
Normal file
@@ -0,0 +1,32 @@
|
||||
use tauri::{AppHandle, Manager, WebviewWindow};
|
||||
|
||||
fn main_window(app: &AppHandle) -> Result<WebviewWindow, String> {
|
||||
app.get_webview_window("main")
|
||||
.ok_or_else(|| "Can't find main window".to_string())
|
||||
}
|
||||
|
||||
#[tauri::command]
|
||||
pub fn win_minimize(app: AppHandle) -> Result<(), String> {
|
||||
main_window(&app)?
|
||||
.minimize()
|
||||
.map_err(|error| error.to_string())
|
||||
}
|
||||
|
||||
#[tauri::command]
|
||||
pub fn win_toggle_maximize(app: AppHandle) -> Result<(), String> {
|
||||
let window = main_window(&app)?;
|
||||
let is_maximized = window.is_maximized().map_err(|error| error.to_string())?;
|
||||
|
||||
if is_maximized {
|
||||
window.unmaximize().map_err(|error| error.to_string())
|
||||
} else {
|
||||
window.maximize().map_err(|error| error.to_string())
|
||||
}
|
||||
}
|
||||
|
||||
#[tauri::command]
|
||||
pub fn win_close(app: AppHandle) -> Result<(), String> {
|
||||
main_window(&app)?
|
||||
.close()
|
||||
.map_err(|error| error.to_string())
|
||||
}
|
||||
Reference in New Issue
Block a user