:以 DataBlock 契约打通帧通道与流通道)
Serial Studio 统一 Block 发布通道Spec 0055以 DataBlock 契约打通帧通道与流通道【免费下载链接】Serial-StudioOpen-source telemetry dashboard. Supports UART, BLE, MQTT, Modbus, CAN Bus and more.项目地址: https://gitcode.com/GitHub_Trending/se/Serial-Studio导读Serial Studio 作为开源遥测仪表盘历史上通过逐帧发布与批量块发布两条并行通道向消费者仪表盘、导出、录制、API 等投递采集数据导致同一个消费者必须实现两套摄取逻辑而 MQTT、gRPC 与远程镜像这类只实现帧通道的消费者对音频等密集源完全失明。本文以仓库内 Spec 0055spec.md为骨架结合其 plan.md、tasks.md 与落地源码完整讲解统一发布契约DataModel::DataBlock的设计动机、数据流、持久化方案、验收标准与实施偏差读完你将对 Serial Studio 内部数据从驱动到消费者的完整链路及其实时性、零分配约束有源码级理解。背景与动机两条平行通道带来的系统性割裂Spec 0055 的出发点是一个真实存在的架构债。Serial Studio 的采集数据通过两条并行通道发布字节导向源UART、TCP/UDP、BLE、Modbus、CAN、MQTT、USB、HID、Process逐帧解析一次发布一个解析帧密集类型源当前是 Audio以及任何用户切换到流通道streamLane的源发布样本块完全绕过逐帧解析。两条通道携带不同的载荷、运行在不同的时间基准上、精度也不同。其后果在消费端集中爆发六个消费者要写两套摄取路径仪表盘Dashboard、CSV 导出、MDF4 导出、会话录制Sessions、API 服务器、WAV taps三个消费者只有帧路径形成静默数据空洞MQTT 发布与 gRPC 服务器对流通道源什么都收不到——一个只有音频源的项目不发布任何 MQTT 消息、不向 gRPC 客户端暴露任何数据且全程无任何报错远程镜像降级而非失明镜像发布者读取的是仪表盘合成的帧携带的是流数据集在显示 tick 上刷新的最新值于是观看 48 kHz 源的观众看到的是一条 tick 频率的折线而不是信号本身——全速样本从未上过网线导出分裂混合项目的一次录制会产出帧源的一个主文件外加每个流源一个额外文件_stream_sourceN.csv/.mf4一次录制散落成多个文件需要用户手工关联。双通道存在的唯一理由是密集源确实需要批处理——48 kHz 源无法承受每样本一次发布。但正如 spec 所述这是批量大小的差异不是种类的差异一个低速率源发布大小为 1 的批、一个 48 kHz 源发布大小为 512 的批可以走同一条契约。解析本身保持逐帧按分隔符切分字节流本质上是顺序的不在改动范围内。目标与非目标清晰划定改动边界目标包括项目中每个数据源通过同一摄取契约到达每个消费者关闭流通道盲区MQTT、gRPC、远程镜像无需逐消费者特殊处理即可携带密集源数据一次录制每格式只产一个文件回放与实时采集不可区分解析热路径吞吐不降或更优消费者侧代码缩减——六消费者的第二条摄取路径消失。非目标同样关键明确防止范围蔓延不改字节解析方式分隔符扫描、校验和、Native/JS/Lua 解析引擎、逐数据集变换语义均不变不改变哪些源走密集通道驱动能力标志与项目级覆盖继续决定批量行为不改变 widget 行为或外观抽取、绘图环形缓冲、FFT 加窗均保持现状不新增导出格式也不改变现有格式记录内容除合并每源文件与 R6 的行布局变更外不重写会话数据库文件格式schema 统一布局文件仍是 SQLite旧文件仍可读不删除 CSV 间隔快照模式不改变任何功能的许可层级。核心方案DataModel::DataBlock统一发布契约Spec 0055 的核心产出是列优先column-major载荷DataModel::DataBlock其定义落地于 core/Core/DataModel/DataBlock.hDataBlock ├─ sourceId / blockNumber / structureGeneration 源身份与代际戳 ├─ t0 (steady_clock) dt (ns) 均匀网格样本 i 时刻 t0 i·dt ├─ times (ns offsets) dt 0 时为不规则逐样本时间 ├─ samples 填充游标池化槽位复用size 只做一次 └─ columns: 每数据集一个 BlockColumn ├─ uniqueId / hasText / hasRaw ├─ values std::vectordouble 数值恒存在 ├─ text std::vectorQString 仅文本数据集 ├─ numeric 逐样本解析为数字标志空 恒真即密集源 ├─ rawValues / rawText 变换前原始值spec 0085 └─ fftWindow 生产者算好的 FFT 窗口契约要点一个契约承载一到多个样本。低速率源发布 1 或几个的批密集源发布按其采集周期定大小的批。消费者只能通过声明的时间与大小推断来源类型无法从契约本身区分。契约是两条旧通道的超集携带管线精度的数值、文本数据集字符串、数据集身份、以及每样本时间。时间同时支持均匀网格dt ! 0密集源用与不规则逐样本时间times[]解析帧用判别规则在 DataBlock.h 中由uniform_grid()/sample_offset_ns()内联实现。密集源纯数值D2契约携带字符串是因为解析帧会产生文本但密集源的逐块 worker 路径不携带任何字符串存储数值块与旧StreamBlockItem分配形态完全一致。配套的StructureSnapshotgenerationFrame data让结构信息与数值分离仅在帧池代际generation变化时重新发布消费者每次布局变更只重配置一次逐帧的compare_frames()校验被直接删除而非迁移。为什么列优先而非帧包络plan.md 的备选方案对比给出了明确结论行优先包络vectorTimestampedFramePtr是发布一个帧列表的朴素读法但它强迫密集源每样本一个Frame——这在会话播放器中早已被拒绝export.md中replayChannelsTyped意味着每样本合成一帧。列优先同时也是两条既有密集载荷的既有形态。为什么保留Frame作为解析产物Span 通道早已把m_frame当作只含结构的模板写入池化槽位因此槽位变成暂存缓冲即可解析代码零改动——这把 256 kHz CI 门槛硬性上限排除在改动范围之外把所有风险都收敛到收益所在的发布尾。数据流与发布尾一次跨线程跳转都未增加plan.md 给出的数据流实现后与 core/Pipeline/DataModel/FrameBuilder/BlockStager.h 及 core/Pipeline/DataModel/FrameBuilder/BlockPublisher.cpp 一致如下Driver驱动线程→ FrameReader::processData管线线程未改 → PipelineHost::routeFrames未改→ FrameBuilder 解析未改 → FrameBuilder::stageParsedFrame新 每解析帧向该源打开中的块追加一行 · 池化槽位、列向量预分配——纯存储零分配 · 触发刷新样本数达 kFrameBlockSampleCap或 flushEpoch 变更tick-or-cap → FrameBuilder::publishBlock管线线程 · PipelineHost 块环SPSC→ Dashboard 显示 tick drain · 若有异步 sink一个 DataBlockPtr 交给所有 sink——无逐帧深拷贝 密集源audio / streamLaneon→ StreamProcessor每源 worker 线程工作未改 → 一个 DataBlock原为 StreamDisplayUpdate StreamBlockItem 两份近重复载荷 · display ring → Dashboard drain · 排队到 GUI → 同一批七个 sink · latestValuesReady → FrameBuilder::ingestStreamValues未改 结构FrameBuilder::publishStructureSnapshot(StructureSnapshotPtr) 仅在代际变化时 → 同一环先于携带该代际的块到达深拷贝为何消失热路径收益改动前只要任一异步 sink 存活hotpathTxFrame就逐帧构造make_sharedTimestampedFrame(frame-data, ...)——一次包含每个Dataset六个QString及alarmBands/fftMarkers向量的完整Frame拷贝。而DataBlock只携带数值、被池化、以shared_ptr共享零拷贝。sink 无法钉住池子因为块在 worker 的m_writeBuffer.clear()运行时即被释放沿用FrameConsumerWorker契约池预算则像旧的kFramePoolBudgetBytes一样约束物化槽位数。无定时器的刷新触发tick-or-cap 与 flush epochtick-or-capD1块在显示 tick 或样本数上限二者先到者处关闭。这既把慢速源的发布延迟限制在一个 tick 内又把快速源的批内存限制在上限内同时在高速率下把多次发布折叠成一次。flush epoch 取代布尔tasks.md T5 记录了最初用std::atomicbool m_flushDueGUI 显示 tick 写、管线清除的方案缺陷——一个 bool 会被第一个消费它的源耗尽导致其他源的打开块挨饿。实现改为单调递增的epochbumpFlushEpoch/flushEpoch每个源比较自己块打开时的 epoch于是所有源在同一 tick 全部刷新。结构环先于块环 drain结构走独立的kStructureRingSize 32小环PipelineHost.h每 tick 先于块环 drain保证块永远不会先于其布局到达仪表盘块环则从 8192 缩到kBlockRingSize 32在途块受 64 槽块池约束旧尺寸只会增加延迟。池化与零分配纪律BlockStager.h 定义了kFrameBlockSampleCap 64、kBlockPoolSlots 64、kBlockPoolBudgetBytes 192 MiB。字符串写入使用块局部的assign_string_ownedDataBlock.h而非Frame.h的assign_string_in_place——后者在目标未 detached 时会 share-assign会让管线下一次assign_utf8_in_place触发 detach 分配common-mistakes.mdrow 21 的坑T2 单测textWriteKeepsTheSourceUniquelyOwned正是回归防线。池化槽位用use_count()1探测空闲发布时以 aliasingshared_ptr交出无逐块控制块开销。关键设计决策 D1–D8Spec 的 Resolved DecisionsD1–D4与 plan 阶段的 D5–D8 共同约束实现D1 — tick-or-cap 刷新发布延迟与批内存双有界cap 值由--benchmark-hotpath扫参决定。D2 — 密集源纯数值契约携带字符串但密集源不产生逐块 worker 路径无字符串存储。D3 — CSV 严格时间序有界重排窗口到达批在小窗口内缓冲、按样本时刻合并排序后刷出剩余在会话关闭时刷出行永不乱序。窗口大小扫参决定。D4 — CSV 间隔快照默认关闭稀疏全速行保持默认既有低速率录制在节奏上逐字节不受影响任何采集都不会被静默降采样。密集源全速录制会得到大文件MDF4 与会话数据库仍是样本率数据推荐归宿。D5 — 线上协议默认保持帧形态API/gRPC 客户端继续收到逐帧载荷不变块形态载荷只给 opt-in 客户端现有stream.subscribe动词即是。无 API 版本升级、无 proto 字段追加、无 SDK 再生成。每消费者一条摄取路径针对的是摄取不是折叠对外发布格式。D6 — 每通道独立样本 cap实现期修订帧通道列须为每个数据集携带解析显示字符串updateDashboardData写dataset.value进 DataGrid/BarPanel/API 序列化的m_lastFrame从 double 重渲染会把22.40变成22.4因此kFrameBlockSampleCap 64携带文本密集列纯数值D2kStreamBlockSampleCap 4096StreamWorker.h。统一 cap 的原始决策因可见的 DataGrid 精度变化被否决。D7 — 单一固定 CSV 重排窗口起点 250 ms48 kHz 立体声约 384 KB 内存可确定地断言否决了单 tick 窗口worker 停顿会乱序与自适应窗口不可确定性测试。D8 — 管线线程是所有 sink 的唯一生产者实现期新增流 worker 不再直连消费者那会让每条 sink 的 SPSC 队列出现两个生产者——这正是历史上每种格式存在两个 sink 的原因而是把blockReady排队给FrameBuilder::ingestStreamBlock管线亲和两条通道都从同一个publishBlock离开。代价是每块一次排队跳转块速率48 kHz 源约 100 块/秒换来真正的单一摄取路径与 SPSC 不变式。StreamWorker的 display ring、dequeueDisplayBlock、Dashboard::drainStreamWorkers随之删除。消费端改造十个消费者各一条摄取路径R3BlockPublisher.cpp 是发布尾的枢纽仪表盘拿到池化槽位异步 sink 共享一份clone_block_trimmed仅数值、修剪到已填充样本仍是它取代的逐帧Frame拷贝的零头masked 块只给只读观察者API/gRPC保证回放或合成刷新永远不会把自己重新录制。消费端按任务清单逐一收敛仪表盘T6hotpathRxFrameapplyStreamUpdateapplyStreamChannel收敛为applyBlock/applyBlockColumndrainDashboardRing与drainStreamWorkers合并为共享 tick 预算下的单一 draincompare_frames()逐帧再校验删除块代际与最近快照不符则丢弃并计数。m_lastFrame/m_datasetReferences保留为 GUI 值镜像dashboard.getData、painter widgets、镜像发布者读取。API 服务器T19hotpathTxFrameingestStreamBlock收敛为ingestBlockD5 要求 worker 重建其序列化所需的帧故FrameBuilder新增structurePublished(sourceId, frame)信号每源保留一个模板FrameTemplate/bind_frame_template()/apply_block_sample()位于 DataBlock.h被 API、gRPC、MQTT 三种转换共享。gRPCT20hotpathTxFrame→ingestBlock首次让密集源项目到达 gRPC 客户端proto 字段号只增不改D5writer 循环按源模板把每块展开为每样本一个 proto 帧模板互斥保护writer 跑在自己的std::thread上。MQTTT21hotpathTxFrame→ingestBlock首次让密集源项目发布 MQTTexpandBlocks()把批物化进复用的帧暂存JSON 与 CSV 发布模式载荷逐字节不变。成本不变以前每帧收一份 detached 拷贝现在每样本一份。MDF4T12流源通过既有buildChannelGroups/m_groupMap路径变成同一个.mf4里的另一个IChannelGroup各自携带独立时间通道——这是单文件容纳不同速率源的机制。此处还暴露了原始值缺口MDF4 每数据集写一个 (raw) 通道块模型补上hasRaw/rawValues/rawText。CSVT10单 worker 稀疏单文件写入器见下节。SessionsT16帧队列与流队列合并为单一块队列raw_bytes与table_snapshots辅助队列保留writeFrameReadingswriteStreamBlocks→writeBlockshashReadingRow/hashStreamBlock→hashBlockRow两条旧哈希函数原样保留——v1/v2 归档必须哈希一致spec 0044。AudioExportT22ingestStreamBlock→ingestBlockFFT/Waterfall 的 WAV taps 继续按数据集 uniqueId 匹配打开的 WAV 会话仍计为活跃 sink。镜像T23见下节偏差说明。热路径基准T32发布尾计数器随改名跟进新增块刷新读数。关闭盲区MQTT 与 gRPC 首次接收密集源数据R4R4 要求项目只有流通道源、无任何字节源时MQTT 发布其值、gRPC 客户端收到它们、远程镜像以真实采样率而非每显示 tick 一个值绘制。实施结果spec 的 Implementation outcome 段落MQTT 半边达成expandBlocks()让 JSON/CSV 发布载荷保持原样密集源首次上 MQTT。gRPC 半边达成writer 循环把块展开为逐样本 proto 帧密集源项目首次被 gRPC 客户端接收。镜像条款未达成已记录偏差镜像发布者仍在显示 tick 上读Dashboard::rawFrame()保持 0055 之前的保真度每数据集每 tick 一个值。其 viewer 必须转换hotpathRxFrame已不存在MirrorSession现在为每个镜像源发布StructureSnapshot并以单样本DataBlock包装——一个样本就是实际到达的数据填充到远端速率等于虚构测量。全速交付意味着重设计快照载荷、分块器、客户端解码、viewer 重建及所有 fixture被刻意推迟并写入 mirror.md。kWireVersion刻意保持 1载荷从未变化R12 的改了什么才 bump 什么由没改就不 bump满足源码确认 MirrorProtocol.h 中kWireVersion 1且 MirrorClient.cpp 第 403/548 行存在版本不符即拒收的分支。导出统一MDF4 单文件与会话录制R5、R7R5 — MDF4 每次会话一个文件任意速率混合的项目录制恰好产出一个.mf4包含所有数据集每组数据集在自己的时间基准上以自己的速率运行不再产出_stream_sourceN.mf4。buildColumnMap()在文件创建时一次性把 uniqueId 解析为通道组, 槽位writeBlockSample只保存块实际触碰的通道组。R7 — 会话录制单一表示录制的会话把所有源的样本以单一布局、全精度、单文件存储无论数据来自哪条通道。Sessions schema 升级为 v3readings与stream_blocks统一为blocks表实际 schema 见 DatabaseSchema.cppCREATE TABLE blocks ( block_id INTEGER PRIMARY KEY AUTOINCREMENT, session_id INTEGER NOT NULL, source_id INTEGER NOT NULL, unique_id INTEGER NOT NULL, block_number INTEGER NOT NULL, t0_ns INTEGER NOT NULL, t_end_ns INTEGER NOT NULL, -- 实现期补充窗口化 seek 只需索引查找 dt_ns INTEGER NOT NULL, -- 0 表示不规则时间 frames INTEGER NOT NULL, is_numeric INTEGER NOT NULL DEFAULT 1, min_value/max_value/sum_value REAL, -- 实现期补充报表聚合保持纯 SQL、O(blocks) finite_count INTEGER NOT NULL DEFAULT 0, values_blob BLOB NOT NULL, -- 小端 float64既有 codec raw_values BLOB, -- 变换前原始值spec 0085 texts BLOB, -- 长度前缀 UTF-8NULL 表示数值 raw_texts BLOB, times BLOB -- dt_ns ! 0 时为 NULL ); CREATE INDEX idx_blocks_session_uid_t0 ON blocks (session_id, unique_id, t0_ns); CREATE INDEX idx_blocks_session_t0 ON blocks (session_id, t0_ns, t_end_ns);帧通道的单样本块就是一个单元素 blob无特殊分支PRAGMA user_version 3、加性迁移、kCaptureFormatVersion升到 2DatabaseManager.h 中kCaptureFormatVersion 2。普通INSERT而非INSERT OR IGNORE——时间戳碰撞是常态以block_id决胜。文本采用长度前缀小端 uint32 UTF-8而非分隔符编码因为录制的值可能合法包含逗号、引号、换行甚至 NUL。R8 — 旧归档仍然可开、可浏览、可回放、可验证v3 判别是按会话探测而非PRAGMA user_version——用当前构建打开 v1 归档会迁移其 schema版本号不再说明既有会话样本的存储位置。sessionUsesBlocks()对blocks表做一行探测BlockReader.h配套的tst_sessions_legacy_archive用legacyArchivesAreNotTreatedAsBlockBacked用例钉死该判别把旧归档的user_version翻到 3断言判别器仍报 legacy。应用永不写旧布局且就地迁移/失效既有会话文件被明确禁止。CSV 稀疏单文件有界重排窗口R6、D3/D4/D7R6 是数据保真要求最苛刻的一条录制恰好产出一个.csv列为项目所有数据集的并集每行对应一个不同的样本时刻单元格只在数据集实际于该时刻采样时填充否则留空——不为补间隙重复旧值也不发明值不再产出_stream_sourceN.csv。实现要点T10重排窗口缓冲的是块而非行48 kHz 下 250 ms 窗口物化行意味着约 12000 行 × 32 格QString常驻内存缓冲块每块只花一个已分配指针合并流式进行。合并逻辑提取为可单测的 SparseRowMerger.happ/src/CSV/SparseRowMerger.h的对应实现。行时刻均匀网格块用其精确推导的偏移不规则块取monotonicFrameNs——没有这一步落在同一粗粒度时钟纳秒Windowssteady_clock约 15 ms上的两帧会合并成一行而丢失其一合并只在跨源时正确那正是每时刻一行性质的价值所在。QTextStream弃用表头与行均用带显式 UTF-8 BOM 的QByteArray复用流通道的无分配appendDouble。间隔快照模式writeSnapshotRow保留且含义不变、默认关闭D4现在从块的最新值前向填充。回放统一三个播放器重新进入统一契约R9所有三个播放器CSV、MDF4、会话都通过实时采集所用的同一契约注入录制数据CSV 播放器与 MDF4 播放器的调用点收敛为replayBlockreplayChannelSpans/replayChannelsTyped仍是它们的输入编码入口T27 记录两者的单元解码器是两种文件格式的真实差异合并会以假差异换真差异会话播放器T28直接用解码出的块调用replayBlock删除了startReplayStreamSource。旧的ReplayStreamSource陪跑驱动被整体删除T29——它存在的唯一理由是把会话播放器接进StreamWorker现在播放器直接发块其借用 span/text 指针的BlockingQueuedConnection例外也随之消失块的值是自有的。回放语义保持scrub、磁带稳定settlepass、回放追赶逻辑不变回放永不重新录制——BlockPublisher的 sink mask 保证回放与合成刷新只达只读观察者API/gRPC绝不喂给导出器。一个实现后果被处理回放 worker 以前算 FFT 窗口worker 没了之后Dashboard::applyBlockColumn在列不带窗口时回退到feedFftFromSamples()否则回放密集会话时 FFT/waterfall widget 会静默失声。吞吐、背压与版本纪律R10–R12R10 — 吞吐保持解析管线在所有分级 tier 上维持今日速率发布路径无稳态分配。实现通过结构性保证解析代码零改动与同一机器上的前后九 tier--benchmark-hotpath对比验证luaexporters读数预期改善——逐帧make_sharedTimestampedFrame深拷贝消失被每块一份修剪后的纯数值拷贝取代。R11 — 背压语义保持消费者跟不上时整块丢弃并计数正如今天整次显示更新被丢弃并计数不因慢消费者而对源降速、跨步或降精度。环满时丢弃整块并计数绝不部分或跨步投递AC10。R12 — 线上与 schema 版本显式 bump任何布局变化的已发布接口声明新版本旧版本对端要么被处理要么被带清晰消息拒绝绝不误读。实现期kWireVersion保持 1载荷未变、user_version升 3、kCaptureFormatVersion升 2、spec-0044 可复现性指纹由hashBlockRow覆盖值/原始值/文本。验收标准与测试验证AC1–AC12Spec 的验收标准以清单呈现实现结果大部分勾选AC1R1/R3✅ 源码审计显示每消费者子系统恰好一个摄取入口六消费者的第二条路径全部消失。AC2R4 部分MQTT broker 收到消息今日无、gRPC 客户端收到值今日无达成镜像 viewer 的固定窗口样本数匹配采样率这一条未达成见偏差 1。每项都是独立检查且每项在改动前构建上失败。AC3R2✅ 往返测试驱动数值、字符串、均匀网格源与不规则时间源数值位精确、字符串字节精确、时间逐样本精确tst_data_block、tst_stream_block_codec。AC4R5 待维护者观察48 kHz 低速率项目录制恰好一个.mf4第三方 MDF4 阅读器显示两个独立时间基的通道组。AC5/AC6R6 稀疏性与快照模式断言已由tst_csv_sparse_writer覆盖AC5 的窗口扫参T34仍需构建运行。AC7R7/R8✅ctest用检入的旧归档 fixturetests/fixtures/sessions/下的legacy_v1_readings.db与legacy_v2_stream.db钉死打开、回放同值同时间戳、同一 0044 结论。AC8R9 待维护者观察回放与 live 的轨迹一致性。AC9R10 T33 的--benchmark-hotpath前后对比与kFrameBlockSampleCap扫参待运行。AC10R11✅ 合成过载快于被故意卡住的消费者的密集源丢弃整块、递增丢弃计数、源采集速率不变。AC11R12 旧线版本的镜像 viewer 被带清晰版本不符消息拒绝MirrorClient.cpp既有路径旧 schema 版本会话文件被探测并通过 legacy 路径读取。AC12 block-lane 部分通过test_block_lane_sinks.py3 passed / 3 skipped缺虚拟采集设备、test_csv_export.py5 passed完整套件被一个无关的既有崩溃spec 0056 的 C2中断约 40 个集成文件跑了 17 个——其余未验证是因为崩溃而非失败。实施结果、偏差与落地后修复Spec 的 Implementation outcome 与 Post-landing fixes 记录了真实落地状态2026-08-1636 项任务落地 34 项scripts/code-verify.py --check在全部 79 个改动文件上干净。R1/R2/R3/R5/R6/R7/R8/R9/R11/R12 全量达成R4 的 MQTT 与 gRPC 半边达成——密集源项目首次到达两者。两条刻意偏差① R4 镜像条款未达成viewer 已转换且工作但保持每 tick 每数据集一值全速交付待镜像协议重设计见 mirror.md② 块支撑会话在探索器报表中不报告标准差逐块摘要携带 min/max/sum/count 但无平方和报错数字比不报更糟需要时加一列即可。三个落地后修复由test_block_lane_sinks.py与集成扫描发现而非评审发现QuickPlot 录制为空CSV::ExportWorker::processItems()曾回退到createCsvFile((*items.begin())-data)块只携带数值该回退被删除后无替代模板又只在连接时ProjectFile 模式门控取一次QuickPlot 中连接时结构尚不存在——启用、无报错、无输出。修复为给每个 worker 加applyPublishedStructure()由FrameBuilder::structurePublished供料test_csv_sink_alone_writes_rows为回归测试。连接时模板获取在 GUI 线程旋转嵌套事件循环connectDevice()内invokeOnBuilderThreadBlocking在 API 命令驱动下崩溃。替换为管线线程发出的FrameBuilder::sessionStructureReady(const Frame)三个 sink 排队消费载荷是并集帧m_frame/m_quickPlotFrame而非单源帧——单文件导出构建一个 schema单源帧会静默丢失其他源的列C1见 findings.md。空结构会覆盖好结构sessionStructureReady是排队的QuickPlot 中其空载荷可能在structurePublished填充模板之后到达令 worker 无 schema 无文件。setTemplateFrame()现在忽略空帧。总结Spec 0055 是 Serial Studio 内部架构的一次收敛性重构把逐帧 vs 逐块两种发布形态统一为一个列优先、池化、零分配的DataBlock契约让十个消费者各自只剩一条摄取路径并首次让 MQTT 与 gRPC 看见密集源数据持久化侧以blocks单表统一会话存储、以稀疏单文件统一 CSV、以多时间基单文件统一 MDF4同时用按会话探测的 legacy 读取路径守住 R8 的旧归档只读历史承诺。其实现纪律解析代码零改动、SPSC 单生产者不变式、发布路径零稳态分配、版本显式 bump与验收清单AC1–AC12 逐项勾选或记录偏差使其成为一份可审计、可复现的架构变更范本。若想深入实现细节可继续阅读 spec.md、plan.md、tasks.md以及核心载荷 DataBlock.h 与发布尾 BlockPublisher.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),仅供参考