Serial Studio 模块化核心重构:`core/` 下七个静态库与类型化进程内消息总线
2026/9/18 23:12:36 网站建设 项目流程

Serial Studio 模块化核心重构:core/下七个静态库与类型化进程内消息总线

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

导读

Serial Studio 是一个开源的遥测数据可视化桌面应用,支持 UART、BLE、MQTT、Modbus、CAN Bus 等多种数据源。在 2026 年 9 月的"模块化核心(Modular core)"重构中,项目将原先单一大目标编译的应用拆分为core/目录下七个静态链接库(Core / Protocols / Pipeline / Devices / Storage / Api / Ui),并新增一个以 C++ 类型为话题(topic)的进程内消息总线Core::Bus::MessageBus,为后续逐步消除跨库单例访问提供了机制基础。本篇以 tasks.md 的任务清单为主线,结合 spec.md、plan.md、handoff.md 与仓库源码,完整还原这次重构的目标、任务编排、分层门禁、消息总线设计与验收方式,读者可以据此理解 Serial Studio 当前的构建结构与参与后续模块化工作的入口。

一、为什么必须模块化:单体构建的问题

重构之前,Serial Studio 只有一个可执行目标,由一份约 2400 行的app/CMakeLists.txt构建:app/src/下的每个源文件都编译进SerialStudio可执行文件,所有头文件通过一个include_directories(src)互相可见,没有任何机制阻止一个线格式编解码器(wire-format codec)直接触达某个单例对象(spec.md 中的问题描述)。

项目已有的 C++ 单元测试层(141 个套件、11 个 fuzz 目标)其实证明了大量模块是"纯净"的——每个套件只重编译它需要的生产.cpp文件加上SSAssert.cpp——但这个证明只存在于测试注册表里,应用本身并没有把这些代码链接为库。后果有三:

  • 测试编译的文件定义,与应用实际构建时使用的定义可能不一致;
  • 每个测试套件各自重编译同一批.cpp,构建浪费;
  • 没有任何编译期或构建期机制阻止向上引用(upward include),跨模块的耦合只能靠纪律维持。

2026-09-03 的一次外部架构评审提议按依赖边界模块化:先有基础库、纯协议库,再在其上叠加 pipeline、device/storage/API/UI 库,依赖只允许向下流动,兄弟模块之间通过接口、信号或不可变消息通信。维护者接受了方向,并附加三条修正:

  1. 静态链接进同一个可执行文件;
  2. 目标名与目录名使用CamelCase(而非ss_xyz);
  3. 库放在新的顶层core/目录下。

二、总体设计:七个静态库与依赖图

重构的最终形态是core/下七个STATIC库目标,每个库恰好拥有一份CMakeLists.txt,最终全部链接进唯一的SerialStudio可执行文件。core/CMakeLists.txt顶部注释给出了目标依赖图:

Ui -> Api -> Storage -> Pipeline -> Protocols -> Core -> Qt6::Core Devices -----^ (a sibling of Pipeline; Api and Ui see both)

当前仓库中的七个库(见 core/CMakeLists.txt):

库目标目录内容职责
SerialStudio::Corecore/Core/断言、热路径宏、SIMD 内核、校验和、SPSC 环形缓冲、解析预算、异步任务树、Bus/消息总线等依赖最轻的基础层,只需 Qt Core
SerialStudio::Protocolscore/Protocols/CAN ISO-TP / J1939 TP、S7comm PDU 与 ISO-TSAP、IEC 60870-5-104 APCI/ASDU、Sparkplug B、Modbus RTU、X/Y/ZMODEM、USB 十六进制解析、protobuf 词法/语法解析纯线格式编解码器,接收字节返回类型化结果
SerialStudio::Pipelinecore/Pipeline/DataModel/DSP.hFrameReaderPipelineHostStreamWorkerAppPlatform数据处理流水线
SerialStudio::Devicescore/Devices/各协议驱动、ConnectionManagerDeviceManagerHAL_DriverAsyncTcpDial、FileTransmission 门面、MQTT/设备接入
SerialStudio::Storagecore/Storage/CSV/MDF4/Sessions/InfluxDB/数据落盘与回放
SerialStudio::Apicore/Api/API/(除API/GRPC/面向脚本 / gRPC 的 API
SerialStudio::Uicore/Ui/UI/Console/Platform/AI/Misc/(除 ModuleManager 与 CLI)界面与交互

可执行文件(app/src/)只保留"组合根"(composition root):main.cppSerialStudio.*AppState.*SessionContext.*Misc/ModuleManager.*Misc/CLI/Licensing/SelfTest/Benchmark/ThirdParty/以及 gRPC 胶水代码。

2.1 目标命名与别名的讲究

按维护者要求使用 CamelCase,SerialStudioCore不会与 Qt 的Core模块冲突;同时提供命名空间别名SerialStudio::Corecore/Core/CMakeLists.txt:136add_library(SerialStudio::Core ALIAS SerialStudioCore)),其余各层同理。

