2 Commits

Author SHA1 Message Date
lenn
ca6ca377df Skip spatial force below display threshold 2026-07-14 15:30:32 +08:00
lenn
801a909c64 Update hand force calibration 2026-07-14 13:51:50 +08:00
2 changed files with 67 additions and 39 deletions

View File

@@ -25,6 +25,7 @@ use std::{
const SUMMARY_POINTS_PER_SERIES: usize = 42;
const HAND_FORCE_PANEL_COUNT: usize = 7;
const HAND_FORCE_SEGMENT_COUNTS: [usize; HAND_FORCE_PANEL_COUNT] = [84, 84, 84, 84, 84, 70, 44];
const HAND_FORCE_DISPLAY_THRESHOLD_N: f32 = 0.05;
const HAND_IMAGE_SIZE_PX: [f32; 2] = [971.0, 1117.0];
const HAND_FINGERTIP_FORCE_ANCHORS_PX: [[f32; 2]; HAND_FINGERTIP_COUNT] = [
[265.0, 495.0],
@@ -246,7 +247,12 @@ impl EskinDesktopApp {
fn analyze_spatial_force(&mut self, values: &[u32]) {
if matches!(self.config_state.mode, SerialMode::Hand) {
self.latest_hand_spatial_forces = self.force_estimator.analyze_fingertips(values);
let enabled = std::array::from_fn(|tip_index| {
hand_segment_force(values, tip_index) > HAND_FORCE_DISPLAY_THRESHOLD_N
});
self.latest_hand_spatial_forces = self
.force_estimator
.analyze_fingertips_masked(values, &enabled);
self.latest_spatial_force = self.strongest_hand_spatial_force();
} else {
self.latest_spatial_force = self.force_estimator.analyze(values);
@@ -707,33 +713,42 @@ fn update_hand_signal_histories(
histories: &mut [Vec<f32>; HAND_FORCE_PANEL_COUNT],
raw_values: &[u32],
) -> f32 {
let mut offset = 0usize;
let mut total_force = 0.0;
for (index, (history, sample_count)) in histories
.iter_mut()
.zip(HAND_FORCE_SEGMENT_COUNTS)
.enumerate()
{
let raw_total = raw_values
.get(offset..offset + sample_count)
.unwrap_or(&[])
.iter()
.fold(0_u64, |sum, value| sum + *value as u64)
.min(u32::MAX as u64) as u32;
let force = raw_to_hand_segment_g(index, raw_total).min(20.6);
let force = if force <= 0.1 { 0.0 } else { force };
for (index, history) in histories.iter_mut().enumerate() {
let force = hand_segment_force(raw_values, index);
let force = if force <= HAND_FORCE_DISPLAY_THRESHOLD_N {
0.0
} else {
force
};
total_force += force;
history.push(force);
if history.len() > SUMMARY_POINTS_PER_SERIES {
history.remove(0);
}
offset += sample_count;
}
total_force
}
fn hand_segment_force(raw_values: &[u32], index: usize) -> f32 {
let offset: usize = HAND_FORCE_SEGMENT_COUNTS[..index.min(HAND_FORCE_PANEL_COUNT)]
.iter()
.sum();
let sample_count = HAND_FORCE_SEGMENT_COUNTS
.get(index)
.copied()
.unwrap_or_default();
let raw_total = raw_values
.get(offset..offset + sample_count)
.unwrap_or(&[])
.iter()
.fold(0_u64, |sum, value| sum + *value as u64)
.min(u32::MAX as u64) as u32;
raw_to_hand_segment_g(index, raw_total).min(20.6)
}
fn raw_to_hand_segment_g(index: usize, raw: u32) -> f32 {
match index {
0 => raw_to_g1(raw),
@@ -741,8 +756,8 @@ fn raw_to_hand_segment_g(index: usize, raw: u32) -> f32 {
2 => raw_to_g3(raw),
3 => raw_to_g4(raw),
4 => raw_to_g5(raw),
// 5 => raw_to_g6(raw),
// 6 => raw_to_g7(raw),
5 => raw_to_g6(raw),
6 => raw_to_g7(raw),
_ => raw_to_gd(raw),
}
}
@@ -784,7 +799,7 @@ fn raw_to_gd(raw: u32) -> f32 {
fn raw_to_g1(raw: u32) -> f32 {
const RAW: [u32; 11] = [
0, 106443, 365678, 449286, 532425, 608187, 685522, 797058, 886997, 1082570, 1244793,
0, 35694, 226640, 319868, 398021, 450448, 510506, 592075, 655760, 771671, 813967,
];
const FORCE_CENTI_N: [f32; 11] = [
0.0, 57.0, 257.0, 357.0, 457.0, 557.0, 657.0, 857.0, 1057.0, 1557.0, 2057.0,
@@ -819,7 +834,7 @@ fn raw_to_g1(raw: u32) -> f32 {
fn raw_to_g2(raw: u32) -> f32 {
const RAW: [u32; 11] = [
0, 111143, 310153, 375593, 427920, 497433, 548676, 648392, 715118, 877487, 994887,
0, 45490, 267579, 352066, 431681, 457605, 550875, 624327, 732735, 883397, 899338,
];
const FORCE_CENTI_N: [f32; 11] = [
0.0, 57.0, 257.0, 357.0, 457.0, 557.0, 657.0, 857.0, 1057.0, 1557.0, 2057.0,
@@ -854,7 +869,7 @@ fn raw_to_g2(raw: u32) -> f32 {
fn raw_to_g3(raw: u32) -> f32 {
const RAW: [u32; 11] = [
0, 66898, 271692, 354094, 443663, 534994, 603246, 721942, 837520, 1030111, 1216809,
0, 51562, 252840, 326531, 386571, 415819, 514679, 631745, 678945, 810847, 900422,
];
const FORCE_CENTI_N: [f32; 11] = [
0.0, 57.0, 257.0, 357.0, 457.0, 557.0, 657.0, 857.0, 1057.0, 1557.0, 2057.0,
@@ -889,7 +904,7 @@ fn raw_to_g3(raw: u32) -> f32 {
fn raw_to_g4(raw: u32) -> f32 {
const RAW: [u32; 11] = [
0, 122337, 381708, 480527, 577445, 677301, 762306, 906751, 1019182, 1254601, 1415051,
0, 33213, 220611, 307383, 369637, 430462, 489659, 584789, 664960, 802749, 926907,
];
const FORCE_CENTI_N: [f32; 11] = [
0.0, 57.0, 257.0, 357.0, 457.0, 557.0, 657.0, 857.0, 1057.0, 1557.0, 2057.0,
@@ -924,7 +939,7 @@ fn raw_to_g4(raw: u32) -> f32 {
fn raw_to_g5(raw: u32) -> f32 {
const RAW: [u32; 11] = [
0, 82466, 287752, 356167, 433286, 506855, 604014, 671789, 770692, 887287, 1032058,
0, 44291, 216657, 301950, 366186, 384791, 496521, 566189, 701459, 845608, 940796,
];
const FORCE_CENTI_N: [f32; 11] = [
0.0, 57.0, 257.0, 357.0, 457.0, 557.0, 657.0, 857.0, 1057.0, 1557.0, 2057.0,
@@ -959,7 +974,7 @@ fn raw_to_g5(raw: u32) -> f32 {
fn raw_to_g6(raw: u32) -> f32 {
const RAW: [u32; 11] = [
0, 0, 4290, 8096, 10567, 14856, 19685, 43856, 84675, 114001, 129068,
0, 0, 2532, 5122, 7256, 11525, 13544, 16044, 18504, 29778, 41845,
];
const FORCE_CENTI_N: [f32; 11] = [
0.0, 57.0, 257.0, 357.0, 457.0, 557.0, 657.0, 857.0, 1057.0, 1557.0, 2057.0,
@@ -994,7 +1009,7 @@ fn raw_to_g6(raw: u32) -> f32 {
fn raw_to_g7(raw: u32) -> f32 {
const RAW: [u32; 11] = [
0, 10081, 40910, 49944, 64291, 68299, 79282, 97767, 113567, 140520, 167132,
0, 233, 9000, 12067, 14311, 16297, 18113, 27918, 32559, 50861, 65857,
];
const FORCE_CENTI_N: [f32; 11] = [
0.0, 57.0, 257.0, 357.0, 457.0, 557.0, 657.0, 857.0, 1057.0, 1557.0, 2057.0,
@@ -1082,8 +1097,8 @@ struct PressureColorRange {
gamma: f32,
}
// The shader begins its orange-to-red transition at intensity 0.66. With this
// gamma, a raw cell value at 80% of its configured range maps to that point.
// Controls the pressure-to-color response curve. Higher values reserve warmer
// colors for larger fractions of each panel's configured raw range.
const PRESSURE_COLOR_GAMMA: f32 = 1.5;
impl PressureColorRange {
@@ -1128,13 +1143,13 @@ impl PressureColorRange {
const DEFAULT_COLOR_RANGE: PressureColorRange = PressureColorRange::new(0, 7000);
const HAND_SENSOR_PANEL_COLOR_RANGES: [PressureColorRange; HAND_FORCE_PANEL_COUNT] = [
PressureColorRange::new(0, 1244793),
PressureColorRange::new(0, 994887),
PressureColorRange::new(0, 1216809),
PressureColorRange::new(0, 1415051),
PressureColorRange::new(0, 1032058),
PressureColorRange::new(0, 32648),
PressureColorRange::new(0, 32648),
PressureColorRange::new(0, 813967),
PressureColorRange::new(0, 899338),
PressureColorRange::new(0, 900422),
PressureColorRange::new(0, 926907),
PressureColorRange::new(0, 940796),
PressureColorRange::new(0, 41845),
PressureColorRange::new(0, 65857),
];
const HAND_FINGER_SENSOR_CELLS: usize = 12 * 7;
@@ -1223,13 +1238,14 @@ mod tests {
}
#[test]
fn pressure_normalization_starts_red_transition_at_eighty_percent() {
fn pressure_normalization_applies_configured_gamma() {
let range = PressureColorRange::new(0, 10_000);
let [intensity, _] = range.normalize(8_000);
let expected = 0.8_f32.powf(PRESSURE_COLOR_GAMMA);
assert!(
(intensity - 0.66).abs() < 0.002,
"80% of the configured raw range should map to the shader's red-transition point"
(intensity - expected).abs() < 0.002,
"the normalized response should follow the configured gamma"
);
}

View File

@@ -2,7 +2,7 @@ use crate::serial_core::multi_dim_force::PztProcessor;
pub const FINGER_SAMPLE_COUNT: usize = 84;
pub const HAND_FINGERTIP_COUNT: usize = 5;
const MIN_TANGENTIAL_MAGNITUDE: f32 = 0.02;
const MIN_TANGENTIAL_MAGNITUDE: f32 = 0.1;
#[derive(Debug, Clone, Copy)]
pub struct HudSpatialForce {
@@ -50,10 +50,22 @@ impl ForceEstimatorState {
pub fn analyze_fingertips(
&mut self,
values: &[u32],
) -> [Option<HudSpatialForce>; HAND_FINGERTIP_COUNT] {
self.analyze_fingertips_masked(values, &[true; HAND_FINGERTIP_COUNT])
}
pub fn analyze_fingertips_masked(
&mut self,
values: &[u32],
enabled: &[bool; HAND_FINGERTIP_COUNT],
) -> [Option<HudSpatialForce>; HAND_FINGERTIP_COUNT] {
let mut forces = [None; HAND_FINGERTIP_COUNT];
for (tip_index, processor) in self.fingertip_processors.iter_mut().enumerate() {
if !enabled[tip_index] {
continue;
}
let start = tip_index * FINGER_SAMPLE_COUNT;
let end = start + FINGER_SAMPLE_COUNT;
let Some(segment) = values.get(start..end) else {