Fix the phase-1/2 review findings in recording, calibration and depth replay

What
- record: one packet->row conversion (record/sample.py). A failed compute chain
  records no force value at all, a newton value is written only for an axis that
  really has a calibration curve, and mono_t (not the wall clock) orders history
  with a per-row calibration hash.
- record lifecycle: explicit starting/recording/stopping/stopped/failed states
  owned by the writer thread; stop() reports completed=False on timeout and does
  not write ended_at; consecutive write failures stop acceptance immediately.
- record capacity: byte budget for the video queue (512 MiB default); a full
  queue sheds the *frame* and parks the tiny data row, so a video overflow no
  longer deletes CSV rows. Lost rows leave a dropped=1 index row and are counted
  as dropped_gaps / unrecorded_gaps.
- calibration identity: sha256 content hash instead of a path, minted from the
  calibration the sensor actually loaded (reported before the first frame) and
  confirmed by the acquisition thread; events are timestamped when they happen.
- calibration diagnostics: the fit summary, residual and validation error now
  come from the model that export/apply would use (piecewise_linear); validation
  points are held out of the build; repeated reference loads are merged with an
  n-weighted mean and pooled std.
- calibration record: every point keeps its full three-axis sample series
  (frame id, time, raw, newtons, valid) plus diagnostics, written to an
  independent calibration_session_*.json that can be reopened for inspection;
  save record / export coefficients / apply to device are three separate actions.
- depth replay: the cursor's frame is bound to the depth result by request
  sequence and frame id, so a moved cursor never shows another frame's map; the
  SDK is stepped from its current position instead of re-reading from the start;
  the live chain (box deformation+contact, finger unroll+tactile) is replayed
  with the exact resolved config path; algorithm state is rebuilt from a reset
  marker or from the recorded pre-roll, and says so when it cannot be.

Why
The review (D:/orisys_ws/_review_phase12/review.md) showed the app writing raw
counts into the newton columns, recording a stale value after a failed compute,
losing CSV rows when the video queue filled, reporting success while a writer was
still running, diagnosing a model nobody applies, applying a calibration before
the device confirmed it, dropping the samples behind a calibration point, showing
a stale depth map under a moved cursor and recomputing depth of the wrong frame.

Verification
- D:/orisys_ws/_review_phase12/reproduce.py: all ten acceptance lines flipped
  (log saved as _review_phase12/reproduce_after.log). Including 25/25 rows kept
  with 8 video frames shed, frames [2]->[3], depth 8.0/8.0, fit 2/2 points.
- apps/studio/tools/ui_check.py: 249 checks, 0 failures (twice).
- shared suites: 91 tests, 0 failures (new: test_record_semantics,
  test_record_lifecycle, test_calibration_model, test_calibration_record,
  test_depth_replay).
- apps/studio/tests/test_ui_smoke.py: 23/30; the 7 remaining are harness
  artifacts of the sandbox (4 tmp_path PermissionError, 3 parametrized tests the
  shim cannot drive), unchanged from before this change.
- compileall clean; locale parity zh/en + t() key coverage: 0 failures.

Not in this change
- P2#10 disk-based browsing of a long recording and range CSV export (next
  round, per your choice).
- Stability-triggered calibration sampling, replay fixed colour scale, PyOpenGL
  three-dimensional view.
- Hardware verification: no camera attached, so the SDK's real processing chain
  and the pre-roll's effect on a live tracker were exercised with fakes only.
This commit is contained in:
ykcheungab
2026-09-13 14:55:53 +08:00
parent 9354d7be1a
commit 22cc59c402
26 changed files with 3387 additions and 290 deletions
+30 -10
View File
@@ -1,4 +1,4 @@
"""Offscreen UI smoke tests: construction, layout adaptation, theme tokens."""
"""Offscreen UI smoke tests: construction, layout adaptation, theme tokens."""
from __future__ import annotations
@@ -309,6 +309,13 @@ def _add_calibration_point(window, *, load: float, raw: float, samples: int = 40
def test_calibration_diagnostics_drive_the_fit(make_window):
"""The diagnostics must describe the model that would be exported.
The export builds a piecewise-linear curve, which passes through every point
it is built from — so the training residual is zero by construction and an
off-trend point can only show up as a *validation* error. A private
least-squares residual would have described a model nobody applies.
"""
window = _show(make_window(), (1400, 880))
window.cal_panel.set_running(True)
_add_calibration_point(window, load=1.0, raw=100.2)
@@ -319,23 +326,36 @@ def test_calibration_diagnostics_drive_the_fit(make_window):
recorder = window.cal_recorder
fit = recorder.fit("fn")
assert fit.usable and fit.n_points == 4
worst = fit.max_residual_n
assert worst > 0.05, "the off-trend point produced no residual"
assert fit.method == "piecewise_linear", fit.method
assert fit.breakpoints == 4, fit.breakpoints
assert fit.max_residual_n <= 1e-9, "the exported model misses its own points"
assert window.cal_panel.fit_label.text()
# Hold the off-trend point (load 2.0 at raw 400) out: the exported model is
# then built without it and the misfit is measurable.
window.cal_panel.table.setCurrentCell(1, 0)
window.cal_panel._on_toggle_validation()
held_out = recorder.fit("fn")
assert held_out.validation_points == 1
assert held_out.validation_error_n > 0.05, held_out.validation_error_n
assert recorder.points[1].is_validation
assert np.isnan(recorder.points[1].residual_n), "validation point entered the fit"
window.cal_panel._on_toggle_validation()
window.cal_panel._on_toggle_excluded()
assert recorder.fit("fn").max_residual_n < worst, "exclusion did not tighten the fit"
assert recorder.points[1].excluded
# An excluded point leaves the export store but stays on record.
assert len(window.cal_panel._store.channel("fn")) == 3
window.cal_panel.table.setCurrentCell(2, 0)
window.cal_panel._on_toggle_validation()
held_out = recorder.fit("fn")
assert held_out.validation_points == 1
assert recorder.points[2].is_validation
assert np.isnan(recorder.points[2].residual_n), "validation point entered the fit"
# The exported model no longer passes through the excluded point.
from orisys_app_shared.force.calibration_io import (
build_export_model,
predict_channel,
)
store = window.cal_panel._store
model = build_export_model({key: store.channel(key) for key in ("fn", "fx", "fy")})
assert abs(predict_channel(model, "fn", 400.0) - 2.0) > 0.05
def test_calibration_point_dialog_shows_three_axes(make_window):