2.2 构建顺序本身就是隔离手段

core/add_subdirectory被刻意放在app/CMakeLists.txt内部——在 Qt 发现与qt_policy(SET QTP0004 NEW)之后、include_directories(src)之前。这样做的原因写在了 core/CMakeLists.txt 的注释中:"隔离是构建顺序的属性,而不是 lint 规则的属性"。因为 CMake 在add_subdirectory时快照目录级定义,库目标天然看不到app/src/;同时它们又能继承根目录的全局优化 / SIMD / PGO / sanitizer 标志与BUILD_COMMERCIAL,而根CMakeLists.txt从不调用find_package(Qt6),因此不能在根目录添加。Windows 头文件卫生(WIN32_LEAN_AND_MEANNOMINMAX_WINSOCKAPI_)也必须在core/CMakeLists.txt顶部单独设置,否则GsUsbProtocol.h/ParseBudget.h里的std::min/std::max会被宏破坏(handoff.md 的评审修正)。

三、Stage 1:Core 与 Protocols(严格层)

Stage 1 是严格阶段:纯协议编解码器与依赖轻的基础层先变成CoreProtocols两个库。

3.1 文件迁移清单

Core库首批迁入 13 个文件(全部为 GPL 基础文件),Protocols库迁入 26 个文件,迁移前后路径与 include 形式对照如下(来自 plan.md 的迁移表,均已落地):

Core(13 个文件)

迁移前(app/src/)迁移后(core/Core/)include 变为
SSAssert.h/.cppSSAssert.h/.cpp"Core/SSAssert.h"
Concepts.hConcepts.h"Core/Concepts.h"
DSPSimd.hDSPSimd.h"Core/DSPSimd.h"
DataModel/HotpathOptimization.hHotpathOptimization.h"Core/HotpathOptimization.h"
DataModel/ParseBudget.hParseBudget.h"Core/ParseBudget.h"
IO/CircularBuffer.hCircularBuffer.h"Core/CircularBuffer.h"
IO/Checksum.h/.cppChecksum.h/.cpp"Core/Checksum.h"
Async/AsyncClock.hAsync/…"Core/Async/…"

Protocols(26 个文件,其中 Pro 门控部分)

迁移前迁移后门控
IO/Drivers/CANBus/CanReassembly.*CANBus/GsUsbProtocol.hCAN/…Pro
IO/Drivers/S7/IsoTsap.*S7/S7Pdu.*S7Address.*S7/…Pro
IO/Drivers/Iec104/Apci.*Asdu.*Iec104/…Pro
IO/Drivers/MQTT/SparkplugPayload.*Sparkplug/…Pro
IO/Drivers/Modbus/ModbusRtuCodec.*Modbus/…Pro
IO/FileTransmission/CRC.hProtocol.hXMODEM.*YMODEM.*ZMODEM.*FileTransfer/…GPL 基础

迁移是**逐字(verbatim)**的:只有路径、include 和 CMake 变化,不改变任何编解码器的行为、签名或文件配对。这也体现在 tasks.md 的任务验收上:git diff -M必须显示每个移动文件只是 rename,且仅有#include行的改动。

3.2 Pro 编解码器的门控(GPL/Pro 源码集守恒)

