fix: 修复打砖块游戏碰撞穿透bug,添加渐进提速机制
This commit is contained in:
@@ -3,6 +3,8 @@
|
||||
//! 仅在 `devkit` feature 启用时编译。
|
||||
|
||||
use tauri::State;
|
||||
#[cfg(feature = "devkit")]
|
||||
use tauri::AppHandle;
|
||||
|
||||
use crate::devkit::{DevKitConfig, DevKitState, DevKitStatusSnapshot, ExportProcessResult};
|
||||
|
||||
@@ -12,9 +14,13 @@ pub fn devkit_status(state: State<'_, DevKitState>) -> DevKitStatusSnapshot {
|
||||
}
|
||||
|
||||
#[tauri::command]
|
||||
pub async fn devkit_start(state: State<'_, DevKitState>, port: Option<u16>) -> Result<DevKitStatusSnapshot, String> {
|
||||
pub async fn devkit_start(
|
||||
app: AppHandle,
|
||||
state: State<'_, DevKitState>,
|
||||
port: Option<u16>,
|
||||
) -> Result<DevKitStatusSnapshot, String> {
|
||||
let target_port = port.unwrap_or(50051);
|
||||
state.start(target_port).await?;
|
||||
state.start(app, target_port).await?;
|
||||
Ok(state.status())
|
||||
}
|
||||
|
||||
@@ -44,4 +50,4 @@ pub async fn devkit_process_export(
|
||||
let config = state.get_config();
|
||||
let use_xlsx = save_as_xlsx.unwrap_or(config.save_as_xlsx);
|
||||
state.process_export(&csv_path, use_xlsx).await
|
||||
}
|
||||
}
|
||||
|
||||
@@ -8,14 +8,24 @@ use std::path::PathBuf;
|
||||
use std::sync::atomic::{AtomicBool, AtomicU32, Ordering};
|
||||
use std::sync::Arc;
|
||||
|
||||
use serde::{Deserialize, Serialize};
|
||||
use tauri::{AppHandle, Emitter};
|
||||
use tokio::sync::mpsc;
|
||||
use tokio::task::JoinHandle;
|
||||
use serde::{Deserialize, Serialize};
|
||||
|
||||
use super::proto::sensor_push_client::SensorPushClient;
|
||||
use super::proto::export_processor_client::ExportProcessorClient;
|
||||
use super::proto::{ProcessRequest, SensorFrame};
|
||||
|
||||
#[derive(Debug, Clone, Serialize)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
struct DevKitPztAngleEvent {
|
||||
seq: u64,
|
||||
timestamp_ms: u64,
|
||||
dts_ms: u32,
|
||||
angle: f32,
|
||||
}
|
||||
|
||||
// ── DevKit 配置 ────────────────────────────────────────────────────
|
||||
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
@@ -145,7 +155,7 @@ impl DevKitState {
|
||||
}
|
||||
|
||||
/// 启动 gRPC client,连接到 Python server 并开始推送数据
|
||||
pub async fn start(&self, port: u16) -> Result<(), String> {
|
||||
pub async fn start(&self, app: AppHandle, port: u16) -> Result<(), String> {
|
||||
if self.running.load(Ordering::SeqCst) {
|
||||
return Err("AlreadyRunning".into());
|
||||
}
|
||||
@@ -161,9 +171,10 @@ impl DevKitState {
|
||||
|
||||
let running = Arc::clone(&self.running);
|
||||
let frame_count = Arc::clone(&self.frame_count);
|
||||
let app_handle = app.clone();
|
||||
|
||||
let handle = tokio::spawn(async move {
|
||||
if let Err(e) = run_grpc_upload(addr, rx, frame_count).await {
|
||||
if let Err(e) = run_grpc_upload(app_handle, addr, rx, frame_count).await {
|
||||
::log::error!("DevKit gRPC upload error: {e:?}");
|
||||
}
|
||||
running.store(false, Ordering::SeqCst);
|
||||
@@ -241,6 +252,7 @@ impl DevKitState {
|
||||
// ── gRPC Upload Client ─────────────────────────────────────────────
|
||||
|
||||
async fn run_grpc_upload(
|
||||
app: AppHandle,
|
||||
addr: String,
|
||||
mut rx: mpsc::Receiver<SensorFrame>,
|
||||
frame_count: Arc<AtomicU32>,
|
||||
@@ -255,14 +267,29 @@ async fn run_grpc_upload(
|
||||
};
|
||||
|
||||
let response = client.upload(stream).await?;
|
||||
let resp = response.into_inner();
|
||||
let mut inbound = response.into_inner();
|
||||
|
||||
::log::info!(
|
||||
"DevKit upload complete: ok={}, frames={}, msg={}",
|
||||
resp.ok,
|
||||
resp.frames_received,
|
||||
resp.message
|
||||
);
|
||||
while let Some(message) = inbound.message().await? {
|
||||
if message.ok {
|
||||
let payload = DevKitPztAngleEvent {
|
||||
seq: message.seq,
|
||||
timestamp_ms: message.timestamp_ms,
|
||||
dts_ms: message.dts_ms,
|
||||
angle: message.angle,
|
||||
};
|
||||
::log::debug!(
|
||||
"python pzt angle: seq={} dts_ms={} angle={:.2}",
|
||||
message.seq,
|
||||
message.dts_ms,
|
||||
message.angle
|
||||
);
|
||||
app.emit("devkit_pzt_angle", payload)?;
|
||||
} else {
|
||||
::log::warn!("DevKit PZT response error: {}", message.message);
|
||||
}
|
||||
}
|
||||
|
||||
::log::info!("DevKit upload stream closed");
|
||||
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
@@ -22,11 +22,43 @@ fn start_server_exe(exe_path: &std::path::Path) {
|
||||
}
|
||||
|
||||
match command.spawn() {
|
||||
Ok(_) => ::log::info!("DevKit Python server started: {}", exe_path.display()),
|
||||
Ok(_) => ::log::info!("DevKit Python server launched: {}", exe_path.display()),
|
||||
Err(error) => ::log::warn!("Failed to start DevKit Python server: {error}"),
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(feature = "devkit")]
|
||||
fn is_local_port_open(port: u16) -> bool {
|
||||
use std::net::{SocketAddr, TcpStream};
|
||||
use std::time::Duration;
|
||||
|
||||
let addr = SocketAddr::from(([127, 0, 0, 1], port));
|
||||
TcpStream::connect_timeout(&addr, Duration::from_millis(250)).is_ok()
|
||||
}
|
||||
|
||||
#[cfg(feature = "devkit")]
|
||||
fn find_server_exe(
|
||||
resource_dir: &std::path::Path,
|
||||
exe_name: &str,
|
||||
) -> Option<std::path::PathBuf> {
|
||||
let mut candidates = Vec::new();
|
||||
candidates.push(resource_dir.join(exe_name));
|
||||
|
||||
if let Ok(current_exe) = std::env::current_exe() {
|
||||
if let Some(parent) = current_exe.parent() {
|
||||
candidates.push(parent.join(exe_name));
|
||||
}
|
||||
}
|
||||
|
||||
if let Ok(current_dir) = std::env::current_dir() {
|
||||
candidates.push(current_dir.join("src-tauri").join("resources").join(exe_name));
|
||||
candidates.push(current_dir.join("devkit").join("dist").join(exe_name));
|
||||
candidates.push(current_dir.join("resources").join(exe_name));
|
||||
}
|
||||
|
||||
candidates.into_iter().find(|path| path.exists())
|
||||
}
|
||||
|
||||
#[cfg_attr(mobile, tauri::mobile_entry_point)]
|
||||
pub fn run() {
|
||||
let builder = tauri::Builder::default()
|
||||
@@ -56,35 +88,35 @@ pub fn run() {
|
||||
.path()
|
||||
.resource_dir()
|
||||
.unwrap_or_else(|_| std::path::PathBuf::from("./resources"));
|
||||
let app_handle = app.handle().clone();
|
||||
|
||||
tauri::async_runtime::spawn(async move {
|
||||
let devkit_port = 50051u16;
|
||||
#[cfg(target_os = "windows")]
|
||||
let exe_name = "je-skin-devkit-server.exe";
|
||||
#[cfg(not(target_os = "windows"))]
|
||||
let exe_name = "je-skin-devkit-server";
|
||||
|
||||
let bundled_exe = resource_dir.join(exe_name);
|
||||
let fallback_exe = std::env::current_exe()
|
||||
.ok()
|
||||
.and_then(|path| path.parent().map(|parent| parent.join(exe_name)));
|
||||
|
||||
let server_exe = if bundled_exe.exists() {
|
||||
Some(bundled_exe)
|
||||
if is_local_port_open(devkit_port) {
|
||||
::log::info!(
|
||||
"DevKit port {} already in use, skipping Python server auto-start",
|
||||
devkit_port
|
||||
);
|
||||
} else {
|
||||
fallback_exe.filter(|path| path.exists())
|
||||
};
|
||||
let server_exe = find_server_exe(&resource_dir, exe_name);
|
||||
|
||||
if let Some(exe_path) = server_exe {
|
||||
start_server_exe(&exe_path);
|
||||
tokio::time::sleep(std::time::Duration::from_millis(1200)).await;
|
||||
} else {
|
||||
::log::info!("DevKit Python server not found, skipping auto-start");
|
||||
if let Some(exe_path) = server_exe {
|
||||
start_server_exe(&exe_path);
|
||||
tokio::time::sleep(std::time::Duration::from_millis(1200)).await;
|
||||
} else {
|
||||
::log::info!("DevKit Python server not found, skipping auto-start");
|
||||
}
|
||||
}
|
||||
|
||||
if let Err(error) = devkit_state_clone.start(50051).await {
|
||||
if let Err(error) = devkit_state_clone.start(app_handle, devkit_port).await {
|
||||
::log::warn!("DevKit auto-start failed: {error}");
|
||||
} else {
|
||||
::log::info!("DevKit auto-started on 127.0.0.1:50051");
|
||||
::log::info!("DevKit gRPC client initialized for 127.0.0.1:{devkit_port}");
|
||||
}
|
||||
});
|
||||
|
||||
|
||||
@@ -11,6 +11,8 @@ pub mod model;
|
||||
pub mod serial;
|
||||
pub mod record;
|
||||
pub mod utils;
|
||||
#[cfg(feature = "multi-dim")]
|
||||
pub mod multi_dim_force;
|
||||
|
||||
pub type TestRecording = Recording<TestFrame>;
|
||||
pub type TactileARecording = Recording<TactileAFrame>;
|
||||
|
||||
122
src-tauri/src/serial_core/multi_dim_force.rs
Normal file
122
src-tauri/src/serial_core/multi_dim_force.rs
Normal file
@@ -0,0 +1,122 @@
|
||||
use ndarray::Array2;
|
||||
|
||||
const TOTAL_PRESSURE_LOW_THRESHOLD: usize = 500;
|
||||
const COP_STABILITY_FRAMES_REQUIRED: usize = 5;
|
||||
const SENSOR_ROWS: usize = 12;
|
||||
const SENSOR_COLS: usize = 7;
|
||||
|
||||
pub struct PztProcessor {
|
||||
first_frame: Option<Vec<f32>>,
|
||||
first_contact_cop_x: Option<f32>,
|
||||
first_contact_cop_y: Option<f32>,
|
||||
contact_initialized: bool,
|
||||
total_pressure_low_counter: usize,
|
||||
}
|
||||
|
||||
impl PztProcessor {
|
||||
pub fn new() -> Self {
|
||||
Self {
|
||||
first_frame: None,
|
||||
first_contact_cop_x: None,
|
||||
first_contact_cop_y: None,
|
||||
contact_initialized: false,
|
||||
total_pressure_low_counter: 0,
|
||||
}
|
||||
}
|
||||
|
||||
fn subtract_baseline(&mut self, current_frame: &[f32]) -> Vec<f32> {
|
||||
if self.first_frame.is_none() {
|
||||
self.first_frame = Some(current_frame.to_vec());
|
||||
}
|
||||
|
||||
let baseline = self.first_frame.as_ref().unwrap();
|
||||
current_frame
|
||||
.iter()
|
||||
.zip(baseline.iter())
|
||||
.map(|(c, b)| (c - b).max(0.0))
|
||||
.collect()
|
||||
}
|
||||
|
||||
fn reset_cop_state(&mut self) {
|
||||
self.first_contact_cop_x = None;
|
||||
self.first_contact_cop_y = None;
|
||||
self.contact_initialized = false;
|
||||
self.total_pressure_low_counter = 0;
|
||||
}
|
||||
|
||||
fn compute_pressure_direction(&mut self, frame: &[f32]) -> (f32, f32) {
|
||||
let frame2d = Array2::from_shape_vec((SENSOR_ROWS, SENSOR_COLS), frame.to_vec()).unwrap();
|
||||
let total_pressure: f32 = frame2d.sum();
|
||||
if total_pressure < TOTAL_PRESSURE_LOW_THRESHOLD as f32 {
|
||||
self.total_pressure_low_counter += 1;
|
||||
} else {
|
||||
self.total_pressure_low_counter = 0;
|
||||
}
|
||||
|
||||
if self.total_pressure_low_counter >= COP_STABILITY_FRAMES_REQUIRED {
|
||||
self.reset_cop_state();
|
||||
return (0.0, 0.0);
|
||||
}
|
||||
|
||||
if total_pressure == 0.0 {
|
||||
return (0.0, 0.0);
|
||||
}
|
||||
|
||||
let mut sum_x = 0.0;
|
||||
let mut sum_y = 0.0;
|
||||
|
||||
for r in 0..SENSOR_ROWS {
|
||||
for c in 0..SENSOR_COLS {
|
||||
let val = frame2d[(r, c)];
|
||||
sum_x += val * c as f32;
|
||||
sum_y += val * r as f32;
|
||||
}
|
||||
}
|
||||
|
||||
let cop_x = sum_x / total_pressure;
|
||||
let cop_y = sum_y / total_pressure;
|
||||
|
||||
if !self.contact_initialized {
|
||||
self.first_contact_cop_x = Some(cop_x);
|
||||
self.first_contact_cop_y = Some(cop_y);
|
||||
self.contact_initialized = true;
|
||||
return (0.0, 0.0);
|
||||
}
|
||||
|
||||
let dx = cop_x - self.first_contact_cop_x.unwrap();
|
||||
let dy = cop_y - self.first_contact_cop_y.unwrap();
|
||||
|
||||
(dx, dy)
|
||||
}
|
||||
|
||||
fn compute_vector_angle(x: f32, y: f32) -> (f32, f32) {
|
||||
let epsilon = 1e-8;
|
||||
let mag = (x * x + y * y).sqrt();
|
||||
let mut angle = (y).atan2(x + epsilon).to_degrees();
|
||||
if angle < 0.0 {
|
||||
angle += 360.0;
|
||||
}
|
||||
(angle, mag)
|
||||
}
|
||||
|
||||
fn compute_pzt_angle(px: f32, py: f32) -> (f32, f32) {
|
||||
Self::compute_vector_angle(px, -py)
|
||||
}
|
||||
|
||||
pub fn get_pzt_angle(&mut self, adc_data: &[f32]) -> Result<f32, &'static str> {
|
||||
if adc_data.len() != 84 {
|
||||
return Err("ADC data length must be 84");
|
||||
}
|
||||
|
||||
let baseline = self.subtract_baseline(adc_data);
|
||||
let (dx, dy) = self.compute_pressure_direction(&baseline);
|
||||
let (angle, _) = Self::compute_pzt_angle(dx, dy);
|
||||
|
||||
Ok(angle)
|
||||
}
|
||||
|
||||
pub fn reset_baseline(&mut self) {
|
||||
self.first_frame = None;
|
||||
self.reset_cop_state();
|
||||
}
|
||||
}
|
||||
@@ -2,12 +2,17 @@ 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};
|
||||
#[cfg(feature = "multi-dim")]
|
||||
use crate::serial_core::multi_dim_force::PztProcessor;
|
||||
use crate::serial_core::record::Recording;
|
||||
use crate::serial_core::record::{FrameTiming, RecordedFrame};
|
||||
#[cfg(feature = "devkit")]
|
||||
use crate::devkit::{proto::SensorFrame, DevKitState};
|
||||
use anyhow::Result;
|
||||
use log::debug;
|
||||
use std::future::pending;
|
||||
#[cfg(feature = "devkit")]
|
||||
use std::sync::atomic::Ordering;
|
||||
use std::sync::{Arc, Mutex};
|
||||
use std::time::Instant;
|
||||
use tauri::{AppHandle, Emitter};
|
||||
@@ -17,14 +22,19 @@ use tokio::io::{AsyncReadExt, AsyncWriteExt};
|
||||
use tokio::time::{self, Duration, MissedTickBehavior};
|
||||
use tokio_serial::SerialStream;
|
||||
use tokio_util::sync::CancellationToken;
|
||||
#[cfg(feature = "devkit")]
|
||||
use std::sync::atomic::Ordering;
|
||||
|
||||
const AUTO_SUB_INTERVAL: Duration = Duration::from_nanos(16_666_667);
|
||||
|
||||
pub enum PollMode<F> {
|
||||
Disable,
|
||||
Enabled(Box<dyn PollRequester<F>>),
|
||||
}
|
||||
|
||||
struct PendingSubFrame<F> {
|
||||
frame: F,
|
||||
values: Vec<i32>,
|
||||
}
|
||||
|
||||
pub trait SerialFrame: Clone + Send + 'static {
|
||||
fn dts_ms(&self) -> u64;
|
||||
|
||||
@@ -215,10 +225,15 @@ where
|
||||
it.set_missed_tick_behavior(MissedTickBehavior::Skip);
|
||||
it
|
||||
});
|
||||
let mut poll_sub_interval = time::interval(AUTO_SUB_INTERVAL);
|
||||
poll_sub_interval.set_missed_tick_behavior(MissedTickBehavior::Skip);
|
||||
|
||||
let mut chart_state = HudChartState::new();
|
||||
let mut buffer = [0u8; 1024];
|
||||
let mut prune_interval = time::interval(Duration::from_millis(450));
|
||||
#[cfg(feature = "multi-dim")]
|
||||
let mut pzt_processor = PztProcessor::new();
|
||||
let mut pending_sub_frame: Option<PendingSubFrame<F>> = None;
|
||||
prune_interval.set_missed_tick_behavior(MissedTickBehavior::Delay);
|
||||
|
||||
loop {
|
||||
@@ -246,6 +261,21 @@ where
|
||||
app.emit("hud_stream", packet)?;
|
||||
}
|
||||
}
|
||||
_ = poll_sub_interval.tick() => {
|
||||
if let Some(pending) = pending_sub_frame.take() {
|
||||
let display_values = build_display_values(
|
||||
&mut chart_state,
|
||||
pending.values.as_slice(),
|
||||
);
|
||||
|
||||
if let Some(packet) = pending
|
||||
.frame
|
||||
.to_hud_packet(&mut chart_state, display_values.as_deref())
|
||||
{
|
||||
app.emit("hud_stream", packet)?;
|
||||
}
|
||||
}
|
||||
}
|
||||
read_result = port.read(&mut buffer) => {
|
||||
let n = read_result?;
|
||||
if n == 0 {
|
||||
@@ -266,25 +296,38 @@ where
|
||||
.await?
|
||||
.map(|vals| vals.into_iter().map(Into::into).collect::<Vec<i32>>());
|
||||
|
||||
let mut record = recording.lock().map_err(|_| anyhow::anyhow!("recording state poisoned"))?;
|
||||
record.push(RecordedFrame{
|
||||
timing: FrameTiming { pts_ms: None, dts_ms: frame.dts_ms() },
|
||||
let mut record = recording
|
||||
.lock()
|
||||
.map_err(|_| anyhow::anyhow!("recording state poisoned"))?;
|
||||
record.push(RecordedFrame {
|
||||
timing: FrameTiming {
|
||||
pts_ms: None,
|
||||
dts_ms: frame.dts_ms(),
|
||||
},
|
||||
frame: frame.clone(),
|
||||
});
|
||||
drop(record);
|
||||
|
||||
let display_values = if let Some(vals) = decode_res.as_ref() {
|
||||
let summary = vals.iter().copied().sum::<i32>();
|
||||
let force = raw_to_g1(summary as u32);
|
||||
chart_state.record_summary(force as f32);
|
||||
chart_state.record_pressure_matrix(vals.as_slice());
|
||||
if let Some(vals) = decode_res {
|
||||
#[cfg(feature = "multi-dim")]
|
||||
{
|
||||
let pzt_values = vals.iter().map(|value| *value as f32).collect::<Vec<f32>>();
|
||||
if let Ok(angle) = pzt_processor.get_pzt_angle(&pzt_values) {
|
||||
// debug!("pzt angle: {:.2}", angle);
|
||||
}
|
||||
}
|
||||
#[cfg(feature = "devkit")]
|
||||
push_devkit_frame(&app, vals.as_slice(), frame.dts_ms(), force);
|
||||
Some(vec![summary])
|
||||
} else {
|
||||
None
|
||||
};
|
||||
{
|
||||
let summary = vals.iter().copied().sum::<i32>();
|
||||
let force = raw_to_g1(summary as u32);
|
||||
push_devkit_frame(&app, vals.as_slice(), frame.dts_ms(), force);
|
||||
}
|
||||
|
||||
if let Some(packet) = frame.to_hud_packet(&mut chart_state, display_values.as_deref()) {
|
||||
pending_sub_frame = Some(PendingSubFrame {
|
||||
frame: frame.clone(),
|
||||
values: vals,
|
||||
});
|
||||
} else if let Some(packet) = frame.to_hud_packet(&mut chart_state, None) {
|
||||
app.emit("hud_stream", packet)?;
|
||||
}
|
||||
}
|
||||
@@ -294,6 +337,14 @@ where
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn build_display_values(chart_state: &mut HudChartState, values: &[i32]) -> Option<Vec<i32>> {
|
||||
let summary = values.iter().copied().sum::<i32>();
|
||||
let force = raw_to_g1(summary as u32);
|
||||
chart_state.record_summary(force as f32);
|
||||
chart_state.record_pressure_matrix(values);
|
||||
Some(vec![summary])
|
||||
}
|
||||
|
||||
#[cfg(feature = "devkit")]
|
||||
fn push_devkit_frame(app: &AppHandle, values: &[i32], dts_ms: u64, resultant_force: f64) {
|
||||
let devkit_state = app.state::<DevKitState>();
|
||||
|
||||
Reference in New Issue
Block a user