Serial Studio 录制会话视图状态打包(Spec 0062)实现剖析:让 Session 回放还原“录制那一刻“的仪表盘
2026/9/17 12:54:41 网站建设 项目流程

Serial Studio 录制会话视图状态打包(Spec 0062)实现剖析:让 Session 回放还原"录制那一刻"的仪表盘

【免费下载链接】Serial-StudioOpen-source telemetry dashboard. Supports UART, BLE, MQTT, Modbus, CAN Bus and more.项目地址: https://gitcode.com/GitHub_Trending/se/Serial-Studio

导读

Serial Studio 的 Session Database 在录制时会随样本数据一并保存项目 JSON,回放时通过restoreProjectFromJson还原仪表盘布局,但光标位置、缩放/平移、暂停状态这类"视图状态"并不属于项目文档,导致回放只能打开默认视图,用户必须重新寻找当初观察的窗口。Spec 0062(Recording setup bundle)为每次录制额外打包一份viewStateJSON 文档,在回放时按用户操作顺序恢复,让仪表盘以"录制那一刻的样子"重新打开。本文基于 spec.md、plan.md 与 tasks.md,结合仓库源码讲解该特性的需求、数据模型、快照节奏、回放顺序与降级策略,读完你将掌握其完整实现链路与关键取舍。

问题背景:项目状态与视图状态的边界

录制已带走什么,还缺什么

一份 Session Database 录制已经内嵌了项目 JSON(sessions.project_json,以及实时项目的project_metadata.project_json),Sessions::Player在回放时通过restoreProjectFromJson恢复它。因此小部件布局、工作区、每个 widget 的widgetSettings(插值、面积填充、扫描配置,以及自 spec 0058 以来的标尺标记/零点)都已随录制"旅行"。

但让样本数据在录制那一刻"有意义"的视图状态却存在于项目文档之外:

  • 光标位置(cursor positions)
  • 缩放/平移(每个 plot 的可见窗口)
  • 哪些 widget 被暂停(paused)
  • 屏幕上激活的是哪个 workspace
  • 外部/弹出窗口
  • 来自 QSettings 的 plot 时间范围(非 ProjectFile 模式下)
  • 主题(theme)

这些是会话状态(session state)而非项目状态(project state)。回放一份会话会打开正确的仪表盘,但视图是默认的,用户需要重新定位自己当时观察的内容。此外,针对"录制完成后磁盘上的项目被修改了"这一场景,此前也没有明确的处理故事:目前内嵌副本会在整个回放期间静默胜出(win),回放结束再恢复回放前项目(schedulePreSessionRestore),方向是对的,但没有任何机制告知用户两份项目存在差异。

为什么不能把视图状态塞进项目 JSON

Spec 0062 明确选择了"在项目 JSON 旁边再放一份更小的文档",而不是把视图状态折叠进项目 JSON。原因很直接:如果把视图状态放进项目文档,那么每次缩放都会把项目标记为已修改setModified(true)),并在保存时落入.ssproj文件,污染项目文件本身。同时启动路径(restoreLastProject)完全不变——它照常在启动时从 QSettings 重开上次项目路径并重放持久化的操作模式,回放通过"换入内嵌项目、关闭时恢复回放前项目"的方式绕开了它。

需求全景:R1–R7

Spec 0062 定义了七条需求,构成整个特性的契约:

  • R1viewState是每个会话一份 JSON 文档(sessions.view_state TEXT,schema 版本升级),由 DB worker 在 GUI 线程之外根据 GUI 侧快照写入。
  • R2— 内容全部可选(缺省 = 默认值):plotTimeRangethemeworkspaceexternalWindows[],以及每个 widget id 下的cursors {ax, ay, bx, by, aVisible, bVisible}view {xZoom, xPan, yZoom, yPan}(或世界窗口)、paused、用户显式设置时的yRange {min, max}
  • R3— 快照触发时机:录制开始、widgetSettingsChanged(本身已防抖)、光标/缩放变化按 1.5 s 定时器合并(与 autosave 防抖一致)、录制结束。
  • R4— 回放顺序:先恢复项目 JSON(既有逻辑),再重新配置仪表盘,最后在 widget 存在之后(widgetCountChanged之后,绝不提前)应用viewState
  • R5— 差异通知:对比内嵌project_json与实时项目的序列化(标题 + 内容哈希),不匹配时弹出一条非模态通知并提供两个选项;"keep mine"用当前项目播放样本(数据集按uniqueId匹配,未匹配的忽略)。
  • R6— 一切优雅降级:没有view_state的会话与今天行为完全一致;viewState引用了已不存在的 widget id 时静默跳过。
  • R7— 离开回放时恢复回放前的项目与视图(schedulePreSessionRestore),bundle 永不泄漏进实时项目。

决策记录:五个开放问题的裁定

plan.md 记录了作者对 spec 中开放问题的裁定,其中部分与 spec 初稿不同,阅读时值得注意:

问题裁定
主题(theme)是否进 bundle不记录、不应用(回放时恢复主题令人意外;仅记录用于上下文而不应用可能已足够,但最终选择两者都不做)
快照节奏QML 侧 500 ms 合并 + worker 侧 1.5 s 防抖;开始与结束时总是写入
"keep mine"选择仅通知(无模态框:API 驱动的回放绝不能阻塞);录制项目总是胜出
差异通知的归属Notification Center(Sessionschannel)
工作区 / 外部窗口本切片不打包(需要从 Taskbar 做 composition-root 连线);已记录在案

值得注意的是 plan 中"work in progress"的边界:T6(workspace + 外部窗口进 bundle,需要 Taskbar 在 composition root 的连线)在 tasks.md 中标记为已完成,但 plan 的 Decision 表中记录该切片暂不打包,说明任务清单与决策表之间存在演进关系——以最终关闭的 spec 状态(status: done,closed 2026-08-20)为准,同时保留 plan 中的偏差记录供复现。

源码剖析:视图状态的存储、推送与恢复

GUI 侧:UI::DashboardViewState是视图状态的唯一真源

视图状态活在UI::Dashboard门面(GUI 线程)里,永远不进入项目文档。其核心实现位于 DashboardViewState.h 与 DashboardViewState.cpp,类注释点明了设计哲学:

"View state is session state, never project state: it never marks the project modified and is dropped whenever the widget identity space changes. Every mutator answers whether it changed anything instead of emitting."

关键 API 一览(源码确认):

  • saveWidgetViewState(widgetId, key, value)/saveGlobalViewState(key, value):记录单值,仅在值真实变化时返回 true(内部用QJsonValue::fromVariant比较新旧值),这样录制 bundle 的防抖看到的是"编辑"而非"重绘"。
  • widgetViewState(widgetId)/globalViewState():读取当前记录。
  • viewStateJson():把整个状态序列化为一个紧凑 JSON 文档(version+global+widgets三个顶层键),这正是录制所打包的内容。
  • setViewStateJson(json):从 bundle 文档整体替换状态;widget 创建后在其Component.onCompleted中读取。畸形输入会被安全地清空。
  • clearViewState():丢弃全部记录值,无可丢弃时返回 false。

实现细节:写入时会做QJSValueQVariant归一化(QML 侧传入的 JavaScript 值会被转换为 JSON 兼容变体);每 widget 一个QJsonObject存储于m_widgetViewState,全局项存于m_globalViewState。此外该类还承载了面板/工具栏/布局偏好(autoHideToolbarshowActionPanelshowAlignmentGuideslayoutMarginlayoutSpacing),这些属于全局偏好并持久化到 QSettings(键如Dashboard/AutoHideToolbarDashboard/LayoutMargin),与"会话视图状态"严格分开。

QML 侧:Plot.qml/MultiPlot.qml的推送与恢复

Plot.qml 与 MultiPlot.qml 通过d.saveWidgetViewState(...)推送光标、缩放/平移、十字线与暂停状态,例如MultiPlot.qml中实际调用的键包括:

d.saveWidgetViewState(widgetId, "cursorAX", plot.cursorAX) d.saveWidgetViewState(widgetId, "cursorAY", plot.cursorAY) d.saveWidgetViewState(widgetId, "cursorBX", plot.cursorBX) d.saveWidgetViewState(widgetId, "cursorBY", plot.cursorBY) d.saveWidgetViewState(widgetId, "cursorAVisible", plot.cursorAVisible) d.saveWidgetViewState(widgetId, "cursorBVisible", plot.cursorBVisible) d.saveWidgetViewState(widgetId, "showCrosshairs", plot.showCrosshairs) d.saveWidgetViewState(widgetId, "xZoom", plot.xAxis.zoom) d.saveWidgetViewState(widgetId, "xPan", plot.xAxis.pan) d.saveWidgetViewState(widgetId, "yZoom", plot.yAxis.zoom) d.saveWidgetViewState(widgetId, "yPan", plot.yAxis.pan) d.saveWidgetViewState(widgetId, "paused", !root.model.running)

推送通过一个500 ms 合并定时器聚合(QML 侧合并),然后在Component.onCompleted中读回状态,因此"项目恢复后重建的仪表盘"无需显式排序即可自动应用状态。

worker 侧:会话快照、防抖与 SQL 写入

Sessions::Export(Export.cpp)负责在录制期间把viewStateJson()快照到项目快照旁边:

  • 快照在主线程完成(订阅Core::Bus::DashboardViewState,注释明确 "Main-thread-only"),随后武装一个1.5 s 单次防抖定时器m_viewStateDebouncekViewStateDebounceMs),超时后通过pushViewStateToWorkerQt::QueuedConnection请求 worker 落库——保证 GUI 线程 JSON 写入只发生在交互速率,SQL 只在 worker 线程。
  • ExportWorker::storeViewState()(注释标注 "spec 0062")执行UPDATE sessions SET view_state = ? WHERE session_id = ?,调用时机为:会话开始(insertSession后)、防抖推送、finalizeSession关闭时——所以 bundle 反映的是最后状态而非首个状态。写入失败会输出[SQLite] view_state update failed:警告。
  • 线程模型:plan.md 明确 "Hotpath & threading impact: None. GUI-thread JSON writes at interaction rate; worker-thread SQL only."