原本这些 Pro 编解码器的.cpp位于app/CMakeLists.txtif(BUILD_COMMERCIAL)块中。迁移后在Protocols中改为if(BUILD_COMMERCIAL OR SS_BUILD_TESTS)条件添加——这正是open62541的既有先例(lib/CMakeLists.txt)。这样 GPL 单元测试层仍然编译它们,而 GPL 应用二进制不会引用其符号:静态归档只贡献被引用的对象,所以 GPL 二进制中的 Pro 代码对象数量保持不变。

3.3 include 重写策略

迁移涉及约 300 个源文件的 include 行改写。plan.md 明确选择了精确字符串sed而非手工编辑:只针对 20 种已知的 include 形式做字面量替换(模板化操作,非代码逻辑),随后用解析器检查 + diff 审查双重验证,每个移动文件在git diff -M下必须只显示 include 行变化。旧的app/src路径不保留转发 shim 头——shim 会让分层慢慢腐化,因此被明确否决。

3.4 链接关系与测试改造

  • ProtocolsPUBLIC方式链接CoreCore只链接Qt6::Core(外加全局 SIMD 内核标志)。OPC UA 编解码是唯一例外,按现状基于内置open62541栈门控(spec.md R3)。
  • app/tests/CMakeLists.txt中所有曾重编译移动.cpp的注册项改为链接库目标:handoff 记录共 122 个注册项(104 个链接SerialStudio::Core,18 个链接SerialStudio::Protocols),套件与 fuzz 数量不变。没有套件被删除、禁用或改动断言——这是 spec 的 R5 硬性要求。

四、Stage 2:五大分区库与依赖债务的"棘轮"

Stage 2(2026-09-04)把app/src剩下的子系统整体搬入core/<Layer>/,共约 998 次 rename(两个阶段合计),每个分区目录保留原有相对 include 路径,因此几乎无需改动任何源码行。分区方式见 plan.md 的分区表:

