滤波配置自描述:去掉覆盖式总开关,每级按自己的值生效 - What: sensor.py 删除 filters.enabled 这个覆盖层——flow_ema_alpha / force_median_window / force_lowpass_cutoff_hz 现在各自按值生效(0<α<1 才滤、≤1 关中值、≤0 关低通),非法值一律回落到"关"而不再回落到"默认开"(原实现里非法取值的 except 分支会把 cutoff 变回 10 Hz);enabled 仅作废弃别名保留(显式 false = 三级全关)并在 verbose 时提示弃用,true 不再有覆盖作用;_filters_enabled 改为派生量(任一级在滤才为真)供旧调用方查询。三份 sensors/{box,mini,finger}/sensor.json 的 filters 段改为自描述且默认全关(flow_ema_alpha 1.0 / force_median_window 1 / force_lowpass_cutoff_hz 0.0 / order 2 / sample_rate "auto")。 - Why: 用户问"flow_ema_alpha 是不是受 filter_enabled 控制、这样合适吗",查证后确认三级都被同一总开关覆盖,且出厂配置写 enabled:false 却仍列着 0.5/3,导致文件说的与设备实际跑的不一致(审计/指纹会误报);同时 flow_ema_alpha 位于 HHD 之前、改变的是测量本身,与中值/低通这类读数滤波性质不同,不应被一个开关捆绑。 - Verification: 用 SDK 的 external-frame 模式逐个 bundle 构造真实 Sensor 并读取解析结果:box/mini/finger 均为 ema=1.0、median=1、cutoff=0.0、lowpass_on=False、any_stage=False;三份 JSON 解析通过;compileall 通过;SDK 内 _filters_enabled 仅剩派生定义一处引用。 - Not in this change: 未动采集路径与 read_info/force_triplets(第 2 步),未动 calibration.py 指纹(第 2 步),未升级 venv 里的 SDK(第 5 步)——因此 app 当前行为不变(它仍在运行时硬编码 lowpass_cutoff_hz=10,压过本配置),"默认关"要到 app 改用 FilterSettings.apply_to(sensor) 后才在界面体现。
This commit is contained in:
@@ -34,9 +34,8 @@
|
||||
"hhd_downsample": 3
|
||||
},
|
||||
"filters": {
|
||||
"enabled": true,
|
||||
"flow_ema_alpha": 0.5,
|
||||
"force_median_window": 3,
|
||||
"flow_ema_alpha": 1.0,
|
||||
"force_median_window": 1,
|
||||
"force_lowpass_cutoff_hz": 0.0,
|
||||
"force_lowpass_order": 2,
|
||||
"force_lowpass_sample_rate_hz": "auto"
|
||||
|
||||
@@ -25,9 +25,8 @@
|
||||
"hhd_downsample": 2
|
||||
},
|
||||
"filters": {
|
||||
"enabled": true,
|
||||
"flow_ema_alpha": 0.5,
|
||||
"force_median_window": 3,
|
||||
"flow_ema_alpha": 1.0,
|
||||
"force_median_window": 1,
|
||||
"force_lowpass_cutoff_hz": 0.0,
|
||||
"force_lowpass_order": 2,
|
||||
"force_lowpass_sample_rate_hz": "auto"
|
||||
|
||||
@@ -34,9 +34,8 @@
|
||||
"hhd_downsample": 3
|
||||
},
|
||||
"filters": {
|
||||
"enabled": true,
|
||||
"flow_ema_alpha": 0.5,
|
||||
"force_median_window": 3,
|
||||
"flow_ema_alpha": 1.0,
|
||||
"force_median_window": 1,
|
||||
"force_lowpass_cutoff_hz": 0.0,
|
||||
"force_lowpass_order": 2,
|
||||
"force_lowpass_sample_rate_hz": "auto"
|
||||
|
||||
+46
-28
@@ -175,12 +175,26 @@ class Sensor:
|
||||
# These settings change the reported force values, so they are part of
|
||||
# the measurement-pipeline identity and are recorded in the calibration
|
||||
# metadata / mismatch check alongside dis_mode and hhd_downsample.
|
||||
# Filtering is ON by default; set filters.enabled=false (or a per-key
|
||||
# zero) to get the unfiltered chain.
|
||||
#
|
||||
# Every stage is switched by its **own value**, so the config states what
|
||||
# runs: ``flow_ema_alpha >= 1``, ``force_median_window <= 1`` and
|
||||
# ``force_lowpass_cutoff_hz <= 0`` each mean "stage off". Values are never
|
||||
# overridden: there is no master switch that could make the file say 0.5
|
||||
# while the device runs 1.0. An invalid value falls back to **off**, never
|
||||
# to a default that filters. ``enabled`` is kept only as a deprecated
|
||||
# alias for "turn every stage off" and is honoured when it is explicitly
|
||||
# false. ``Sensor.filters_state()`` reports what actually took effect.
|
||||
filters_cfg = self._config.get("filters")
|
||||
if not isinstance(filters_cfg, dict):
|
||||
filters_cfg = {}
|
||||
self._filters_enabled = bool(filters_cfg.get("enabled", True))
|
||||
legacy_off = filters_cfg.get("enabled", None) is False
|
||||
|
||||
def _cfg(key, default):
|
||||
"""The configured value, or the documented default when unset."""
|
||||
if legacy_off:
|
||||
return default
|
||||
value = filters_cfg.get(key, None)
|
||||
return default if value is None else value
|
||||
|
||||
self._lowpass_order = 2
|
||||
self._lowpass_sample_rate_hz = float(DEFAULT_SAMPLE_RATE_HZ)
|
||||
@@ -195,34 +209,31 @@ class Sensor:
|
||||
self._config_fps = float(DEFAULT_SAMPLE_RATE_HZ)
|
||||
self._lowpass_sample_rate_hz = self._config_fps
|
||||
try:
|
||||
self._lowpass_order = max(1, int(filters_cfg.get("force_lowpass_order", 2)))
|
||||
rate_cfg = filters_cfg.get("force_lowpass_sample_rate_hz", "auto")
|
||||
self._lowpass_order = max(1, int(_cfg("force_lowpass_order", 2)))
|
||||
except (TypeError, ValueError):
|
||||
self._lowpass_order = 2
|
||||
rate_cfg = _cfg("force_lowpass_sample_rate_hz", "auto")
|
||||
if isinstance(rate_cfg, str):
|
||||
self._lowpass_rate_auto = rate_cfg.strip().lower() in ("auto", "", "measured")
|
||||
else:
|
||||
rate = float(rate_cfg) if rate_cfg is not None else 0.0
|
||||
try:
|
||||
rate = float(rate_cfg)
|
||||
except (TypeError, ValueError):
|
||||
rate = 0.0
|
||||
self._lowpass_rate_auto = rate <= 0.0
|
||||
if not self._lowpass_rate_auto:
|
||||
self._lowpass_sample_rate_hz = rate
|
||||
if self._filters_enabled:
|
||||
try:
|
||||
self._lowpass_cutoff_hz = float(
|
||||
filters_cfg.get("force_lowpass_cutoff_hz", DEFAULT_FORCE_LOWPASS_HZ)
|
||||
_cfg("force_lowpass_cutoff_hz", DEFAULT_FORCE_LOWPASS_HZ)
|
||||
)
|
||||
except (TypeError, ValueError):
|
||||
self._lowpass_order = 2
|
||||
self._lowpass_sample_rate_hz = self._config_fps
|
||||
self._lowpass_rate_auto = True
|
||||
self._lowpass_cutoff_hz = (
|
||||
DEFAULT_FORCE_LOWPASS_HZ if self._filters_enabled else 0.0
|
||||
)
|
||||
self._lowpass_cutoff_hz = 0.0
|
||||
if self._lowpass_sample_rate_hz <= 0:
|
||||
self._lowpass_sample_rate_hz = self._config_fps
|
||||
# An unusable cutoff is "off", never a silent jump to the default.
|
||||
if not (0.0 <= self._lowpass_cutoff_hz < 0.5 * self._lowpass_sample_rate_hz):
|
||||
self._lowpass_cutoff_hz = (
|
||||
DEFAULT_FORCE_LOWPASS_HZ
|
||||
if (self._filters_enabled and 0.0 < DEFAULT_FORCE_LOWPASS_HZ < 0.5 * self._lowpass_sample_rate_hz)
|
||||
else 0.0
|
||||
)
|
||||
self._lowpass_cutoff_hz = 0.0
|
||||
self._lowpass_enabled = self._lowpass_cutoff_hz > 0
|
||||
self._force_filter = LowpassFilter(
|
||||
cutoff_hz=self._lowpass_cutoff_hz,
|
||||
@@ -234,25 +245,32 @@ class Sensor:
|
||||
self._force_rate = SampleRateEstimator(initial_hz=self._config_fps)
|
||||
self._force_rate_source = "config"
|
||||
|
||||
self._force_median_window = 1
|
||||
if self._filters_enabled:
|
||||
try:
|
||||
self._force_median_window = max(
|
||||
1, int(filters_cfg.get("force_median_window", DEFAULT_FORCE_MEDIAN_WINDOW))
|
||||
)
|
||||
self._force_median_window = max(1, int(_cfg("force_median_window", DEFAULT_FORCE_MEDIAN_WINDOW)))
|
||||
except (TypeError, ValueError):
|
||||
self._force_median_window = DEFAULT_FORCE_MEDIAN_WINDOW
|
||||
self._force_median_window = 1
|
||||
self._force_median = CausalMedianFilter(window=self._force_median_window, n_channels=3)
|
||||
|
||||
self._flow_ema_alpha = 1.0
|
||||
if self._filters_enabled:
|
||||
try:
|
||||
alpha = float(filters_cfg.get("flow_ema_alpha", DEFAULT_FLOW_EMA_ALPHA))
|
||||
alpha = float(_cfg("flow_ema_alpha", DEFAULT_FLOW_EMA_ALPHA))
|
||||
except (TypeError, ValueError):
|
||||
alpha = DEFAULT_FLOW_EMA_ALPHA
|
||||
alpha = 1.0
|
||||
if 0.0 < alpha < 1.0:
|
||||
self._flow_ema_alpha = alpha
|
||||
self._ema_flow = None
|
||||
if legacy_off and self.verbose:
|
||||
print(
|
||||
"Note: filters.enabled is deprecated and means 'all stages off'; "
|
||||
"remove it and switch each stage by its own value "
|
||||
"(flow_ema_alpha=1, force_median_window=1, force_lowpass_cutoff_hz=0)."
|
||||
)
|
||||
# Derived, for callers that ask "is any stage filtering?".
|
||||
self._filters_enabled = bool(
|
||||
self._flow_ema_alpha < 1.0
|
||||
or self._force_median_window > 1
|
||||
or self._lowpass_enabled
|
||||
)
|
||||
|
||||
if self.verbose:
|
||||
if self.calibration is not None and self.calibration.source_path is not None:
|
||||
|
||||
Reference in New Issue
Block a user