线性算法验证失败,蠕变导致时飘一直上升

This commit is contained in:
lenn
2026-04-09 16:45:18 +08:00
parent d415c25afb
commit 163128cbd7
213 changed files with 1451 additions and 55 deletions

View File

@@ -110,6 +110,17 @@ pub fn serial_enum() -> Result<Vec<String>, SerialError> {
Ok(ports)
}
// 100g_output.csv, mean: 74649.5754176611, median: 75773.0
// 200g_output.csv, mean: 105524.73576309795, median: 107076.0
// 300g_output.csv, mean: 131465.96942446043, median: 132937.5
// 400g_output.csv, mean: 153465.0307174888, median: 155929.5
// 500g_output.csv, mean: 172101.9855780414, median: 174336.0
// 600g_output.csv, mean: 193798.99975442042, median: 195302.5
// 800g_output.csv, mean: 218962.3806359753, median: 223834.5
// 1000g_output.csv, mean: 240601.29850407978, median: 242600.0
// 1500g_output.csv, mean: 295079.9297642436, median: 298690.5
// 2000g_output.csv, mean: 332238.91334396595, median: 340790.0
#[tauri::command]
pub async fn serial_connect(
app: AppHandle,

View File

@@ -241,8 +241,8 @@ impl FrameHandler<TactileAFrame, i32> for TactileAHandler {
match frame {
TactileAFrame::Rep(rep) => {
let vals = TactileACodec::parse_data_frame(&rep.payload)?;
let g = raw_to_g1(vals.iter().sum::<i32>() as u32);
debug!("force is {g}");
// let g = raw_to_g1(vals.iter().sum::<i32>() as u32);
// debug!("force is {}, total: {}", g, vals.iter().sum::<i32>());
Ok(Some(vals))
}
_ => Ok(None),
@@ -261,10 +261,10 @@ fn raw_to_g1(raw: u32) -> f64 {
let n = X.len();
if raw <= X[0] {
return Y[0];
return Y[0] / 100.0;
}
if raw >= X[n - 1] {
return Y[n - 1];
return Y[n - 1] / 100.0;
}
let mut left = 0;
@@ -280,7 +280,7 @@ fn raw_to_g1(raw: u32) -> f64 {
}
let ratio = (raw - X[left]) as f64 / (X[right] - X[left]) as f64;
Y[left] + ratio * (Y[right] - Y[left]) / 100.0
Y[left] / 100.0 + ratio * (Y[right] - Y[left]) / 100.0
}
#[cfg(test)]

View File

@@ -0,0 +1,442 @@
#[derive(Debug, Clone, Copy)]
pub struct CalibPoint {
pub x: f64,
pub weight_g: f64,
}
pub struct RealtimeDriftCompensator {
// 空载基线更新速度
baseline_alpha: f64,
// 受压时,新增压缩量转成漂移的比例
drift_gain: f64,
// 未按压时,漂移恢复系数
drift_decay: f64,
// 输出滤波系数
filter_beta: f64,
rise_filter_beta: f64,
// 判断“明显增加/减少”的死区阈值
signal_epsilon: f64,
direct_release_ratio_threshold: f64,
direct_release_signal_threshold: f64,
direct_unload_profile: ReboundProfile,
partial_unload_profile: ReboundProfile,
calib_table: Vec<CalibPoint>,
baseline: f64,
drift: f64,
filtered_x: f64,
last_raw_signal: f64,
direct_rebound_fast: f64,
direct_rebound_slow: f64,
partial_rebound_fast: f64,
partial_rebound_slow: f64,
press_peak_signal: f64,
press_reference_signal: f64,
press_rise_accum: f64,
plateau_rise_accum: f64,
plateau_elapsed_ms: f64,
plateau_lock_ms: f64,
plateau_baseline_alpha: f64,
compensation_armed: bool,
initialized: bool,
}
#[derive(Clone, Copy)]
struct ReboundProfile {
fast_scale: f64,
slow_scale: f64,
fast_tau_ms: f64,
slow_tau_ms: f64,
}
#[derive(Clone, Copy)]
enum ReboundKind {
DirectUnload,
PartialUnload,
}
impl RealtimeDriftCompensator {
pub fn new(
baseline_alpha: f64,
drift_gain: f64,
drift_decay: f64,
filter_beta: f64,
signal_epsilon: f64,
calib_table: Vec<CalibPoint>,
) -> Result<Self, String> {
Self::validate_table(&calib_table)?;
Ok(Self {
baseline_alpha,
drift_gain,
drift_decay,
filter_beta,
rise_filter_beta: 0.42,
signal_epsilon,
direct_release_ratio_threshold: 0.45,
direct_release_signal_threshold: 9_000.0,
direct_unload_profile: ReboundProfile {
fast_scale: 0.14,
slow_scale: 0.06,
fast_tau_ms: 150.0,
slow_tau_ms: 720.0,
},
partial_unload_profile: ReboundProfile {
fast_scale: 0.07,
slow_scale: 0.025,
fast_tau_ms: 180.0,
slow_tau_ms: 520.0,
},
calib_table,
baseline: 0.0,
drift: 0.0,
filtered_x: 0.0,
last_raw_signal: 0.0,
direct_rebound_fast: 0.0,
direct_rebound_slow: 0.0,
partial_rebound_fast: 0.0,
partial_rebound_slow: 0.0,
press_peak_signal: 0.0,
press_reference_signal: 0.0,
press_rise_accum: 0.0,
plateau_rise_accum: 0.0,
plateau_elapsed_ms: 0.0,
plateau_lock_ms: 180.0,
plateau_baseline_alpha: 0.035,
compensation_armed: false,
initialized: false,
})
}
pub fn update(&mut self, raw: f64, is_pressed: bool) -> f64 {
self.update_with_dt(raw, is_pressed, 10.0)
}
pub fn update_with_dt(&mut self, raw: f64, is_pressed: bool, dt_ms: f64) -> f64 {
let dt_ms = dt_ms.clamp(1.0, 200.0);
let dt_ratio = dt_ms / 10.0;
if !self.initialized {
self.baseline = raw;
self.drift = 0.0;
self.filtered_x = 0.0;
self.last_raw_signal = 0.0;
self.direct_rebound_fast = 0.0;
self.direct_rebound_slow = 0.0;
self.partial_rebound_fast = 0.0;
self.partial_rebound_slow = 0.0;
self.press_peak_signal = 0.0;
self.press_reference_signal = 0.0;
self.press_rise_accum = 0.0;
self.plateau_rise_accum = 0.0;
self.plateau_elapsed_ms = 0.0;
self.compensation_armed = false;
self.initialized = true;
}
// 1. 只有未按压时更新 baseline
if !is_pressed {
self.baseline =
(1.0 - self.baseline_alpha) * self.baseline + self.baseline_alpha * raw;
}
// 2. 当前相对基线的原始信号
let raw_signal = (raw - self.baseline).max(0.0);
// 3. 计算相对上一时刻的变化量
let delta_signal = raw_signal - self.last_raw_signal;
self.update_press_tracking(raw_signal, delta_signal, dt_ms, is_pressed);
self.decay_rebound(dt_ms);
// 4. 更新 drift
if is_pressed {
// 只在“明显继续压深”时累积 drift
if false && self.compensation_armed && delta_signal > self.signal_epsilon {
self.drift += self.drift_gain * delta_signal * dt_ratio;
}
// 持压稳定 or 小幅下降:不恢复,不衰减,保持 drift 不变
} else {
// 只有真正松开时才恢复
self.reset_press_phase();
self.drift *= self.drift_decay.powf(dt_ratio);
}
if self.drift < 0.0 {
self.drift = 0.0;
}
if delta_signal < -self.signal_epsilon {
let rebound_kind = self.classify_rebound(raw_signal, is_pressed);
self.apply_rebound_event(rebound_kind, -delta_signal);
}
// 5. 漂移补偿
let rebound_offset = self.direct_rebound_fast
+ self.direct_rebound_slow
+ self.partial_rebound_fast
+ self.partial_rebound_slow;
let mut x = raw - self.baseline - self.drift - rebound_offset;
if x < 0.0 {
x = 0.0;
}
// 6. 低通滤波
let filter_beta = if x > self.filtered_x {
self.rise_filter_beta
} else {
self.filter_beta
};
self.filtered_x = (1.0 - filter_beta) * self.filtered_x + filter_beta * x;
// 7. 更新历史量
self.last_raw_signal = raw_signal;
// 8. 查表得到重量
Self::interpolate_piecewise_linear(&self.calib_table, self.filtered_x)
}
pub fn baseline(&self) -> f64 {
self.baseline
}
pub fn drift(&self) -> f64 {
self.drift
}
pub fn filtered_x(&self) -> f64 {
self.filtered_x
}
fn update_press_tracking(
&mut self,
raw_signal: f64,
delta_signal: f64,
dt_ms: f64,
is_pressed: bool,
) {
if !is_pressed {
return;
}
let positive_delta = delta_signal.max(0.0);
let peak_band = self.signal_epsilon * 1.5;
let loading_step_threshold = self.signal_epsilon * 0.35;
let loading_accum_threshold = self.signal_epsilon * 1.2;
let rearm_step_threshold = self.signal_epsilon * 0.9;
let rearm_signal_threshold = self.signal_epsilon * 1.6;
let plateau_creep_epsilon = self.signal_epsilon * 0.25;
let dt_ratio = dt_ms / 10.0;
if self.press_peak_signal <= 0.0 {
self.press_peak_signal = raw_signal;
self.press_reference_signal = raw_signal;
}
self.press_peak_signal = self.press_peak_signal.max(raw_signal);
self.press_rise_accum += positive_delta;
if raw_signal + peak_band < self.press_peak_signal {
self.press_peak_signal = raw_signal;
self.press_reference_signal = raw_signal;
self.press_rise_accum = 0.0;
self.plateau_rise_accum = 0.0;
self.plateau_elapsed_ms = 0.0;
self.compensation_armed = false;
return;
}
if !self.compensation_armed {
self.plateau_rise_accum += positive_delta;
let still_loading = delta_signal > loading_step_threshold
|| self.plateau_rise_accum >= loading_accum_threshold;
if still_loading {
self.press_peak_signal = raw_signal;
self.press_reference_signal = raw_signal;
self.plateau_rise_accum = 0.0;
self.plateau_elapsed_ms = 0.0;
return;
}
self.plateau_elapsed_ms += dt_ms;
if self.press_rise_accum >= self.signal_epsilon * 3.0
&& self.plateau_elapsed_ms >= self.plateau_lock_ms
{
self.compensation_armed = true;
self.press_peak_signal = raw_signal;
self.press_reference_signal = raw_signal;
self.plateau_rise_accum = 0.0;
}
return;
}
let resumed_loading = delta_signal > rearm_step_threshold
|| raw_signal > self.press_reference_signal + rearm_signal_threshold;
if resumed_loading {
self.press_peak_signal = raw_signal;
self.press_reference_signal = raw_signal;
self.plateau_rise_accum = 0.0;
self.plateau_elapsed_ms = 0.0;
self.compensation_armed = false;
return;
}
let creep_signal = (raw_signal - self.press_reference_signal).max(0.0);
if creep_signal <= plateau_creep_epsilon {
self.plateau_elapsed_ms += dt_ms;
return;
}
let baseline_step = creep_signal * self.plateau_baseline_alpha * dt_ratio;
self.drift += self.drift_gain * baseline_step;
self.press_reference_signal += baseline_step;
self.plateau_elapsed_ms += dt_ms;
}
fn reset_press_phase(&mut self) {
self.press_peak_signal = 0.0;
self.press_reference_signal = 0.0;
self.press_rise_accum = 0.0;
self.plateau_rise_accum = 0.0;
self.plateau_elapsed_ms = 0.0;
self.compensation_armed = false;
}
fn classify_rebound(&self, raw_signal: f64, is_pressed: bool) -> ReboundKind {
if !is_pressed
|| raw_signal <= self.direct_release_signal_threshold
|| raw_signal <= self.last_raw_signal * self.direct_release_ratio_threshold
{
ReboundKind::DirectUnload
} else {
ReboundKind::PartialUnload
}
}
fn apply_rebound_event(&mut self, kind: ReboundKind, drop_magnitude: f64) {
let profile = match kind {
ReboundKind::DirectUnload => self.direct_unload_profile,
ReboundKind::PartialUnload => self.partial_unload_profile,
};
match kind {
ReboundKind::DirectUnload => {
self.direct_rebound_fast += drop_magnitude * profile.fast_scale;
self.direct_rebound_slow += drop_magnitude * profile.slow_scale;
}
ReboundKind::PartialUnload => {
self.partial_rebound_fast += drop_magnitude * profile.fast_scale;
self.partial_rebound_slow += drop_magnitude * profile.slow_scale;
}
}
}
fn decay_rebound(&mut self, dt_ms: f64) {
self.direct_rebound_fast *= (-dt_ms / self.direct_unload_profile.fast_tau_ms).exp();
self.direct_rebound_slow *= (-dt_ms / self.direct_unload_profile.slow_tau_ms).exp();
self.partial_rebound_fast *= (-dt_ms / self.partial_unload_profile.fast_tau_ms).exp();
self.partial_rebound_slow *= (-dt_ms / self.partial_unload_profile.slow_tau_ms).exp();
if self.direct_rebound_fast < 1e-6 {
self.direct_rebound_fast = 0.0;
}
if self.direct_rebound_slow < 1e-6 {
self.direct_rebound_slow = 0.0;
}
if self.partial_rebound_fast < 1e-6 {
self.partial_rebound_fast = 0.0;
}
if self.partial_rebound_slow < 1e-6 {
self.partial_rebound_slow = 0.0;
}
}
fn validate_table(table: &[CalibPoint]) -> Result<(), String> {
if table.is_empty() {
return Err("calibration table is empty".to_string());
}
for i in 0..table.len() - 1 {
if table[i + 1].x <= table[i].x {
return Err(format!(
"calibration table x must be strictly increasing, index {}: {} -> {}",
i,
table[i].x,
table[i + 1].x
));
}
}
Ok(())
}
fn interpolate_piecewise_linear(table: &[CalibPoint], x: f64) -> f64 {
if x <= table[0].x {
return table[0].weight_g;
}
if x >= table[table.len() - 1].x {
return table[table.len() - 1].weight_g;
}
for i in 0..table.len() - 1 {
let p0 = table[i];
let p1 = table[i + 1];
if x >= p0.x && x <= p1.x {
let dx = p1.x - p0.x;
if dx.abs() < 1e-12 {
return p0.weight_g;
}
let t = (x - p0.x) / dx;
return p0.weight_g + t * (p1.weight_g - p0.weight_g);
}
}
table[table.len() - 1].weight_g
}
}
pub struct SensorSystem {
compensator: RealtimeDriftCompensator,
}
impl SensorSystem {
pub fn new() -> Result<Self, String> {
let calib_table = vec![
CalibPoint { x: 0.0, weight_g: 0.0 },
CalibPoint { x: 75773.0, weight_g: 160.0 },
CalibPoint { x: 107076.0, weight_g: 260.0 },
CalibPoint { x: 132937.5, weight_g: 360.0 },
CalibPoint { x: 155929.5, weight_g: 460.0 },
CalibPoint { x: 174336.0, weight_g: 560.0 },
CalibPoint { x: 195302.5, weight_g: 660.0 },
CalibPoint { x: 223834.5, weight_g: 860.0 },
CalibPoint { x: 242600.0, weight_g: 1060.0 },
CalibPoint { x: 298690.5, weight_g: 1560.0 },
CalibPoint { x: 340790.0, weight_g: 2060.0 },
];
let compensator = RealtimeDriftCompensator::new(
0.001,
0.105,
0.988,
0.22,
34.0,
calib_table,
)?;
Ok(Self { compensator })
}
pub fn process_one_sample(&mut self, raw: f64, is_pressed: bool) -> f64 {
self.compensator.update(raw, is_pressed)
}
pub fn process_one_sample_with_dt(&mut self, raw: f64, is_pressed: bool, dt_ms: f64) -> f64 {
self.compensator.update_with_dt(raw, is_pressed, dt_ms)
}
}

View File

@@ -51,10 +51,7 @@ pub enum TactileAFrame {
Rep(TactileARepFrame),
}
// TODO: filter
// pub trait FrameFilter<F> {
// fn apply(&self)
// }
#[async_trait]
pub trait FrameHandler<F, T>: Send {

View File

@@ -10,8 +10,10 @@ pub mod error;
pub mod frame;
pub mod model;
pub mod record;
pub mod sensor_runtime;
pub mod serial;
pub mod utils;
pub mod filter;
pub type TestRecording = Recording<TestFrame>;
pub type TactileARecording = Recording<TactileAFrame>;

View File

@@ -0,0 +1,157 @@
use crate::serial_core::filter::SensorSystem;
#[derive(Clone, Copy, PartialEq, Eq)]
enum MotionState {
Idle,
Pressed,
}
pub struct SensorRuntimeFilter {
sensor_system: SensorSystem,
initialized: bool,
baseline: f64,
baseline_alpha: f64,
last_dts_ms: Option<u64>,
motion_state: MotionState,
press_threshold_raw: f64,
release_threshold_raw: f64,
zero_snap_weight_g: f64,
press_confirm_frames: usize,
release_confirm_frames: usize,
press_candidate_frames: usize,
release_candidate_frames: usize,
}
impl SensorRuntimeFilter {
pub fn new() -> Result<Self, String> {
Ok(Self {
sensor_system: SensorSystem::new()?,
initialized: false,
baseline: 0.0,
baseline_alpha: 0.05,
last_dts_ms: None,
motion_state: MotionState::Idle,
press_threshold_raw: 12_000.0,
release_threshold_raw: 6_000.0,
zero_snap_weight_g: 12.0,
press_confirm_frames: 3,
release_confirm_frames: 6,
press_candidate_frames: 0,
release_candidate_frames: 0,
})
}
pub fn process_sample(&mut self, raw: f64) -> f64 {
self.process_sample_with_dt(raw, 10.0)
}
pub fn process_sample_with_dts(&mut self, raw: f64, dts_ms: u64) -> f64 {
let dt_ms = match self.last_dts_ms {
Some(last_dts_ms) if dts_ms > last_dts_ms => (dts_ms - last_dts_ms) as f64,
_ => 10.0,
};
self.last_dts_ms = Some(dts_ms);
self.process_sample_with_dt(raw, dt_ms)
}
fn process_sample_with_dt(&mut self, raw: f64, dt_ms: f64) -> f64 {
if !self.initialized {
self.baseline = raw;
self.initialized = true;
let _ = self.sensor_system.process_one_sample_with_dt(raw, false, dt_ms);
return 0.0;
}
let raw_signal = (raw - self.baseline).max(0.0);
self.update_motion_state(raw_signal);
let is_pressed = self.motion_state == MotionState::Pressed;
let weight = self
.sensor_system
.process_one_sample_with_dt(raw, is_pressed, dt_ms);
if !is_pressed {
self.baseline =
(1.0 - self.baseline_alpha) * self.baseline + self.baseline_alpha * raw;
if weight <= self.zero_snap_weight_g {
return 0.0;
}
}
weight
}
fn update_motion_state(&mut self, raw_signal: f64) {
match self.motion_state {
MotionState::Idle => {
if raw_signal >= self.press_threshold_raw {
self.press_candidate_frames += 1;
if self.press_candidate_frames >= self.press_confirm_frames {
self.motion_state = MotionState::Pressed;
self.press_candidate_frames = 0;
self.release_candidate_frames = 0;
}
} else {
self.press_candidate_frames = 0;
}
}
MotionState::Pressed => {
if raw_signal <= self.release_threshold_raw {
self.release_candidate_frames += 1;
if self.release_candidate_frames >= self.release_confirm_frames {
self.motion_state = MotionState::Idle;
self.release_candidate_frames = 0;
self.press_candidate_frames = 0;
}
} else {
self.release_candidate_frames = 0;
}
}
}
}
}
#[cfg(test)]
mod tests {
use super::SensorRuntimeFilter;
#[test]
fn idle_noise_stays_zero() {
let mut filter = SensorRuntimeFilter::new().expect("runtime filter should initialize");
let idle_samples = [
100_000.0,
101_500.0,
99_200.0,
102_100.0,
98_900.0,
100_400.0,
99_700.0,
];
for raw in idle_samples {
assert_eq!(filter.process_sample(raw), 0.0);
}
}
#[test]
fn release_returns_to_zero() {
let mut filter = SensorRuntimeFilter::new().expect("runtime filter should initialize");
for _ in 0..10 {
assert_eq!(filter.process_sample(100_000.0), 0.0);
}
let mut peak_weight = 0.0_f64;
for _ in 0..20 {
peak_weight = peak_weight.max(filter.process_sample(180_000.0));
}
assert!(peak_weight > 120.0);
let mut final_weight = 0.0_f64;
for _ in 0..60 {
final_weight = filter.process_sample(100_200.0);
}
assert_eq!(final_weight, 0.0);
}
}

View File

@@ -5,8 +5,9 @@ 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 crate::serial_core::sensor_runtime::SensorRuntimeFilter;
use anyhow::Result;
use log::{debug, info};
use log::info;
use std::fs::File;
use std::future::pending;
use std::sync::{Arc, Mutex};
@@ -206,7 +207,9 @@ where
H: FrameHandler<F, T> + Send + 'static,
T: Into<i32>,
{
info!("run_serial_with_poll");
let mut sensor_runtime =
SensorRuntimeFilter::new().map_err(|error| anyhow::anyhow!(error))?;
let mut requester = match poll_mode {
PollMode::Disable => None,
PollMode::Enabled(r) => Some(r),
@@ -238,7 +241,7 @@ where
if r.should_request() {
if let Some(req) = r.next_request()? {
let bytes = codec.encode(&req)?;
debug!("send {:02X?}", bytes);
// debug!("send {:02X?}", bytes);
port.write_all(&bytes).await?;
}
}
@@ -276,10 +279,14 @@ where
});
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);
let raw_summary = vals.iter().copied().sum::<i32>();
let raw_force_g = raw_to_g1(raw_summary as u32);
let stable_force_g =
sensor_runtime.process_sample_with_dts(raw_summary as f64, frame.dts_ms());
info!("raw force(g) = {raw_force_g:.3}, stable force(g) = {stable_force_g:.3}");
chart_state.record_summary(stable_force_g as f32);
chart_state.record_pressure_matrix(vals.as_slice());
Some(vec![summary])
Some(vec![stable_force_g.round() as i32])
} else {
None
};
@@ -294,7 +301,38 @@ where
Ok(())
}
// 鍦?src-tauri/src/serial_core/serial.rs 涓坊鍔?
fn raw_to_g1(raw: u32) -> f64 {
const X: [u32; 11] = [
0, 74602, 105503, 131459, 153512, 172041, 193794, 218947, 240580, 295118, 332346,
];
const Y: [f64; 11] = [
0.0, 160.0, 260.0, 360.0, 460.0, 560.0, 660.0, 860.0, 1060.0, 1560.0, 2060.0,
];
let n = X.len();
if raw <= X[0] {
return Y[0] / 100.0;
}
if raw >= X[n - 1] {
return Y[n - 1] / 100.0;
}
let mut left = 0;
let mut right = n - 1;
while left + 1 < right {
let mid = (left + right) / 2;
if raw < X[mid] {
right = mid;
} else {
left = mid;
}
}
let ratio = (raw - X[left]) as f64 / (X[right] - X[left]) as f64;
Y[left] + ratio * (Y[right] - Y[left])
}
pub async fn run_serial_with_calibration(
app: AppHandle,
mut port: SerialStream,