目录内容
SerialStudio::Pipelinecore/Pipeline/DataModel/(整体)、DSP.hDSPDownsample.hIO/FrameReader.*IO/PipelineHost.*IO/StreamWorker.*IO/FrameConfig.hPlatform/AppPlatform.*
SerialStudio::Devicescore/Devices/IO/(其余部分:驱动、ConnectionManager、DeviceManager、HAL_Driver、AsyncTcpDial、FileTransmission 门面)、MQTT/
SerialStudio::Storagecore/Storage/CSV/MDF4/Sessions/InfluxDB/
SerialStudio::Apicore/Api/API/(除API/GRPC/
SerialStudio::Uicore/Ui/UI/Console/Platform/(其余)、AI/Misc/(除ModuleManager.*CLI/

4.1 诚实声明依赖环

关键设计决策是:这五个库今天的真实依赖图是穿过单例的完整环。既然无法在一夜之间消除它,就诚实地声明它:五个分区库互相PUBLIC链接(环),让 CMake 为 GNU ld/lld 发出归档重复链接;同时把每个方向的向上 include 数记入基线scripts/layer-baseline.json,由layer-verify.py作为棘轮(ratchet)——增长即失败 CI,Core/Protocols保持严格。评审目标的下行图是基线逐步逼近的方向。

迁移前的 include 边普查(穿越边界的引号 include 数量):

Pipeline→Ui 99 Pipeline→App 91 Pipeline→Devices 50 Ui→Pipeline 93 Ui→App 75 Api→Pipeline 89 Api→App 37 Devices→Pipeline 58 Devices→App 44 Devices→Ui 36 Storage→Pipeline 63 Storage→App 35 … (完整表见 scripts/layer-baseline.json)

其中App边是对SerialStudio.h/AppState.h/SessionContext.h/Misc/ModuleManager.h的 include——即"从下方触达组合根",这正是"单例地狱"被量化的样子。

4.2 (file, condition) 守恒检查

Stage 2 有一个专门的验收(AC10):用脚本解析旧的(git show HEAD:app/CMakeLists.txt)与新的库源码列表,断言(file, condition)集合守恒——0 缺失、0 多余、0 重新门控。handoff 记录该检查覆盖 24 个平台 × 特性组合,全部通过。

4.3 可执行文件级设置的审计与复制

迁移中,凡是被移动文件读取的可执行文件级编译定义/选项,都必须复制到对应库上,否则文件在库中编译时会静默丢失特性。handoff 记录了其中几项:

  • ENABLE_GRPC定义 + 生成头 include 目录 +ss_proto_generated依赖应用于 Pipeline/Devices;
  • miniaudio 定义通过ss_apply_miniaudio_definitions应用于 Devices(QuickPlotBuilder.cpp通过IO/Drivers/Audio.h引入ThirdParty/miniaudio.hMA_*布局开关必须与可执行文件的miniaudio.cpp完全一致,否则ma_device结构体尺寸不一致会静默损坏);
  • core/{Api,Storage,Ui}/CMakeLists.txt直接链接 luajit 与 QCodeEditor——它们 include 了FrameBuilder.h等头,过去只经由声明的环拿到 Lua include 目录,若先把某条边翻为严格,就会先报"缺头"而不是分层错误;
  • 可见性、/W4、加固(serial_studio_harden())、unity 构建的对齐。

4.4 几个"没有编译器在场"的约束

仓库规则禁止 Agent 在本轮运行cmake或编译器,因此置信度完全来自:(a) 逐字移动、无语义编辑;(b) 既有测试层已证明每个移动单元可独立编译链接;(c) 脚本化验证 include 解析、CMake 源码列表完整性与分层。凡无法这样验证的都不在范围内(见 spec.md 的 Non-Goals)。

五、分层门禁:scripts/layer-verify.py

分层不是口头约定,而是由 layer-verify.py 强制执行的构建期门禁,并接入 CI lint 任务。脚本顶部用一张表描述分层(无新 JSON 配置文件):

LAYERS = {"Core": [], "Protocols": ["Core"]} # 下层可 include 的层 APP_ROOTS = ("app/src", "app/tests") # 可 include 任何东西

Stage 2 后扩展为七层 +DEBT_LAYERS棘轮边 +pair-split+include-ambiguous。脚本的检查项(每项都是一个错误,见脚本头部文档):

  1. include-unresolved——core/**app/src/**app/tests/**中每个引号#include "…"都能在"包含者所在目录 /core//app/src"中解析(Qt/系统尖括号 include 跳过);
  2. layer-upward——core/<Layer>/下的文件 include 了core/<Layer>/core/<Allowed>/或 Qt/系统之外的东西;
  3. core-unowned——core/下的.cpp/.h没有出现在任何core/**/CMakeLists.txt中,或出现在多于一个目标中;
  4. cmake-missing——app/CMakeLists.txtsrc/…项、app/tests/CMakeLists.txt${SS_APP_SRC}/…项、或任何core/**/CMakeLists.txt项指向了不存在的文件;
  5. moc-double-listed——core/下的头同时出现在app/CMakeLists.txt(双重 moc = 重复staticMetaObject符号);
  6. include-ambiguous——一个相对 include 在多个根中都能解析(移动后不可能,但仍被检查);
  7. pair-split——.cpp与同名.h被列在不同目标中;
  8. layer-debt-growth——一条棘轮边携带的 include 数超过scripts/layer-baseline.json的基线。

脚本支持--json(CI 友好),任何错误都以退出码 1 结束;--accept在仍有严格错误时拒绝重播种基线,防止用基线搅动掩盖增长。该脚本同时在 doc/claude/scripts.md 中有一行文档。handoff 记录的 Stage 2 基线为 16 条边、594 个向上 include。

六、进程内消息总线:Core::Bus::MessageBus

第二个关键机制是core/Core/Bus/下的进程内发布/订阅总线,设计意图是"库之间经由一个 struct 而非一个 class 耦合"(MessageBus.h文件级注释)。它被描述为库之间对话的"虚拟 CAN 总线"。

6.1 核心设计决策

  • 话题是类型,不是字符串:订阅表是unordered_map<std::type_index, vector<Subscriber>>MessageBus.h:226),键永远是std::type_index(typeid(T)),API 中不存在任何字符串标识符。消息词汇表集中在 Messages.h——"the DBC"——只有 Core/Qt-Core 值类型的聚合 struct,任何层只要链接 Core 就能发言。
  • 每次发布只构造一个对象compose<T>()用花括号初始化构造T message{args...}make_shared<const T>(std::move(message))——聚合话题无需构造函数;随后擦除为ErasedMessage = shared_ptr<const void>(保留原控制块),订阅端static_pointer_cast<const T>恢复类型化指针,无第二次分配。每个订阅者拿到的是同一个std::shared_ptr<const T>tst_message_bus.cpp中有断言此身份一致性的用例)。
  • 接收者亲和(receiver affinity)deliver()检查receiver->thread() == QThread::currentThread()——同线程直接内联调用;跨线程则QMetaObject::invokeMethod(receiver, lambda, Qt::QueuedConnection),lambda 同时捕获 handler 副本与shared_ptr,消息生命周期覆盖接收者事件循环的任意延迟排空。Qt::BlockingQueuedConnection在订阅时被拒绝:debug 中止,release 降级为 queued——因为"发布者在订阅者线程上阻塞"会在两个线程互相发布时死锁。
  • 派发:锁下拷贝、锁外执行dispatch()m_mutex下拷贝订阅者 vector 到局部量后立即释放锁再逐个投递,因此 handler 可以重入地发布、订阅或退订(测试在tst_message_bus.cpp);由 handler 新增的订阅者加入下一次发布而非本次。
  • 保留状态(retained state)publishState<T>既派发又把最新消息存入m_retainedlatest<T>()返回保留指针(可空);subscribe(..., replayLatest = true)在返回句柄前同步重放(直接在订阅者自身线程调用 handler,正确性有保证,因为订阅总是从该线程发起)。普通publish从不保留。这就是"可读的共享内存区":一个状态话题是一个被所有库以指针读取的不可变对象。
  • 生命周期Subscription是 move-only RAII 句柄,持有{id, topic, QPointer<MessageBus>},析构时退订并脱离;release()只脱离不退订,用于"与接收者同生共死"的订阅。总线通过QPointer持有,句柄晚于总线析构时退化为空操作。此外总线对每个接收者的destroyed信号(DirectConnection)连接一次,purgeReceiver()同时清扫匹配地址与所有已空QPointer——因为QObject在发射destroyed之前就已清空其QPointer
  • 所有权:按当前树(spec 0077 之后),总线由组合根构造并注入每个模块构造函数与Core::Services"总线从不自构、无全局访问器"MessageBus.h:62-63注释明确写到 "spec 0077 T73")。0076 阶段曾是文件级静态s_instance+setInstance()的过渡形式,0077 将其移除——这是从"单例迁移脚手架"到"构造注入"的演进。

