Commit Graph

27 Commits

Author SHA1 Message Date
ykcheungab d11441cd79 Show only the bundle's name in the sensor configuration row
The row carried the origin, the calibration selection and the SDK's verdict, which a 30 px
line cannot explain; it is now the descriptor's display_name, else the folder name, and the
sensor dialog states the rest under the selector — name and folder, profile, shape, root,
verdict, calibration and every finding — with a suffix only where two rows would otherwise
read alike. Verified with the studio suite, the catalog tests in shared/tests, ui_check and
the lint gate.
2026-10-05 17:01:42 +08:00
ykcheungab 92b2f2fcd7 裸视频可分析:生成工作区、设置提示、逐帧力与按来源选画笔
pass 只认 frames.csv 与 session.json,裸视频此前因此只能播放;现在为它在应用自己的目录里生成
这两份文件并复用既有链路,链路重算的力按帧存入结果并标注为「重算」,box 的空白箭头画布改按帧
来源走共享画笔。
apps/studio/tests 497 项、shared/tests 471 项(2 个已知墙钟失败)、autocal 358 项、ui_check 634 项
均 0 失败,lint_gate 与 doc_lint 无发现;真实 GL 快照见 .scratch/bare-video/snapshots。
2026-10-05 16:57:02 +08:00
ykcheungab c0a10e6adb Present replay 3D with the cursor and open Analyze on a remembered video
Replay left the 3D pane on the rest mesh or a sparse overlay, and Analyze skipped the setup dialog unless the file was re-picked. Live Run no longer starts a chain on a video path. Verified with Studio tests for flush, overlay mask, Analyze open, live-video refusal, and locales.
2026-10-05 16:52:27 +08:00
ykcheungab 28e955a817 把 Studio 主机协议改成单向端口,并让「从视频重算」走分析入口
控制器经 AcquireGate / ImageSink / ShellNotes 与信号接线,采集 tick 用 acquire_presenter 上屏;三维测试证明分析结果已喂 3D 后,重算并入 on_analysis_requested。
验证:apps/studio 下相关 pytest 与 tools/ui_check.py(634 checks)通过。
2026-10-05 14:00:24 +08:00
ykcheungab a92a2ba237 Read the analysis cache before refusing a pass, and own the cache root
Opening a recording while the session that recorded it was still live never reached
``stored_result()``, so the panel reported no result over a recording whose result was on
disk. The guard now follows the cache read, results move to the app's derived-data root
(swept at startup, the old in-recording root ignored), and the panel states which of the
four no-result states it is in, with the pass's own reason.
Verified: apps/studio 462/0/18, shared 431/0/1, autocal 358, ui_check 628/0, both gates
clean, real-GL snapshots per state.
2026-10-04 16:53:50 +08:00
ykcheungab 1aa454311f Make the cached analysis views reachable over a recording
Replay moved the cursor onto the result's timeline, so the recording's own
picture was not offered at all and ``use_recording()`` had no caller. Adopting a
result now keeps the cursor on the recording, opens the panel on a derived view,
and serves scrubs, playback and display changes from the cache (Part B §9).
Verified: apps/studio 455 items / 0 failed / 18 skipped, shared 427 / 0 / 1,
autocal 358, ui_check 628 checks / 0 failures, lint gate clean.
2026-10-04 16:04:02 +08:00
ykcheungab fbf32b5303 Serve replay playback from a cached analysis result
Replay re-ran the SDK chain on every seek; Part A's pass stores the flow
samples once. Opening a recording now reuses a compatible result or runs the
pass behind a progress/cancel surface, and one cursor with two modes
(recorded CSV, analysed) feeds the charts, the 2D canvas and the 3D pane by
frame id. A threshold, colour or idle-dot change redraws from the stored
frames and writes nothing; a cancelled pass is reported and never played.

