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

Serial Studio 录制会话视图状态打包(Spec 0062)实现剖析:让 Session 回放还原“录制那一刻“的仪表盘 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 0062Recording setup bundle为每次录制额外打包一份viewStateJSON 文档在回放时按用户操作顺序恢复让仪表盘以录制那一刻的样子重新打开。本文基于 spec.md、plan.md 与 tasks.md结合仓库源码讲解该特性的需求、数据模型、快照节奏、回放顺序与降级策略读完你将掌握其完整实现链路与关键取舍。问题背景项目状态与视图状态的边界录制已带走什么还缺什么一份 Session Database 录制已经内嵌了项目 JSONsessions.project_json以及实时项目的project_metadata.project_jsonSessions::Player在回放时通过restoreProjectFromJson恢复它。因此小部件布局、工作区、每个 widget 的widgetSettings插值、面积填充、扫描配置以及自 spec 0058 以来的标尺标记/零点都已随录制旅行。但让样本数据在录制那一刻有意义的视图状态却存在于项目文档之外光标位置cursor positions缩放/平移每个 plot 的可见窗口哪些 widget 被暂停paused屏幕上激活的是哪个 workspace外部/弹出窗口来自 QSettings 的 plot 时间范围非 ProjectFile 模式下主题theme这些是会话状态session state而非项目状态project state。回放一份会话会打开正确的仪表盘但视图是默认的用户需要重新定位自己当时观察的内容。此外针对录制完成后磁盘上的项目被修改了这一场景此前也没有明确的处理故事目前内嵌副本会在整个回放期间静默胜出win回放结束再恢复回放前项目schedulePreSessionRestore方向是对的但没有任何机制告知用户两份项目存在差异。为什么不能把视图状态塞进项目 JSONSpec 0062 明确选择了在项目 JSON 旁边再放一份更小的文档而不是把视图状态折叠进项目 JSON。原因很直接如果把视图状态放进项目文档那么每次缩放都会把项目标记为已修改setModified(true)并在保存时落入.ssproj文件污染项目文件本身。同时启动路径restoreLastProject完全不变——它照常在启动时从 QSettings 重开上次项目路径并重放持久化的操作模式回放通过换入内嵌项目、关闭时恢复回放前项目的方式绕开了它。需求全景R1–R7Spec 0062 定义了七条需求构成整个特性的契约R1—viewState是每个会话一份 JSON 文档sessions.view_state TEXTschema 版本升级由 DB worker 在 GUI 线程之外根据 GUI 侧快照写入。R2— 内容全部可选缺省 默认值plotTimeRange、theme、workspace、externalWindows[]以及每个 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— 离开回放时恢复回放前的项目与视图schedulePreSessionRestorebundle 永不泄漏进实时项目。决策记录五个开放问题的裁定plan.md 记录了作者对 spec 中开放问题的裁定其中部分与 spec 初稿不同阅读时值得注意问题裁定主题theme是否进 bundle不记录、不应用回放时恢复主题令人意外仅记录用于上下文而不应用可能已足够但最终选择两者都不做快照节奏QML 侧 500 ms 合并 worker 侧 1.5 s 防抖开始与结束时总是写入keep mine选择仅通知无模态框API 驱动的回放绝不能阻塞录制项目总是胜出差异通知的归属Notification CenterSessionschannel工作区 / 外部窗口本切片不打包需要从 Taskbar 做 composition-root 连线已记录在案值得注意的是 plan 中work in progress的边界T6workspace 外部窗口进 bundle需要 Taskbar 在 composition root 的连线在 tasks.md 中标记为已完成但 plan 的 Decision 表中记录该切片暂不打包说明任务清单与决策表之间存在演进关系——以最终关闭的 spec 状态status: doneclosed 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 文档versionglobalwidgets三个顶层键这正是录制所打包的内容。setViewStateJson(json)从 bundle 文档整体替换状态widget 创建后在其Component.onCompleted中读取。畸形输入会被安全地清空。clearViewState()丢弃全部记录值无可丢弃时返回 false。实现细节写入时会做QJSValue→QVariant归一化QML 侧传入的 JavaScript 值会被转换为 JSON 兼容变体每 widget 一个QJsonObject存储于m_widgetViewState全局项存于m_globalViewState。此外该类还承载了面板/工具栏/布局偏好autoHideToolbar、showActionPanel、showAlignmentGuides、layoutMargin、layoutSpacing这些属于全局偏好并持久化到 QSettings键如Dashboard/AutoHideToolbar、Dashboard/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::ExportExport.cpp负责在录制期间把viewStateJson()快照到项目快照旁边快照在主线程完成订阅Core::Bus::DashboardViewState注释明确 Main-thread-only随后武装一个1.5 s 单次防抖定时器m_viewStateDebouncekViewStateDebounceMs超时后通过pushViewStateToWorker以Qt::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测试仍通过的要求。回放链路PlayerLoaderWorker→Sessions::PlayerPlayerLoaderWorker在加载会话时读取该列SELECT view_state FROM sessions WHERE session_id ?把结果放进 payload 的viewState字段见 PlayerLoaderWorker.cpp。Sessions::Player在回放开始前捕获回放前的视图状态 回放前的项目执行restoreProjectFromJson后应用 bundlerestorePreSessionState恢复捕获的回放前状态——bundle 永不泄漏进实时项目R7。当内嵌项目与磁盘上项目不一致时通过 Notification CenterSessionschannel发布一次警告文案指出录制时的项目胜出as before。验收标准行为即测试Spec 0062 的五个验收标准全部勾选[x]既是行为契约也是手工验证清单AC1— 在 plot 1 上带两个光标录制plot 2 放大 4 倍plot 3 暂停激活 workspace Bench停止回放四项全部保持原样。AC2— 录制后编辑项目重命名一个数据集回放通知出现一次use recordings 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、restoreLastProject、SessionContext均不动——该特性与既有 pre-session-restore 路径组合不替换它。视图状态 ≠ 项目状态任何情况下都不得对项目调用setModified(true)。非目标不录制逐帧视图变化作为时间线没有重放我的缩放过程不改变项目 JSON 的内容或restoreLastProject的机制不为 CSV/MDF4 回放打包它们没有承载它的按会话容器。设计要点与取舍小结双文档模型项目 JSON布局、widget 设置view_state光标、缩放、暂停等会话态后者绝不污染.ssproj。两级防抖QML 500 ms 合并 worker 1.5 s 防抖兼顾交互流畅与落库频率快照开销不随帧率增长。可空列 版本号升级view_state TEXT可空kUserVersion3→4旧归档零迁移成本回放行为与旧版一致。回放顺序即用户操作顺序项目 → 仪表盘重建 →widgetCountChanged之后才应用视图状态避免 widget 尚不存在时引用落空。非模态差异通知API 驱动的回放绝不能被模态框阻塞录制项目胜出延续既有行为用户仅被告知差异。优雅降级缺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),仅供参考