6.2 双线程时序

bus-design-review.md 给出了发布者(pipeline 线程)与 GUI 线程之间的完整时序:

pipeline thread GUI thread --------------- ---------- sub = bus.subscribe<ConnectionStateChanged>( &dashboard, h, AutoConnection); -> lock; id=7 appended; destroyed-guard connected bus.publishState<ConnectionStateChanged>(3, true, false) compose -> one make_shared<const T> (1 allocation) retain -> lock; m_retained[typeid] = ptr dispatch -> lock; copy vector<Subscriber>; unlock deliver -> receiver->thread() != current; invokeMethod(dashboard, [h, ptr]{…}, Queued) -> QMetaCallEvent posted returns (never waits) loop runs the lambda: h(static_pointer_cast<const T>(ptr)) ptr released; refcount 0; T destroyed here

之后任何线程调用bus.latest<ConnectionStateChanged>()都会在锁下返回同一对象。

6.3 与热路径的关系:两条互补的通道

总线明确永不进入每帧路径MessageBus.h:58注释写着 "NEVER on the per-frame path"。帧/数据块走 spec 0055 的专用池化 SPSC 路径(BlockStager预留 64 个槽位、BlockPublisher单生产者扇出、Dashboard::onDisplayTick在 32 槽环形缓冲内以墙钟预算排空、预算之外除最新块外全部丢弃的 latest-wins 槽位)。scripts/code-verify.pybus-on-hotpathlint(约 2008-2029 行)禁止热路径允许列表中的文件引用MessageBus——这是"总线绝不因意外出现在帧速率"的第二道保险。bus-design-review.md 因而把两者定性为互补而非竞争:块路径是 10 kHz+ 遥测的有损 60 Hz 通道,总线是"因为稀有所以无损"的控制/状态/通知通道。