Verified: apps/studio/tests 440 items / 0 failed / 18 skipped (15 new, no
existing assertion changed); shared 417 / 0 / 1; autocal 358.
2026-10-04 14:13:05 +08:00
ykcheungab 9e20fd0277 Store the depth channel in an analysis result and serve it to the 2D depth view
A replay could draw arrows but not depth, and the recording view said there was
nothing to draw even when a result existed. Depth is now quantised per frame
(14 B empty, 486 B at the worst frame seen), read from the cache by the same
draw path live uses, and the raw-recording note appears only when no result
exists. Format version 2; see apps/studio/docs/replay-analysis.md.
2026-10-04 14:11:22 +08:00
ykcheungab cf49b0836e Serve replay playback from a cached analysis result
Replay re-ran the SDK chain on every seek; Part A's pass stores the flow
samples once. Opening a recording now reuses a compatible result or runs the
pass behind a progress/cancel surface, and one cursor with two modes
(recorded CSV, analysed) feeds the charts, the 2D canvas and the 3D pane by
frame id. A threshold, colour or idle-dot change redraws from the stored
frames and writes nothing; a cancelled pass is reported and never played.

Verified: apps/studio/tests 440 items / 0 failed / 18 skipped (15 new, no
existing assertion changed); shared 417 / 0 / 1; autocal 358.
2026-10-04 13:38:03 +08:00
ykcheungab b6f6a06851 Add an offline analysis pass and result cache for replay
Replay re-ran the SDK chain on every seek: 0.48-0.51 wall-seconds per video
second, about 4.8 minutes for a ten-minute recording. The new
orisys_app_shared.analysis package runs that pass once and stores the flow
samples a replay draws - 4,034 B per frame at the shipped finger grid instead of
the chain's 1,176,000 B; the result contract is in
apps/studio/docs/replay-analysis.md.

Verified: shared/tests 416 passed / 1 skipped (15 new) and apps/studio/tests
unchanged at 425 items / 0 failed / 18 skipped; tools/lint_gate.py clean; a
264-frame pass costs 0.5036 wall-s per video-s against 0.5052 for the bare chain
measured the same day.
2026-10-04 13:37:58 +08:00
ykcheungab 6fb2f72c6e Make the rail reset zero the sensor in live and the display in replay
The replay view disabled a control whose display reset is exactly what an
operator wants there, and the bare "Reset" hid that the live one zeroes the
sensor tracker; the label, tooltip and status line now name the target, and
Ctrl+R follows the view. Verified with the studio suite, ui_check (628
checks) and real-GL rail snapshots of both views.
2026-10-04 11:09:22 +08:00
ykcheungab eb70ac7638 add the Studio element manifest and its window-matching test
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.
2026-10-01 15:18:16 +08:00
ykcheungab 33a3d8c9c2 add the Studio UI layout contract
Studio had a colour contract with a mechanical audit but no layout equivalent,
which is where the recurring visual defects live: window flashing, the label
under the 3D view, the platform-not-moving report, and the 2D/3D sync work all
passed a 3,408-assertion tool suite because that suite asserts widget state, not
the visible result.
apps/studio/docs/ui-layout.md states the contract: ViewLayoutController owns the
top-level arrangement and each panel owns its internal layout; every path that
changes the view must go through apply_preset(); each view's pane priority and
degradation order; and which numeric geometry arguments are legitimate (a 1px
separator is not the same defect as a magic width on a flexible panel).
It also records the one transition that is broken today.
on_cal_panel_toggled(False) sets view_preset = "live" directly instead of
calling apply_preset("live"), so the replay -> calibration -> calibration-off
sequence leaves the rail on the replay source, the run button in playback role,
and the previous view's splitter sizes. The live branch does call apply_preset,
which is what makes the asymmetry easy to miss.
Three claimed enforcement gaps are stated so the contract is not mistaken for
something already enforced: audit_palette reads theme JSON and never scans
source for the hex literals ui-palette.md forbids; the containment test compares
width and height without x/y, so a shifted or overlapping child passes; and the
GL pixel suite skips invisibly without a real context.
Verified: doc_lint default and --all both exit 0; the new document is registered
in the workspace inventory (63 collected, 63 inventoried, 0 missing) and linked
from README.md beside the palette reference. No source file changed.
2026-10-01 11:59:00 +08:00
ykcheungab b8dc5e925b docs: repoint architecture docs at the workspace docs tree; ignore .scratch/
The workspace-root scratch cleanup (2026-09-27) deleted .repro/, the _review_*/
probe bundles and _probe_dataarea/. These tracked documents cited files inside
those directories, so their references are now explicitly marked as historical
and, where the evidence survived, repointed:

