Spaces:
Running
Download collector/docs/ui.md from Stouching-Robotics/DataFlowing: direct link, hf CLI and curl.
- Browser
- Download file 73.1 kB
-
https://huggingface.co/spaces/Stouching-Robotics/DataFlowing/resolve/main/collector/docs/ui.md
- Command line
-
hf download hf://spaces/Stouching-Robotics/DataFlowing/collector/docs/ui.md
-
curl -L -o ui.md https://huggingface.co/spaces/Stouching-Robotics/DataFlowing/resolve/main/collector/docs/ui.md
ui/
定位
ui/ 是 DAQ 桌面主程序的 PyQt5 界面层:主窗口把设备枚举面板、多画面网格、录制控制、录制历史、任务选择、上传与回放全部串成图形界面,后台数据链路(管线、数据库、网络服务)均由本层发起的对象持有并注入。界面层本身不写数据文件,持久化一律经 config.settings 与 core.* 完成。
被以下模块引用(证据):
main.pyfrom ui.main_window import MainWindow—— 主入口(Qt-Material 暗色主题),构造并show()。tools/tests/多个回归/冒烟测试直接构造界面类:exposure_control_test.py、device_panel_gui_smoke_test.py、grid_drag_fps_test.py、test_playback_multifps.py(还 import_get_effective_fps)、d435_playback_test.py、glove_widget_test.py等。- 包内互引:
ui/main_window.py(CameraGrid/DevicePanel/ExposureDialog/LoginDialog/GuideDialog/PlaybackDialog/UploadDialog/TaskSelectionPage)、ui/playback_dialog.py(ZoomableVideoWidget、CameraGrid 及两个分割条常量)、ui/camera_grid.py与ui/glove_widget.py(CameraWidget)、ui/login_dialog.py(复用ui/guide_dialog.py的VisibleCheckBox)。
注:磁盘上存在 ui/hand_model/ 目录,但其中只剩未跟踪的 __pycache__/(已删除 __init__.py、state.py 的编译残留),无任何源码 .py,故不列入文件清单(其源码已在开源准备提交 8ea2f44 的死代码清理中移除)。
文件清单
| 文件 | 一句话作用 |
|---|---|
ui/main_window.py |
主窗口:任务/采集双页面栈、设备面板路由(UVC/D435/S80M/蓝牙统一开关)、录制控制、录制历史表、上传与回放入口 |
ui/camera_grid.py |
嵌套 QSplitter 多画面网格容器:画面拖拽调位、分割条调大小、空状态提示 |
ui/camera_widget.py |
单路画面控件:ZoomableVideoWidget(滚轮缩放/拖拽平移/双击还原)+ CameraWidget(信息条、FPS、状态灯、☀ 曝光按钮) |
ui/device_panel.py |
左侧设备检测面板:已连接设备统一分组列表(UVC/D435/S80M/手套/其他蓝牙),勾选开关、双击命名 |
ui/exposure_dialog.py |
每设备曝光设置对话框(自动曝光开关 + 滑块,量程/单位语义由调用方注入) |
ui/glove_widget.py |
BLE 手套仿生手掌画面控件,直接嵌入主网格,录制数据经 pipeline.write_sensor 落盘 |
ui/login_dialog.py |
启动/切换账号登录对话框:账号登录(对话框内同步校验)或游客登录,关闭窗口等价游客;结果经 choice()/server_url()/username()/cookies() 等访问器读取 |
ui/guide_dialog.py |
使用说明窗口(首次启动自动弹出、帮助菜单重开;中英 HTML 模板;「不再显示」持久化到 QSettings)+ VisibleCheckBox 复选组件 |
ui/playback_dialog.py |
录制回放对话框:统一网格同步播放全部视频 + 传感器渲染,深度槽识别、手部关键点叠加、批量删除 |
ui/pose_view_qt.py |
夹爪位姿视图控件:QPainter 手绘的实时轨迹 + 世界坐标轴 + 跟随夹爪的体轴三箭头 + 夹爪模型(两张夹板按开合度张合) |
ui/task_page.py |
启动时的任务选择页:任务表(5 列,含「来源」)、新建/改名/删除本机任务、服务器地址/凭据输入、连接状态、进入采集 |
ui/task_create_dialog.py |
本机任务对话框:新建(名称/目标段数/描述 + 实时校验)与改名(已有录制目录时禁止),错误码本地化 |
ui/upload_dialog.py |
录制会话一键打包上传对话框 |
ui/__init__.py |
空包标记(0 行) |
各文件详解
ui/main_window.py
作用:应用程序主窗口,界面层中枢。组合页面栈(任务选择页 + 数据采集页)、CameraGrid 主网格、左侧 DevicePanel 设备面板(_device_dock:可拖宽/拖窄,带 Closable 故标题栏有 × 可收起;宽度与收起状态经 server_config.json 的 device_dock 段跨启动保持,恢复入口为「视图 ▸ 设备检测栏」/ Ctrl+D;只有用户明确收起(点 × / 走菜单)才记偏好,正常关窗与最小化不算收起)、底部日志 dock、右侧录制历史 dock、工具栏与菜单;启动时经 _show_login_flow 弹 LoginDialog 确定账号/游客身份,首次启动经 _show_guide 弹 GuideDialog 使用说明;持有 CameraPipeline、DeviceScanner、TaskService、UploadManager 与 core.device_manager.DeviceManager 注册表(_workers 为其 entries 别名)等核心对象并负责信号接线。所有设备的开启/关闭都经左侧面板开关按 core.device_manager.dispatch_toggle 分派到 _open_*/_close_* 系列方法:S80M 双目子进程 + 管道解析在 core.s80m_manager.S80MDeviceManager,D435 worker 生命周期与帧口径在 core.d435_manager.D435DeviceManager,手套以 BLE 引擎 + 渲染定时器运行。
模块级辅助与常量(v1.0.3 后多为 core 模块的兼容 re-export,离线测试直接 patch 这些名字):
| 名称 | 签名要点 | 作用 | 返回/副作用 |
|---|---|---|---|
_realsense_short / _slot_base / _normalize_original |
— | core.device_naming 的 realsense_short/slot_base/normalize_original 旧名兼容别名 |
同 core 定义 |
_LEGACY_CAM_NAMES |
常量 | 旧录制记录的退化相机名("多路录制"/"Multi-Camera"/""),历史表自愈判据 |
— |
_STEREO_AVAILABLE |
常量 | core.s80m_manager.STEREO_AVAILABLE 别名(自包含双目模块 tools/stereo_s80m/read_stereo_rgb.py,内含 libfays_vikit.so 3.9.0 与 yaml,与外部 SDK 目录独立) |
— |
_D435_AVAILABLE / D435Worker / list_d400_devices |
常量 | 导入 core.d435_camera 并缓存 d435_available();导入失败全置 None/False(模块全局保留供离线测试 patch) |
— |
_HAND_PROC_AVAILABLE |
常量 | core.hand_processor.SessionHandProcessor + core.auto_labeler.AutoLabeler 可选导入结果 |
— |
_GLOVE_AVAILABLE |
常量 | ui.glove_widget.GloveWidget 可选导入结果(依赖 bleak 子系统) |
— |
类/信号(class MainWindow(QMainWindow)):
| 名称 | 签名要点 | 作用 | 返回/副作用 |
|---|---|---|---|
stereo_frame_ready |
pyqtSignal(str, np.ndarray, object, list) |
双目帧信号(FIFO 读取线程 → 主线程),参数 slot_id, frame, hardware_ns, imu_samples |
连接到 _on_stereo_frame。★ 封送坑:hardware_ns 必须用 object——PyQt5 队列信号把 Python int 按 C++ qint32 封送,超过 2^31(约 2.1s 纳秒)会静默截断为负数,录制数据受害 |
log_message |
pyqtSignal(str) |
跨线程日志信号(后台线程 → 主线程 QTextEdit.append) |
_log() 在非主线程时经此投递 |
_upload_session_deleted |
pyqtSignal(str, str) |
上传后自动删除线程 → 主线程,参数 (session_path, error_msg) |
连接到 _on_upload_session_deleted |
相机管理(设备开启/关闭全在此区):
| 名称 | 签名要点 | 作用 | 返回/副作用 |
|---|---|---|---|
_auto_detect_cameras |
() |
工具栏「🔍 扫描」:立即请求一次设备扫描(接入统一走面板开关,不自动打开画面) | 记日志;DeviceScanner.request_scan() |
_teardown_all_workers |
() |
委托 core.device_manager.DeviceManager.teardown_all(close_fns) 按 kind 分类关闭全部已开启设备 |
清空注册表 |
_open_uvc / _close_uvc |
(dev) -> bool / (dev_key) |
UVC 面板开关:增量建槽(settings._camera_slot_name(dev.video_index))/ 按注册表拆槽;条目经 DeviceManager.uvc_entry 创建 |
bool 成功与否 |
_open_s80m |
(dev) -> bool |
面板开关打开 S80M 双目:先注入静态标定(config/s80m_stereo_calibration.json → StereoCalibration.load),创建 stereo_left/stereo_right 两槽并注册外部帧源(fps=settings.STEREO_RECORD_FPS=30);s80m_depth_available() 时追加第三格 stereo_depth(set_depth_camera,PNG16,v1.0.11),随后子进程/Popen/管道/watchdog/读线程全部交 core.s80m_manager.S80MDeviceManager.spawn |
bool;与 D435 互斥(共用 video0/video2,后开者弹窗拒绝) |
_on_stereo_frame |
(slot_id, frame, hardware_ns=0, imu_samples=None) |
主线程槽(S80MDeviceManager.frame_ready 转投):显示帧直推画面;录制时按 core.s80m_manager.frame_record_decision(1/30s 桶)抽帧;IMU 仅随 stereo_left 落盘 |
pipeline.write_external_frame(...) |
_on_s80m_depth |
(slot_id, depth, hardware_ns=0) |
S80MDeviceManager.depth_ready 槽:depth_to_heatmap(固定色标 + 中值滤波)→ 深度格显示;录制中 pipeline.write_depth |
— |
_close_s80m |
(dev_key) |
委托 S80MDeviceManager.close(停 watchdog → 关 stdin → terminate/kill → join 读线程 → 清理临时文件),再注销两路外部帧源、clear_depth_camera(stereo_depth)、移除 UI 控件、清除本设备标定 |
— |
_open_d435 |
(dev) -> bool |
面板开关打开 RealSense:按 serial 活体复查 list_d400_devices()(不信任导入期缓存)、按 serial 查重、与 S80M 反向冲突检查;槽名 = core.device_naming.allocate_slot_names 派生 {base}_rgb + {base}_depth,RGB 注册外部帧源、深度注册深度伪相机(set_depth_camera,热力图 MP4 + PNG16 双通道,raw_depth=True);worker 创建/信号接线/启动交 core.d435_manager.D435DeviceManager.spawn |
bool;S80M 已开时弹窗拒绝 |
_on_d435_frames |
(slot_id, frame, hardware_ns=0, imu_samples=None, dev_key=None) |
委托 core.d435_manager.D435DeviceManager.process_frame(深度槽 → 热力图显示 + write_depth;RGB 槽 → 显示 + write_external_frame;首帧后标定送进管线 calibration/head_stereo.json) |
已关闭设备的迟到信号直接丢弃 |
_close_d435 |
(dev_key) |
委托 D435DeviceManager.close(断信号 → stop → deleteLater)→ 注销外部帧源 + 清除深度伪相机 + 清除标定 → 移除双槽 |
— |
_add_camera_slot |
(slot_id, camera_index, backend="", label="") |
UVC 建槽:管线 add_camera + 网格 add_camera + _connect_slot_signals |
失败仅记日志 |
_connect_slot_signals |
(slot_id) |
单个 CameraSlot 信号 → UI 回调:frame_ready/state_changed/error_occurred/camera_opened/fps_updated,以及 widget 的 remove_requested/exposure_clicked |
— |
_remove_camera / _remove_camera_slot_no_confirm |
(slot_id) |
移除单槽(录制中先弹确认)/ 不确认版;若槽属于某面板开启设备则整机关闭(d435 移除双槽之一即整机关)并取消勾选 | 同步面板勾选与高亮 |
_remove_all_cameras |
() |
工具栏「✕ 全部移除」:确认后先 _teardown_all_workers() 再逐槽移除 |
清面板勾选/高亮集合 |
设备面板路由与曝光:
| 名称 | 签名要点 | 作用 | 返回/副作用 |
|---|---|---|---|
_device_label |
(dev) staticmethod |
DeviceInfo → 日志显示标签(用户命名优先,有 serial 时附 — serial) |
str |
_on_devices_scanned |
(devices) |
扫描结果(每 2s 轮询,settings.DEVICE_POLL_INTERVAL_MS=2000):填用户命名 → DevicePanel.set_devices + 恢复勾选/高亮 → 插拔 diff 日志(只记变化)→ 录制中拔线暂存 _lost_device_keys 重插恢复勾选 → 已开启但已消失的设备逐个 _handle_active_device_lost |
— |
_handle_active_device_lost |
(key) |
已开启设备被拔出:录制中不自动移除(重插恢复),否则按 kind 关设备 | — |
_on_device_toggled |
(dev, on) |
面板 device_toggled 路由:委托 core.device_manager.dispatch_toggle(录制锁双保险 + kind 路由到 _open_*/_close_* 回调,多路并发,互不拆台);打开失败回退勾选 |
维护 _active_device_keys |
_on_device_renamed |
(dev, name) |
面板双击命名后:刷新插拔标签缓存与 worker 注册表标签 | 持久化已在面板完成 |
_show_exposure_button |
(slot_id) |
在设备的「主槽位」(S80M 左目、D435 的 RGB 槽)信息条显示 ☀ 按钮 | — |
_open_exposure_dialog |
(slot_id) |
☀ 入口:对话框参数口径在 core.exposure_controller.exposure_dialog_params(D435:worker 读回 µs 量程;S80M:1.0~885.0,decimals=1;UVC 无曝光入口固定自动曝光);按类型弹 ExposureDialog |
模态 exec_() |
_apply_exposure |
(dev_key, entry, auto, value) |
委托 core.exposure_controller.apply_exposure(d435 → worker.set_exposure;s80m → _s80m_set_exposure 回调)并持久化(下次开启自动应用) |
— |
_s80m_set_exposure |
(dev_key, auto, value) |
委托 S80MDeviceManager.send_exposure(stdin 行协议 "SET_EXPOSURE <float>",-1.0=自动曝光),SDK 运行时生效无需重启 |
BrokenPipe 时记日志 |
_set_exposure_buttons_enabled |
(enabled) |
录制锁:所有已显示的 ☀ 按钮随录制状态禁用/恢复 | 用 isHidden() 判断(窗口未 show 时 isVisible 恒 False) |
_open_glove / _close_glove |
(dev) -> bool / (dev_key) |
手套开关:广播名 L/R → settings.assign_glove_sensor_role 优先绑左右手;创建 GloveWidget(slot=sensor:{key})注入管线并 start(address),pipeline.register_sensor(role);关闭时停 BLE、撤画面、注销传感器列 |
bool |
_open_ble_placeholder / _close_ble_placeholder |
(dev) -> bool / (dev_key) |
无数据蓝牙(耳机类):主网格显示「该设备无可视化数据」占位画面 | bool |
录制:
| 名称 | 签名要点 | 作用 | 返回/副作用 |
|---|---|---|---|
_reset_s80m_record_state |
() |
委托 DeviceManager.reset_s80m_record_state:录制起止时重置 S80M 50→30 抽帧状态(last_bucket 桶号 + pending_imu 缓冲) |
— |
_build_device_meta |
() -> list |
委托 DeviceManager.build_device_meta:手套无画面槽附 sensor_column;uvc/d435/s80m 附 slots(及 serial);无数据蓝牙不进 devices 段 |
list[dict] |
_record_all |
() |
工具栏「⏺ 全部录制」:任务名取自所选任务;所选任务进度已满(completed_count >= total_required,本地 load_tasks() 计数)时弹窗阻止;否则 pipeline.start_recording(首个槽, task_name=..., batch_index=已完成数+1, device_meta=...) |
— |
_stop_all |
() |
「⏹ 完成录制」:pipeline.finish_recording("") + 重置抽帧状态 |
— |
_abort_recording |
() |
「⛔ 异常终止」:pipeline.abort_recording("")(永久删除本次文件) |
记日志、刷新状态栏 |
信号处理器(管线 → 控件):
| 名称 | 签名要点 | 作用 | 返回/副作用 |
|---|---|---|---|
_on_slot_added / _on_slot_removed |
(_slot_id) |
空实现(控件已在 _add_camera_slot 中创建/移除) |
— |
_on_frame |
(slot_id, frame) |
UVC 帧 → 画面(settings.CAMERA_MIRROR_HORIZONTAL 时先水平镜像) |
w.update_frame |
_on_camera_state |
(slot_id, state) |
状态 → 指示灯 + 日志;CameraState.DISCONNECTED 时 pipeline.record_event(slot_id, "disconnected"),否则记 "connected" |
— |
_on_camera_fps |
(slot_id, fps) |
UVC 由 CameraWorker 自带的 fps 信号 → 标签 |
— |
_note_frame_arrival |
(slot_id) |
记录一帧到达时刻(D435/S80M 无 CameraWorker,主窗口统一计 FPS) | 写入 _fps_ring |
_update_fps_labels |
() |
每秒刷新非 UVC 槽的 FPS(过去 1 秒到达帧数) | — |
_on_recording_started |
(slot_id) |
录制开始:锁死设备面板开关 + 禁用曝光按钮 + 日志(含编码与任务名) | — |
_on_recording_finished |
(slot_id, session_path) |
录制完成:解锁面板/曝光;core.helpers.session_summary(session_path, pipeline.last_recording_frames) 算摄像机列/时长/大小 → RecordingRecord 存库 → 刷新历史;increment_task_completed(实际任务名) 更新任务进度;自动上传入队(开关开启且该会话尚未 completed 时 _upload_manager.add_task);HAND_TRACK_ENABLED 时延迟 500ms 自动处理手部关键点(silent) |
— |
_on_recording_aborted |
(slot_id) |
异常停止:解锁面板/曝光,取消手部后处理与标注 | — |
_on_duration |
(_slot_id, seconds) |
录制时长 → 状态栏 | — |
录制历史:
| 名称 | 签名要点 | 作用 | 返回/副作用 |
|---|---|---|---|
_refresh_history |
() |
RecordingRepo.list_all(limit=100) → 表格 5 列:摄像机/文件/时长/大小/状态;旧记录自愈:退化值(无大小、_LEGACY_CAM_NAMES 相机名、无时长)实时从磁盘 session_summary 补算并回写 DB;状态列按 completed/uploaded/uploaded_deleted 绿色(uploaded 含自愈兜底:completed 且目录仍在、上传任务表记为 completed 的旧行也显示「已上传」)、其余(已丢弃)异常色 |
启动时即加载 |
上传 / 回放入口:
| 名称 | 签名要点 | 作用 | 返回/副作用 |
|---|---|---|---|
_open_playback |
() |
「📂 回放」:PlaybackDialog(self, settings.RECORDING_DIR) 模态打开 |
— |
_open_upload |
() |
「☁ 上传」:UploadDialog(self, settings.RECORDING_DIR, session=TaskService._session) 模态打开 |
— |
_on_upload_task_done |
(task_id) |
自动上传完成:记日志;UPLOAD_DELETE_AFTER 开启时后台线程删本地目录,否则 RecordingRepo.mark_uploaded 标「已上传(本地保留)」并刷新历史 |
— |
_delete_session_after_upload |
(session_path, episode_index=0) |
后台线程 delete_local_session(真删除 + 删除后校验,防大目录阻塞 UI),结果经 _upload_session_deleted 回主线程 |
— |
_on_upload_session_deleted |
(session_path, err) |
删除收尾:失败(含校验残留)记日志;成功 RecordingRepo.delete_local_episode 真删录制行并刷新 |
— |
_on_upload_task_failed |
(task_id, error) |
自动上传失败(重试 3 次后仍失败才触发)→ 日志 | — |
_on_upload_auto_toggled / _on_upload_delete_toggled |
(on) |
两个工具栏开关:settings.save_upload_auto_sync / save_upload_delete_after 持久化 + 运行时立即生效 + 按钮文字/样式刷新 |
— |
_style_toolbar_toggle |
(action) |
开关类 QAction 按钮高亮样式(开启绿底白字加粗) | — |
任务服务与页面切换:
| 名称 | 签名要点 | 作用 | 返回/副作用 |
|---|---|---|---|
_connect_task_service |
() |
TaskService 信号 ↔ TaskSelectionPage:tasks_updated/connection_status/login_result/identity_changed/identity_expired;任务页 refresh_requested→poll_now、switch_account_requested→_on_switch_account;错误首次出现只记一次(_last_task_error 判重) |
轮询在身份确定后(_show_login_flow 末尾)才 start() |
maybe_show_login / _show_login_flow |
() |
启动入口(main.py 调用)→ 登录流程:LoginDialog exec 后 save_server_url;账号登录成功经 TaskService.adopt_login 接管 cookie(save_remembered_username),否则 set_guest;同步 _upload_manager.server_url 与任务页 set_server_display 后 task_service.start() |
— |
_on_switch_account |
() |
任务页「切换账号」按钮 → 重开登录对话框 | — |
_on_identity_expired |
() |
登录态轮询遇 401/403 → 提示重登;关闭对话框(Esc)自动降级游客 | — |
_on_task_confirmed |
(task) |
选中任务进入采集页(index 1),更新窗口标题;不自动扫描,提示先选模式再点扫描 | — |
_on_page_changed |
(index) |
页面切换控制工具栏/dock 可见性;任务页停设备轮询,采集页立即扫描 + 启动 _device_timer(2s 轮询);末尾统一调 _apply_device_dock_visibility(设备栏:任务页隐藏、采集页显示,但用户收起过就保持收起) |
— |
_go_back_to_task_selection |
() |
返回任务选择:录制中先确认(同意则 abort);取消手部后处理/标注;_teardown_all_workers + 清网格 + 清面板状态;切回 index 0 并 poll_now() 刷新任务列表 |
— |
其余(语言/视图/手部后处理/日志/退出):
| 名称 | 签名要点 | 作用 | 返回/副作用 |
|---|---|---|---|
_on_language_changed |
(lang) |
中英文切换:菜单/工具栏/dock 标题/历史表头/手部模式下拉全部 tr() 重刷,并调 _task_page._on_language_changed |
— |
_reset_camera_sizes |
() |
视图菜单「重置画面大小」:grid._rebuild_layout() 等分 |
— |
_apply_device_dock_visibility |
() |
设备栏可见性的唯一口径:当前页==1 且 用户未收起 → 先 _capture_device_dock_width(收起前记住栏宽)再 setVisible,显示且非浮动态才 resizeDocks 复原宽度;期间置 _device_dock_syncing 挡「程序化改动被当成用户收起」 |
末尾同步菜单勾选 |
_toggle_device_dock |
(checked) |
视图菜单「设备检测栏」/ Ctrl+D:置 _device_dock_user_hidden → 应用可见性 → 落盘 |
任务页只记偏好不显示 |
_on_device_dock_visibility |
(visible) |
设备栏可见性变化 → 同步菜单勾选 + 落盘(幂等)。不从可见性推断用户意图:关窗/最小化/切页都会让 dock 不可见,把它们当成「用户收起」会让每次正常退出都写坏偏好;_device_dock_syncing 置位时直接返回 |
用户收起只由 × 的 Close 认定(见 eventFilter) |
_sync_device_dock_action |
() |
菜单勾选 = 用户是否想看设备栏(与当前页是否显示无关;blockSignals 防递归,C++ action 已删时吞 RuntimeError) |
— |
_save_device_dock_prefs |
() |
_capture_device_dock_width() 后 settings.save_device_dock_prefs(未收起, 栏宽);写盘失败只丢一次偏好不抛异常(PyQt5 虚方法抛异常 = SIGABRT 134,会连带跳过停线程/拆 worker) |
closeEvent 也调一次 |
_capture_device_dock_width |
() |
抓「此刻真实栏宽」:浮动态(width() 是浮动窗口宽)与不可见时直接返回。不跟随普通 Resize —— 窗口缩小/最大化时 Qt 会把 dock 压到最小宽,那不是用户拖出来的宽度 | 收起前 / 退出前各抓一次 |
eventFilter |
(obj, event) |
仅拦 _device_dock 的 QEvent.Close → 置 _device_dock_user_hidden=True + 抓栏宽,这是唯一认定「用户收起」的地方(点标题栏 × / 右键菜单关闭 / 浮动窗口关闭都走 close() 发 Close 事件;而主窗口关闭/最小化只发 Hide,不给 dock 发 Close —— 2026-10-13 实测) |
其余事件走 super() |
_update_status |
() |
状态栏:摄像机数(排除 sensor:/ble: 占位槽)+ 录制中标记与颜色 |
— |
_process_hand_keypoints |
(session_path="", silent=False) |
后台提取手部关键点(SessionHandProcessor.process_session,mode 由手套/裸手下拉决定);已有结果时弹确认(silent 模式跳过);无路径用最近一条录制 |
— |
_on_hand_proc_progress / _on_hand_proc_finished |
(current, total) / (session_path, error) |
进度 → 状态栏+按钮;完成后 300ms 自动触发 _auto_label(silent) |
— |
_auto_label / _on_auto_label_finished |
(session_path="", silent=False) / (session_path, error) |
基于关键点 parquet 自动生成手势标签(AutoLabeler.label_session) |
— |
_log |
(msg) |
时间戳日志:主线程直接 append,后台线程经 log_message 信号安全投递 |
— |
_show_about |
() |
关于对话框(应用名/版本/功能简介) | — |
closeEvent |
(event) |
安全退出:置 _shutting_down → 停轮询/扫描器 → 录制中 abort → 拆全部 worker → 移除全部槽 → 取消手部后处理 → 停 TaskService/UploadManager |
accept |
关键数据:
_workers设备注册表:DeviceManager.entries的别名,key → {"kind": "uvc"|"d435"|"s80m"|"data_ble"|"ble", "slots": [网格槽位], "label": 日志标签},另含 kind 专属字段(d435:worker/rgb_slot/depth_slot/near_mm/far_mm/smooth_k/temporal_alpha/heat_smoother/calib_sent/serial;s80m:proc/stdin/watchdog/reader_thread/stderr_file/config_file/original_exp/entry_depth/last_bucket/pending_imu;data_ble:sensor_column/glove)。- 设备面板状态:
_active_device_keys(勾选集合=UI 状态源)、_lost_device_keys(录制中拔线暂存)、_last_device_keys(key→标签,插拔 diff 日志)。 - FPS:
_fps_ring(slot_id → 到达时刻 deque),1s 定时刷新。 - 历史表列名:
摄像机 / 文件 / 时长 / 大小 / 状态。 - S80M 管道帧格式与曝光行协议见
core/s80m_manager.py,50→30 抽帧桶长settings.STEREO_RECORD_MIN_INTERVAL_S。 - D435 槽名约定:RGB 槽 = 前缀 +
_rgb,深度槽 = 前缀本身若以_depth结尾否则补_depth;depth后缀仅用于回放过滤,是否落盘由显式注册决定。 - meta 写入点:
_build_device_meta()产出devices段(key/kind/name/slots/serial/sensor_column)。
调用关系:被 main.py 构造。创建并持有:core.pipeline.CameraPipeline、core.device_detector.DeviceScanner、core.device_manager.DeviceManager、core.s80m_manager.S80MDeviceManager、core.d435_manager.D435DeviceManager、core.task_service.TaskService、core.uploader.UploadManager、core.recording_repository.RecordingRepo(静态调用)、ui 内全部其余组件。调用 core.d435_camera.D435Worker、core.stereo_depth.StereoDepthComputer/depth_to_heatmap/DepthHeatmapSmoother、core.calibration.StereoCalibration、core.task_record.load_tasks/increment_task_completed、core.helpers.session_summary/format_duration/format_size_mb/hand_kpts_parquet_path、core.device_naming/core.exposure_controller 口径;可选手部子系统 core.hand_processor.SessionHandProcessor、core.auto_labeler.AutoLabeler。
ui/camera_grid.py
作用:多画面网格容器 CameraGrid(继承 QScrollArea),把每路画面排进嵌套 QSplitter 布局;所有画面(相机 CameraWidget、回放传感器格、手套仿生手掌、占位画面)同格共存。提供两种交互:按住画面顶部信息条拖到另一画面上松手即互换位置(事件过滤器 + 鼠标抓取);画面间的分割条加宽(8px)并带悬停/按住高亮,可拖动调大小。
类/函数:
| 名称 | 签名要点 | 作用 | 返回/副作用 |
|---|---|---|---|
add_camera |
(slot_id: str, camera_name: str = "") |
添加一个 CameraWidget 到网格 |
返回 CameraWidget |
add_widget |
(slot_id: str, widget: QWidget) |
添加任意控件(手套仿生手掌/传感器格/占位等);重复 slot_id 直接返回已有控件 | 返回 QWidget;触发重建布局 |
remove_camera |
(slot_id: str) |
移除指定控件:先 _end_drag() 复位拖拽状态(防抓取/光标残留),再 deleteLater() |
触发重建布局 |
camera_widget |
(slot_id: str) |
按 slot_id 取控件 | Optional[CameraWidget] |
clear |
() |
移除所有画面 | — |
widget_count |
() -> int |
当前画面数量 | int |
slot_ids |
() -> List[str] |
全部 slot_id 列表(字典顺序即布局顺序) | list |
eventFilter |
(obj, ev) |
拖拽手柄事件:顶部 DRAG_STRIP_H(32px)内按下左键→grabMouse 抓取 + 闭合手掌光标;移动→悬停目标高亮(DRAG_TARGET_BORDER);松开→_swap_widgets 互换位置;双击信息条不触发缩放还原;带 _no_drag 属性的控件(曝光按钮、模式下拉等)直接放行 |
消费事件时返回 True |
_rebuild_layout |
() |
按画面数重建布局:0 路=空提示文案(默认 i18n「尚未检测到摄像机…」,可 empty_text 覆盖);1 路=填满;2 路=[A|B];3 路=[A|B]/[C];4 路=2×2;5+ 路按 ceil(sqrt(n)) 行列嵌套分割;初始等分 |
— |
_install_drag_filter / _remove_drag_filter |
(w) |
给画面控件及其全部子控件装/卸事件过滤器(子控件才真正接收鼠标事件) | — |
_root_widget / _in_drag_strip / _widget_at_global |
辅助 | 事件来源控件沿父链找所属画面 / 判断全局坐标是否落在信息条内 / 命中画面 | QWidget 或 None |
_set_drag_hover / _swap_widgets / _end_drag |
辅助 | 高亮悬停目标(恢复旧样式)/ 互换字典顺序并重建 / 释放抓取、还原光标与高亮 | — |
关键数据:
| 名称 | 值 | 说明 |
|---|---|---|
DRAG_STRIP_H |
32 | 每画面顶部拖拽手柄信息条高度(像素) |
SPLITTER_HANDLE_WIDTH |
8 | 分割条宽度(被 ui/playback_dialog.py:29 复用) |
SPLITTER_HANDLE_QSS |
灰条 + 悬停/按住高亮 | 暗色主题下分割条可见性(被 ui/playback_dialog.py:29 复用) |
DRAG_TARGET_BORDER |
border:2px solid #4FC3F7 |
拖拽悬停目标边框高亮 |
调用关系:被 ui/main_window.py(主网格)、ui/playback_dialog.py(回放统一网格)使用;tools/tests/exposure_control_test.py、tools/tests/grid_drag_fps_test.py 直接构造。依赖 ui/camera_widget.py 的 CameraWidget 与 config.i18n.tr(空提示文案)。
ui/camera_widget.py
作用:单路画面的两个控件。ZoomableVideoWidget 是纯视频显示画布:接收 BGR np.ndarray 帧,内部 BGR→RGB 转 QImage.Format_RGB888(Qt x86 上 Format_BGR888 会红蓝互换),支持滚轮缩放(以鼠标位置为基准)、左键拖拽平移、双击还原、缩放百分比提示。CameraWidget 把视频画布 + 顶部覆盖条(名称、☀ 曝光按钮、FPS 标签、状态灯)组合成一路「画面」,供主网格与回放共用。
类/函数:
| 名称 | 签名要点 | 作用 | 返回/副作用 |
|---|---|---|---|
ZoomableVideoWidget.reset_view |
() |
还原缩放(1.0)与平移(0,0) | 重绘 |
ZoomableVideoWidget.set_frame |
(bgr_frame: np.ndarray, flip_vertical: bool = False) |
传入 BGR 帧:可选上下翻转(摄像机视频需要,传感器渲染不需要),BGR→RGB 后存为 QPixmap |
触发重绘 |
ZoomableVideoWidget.set_status_text |
(text: str) |
设置无画面时居中显示的文字(如「无信号」「已断开」) | 清空画面 |
ZoomableVideoWidget.wheelEvent |
(event) |
滚轮缩放:1.08×/级,范围 0.25×~8.0×,以鼠标位置为基准调偏移,右下角短暂显示百分比 | 重绘 |
ZoomableVideoWidget.mousePressEvent / mouseMoveEvent / mouseReleaseEvent / mouseDoubleClickEvent |
(event) |
左键拖拽平移(闭合手掌光标);双击 reset_view |
— |
CameraWidget(信号) |
remove_requested(str) / exposure_clicked(str) |
请求移除此摄像机 / 点击 ☀(主窗口按槽位所属设备弹曝光对话框) | 由 MainWindow._connect_slot_signals 连接 |
CameraWidget.update_frame |
(bgr_frame) |
相机采集帧更新显示(flip_vertical=True 适配安装方向) |
— |
CameraWidget.update_fps |
(fps: float) |
更新 FPS: n 标签 |
— |
CameraWidget.set_frame_number |
(n: int) |
回放用:信息条右侧显示当前帧号 #n(复用 FPS 标签位) |
— |
CameraWidget.set_camera_state |
(state: str) |
recording/error/disconnected/idle → 状态灯颜色与「已断开/等待中…」文字 |
— |
CameraWidget.set_exposure_button_visible / set_exposure_enabled |
(visible: bool) / (enabled: bool) |
显示/隐藏 ☀(只对设备主槽位);启用/禁用(录制中禁用,tooltip 随之切换) | — |
CameraWidget._refresh_texts |
(_lang="") |
语言切换刷新文案 | 连接 lang_manager.language_changed |
CameraWidget.resizeEvent |
(event) |
调整覆盖条宽度与位置 | — |
关键数据:
ZOOM_MIN = 0.25、ZOOM_MAX = 8.0、ZOOM_STEP = 1.08。- 覆盖条:固定高 28px、初始宽 260px(随 resize 收缩到 ≤300px),位于画面左上角;刻意不设
WA_TransparentForMouseEvents(会连 ☀ 按钮一起屏蔽),画面信息条拖拽由CameraGrid事件过滤器统一拦截。 - ☀ 按钮带
_no_drag属性标记,CameraGrid.eventFilter对按下直接放行(不触发拖拽/双击还原)。 - 注意:
reset_view在文件中定义了两次(第 74-78 行与第 109-113 行),后者覆盖前者,行为一致。
调用关系:被 ui/camera_grid.py、ui/glove_widget.py(GloveWidget 继承 CameraWidget)、ui/playback_dialog.py(ZoomableVideoWidget)使用。依赖 config.settings(FEED_MIN_WIDTH 等尺寸与颜色常量)、config.i18n.tr/lang_manager、core.helpers.format_duration(导入存在,当前代码未直接调用)。
ui/device_panel.py
作用:左侧停靠的设备检测面板。DeviceScanner(core/device_detector)每 2s 扫描的结果由 MainWindow._on_devices_scanned 调 set_devices 重建列表;三组展示(📷 相机 / 🧤 手套 / 🎧 其他蓝牙),「其他蓝牙」默认折叠、空组隐藏;每台设备带独立 QCheckBox,勾选发射 device_toggled(DeviceInfo, bool) 由主窗口路由到主网格显示;双击条目弹命名框并持久化到 device_names.json(此后每次连接显示用户命名)。
类/函数(class DevicePanel(QWidget)):
| 名称 | 签名要点 | 作用 | 返回/副作用 |
|---|---|---|---|
device_toggled / device_renamed(信号) |
pyqtSignal(object, bool) / pyqtSignal(object, str) |
勾选变化(DeviceInfo, checked)/ 双击命名完成(DeviceInfo, new_name) | 由 MainWindow 连接 |
set_devices |
(devices: list) |
blockSignals 下清空重建分组树;勾选状态由随后的 set_checked_keys 恢复;组展开状态跨重建保留 |
空列表显示「未检测到设备」占位 |
set_checked_keys |
(keys: set) |
按 key 集合静默勾选(不发信号) | — |
set_checked |
(key: str, on: bool) |
程序化勾选单个(不发信号,供失败回退) | — |
checked_keys |
() -> set |
当前勾选的设备 key 集合 | set(开关状态=UI 状态源) |
set_active_keys |
(keys: set) |
高亮正在显示中的设备(绿色前景),其余还原;空集合全还原 | — |
set_active_key |
(key) |
兼容旧调用:单 key 高亮(None 全还原) | — |
set_locked |
(on: bool) |
录制中锁死全部开关(去 Qt.ItemIsEnabled,命名同步禁用) |
— |
key_for_kind |
(kind: str) |
列表中第一个 kind 匹配的设备 key | str 或 None |
device_for_key |
(key: str) |
key → DeviceInfo(存在 Qt.UserRole) |
DeviceInfo 或 None |
key_for_serial |
(serial: str) |
按序列号找 key(多台 RealSense 时精确高亮) | str 或 None |
key_for_video_index |
(video_index: int) |
第一个 video_index 匹配的设备 key |
str 或 None |
refresh_texts |
() |
语言切换刷新提示文字、组标题与占位项 | — |
_row_text |
(dev) -> str |
行文本 = 图标 + label(+ — serial) |
— |
_on_item_changed |
(item, column) |
勾选状态真正变化(用 _last_check 挡文字类变化与组标题)才发射 device_toggled |
— |
_on_item_double_clicked |
(item, _column) |
双击 → QInputDialog 命名 → settings.save_device_name(dev.stable_key, name) 持久化 → 更新行文本 + device_renamed;对话框打开期间列表可能已被 2s 轮询重建,必须用当前列表里的条目更新 |
— |
gripper_recalibration_requested(信号) |
pyqtSignal(object) |
夹爪条目右键选「重新读取出厂标定」(v1.3.4) | 由 MainWindow 连接 |
gripper_binding_requested(信号) |
pyqtSignal(object) |
夹爪条目右键选「读取/写入 Fays 绑定序列号」 | 由 MainWindow 连接 |
gripper_diagnostics_requested(信号) |
pyqtSignal(object) |
夹爪条目右键选「ESP32 串口诊断」 | 由 MainWindow 连接 |
_on_context_menu |
(pos) |
Qt.CustomContextMenu 回调:仅 dev.group == "gripper" 且非组标题/占位项才弹菜单(表驱动三项,顺序=重读标定 / 配对序列号 / 串口诊断,顺序是契约的一部分,见 test_gripper_calibration_autogen.py §14);_locked(录制中)时菜单项 setEnabled(False) 且选中后直接返回;按 chosen is action 判等,只弹不选不发信号 |
— |
_refresh_hint_text |
() |
底部提示:有夹爪时追加「;右键夹爪可重读标定 / 配对序列号 / 串口诊断」 | 由 refresh_texts 调用 |
关键数据:
_ICON = {"uvc": "📹", "d435": "🔭", "s80m": "👁", "data_ble": "🧤", "usb_glove": "🧤", "ble": "🎧", "gripper": "🤖"};_GROUP_ORDER = ["camera", "glove", "gripper", "other_ble"];_GROUP_TITLE对应中文组标题。_items:device key →QTreeWidgetItem;_last_check:key → 上次勾选状态(防itemChanged误触发);_group_expanded:组展开状态(初始 camera/glove 展开、other_ble 折叠);_has_gripper:列表里是否有夹爪(决定底部提示是否提右键入口,两个分支都要更新,空设备分支需重置)。- ⚠️ 面板内所有 QLabel 提示都必须
setWordWrap(True):不换行的 QLabel,其minimumSizeHint().width()就是整行文字宽,会成为面板与停靠栏的硬地板。底部提示那句中文(「双击设备可重命名;右键夹爪可重读标定 / 配对序列号 / 串口诊断」)曾在夹爪在场时把栏宽地板顶到 289px,导致设备栏只能拖宽、拖不窄(2026-10-13 修);面板另设setMinimumWidth(settings.DEVICE_DOCK_MIN_WIDTH)作为可拖到的最窄。回归哨兵见tools/tests/device_dock_collapse_test.py。 - 命名持久化:
config.settings.save_device_name(device_names.json)。
调用关系:由 ui/main_window.py 构造并连接 device_toggled/device_renamed/gripper_recalibration_requested/gripper_binding_requested/gripper_diagnostics_requested(信号名按字符串 connect,写错不会报错、只是永远不触发 —— tools/tests/test_gripper_esp_binding.py §9 静态比对两处名字);tools/tests/device_panel_gui_smoke_test.py 直接构造。依赖 config.settings.save_device_name、config.i18n.tr。夹爪右键三条链路的完整链路(关夹爪 → 后台线程 → 完成后按原样开回)见 docs/core.md「夹爪」节与 ui/main_window.py 的 _on_gripper_recalibration / _on_gripper_binding / _on_gripper_diagnostics;三条共用 _gripper_offline_guard(序列号+录制检查)、_gripper_offline_take_over(让出串口)、_reopen_after_offline(善后)三件骨架。
ui/exposure_dialog.py
作用:每设备曝光设置模态对话框:自动曝光开关 + 曝光值滑块(内部统一 0..1000 刻度映射),拖动即时下发 apply_requested(auto, value),勾选自动曝光立即生效。范围/值语义由调用方(MainWindow)按设备类型注入:UVC 是 V4L2 原始曝光值、D435/D405 是 µs(流启动后读 rs.option.exposure 量程)、S80M 是 SDK 曝光值 1.0~885.0(与 yaml stereo_init_exposure 同单位)。对话框只发参数不改持久化(持久化由主窗口统一处理)。
类/函数(class ExposureDialog(QDialog)):
| 名称 | 签名要点 | 作用 | 返回/副作用 |
|---|---|---|---|
apply_requested(信号) |
pyqtSignal(bool, float) |
参数变化:apply(auto, value)(auto=True 时 value 忽略) |
由 MainWindow._open_exposure_dialog 连接到 _apply_exposure |
__init__ |
(parent, title, vmin, vmax, value, auto, decimals=0, original=None) |
构造:自动曝光勾选框 + 0..1000 滑块 + 数值标签 + 「恢复默认」按钮(仅传入 original 时显示)+ 关闭 |
模态 |
_to_ticks / _from_ticks |
(value) -> int / (ticks) -> float |
曝光值 ↔ 0..1000 刻度线性映射 | — |
_fmt |
(value) -> str |
按 decimals 格式化数值标签 |
— |
_on_auto_toggled |
(checked) |
自动曝光开关:禁用/启用滑块并立即 apply_requested.emit(checked, 当前值) |
— |
_on_moved / _on_released |
(ticks) / () |
拖动中/松手时更新值并下发(auto 时忽略) |
— |
_on_reset |
() |
「恢复默认」:控件静默(blockSignals)回到「最一开始」曝光基线 (auto, value) 并下发一次 |
— |
关键数据:_original = 设备首次开启、应用任何设置之前读回的 (auto, value) 基线(来源为 settings.device_original 或 worker 读回值,经 core.device_naming.normalize_original 归一化)。
调用关系:由 ui/main_window.py(_open_exposure_dialog)按设备类型构造并 exec_();tools/tests/exposure_control_test.py 直接构造。依赖 config.i18n.tr。
ui/glove_widget.py
作用:BLE 手套仿生手掌画面控件(继承 CameraWidget,复用其覆盖条),直接嵌入主网格,替代旧的底部传感器 dock。面板开关打开手套后由主窗口创建;内部持有 core.ble_engine.SensorBLEEngine,30ms 渲染定时器处理一帧数据并用 core.render_engine.render_hand 画成 BGR 帧显示;录制中数据经 pipeline.write_sensor 写入 parquet 对应传感器列。
类/函数(class GloveWidget(CameraWidget)):
| 名称 | 签名要点 | 作用 | 返回/副作用 |
|---|---|---|---|
__init__ |
(slot_id, address, sensor_column, label="", parent=None) |
固定 _RENDER_W=1280×_RENDER_H=720 画布;左/右手套配置由 core.sensor_hand_config.load_sensor_hand_config(sensor_column) 加载(左/右不同配置文件);启动 30ms 渲染定时器 |
— |
start |
(address: str = "") |
连接 BLE 设备(懒创建引擎并连接 connected/disconnected/fps_updated/calibration_progress/error_occurred 信号) |
画面显示「连接中…」 |
stop |
() |
断开连接、停止渲染 | 画面显示「已断开」 |
set_pipeline |
(pipeline) |
主窗口注入/清除当前录制管线引用 | — |
_on_connected / _on_disconnected |
(addr) / () |
连接状态 → 画面文字 + pipeline.record_event(sensor_column, "connected"/"disconnected") |
— |
_on_error |
(msg) |
引擎错误显示到画面状态栏(失败原因不再只有控制台可见) | — |
_on_fps |
(fps) |
硬件 FPS 显示到信息条(HW: n) |
— |
_on_calib_progress |
(progress) |
校准进度显示(校准中… n% / 校准完成) |
— |
_render_tick |
() |
定时器触发:engine.process_frame() 拿一帧;录制中 pipeline.write_sensor(processed, capture_ts, sensor_name=sensor_column);render_hand(...) 渲染并传给画面(逻辑平移自旧 SensorPanel) |
— |
_display_frame |
(frame) |
渲染帧叠加传感器名 + 硬件 FPS + 数据帧龄(Age: n ms,按 <100/<300ms 绿/黄/红) |
— |
关键数据:
- 渲染配置常量
CONFIG_FILE/CONFIG_FILE_LEFT定义在core.render_engine(指向config/sensors/hand_ble_config.json与hand_ble_config_left.json);加载与合并口径在core.sensor_hand_config.load_sensor_hand_config。 _current_vmax初始 5000.0,跨帧回传保持色标稳定。
调用关系:由 ui/main_window.py(try/except 可选导入)在 _open_glove 中创建。依赖 core.ble_engine.SensorBLEEngine、core.render_engine、core.sensor_hand_config、ui.camera_widget.CameraWidget;tools/tests/glove_widget_test.py import 本模块做自检。
ui/login_dialog.py
作用:登录对话框——启动时(或任务页点「切换账号」)弹出,确定会话身份。账号登录在对话框内同步校验(TaskService.verify_credentials,独立 requests.Session,≤8s),失败就地提示可重试,成功后携带 auth cookie 返回;游客登录直接进入(仅见公共任务);关闭窗口(Esc/X)等价游客——后端不可达时不把用户锁死在启动页。
类/函数(class LoginDialog(QDialog)):
| 名称 | 签名要点 | 作用 | 返回/副作用 |
|---|---|---|---|
choice |
() -> str |
结果:"login"(点登录且校验成功)或 "guest"(游客/关闭窗口) | str |
server_url |
() -> str |
地址输入框内容;空则回退 settings.SERVER_URL |
str |
username / password |
() -> str |
账号/密码输入框内容 | str |
remember_checked |
() -> bool |
「记住账号」勾选状态 | bool |
cookies |
() |
登录成功后的 auth cookie(失败/游客为 None) | cookie 或 None |
_on_login_clicked |
() |
空用户名就地提示;同步校验(按钮置「登录中…」防误以为卡死),成功记 cookie 并 accept,失败 _show_error |
— |
_on_guest_clicked |
() |
choice="guest" 并 accept | — |
关键数据:表单回填——服务器地址(与出厂默认相同则不回填、以 placeholder 显示)、用户名(settings.load_remembered_username);错误提示仅登录失败时显示。
调用关系:由 ui/main_window.py(_show_login_flow)构造。依赖 config.settings、config.i18n.tr、core.task_service.TaskService.verify_credentials、ui.guide_dialog.VisibleCheckBox。
ui/guide_dialog.py
作用:使用说明窗口——首次启动自动弹出(guide/shown_once 未置位,或环境变量 DAQ_SHOW_GUIDE=1 强制),也可从菜单 帮助 → 使用说明 随时重开;「下次启动不再自动显示」勾选持久化到 QSettings(guide/dont_show,取消勾选即恢复自动弹出;每次关闭按当前勾选落盘并 sync() 立即写盘)。正文为整段 HTML 使用步骤(登录 → 设备 → 录制 → 回放 → 上传),按当前界面语言选择 GUIDE_HTML_ZH/GUIDE_HTML_EN 模板,与根目录 使用说明.md 内容一致。
类/函数:
| 名称 | 签名要点 | 作用 | 返回/副作用 |
|---|---|---|---|
guide_html |
() -> str |
按 lang_manager.current 返回当前语言的 HTML 正文 |
str |
VisibleCheckBox |
(QCheckBox) |
暗色主题下勾选指示不明显:指示框加显式描边/填充,勾选时自绘白色 ✓(login_dialog.py 复用) |
— |
GuideDialog |
(parent=None) |
760×640 对话框:标题(APP_NAME + 版本)、QTextBrowser 渲染 HTML、底部 VisibleCheckBox + 「关闭」 |
— |
dont_show_checked |
() -> bool |
供调用方持久化「下次不再显示」 | bool |
调用关系:由 ui/main_window.py(_show_guide,惰性 import)构造;VisibleCheckBox 被 ui/login_dialog.py 复用。依赖 config.settings、config.i18n。
ui/playback_dialog.py
作用:本地录制回放对话框。左侧会话列表(多选、按上传状态筛选、批量移入回收站),右侧复用 CameraGrid 的统一网格:摄像机格与传感器格同格共存,可拖拽调位、分割条调大小;底部播放控制(播放/暂停、逐帧、进度条、倍速 0.25~4×、手部关键点叠加开关、追踪模式、提取关键点)。时间戳对齐经 Parquet 统一时间线(frame_idx 与 timestamp_us);会话加载(parquet 合并 + 手部关键点)全部放后台线程,视频打开与播放留在主线程(OpenCV FFmpeg 后端不承诺跨线程安全)。
模块级:
| 名称 | 签名要点 | 作用 | 返回/副作用 |
|---|---|---|---|
_get_effective_fps |
(info: dict) -> float |
core.session_catalog.get_effective_fps 的兼容别名(离线测试仍按旧名 import) |
float |
类/函数:
| 名称 | 签名要点 | 作用 | 返回/副作用 |
|---|---|---|---|
SessionLoader |
(parent=None) QObject |
core.session_loader.SessionLoader re-export:后台加载器,parquet 读取全在 worker 线程,loaded(str, object, int) 携带 {"timeline": SensorTimeline, "hand_kpts": dict};load(session_dir, gen) 已有加载在跑时排队(新请求覆盖旧请求,过期结果被 gen 丢弃);cancel() 取消 |
信号 loaded/failed(int, str) |
PlaybackSensorWidget |
(sensor_name, title="", parent=None) QFrame |
单路传感器回放格:标题 + 模式下拉(🔥 热力图/📝 轨迹/📊 网格/🦾 仿生手掌/🕸 形变网格,默认仿生手掌)+ ZoomableVideoWidget + TS 标签;头部行 ≈32px 与网格拖拽手柄区对齐,mode_combo/ts_label 标 _no_drag 放行点击 |
— |
PlaybackDialog._toggle_fullscreen |
() |
F11 切换全屏(QShortcut WindowShortcut 上下文,任意子控件聚焦时可用) |
— |
PlaybackDialog._rebuild_grid |
() |
按当前会话重建统一网格:先摄像机格(双目四路 stereo_left/right + _aux 保持 2×2 特殊排列),后传感器格(slot_id=sensor:{name},与主窗口手套格约定一致);摄像机格隐藏状态灯 |
— |
_browse_dir / _refresh_list |
() |
选择录制目录 / 重建会话列表(core.session_catalog.list_sessions;每项含上传状态图标 ✅/❌/⬜、路数、FPS、时长、任务 tag) |
— |
_on_session_clicked |
(item) |
点击会话 → _load_session(path) |
— |
_filter_by_upload |
(uploaded: bool) |
「⬆ 选中已上传」/「⬇ 选中未上传」:按 UploadManager.get_upload_status 勾选 |
— |
_delete_selected |
() |
勾选会话移入回收站:先停播并释放全部 cv2.VideoCapture 句柄(否则 Windows 阻止删除)+ 强制 GC;core.helpers.send_to_recycle_bin 逐个移入 |
可逆 |
_load_session |
(session_dir) |
加载会话:主线程同步读元数据(core.session_catalog.load_session_meta:格式探测 → EgoData metadata.json 或 meta/info.json;传感器列表读 info["sensors"],无则从 info["features"] 的 observation.* 键推断,最旧格式回退 ["state"]);parquet 合并 + 关键点交 SessionLoader 后台加载 |
— |
_on_session_loaded |
(gen, payload) |
主线程槽:过期 gen 丢弃;过滤幽灵传感器列(时间线中确有 16×16=256 宽压力矩阵的才留,防 observation.imu 等非手套特征被误判);打开每路 MP4(深度槽取 depth/<slot>/<slot>.mp4);用户命名叠加(device_names + devices 段 slots/sensor_column → name);算主时钟(info fps → 视频实际 fps → 全局兜底)、每路总帧数;重建网格、_seek(0) |
— |
_on_session_load_failed |
(gen, error) |
后台加载失败 → 信息标签 | — |
_toggle_play / _stop |
() |
播放/暂停切换;_stop 也用于切会话前收尾 |
— |
_tick |
() |
定时器推进:按真实流逝时间补帧追赶(主线程被 seek/渲染拖慢时保持原速);到末尾自动停 | — |
_prev_frame / _next_frame |
() |
逐帧进退 | — |
_seek |
(idx: int) |
主时钟帧号 → 每路帧号(t_s × fps_i 四舍五入,低帧率路按比例抽帧);小步前进(≤5 帧,定时器抖动/追赶所致)顺序 read() 到底(每帧 1-2ms),随机大跳(拖进度条)才 set+read seek(43-89ms/路)——小步也 seek 会因 seek 耗时把追赶 steps 推大形成死亡螺旋(HEVC B 帧 seek 更贵);随后 _update_sensor + 时间标签 |
— |
_on_slider_pressed / _on_slider_moved / _on_slider_released |
(value) |
拖动中不 seek(每秒数十个 move 事件 × 单次 90-180ms 会卡死),松手单次 seek;点击滑槽未拖动也跳到点击处;原在播则恢复播放 | — |
_set_video_frame |
(slot_id, frame, frame_num) |
一帧显示到摄像机格 + 帧号 #n;可选手部关键点叠加(draw_kpts_overlay);MP4 帧录制时已 np.flip(axis=0) 过,回放不再翻转 |
— |
_on_sensor_mode_changed |
(sensor_idx, mode_idx) |
切换传感器可视化模式(heatmap/trace/grid/hand/deform),trace 时 clear_trace_canvas(),vmax 重置 5000.0 |
— |
_update_sensor |
(frame_idx, t_s=0.0) |
渲染全部传感器:多帧率混合会话按主时钟时间二分(nearest_for_column_time),帧率一致会话走帧号二分(nearest_for_column,时间戳含暂停负跳变时更稳);16×16 矩阵按模式调 render_heatmap/trace/grid/hand/deform_mesh;TS 标签显示传感器时间戳与 Δ(ms/帧) |
— |
_cycle_speed |
() |
倍速循环 0.25/0.5/1/2/4×,播放中即时改定时器间隔 | — |
_toggle_hand_overlay |
(checked) |
手部关键点叠加开关 → 重刷当前帧 | — |
_process_current_kpts / _on_hand_proc_finished |
() / (session_path, error) |
对当前会话后台提取手部关键点(主线程延迟创建 SessionHandProcessor 保证信号正常);完成后 kpts-only 重载(load_timeline=False) |
— |
closeEvent |
(event) |
置 _closing(迟到 loader 回调丢弃)、取消手部处理、停播、释放全部 VideoCapture |
— |
关键数据:
- 深度槽判定(
_on_session_loaded):cameras[slot]的type == "depth"或槽名以_depth结尾(槽名约定与主窗口_open_d435一致);深度槽仅当其热力图 MP4(depth/<slot>/<slot>.mp4)存在时加入回放列表。 - 双目四路特殊排序:
_STEREO_IDS = {"stereo_left", "stereo_right", "stereo_left_aux", "stereo_right_aux"}四路齐备时按固定顺序排列。 - 传感器状态列表:
_sensor_modes/_sensor_vmax_list/_sensor_mesh_states/_sensor_widgets/_sensor_ts_labels/_sensor_hand_configs/_sensor_cells按info["sensors"]动态创建。 - meta 字段读取点:
cameras[].fps、cameras[].type、sensors、features、device_names、devices[].slots/sensor_column/name/kind、task_name、fps(info["fps"]兜底);EgoData 会话的 devices 全量段在metadata.json、device_names 在meta/info.json,两处合并读。 - 会话列表项把上传状态存在
Qt.UserRole + 1。
调用关系:由 ui/main_window.py(_open_playback)构造;tools/tests/test_playback_multifps.py、tools/tests/d435_playback_test.py 直接构造。依赖 core.session_timeline.load_timeline/SensorTimeline、core.session_catalog(fps/扫描/元数据)、core.session_loader.SessionLoader、core.render_engine 五种渲染器与 CONFIG_FILE/CONFIG_FILE_LEFT、core.sensor_hand_config、core.helpers 路径与扫描函数、core.uploader.UploadManager.get_upload_status;可选手部叠加来自 core.hand_tracking。
ui/pose_view_qt.py
作用:夹爪位姿视图控件(QWidget 子类),挂在采集页网格的 pose 槽里,把夹爪的实时 SLAM 位姿画成一幅三维示意:轨迹折线(跟随夹爪累计的点)、世界坐标三箭头(固定在原点)、跟随夹爪的体轴三箭头(画的是发布姿态的三列),以及夹爪模型(两块夹板按 set_grip_state 的开合度张合)。视角可用鼠标拖拽旋转、滚轮缩放、双击复位,视野在轨迹走近边缘时自动扩张。
模块级常量:
| 常量 | 值 | 说明 |
|---|---|---|
DISPLAY_TRAJECTORY_MAX_POINTS |
6000 |
显示上限(原 400 太小,见 docs/postmortem_trajectory_and_rgb.md);超出按 2 的幂翻倍 stride 抽稀 |
TRAJECTORY_UPDATE_MIN_INTERVAL |
1/25 秒 |
轨迹重算节流(位姿本身不受影响) |
SPAN_EXPAND_TRIGGER / SPAN_EXPAND_TARGET / SPAN_EXPAND_ANIM_SECONDS |
0.85 / 1.35 / 0.3 |
视野自动扩张的触发比、目标比、动画时长 |
AZIMUTH_DEFAULT / ELEVATION_DEFAULT |
-150° / 30° |
初始视角 |
BG / TITLE / TRAJECTORY / PLATE_GRIP / PLATE_OPEN / CONNECTOR / CAM_ORIGIN |
暗色主题配色 | 背景、标题、轨迹、合拢/张开夹板、连接件、相机原点 |
WORLD_AXES / CAM_AXES |
颜色 + 轴名 | 世界三箭头与跟随夹爪三箭头的配色(WORLD_AXES 的标签恰好是 ["X","Y","Z"],被测试断言) |
_BODY_RELABEL |
帧 id → 换基矩阵 | 见下:新帧 = M,旧帧 = 单位阵;把模型顶点从旧体轴搬到新体轴 |
TITLE_TOP |
2.0 |
画布上两行字(标题、坐标帧)的顶;行高取字体实测高度不写死像素(写死会裁掉中文笔画,而帧那行正是「一眼确认改没改」的入口) |
★ 坐标帧契约(v1.3.13):发布侧 core/gripper/slam/protocol.py 给位姿做了两次左乘 + 一次右乘(右乘 = 体轴改名),本控件是那个契约的另一半:
_draw_frame_axes画的是rotation的三列(= 夹爪体轴),不是三行。同一张M也在发布侧给体轴改了名,所以重新锚定那一瞬发布姿态 =M·Mᵀ= 单位矩阵,跟随夹爪的三箭头与世界三箭头重合,标签同写 下/左/前。_draw_gripper用_BODY_RELABEL(与发布侧同一张M)把模型顶点从旧体轴坐标搬到新体轴坐标,于是position + R·(M·v) == position + R_旧·v,夹爪的物理朝向与画面逐像素与改前相同(该恒等式对任意R、v代数上精确成立 ——M·R·Mᵀ·M·v = M·R·v,因为MᵀM = I;渲染侧由test_pose_view_qt.py::check_gripper_model_frame_invariant逐像素钉住)。★式中的R_旧是改前那次发布的姿态(已含发布侧左乘的M),不是夹爪自己的相对姿态R_rel—— 拿R_rel代进去会差一个M,算出"夹爪歪了 0.3–0.4 m"的假警报;对照:两处都改时残差只在浮点噪声量级,只改一半就是 0.3–0.4 m 的实偏差。- 后果:只改发布侧不改这里(或反过来)夹爪会被画歪 120°,而两种改法各自都"看起来自洽"——别只动一处。测试见
tools/tests/test_pose_view_qt.py。 - 三箭头的轴名来自
config.settings.GRIPPER_SLAM_FRAME_AXES_CN,不自己抄(屏幕上的字与落进info.json的frame_axes必须同源)。
关键方法:
| 方法 | 签名 | 说明 |
|---|---|---|
update_pose |
(position, rotation, trajectory=()) |
主入口:更新位姿与轨迹,节流重算 |
set_grip_state |
(percentage, gripped) |
夹板开合度与抓取态(决定配色 PLATE_GRIP/PLATE_OPEN) |
set_localization_backend / reset |
(backend) / () |
标题栏后端标注 / 清空轨迹与视角(换夹爪、重新录制时调) |
paintEvent / _draw_static |
— | 全部绘制走 QPainter;静态层(世界轴、模型)进缓存,只有轨迹逐帧重画 |
调用关系:由 ui/main_window.py:1616 惰性 import 后构造(pose_view = PoseViewQt()),放进该夹爪的 entry["pose_view"];位姿经 _on_gripper_pose → update_pose,抓取态经 set_grip_state,reset() 在换设备/重录时调。tools/tests/test_pose_view_qt.py 直接构造并离屏渲染(QT_QPA_PLATFORM=offscreen)。依赖 PyQt5、config.settings(GRIPPER_SLAM_FRAME_AXES_CN 等)、numpy。lite 包里没有本控件(lite 不含位姿视图)。
ui/task_page.py
作用:启动时显示的任务选择页(页面栈 index 0)。顶栏只读显示服务器地址(set_server_display)与当前身份,附「切换账号」「+ 新建任务」「🔄 刷新」按钮;中部任务表(5 列,含「来源」),底部「→ 进入采集」「✏ 改名」「🗑 删除」。任务数据来自 data/tasks.json(core.task_record.load_tasks),后端任务经 merge_backend_tasks 合并,并 set_identity 按身份过滤(游客仅见公共任务);用户选中任务点「进入采集」发射 task_selected(dict),主窗口切换到数据采集页。
采集端自建任务:顶栏「+ 新建任务」打开 TaskCreateDialog,落库为 source="local"(id 前缀 local-、created_by 记当前身份)。本机任务仅创建者可见(过滤在 core.task_record.filter_by_identity),进度只在本机累计、不上报服务器;「来源」列显示「本机」,服务端任务显示「服务端」。本机任务可「✏ 改名」(已有录制目录时拒绝)与删除(真删数据 + remove_local_task 整条移除,无 hidden 墓碑);服务端任务删除仍走 mark_hidden 墓碑。
模块级:
| 名称 | 签名要点 | 作用 | 返回/副作用 |
|---|---|---|---|
_compute_status |
(task: dict) -> str |
按 total_required/completed_count 算状态:pending/in_progress/completed |
str |
_tid |
(task: dict) -> str |
取任务 id(id 或 task_id) |
str |
_format_date |
(iso_str: str) -> str |
日期截取前 10 字符 | str |
类/函数(class TaskSelectionPage(QWidget)):
| 名称 | 签名要点 | 作用 | 返回/副作用 |
|---|---|---|---|
task_selected / refresh_requested / switch_account_requested(信号) |
pyqtSignal(dict) / pyqtSignal() / pyqtSignal() |
点击「进入采集」/ 点击「🔄 刷新」/ 点击「切换账号」 | 由 MainWindow._connect_task_service 连接 |
update_tasks |
(tasks: list[dict]) |
后端任务列表到达:merge_backend_tasks 合并(空则回退本地 load_tasks)→ 重算状态 → 重建表 |
— |
update_task_progress |
(task_id: str, completed_count: int) |
录制完成回写进度:refresh_progress() 后按 _tid 找行刷新 |
— |
set_connection_status |
(connected: bool) |
更新连接圆点与「后端已连接/未连接」文案 | — |
on_login_result |
(ok: bool, msg: str) |
登录结果反馈(登陆成功 / 登陆失败: msg[:30]) |
— |
set_identity |
(identity: str | None) |
更新当前身份("guest" 或用户名)并即时按新身份重滤任务表(含本机任务可见性) | — |
set_server_display |
(url) |
更新顶栏服务器地址显示与 tooltip(登录对话框改地址后由主窗口调用) | — |
current_task |
() -> dict | None |
当前选中行任务 | dict 或 None |
_on_cell_changed |
(row, _col, _pr, _pc) |
选行联动:completed 任务禁用「进入采集」;本机任务才启用「✏ 改名」,任一选中行启用「🗑 删除」 | — |
_on_double_clicked / _on_enter_clicked |
() |
双击行=快捷进入;进入前 completed 任务弹警告 | 发射 task_selected |
_on_new_task_clicked |
() |
打开 TaskCreateDialog(传入当前身份)→ create_local_task → _refilter → 选中新行;校验失败弹「创建失败」 |
写 tasks.json |
_on_rename_clicked |
() |
仅本机任务:打开 TaskRenameDialog → rename_local_task → _refilter → 保持选中;失败弹「改名失败」 |
写 tasks.json |
_select_task_by_id |
(task_id) -> int |
按 id 选中行(新建/改名后保持选中) | 行号或 -1 |
_task_error_message |
(code) -> str |
TaskValidationError.code → 本地化弹窗文案(has_data/name_too_long 补默认参数) |
str |
_do_delete_task |
() |
删除任务(按钮与右键菜单共用):二次确认后 delete_local_session 真删该任务录制目录 + RecordingRepo.delete_local_dir 删录制行;本机任务调 remove_local_task 整条移除,服务端任务调 mark_hidden 留墓碑 → 重载列表;目录没删净则弹错并不删除任务。删除目标先做兜底校验(必须是 RECORDING_DIR 的直接子目录)——_sanitize_tag 把纯空白任务名清洗成空串后 task_dir_of 会退化成 RECORDING_DIR 本身,不拦就会把所有任务的录制整根 rmtree |
— |
_refresh_after_delete |
() |
删除后重载列表并清空选中态(三个操作按钮禁用) | — |
_on_context_menu |
(pos) |
右键菜单:服务端任务只有「🗑 删除任务」,本机任务多一项「✏ 改名任务…」;先 setCurrentCell 到右键行,避免操作落到之前选中的别的任务 |
— |
_on_refresh_clicked |
() |
发射 refresh_requested,按钮 1.5s 内显示「刷新中…」禁用 |
— |
_on_language_changed |
(lang) |
5 列表头/按钮文字(新建/改名/删除等)刷新并重建表 | — |
关键数据:
- 表格 5 列:
任务名称 / 来源 / 状态 / 进度 / 发布时间(_COL_NAME=0、_COL_SOURCE=1、_COL_STATUS=2、_COL_PROGRESS=3、_COL_DATE=4;列宽 90/150/110/130)。来源列「本机」绿色、「服务端」灰色;状态列用彩色方块+文字的 badge(_STATUS_CONFIG:pending 灰#757575、in_progress 蓝#42A5F5、completed 绿#66BB6A);进度列completed/total右对齐;名称 tooltip 显示描述。 - 身份与地址:服务器地址/记住账号由
LoginDialog校验后经MainWindow._show_login_flow写入(settings.save_server_url/save_remembered_username);登录态 cookie 由TaskService.adopt_login接管;身份过滤 scope 经_identity_scope()传给core.task_record。
调用关系:由 ui/main_window.py 构造;任务服务信号在 _connect_task_service 中双向接线。依赖 core.task_record(load_tasks/merge_backend_tasks/refresh_progress/mark_hidden/is_local_task/create_local_task/rename_local_task/remove_local_task/TaskValidationError)、ui.task_create_dialog、config.settings、config.i18n。
ui/task_create_dialog.py
作用:本机任务对话框(v1.3.16)。TaskCreateDialog 采集「任务名称 + 目标段数 + 描述」,TaskRenameDialog 只改名称;两者都做实时校验并在对话框内拦住非法输入。落盘由调用方(ui/task_page.py)经 core.task_record 完成,本模块不写文件。名称规则与 core.task_record.validate_task_name 一致:只允许字母/数字/汉字/下划线/连字符(空格、点、斜杠等一律拒绝),去首尾空白后 ≤64 字符,拒绝 Windows 保留名与重名;因此任务名 = 录制目录名 = 上传会话/项目名。新建时显示本机目录预览与「仅当前身份可见」提示;若同名目录已存在(无任务记录的孤儿目录)给出追加录制警告(允许创建)。改名时若本机已有录制目录则禁用确认按钮并说明原因(改名不迁移数据)。
模块级:
| 名称 | 签名要点 | 作用 | 返回/副作用 |
|---|---|---|---|
_ERROR_TEXT |
dict | 错误码 → 中文源文案(缺失键回退「操作失败:code」) | — |
task_error_text |
(code, *args) -> str |
错误码经 tr() 本地化(英文文案在 config/i18n.py) |
str |
_identity_display |
(identity) -> str |
"guest" 显示「游客」,其余原样 |
str |
_label / _cancel_button / _ok_button |
控件工厂 | 与 LoginDialog 一致的深色样式 |
控件 |
类/函数:
| 名称 | 签名要点 | 作用 | 返回/副作用 |
|---|---|---|---|
TaskCreateDialog |
(parent=None, identity="guest") |
新建任务对话框;exec_() 后 name()/total_required()/description() 取值 |
— |
_validate |
() -> str |
名称/段数/描述校验,返回错误码(空串=通过) | str |
_refresh_state |
() |
实时刷新目录预览、错误/警告行、创建按钮可用性 | — |
accept |
() |
再次校验后 QDialog.accept()(非法输入不关闭) |
— |
TaskRenameDialog |
(parent=None, task=None) |
改名对话框;new_name() 取值;本机已有录制目录时 _has_data=True 禁止确认 |
— |
_refresh_state / accept |
() |
校验新名称与目录占用,重复/非法/已有数据时保持打开 | — |
调用关系:仅被 ui/task_page.py 构造;依赖 core.task_record(校验/常量)、core.helpers(task_dir_of/list_task_episodes)、config.settings、config.i18n。
ui/upload_dialog.py
作用:录制数据一键上传对话框:顶部服务器地址 + 「测试连接」(core.api_client.APIClient.health_check),中部会话列表(多选、全选、按上传状态筛选),底部进度条与状态栏。「⬆ 一键上传」把勾选会话入队 UploadManager(复用已认证的 requests.Session);每项成功后遵循「上传后自动删除」开关:开启时后台线程删本地目录并把 recording 记录标记为 uploaded_deleted,否则标 uploaded(本地保留);成功/失败结果均经 _parent_log 写进主窗口日志面板(与自动上传同一口径)。
模块级:
| 名称 | 签名要点 | 作用 | 返回/副作用 |
|---|---|---|---|
| (无) | — | 会话扫描用 core.session_catalog.list_recordings(轻量版,不读元数据) |
按名称倒序 |
类/函数(class UploadDialog(QDialog)):
| 名称 | 签名要点 | 作用 | 返回/副作用 |
|---|---|---|---|
_session_deleted(信号) |
pyqtSignal(str, str) |
后台删除线程 → 主线程:(session_path, error) |
连接 _on_session_deleted |
__init__ |
(parent=None, data_dir="", session=None) |
session 为复用已认证的 requests.Session;QTimer.singleShot(50, _init_manager) 延迟建 UploadManager(读 _url_edit 当前值) |
— |
_refresh_list |
() |
重建会话列表:每项图标按 UploadManager.get_upload_status(✅ completed / ❌ failed / ⬜ pending),默认勾选未完成项;「全选」用 blockSignals 复位防 _toggle_all 连锁 |
— |
_on_item_clicked |
(item) |
点击整行切换勾选(不必精确点复选框) | — |
_toggle_all |
(checked) |
全选/全不选 | — |
_filter_uploaded |
(uploaded) |
「⬆ 选中已上传」/「⬇ 选中未上传」 | — |
_test_connection |
() |
APIClient(url).health_check() 弹结果框 |
— |
_start_upload |
() |
勾选会话(过滤不存在的目录)_manager.add_tasks(valid) 入队并 start();_task_path_map 记 task_id → session_path;禁用上传按钮 |
— |
_on_status |
(task_id, msg) |
串行处理时状态栏显示「第 x/y 条 [会话名] 当前动作」 | — |
_on_progress |
(task_id, ratio) |
总进度 =(已完成数 + 当前比例)/ 总数 | 进度条 |
_on_task_done |
(task_id) |
单条完成:刷新列表;UPLOAD_DELETE_AFTER 开启时 _delete_after_upload(与主窗口自动上传同规则),否则 RecordingRepo.mark_uploaded + 主窗口日志「☁ 上传完成(本地保留)」 |
— |
_delete_after_upload |
(session_path, episode_index) |
后台线程 delete_local_session(真删除 + 删除后校验),结果经 _session_deleted 回主线程 |
— |
_on_session_deleted |
(session_path, err) |
删除收尾:失败仅提示(含校验残留);成功 RecordingRepo.delete_local_episode 真删录制行 + 刷新列表 + 主窗口日志「☁ 上传完成,本地文件已删除」 |
— |
_on_task_failed / _on_all_done |
(task_id, error) / () |
失败:状态栏「❌ 上传失败」+ 主窗口日志 + 刷新列表 / 全部完成:恢复按钮、进度条 100、状态「全部完成」 | — |
closeEvent |
(event) |
停止 _manager |
— |
关键数据:
- 上传状态来源
UploadManager.get_upload_status(path):completed/failed/pending。 _task_path_map:task_id → session_path(删除与状态显示用)。- 「上传后自动删除」开关
settings.UPLOAD_DELETE_AFTER对自动与手动上传统一生效。
调用关系:由 ui/main_window.py(_open_upload)构造。依赖 core.uploader.UploadManager、core.api_client.APIClient(测试连接)、core.recording_repository.RecordingRepo、core.session_catalog.list_recordings、config.settings、config.i18n.tr。
ui/init.py
作用:ui 包的空标记文件(0 行),仅使目录成为可导入包。无类、无函数、无数据。