6.4 库选型对比

bus-design-review.md 还逐一评估了外部建议的候选库并说明为何不用:entt(无线程亲和模型)、Boost.Signals2(更慢、无接收者亲和、引入 Boost)、CAF(模块已是带事件循环的 QObject,等于进程内两个调度器)、NNG(C 字节缓冲 API,需要序列化往返)、iceoryx2/Zenoh/eCAL(多进程专用,0076 路线图无此需求)。自研实现约 370 行、四个文件,换来的是"一次发布一个对象、所有订阅者看到同一个 const 指针"的不可变契约。内存池建议也被否:mimalloc 已是全局分配器(SS_USE_MIMALLOC,默认 ON),帧路径由BlockStager的别名shared_ptr专用池服务(无每块控制块),命令速率下每次发布的单次make_shared不可测量。

七、迁移路径:从单例访问到总线话题

Stage 2 完成后对跨库单例访问做了普查:1091 个跨库X::instance()/SessionContext::current()调用点,覆盖 142 个 (调用方库, 被调类) 对。handoff.md 记录了每种访问形态在总线上的对应形式:

访问形态今天总线形式
读取外部状态(X::instance().isConnected()pull保留状态话题,latest<T>()或订阅
对外部模块发命令(X::instance().connectDevice()直接调用所有者订阅的请求消息T;结果作为后续状态话题
connect(&X::instance(), &X::sig, …)单例上的 Qt 信号subscribe<T>T是该信号负载的 struct

7.1 消息词汇表与后续规格顺序

初始词汇表(Messages.h,8 个话题)派生自当晚的单例普查:连接状态、项目加载/修改、通知、仪表板结构变化、录制会话边界、设置变更等。每条边一个后续 spec(每条编译、每条把一条边翻为严格):

  1. OperationModeChanged+LicenseStateChanged+LanguageChanged保留话题;TimerEventsWorkspaceManagerNotificationCenter移入 Core——消灭大部分*→App*→Ui读取;
  2. ConnectionManager发布ConnectionStateChanged,Pipeline/Ui/Storage 读取;
  3. ProjectSnapshot保留话题——消灭 Api→Pipeline 与 Ui→Pipeline 读取;
  4. Storage↔Ui 回放命令、Api→Ui 请求;
  5. ProjectModel编辑器/表单移出 Pipeline 进 Ui(消灭IconRegistry访问);
  6. 面向目标图允许但应为接口的边的端口(IDeviceOutputIDataSink),随后拆分ConnectionManagerFrameBuilder协调者。

handoff.md 顶部还注明:后续的 spec 0077(doc/claude/specs/0077-independent-modules/,2026-09-08/09)已把本 handoff 留下的棘轮边全部驱动到零——每层严格、无跨库instance()访问、过渡性的MessageBus::instance()已被移除。也就是说,0076 埋下的机制在 0077 兑现成了最终形态。

7.2 成功度量

迁移成功的信号是单例普查总数下降而MessageBus计数保持在 1 附近——初期普查为 1554 个 reach(93 个类,分桶 static-cache 1071 / root 160 / ctor-capture 123 / accessor 70 / loose 96 / deferred 34);MessageBus从当前计数 1 上升即为计划中明说的度量本身,而非新增债务。bus-design-review.md 建议进一步采用构造注入而非增加第十个SessionContext槽位,使下降"无补偿"——这个决策影响约 1091 个调用点的构造签名形状,必须在第一个后续 spec 之前定下。

八、任务的编排与验收(tasks.md 全览)

这次重构本身是按"Wave"组织的多 Agent 协作任务,tasks.md 完整记录了编排结构,这既是文档也是可复用的流程模板:

Wave 0 —— 移动(owner:M)

  • T1创建core/布局并git mv39 个文件;验收:git status只显示 rename,旧路径消失;
  • T2重写app/core/的 include 行;验收:grep 找不到任何残留的旧 include 形式,每个移动文件的git diff -M只显示#include行。

Wave 1 —— 并行(A/B/C/D 四个执行者)

  • T3CMake:库目标 + 可执行 + 测试(A);
  • T4scripts/layer-verify.py+ CI 接线(B);
  • T5lint/工具根扩展:code-verify.pyclaim-verify.pytranslation_manager.py认识core/(C);
  • T6AI 面向文档:CLAUDE.mddoc/claude/directory-map.md、架构文档、skills 更新(D)。

Wave 2 —— 集成与评审

  • T7跑全部静态门禁并修复循环;
  • T8独立评审 CMake 与门禁脚本;
  • T9重播种 census / claim 基线(仅针对移动的键),sanitize-commit.py
  • T10handoff.md(晨间清单 + 分诊)。

Wave 3 —— Stage 2 分区与总线(2026-09-04)

  • T11git mv五个子系统树到core/<Layer>/
  • T12测试 CMake${SS_CORE_SRC}重指向(283 项);
  • T13可执行文件级设置审计(只读);
  • T14五个core/<Layer>/CMakeLists.txt+core/CMakeLists.txt+app/CMakeLists.txt
  • T15layer-verify.py七层 + 逐边棘轮 +pair-split+include-ambiguous
  • T16工具常量重指向;
  • T17文档/skills 重指向 + CLAUDE.md 契约行;
  • T18Core::Bus::MessageBus+Messages.h+ tst_message_bus.cpp;
  • T19(file, condition)守恒检查 + 全门禁运行 + 评审 + 修复;
  • T20handoff.mdStage 2 章节与迁移表。

Definition of Done(全部勾选):spec.md 的 AC1-AC5 已检查(AC6/AC7 留给维护者执行并附确切命令);layer-verify.pycode-verify.py --check、各 census、claim-verify.pyregistry-verify.pydocumentation-verify.py全部干净;git diff -M显示每个移动文件都是仅含 include 改动的 rename;diff 正是所要求的内容且仅此而已,无任何提交。

8.1 静态门禁与维护者晨间门

handoff.md 给出了完整可复现的验证命令。静态门禁(当晚):

python scripts/layer-verify.py python scripts/code-verify.py --check python scripts/claim-verify.py python scripts/registry-verify.py python scripts/documentation-verify.py reuse lint # 若已安装 git diff -M --stat # 每个移动文件应为 rename + 仅 include 行改动 python scripts/sanitize-commit.py

需要编译器的门(维护者晨间执行):

cmake -G Ninja -B build/0076 -DCMAKE_BUILD_TYPE=Debug -DSS_BUILD_TESTS=ON -DBUILD_GPL3=ON \ -DENABLE_GRPC=OFF -DWITH_WEBENGINE=OFF -DSS_USE_MIMALLOC=OFF cmake --build build/0076 --target ss_unit_tests && ctest --test-dir build/0076 --output-on-failure cmake --build build/0076 # 应用本体 # 之后是惯用的 Pro 配置,以及 release 二进制上的 --benchmark-hotpath

纯移动可能产生的三种错误形态及分诊方法(handoff.md):

  • fatal error: 'X.h' file not found(core/ 内)——移动文件曾依赖app/src的传递 include:补Core/…include;若该头是 app 专有,说明该文件被误分类,移回去并从库的 CMakeLists 移除;
  • 重复符号staticMetaObject/qt_metacall——Q_OBJECT头被列在两个目标中:layer-verify.py规则 5 覆盖app/CMakeLists.txt,同时检查app/tests注册;
  • 测试二进制未定义引用——某注册项丢失了仍需要的.cpp,或需要LIBS SerialStudio::Protocols(它会传递链接 Core)而非Core:只修那一个注册项,不要重新添加.cpp

验收时还用到这条 grep 确认无残留旧 include(AC5):

grep -rn '"IO/Drivers/S7/\|"IO/Drivers/Iec104/\|"IO/Drivers/CANBus/CanReassembly\|"IO/FileTransmission/\|"IO/Checksum.h\|"IO/CircularBuffer.h\|"SSAssert.h"\|"Async/' app core

8.2 当晚落地的门禁结果

handoff 的 Gate 表:layer-verify.py0 错误;code-verify.py --check0 错误(10 条既有 advisory);singleton/tu/dup census 与基线持平、无需重播种;claim-verify.py0 错误(8 条既有 advisory);registry-verify.pyCLEAN;documentation-verify.py0 发现;reuse lint合规;pytest的失败均为 HEAD 上既有、与 0076 无关。移动之外唯一的源码改动是两处头自包含修补(core/Protocols/FileTransfer/CRC.h<QtGlobal>core/Core/Checksum.h<functional>+<cstddef>)和一处FrameReader.h的 include 路径修正。

九、明确不做的事(Non-Goals)

spec.md 划清了边界,防止范围蔓延,也回答了"为什么 Frame 没进 Core"这类问题:

  • 今晚不追求五个分区库之间的完全下行图——这需要把单例访问替换为总线话题,一条边一条边地编译推进;
  • 帧/块热路径不经过总线:DataBlockReady/结构扇出保持在 spec 0055 的专用池化块路径;
  • 不拆分DataModel/Frame.h、不把帧值类型移入 Core——其编译闭包(SerialStudio的 QObject 枚举、PropertyHooks.h → ProjectModel.hcommercialCfg许可)与应用纠缠,需带编译器在环的后续 spec;
  • 不做 Pipeline 库拆分、FrameBuilder分解、ConnectionManager拆分、IDataSink端口、ProjectSnapshot、视图模型抽取或单例移除——各自独立成 spec;
  • 不移动FrameReader(它触达NotificationCenterAppPlatformSerialStudio元对象)或任何驱动;
  • 不改变任何编解码器的行为、签名或文件配对;
  • 不做共享/动态库、不导出库目标、不做公共 SDK、不改内部标识符或命名空间。

plan.md 的"Deferred: Frame into Core"章节还给出了Frame.cpp的完整编译闭包审计(SerialStudio.hAppInfo.hDatasetSerialization.cppPropertyValidators.cppSerialStudioFrameSupport.cpp等),说明把 Frame 移入 Core 意味着拆分SerialStudio为 Core 枚举持有者加应用类、切断PropertyHooks.hProjectModel.h、迁移commercialCfg、重指向生成器——这是需要编译器在环的语义工作,只能独立成 spec。

十、最终形态验证

当前仓库中可以看到这次重构的完整落地痕迹:

  • core/CMakeLists.txt 依次add_subdirectory七个层,顶部注释描述目标依赖图与"依赖只向下流"规则;
  • core/Core/CMakeLists.txt 以qt_add_library(SerialStudioCore STATIC …)列出基础层源码与头文件(Q_OBJECT头必须列在库中且不得再出现在可执行文件的HEADERS里,否则双重 moc 导致重复符号);Qt6::Core是完整链接集,serial_studio_harden()施加逐目标加固;
  • core/Core/Bus/MessageBus.h 与Messages.hSubscription.h/.cpp构成总线实现,配套 tst_message_bus.cpp 覆盖直接、排队、保留、析构退订与多订阅者投递(10 个用例);
  • scripts/layer-verify.py 承载分层棘轮,scripts/layer-baseline.json 记录 16 条边的向上 include 基线;
  • scripts/code-verify.pybus-on-hotpathlint 防止总线进入热路径允许列表。

阅读完本文,读者应当能够:理解 Serial Studio 当前七个静态库的边界与依赖方向;用layer-verify.py及配套门禁验证任意改动不破坏分层;理解Core::Bus::MessageBus的类型化话题、保留状态与接收者亲和语义;并按 tasks.md 的 Wave 结构与验收标准,参与到把剩余单例访问迁移为总线话题的后续工作中。

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

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

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

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

立即咨询