ui-layout.md named the arrangement's owners but nothing knew which elements exist, so a
view switch could silently take a running session's tracker reset away: docs/ui-elements.json
now states all 195 by region, pane and view, ui/elements.py loads it, and
tools/ui_elements_dump.py drafts it.
Verified: apps/studio 393 items (13 new) and ui_check 600/0, autocal 358, shared 390 with its
2 known wall-clock failures, lint and doc gates clean; a removed element, a renamed
objectName and a new named widget each fail the suite.
What
- record/index.py: RecordingIndex streams forces.csv once and keeps only the
small things — one time, one validity flag, one frame id per row plus a byte
offset every 4096 rows (~6.5 MB per 500k rows). row_range() bisects the times,
read_rows() seeks to the nearest checkpoint and scans forward, and overview()
folds every row into its bucket during the same pass to build a per-axis
min/max envelope.
- read_rows(max_points, unit) decimates **while streaming**: each bucket keeps
the extreme row of every axis in the requested unit, so a long interval never
has to exist in memory in full and a shear spike survives a flat normal channel.
- RecordingReader: info() decimates above max_rows (default 200k) and marks
itself decimated, with total_rows and a duration taken from the frame index
rather than from the view; window()/window_all()/overview()/statistics() read
the interval being asked about. list/window semantics unchanged for small
recordings.
- force/source.py: StoreCurveSource (live memory) and RecordingCurveSource (disk
interval + cached envelope) implement one interface — extent, window,
envelope, statistics, duration_s — so ForcePlotsPanel asks a *source* instead
of knowing where the data lives.
- force_plots: reads through the source (window, envelope, statistics, extent),
which also replaces its last direct store accesses.
- playback_dialog: draws the whole-session view through window_all() in the unit
being shown (bounded by CHART_MAX_POINTS), and says so when the view is
decimated ("显示 N / 共 M 行") instead of letting the operator count points.
- experiment._run: a failure that lands while the files are opening can no longer
be overwritten by the writer's own transition to "recording" (found by a flaky
test; the state check now covers STATE_FAILED).
Why
The store releases old blocks above ~4.6 h and the replay loaded every CSV row
into Python lists before drawing anything, so neither the live browser nor the
replay could show an earlier interval of a long recording. The index makes the
disk the data source and memory a cache of what is on screen.
Verification
- shared/tests/test_history_source.py (new, 8 cases) over a 12 000-row recording:
the index reports its rows and reads a bounded interval; a large recording is
marked decimated with its true duration and total rows; a small one is not; an
interval read sees the exact rows including a single peak; a decimated window
keeps the peak; the envelope fits the whole session in ≤200 points, keeps the
spike and is cached; the disk and memory sources agree on extent, values and
statistics; the whole session can be read without holding it all. Shared
suites: 146 tests, 0 failures (three consecutive runs).
- apps/studio/tools/ui_check.py: the replay section now checks the index row
count, that an interval read spans only the interval, the envelope size and
total_rows. 324 checks, 0 failures.
- apps/studio/tests/test_ui_smoke.py: 43 tests, 36 pass; the 7 failures are the
unchanged sandbox/harness artifacts.
- D:/orisys_ws/_review_phase12/reproduce.py: all ten lines unchanged;
compileall, locale and prefs parity clean.
- apps/studio/docs/record-channel.md §8 documents the index, the reader API and
the unified source, and lists what is still hardware work.
Not in this change
- Maximum RSS / backlog / loss measurements at real resolution on the target ARM
board (needs the hardware), and range CSV export.
Phase 2 of the review: browse the whole session instead of only the rolling
window, diagnose a calibration from the recorded points, and provide the
reviewed two-panel layout.
What
- SessionStore gains extent(), visible(t0, t1, max_points) and
statistics(t0, t1, unit). visible() walks only the blocks that overlap the
window and decimates by taking each bucket's minimum and maximum, so a spike
survives decimation and browsing a long session does not copy all of it.
- ForcePlotsPanel gains follow/browse modes, a whole-session overview strip
with the current window, pan and wheel zoom, a rubber-band zoom toggle,
a shared time cursor across all three channels, and a selection readout
(duration, valid samples, per-axis peak/mean/std).
- CalibrationPoint records a bounded per-axis trace, an excluded flag, a
validation flag and its fit residual. CalibrationRecorder gains
set_excluded/set_validation/fit; the fit is a least-squares line over the
non-excluded, non-validation points with per-point residuals and an
independent validation error.
- The calibration table becomes the reviewed diagnostic view
(reference load | FN | FX | FY | stability | state) with the residual shown
next to the loaded axis, plus exclude/restore and mark-validation actions and
a fit summary line. Double clicking a row opens the point's three-axis trace.
- Add a layout preset: free layout or 2D mode (depth map on the left, force
charts on the right, calibration folded away), persisted in preferences.
Why
- Browsing is a display pause, not a capture pause: the record channel keeps
running while the charts show an older window.
- Excluding a point removes it from the fit and from the exported calibration
but keeps it on record, so the evidence for the decision is not destroyed.
- The non-loaded axes are recorded but never given a reference load: their true
load is unknown during a single-axis calibration, so it stays empty.
Verification
- shared/tests/test_record_channel.py grows to 18 cases, covering extent,
peak-preserving decimation, windowed reads, inverted/empty windows, unit-aware
statistics and the time origin.
- tools/ui_check.py gains "session browsing" and "layout preset" sections
(223 checks, 0 failures overall).
- apps/studio/tests/test_ui_smoke.py covers the same invariants.
Not in this change: synchronised playback of video, depth and curves. It needs
a decision on how depth is stored for replay (per-frame PNG, reduced-rate PNG,
or offline recomputation); the cursor plumbing (cursor_moved / set_cursor) is
already in place for it.