ARTICLE DETAIL

资讯详情

深耕编程入门与网站建设的一线实战洞察。

Serial Studio 连接判定统一架构(Spec 0050):openFinished 信号、轮询退役与 setter 守卫 lint 实战解析

Serial Studio 连接判定统一架构(Spec 0050):openFinished 信号、轮询退役与 setter 守卫 lint 实战解析 Serial Studio 连接判定统一架构Spec 0050openFinished 信号、轮询退役与 setter 守卫 lint 实战解析【免费下载链接】Serial-StudioOpen-source telemetry dashboard. Supports UART, BLE, MQTT, Modbus, CAN Bus and more.项目地址: https://gitcode.com/GitHub_Trending/se/Serial-Studio本文基于 Serial Studio 仓库中doc/claude/specs/0050-connection-verdict-unification/下的tasks.md配合spec.md、plan.md与doc/claude/architecture/io.md展开。该规格解决的是遥测仪表盘中一个真实而棘手的工程问题一次连接尝试open attempt的结局必须只有一个唯一的所有者owner。本文会完整讲解驱动层HAL_Driver::openFinished(ok, reason)信号与 emit-once 闩锁latch的机制、ConnectionManager如何消费判定并退役旧的轮询清扫sweep以及驱动 setter 同值守卫 lint 规则与集成测试的落地方式。读者读完后可以掌握在 Qt/QML 多总线架构UART、BLE、MQTT、Modbus、Process、CANBus 等中设计“一次尝试、恰好一个判定、绝不卡死 connecting”的工程范式。背景为什么“连接判定”需要统一问题来源连接处理的组合式退化Spec 0050 的动机源自 2026-08-10 的一次连接后验post-mortem连接处理是“组合式退化”的——拨号重试、看门狗、DNS 门控、配置编辑自动重开、判定轮询等机制各自订阅了其他机制发出的通知形成了反馈环没有任何单一变更能向评审者展示全貌。由此带来的可测量后果包括TCP telnet 会话每隔几秒被重启直到远程服务把用户 IP 封禁 30 天健康的链路被过期的 peer-validity 读取撕掉重复的 helper 进程争夺同一个端口演示项目需要点击两次连接因为拨号与 helper 的 bind 在竞态。核心缺陷尝试的结局没有单一所有者更深的缺陷是**一次连接尝试的结局没有单一所有者。**同步驱动通过 open 调用的返回值报告异步驱动则通过轮询清扫polling sweep报告成功而失败——在五个驱动里——报告给了“没有人”。一次失败的蓝牙或 Modbus 拨号可能让应用永远停留在 “connecting”。这正是本规格要消灭的 bug 类别“检查 isOpen() 的延迟结算路径”导致 connect 按钮被卡死。而 R1/R2 需求对应的现象是任何总线上连接不可达或拒绝端点都应该在有限、可陈述的时间内产生恰好一个用户可见的失败通知之后应用完全进入断开状态connecting指示器绝不能无限期持续。架构核心openFinished 信号 emit-once 闩锁HAL_Driver 的判定原语T1T1 的任务是在app/src/IO/HAL_Driver.h当前仓库实际路径为core/Core/IO/HAL_Driver.h中引入判定原语。当前源码中这组 API 已经落地与任务描述完全一致新增信号void openFinished(bool ok, const QString reason);公开的闩锁操作armOpenReport()、disarmOpenReport()、openReportArmed()受保护的报告入口reportOpenFinished(bool ok, const QString reason QString())reportOpenFinished的核心逻辑core/Core/IO/HAL_Driver.h第 292-299 行体现了“恰好一次”的强制力void reportOpenFinished(bool ok, const QString reason QString()) { if (!m_openReportArmed) return; m_openReportArmed false; Q_EMIT openFinished(ok, reason); }这是一个纯 bool 闩锁没有任何定时器符合规格中 “No timer may act on connection state” 的不变量。它保证只有闩锁被armOpenReport()武装后才允许发射第一次发射后自动解除武装因此后续已建立链路的掉线事件永远不会伪装成拨号判定emit-exactly-once由基类强制而不是依赖各驱动的自律——plan.md 明确说明“per-driver discipline is exactly what stranded verdicts before”正是各驱动的自律导致此前判定被搁浅。ConnectionManager 的消费路径T2core/Devices/IO/ConnectionManager.cpp中connectDevice(int)在调用DeviceManager::open()之前先调用halDriver-armOpenReport()第 831 行并在同步结算或用户取消时disarmOpenReport()第 846、865 行。随后onDriverOpenFinished(bool ok, const QString reason)第 616 行作为唯一消费槽通过既有 sender 反向查找解析出是哪个驱动报告的QObject::sender()将该设备 id 从 pending 集合中移除转发给既有的onDeviceOpenFinished(id, ok, reason)诊断、conclude、notify 一条龙当!ok时静默关闭设备且绝不发射sessionClosed。openFinished到onDriverOpenFinished的接线发生在setBusType()第 1031-1033 行和buildDeviceForSource()第 1235-1237 行均为同线程直接连接默认 Auto 连接在同线程退化为直接调用符合 plan 中 “No new cross-thread signal/slot” 的约定。同时任务明确要求删除settlePendingDialVerdicts()及其在notifyConnectedStateChanged()内的调用。全仓库搜索确认当前代码中settlePendingDialVerdicts仅存在于规格文档中实现树中已彻底移除——这正是 “sweep retirement”轮询清扫退役的最终证据。为什么淘汰轮询清扫在旧设计中notifyConnectedStateChanged()既发布连接状态又兼职“判定清扫”periodically check isOpen() 来结算未决的拨号。这带来两个问题判定产生于轮询而非推送结算延迟不可预测清扫只覆盖了“成功”的观察失败路径在五个驱动中各自为政导致connecting卡死。新设计中refreshConnectedState()queued仍然负责发布连接状态转换但不再兼任判定清扫。plan.md 原文“it publishes connected-state transitions; it just no longer doubles as the verdict sweep.”各异步驱动的报告改造T3-T7T3 — BluetoothLE双向报告core/Devices/IO/Drivers/BluetoothLE.cppannounceGattReady()→reportOpenFinished(true)第 546 行onControllerError()→reportOpenFinished(false, reason)第 377 行删除临时补救ConnectionManager::disconnectDevice(this)——manager 的统一 teardown 现在处理失败的拨号。源码中还有两个额外的失败报告点第 339 行“connected 完成前设备断开”、第 935 行“BLE service error during connect”说明所有 controller 失败路径都会汇入onControllerError或 pre-readydisconnected → close路径。这正是 T3 验证步骤“read-back confirms every controller failure path funnels through onControllerError”的实现证据。T4 — Modbus状态机双出口core/Devices/IO/Drivers/Modbus.cpponStateChanged(ConnectedState)→reportOpenFinished(true)第 1191 行failDial()→reportOpenFinished(false, error)第 484 行。Queued-box 规则保留failDial 的 box 已经排队。T5 — MQTT拨号窗口内失败收敛core/Devices/IO/Drivers/MQTT.cpponStateChanged(Connected)→reportOpenFinished(true)第 1135 行拨号窗口内的onErrorChanged()→reportOpenFinished(false, reason)第 146 行替换原临时补救的 disconnect 块同时保留m_userWantsOpen/m_reconnectPending的清理。第 1140、1231 行还覆盖了“broker 在尝试期间关闭连接”等路径。T6 — Process两种模式都报告core/Devices/IO/Drivers/Process.cppLaunch 模式QProcess::started→ report(true)FailedToStart或仍在 Starting 时退出 → report(false, reason)Pipe 模式marshal 后的 peer-attach 槽 → report(true)!m_pipeConnected时的管道错误 → report(false, reason)。线程不变量报告只从主线程槽触发管道事件已经 marshal绝不在管道线程上调用 report。T7 — CANBus插件异步拨号时报告core/Devices/IO/Drivers/CANBus.cpp第 960-964 行onStateChanged中闩锁武装时 Connected →reportOpenFinished(true)武装时 Unconnected →reportOpenFinished(false, errorString)。gs_usb 路径是同步的从不报告open 返回结算后闩锁已被 ConnectionManager 清除。注除上述任务列出的驱动外当前源码中Iec104.cpp、Network.cpp、OpcUa.cpp同样接入了reportOpenFinished。它们属于后续规格如 0066/0067/0075对同一判定范式的扩展——doc/claude/architecture/io.md中 “The verdict has ONE owner per attempt (spec 0050)” 一节将此描述为设计被持续复用的证据。同值守卫UART setPortIndex 与 driver-setter-guard lintT8-T9背景为什么 setter 需要同值守卫AC5R5幂等设置应用要求重新应用与当前值相同的值必须是完整 no-op——无 DNS 查找、无 undo 历史条目、无 autosave、无通知级联。因为applyConnectionSettings()HAL_Driver.h第 268-279 行会重放项目持久化的每个 key若 setter 在值未变化时仍然发射configurationChanged就会引发重连/autosave 的涟漪。T8 — UART 的修复core/Devices/IO/Drivers/UART.cpp第 581-596 行的setPortIndex(const quint8 portIndex)当 clamp 后的索引等于m_portIndex且持久化名称已经是最新时提前返回使重复应用无法再发射。关键设计是自动重连的setPortIndex connectDevice()流程不受影响——显式 connect 无论 emit 是否被跳过都会继续执行。if (portIndex m_portIndex clamped m_portIndex) // early return同值重应用不发射 configurationChanged return; m_portIndex clamped;T9 — driver-setter-guard lint 规则AC8 要求仓库 linter 强制所有驱动配置 setter 具备同值守卫且全树检查通过。规则要点错误级别error severity作用域限定app/src/IO/Drivers/*.cpp具体set*(scalar|QString)方法必须包含同值提前返回或isOpen()门setDriverProperty分发 override 豁免它扇出到具体 setter具体 setter 才是被守卫的表面实现位于scripts/code_verify_rules.pyC 规则宿主scripts/code-verify.py并重新生成.code-report验证方式python scripts/code-verify.py --check全树干净故意破坏一个守卫以观察规则触发然后恢复。plan.md 解释了为何是错误级别而非 advisory“the missing guard was the telehack loops engine”——缺失守卫正是之前 IP 被封循环的引擎advisory 级别的基线债务会让下一个驱动带着同样的 bug 上船。Live Text Apply驱动面板文本实时生效T10设计动机规格中的 “edit-lock”R3连接期间锁定连接配置控件与“reopen 机制已删除”2026-08-10使旧有的防御失效驱动文本字段此前只在editingFinishedEnter 或失焦时提交这是 reopen 时代的“护甲”——那时每次击键都会重启 DNS 并重拨链路。如今reopen 机制已删除连接期间设置被 UI 锁定每次击键的 hostname DNS 查找现在是惰性的lookup 下游没有任何东西能触碰连接无需防抖。实现要点文本字段改为在onTextEdited上提交仅用户输入触发onTextChanged会与每个面板Connectionshandler 的程序化 write-back 形成循环——这个不对称性就是绑定不变量。保留editingFinished作为空字段默认地址场景的回退。涉及面板grep 确认app/qml/MainWindow/Panes/SetupPanes/Drivers/Network.qml地址以及 Modbus host、MQTT hostname/topic、Process executable/arguments、UART custom device 等对等面板。判定集成测试T11任务要求新建tests/integration/test_connection_verdicts.py当前仓库中该文件已存在tests/integration/test_connection_verdicts.py覆盖AC1 死端口判定Network TCP Modbus TCP 拨号关闭的本地端口 → 状态在 ≤6 s 内结算为 disconnectedlinkState绝不卡在connecting测试在第 47-55 行有一个专门的poll until linkState leaves connecting辅助函数超出预算即pytest.failAC2 connecting 标志回落Modbus/Process 死端点拨号连接中标志在界内回落AC7 20 次循环每个可脚本化总线Network、Modbus、Process20 次 connect/disconnect 循环最终状态与全新状态相等且单 helper 断言无重复 helper 进程AC5 回归钉10 次相同设置重应用 → 无重连、undo 深度不增长通过project.getStatus/ undo depth 验证。测试基架api_client、clean_state与feed_server提供了运行中的应用上下文。运行方式应用启动后执行pytest tests/integration/test_connection_verdicts.py -v维护者负责拉起应用实例。Definition of Done 与验证闭环任务的完成定义全部已勾选构成完整的质量闭环spec.md中每个验收标准都达成并在该文件勾选AC3/AC2-BLE 保留维护者在舞台硬件上的手工验证python scripts/code-verify.py --check在所有改动文件上干净包括新的driver-setter-guard规则全树通过qt-cpp-review对 C diff 运行发现项已处理或记录热路径未触碰plan 中明确 “Touches the hotpath? No”无FrameReader/CircularBuffer/FrameBuilder/Dashboard-draw 代码被读写无基准回归预期pytest tests/integration/test_connection_verdicts.py列入维护者清单python scripts/sanitize-commit.py运行工作树无 lint 债务diff 是“被要求的内容且仅此而已”——无范围蔓延、无无关文件spec.md状态置为done。架构落点与扩展从 io.md 看统一判定范式的全貌doc/claude/architecture/io.md的 “The verdict has ONE owner per attempt (spec 0050)” 一节给出了该设计的精确定义可作为权威总结同步驱动open()返回值由connectDevice(int)传给onDeviceOpenFinished(deviceId, ok, reason)。异步驱动BLE、Modbus、MQTT、Process、异步 CAN 插件HAL_Driver::openFinished(ok, reason)信号每次尝试恰好发射一次——通过基类闩锁manager 在open()前armOpenReport()驱动在两种结局上都reportOpenFinished()在首次报告、同步结算、用户取消时解除武装。ConnectionManager::onDriverOpenFinished结算 pending id失败拨号时静默关闭设备绝不sessionClosed并转发给onDeviceOpenFinished——因此 spec-0035 的诊断自动触发现在也能看到异步失败。不存在轮询清扫settlePendingDialVerdicts()已删除切勿重新引入 “later check isOpen()” 的结算路径。文档同时给出了这个设计与其他 IO 不变量的协同关系sessionClosed语义只有用户或 API 客户端/播放器接管结束一个真实存在的会话时才发射仅来自显式无参disconnectDevice()路径。驱动发起的掉线、被取消的拨号、rebuildDeviceschurn、失败的拨号都不发射它——因为API::ProcessLauncher依赖该信号收割脚本启动的 helperhelper 往往正服务于正在掉线/重试的链路。rebuildDevices()顺序必须在销毁注定失败的驱动前断开其信号现有模式这样迟到的openFinished不可能到达——这是 T2 明确列出的绑定不变量。isConnecting()十一个类 overrideNetwork、Iec104、Modbus、MQTT、OpcUa、S7、EthernetIp、CANBus、BluetoothLE、Process 及测试桩Test::FakeDriver。toggleConnection()在任一设备报告进行中的拨号时中止ConnectionManager::isConnecting驱动工具栏 “Connecting…” 标签。失败仍需到达disconnectDevice(this)已建立链路的掉线路径保留各驱动的disconnectDevice(this)调用BLEonControllerError、ModbusfailDial、MQTT 拨号窗口onErrorChanged使 pending 判定结算、connecting总能回落——但这是掉线路径与拨号失败路径由onDriverOpenFinished统一处理职责分离。工程启示从任务清单反推可迁移的设计原则tasks.md的约定部分本身即是工程方法论一个任务 一个聚焦、可评审的变更触碰 3 文件或需一整段描述就拆分每个任务有独立的Verify通常python scripts/code-verify.py --check files加一个测试或回读和Deps前置依赖顺序保证每步之后概念上可编译。T1→T9 的依赖链T2 依赖 T1T3-T7 依赖 T2T9 依赖 T8展示了如何在共享基类上先立原语、再逐驱动迁移、最后用 lint 守住边界。对任何 Qt 多总线应用这套方案可迁移的关键决策是判定推送化用基类闩锁信号取代轮询清扫从机制上消除“判定搁浅”单一消费者所有驱动只报告teardown 由 manager 集中执行删除 N 份重复的 teardown 排序代码与误导性的 “device dropped” 日志幂等 setter lint 强制同值 no-op 通过自动化规则固化为架构约束而不是靠 code review 提醒测试钉死回归AC1/AC2/AC5/AC7 以 pytest 形式常驻任何驱动改动若重新引入卡死connecting或重复 helper都会在 CI 中立即暴露。相关阅读与验证入口规格三件套spec.mdWHAT/WHY、plan.mdHOW、tasks.md有序清单架构权威描述io.md 的 Verdict/Opening a Link 章节基类实现HAL_Driver.h第 206-212 行信号、第 292-299 行reportOpenFinished管理器实现ConnectionManager.cppconnectDevice/onDriverOpenFinished/接线点驱动示例BluetoothLE.cpp、Modbus.cpp、MQTT.cpp、Process.cpp、CANBus.cpp、UART.cpp集成测试test_connection_verdicts.py静态验证python scripts/code-verify.py --check与规则宿主scripts/code_verify_rules.py【免费下载链接】Serial-StudioOpen-source telemetry dashboard. Supports UART, BLE, MQTT, Modbus, CAN Bus and more.项目地址: https://gitcode.com/GitHub_Trending/se/Serial-Studio创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考
返回列表