四个滤波预设落成机器可读的配置文件,并加上防漂移测试 - What: 新增 examples/filters/filters_{off,low_latency,balanced,low_noise}.json —— 每个都是完整可整块替换的 filters 段(五键齐全:flow_ema_alpha / force_median_window / force_lowpass_cutoff_hz / force_lowpass_order / force_lowpass_sample_rate_hz),数值即 FILTER_PRESETS 展开后的结果;examples/filters/README.md 说明这四份副本的存在与用途;新增 tests/test_filter_presets.py(12 项):预设名恰为四个、每个示例文件 == 默认块叠加该预设(改预设或改文件都会红)、每份都是含三个开关键的真实 filters 段、三个出厂 bundle(box/mini/finger)的 filters 段仍为全关、且用 external-frame 模式构造真实 Sensor 核对 filters_state() 报的也是全关(文件与设备必须一致)。 - Why: 之前的实现计划里写的是"四个 sensor_<preset>.json 整机配置",但整机配置要按传感器各复制一份、且必然随 bundle 漂移;README 里内嵌的 JSON 又无法被测试覆盖,等于把"建议用哪档滤波"这句话留在了一处会过时的散文里。改成"filters 片段 + 断言与 FILTER_PRESETS 逐值一致的测试",既不谎称是完整配置,也不会静默过时。 - Verification: pytest tests/test_filter_presets.py → 12 passed;SDK 全量 335 passed / 15 skipped(唯一失败仍是需要 1200 帧而样例只有 577 帧的 longrun 测试)。 - Not in this change: 未生成按传感器复制的整机配置;docs/filters.md 的预设表未纳入测试(只有这四个文件与代码常量对齐);app 侧未动。

This commit is contained in:
ykcheungab
2026-09-14 10:44:07 +08:00
parent 35190562e5
commit 7f8e5d0975
6 changed files with 132 additions and 0 deletions
+90
View File
@@ -0,0 +1,90 @@
"""The shipped filter presets and the example configs that document them.
``json.load`` cannot carry comments, so the answer to "which filter should I
use?" lives in three places that must not drift apart: ``FILTER_PRESETS`` in the
sensor, the table in ``docs/filters.md``, and the copy-paste files in
``examples/filters/``. The presets are the source of truth; these tests fail
when a copy stops matching them.
"""
from __future__ import annotations
import json
from pathlib import Path
import pytest
from orisys import Sensor
from orisys.calibration import filters_fingerprint
from orisys.sensor import FILTER_PRESETS, available_filter_presets
from orisys import configs as orisys_configs
REPO_ROOT = Path(__file__).resolve().parents[1]
EXAMPLES = REPO_ROOT / "examples" / "filters"
BUNDLES = ("box", "mini", "finger")
#: What every shipped bundle says: filtering is opt-in, so a fresh install
#: records the raw reading and the operator turns a stage on deliberately.
ALL_OFF = {
"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",
}
def test_the_preset_names_are_the_documented_ones():
assert sorted(available_filter_presets()) == [
"balanced",
"low_latency",
"low_noise",
"off",
]
# ``available_filter_presets`` hands out copies: a UI cannot edit the truth.
snapshot = available_filter_presets()
snapshot["off"]["flow_ema_alpha"] = 0.0
assert FILTER_PRESETS["off"]["flow_ema_alpha"] == 1.0
@pytest.mark.parametrize("preset", sorted(FILTER_PRESETS))
def test_every_example_file_still_matches_its_preset(preset: str):
path = EXAMPLES / f"filters_{preset}.json"
assert path.is_file(), f"missing example for preset {preset!r}: {path}"
block = json.loads(path.read_text(encoding="utf-8"))
# A whole ``filters`` block: the preset's own keys on top of the defaults
# every config needs, and nothing else.
expected = dict(ALL_OFF)
expected.update(FILTER_PRESETS[preset])
assert block == expected, f"{path.name} drifted from preset {preset!r}"
def test_every_example_file_is_a_real_filters_block():
"""A file that cannot be dropped into a config is documentation that lies."""
for preset in FILTER_PRESETS:
block = json.loads((EXAMPLES / f"filters_{preset}.json").read_text(encoding="utf-8"))
assert isinstance(block, dict) and block, preset
for key in ("flow_ema_alpha", "force_median_window", "force_lowpass_cutoff_hz"):
assert key in block, f"{preset}: {key} missing"
@pytest.mark.parametrize("bundle", BUNDLES)
def test_shipped_bundles_ship_with_filtering_off(bundle: str):
"""Nothing filters unless it was asked for."""
path = orisys_configs.resolve_sensor_config_path(bundle)
assert path is not None and path.is_file(), f"missing bundle: {bundle}"
cfg = json.loads(path.read_text(encoding="utf-8"))
assert cfg.get("filters") == ALL_OFF, f"{bundle} no longer ships with filtering off"
@pytest.mark.parametrize("bundle", BUNDLES)
def test_shipped_bundles_report_all_off(bundle: str):
"""The file and the running device have to agree about the shipped default."""
sensor = Sensor("external", config_name=bundle, verbose=False, backend="cpu")
try:
fp = filters_fingerprint(sensor.filters_state())
finally:
sensor.disconnect()
assert fp["measurement"] == {"flow_ema_alpha": 1.0}
assert fp["readout"]["force_median_window"] == 1
assert fp["readout"]["force_lowpass_cutoff_hz"] == 0.0