- record-channel.md section 7: the ten reproduction conditions are recorded in the
  archived review (docs/archive/reviews/phase12-review.md); reproduce.py is gone.
- studio-3d-stages.md: the _review_scene3d_20260914 review is archived under
  docs/archive/reviews/; the probe_stage6_* scripts and the .repro/ scan scripts
  are marked deleted. The perf report's stale .repro package path is explained in
  a note instead of editing the committed probe report, which feeds
  compile_scene.py --lock-from-probe.
- studio-architecture.md: the scan scripts that generated the diagrams are gone;
  the committed .svg/.png remain the surviving artefacts.

Also ignore .scratch/, the new per-task scratch directory (see
D:\orisys_ws\AGENTS.md). No source file is touched by this commit.
2026-09-27 21:14:20 +08:00
ykcheungab 8d374e5850 refactor(studio): 回放引擎改名 ReplayEngine —— 把「引擎」与「窗口级控制器」的名字分开
- What: `apps/studio/src/orisys_studio/replay.py` 的 `ReplayController` 改名为 `ReplayEngine`
  (引用只有 4 处:`app_window.py` 的 import 与构造、`ui_check.py` 的一处 import、模块内定义);
  模块 docstring 写明它为什么叫 engine(只回答"t 时刻是什么",不知道传输策略、不知道控件),
  并指路即将落地的窗口级 `ReplayController`;`app_window.replay` 这个属性名**不动**(36 处使用)。
  同步更新两份既有文档里的类名(`apps/studio/docs/shell-lifecycle.md`、`record-channel.md`、
  `docs/architecture/studio-architecture.md` 的框图标签)。
- Why: 用户已同意分拆 `app_window.py`,并选定"引擎改名 ReplayEngine + 新类叫 ReplayController"
  这一方案(另一方案是让两个名字 `ReplayController`/`ReplayCoordinator` 并存,容易进错门)。
  这一步是后续把窗口里那 30 个回放方法搬进 `ReplayController` 的前提:先把名字腾出来,
  否则搬迁时无法区分"引擎"与"控制器"。
- Verification: shared+apps 同一次运行 **588 项 0 失败 15 跳过**;`ui_check` 587 项 0 失败;
  全仓 `grep ReplayController` 只剩两处有意保留的提及(`replay.py` docstring 里指路、
  `app_window.py` 注释里指路),没有悬空引用。
- Not in this change: 不搬运任何方法、不改行为——`app_window.py` 仍是 3043 行;`SessionController`、
  `ReplayController`、`ViewLayoutController`、`DisplayPrefsController` 四个模块的搬迁尚未开始
  (顺序与契约见 docs/architecture/studio-architecture.md 建议 1)。
