Files
orisys-app/apps/studio/docs/shell-lifecycle.md
T
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

13 KiB
Raw Blame History

Studio 外壳:视图与关闭流程

这份文档记录主窗口的三件"外壳"行为:三个顶层视图如何切换面板、回放如何并入主窗口 (含可拖拽进度条),以及窗口关闭后进程如何回到命令行。它们都不属于记录通道,但都会 直接影响可用性。


1. 三个顶层视图 = 同一窗口的三套布局

品牌轨上的三个导航项不打开新窗口,而是切换主窗口的布局 (StudioMainWindow._apply_view_preset,预设在 VIEW_PRESETS / VIEW_SPLITTER_SIZES):

视图 面板 曲线 比例(3D,2D,曲线,标定)
实时监测 live 2D 图像 + 力曲线(+ 手指三维) 跟随实时 0, 360, 1080, 0
历史回放 replay 2D 图像 + 力曲线 + 回放条 读打开的录制;未打开时仍是实时跟随 0, 320, 1120, 0
标定 calibration 标定面板 + 力曲线 不变 0, 0, 560, 880
自由 custom(启动默认) 由视图菜单决定 不变 用户在分割条上摆的位置
  • 为什么标定视图给标定面板 880 px:六列诊断表头加九个动作按钮,在 300–400 px 的 三栏布局里必然被挤压。标定面板自身还有纵向滚动区,窗口再矮也够得到「应用到设备」。
  • custom 不点亮任何导航项:手工摆出来的布局不属于三个视图中的任何一个。
  • 视图 → 标定面板 与「标定」导航是同一个开关:勾上进入标定视图,取消回到实时视图。
  • 布局菜单里的「自由布局 / 二维模式」已删除:导航视图 + 分割条已经覆盖它们,而 「深度图 + 曲线」由 2D 面板自带的「深度图」模式一键完成;layout_preset 偏好一并移除。

1.1 采集来源随视图,不再是「来源」下拉

ToolbarWidget 的状态轨上有一个来源槽(source_slot):

情形 轨上显示
实时监测 + 摄像机 摄像机 [编号] [搜索]
实时监测 + 视频文件 使用摄像机 按钮 + 摘要里的 视频文件: xxx.mp4
历史回放 不占位(回放条里有录制选择与路径)

「用视频文件跑一次采集」仍从「文件 → 打开视频…」进入,source_mode / video_path 偏好保留,但不再有一个需要先选「来源」才能用的下拉。

1.2 传感器与标定共用一个弹窗

轨上只有一个摘要按钮(BOX · box/sensor.json,ToolbarWidget.btn_sensor); 点击打开 apps/studio/src/orisys_studio/ui/sensor_dialog.py::SensorDialog:

  • 传感器类型 / 传感器配置两个下拉(原 SensorPickerWidget,仍是窗口的属性, 因此 sensor_type() / config_name() 与信号语义不变);
  • 「当前生效的标定」:设备确认的版本(_active_calibration_id)与已标定轴;
  • 「最近导出的系数」:文件、大小、时间与文件身份(类型 / SN / 配置,来自 calibration_identity());导出/应用按钮转调 CalPanel.export_coefficients() / apply_coefficients(),实现只有一份。

弹窗常驻一个实例、每次打开 refresh(),所以设备刚确认的标定立刻可见。


2. 回放并入主窗口(含进度条)

回放不再是一个独立窗口。PlaybackDialog 已删除,它被拆成两半:

  • apps/studio/src/orisys_studio/replay.py::ReplayController(QObject):录制读取、游标、 深度重算(daemon 线程 + 120 ms 轮询、请求绑定 (_record_serial, seq, frame)、旧结果一律丢弃)、 深度色标(按录制锁存,与实时同一规则)、回放提示(降采样 / 跨版本 / 深度状态)。 它不画任何东西,只发信号:position_changed / frames_changed / status_changed / recording_changed。
  • apps/studio/src/orisys_studio/ui/replay_bar.py::ReplayBar:录制选择、录制信息、 可拖拽的 QSlider 进度条、pos / dur 读数、单位切换、重算深度、以及第二行的提示。

