Improve hand mode panels and recording

This commit is contained in:
lenn
2026-07-01 17:08:07 +08:00
parent 1ed729f8da
commit da77f2f194
5 changed files with 120 additions and 32 deletions

View File

@@ -230,9 +230,6 @@ impl EskinDesktopApp {
self.latest_matrix_rows = sample.rows; self.latest_matrix_rows = sample.rows;
self.latest_matrix_cols = sample.cols; self.latest_matrix_cols = sample.cols;
// Feed data to recorder
self.recorder.add_frame(&sample.matrix);
self.latest_spatial_force = self.force_estimator.analyze(&sample.matrix); self.latest_spatial_force = self.force_estimator.analyze(&sample.matrix);
// Keep JE-Skin's summary path separate from the optional spatial-force vector. // Keep JE-Skin's summary path separate from the optional spatial-force vector.
@@ -367,11 +364,18 @@ impl EskinDesktopApp {
fn draw_floating_panels(&mut self, ctx: &egui::Context) { fn draw_floating_panels(&mut self, ctx: &egui::Context) {
// draw_scene_panel(ctx, &mut self.scene_panel); // draw_scene_panel(ctx, &mut self.scene_panel);
if self.config_state.mode == SerialMode::Hand && self.stats_panel.visible {
self.config_panel.visible = false;
self.export_panel.visible = false;
self.matrix_config_panel.visible = false;
}
if let Some(next_mode) = draw_config_panel( if let Some(next_mode) = draw_config_panel(
ctx, ctx,
&mut self.config_panel, &mut self.config_panel,
&mut self.config_state, &mut self.config_state,
&self.connection, &self.connection,
&self.recorder,
) { ) {
self.switch_mode(next_mode); self.switch_mode(next_mode);
} }
@@ -430,7 +434,20 @@ impl EskinDesktopApp {
panel_restore_item(ui, "配置", &mut self.config_panel); panel_restore_item(ui, "配置", &mut self.config_panel);
panel_restore_item(ui, "录制", &mut self.export_panel); panel_restore_item(ui, "录制", &mut self.export_panel);
panel_restore_item(ui, "矩阵", &mut self.matrix_config_panel); panel_restore_item(ui, "矩阵", &mut self.matrix_config_panel);
if self.stats_panel.visible {
if ui
.add_sized(
egui::vec2(ui.available_width(), 0.0),
egui::Button::new("隐藏 统计"),
)
.clicked()
{
self.stats_panel.visible = false;
close_menu = true;
}
} else {
panel_restore_item(ui, "统计", &mut self.stats_panel); panel_restore_item(ui, "统计", &mut self.stats_panel);
}
if ui if ui
.add_sized( .add_sized(
egui::vec2(ui.available_width(), 0.0), egui::vec2(ui.available_width(), 0.0),

View File

@@ -4,6 +4,7 @@ use std::time::Duration;
use crossbeam_channel::{self, Receiver, Sender, TryRecvError}; use crossbeam_channel::{self, Receiver, Sender, TryRecvError};
use crate::recording::Recorder;
use crate::serial_core::serial::{ use crate::serial_core::serial::{
SerialIoStats, SerialPortReadWrite, SerialProtocol, run_serial_loop, SerialIoStats, SerialPortReadWrite, SerialProtocol, run_serial_loop,
}; };
@@ -83,6 +84,7 @@ impl ConnectionManager {
cols: u32, cols: u32,
baud_rate: u32, baud_rate: u32,
protocol: SerialProtocol, protocol: SerialProtocol,
recorder: Recorder,
) { ) {
self.disconnect(); self.disconnect();
self.set_state(ConnectionState::Connecting); self.set_state(ConnectionState::Connecting);
@@ -108,6 +110,7 @@ impl ConnectionManager {
&sample_tx, &sample_tx,
&stats_tx, &stats_tx,
&latest_sample, &latest_sample,
recorder,
); );
if let Err(e) = result { if let Err(e) = result {
eprintln!("[connection] device loop error: {e}"); eprintln!("[connection] device loop error: {e}");
@@ -187,9 +190,10 @@ fn run_device_loop(
sample_tx: &Sender<Vec<i32>>, sample_tx: &Sender<Vec<i32>>,
stats_tx: &Sender<SerialIoStats>, stats_tx: &Sender<SerialIoStats>,
latest_sample: &Arc<Mutex<Option<PressureSample>>>, latest_sample: &Arc<Mutex<Option<PressureSample>>>,
recorder: Recorder,
) -> Result<(), Box<dyn std::error::Error + Send + Sync>> { ) -> Result<(), Box<dyn std::error::Error + Send + Sync>> {
let port = serialport::new(port_name, baud_rate) let port = serialport::new(port_name, baud_rate)
.timeout(Duration::from_millis(100)) .timeout(Duration::from_millis(1))
.open()?; .open()?;
*state.lock().unwrap() = ConnectionState::Connected; *state.lock().unwrap() = ConnectionState::Connected;
@@ -205,6 +209,7 @@ fn run_device_loop(
cancel_rx, cancel_rx,
sample_tx, sample_tx,
Some(stats_tx), Some(stats_tx),
Some(&recorder),
); );
if let Ok(mut latest) = latest_sample.lock() { if let Ok(mut latest) = latest_sample.lock() {

View File

@@ -1,3 +1,4 @@
use crate::recording::Recorder;
use crate::serial_core::codec::Codec; use crate::serial_core::codec::Codec;
use crate::serial_core::codecs::hand_gateway::{HandGatewayCodec, HandGatewayFrame}; use crate::serial_core::codecs::hand_gateway::{HandGatewayCodec, HandGatewayFrame};
use crate::serial_core::codecs::tactile_a::TactileACodec; use crate::serial_core::codecs::tactile_a::TactileACodec;
@@ -6,7 +7,7 @@ use crossbeam_channel::{Receiver, Sender};
use std::io::{Read, Write}; use std::io::{Read, Write};
use std::time::{Duration, Instant}; use std::time::{Duration, Instant};
const POLL_INTERVAL_MS: u64 = 10; const POLL_INTERVAL_MS: u64 = 5;
const DEFAULT_HAND_GATEWAY_NODE_SAMPLE_COUNTS: &[u16] = &[84, 84, 84, 84, 84, 70, 44]; const DEFAULT_HAND_GATEWAY_NODE_SAMPLE_COUNTS: &[u16] = &[84, 84, 84, 84, 84, 70, 44];
#[derive(Debug, Default, Clone, Copy, PartialEq, Eq)] #[derive(Debug, Default, Clone, Copy, PartialEq, Eq)]
@@ -31,12 +32,15 @@ pub fn run_serial_loop(
cancel_rx: &Receiver<()>, cancel_rx: &Receiver<()>,
sample_tx: &Sender<Vec<i32>>, sample_tx: &Sender<Vec<i32>>,
stats_tx: Option<&Sender<SerialIoStats>>, stats_tx: Option<&Sender<SerialIoStats>>,
recorder: Option<&Recorder>,
) { ) {
match protocol { match protocol {
SerialProtocol::TactileA => { SerialProtocol::TactileA => {
run_tactile_a_loop(port, rows, cols, cancel_rx, sample_tx, stats_tx) run_tactile_a_loop(port, rows, cols, cancel_rx, sample_tx, stats_tx, recorder)
}
SerialProtocol::HandGateway => {
run_hand_gateway_loop(port, cancel_rx, sample_tx, stats_tx, recorder)
} }
SerialProtocol::HandGateway => run_hand_gateway_loop(port, cancel_rx, sample_tx, stats_tx),
} }
} }
@@ -47,6 +51,7 @@ fn run_tactile_a_loop(
cancel_rx: &Receiver<()>, cancel_rx: &Receiver<()>,
sample_tx: &Sender<Vec<i32>>, sample_tx: &Sender<Vec<i32>>,
stats_tx: Option<&Sender<SerialIoStats>>, stats_tx: Option<&Sender<SerialIoStats>>,
recorder: Option<&Recorder>,
) { ) {
let session_started_at = Instant::now(); let session_started_at = Instant::now();
let mut codec = TactileACodec::new(cols, rows); let mut codec = TactileACodec::new(cols, rows);
@@ -90,6 +95,11 @@ fn run_tactile_a_loop(
for frame in frames { for frame in frames {
if let TactileAFrame::Rep(rep) = frame { if let TactileAFrame::Rep(rep) = frame {
if let Ok(vals) = TactileACodec::parse_data_frame(&rep.payload) { if let Ok(vals) = TactileACodec::parse_data_frame(&rep.payload) {
if let Some(recorder) = recorder {
let pressures: Vec<u32> =
vals.iter().map(|v| (*v).max(0) as u32).collect();
recorder.add_frame(&pressures);
}
let _ = sample_tx.try_send(vals); let _ = sample_tx.try_send(vals);
} }
} }
@@ -116,6 +126,7 @@ fn run_hand_gateway_loop(
cancel_rx: &Receiver<()>, cancel_rx: &Receiver<()>,
sample_tx: &Sender<Vec<i32>>, sample_tx: &Sender<Vec<i32>>,
stats_tx: Option<&Sender<SerialIoStats>>, stats_tx: Option<&Sender<SerialIoStats>>,
recorder: Option<&Recorder>,
) { ) {
let session_started_at = Instant::now(); let session_started_at = Instant::now();
let mut codec = HandGatewayCodec::new(DEFAULT_HAND_GATEWAY_NODE_SAMPLE_COUNTS); let mut codec = HandGatewayCodec::new(DEFAULT_HAND_GATEWAY_NODE_SAMPLE_COUNTS);
@@ -174,6 +185,11 @@ fn run_hand_gateway_loop(
if parse_ok { if parse_ok {
// println!("[hand-packet-values] samples={}", vals.len()); // println!("[hand-packet-values] samples={}", vals.len());
if let Some(recorder) = recorder {
let pressures: Vec<u32> =
vals.iter().map(|v| (*v).max(0) as u32).collect();
recorder.add_frame(&pressures);
}
let _ = sample_tx.try_send(vals); let _ = sample_tx.try_send(vals);
} }
} }

View File

@@ -1,4 +1,4 @@
use eframe::egui; use eframe::egui::{self, Color32};
#[derive(Clone, Copy)] #[derive(Clone, Copy)]
pub struct AppTheme { pub struct AppTheme {
@@ -213,6 +213,19 @@ pub fn tag_button(label: impl Into<egui::WidgetText>) -> egui::Button<'static> {
.min_size(egui::vec2(0.0, METRICS.button_height)) .min_size(egui::vec2(0.0, METRICS.button_height))
} }
pub fn rich_tag_button(
label: impl Into<String>,
color: impl Into<Color32>,
) -> egui::Button<'static> {
let text = egui::RichText::new(label.into()).color(color);
egui::Button::new(text)
.fill(ONE_DARK_PRO.panel_strong)
.stroke(egui::Stroke::new(1.0, ONE_DARK_PRO.border))
.corner_radius(egui::CornerRadius::same(4))
.min_size(egui::vec2(0.0, METRICS.button_height))
}
pub fn primary_button(label: impl Into<egui::WidgetText>) -> egui::Button<'static> { pub fn primary_button(label: impl Into<egui::WidgetText>) -> egui::Button<'static> {
egui::Button::new(label) egui::Button::new(label)
.fill(ONE_DARK_PRO.accent) .fill(ONE_DARK_PRO.accent)

View File

@@ -7,7 +7,7 @@ use crate::{
serial_core::serial::{SerialIoStats, SerialProtocol}, serial_core::serial::{SerialIoStats, SerialProtocol},
style::{ style::{
self, ACCENT_BLUE, ACCENT_GREEN, ACCENT_ORANGE, ACCENT_RED, METRICS, ONE_DARK_PRO, self, ACCENT_BLUE, ACCENT_GREEN, ACCENT_ORANGE, ACCENT_RED, METRICS, ONE_DARK_PRO,
dim_text, group_frame, layout, panel_frame, tag_button, dim_text, group_frame, layout, panel_frame, rich_tag_button, tag_button,
}, },
utils::serial_enum, utils::serial_enum,
}; };
@@ -196,7 +196,7 @@ pub fn draw_connect_panel(
}); });
ui.add_space(METRICS.row_gap); ui.add_space(METRICS.row_gap);
draw_connect_action_row(ui, conn_state, is_connected, config, connection); draw_connect_action_row(ui, conn_state, is_connected, config, connection, recorder);
if is_connected { if is_connected {
draw_recording_toolbar(ui, recorder, export_path); draw_recording_toolbar(ui, recorder, export_path);
@@ -289,6 +289,7 @@ fn draw_connect_action_row(
is_connected: bool, is_connected: bool,
config: &ConnectPanelState, config: &ConnectPanelState,
connection: &ConnectionManager, connection: &ConnectionManager,
recorder: &Recorder,
) { ) {
ui.horizontal(|ui| { ui.horizontal(|ui| {
let status_text = match conn_state { let status_text = match conn_state {
@@ -324,6 +325,7 @@ fn draw_connect_action_row(
config.cols as u32, config.cols as u32,
config.mode.baud_rate(), config.mode.baud_rate(),
config.mode.protocol(), config.mode.protocol(),
recorder.clone(),
); );
} }
} }
@@ -336,6 +338,7 @@ pub fn draw_config_panel(
panel: &mut FloatingPanelState, panel: &mut FloatingPanelState,
config: &mut ConfigPanelState, config: &mut ConfigPanelState,
connection: &ConnectionManager, connection: &ConnectionManager,
recorder: &Recorder,
) -> Option<SerialMode> { ) -> Option<SerialMode> {
let mut changed_mode = None; let mut changed_mode = None;
let conn_state = connection.state(); let conn_state = connection.state();
@@ -354,7 +357,7 @@ pub fn draw_config_panel(
// draw_mode_row(ui, config); // draw_mode_row(ui, config);
ui.separator(); ui.separator();
draw_connection_row(ui, config, connection, conn_state); draw_connection_row(ui, config, connection, recorder, conn_state);
ui.add_space(8.0); ui.add_space(8.0);
// Legacy serial parameter grid is intentionally kept commented for future hardware: // Legacy serial parameter grid is intentionally kept commented for future hardware:
// draw_serial_grid(ui, config); // draw_serial_grid(ui, config);
@@ -392,6 +395,7 @@ fn draw_connection_row(
ui: &mut egui::Ui, ui: &mut egui::Ui,
config: &mut ConfigPanelState, config: &mut ConfigPanelState,
connection: &ConnectionManager, connection: &ConnectionManager,
recorder: &Recorder,
conn_state: ConnectionState, conn_state: ConnectionState,
) { ) {
ui.horizontal(|ui| { ui.horizontal(|ui| {
@@ -460,6 +464,7 @@ fn draw_connection_row(
7, 7,
config.mode.baud_rate(), config.mode.baud_rate(),
config.mode.protocol(), config.mode.protocol(),
recorder.clone(),
); );
} }
} }
@@ -744,10 +749,13 @@ const FORCE_CHART_MAX_N: f32 = 25.6;
const FORCE_CHART_SAMPLE_SECONDS: f32 = 0.1; const FORCE_CHART_SAMPLE_SECONDS: f32 = 0.1;
const FORCE_PANEL_ACTIVE_TAIL: usize = 8; const FORCE_PANEL_ACTIVE_TAIL: usize = 8;
const HAND_FORCE_PANEL_WIDTH: f32 = 260.0; const HAND_FORCE_PANEL_MIN_WIDTH: f32 = 280.0;
const HAND_FORCE_PANEL_HEIGHT: f32 = 150.0; const HAND_FORCE_PANEL_MAX_WIDTH: f32 = 500.0;
const HAND_FORCE_PANEL_MIN_HEIGHT: f32 = 140.0;
const HAND_FORCE_PANEL_MAX_HEIGHT: f32 = 190.0;
const HAND_FORCE_PANEL_GAP: f32 = 12.0; const HAND_FORCE_PANEL_GAP: f32 = 12.0;
const HAND_FORCE_PANEL_BOTTOM_MARGIN: f32 = 24.0; const HAND_FORCE_PANEL_SIDE_MARGIN: f32 = 24.0;
const HAND_FORCE_PANEL_VERTICAL_MARGIN: f32 = 28.0;
const HAND_FORCE_PANEL_TITLES: [(&str, &str); 7] = [ const HAND_FORCE_PANEL_TITLES: [(&str, &str); 7] = [
("T1", "拇指"), ("T1", "拇指"),
("T2", "食指"), ("T2", "食指"),
@@ -772,29 +780,49 @@ pub fn draw_hand_force_panels(ctx: &egui::Context, visible: bool, histories: &[V
.any(|history| has_recent_resultant_force(history)); .any(|history| has_recent_resultant_force(history));
let target_visible = visible && hand_active; let target_visible = visible && hand_active;
let screen = ctx.content_rect(); let screen = ctx.content_rect();
let first_row_y = screen.bottom() let side_width = ((screen.width() - HAND_FORCE_PANEL_SIDE_MARGIN * 4.0) * 0.25).max(0.0);
- HAND_FORCE_PANEL_BOTTOM_MARGIN let panel_width = side_width
- HAND_FORCE_PANEL_HEIGHT * 2.0 .min(HAND_FORCE_PANEL_MAX_WIDTH)
- HAND_FORCE_PANEL_GAP; .max(HAND_FORCE_PANEL_MIN_WIDTH.min(side_width));
let second_row_y = screen.bottom() - HAND_FORCE_PANEL_BOTTOM_MARGIN - HAND_FORCE_PANEL_HEIGHT; let left_x = screen.left() + HAND_FORCE_PANEL_SIDE_MARGIN;
let right_x = screen.right() - HAND_FORCE_PANEL_SIDE_MARGIN - panel_width;
let left_count = 4usize;
let right_count = HAND_FORCE_PANEL_TITLES.len() - left_count;
let available_height = (screen.height() - HAND_FORCE_PANEL_VERTICAL_MARGIN * 2.0).max(0.0);
let panel_height = ((available_height - (left_count - 1) as f32 * HAND_FORCE_PANEL_GAP)
/ left_count as f32)
.clamp(
HAND_FORCE_PANEL_MIN_HEIGHT.min(available_height),
HAND_FORCE_PANEL_MAX_HEIGHT,
);
let left_height =
left_count as f32 * panel_height + (left_count - 1) as f32 * HAND_FORCE_PANEL_GAP;
let right_height = right_count as f32 * panel_height
+ (right_count.saturating_sub(1)) as f32 * HAND_FORCE_PANEL_GAP;
let left_top = screen.center().y - left_height * 0.5;
let right_top = screen.center().y - right_height * 0.5;
for index in 0..HAND_FORCE_PANEL_TITLES.len() { for index in 0..HAND_FORCE_PANEL_TITLES.len() {
let history = histories.get(index).map(Vec::as_slice).unwrap_or(&[]); let history = histories.get(index).map(Vec::as_slice).unwrap_or(&[]);
let (code, title) = HAND_FORCE_PANEL_TITLES[index]; let (code, title) = HAND_FORCE_PANEL_TITLES[index];
let row_count: usize = if index < 4 { 4 } else { 3 }; let (target_x, target_y) = if index < left_count {
let row_index = if index < 4 { index } else { index - 4 }; (
let total_width = row_count as f32 * HAND_FORCE_PANEL_WIDTH left_x,
+ (row_count.saturating_sub(1)) as f32 * HAND_FORCE_PANEL_GAP; left_top + index as f32 * (panel_height + HAND_FORCE_PANEL_GAP),
let row_left = screen.center().x - total_width * 0.5; )
let target_x = } else {
row_left + row_index as f32 * (HAND_FORCE_PANEL_WIDTH + HAND_FORCE_PANEL_GAP); let right_index = index - left_count;
let target_y = if index < 4 { first_row_y } else { second_row_y }; (
right_x,
right_top + right_index as f32 * (panel_height + HAND_FORCE_PANEL_GAP),
)
};
draw_force_chart_area( draw_force_chart_area(
ctx, ctx,
egui::Id::new(format!("hand_force_panel_{index}")), egui::Id::new(format!("hand_force_panel_{index}")),
egui::pos2(target_x, target_y), egui::pos2(target_x, target_y),
egui::vec2(HAND_FORCE_PANEL_WIDTH, HAND_FORCE_PANEL_HEIGHT), egui::vec2(panel_width, panel_height),
target_visible, target_visible,
code, code,
title, title,
@@ -1079,7 +1107,10 @@ fn draw_floating_panel(
.show(ctx, |ui| { .show(ctx, |ui| {
ui.spacing_mut().item_spacing = egui::vec2(METRICS.item_gap, 6.0); ui.spacing_mut().item_spacing = egui::vec2(METRICS.item_gap, 6.0);
ui.horizontal(|ui| { ui.horizontal(|ui| {
if ui.add(tag_button("隐藏")).clicked() { if ui
.add(rich_tag_button("隐藏", style::ONE_DARK_PRO.text_dim))
.clicked()
{
hide_requested = true; hide_requested = true;
} }
ui.add_space(6.0); ui.add_space(6.0);
@@ -1685,7 +1716,10 @@ pub fn draw_matrix_config_panel(
(48, 24, "48×24"), (48, 24, "48×24"),
(64, 32, "64×32"), (64, 32, "64×32"),
] { ] {
if ui.add(tag_button(*name)).clicked() { if ui
.add(rich_tag_button(*name, style::ONE_DARK_PRO.text_dim))
.clicked()
{
config.rows = *r; config.rows = *r;
config.cols = *c; config.cols = *c;
} }
@@ -1715,7 +1749,10 @@ pub fn draw_matrix_config_panel(
ui.add_space(METRICS.row_gap); ui.add_space(METRICS.row_gap);
if ui.add(tag_button("重置默认")).clicked() { if ui
.add(rich_tag_button("重置默认", style::ONE_DARK_PRO.text_dim))
.clicked()
{
*config = MatrixConfigState::default(); *config = MatrixConfigState::default();
} }
}); });