线性算法验证失败,蠕变导致时飘一直上升
This commit is contained in:
@@ -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,
|
||||
|
||||
@@ -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)]
|
||||
|
||||
442
src-tauri/src/serial_core/filter.rs
Normal file
442
src-tauri/src/serial_core/filter.rs
Normal 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)
|
||||
}
|
||||
}
|
||||
@@ -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 {
|
||||
|
||||
@@ -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>;
|
||||
|
||||
|
||||
157
src-tauri/src/serial_core/sensor_runtime.rs
Normal file
157
src-tauri/src/serial_core/sensor_runtime.rs
Normal 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);
|
||||
}
|
||||
}
|
||||
@@ -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,
|
||||
|
||||
Reference in New Issue
Block a user