From 770d713d032548a54c90a8d8fc66ad475ae05e88 Mon Sep 17 00:00:00 2001 From: lennlouisgeek Date: Tue, 7 Apr 2026 01:46:37 +0800 Subject: [PATCH] =?UTF-8?q?=E5=88=9D=E6=AD=A5=E6=B7=BB=E5=8A=A0=E4=BA=86?= =?UTF-8?q?=E6=A0=87=E5=AE=9A=E6=94=AF=E6=8C=81=EF=BC=8C=E9=9C=80=E8=A6=81?= =?UTF-8?q?=E5=AE=8C=E5=96=84=E5=92=8C=E6=B5=8B=E8=AF=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src-tauri/program.log2026-04-06 | 19 + src-tauri/resource/round_finish_once.mp3 | Bin 0 -> 19296 bytes src-tauri/src/commands/calibration.rs | 150 +++ src-tauri/src/commands/mod.rs | 1 + src-tauri/src/commands/serial.rs | 60 +- src-tauri/src/commands/window.rs | 14 +- src-tauri/src/lib.rs | 3 + src-tauri/src/log.rs | 35 +- .../src/serial_core/calibration_session.rs | 109 ++ src-tauri/src/serial_core/codecs/mod.rs | 5 +- src-tauri/src/serial_core/codecs/tactile_a.rs | 91 +- src-tauri/src/serial_core/codecs/test.rs | 44 +- src-tauri/src/serial_core/frame.rs | 27 +- src-tauri/src/serial_core/mod.rs | 4 +- src-tauri/src/serial_core/record.rs | 41 +- src-tauri/src/serial_core/serial.rs | 196 ++- src/app.html | 2 +- src/lib/components/CenterStage.svelte | 1198 ++++++++++++----- src/routes/+page.svelte | 89 +- 19 files changed, 1599 insertions(+), 489 deletions(-) create mode 100644 src-tauri/program.log2026-04-06 create mode 100644 src-tauri/resource/round_finish_once.mp3 create mode 100644 src-tauri/src/commands/calibration.rs create mode 100644 src-tauri/src/serial_core/calibration_session.rs diff --git a/src-tauri/program.log2026-04-06 b/src-tauri/program.log2026-04-06 new file mode 100644 index 0000000..60c7095 --- /dev/null +++ b/src-tauri/program.log2026-04-06 @@ -0,0 +1,19 @@ +[2026-04-06T07:28:34Z DEBUG tauri_demo_lib::log] logging initialized +[2026-04-06T07:28:34Z DEBUG JE_Skin] logging initialized +[2026-04-06T07:29:01Z DEBUG tauri_demo_lib::log] logging initialized +[2026-04-06T07:29:01Z DEBUG JE_Skin] logging initialized +[2026-04-06T07:29:25Z DEBUG tao::platform_impl::platform::event_loop::runner] NewEvents emitted without explicit RedrawEventsCleared +[2026-04-06T07:29:25Z DEBUG tao::platform_impl::platform::event_loop::runner] RedrawEventsCleared emitted without explicit MainEventsCleared +[2026-04-06T07:29:27Z DEBUG mio_serial] opening serial port in synchronous blocking mode +[2026-04-06T07:29:27Z DEBUG mio_serial] switching COM1 to asynchronous mode +[2026-04-06T07:29:27Z DEBUG mio_serial] reading serial port settings +[2026-04-06T07:29:27Z DEBUG mio_serial] closing synchronous port to re-open in FILE_FLAG_OVERLAPPED mode +[2026-04-06T07:29:27Z DEBUG mio_serial] re-setting serial port parameters to original values from synchronous port +[2026-04-06T07:29:36Z DEBUG tao::platform_impl::platform::event_loop::runner] NewEvents emitted without explicit RedrawEventsCleared +[2026-04-06T07:29:36Z DEBUG tao::platform_impl::platform::event_loop::runner] RedrawEventsCleared emitted without explicit MainEventsCleared +[2026-04-06T07:30:02Z DEBUG tauri_demo_lib::serial_core::codecs::tactile_a] unexpected payload length: expected 168, got 20746, buffer_len=178 +[2026-04-06T07:30:07Z DEBUG tauri_demo_lib::serial_core::codecs::tactile_a] unexpected payload length: expected 168, got 20746, buffer_len=178 +[2026-04-06T07:30:12Z DEBUG tao::platform_impl::platform::event_loop::runner] NewEvents emitted without explicit RedrawEventsCleared +[2026-04-06T07:30:12Z DEBUG tao::platform_impl::platform::event_loop::runner] RedrawEventsCleared emitted without explicit MainEventsCleared +[2026-04-06T07:30:14Z DEBUG tauri_demo_lib::log] logging initialized +[2026-04-06T07:30:14Z DEBUG JE_Skin] logging initialized diff --git a/src-tauri/resource/round_finish_once.mp3 b/src-tauri/resource/round_finish_once.mp3 new file mode 100644 index 0000000000000000000000000000000000000000..4a8b364f08f40196153f06587887e1e49e5dd238 GIT binary patch literal 19296 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, +} + +struct CalibrationSessionData { + cancel: CancellationToken, + task: JoinHandle<()>, +} + +#[tauri::command] +pub async fn serial_calibrate_with_coarse( + app: AppHandle, + port: String, + target_frames: usize, + max_rounds: usize, + state: State<'_, SerialConnectionState>, +) -> Result { + let port_name = port.trim().to_string(); + if port_name.is_empty() { + return Err(SerialError::InvalidConfig); + } + + // 检查是否有活跃的标定会话 + { + let calibration_session = state + .calibration_session + .lock() + .map_err(|_| SerialError::StateError)?; + if calibration_session.is_some() { + return Err(SerialError::AlreadyConnected); + } + } + + // 创建新的标定会话 + let mut session = CalibrationSession::new(target_frames, max_rounds); + session.start(); + + let cancel = CancellationToken::new(); + let session_started_at = Instant::now(); + + let task_cancel = cancel.clone(); + let task_app = app.clone(); + // let task_port_name = port_name.clone(); + let progress = session.get_progress(); + let session_for_state = session.clone(); + let port = tokio_serial::new(&port_name, 921600) + .open_native_async() + .map_err(|_| SerialError::OpenError)?; + + let _ = tauri::async_runtime::spawn(async move { + // 这里调用新的标定处理函数 + if let Err(error) = run_serial_with_calibration( + task_app.clone(), + port, + session_started_at, + task_cancel, + session, + ) + .await + { + eprintln!("标定任务异常退出: {error}"); + } + }); + + // 保存标定会话状态 + let mut calibration_session = state + .calibration_session + .lock() + .map_err(|_| SerialError::StateError)?; + *calibration_session = Some(session_for_state); + + Ok(CalibrationResponse { + success: true, + message: "标定已开始".to_string(), + progress: Some(progress), + }) +} + +#[tauri::command] +pub async fn serial_calibrate_add_weight( + state: State<'_, SerialConnectionState>, +) -> Result { + let mut calibration_session = state + .calibration_session + .lock() + .map_err(|_| SerialError::StateError)?; + + if let Some(session) = calibration_session.as_mut() { + match session.weight_added() { + Ok(_) => Ok(CalibrationResponse { + success: true, + message: "配重已添加,继续标定".to_string(), + progress: Some(session.get_progress()), + }), + Err(e) => Err(SerialError::StateError), + } + } else { + Err(SerialError::StateError) + } +} + +#[tauri::command] +pub fn serial_calibrate_status( + state: State<'_, SerialConnectionState>, +) -> Result { + let calibration_session = state + .calibration_session + .lock() + .map_err(|_| SerialError::StateError)?; + + if let Some(session) = calibration_session.as_ref() { + Ok(CalibrationResponse { + success: true, + message: "标定状态".to_string(), + progress: Some(session.get_progress()), + }) + } else { + Ok(CalibrationResponse { + success: false, + message: "没有活跃的标定会话".to_string(), + progress: None, + }) + } +} diff --git a/src-tauri/src/commands/mod.rs b/src-tauri/src/commands/mod.rs index b5e49fa..a4d46c9 100644 --- a/src-tauri/src/commands/mod.rs +++ b/src-tauri/src/commands/mod.rs @@ -1,3 +1,4 @@ +pub mod calibration; pub mod file_explorer; pub mod serial; pub mod window; diff --git a/src-tauri/src/commands/serial.rs b/src-tauri/src/commands/serial.rs index 2441060..77d0dda 100644 --- a/src-tauri/src/commands/serial.rs +++ b/src-tauri/src/commands/serial.rs @@ -1,3 +1,4 @@ +use crate::serial_core::calibration_session::CalibrationSession; use crate::serial_core::codecs::tactile_a::{ export_recording_csv, TactileACodec, TactileACsvImporter, TactileAHandler, }; @@ -23,7 +24,6 @@ const DEFAULT_TACTILE_REPLY_TIMEOUT_MS: u64 = 140; type SharedTactileRecording = Arc>; - #[derive(Serialize)] #[serde(rename_all = "camelCase")] pub struct SerialConnectResponse { @@ -74,7 +74,8 @@ struct SerialSession { #[derive(Default)] pub struct SerialConnectionState { session: Mutex>, - last_record: Mutex> + last_record: Mutex>, + pub calibration_session: Mutex>, } #[tauri::command] @@ -176,7 +177,7 @@ pub async fn serial_connect( port: port_name.clone(), cancel, task, - current_record + current_record, }); Ok(SerialConnectResponse { @@ -190,6 +191,24 @@ pub async fn serial_connect( pub async fn serial_disconnect( state: State<'_, SerialConnectionState>, ) -> Result { + let Some(port) = disconnect_active_session(state.inner()).await? else { + return Ok(SerialConnectResponse { + port: String::new(), + connected: false, + message: "already disconnected".to_string(), + }); + }; + + Ok(SerialConnectResponse { + port, + connected: false, + message: "disconnected".to_string(), + }) +} + +pub(crate) async fn disconnect_active_session( + state: &SerialConnectionState, +) -> Result, SerialError> { let session = { let mut guard = state.session.lock().map_err(|_| SerialError::StateError)?; guard.take() @@ -202,18 +221,15 @@ pub async fn serial_disconnect( current_record, }) = session else { - return Ok(SerialConnectResponse { - port: String::new(), - connected: false, - message: "already disconnected".to_string(), - }); + return Ok(None); }; cancel.cancel(); let _ = task.await; - let frame_count = current_record.lock().map(|record| { - record.frames.len() - }).unwrap_or(0); + let frame_count = current_record + .lock() + .map(|record| record.frames.len()) + .unwrap_or(0); info!("last_record has {} frames", frame_count); @@ -221,12 +237,7 @@ pub async fn serial_disconnect( *last_record = Some(current_record); } - - Ok(SerialConnectResponse { - port, - connected: false, - message: "disconnected".to_string(), - }) + Ok(Some(port)) } #[tauri::command] @@ -290,7 +301,10 @@ pub fn serial_export_csv_to_path( } #[tauri::command] -pub fn serial_import_csv(file_name: String, csv_content: String) -> Result { +pub fn serial_import_csv( + file_name: String, + csv_content: String, +) -> Result { let mut importer = TactileACsvImporter::new(file_name.as_str()); let packets = importer .load(Cursor::new(csv_content.into_bytes())) @@ -347,7 +361,10 @@ fn resolve_record_for_export( return Ok(recording); } - let last_record = state.last_record.lock().map_err(|_| SerialError::StateError)?; + let last_record = state + .last_record + .lock() + .map_err(|_| SerialError::StateError)?; last_record.clone().ok_or(SerialError::NoRecordedData) } @@ -368,7 +385,10 @@ fn snapshot_record_frame_count( .map_err(|_| SerialError::StateError); } - let last_record = state.last_record.lock().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); }; diff --git a/src-tauri/src/commands/window.rs b/src-tauri/src/commands/window.rs index 8b40618..8e4ca13 100644 --- a/src-tauri/src/commands/window.rs +++ b/src-tauri/src/commands/window.rs @@ -1,4 +1,5 @@ -use tauri::{AppHandle, Manager, WebviewWindow}; +use crate::commands::serial::{disconnect_active_session, SerialConnectionState}; +use tauri::{AppHandle, Manager, State, WebviewWindow}; fn main_window(app: &AppHandle) -> Result { app.get_webview_window("main") @@ -25,8 +26,11 @@ pub fn win_toggle_maximize(app: AppHandle) -> Result<(), String> { } #[tauri::command] -pub fn win_close(app: AppHandle) -> Result<(), String> { - main_window(&app)? - .close() - .map_err(|error| error.to_string()) +pub async fn win_close(app: AppHandle, state: State<'_, SerialConnectionState>) -> Result<(), String> { + disconnect_active_session(state.inner()) + .await + .map_err(|error| error.to_string())?; + + app.exit(0); + Ok(()) } diff --git a/src-tauri/src/lib.rs b/src-tauri/src/lib.rs index ffd5864..f6d5ef5 100644 --- a/src-tauri/src/lib.rs +++ b/src-tauri/src/lib.rs @@ -18,6 +18,9 @@ pub fn run() { commands::serial::serial_export_csv_to_path, commands::serial::serial_import_csv, commands::serial::serial_import_csv_from_path, + commands::calibration::serial_calibrate_with_coarse, + commands::calibration::serial_calibrate_add_weight, + commands::calibration::serial_calibrate_status, commands::window::win_minimize, commands::window::win_toggle_maximize, commands::window::win_close diff --git a/src-tauri/src/log.rs b/src-tauri/src/log.rs index 3ffb5be..e4a4fa5 100644 --- a/src-tauri/src/log.rs +++ b/src-tauri/src/log.rs @@ -1,12 +1,40 @@ use fern::{Dispatch, colors::{Color, ColoredLevelConfig}}; use log::{debug}; +use std::fs; +use std::path::PathBuf; use std::time::SystemTime; + +fn resolve_log_dir() -> PathBuf { + if let Some(override_dir) = std::env::var_os("JE_SKIN_LOG_DIR") { + return PathBuf::from(override_dir); + } + + if cfg!(target_os = "windows") { + if let Some(local_app_data) = std::env::var_os("LOCALAPPDATA") { + return PathBuf::from(local_app_data).join("JE-Skin").join("logs"); + } + } + + std::env::temp_dir().join("JE-Skin").join("logs") +} + +fn ensure_log_dir() -> PathBuf { + let preferred = resolve_log_dir(); + if fs::create_dir_all(&preferred).is_ok() { + return preferred; + } + + let fallback = std::env::temp_dir().join("JE-Skin").join("logs"); + let _ = fs::create_dir_all(&fallback); + fallback +} + pub fn setup_logger() { let colors_line = ColoredLevelConfig::new() .error(Color::Red) .warn(Color::Yellow) .info(Color::Green) - .debug(Color::White) + .debug(Color::BrightBlue) .trace(Color::BrightBlack); let colors_level = colors_line.info(Color::Green); @@ -38,6 +66,9 @@ pub fn setup_logger() { // .apply() // .unwrap(); + let log_dir = ensure_log_dir(); + let log_file_base = log_dir.join("program.log"); + let file_config = fern::Dispatch::new() .format(move |out, message, record| { out.finish( @@ -51,7 +82,7 @@ pub fn setup_logger() { ); }) .level(level) - .chain(fern::DateBased::new("program.log", "%Y-%m-%d")); + .chain(fern::DateBased::new(log_file_base, "%Y-%m-%d")); Dispatch::new() .level(log::LevelFilter::Debug) diff --git a/src-tauri/src/serial_core/calibration_session.rs b/src-tauri/src/serial_core/calibration_session.rs new file mode 100644 index 0000000..04beb75 --- /dev/null +++ b/src-tauri/src/serial_core/calibration_session.rs @@ -0,0 +1,109 @@ +use crate::serial_core::frame::TactileAFrame; +use crate::serial_core::record::{RecordedFrame, Recording}; +use serde::Serialize; +use std::sync::{Arc, Mutex}; + +#[derive(Debug, Clone, Serialize, PartialEq)] +#[serde(rename_all = "camelCase")] +pub enum CalibrationState { + Idle, + CollectingData, + ExportingData, + WaitingForWeight, + Completed, +} + +#[derive(Debug, Clone, Serialize)] +#[serde(rename_all = "camelCase")] +pub struct CalibrationSession { + pub state: CalibrationState, + pub target_frame: usize, + pub collected_frames: usize, + pub current_round: usize, + pub max_rounds: usize, + pub data: Vec>, +} + +impl CalibrationSession { + pub fn new(targt_frame: usize, max_round: usize) -> Self { + Self { + state: CalibrationState::Idle, + target_frame: targt_frame, + collected_frames: 0, + current_round: 1, + max_rounds: max_round, + data: Vec::new(), + } + } + + pub fn start(&mut self) { + self.state = CalibrationState::CollectingData; + self.collected_frames = 0; + self.data.clear(); + println!( + "标定第 {} 轮开始,目标收集 {} 个有效帧", + self.current_round, self.target_frame + ); + } + + pub fn add_frame(&mut self, frame: RecordedFrame) -> bool { + if self.state != CalibrationState::CollectingData { + return false; + } + + self.data.push(frame); + self.collected_frames += 1; + + if self.collected_frames >= self.target_frame { + self.state = CalibrationState::ExportingData; + return true; + } + + return false; + } + + pub fn export_completed(&mut self) { + self.state = CalibrationState::WaitingForWeight; + println!("请修改配重,继续标定"); + } + + pub fn weight_added(&mut self) -> Result<(), String> { + if self.current_round >= self.max_rounds { + self.state = CalibrationState::Completed; + println!("标定完成,共 {} 轮", self.current_round); + } else { + self.current_round += 1; + self.start(); + } + + Ok(()) + } + + pub fn get_progress(&self) -> CalibrationProgress { + CalibrationProgress { + state: self.state.clone(), + current_round: self.current_round, + max_rounds: self.max_rounds, + collected_frames: self.collected_frames, + target_frames: self.target_frame, + progress_percentage: if self.target_frame > 0 { + (self.collected_frames as f32 / self.target_frame as f32) * 100.0 + } else { + 0.0 + }, + } + } +} + +#[derive(Debug, Clone, Serialize)] +#[serde(rename_all = "camelCase")] +pub struct CalibrationProgress { + pub state: CalibrationState, + pub current_round: usize, + pub max_rounds: usize, + pub collected_frames: usize, + pub target_frames: usize, + pub progress_percentage: f32, +} + +pub type SharedCalibrationSession = Arc>>; diff --git a/src-tauri/src/serial_core/codecs/mod.rs b/src-tauri/src/serial_core/codecs/mod.rs index d4b0944..369948c 100644 --- a/src-tauri/src/serial_core/codecs/mod.rs +++ b/src-tauri/src/serial_core/codecs/mod.rs @@ -1,5 +1,6 @@ use crate::serial_core::{frame::TestFrame, record::Recording}; -pub mod test; + pub mod tactile_a; -pub type TestRecording = Recording; \ No newline at end of file +pub mod test; +pub type TestRecording = Recording; diff --git a/src-tauri/src/serial_core/codecs/tactile_a.rs b/src-tauri/src/serial_core/codecs/tactile_a.rs index 1c0e9f3..9f6f6f9 100644 --- a/src-tauri/src/serial_core/codecs/tactile_a.rs +++ b/src-tauri/src/serial_core/codecs/tactile_a.rs @@ -8,13 +8,15 @@ use crate::serial_core::{ codec::Codec, frame::{TactileAFrame, TactileAFrameStatusCode}, }; +use anyhow::anyhow; use async_trait::async_trait; use csv::StringRecord; -use anyhow::anyhow; -use std::io::Read; use log::debug; +use std::io::Read; +use std::os::raw; const FRAME_BUFFER_MIN_LENGTH: usize = 15; +const IGNOR_RAW_DATA_VAL: i32 = 10; pub struct TactileACodec { buffer: Vec, @@ -24,6 +26,7 @@ pub struct TactileACodec { pub struct TactileACsvExporter { channels: usize, + limit: Option, } pub struct TactileACsvImporter { @@ -77,7 +80,14 @@ impl TactileACodec { let vals: Vec = data .chunks_exact(2) - .map(|chunk| u16::from_le_bytes([chunk[0], chunk[1]]) as i32) + .map(|chunk| { + let mut raw_val = u16::from_le_bytes([chunk[0], chunk[1]]) as i32; + println!("raw_val: {}", raw_val); + if raw_val < IGNOR_RAW_DATA_VAL { + raw_val = 0; + } + raw_val + }) .collect::>(); Ok(vals) @@ -216,16 +226,15 @@ impl Codec for TactileACodec { 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()); + 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) - } + _ => Err(CodecError::InvalidFrameType), } } } @@ -245,8 +254,18 @@ impl FrameHandler for TactileAHandler { } impl TactileACsvExporter { - fn new(channels: usize) -> Self { - TactileACsvExporter { channels } + pub fn new(channels: usize) -> Self { + TactileACsvExporter { + channels, + limit: None, + } + } + + pub fn with_coarse_calibration(channels: usize, li: i32) -> Self { + TactileACsvExporter { + channels, + limit: Some(li), + } } } @@ -265,11 +284,21 @@ impl CsvExporter for TactileACsvExporter { fn csv_row( &self, item: &RecordedFrame, - ) -> anyhow::Result> { + ) -> anyhow::Result>> { let packet = TactileADataPacket::try_from(&item.frame)?; - let mut row: Vec = packet.data.iter().map(|x| x.to_string()).collect(); - row.push(packet.dts_ms.to_string()); - Ok(row) + if let Some(li) = self.limit { + if li > packet.data.iter().sum() { + Ok(None) + } else { + let mut row: Vec = packet.data.iter().map(|x| x.to_string()).collect(); + row.push(packet.dts_ms.to_string()); + Ok(Some(row)) + } + } else { + let mut row: Vec = packet.data.iter().map(|x| x.to_string()).collect(); + row.push(packet.dts_ms.to_string()); + Ok(Some(row)) + } } } @@ -286,19 +315,28 @@ impl CsvExporter for TactileACsvExporter { header } - fn csv_row( - &self, - item: &RecordedFrame, - ) -> anyhow::Result> { + fn csv_row(&self, item: &RecordedFrame) -> anyhow::Result>> { let rep = match &item.frame { TactileAFrame::Rep(rep) => rep, - TactileAFrame::Req(_) => return Err(anyhow!("request frame cannot be exported to csv row")), + TactileAFrame::Req(_) => { + return Err(anyhow!("request frame cannot be exported to csv row")) + } }; let packet = TactileADataPacket::try_from(rep)?; - let mut row: Vec = packet.data.iter().map(|x| x.to_string()).collect(); - row.push(packet.dts_ms.to_string()); - Ok(row) + if let Some(li) = self.limit { + if li > packet.data.iter().sum() { + Ok(None) + } else { + let mut row: Vec = packet.data.iter().map(|x| x.to_string()).collect(); + row.push(packet.dts_ms.to_string()); + Ok(Some(row)) + } + } else { + let mut row: Vec = packet.data.iter().map(|x| x.to_string()).collect(); + row.push(packet.dts_ms.to_string()); + Ok(Some(row)) + } } } @@ -322,7 +360,9 @@ impl TactileACsvImporter { 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"))?; + let cell = record + .get(index) + .ok_or_else(|| anyhow!("missing channel cell"))?; data.push(cell.parse::()?); } @@ -357,7 +397,10 @@ impl CsvImporter for TactileACsvImporter { } } -pub fn export_recording_csv(recording: &Recording, writer: W) -> anyhow::Result<()> +pub fn export_recording_csv( + recording: &Recording, + writer: W, +) -> anyhow::Result<()> where W: std::io::Write, { diff --git a/src-tauri/src/serial_core/codecs/test.rs b/src-tauri/src/serial_core/codecs/test.rs index ad4fc60..07ac193 100644 --- a/src-tauri/src/serial_core/codecs/test.rs +++ b/src-tauri/src/serial_core/codecs/test.rs @@ -1,15 +1,12 @@ -use std::io::Read; -use std::time::Instant; -use crate::serial_core::frame::{FrameHandler}; +use crate::serial_core::frame::FrameHandler; +use crate::serial_core::record::{write_csv, CsvExporter, CsvImporter, RecordedFrame, Recording}; +use crate::serial_core::utils::{elapsed_millis, usize_to_u16_be_bytes}; 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 -}; +use std::io::Read; +use std::time::Instant; pub struct TestCodec { buffer: Vec, } @@ -23,7 +20,11 @@ impl TestCodec { } impl Codec for TestCodec { - fn decode(&mut self, input: &[u8], session_started_at: Instant) -> Result, CodecError> { + fn decode( + &mut self, + input: &[u8], + session_started_at: Instant, + ) -> Result, CodecError> { self.buffer.extend_from_slice(input); let mut frames = Vec::new(); @@ -126,7 +127,7 @@ pub struct TestCsvImporter { #[derive(Clone)] pub struct TestDataPacket { pub data: Vec, - pub dts_ms: u64 + pub dts_ms: u64, } impl TryFrom<&TestFrame> for TestDataPacket { @@ -134,7 +135,10 @@ impl TryFrom<&TestFrame> for TestDataPacket { fn try_from(frame: &TestFrame) -> Result { let data = parse_data_frame(&frame.payload)?; let dts = frame.dts_ms; - Ok(TestDataPacket { data: data, dts_ms: dts }) + Ok(TestDataPacket { + data: data, + dts_ms: dts, + }) } } // impl From for TestDataPacket { @@ -145,14 +149,17 @@ impl TryFrom<&TestFrame> for TestDataPacket { // } // } - impl CsvExporter for TestCsvExporter { type Error = CodecError; fn csv_header(&self, recording: &Recording) -> Vec { let channel_nb = recording .frames .iter() - .find_map(|frame| parse_data_frame(&frame.frame.payload).ok().map(|vals| vals.len())) + .find_map(|frame| { + parse_data_frame(&frame.frame.payload) + .ok() + .map(|vals| vals.len()) + }) .unwrap_or(0); let mut header: Vec = Vec::new(); for i in 0..channel_nb { @@ -163,11 +170,11 @@ impl CsvExporter for TestCsvExporter { header } - fn csv_row(&self, item: &RecordedFrame) -> anyhow::Result> { + fn csv_row(&self, item: &RecordedFrame) -> anyhow::Result>> { let packet: TestDataPacket = TestDataPacket::try_from(&item.frame)?; let mut row: Vec = packet.data.iter().map(|&x| x.to_string()).collect(); row.push(packet.dts_ms.to_string()); - Ok(row) + Ok(Some(row)) } } @@ -180,7 +187,7 @@ impl TestCsvImporter { } } - fn parse_record(&mut self, record: StringRecord) -> anyhow::Result{ + fn parse_record(&mut self, record: StringRecord) -> anyhow::Result { if self.channels == 0 { return Err(anyhow!("csv header is missing channel columns")); } @@ -191,7 +198,9 @@ impl TestCsvImporter { 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"))?; + let cell = record + .get(index) + .ok_or_else(|| anyhow!("missing channel cell"))?; data.push(cell.parse::()?); } @@ -226,7 +235,6 @@ impl CsvImporter for TestCsvImporter { } } - pub fn export_recording_csv(recording: &Recording, writer: W) -> anyhow::Result<()> where W: std::io::Write, diff --git a/src-tauri/src/serial_core/frame.rs b/src-tauri/src/serial_core/frame.rs index 42d23a6..610a051 100644 --- a/src-tauri/src/serial_core/frame.rs +++ b/src-tauri/src/serial_core/frame.rs @@ -1,16 +1,17 @@ use anyhow::Result; use async_trait::async_trait; -#[derive(Debug, Clone, PartialEq, Eq)] +use serde::Serialize; +#[derive(Debug, Clone, PartialEq, Eq, Serialize)] pub struct TestFrame { pub header: [u8; 2], pub cmd: u8, pub length: usize, pub payload: Vec, pub checksum: u8, - pub dts_ms: u64 + pub dts_ms: u64, } -#[derive(Debug, Clone, PartialEq, Eq)] +#[derive(Debug, Clone, PartialEq, Eq, Serialize)] pub struct TactileAFrameMetaData { pub header: [u8; 2], pub payload_len: usize, @@ -25,33 +26,37 @@ pub struct TactileAFrameMetaData { // pub dts_ms: u64, } -#[derive(Debug, Clone, PartialEq, Eq)] +#[derive(Debug, Clone, PartialEq, Eq, Serialize)] pub struct TactileAReqFrame { pub meta: TactileAFrameMetaData, } -#[derive(Debug, Clone, PartialEq, Eq)] +#[derive(Debug, Clone, PartialEq, Eq, Serialize)] pub struct TactileARepFrame { pub meta: TactileAFrameMetaData, pub status: TactileAFrameStatusCode, pub payload: Vec, - pub dts_ms: u64 + pub dts_ms: u64, } -#[derive(Debug, Clone, PartialEq, Eq)] +#[derive(Debug, Clone, PartialEq, Eq, Serialize)] pub enum TactileAFrameStatusCode { Success, - Failure + Failure, } -#[derive(Debug, Clone, PartialEq, Eq)] +#[derive(Debug, Clone, PartialEq, Eq, Serialize)] pub enum TactileAFrame { Req(TactileAReqFrame), - Rep(TactileARepFrame) + Rep(TactileARepFrame), } +// TODO: filter +// pub trait FrameFilter { +// fn apply(&self) +// } + #[async_trait] pub trait FrameHandler: Send { async fn on_frame(&mut self, frame: &F) -> Result>>; } - diff --git a/src-tauri/src/serial_core/mod.rs b/src-tauri/src/serial_core/mod.rs index a3adf2c..b27817c 100644 --- a/src-tauri/src/serial_core/mod.rs +++ b/src-tauri/src/serial_core/mod.rs @@ -3,15 +3,15 @@ use crate::serial_core::{ record::Recording, }; +pub mod calibration_session; pub mod codec; pub mod codecs; pub mod error; pub mod frame; pub mod model; -pub mod serial; pub mod record; +pub mod serial; pub mod utils; - pub type TestRecording = Recording; pub type TactileARecording = Recording; diff --git a/src-tauri/src/serial_core/record.rs b/src-tauri/src/serial_core/record.rs index 7a20d35..e57a821 100644 --- a/src-tauri/src/serial_core/record.rs +++ b/src-tauri/src/serial_core/record.rs @@ -1,31 +1,49 @@ -#[derive(Clone)] +use serde::Serialize; + +#[derive(Clone, Serialize, Debug)] pub struct FrameTiming { pub pts_ms: Option, pub dts_ms: u64, } -#[derive(Clone)] +#[derive(Clone, Serialize, Debug)] pub struct RecordedFrame { pub timing: FrameTiming, - pub frame: F + pub frame: F, } #[derive(Clone, Default)] pub struct Recording { - pub frames: Vec> + pub frames: Vec>, + pub count: usize, + pub except_count: Option, } impl Recording { - pub fn new() -> Recording { Self { frames: Vec::new() } } + pub fn new() -> Recording { + Self { + frames: Vec::new(), + count: 0, + except_count: None, + } + } + pub fn with_except_count(except_count: usize) -> Recording { + Self { + frames: Vec::new(), + count: 0, + except_count: Some(except_count), + } + } pub fn push(&mut self, ite: RecordedFrame) { self.frames.push(ite); } + pub fn check_frame_need_record(ite: RecordedFrame) {} } pub trait CsvExporter { type Error: std::error::Error + Send + Sync + 'static; fn csv_header(&self, recording: &Recording) -> Vec; - fn csv_row(&self, item: &RecordedFrame) -> anyhow::Result>; + fn csv_row(&self, item: &RecordedFrame) -> anyhow::Result>>; } // TODO: CsvImporter @@ -33,11 +51,7 @@ pub trait CsvImporter