主窗口把两者接起来:

  • 曲线:ForcePlotsPanel.set_recording_source(reader) 用 RecordingCurveSource 直接读 录制目录(区间按需读盘 + 缓存包络),所以全程总览、拖动、缩放、框选都在同一套曲线上; 表头显示「回放录制(浏览)」,返回最新 在录制模式下禁用。
  • 回放按录制实际有的单位打开(_replay_unit_for):FN_N 等列全空的录制退回 raw, 否则在 newton 偏好下会画出三张空图、每张数值卡一个横杠——看起来就是"回放没加载"。 单位由 ReplayController.set_unit 一处决定,回放条、曲线、数值卡与设置区下拉一起改。
  • 2D 面板:ArrowPanel.set_source_kind("replay") 把视图下拉换成「原始图像 / 深度图」 (录制里没有光流,不提供箭头),页脚显示「录制视频 → 深度重算」并隐藏放大倍率与有效区域; 深度模式显示按录制锁存的色条与范围。
  • 游标是同一个:拖进度条 → 视频帧 + 曲线游标;拖曲线游标 → 回放位置。
  • 数值卡读游标处的那一帧(ForcePlotsPanel._readout_at_cursor)。浏览历史或回放时, 三个大数字是播放头所指的样本;跟随实时时"最新样本就是游标",不额外读盘。旧写法在回放 时显示实时采集的最后一个样本,于是一张 7.5e5 原始计数的曲线旁边写着实时的 0.0。
  • 2D 面板归当前视图的来源所有(_replay_owns_image()):回放视图打开录制后,采集线程 仍然在跑,但 _on_run_tick / _on_preview_tick 不再把实时帧画进 2D 面板, _render_run_image 直接返回。否则回放画面会被摄像头帧一帧一帧盖掉("回放时好像用的是 摄像头")。切回实时视图才把面板交还给实时帧。
  • 离开回放视图:曲线挂回实时 store 并恢复跟随,回放条隐藏,已打开的录制保留。
  • 文件菜单两条入口(打开录制目录… / 回放最近一次录制)都只是切到回放视图并加载录制, 不再弹第二个窗口。

apps/studio/tools/ui_snapshot.py --scene replay 会合成一段短录制并打开它,用于人工复核 进度条与回放布局(不写业务数据,也不需要硬件)。

曲线缩放与默认模式

  • 三条曲线在所有模式下 mouseEnabled(x=True):滚轮缩放、左键平移。手势信号用 pyqtgraph 的 sigRangeChangedManually(只有真实滚轮/拖动才会发),不是 sigXRangeChanged——后者在窗口缩放沿 X 轴联动传播时也会发,会把"缩放窗口"误判成 "用户开始浏览"。y 轴仍按配置的固定量程。
  • 浏览模式下共享光标线可拖动,拖动即移动播放头(回放视图里就是回放位置)。
  • 启动、新一次运行、切换单位/传感器、切回「实时监测」都保证回到「跟随实时」; 只有打开录制后的回放视图才处于浏览状态。

曲线表头:动作是图标,单位是开关

表头右侧三个动作(ForcePlotsPanel._header_button)只画符号、不写字,符号由 apps/studio/src/orisys_studio/ui/icons.py 在运行时用当前主题的颜色绘制 (zoom_fit 取景框+角标、box_zoom 虚线框、latest 箭头进入竖条),因此没有新增色值, 换主题时按令牌重新着色,也不会出现位图在不同底色上发灰:

动作 符号 文字(tooltip / 无障碍名)
全程总览 zoom_fit chart.overview
框选 box_zoom chart.box_zoom
返回最新 latest chart.return_latest
  • 符号不替代语义:每个按钮都带 tooltip 与 accessibleName,ui_check.py 的 chart header and unit chip 段逐项断言 text()=="" 且这三样齐全——只有符号没有说明 等于把动作藏起来。全程总览区域名(window_caption)仍是文字,它标注的是一块区域。
  • 选区统计在数值卡上,不在下方:框选/缩放出一段窗口后,每张卡的大号数字变成该窗口的 峰值、下方小字是该窗口的均值 · σ(chart.selection_detail),卡片的 tooltip 说明 大数字是峰值;曲线下方只剩「选区 4.13 s / 183 点」(完整的一行仍在它的 tooltip 里)。 以前把三通道的峰/均/σ 塞在一行 QLabel 里,中文长句直接把面板顶出右边界。 窗口等于整个会话(「全程总览」)不算选区——那是默认适配,此时卡片回到"游标处样本"。
  • 单位芯片是可点的按钮(QPushButton#unitBadge,不再是 QLabel):点一下就切换 N ⇄ raw,同一条命令(_set_display_unit)同时改曲线、数值卡与设置区下拉,三处永远 一致。旧写法只有「raw/N」两个字,看起来像只读状态。
  • 箭头图页脚的放大倍率写成「箭头按位移放大 ×2」,不再只有一个「×2」。

状态栏是一条通知行,不是第二条状态行

底部 QStatusBar 只承载一次性通知(录制结束/失败、标定已应用、连接慢、错误原因):

  • 没有常驻控件。以前右下角有一条 配置 sensor.json(status.device_label),而状态轨 的传感器摘要按钮已经写着 MINI · sensor.json,右下角是同一件事实的第二遍。
  • 消息会过期(_NoticeBar.DEFAULT_TIMEOUT_MS = 6 s)。Qt 的 showMessage(text) 默认 msecs=0,意思是"永不消失",于是会话刚开始那句「运行中(0)」一直挂到退出;只读状态 的读法应该在顶栏(阶段芯片 + 来源槽),所以子类把"没有超时"一律换成 6 秒。
  • 与状态轨重复的两条提示被删掉:status.run_started(「运行中(0)」)与 status.connecting(「正在连接…」)在轨上分别有「采集中」芯片与相机编号、以及 「连接中」芯片。
  • 逐帧重复的通告只发一次:status.units_raw_fallback(无标定 → 显示原始单位)以前每 一帧都发一次,等于永久占住通知行;现在每次运行只提示一次。

单位切换是"开关",不是"重新解释"

曲线缓冲区里的每个数都带着它被测量时的单位。设备在头几帧还没确认标定时,显示会退到 raw;标定一到位就切到 N —— 这一次切换丢弃缓冲区里的旧单位样本(会话存储仍然保留 它们,RecordingCurveSource / SessionStore 可重建),而不是把 raw 计数画到牛顿轴上。 旧写法只把 _unit 记成新单位、样本原样留着,于是每次开始采集的第一个画面都是一根 ~1e5 的尖峰。触发切换的那一帧本身仍然会刷新数值卡(早期实现里它被 return 吃掉了), 而浏览/回放时卡片改为按新单位重读游标处的样本。

同样地,_start_run 现在先 session_store.reset() 再 force_plots.set_store(...): 反过来的话新会话开局会带着上一次会话的尾巴。

切换单位后(set_units)立即按新单位重读游标处的样本,因为浏览/回放时数值卡说的是游标, 不是实时样本——否则换一下单位,卡片会短暂显示上一次实时读数。


3. 关闭流程:窗口关掉后进程必须回到命令行

关闭顺序(StudioMainWindow.closeEvent → main()):

  1. closeEvent:
    • 先摘掉记录通道的 sink,避免正在收尾的采集线程继续持有窗口;
    • _stop_run(reason="user_stop", wait=True):阻塞地释放设备;
    • _stop_recording(wait=True):写出 session.json 并关闭 MP4/CSV;
    • _persist_prefs():保存窗口几何、分割比例、视图与各项开关。
  2. app.exec_() 返回后,main() 立刻结束进程:
code = int(app.exec_())
logging.shutdown()
for stream in (sys.stdout, sys.stderr):
    try:
        stream.flush()
    except Exception:
        pass
os._exit(code)

为什么用 os._exit:相机 SDK 与 OpenCV 各自持有原生线程与句柄,正常返回会在它们之上 再跑一遍解释器收尾(模块销毁、DLL 卸载),进程就卡在那里——症状正是"命令行不会完全退出"。 所有需要落盘的东西在 closeEvent 与 logging.shutdown() 里已经写完,这里跳过的只是 "已经没有东西可写的库"的析构。

tools/ui_check.py 的 clean shutdown 段在子进程里跑真实的 main(),用定时器关掉窗口 并断言进程在 45 秒内以 0 返回(实测约 2 秒),所以这条路径有回归保护。


4. 复核方式

$py = "D:\orisys_ws\orisys-app\.venv-build\Scripts\python.exe"
$env:PYTHONPATH = "D:\orisys_ws\orisys-app\shared\src;D:\orisys_ws\orisys-app\apps\studio\src"
cd D:\orisys_ws\orisys-app\apps\studio

& $py tools/ui_check.py                                  # 含 top area / views / instrument panels / replay / clean shutdown
& $py tools/ui_snapshot.py --demo --scene replay --out outputs/_snap_replay.png
& $py tools/ui_snapshot.py --demo --preset calibration --out outputs/_snap_calibration.png