2026-09-15 08:41:44 +08:00
ykcheungab da8165cf03 删除深度重算引擎;修好回放视频每帧重开文件导致的无法播放,运输控制改到状态轨主按钮 - What: 1) 彻底删除深度重算引擎:shared/.../record/depth.py(DepthRecomputer/DepthResult)与 depth_backends.py(BoxReplay/FingerReplay)、其专属测试 test_depth_replay.py,record/__init__ 不再导出,playback.py::replay_warnings 不再报 depth_recomputed;apps/studio/replay.py 由 515 行瘦身为只做读盘+游标+视频帧+提示,回放视图的 2D 面板只提供「原始图像」;session.json 的 depth.replay 由 recompute 改为 none;删除 31 个只剩深度使用的文案键(depth_*/warn_state_*/warn_reset_*),warn_config_changed 与 warn_sdk_changed 改为描述"由视频重算";ui_check 深度段、studio 两个深度用例、test_playback 三例、drive_shared 的 test_depth_replay 与 reproduce.py 的深度段一并清理(shared 套件 150→134 项)。2) 无法播放的根因是 RecordingReader.video_frame_at 每帧都 cv2.VideoCapture 重开并 seek:小片段已 ~10 ms/帧,1080p H.264 每次 seek 需从关键帧解码(上百 ms),而播放每 33 ms 要一帧且全在 UI 线程上,视频因此看着不动;改为按分段缓存解码器、顺序帧用 read() 步进(实测 10 ms → 0.5 ms/帧)、跳转才 seek,新增 RecordingReader.close() 释放。运输控制按用户建议上移到状态轨主按钮:toolbar.set_run_role("playback") 使回放视图的主按钮变成「▶ 回放」/「⏸ 暂停」(实时视图仍是「▶ 运行」),点按以 1× 推进游标(墙钟计时,漏拍不变慢)、到末尾自动停,拖动游标/框选/滚轮/拖总览条经 ForcePlotsPanel.timeline_taken_over 自动暂停,空格键同效;曲线表头不再放第二个播放按钮;按钮样式在回放角色下不再套用"停止"的红色语义。 - Why: 用户要求先清掉深度重算引擎、并反馈"现在无法播放视频",建议把上方播放按钮在历史回放形式下改为回放。 - Verification: ui_check.py 554 项 0 失败(新增 seek<250ms/步进<60ms 的解码预算、回放按钮文字随播放/暂停切换、手动拖游标即暂停、切回实时视图按钮恢复运行、按深度视图已移除);test_ui_smoke.py 71 项 0 失败;drive_shared.py 134 项 0 失败(深度引擎用例随代码删除);locale_studio.py 262 键齐全、zh==en;reproduce.py 退出 0;.repro/probe_play.py 实测按「回放」后 0.75 s 内游标推进 0.689 s、面板 12/12 帧都在更新;两张复核图(回放视图的「▶ 回放」、实时视图的「▶ 运行」)。 - Not in this change: 录制文件格式与 CSV 列序不变(raw_*.mp4 与 forces.csv 继续一起写);「由视频重算」复核模式保留(它复用实时管线,是有状态、只能顺序前进的一遍)。 2026-09-14 09:31:21 +08:00
ykcheungab 9b3c5fe46c 回放与实时统一为一套界面;修复 30 fps 设备被以 16.7 fps 消费的计时器缺陷 - What: 1) 删除 ui/replay_bar.py(进度条、录制目录选择、重算深度按钮、提示行)与回放条的全部接线:历史回放现在与实时监测同布局(分割比例同为 0,360,1080,0)、同一条状态轨、同一套曲线手势,唯一差别是来源槽——实时是摄像机,回放是「选择录制视频…」(toolbar.btn_replay_video,与文件菜单「打开视频…」同一条 _load_replay_video 路径:视频所在目录即录制目录,曲线自动配对同目录 forces.csv)。时间轴改为曲线游标本身:表头新增播放/暂停图标按钮(icons.play/pause,空格键同效)以 1× 推进(按墙钟计时,漏拍不变慢),到末尾自动停;拖动游标、框选、滚轮平移或拖总览条一律自动暂停(ForcePlotsPanel.pause_playback)。回放的降采样/跨版本提示移入来源摘要 tooltip,打开录制时另发一条 6 秒通知。修复视频文件名残留在实时视图的问题:_refresh_status_rail 只在 _view_preset == "replay" 时写视频名。2) 录制格式不变:raw_*.mp4 与 forces.csv/frames.csv/session.json 继续一起写。按「两者都要」新增 视图 → 由视频重算力值(act_recompute):默认曲线读录制的 CSV,打开后把录制视频重新跑一遍视觉管线(进 SessionStore、2D 面板交给管线绘制、只能顺序前进,播放按钮即开始/暂停,关闭即挂回 CSV),启动路径唯一(set_playing → _on_chart_play_toggled → _start_run),并让文件源的时间轴改为视频自身时间(mono_t = 帧号 × 1/fps),否则重算结果会落在与 CSV 不同的横轴上。回放 2D 面板不再提供深度视图(REPLAY_VIEW_MODES=("recorded",)),离线深度重算引擎保留但无界面入口。3) 采集只有 16.67 fps 的根因是上一轮引入的等待:Windows 默认计时器分辨率 15.6 ms,time.sleep(0.002) 实测睡 15.5 ms,重复帧等两拍即跨过 30 fps 的 33 ms 周期,30 fps 设备被以 ~16.7 fps 消费并隔帧丢弃(配置本身正确:mini/sensor.json 写 camera.fps=30;离线实测整套管线 15.2 ms/帧≈66 fps,余量充足)。新增 _high_resolution_timer() 在会话期间把分辨率提到 1 ms(结束还原),并把重复帧等待改为 min(距下一帧时间, 半个周期)、到期只 sleep(0)。 - Why: 用户评审 3 点——回放界面与实时不一致且拖拽条效果一般、不想为回放录制深度信息、想同时保留视频与 CSV 记录、以及采集帧率只有 16.67 fps(应为 30)。 - Verification: ui_check.py 562 项 0 失败(新增两视图同布局/无 bar、来源槽随视图切换且无残留、播放暂停与手动手势暂停、重算模式切换与视频源、回放提示在 tooltip);test_ui_smoke.py 73 项 0 失败(新增视图统一、传输控制、重算模式);drive_shared.py 150 项 0 失败(新增 shared/tests/test_worker_frame_rate.py::test_a_file_source_runs_on_the_video_s_own_timeline,并把 30 fps 用例改成必须落在 27–31.5 fps;同文件实测无 1 ms 计时器 21.6 fps、有则 30.0 fps,同一帧返回 3 次时 11317:39);locale_studio.py 266 键齐全、zh==en;reproduce.py 退出 0;ui_snapshot.py 三张复核图(统一后的回放、实时、选区中英)。 - Not in this change: 录制文件格式与 CSV 列序不变;离线深度重算引擎(ReplayController.request_depth / DepthRecomputer)保留并有独立测试,但界面已无入口,可下一步删除;重算模式用有损 mp4,数值与原 CSV 会有差异(这正是它要显式呈现的)。 2026-09-13 22:04:01 +08:00
ykcheungab e6a1e77a5b 采集循环只认新帧,选区统计移到数值卡,状态栏改为会过期的通知行 - What: 1) 选区统计不再挤在曲线下方(中文长句会顶出面板右边界)——框选后三个数值卡的大号数字变成该窗口的峰值、下方小字为该窗口的均值·σ(chart.selection_detail),峰值语义放在卡片 tooltip;曲线下方只保留「选区 4.00s / 121 点」(完整一行留在它的 tooltip 里)。「全程总览」不算选区(那是默认适配),此时卡片回到"游标处样本";切换单位/换数据源/返回最新都会正确切回。2) 上方的 63.0 FPS 是采集循环自己的轮询速率,根因是同一帧被反复当成新样本:CaptureSource.read_latest 返回后台线程的最新帧,没有新帧时把同一帧再给一次,循环未做判断,于是 30 fps 的设备录出约 63 行/秒(frame_id 还各不相同)。新增 _capture_sequence():读 capture_source.status.last_seq,计数未变即"还没有新帧",睡 2 ms 重试、不进处理链、不发 packet;只有带后台读取线程的实时相机有该标记,视频文件是同步解码、每次都取下一帧,因此忽略(行为不变)。packet.fps 改为速率 (窗口帧数-1)/(首末到达差),窗口 1 s——按"1 秒内到达几帧"数数会系统性偏大。容器速率仍取传感器配置值,与此层无关。3) 底部状态栏只留一次性通知:删除右下角常驻「配置 sensor.json」(顶栏摘要按钮已写明),删除与顶栏重复的 status.run_started(运行中(0))与 status.connecting(正在连接…);QStatusBar 换成 _NoticeBar——Qt 的 showMessage(text) 默认 msecs=0 表示永不消失,这正是那句「运行中(0)」一直挂到底部的原因,子类统一补 6 秒超时;status.units_raw_fallback 之前每帧都发(等于永久占住通知行),改为每次运行只提示一次。附带修复:英文下表头模式名被截断(长句移入 tooltip,标签只留模式名 chart.mode_*_tip);数据源切换后曲线下方的选区文案会残留,改为在 _on_source_changed 里清零。 - Why: 用户评审 3 点——选区统计显示不全、顶部 fps 应受硬件限制最大 30、右下角配置文件名与左下角「运行中(0)」和顶栏信息重复。第 2 点查出根因是重复采样(数据缺陷,不只是显示问题),修掉后设备速率与记录行数才真正一致。 - Verification: ui_check.py 546 项 0 失败(新增:选区统计在卡片、卡片小字不裁切、状态栏无常驻标签且消息过期、顶栏 FPS 即 packet 交付速率、表头模式名中英不裁切);test_ui_smoke.py 71 项 0 失败(新增选区读数与通知行过期两例);drive_shared.py 148 项 0 失败(新增 shared/tests/test_worker_frame_rate.py:同一帧返回 3 次时 packet 数≈交付帧数,去掉判据为 11317:39,且 fps 与交付速率一致);locale_studio.py 267 键齐全、zh==en、prefs 对齐;reproduce.py 退出 0;ui_snapshot.py 新增 --select A,B,六张复核图(选区中英、实时、录制中、回放、标定 1280x720)。 - Not in this change: 录制文件格式与 CSV 列序不变;去重只影响"哪些帧算一帧",视频文件源行为不变;曲线下方不再显示三通道峰/均/σ(数值在卡片上,完整一行在 tooltip 里)。 2026-09-13 21:19:23 +08:00
ykcheungab 824544fc7a 回放并入主窗口,并修掉开场尖峰 / 录制时长 / 录制帧率 / 回放被摄像头覆盖等 9 项评审问题 - What: 回放不再是独立窗口——删除 PlaybackDialog,拆成 replay.py::ReplayController(读盘、游标、深度重算,只发信号)与 ui/replay_bar.py(录制选择、可拖拽进度条、单位、重算深度、提示行),品牌轨「历史回放」直接换主窗口布局,文件菜单两条入口都切到该视图,曲线改读 RecordingCurveSource(区间按需读盘 + 缓存包络),2D 面板切「原始图像 / 深度图」;传感器与标定合并到 SensorDialog(类型/配置下拉 + 当前生效标定 + 最近导出系数),标定面板加纵向滚动区、九按钮两行、六列表头 ResizeToContents。评审 9 项:1) 开场尖峰两个根因——push() 遇单位变化只记 _unit 而保留旧单位样本(raw 计数被画在牛顿轴上),改为「切换即换单位」:清缓冲按新单位重建、触发切换的那一帧仍刷数值卡;_start_run 先 session_store.reset() 再 set_store(),新会话不再带上次的尾巴(复现脚本 .repro/probe_item1.py:旧 39 样本峰值 9.841 → 新 37 样本峰值 2.390)。2) 箭头页脚改为「箭头按位移放大 ×{factor}」。3) 表头单位芯片由只读 QLabel 改为可点按钮,一次点击同时改曲线、数值卡与设置区下拉。4-6) 全程总览/框选/返回最新 改为 ui/icons.py 按主题令牌运行时绘制的图标,tooltip 与 accessibleName 保留文字,全程总览区域名仍是文字。7) stats() 在录制进行中即返回 _last_t - _t0,记录条开始计时(此前只在 _finalise 写、整场停在 00:00.0)。8) 容器 fps 改用传感器配置的速率(packet.fps 是"每秒完成多少帧"的计数器,突发/文件源会读到 48),视频按 1/fps 节流并把超前的帧计入 resampled_video(frames.csv/forces.csv 一行不少,报告写明原因),session.json 同时记 fps 与 measured_fps。9) _replay_owns_image() 让回放视图独占 2D 面板,实时帧不再覆盖;push() 在录制源挂载时连单位都不切,曲线/数值卡/单位一并归录制。附带:数值卡改读游标处样本(_readout_at_cursor)、回放按录制实际有的单位打开(_replay_unit_for + ReplayController.set_unit,避免 raw-only 录制在 newton 偏好下画三张空图)、视频速率下限。 - Why: 用户评审 9 点,其中 1/7/8/9 是代码缺陷(开场尖峰、录制时长不更新、录制 48fps 与实际 30fps 不符、回放时 2D 面板仍被摄像头刷新),2-6 是可读性问题(×2 含义不明、单位与视图控制应可点/应符号化)。 - Verification: ui_check.py 524 项 0 失败(新增 chart header and unit chip / acquisition correctness / replay owns the 2D pane 三段)、test_ui_smoke.py 69 项 0 失败(新增 7 个回归用例:单位切换不混单位、单位芯片、图标语义、录制中时长、传感器帧率+节流、回放独占面板、回放游标读数与单位回退)、drive_shared.py 146 项 0 失败、locale_studio.py(268 键齐全、zh==en、prefs 对齐)、reproduce.py、ui_snapshot.py 四张复核图(深色实时/回放、浅色 1280x720 实时与标定)。 - Not in this change: 录制文件格式(forces.csv 列序、frames.csv、session.json 路径与分段规则)不变,节流只影响"哪些帧进视频",并显式计数上报;三维面板仍是占位符。 2026-09-13 20:46:27 +08:00
ykcheungab 35e86ec1f7 界面改用「精密仪器工作台」布局:顶栏压缩、视图切布局、标定与回放面板不再裁切
- What: 品牌条 40→28px 并加 实时监测/历史回放/标定 导航;导航改为切换主界面三套布局(实时=2D图像+曲线、回放=自动全程浏览、标定=标定面板 880px+曲线,2D 图像整体缩小);传感器选择(传感器 / 传感器配置 两个带 tooltip 的下拉 + 产品徽标)移到状态轨最左侧,记录模式移入设置区;新增 RecordStatusChip(记录中时长、丢帧、写入异常)与「同一时刻只有一个实心按钮」(新增 dangerOutline 描边危险样式);三轴读数移到曲线上方(16pt 等宽)、只保留最下方时间轴、网格淡化、全程浏览条可拖动;三轴配色统一为蓝/橙/绿并把语义色对比度审计扩展到 toolbar_bg;箭头页脚显示放大倍率与有效区域,深度模式才显示色条与范围;左侧面板改名「2D 图像」且固定显示图像宽高比;标定面板加纵向滚动区、九按钮分两行、六列表头 ResizeToContents;PlaybackDialog 补上从未调用的 retranslate;main() 在事件循环结束后 flush 日志并 os._exit,避免设备 SDK 原生线程把进程挂在解释器收尾。
- Why: 评审提出的 7 点:顶部占高与装饰过多、记录状态不突出、配置切换看不懂且藏得太深、切视图应换主界面布局、标定与回放面板文字被裁、关窗后命令行不退出、2D 图像无固定宽高比且名字不符。
- Verification: ui_check.py(430 项,新增 top area / instrument panels / clean shutdown 段,含中英×1280/1440 的轨不挤压检查)、test_ui_smoke.py(57 项,新增视图切换、标定不裁切、2D 宽高比、回放按钮文字、传感器选择器等用例)、drive_shared.py(146 项)、locale_studio.py(261 键齐全、zh==en、prefs 对齐)、reproduce.py、ui_snapshot.py 四张复核图。
- Not in this change: 录制文件格式、CSV/MP4 写入路径与记录通道数据流不变;三维面板仍是占位符;回放对话框(含离线深度重算)本身只修了按钮文字。
2026-09-13 18:54:31 +08:00
ykcheungab 73592f085f Browse a long recording from disk instead of loading it into memory
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.
2026-09-13 16:54:26 +08:00
ykcheungab 22cc59c402 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.
2026-09-13 14:55:53 +08:00
ykcheungab 9354d7be1a cd D:\orisys_ws\orisys-app
git add apps/studio/config/locales/zh_CN.json `
        apps/studio/config/locales/en_US.json `
        apps/studio/tests/test_locales.py `
        apps/studio/tools/ui_check.py `
        apps/studio/tests/test_ui_smoke.py `
        apps/studio/src/orisys_studio/app_window.py `
        apps/studio/src/orisys_studio/ui/cal_panel.py `
        apps/studio/src/orisys_studio/ui/playback_dialog.py `
        apps/studio/src/orisys_studio/ui/force_plots.py `
        apps/studio/docs/record-channel.md `
        shared/src/orisys_app_shared/force/calibration_recorder.py `
        shared/src/orisys_app_shared/record/__init__.py `
        shared/src/orisys_app_shared/record/playback.py `
        shared/src/orisys_app_shared/record/depth.py `
        shared/tests/test_record_channel.py `
        shared/tests/test_playback.py
2026-09-13 14:01:24 +08:00
ykcheungab 817f830565 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.
2026-09-13 13:14:10 +08:00
ykcheungab db228b6f57 subject: store the whole session and drive calibration from the acquisition stream 2026-09-13 12:34:47 +08:00
ykcheungab 904c27125f changed customer to studio 2026-09-12 14:03:00 +08:00
ykcheungab a2db648560 dsh ui improve: add brand bar 2026-09-12 13:51:05 +08:00