record the full experiment: video, CSV and metadata
Phase 1 of the review asked for one directory per experiment, a force CSV, video of the pre-processing image, and metadata that ties them together. The record channel from the previous change already kept the whole session; this adds the on-disk format. What - Add record/experiment.py: ExperimentRecorder writes one directory per session containing session.json, forces.csv, frames.csv, events.csv, raw_NNNN.mp4 (segmented) and an optional lossless PNG sequence. - forces.csv uses the reviewed column order (session_id, frame_id, t_rel_s, FN_raw, FX_raw, FY_raw, FN_N, FX_N, FY_N, valid, calibration_id) and leaves a cell empty when a value is unavailable: an uncalibrated newton value or a failed readout is never written as 0. - frames.csv is the frame-index mapping: frame_id, t_rel_s, acquisition wall clock, segment, segment_frame, in_video. forces.csv and frames.csv share the packet frame_id and the same t_rel_s origin, so video and data can be aligned after dropped or segmented frames. - session.json carries format version, session id, start/end time in UTC, device and config (with a sha256 of the config file), app/shared/SDK versions, calibration reference, axis convention, filter parameters, video codec and the lossy-MP4 caveat, plus the event log and end-of-run statistics. It is written once at start so a crash still leaves identifiable metadata. - Add a record control to the toolbar: mode (video+data / data only) and a start/stop toggle that stays enabled while recording. - Log zeroing, calibration application and session end as timestamped events. Why - The recorder is fed from the worker sink list on the acquisition thread and only appends to a bounded queue, so disk I/O can never stall acquisition. - On_packet copies the raw frame instead of holding a reference: the packet frame may alias a buffer the SDK reuses between frames. - A queue overflow is counted and alarmed (rate limited), and a write or encode failure is counted, reported and after 5 consecutive errors stops the recording with failed=true rather than silently losing data. Verification - shared/tests/test_experiment_recorder.py: 12 cases covering the directory layout, the CSV contract, frame-id/time-base agreement, segment rolling, data-only and lossless modes, queue overflow accounting, encoder failure, metadata fields, crash-before-stop, and that on_packet never blocks. - tools/ui_check.py gains an "experiment record" section (169 checks, 0 failures overall), driving a recording through the UI and asserting the layout, the empty-cell rule and the event sequence. - apps/studio/tests/test_ui_smoke.py mirrors those invariants. - docs/record-channel.md documents the format and the remaining phase-2 work. Not in this change: CSV export of a selected time range, full-session curve browsing, calibration diagnostics (outlier exclusion, fit residuals) and PyOpenGL 3D.
This commit is contained in:
@@ -104,6 +104,22 @@
|
||||
"fy": "FY",
|
||||
"no_data": "—"
|
||||
},
|
||||
"record": {
|
||||
"label": "Record",
|
||||
"mode_video_data": "Video + data",
|
||||
"mode_data_only": "Data only",
|
||||
"mode_tip": "Video records the pre-processing camera input (no arrows or text overlays). MP4 is a lossy container, not a lossless copy of the raw data.",
|
||||
"start_label": "● Start recording",
|
||||
"stop_label": "■ Stop recording",
|
||||
"start_tip": "Record this experiment: video, force data and metadata into one directory",
|
||||
"stop_tip": "Stop recording and write session.json (always available)",
|
||||
"requires_run": "Press Run to start acquisition before recording",
|
||||
"started": "Recording started ({mode})",
|
||||
"start_failed": "Could not start recording: {error}",
|
||||
"finished": "Recording finished — {frames} frames / {segments} segment(s) in {path}",
|
||||
"finished_with_problems": "Recording finished with problems: {detail}",
|
||||
"alarm": "Recording problem: {detail}"
|
||||
},
|
||||
"cal": {
|
||||
"weight": "Weight (N)",
|
||||
"duration": "Record duration",
|
||||
|
||||
Reference in New Issue
Block a user