初步添加了标定支持,需要完善和测试

This commit is contained in:
lennlouisgeek
2026-04-07 01:46:37 +08:00
parent aeb17f194c
commit 770d713d03
19 changed files with 1599 additions and 489 deletions

View File

@@ -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<RecordedFrame<TactileAFrame>>,
}
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<TactileAFrame>) -> 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<Mutex<Option<CalibrationSession>>>;

View File

@@ -1,5 +1,6 @@
use crate::serial_core::{frame::TestFrame, record::Recording};
pub mod test;
pub mod tactile_a;
pub type TestRecording = Recording<TestFrame>;
pub mod test;
pub type TestRecording = Recording<TestFrame>;

View File

@@ -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<u8>,
@@ -24,6 +26,7 @@ pub struct TactileACodec {
pub struct TactileACsvExporter {
channels: usize,
limit: Option<i32>,
}
pub struct TactileACsvImporter {
@@ -77,7 +80,14 @@ impl TactileACodec {
let vals: Vec<i32> = 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::<Vec<i32>>();
Ok(vals)
@@ -216,16 +226,15 @@ impl Codec<TactileAFrame> 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<TactileAFrame, i32> 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<TactileARepFrame> for TactileACsvExporter {
fn csv_row(
&self,
item: &RecordedFrame<TactileARepFrame>,
) -> anyhow::Result<Vec<String>> {
) -> anyhow::Result<Option<Vec<String>>> {
let packet = TactileADataPacket::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)
if let Some(li) = self.limit {
if li > packet.data.iter().sum() {
Ok(None)
} else {
let mut row: Vec<String> = packet.data.iter().map(|x| x.to_string()).collect();
row.push(packet.dts_ms.to_string());
Ok(Some(row))
}
} else {
let mut row: Vec<String> = 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<TactileAFrame> for TactileACsvExporter {
header
}
fn csv_row(
&self,
item: &RecordedFrame<TactileAFrame>,
) -> anyhow::Result<Vec<String>> {
fn csv_row(&self, item: &RecordedFrame<TactileAFrame>) -> anyhow::Result<Option<Vec<String>>> {
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<String> = 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<String> = packet.data.iter().map(|x| x.to_string()).collect();
row.push(packet.dts_ms.to_string());
Ok(Some(row))
}
} else {
let mut row: Vec<String> = 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::<i32>()?);
}
@@ -357,7 +397,10 @@ impl CsvImporter<TactileADataPacket> for TactileACsvImporter {
}
}
pub fn export_recording_csv<W>(recording: &Recording<TactileAFrame>, writer: W) -> anyhow::Result<()>
pub fn export_recording_csv<W>(
recording: &Recording<TactileAFrame>,
writer: W,
) -> anyhow::Result<()>
where
W: std::io::Write,
{

View File

@@ -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<u8>,
}
@@ -23,7 +20,11 @@ impl TestCodec {
}
impl Codec<TestFrame> for TestCodec {
fn decode(&mut self, input: &[u8], session_started_at: Instant) -> Result<Vec<TestFrame>, CodecError> {
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();
@@ -126,7 +127,7 @@ pub struct TestCsvImporter {
#[derive(Clone)]
pub struct TestDataPacket {
pub data: Vec<i32>,
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<TestDataPacket, Self::Error> {
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<TestFrame> for TestDataPacket {
@@ -145,14 +149,17 @@ impl TryFrom<&TestFrame> for TestDataPacket {
// }
// }
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()))
.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 {
@@ -163,11 +170,11 @@ impl CsvExporter<TestFrame> for TestCsvExporter {
header
}
fn csv_row(&self, item: &RecordedFrame<TestFrame>) -> anyhow::Result<Vec<String>> {
fn csv_row(&self, item: &RecordedFrame<TestFrame>) -> anyhow::Result<Option<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)
Ok(Some(row))
}
}
@@ -180,7 +187,7 @@ impl TestCsvImporter {
}
}
fn parse_record(&mut self, record: StringRecord) -> anyhow::Result<TestDataPacket>{
fn parse_record(&mut self, record: StringRecord) -> anyhow::Result<TestDataPacket> {
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::<i32>()?);
}
@@ -226,7 +235,6 @@ impl CsvImporter<TestDataPacket> for TestCsvImporter {
}
}
pub fn export_recording_csv<W>(recording: &Recording<TestFrame>, writer: W) -> anyhow::Result<()>
where
W: std::io::Write,

View File

@@ -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<u8>,
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<u8>,
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<F> {
// fn apply(&self)
// }
#[async_trait]
pub trait FrameHandler<F, T>: Send {
async fn on_frame(&mut self, frame: &F) -> Result<Option<Vec<T>>>;
}

View File

@@ -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<TestFrame>;
pub type TactileARecording = Recording<TactileAFrame>;

View File

@@ -1,31 +1,49 @@
#[derive(Clone)]
use serde::Serialize;
#[derive(Clone, Serialize, Debug)]
pub struct FrameTiming {
pub pts_ms: Option<u64>,
pub dts_ms: u64,
}
#[derive(Clone)]
#[derive(Clone, Serialize, Debug)]
pub struct RecordedFrame<F> {
pub timing: FrameTiming,
pub frame: F
pub frame: F,
}
#[derive(Clone, Default)]
pub struct Recording<F> {
pub frames: Vec<RecordedFrame<F>>
pub frames: Vec<RecordedFrame<F>>,
pub count: usize,
pub except_count: Option<usize>,
}
impl<F> Recording<F> {
pub fn new() -> Recording<F> { Self { frames: Vec::new() } }
pub fn new() -> Recording<F> {
Self {
frames: Vec::new(),
count: 0,
except_count: None,
}
}
pub fn with_except_count(except_count: usize) -> Recording<F> {
Self {
frames: Vec::new(),
count: 0,
except_count: Some(except_count),
}
}
pub fn push(&mut self, ite: RecordedFrame<F>) {
self.frames.push(ite);
}
pub fn check_frame_need_record(ite: RecordedFrame<F>) {}
}
pub trait CsvExporter<F> {
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>>;
fn csv_row(&self, item: &RecordedFrame<F>) -> anyhow::Result<Option<Vec<String>>>;
}
// TODO: CsvImporter
@@ -33,11 +51,7 @@ pub trait CsvImporter<P> {
fn load<R: std::io::Read>(&mut self, reader: R) -> anyhow::Result<Vec<P>>;
}
pub fn write_csv<F, E, W>(
recording: &Recording<F>,
exporter: &E,
writer: W,
) -> anyhow::Result<()>
pub fn write_csv<F, E, W>(recording: &Recording<F>, exporter: &E, writer: W) -> anyhow::Result<()>
where
E: CsvExporter<F>,
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()?;

View File

@@ -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<F> {
Disable,
Enabled(Box<dyn PollRequester<F>>)
Enabled(Box<dyn PollRequester<F>>),
}
pub trait SerialFrame: Clone + Send + 'static {
@@ -169,11 +175,19 @@ where
F: SerialFrame,
C: Codec<F> + Send + 'static,
H: FrameHandler<F, T> + Send + 'static,
T: Into<i32>
T: Into<i32>,
{
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<C, H, T, F>(
@@ -184,7 +198,7 @@ pub async fn run_serial_with_poll<C, H, T, F>(
session_started_at: Instant,
recording: Arc<Mutex<Recording<F>>>,
cancel: CancellationToken,
poll_mode: PollMode<F>
poll_mode: PollMode<F>,
) -> Result<()>
where
F: SerialFrame,
@@ -192,15 +206,13 @@ where
H: FrameHandler<F, T> + Send + 'static,
T: Into<i32>,
{
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::<Vec<i32>>());
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::<i32>();
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<Mutex<Recording<TactileAFrame>>>,
) -> 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(())
}