新增统一力读数会话 ForceSession

把同帧测量、空载归零、模型推断与共用后处理收进一个 SDK 会话,Studio 显示与记录使用同一样本;旧 Sensor 读数接口保持兼容。以 force session 与受影响 SDK 套件、lint 与文档门禁验证,硬件与真实载荷精度未验证。

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01CpCfJF6SvonMKmPZhzKZHb
This commit is contained in:
ykcheungab
2026-10-20 07:07:22 +00:00
parent dc8a630df3
commit ee830d8505
17 changed files with 4175 additions and 29 deletions
+82
View File
@@ -628,3 +628,85 @@ def test_every_preset_reports_what_it_does(
assert fp["measurement"]["flow_ema_alpha"] == pytest.approx(alpha)
assert fp["readout"]["force_median_window"] == median
assert fp["readout"]["force_lowpass_cutoff_hz"] == pytest.approx(cutoff)
# ------------------------------------------------- session reference adapter
def _nonlinear_scalar_calibration(schema_version: int = 2):
return parse(
{
"schema_version": schema_version,
"unit": "N",
"channels": {
"normal": {
"method": "piecewise_linear",
"points": [[0.0, 0.0], [100.0, 1.0], [200.0, 3.0]],
"extrapolate": True,
"clamp_output_min": None,
},
"shear_x": {
"method": "piecewise_linear",
"signed": True,
"input": "components",
"points": [[0.0, 0.0], [100.0, 2.0]],
"extrapolate": True,
"clamp_output_min": None,
},
},
}
)
@pytest.mark.parametrize("schema_version", [1, 2])
def test_convert_referenced_subtracts_the_session_baseline_once(schema_version: int):
calibration = _nonlinear_scalar_calibration(schema_version)
curve = calibration.get("fn")
assert curve is not None
reference = {"fn": 150.0}
converted = calibration.convert_referenced((250.0, 0.0, 0.0), reference)
# curve(250) - curve(150) = 4.0 - 2.0: one subtraction, and the curve's own
# offset/zero point is not removed a second time.
assert converted[0] == pytest.approx(4.0 - 2.0)
assert converted[0] != pytest.approx(4.0 - 2.0 * 2.0)
# A raw-domain delta would convert 250-150 = 100 (1.0) instead of 4.0-2.0.
assert converted[0] != pytest.approx(1.0)
# The historical entry point is unchanged.
assert calibration.convert(250.0, 0.0, 0.0)[0] == pytest.approx(4.0)
def test_convert_referenced_keeps_signed_shear_and_missing_axes_unavailable():
calibration = _nonlinear_scalar_calibration()
converted = calibration.convert_referenced(
(250.0, -50.0, 12.0), {"fn": 150.0, "fx": 10.0}
)
assert converted[0] == pytest.approx(2.0)
# signed curve: convert_component(-50) - convert_component(10) = -1.0 - 0.2.
assert converted[1] == pytest.approx(-1.2)
# FY has no curve, no reference: unavailable, never a zero.
assert converted[2] is None
missing_reference = calibration.convert_referenced((250.0, 0.0, 0.0), None)
assert missing_reference == (None, None, None)
invalid_raw = calibration.convert_referenced(
(float("nan"), 0.0, 0.0), {"fn": 150.0}
)
assert invalid_raw[0] is None
def test_legacy_fingerprint_is_content_identity():
calibration = _nonlinear_scalar_calibration()
assert len(calibration.fingerprint) == 64
assert calibration.fingerprint == _nonlinear_scalar_calibration().fingerprint
changed = parse(
{
"schema_version": 2,
"unit": "N",
"channels": {
"normal": {
"method": "piecewise_linear",
"points": [[0.0, 0.0], [100.0, 1.0], [200.0, 4.0]],
"extrapolate": True,
"clamp_output_min": None,
}
},
}
)
assert changed.fingerprint != calibration.fingerprint