schema 升级:view_state列与kUserVersion4

数据库侧在 DatabaseSchema.cpp 的migrateSessionsTable中添加了可空列:

{ "view_state", "TEXT" },

并伴随sessions表 schema 用户版本从 3 → 4,见 DatabaseManager.h 中的static constexpr int kUserVersion = 4;。可空列保证旧归档完全不受影响:回放旧会话时view_state为 NULL,走既有读取路径,行为与 0062 之前一致(对应 AC3 与tst_sessions_legacy_archive测试仍通过的要求)。

回放链路:PlayerLoaderWorkerSessions::Player

  • PlayerLoaderWorker在加载会话时读取该列:SELECT view_state FROM sessions WHERE session_id = ?,把结果放进 payload 的viewState字段(见 PlayerLoaderWorker.cpp)。
  • Sessions::Player在回放开始前捕获回放前的视图状态 + 回放前的项目;执行restoreProjectFromJson后应用 bundle;restorePreSessionState恢复捕获的回放前状态——bundle 永不泄漏进实时项目(R7)。
  • 当内嵌项目与磁盘上项目不一致时,通过 Notification Center(Sessionschannel)发布一次警告,文案指出"录制时的项目胜出(as before)"。

验收标准:行为即测试

Spec 0062 的五个验收标准全部勾选([x]),既是行为契约也是手工验证清单:

  • AC1— 在 plot 1 上带两个光标录制,plot 2 放大 4 倍,plot 3 暂停,激活 workspace "Bench";停止;回放:四项全部保持原样。
  • AC2— 录制后编辑项目(重命名一个数据集);回放:通知出现一次;"use recording's project"显示旧名称,"keep mine"显示新名称。
  • AC3— 0062 之前的旧会话文件照常回放(无通知、默认视图)。
  • AC4— 停止回放:实时项目与视图恢复回放前的样子。
  • AC5— 无 GUI 线程 DB 访问(既有规则);快照开销不是逐帧的。

验证计划还明确:AC1/AC3/AC4 在运行中的应用内验证;AC2 以"通知"而非"选择框"形式出现;tst_sessions_legacy_archive必须继续通过(可空列、旧读取路径未动)。

约束与不变量

  • Session DB 规则集:仅 worker 线程写入、代理键(surrogate keys)、不使用INSERT OR IGNORE、schema 版本升级配合新增可空列的迁移。
  • 组合而非替换:composition root、restoreLastProjectSessionContext均不动——该特性与既有 pre-session-restore 路径组合,不替换它。
  • 视图状态 ≠ 项目状态:任何情况下都不得对项目调用setModified(true)
  • 非目标:不录制逐帧视图变化作为时间线(没有"重放我的缩放过程");不改变项目 JSON 的内容或restoreLastProject的机制;不为 CSV/MDF4 回放打包(它们没有承载它的按会话容器)。

设计要点与取舍小结

  1. 双文档模型:项目 JSON(布局、widget 设置)+view_state(光标、缩放、暂停等会话态),后者绝不污染.ssproj
  2. 两级防抖:QML 500 ms 合并 + worker 1.5 s 防抖,兼顾交互流畅与落库频率,快照开销不随帧率增长。
  3. 可空列 + 版本号升级view_state TEXT可空,kUserVersion3→4,旧归档零迁移成本,回放行为与旧版一致。
  4. 回放顺序即用户操作顺序:项目 → 仪表盘重建 →widgetCountChanged之后才应用视图状态,避免 widget 尚不存在时引用落空。
  5. 非模态差异通知:API 驱动的回放绝不能被模态框阻塞;"录制项目胜出"延续既有行为,用户仅被告知差异。
  6. 优雅降级:缺view_state或引用已删除 widget 都静默跳过,任何环节失败都不会破坏回放。

通过这套设计,Serial Studio 把"样本数据 + 项目布局 + 视图状态"三者统一进一次录制,让 Session 回放真正还原录制瞬间的分析现场。对后续要扩展 bundle 内容(如 T6 的工作区与外部窗口)的开发者而言,spec.md 的需求契约、plan.md 的决策记录与 tasks.md 的任务拆分提供了完整的可追溯链路,配合 DashboardViewState.cpp、Export.cpp 与 DatabaseSchema.cpp 的源码即可快速上手。

【免费下载链接】Serial-StudioOpen-source telemetry dashboard. Supports UART, BLE, MQTT, Modbus, CAN Bus and more.项目地址: https://gitcode.com/GitHub_Trending/se/Serial-Studio

创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询