{ fn load(&mut self, reader: R) -> anyhow::Result>; } -pub fn write_csv( - recording: &Recording, - exporter: &E, - writer: W, -) -> anyhow::Result<()> +pub fn write_csv(recording: &Recording, exporter: &E, writer: W) -> anyhow::Result<()> where E: CsvExporter, W: std::io::Write, @@ -46,8 +60,9 @@ where let mut wrt = csv::Writer::from_writer(writer); wrt.write_record(header)?; for f in &recording.frames { - let row = exporter.csv_row(f)?; - wrt.write_record(&row)?; + if let Some(row) = exporter.csv_row(f)? { + wrt.write_record(&row)?; + } } wrt.flush()?; diff --git a/src-tauri/src/serial_core/serial.rs b/src-tauri/src/serial_core/serial.rs index 8308d90..3f54395 100644 --- a/src-tauri/src/serial_core/serial.rs +++ b/src-tauri/src/serial_core/serial.rs @@ -1,23 +1,29 @@ +use crate::serial_core::calibration_session::*; use crate::serial_core::codec::Codec; use crate::serial_core::codecs::tactile_a::TactileACodec; use crate::serial_core::frame::{FrameHandler, TactileAFrame, TestFrame}; use crate::serial_core::model::{HudChartState, HudPacket}; use crate::serial_core::record::Recording; +use crate::serial_core::record::{FrameTiming, RecordedFrame}; use anyhow::Result; +use log::{debug, info}; +use std::fs::File; +use std::future::pending; +use std::sync::{Arc, Mutex}; +use std::time::Instant; use tauri::{AppHandle, Emitter}; use tokio::io::{AsyncReadExt, AsyncWriteExt}; use tokio::time::{self, Duration, MissedTickBehavior}; use tokio_serial::SerialStream; use tokio_util::sync::CancellationToken; -use std::future::pending; -use std::sync::{Arc, Mutex}; -use std::time::Instant; -use log::{info, debug}; -use crate::serial_core::record::{FrameTiming, RecordedFrame}; - +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; +use crate::serial_core::codecs::tactile_a::TactileAHandler; pub enum PollMode { Disable, - Enabled(Box>) + Enabled(Box>), } pub trait SerialFrame: Clone + Send + 'static { @@ -169,11 +175,19 @@ where F: SerialFrame, C: Codec + Send + 'static, H: FrameHandler + Send + 'static, - T: Into + T: Into, { run_serial_with_poll( - app, port, codec, handler, session_started_at, recording, cancel, PollMode::Disable - ).await + app, + port, + codec, + handler, + session_started_at, + recording, + cancel, + PollMode::Disable, + ) + .await } pub async fn run_serial_with_poll( @@ -184,7 +198,7 @@ pub async fn run_serial_with_poll( session_started_at: Instant, recording: Arc>>, cancel: CancellationToken, - poll_mode: PollMode + poll_mode: PollMode, ) -> Result<()> where F: SerialFrame, @@ -192,15 +206,13 @@ where H: FrameHandler + Send + 'static, T: Into, { + info!("run_serial_with_poll"); 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 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 @@ -211,7 +223,6 @@ where let mut prune_interval = time::interval(Duration::from_millis(450)); prune_interval.set_missed_tick_behavior(MissedTickBehavior::Delay); - loop { tokio::select! { _ = cancel.cancelled() => break, @@ -227,6 +238,7 @@ where if r.should_request() { if let Some(req) = r.next_request()? { let bytes = codec.encode(&req)?; + debug!("send {:02X?}", bytes); port.write_all(&bytes).await?; } } @@ -281,3 +293,155 @@ where } Ok(()) } + +// 在 src-tauri/src/serial_core/serial.rs 中添加 +pub async fn run_serial_with_calibration( + app: AppHandle, + mut port: SerialStream, + session_started_at: Instant, + cancel: CancellationToken, + mut calibration_session: CalibrationSession, +) -> Result<()> { + let mut codec = TactileACodec::new(DEFAULT_TACTILE_COLS, DEFAULT_TACTILE_ROWS); + let mut handler = TactileAHandler; + let mut requester = TactileAPollRequester::new( + Duration::from_millis(DEFAULT_TACTILE_POLL_INTERVAL_MS), + DEFAULT_TACTILE_COLS, + DEFAULT_TACTILE_ROWS, + Duration::from_millis(DEFAULT_TACTILE_REPLY_TIMEOUT_MS), + ); + + let mut poll_interval = time::interval(Duration::from_millis(DEFAULT_TACTILE_POLL_INTERVAL_MS)); + poll_interval.set_missed_tick_behavior(MissedTickBehavior::Skip); + + let mut buffer = [0u8; 1024]; + let recording = Arc::new(Mutex::new(Recording::new())); + let mut chart_state = HudChartState::new(); + let mut prune_interval = time::interval(Duration::from_millis(450)); + prune_interval.set_missed_tick_behavior(MissedTickBehavior::Delay); + + loop { + tokio::select! { + _ = cancel.cancelled() => break, + _ = poll_interval.tick() => { + if requester.should_request() { + if let Some(req) = requester.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)?; + } + } + read_result = port.read(&mut buffer) => { + let n = read_result?; + if n == 0 { + tokio::task::yield_now().await; + continue; + } + + let frames = codec.decode(&buffer[..n], session_started_at)?; + for frame in frames { + requester.on_rx_frame(&frame); + + let decode_res = handler + .on_frame(&frame) + .await? + .map(|vals| vals.into_iter().map(Into::into).collect::>()); + + let recorded_frame = RecordedFrame { + timing: FrameTiming { pts_ms: None, dts_ms: frame.dts_ms() }, + frame: frame.clone(), + }; + + { + let mut record = recording + .lock() + .map_err(|_| anyhow::anyhow!("recording state poisoned"))?; + record.push(recorded_frame.clone()); + } + + let display_values = if let Some(vals) = decode_res.as_ref() { + let summary = vals.iter().copied().sum::(); + chart_state.record_summary(summary as f32); + chart_state.record_pressure_matrix(vals.as_slice()); + Some(vec![summary]) + } else { + None + }; + + if let Some(packet) = frame.to_hud_packet(&mut chart_state, display_values.as_deref()) { + app.emit("hud_stream", packet)?; + } + + // 检查是否达到目标帧数 + let should_export = calibration_session.add_frame(recorded_frame); + + if should_export { + // 导出数据 + export_calibration_data(&app, &calibration_session, &recording).await?; + + // 发送语音提示(这里用事件代替,前端可以播放语音) + app.emit("calibration_voice_prompt", "请添加配重")?; + + // 更新状态 + calibration_session.export_completed(); + + if let Ok(mut record) = recording.lock() { + record.frames.clear(); + } + } + } + } + } + } + + Ok(()) +} +use crate::serial_core::codecs::tactile_a::TactileACsvExporter; +use crate::serial_core::record::write_csv; +use std::time::{SystemTime, UNIX_EPOCH}; +use tauri::Manager; +async fn export_calibration_data( + app: &AppHandle, + calibration_session: &CalibrationSession, + recording: &Arc>>, +) -> Result<()> { + let timestamp = SystemTime::now() + .duration_since(UNIX_EPOCH) + .map(|duration| duration.as_millis()) + .unwrap_or_default(); + + let filename = format!( + "calibration_round{}_{}.csv", + calibration_session.current_round, timestamp + ); + + // 创建导出目录 + let mut output_dir = match app.path().desktop_dir() { + Ok(path) => path, + Err(_) => std::env::current_dir()?, + }; + output_dir.push("calibration_data"); + std::fs::create_dir_all(&output_dir)?; + + let output_path = output_dir.join(&filename); + let file = File::create(&output_path)?; + + // 使用现有的导出逻辑 + let recording_lock = recording + .lock() + .map_err(|_| anyhow::anyhow!("Recording poisoned"))?; + let exporter = TactileACsvExporter::with_coarse_calibration( + DEFAULT_TACTILE_COLS * DEFAULT_TACTILE_ROWS, + 7 * 12 * 10, + ); + + write_csv(&recording_lock, &exporter, file)?; + + info!("标定数据已导出到: {}", output_path.display()); + Ok(()) +} diff --git a/src/app.html b/src/app.html index 92e7e33..64c806e 100644 --- a/src/app.html +++ b/src/app.html @@ -2,7 +2,7 @@ - + Tauri + SvelteKit + Typescript App %sveltekit.head% diff --git a/src/lib/components/CenterStage.svelte b/src/lib/components/CenterStage.svelte index 5a316a1..20794ec 100644 --- a/src/lib/components/CenterStage.svelte +++ b/src/lib/components/CenterStage.svelte @@ -1,183 +1,274 @@

@@ -187,7 +278,7 @@ bind:this={stagePlaneEl} style="--panel-zone-top-dyn: {panelZoneTopPx}px; --rail-scale-left: {leftRailScale}; --rail-scale-right: {rightRailScale};" > - {#if !showPrecisionTestPanel} + {#if !showPrecisionTestPanel && !showCalibrationPanel}

WebGL2 Stage

@@ -234,6 +325,89 @@ />
+ {:else if showCalibrationPanel} +
+
+
+

{splitMatrixTitle}

+ {splitMatrixHint} +
+
+ {#key `${matrixRows}x${matrixCols}:${colorMapPreset}:calibration-split`} + + {/key} +
+
+ +
+
+
+

{locale === "zh-CN" ? "校准控制" : "Calibration Control"}

+ {calibrationPanelHint} +
+ +
+
+
+

{calibrationMethodLabel}

+
+ {#each calibrationMethodOptions as method (method.id)} +
+
+

{method.label}

+

{method.description}

+
+ +
+ + + handleCalibrationRoundsInput(event, method.id)} + on:input={(event) => + handleCalibrationRoundsInput(event, method.id)} + /> +
+ + +
+ {/each} +
+
+
+
+
{:else}
{#key `${matrixRows}x${matrixCols}:${colorMapPreset}`} @@ -250,149 +424,248 @@
{/if} - {#if showConfigPanel && !showPrecisionTestPanel} -
- dispatch("configclose")} - /> + {#if showConfigPanel && !showPrecisionTestPanel && !showCalibrationPanel} +
+ dispatch("configclose")} + /> +
+ {/if} + + {#if !showPrecisionTestPanel && !showCalibrationPanel} +
+ + + +
+ {/if} + + {#if replayHasData && !showPrecisionTestPanel && !showCalibrationPanel} + + {/if} + + {#if !showPrecisionTestPanel && !showCalibrationPanel} +
+ +
+ {/if}
- {/if} - - {#if !showPrecisionTestPanel} -
- - - -
- {/if} - - {#if replayHasData && !showPrecisionTestPanel} - - {/if} - - {#if !showPrecisionTestPanel} -
- -
- {/if} - - + diff --git a/src/routes/+page.svelte b/src/routes/+page.svelte index f3c47a0..686d5e8 100644 --- a/src/routes/+page.svelte +++ b/src/routes/+page.svelte @@ -40,6 +40,18 @@ dtsMs: number; } + type CalibrationMethodId = "coarse"; + + interface CalibrationStartPayload { + methodId: CalibrationMethodId; + rounds: number; + } + + interface CalibrationInvokeResult { + success: boolean; + message: string; + } + const copyByLocale: Record = { "zh-CN": { appName: "JE-Skin", @@ -159,6 +171,7 @@ const summaryPointsPerSeries = 42; const signalRenderTickMs = 1200; const replayDefaultFrameMs = 40; + const defaultCalibrationTargetFrames = 100; const showSignalPanels = false; const mockToneCycle: SignalTone[] = ["cyan", "lime", "orange", "violet", "gold", "rose"]; @@ -205,6 +218,7 @@ let activeConfigLinkId = "stream-on"; let isConfigPanelOpen = false; let isPrecisionTestOpen = false; + let isCalibrationTestOpen = false; let hasSignalData = false; let signalPanels: HudSignalPanel[] = buildInactivePanels(); let summary: HudSummary = buildEmptySummary(); @@ -233,7 +247,7 @@ let fileExplorerFileName = ""; $: uiCopy = copyByLocale[locale]; - $: configLinks = buildConfigLinks(locale, activeConfigLinkId, isConfigPanelOpen, isPrecisionTestOpen); + $: configLinks = buildConfigLinks(locale, activeConfigLinkId, isConfigPanelOpen, isPrecisionTestOpen, isCalibrationTestOpen); $: stageStatusText = webglStatusTone === "ok" ? uiCopy.runtimeReady : uiCopy.runtimeFallback; $: leftSignalPanels = signalPanels.filter((panel) => panel.side === "left"); $: rightSignalPanels = signalPanels.filter((panel) => panel.side === "right"); @@ -975,7 +989,8 @@ currentLocale: LocaleCode, activeId: string, isSettingsOpen: boolean, - isPrecisionOpen: boolean + isPrecisionOpen: boolean, + isCalibrationOpen: boolean ): HudConfigLink[] { const labels = currentLocale === "zh-CN" @@ -1011,7 +1026,7 @@ id: "calibrate", label: labels.calibrate, tone: "cyan", - active: activeId === "calibrate" + active: isCalibrationOpen }, { id: "precision-test", @@ -1460,19 +1475,80 @@ if (event.detail === "precision-test") { isPrecisionTestOpen = !isPrecisionTestOpen; isConfigPanelOpen = false; + isCalibrationTestOpen = false; + return; + } + + if (event.detail === "calibrate") { + isCalibrationTestOpen = !isCalibrationTestOpen; + isConfigPanelOpen = false; + isPrecisionTestOpen = false; return; } if (event.detail === "settings") { isPrecisionTestOpen = false; + isCalibrationTestOpen = false; isConfigPanelOpen = !isConfigPanelOpen; return; } isPrecisionTestOpen = false; + isCalibrationTestOpen = false; isConfigPanelOpen = false; activeConfigLinkId = event.detail; - console.info("[hud] config link clicked:", event.detail); + } + + async function handleCalibrationStart(event: CustomEvent): Promise { + const targetRounds = clamp(Math.round(Number(event.detail.rounds) || 1), 1, 20); + + if (!isTauriRuntime()) { + connectionNotice = + locale === "zh-CN" + ? `当前运行环境不支持启动标定(目标 ${targetRounds} 轮)。` + : `Current runtime does not support calibration start (${targetRounds} rounds).`; + connectionNoticeTone = "warn"; + return; + } + + if (!serialPortValue) { + connectionNotice = + locale === "zh-CN" ? "请先选择串口,再启动标定。" : "Please select a serial port before starting calibration."; + connectionNoticeTone = "warn"; + return; + } + + if (event.detail.methodId !== "coarse") { + connectionNotice = + locale === "zh-CN" ? "当前标定方法暂未接入后端。" : "Selected calibration method is not wired to backend yet."; + connectionNoticeTone = "warn"; + return; + } + + try { + const result = await invoke("serial_calibrate_with_coarse", { + port: serialPortValue, + targetFrames: defaultCalibrationTargetFrames, + maxRounds: targetRounds + }); + + if (result.success) { + connectionNotice = + locale === "zh-CN" + ? `粗标定已启动:目标 ${targetRounds} 轮(每轮 ${defaultCalibrationTargetFrames} 帧)` + : `Coarse calibration started: ${targetRounds} rounds (${defaultCalibrationTargetFrames} frames/round)`; + connectionNoticeTone = "ok"; + } else { + connectionNotice = result.message; + connectionNoticeTone = "warn"; + } + } catch (error) { + const fallback = + locale === "zh-CN" ? "启动粗标定失败,请检查串口连接状态。" : "Failed to start coarse calibration."; + connectionNotice = normalizeInvokeError(error) || fallback; + connectionNoticeTone = "warn"; + console.error("Calibration start failed:", error); + } } async function handleWindowControl(event: CustomEvent): Promise { @@ -1623,6 +1699,7 @@ {pressureMatrix} showConfigPanel={isConfigPanelOpen} showPrecisionTestPanel={isPrecisionTestOpen} + showCalibrationPanel={isCalibrationTestOpen} {summary} on:replaytoggle={handleReplayToggle} on:replaystop={handleReplayStop} @@ -1630,8 +1707,10 @@ on:replayspeed={handleReplaySpeed} on:replayclose={handleReplayClose} on:configclose={() => (isConfigPanelOpen = false)} + on:calibrationclose={() => (isCalibrationTestOpen = false)} + on:calibrationstart={handleCalibrationStart} > - {#if !isPrecisionTestOpen} + {#if !isPrecisionTestOpen && !isCalibrationTestOpen}

Range