Serial Studio 脚本宏(Macros)技术设计全解析:在 API 终端内运行 JS/Lua 自动化脚本
2026/9/17 19:49:07 网站建设 项目流程

Serial Studio 脚本宏(Macros)技术设计全解析:在 API 终端内运行 JS/Lua 自动化脚本

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

本文基于 Serial Studio 仓库内的规格文档 0046-script-macros/plan.md(配套 spec.md 与 tasks.md)展开。该特性为 API 终端(Macros 窗口)引入"迷你 VBA"式脚本能力:用户可在内置代码编辑器中编写多行 JavaScript 或 Lua 宏,通过工具栏一键加载、保存、校验、执行与停止,脚本可调用与终端单条命令完全相同的完整 SDK/apiCall命令面,且输出与错误实时汇入共享终端回滚区。读完本文,你将掌握该特性的需求来源、整体架构、JS/Lua 双执行路径、中断与看门狗机制、文件持久化约定以及验收标准,并能对照仓库源码定位每一处关键实现。

一、背景与动机:为什么需要"脚本宏"

该特性的直接前身是规格 0045 的 API 终端。0045 验证了"命令发现 + 执行"的闭环,但首次实机使用便暴露了它的天花板:

  • 单行输入对真实命令无能为力——例如workspace.addTile自动填充的 JSON 骨架比窗口还宽;
  • 每次 Enter 只能执行一条命令,无法表达任何用户逻辑(遍历数据集、检查响应、分支、组合调用);
  • 应用虽然早已内嵌 JS 与 Lua SDK(apiCall()及完整 SDK 被安装进每个脚本引擎),但用户只能在绑定项目生命周期的地方(帧解析器、数据变换、控制脚本、MQTT 发布器)触达它,缺少一种针对运行中实例编写并执行临时自动化的途径——也就是文档中所说的 "the VBA of Serial Studio"。

在此之前,自动化用户只能通过 TCP/gRPC 在进程外写脚本,每次调用都要付出连接建立、序列化和往返延迟的成本;而进程内宏可以在零额外开销的情况下使用同一命令面(见 spec.md 的 Problem/Motivation 一节)。

二、需求全景:Goals、Non-Goals 与 R1–R9

Goals(目标)

  1. 用户在 API 终端窗口内的真实代码编辑器中编写多行 JS/Lua 脚本(语法高亮、等宽字体),且可用完整 SDK/apiCall命令面;
  2. 工具栏驱动宏的完整生命周期:加载、保存、校验、执行、清空——脚本可在磁盘上往返,且不运行即可检查;
  3. 脚本输出(结果、print/console.log、带行号的错误)与单命令输出一同进入终端回滚区;
  4. 失控脚本(死循环)可中断,绝不能永久卡死应用;
  5. 单命令输入行行为完全不变——宏是新增能力,不是替代品。

Non-Goals(明确不做)

  • 不做 QML 生成插件层(作为未来扩展预留,但本设计不得阻碍它,也不在此交付 QML 执行能力);
  • 不做后台/定时/开机自启执行——宏只在用户按下执行时运行;
  • 不新增任何 API 命令,不改变线上(wire)接口;
  • 不替代项目级控制脚本、帧解析器、变换、MQTT 发布器脚本——宏是用户级、临时性的;
  • 不做宏市场/分享格式,仅保留磁盘上的纯脚本文件。

需求 R1–R9 要点

编号内容
R1终端窗口获得脚本编辑区(标签页或面板),承载带 JS 语法高亮的既有内嵌编辑器组件,语言可在 JS/Lua 间切换
R2脚本以既有引擎相同的 SDK/apiCall环境运行:所有注册命令可达、响应形状一致、Trusted 来源,分发层与 0045 单命令路径语义一致
R3工具栏动作:加载(文件对话框,纯.js/.lua)、保存/另存为、校验(不执行仅解析,错误带行号)、执行、清空;执行中禁用 Execute
R4脚本输出进入终端回滚区:显式 print/log 调用、脚本最终值(若有)、带行号的运行时错误
R5运行中的宏可中断:UI 提供停止控件,且已有看门狗纪律约束挂起宏;中断/崩溃的宏绝不能让宿主崩溃或卡死
R6宏与终端单命令一样针对实时应用状态执行(同一事实线程、相同破坏性动词对等——宏只能做 socket API 能做的事)
R7未保存的编辑器内容在同一会话内关闭/重开窗口后仍保留;加载/清空前提示丢弃未保存改动
R8与 0045 相同的构建可用性:GPL 构建中同样存在且可用
R90045 表面(发现面板、文档、补全、单行输入、历史)保持完整、行为不变

三、整体架构:从计划到落地

plan.md 给出的 Approach 一句话概括为:API 终端的右侧面板变成"Terminal / Script"双标签栈。Script 标签页托管一个新的、与项目无关的内嵌代码编辑器DataModel::MacroEditorControlScriptEditor的兄弟组件,去掉 ProjectModel 耦合,带 JS/Lua 高亮切换),其上是小型窗口内工具栏(语言下拉 + 加载/保存/校验/运行/停止/清空)。执行则经由一个新的、可被 QML 实例化的DataModel::MacroRunner门面完成。

落地后的文件布局与 plan.md 的规划一致,只是实现时按维护者指示做了一次重命名(见 tasks.md 的 T5/T8 记录):窗口文件最终为 app/qml/Dialogs/Macros.qml,命令为app.macros,原先的app.apiTerminal命令与图标被移除,0045 的终端表面以该窗口的 Terminal 标签页形式延续。

三个新 C++ 类的实际落点如下:

实际位置职责
DataModel::MacroWorkercore/Pipeline/DataModel/Scripting/MacroWorker.h /.cpp工作线程 JS 运行器
DataModel::MacroRunnercore/Pipeline/DataModel/Scripting/MacroRunner.h /.cppQML 可实例化门面:线程所有者、Lua 路径、校验、文件对话框
DataModel::MacroEditorcore/Ui/ProjectEditor/Editors/MacroEditor.h /.cpp项目无关的内嵌代码编辑器

这三个类通过 app/src/Misc/ModuleManager.cpp 的registerQmlTypes()qmlRegisterType<DataModel::MacroEditor>("SerialStudio", 1, 0, "MacroEditor")qmlRegisterType<DataModel::MacroRunner>(...)注册为 QML 类型(非单例,采用 TerminalBridge 模式)。plan.md 特别强调:除此之外再无其他改动——无 API handler、无 manifest、无生成面、无注册表条目(工具栏是窗口局部 QML,不属于 0028 规范的命令表面)。

四、双执行路径:JS 走工作线程,Lua v1 走 GUI 同步

4.1 Execute(JS):工作线程 + 每次全新引擎

QML 中的 Run 按钮 →MacroRunner.runJs(text)→ 通过队列连接(queued connection)把run(source)投递到工作线程 → 在全新的QJSEngine中执行(注入 SDK prelude 与ControlApiBridge__ss_bridge)→ 顶层求值 →finished/scriptError/logMessage再经队列信号回传给 QML → 追加进回滚区。

关键实现细节(MacroWorker.h):

  • 每个apiCall通过BlockingQueuedConnection编组到 GUI 线程,进入ControlApiMarshaller::dispatch——与应用内其他一切路径共用同一条分发主干;
  • print/console.log经由桥的 log 路径(prelude 映射)路由,因此宏运行期间输出就能持续流入回滚区
  • JsWatchdog在整个运行期间武装。

plan.md 还特意指出工作线程引擎的桥接纪律:worker 引擎只能拿到__ss_bridge。把直接辅助桥(__ss__ss_db……)安装到工作线程引擎上是 scripting.md 中具名的线程缺陷,这里绝不发生。

4.2 Execute(Lua v1):GUI 同步 + MASKCOUNT 截止时间

MacroRunner.runLua(text)在 GUI 线程同步执行:新建lua_State,调用ScriptApiCall::installAll(GUI 线程上安装直接桥是正确的),并挂上LUA_MASKCOUNT+QDeadlineTimer钩子,以 30 秒的宽松预算作为防挂起边界。v1 文档明确将其记为阻塞式执行。

4.3 Stop:仅对 JS 宏有效

  • Stop 仅在 JS 宏运行时启用;
  • MacroRunner.stop()JsWatchdog立即截止时间武装 →JsWatchdogThread(20 ms 轮询)调用setInterrupted(true)——这是唯一允许调用该方法的文件
  • 错误以中断消息形式呈现在回滚区。

这里的核心差异在于:ControlApiBridge只响应进程级关闭标志,因此 Stop 无法打断一个长delay();而宏专用的MacroApiBridge(MacroWorker.h)是**停止感知(stop-aware)**的变体——它的delay()call()writeAndWait()都会检查停止标志,这正是"VBA 式"宏(含延时、设备往返等待)在不冻结应用的前提下能被 Stop 打断的关键。

4.4 Verify:只编译、不执行

校验采用controlscript.dryRun模型的"一次性引擎、仅编译"模式:

  • JS:在函数包裹下解析,或直接检查QJSEngine::evaluate的错误(带行号);
  • LualuaL_loadstring(只加载,绝不调用)。

规格已决议 Verify 为纯解析(parse-only),无任何副作用。

4.5 每次执行全新引擎

MacroWorker在每次运行后释放引擎(releaseEngine(),见 MacroWorker.h 声明)。崩溃或中断的运行不会留下任何状态,保证结果可复现。

五、编辑器实现:三个不变量与项目解耦

MacroEditorControlScriptEditor的兄弟组件,离屏QCodeEditor管道逐字复制,但彻底去掉了 ProjectModel/ProjectEditor 成员(宏与项目无关)。它提供纯text属性(get/set)、language属性(0 = JS,1 = Lua)切换高亮器、isModified/setModified,以及标准编辑槽(cut/copy/paste/undo/redo/selectAll/clear)。

plan.md 与 tasks.md(T3)强调必须保留宿主管道的三个已知受保护不变量

  1. renderWidget()首先调用syncWidgetPosition()(先同步位置再渲染);
  2. event()ShortcutOverride转发给内嵌控件(因此无需在窗口层新增Shortcut,规避与 SmartDialog 已拥有的 Close 产生歧义);
  3. keyPressEvent在补全弹窗可见时重路由到completer()->popup()

此外,输入处理器中直接调用renderWidget()(不能有 timer-tick 延迟)。

六、数据模型与持久化:纯文件,无项目 JSON

plan.md 的 Data model & persistence 一节非常明确:

  • 无项目 JSON、无Keys::、无数据库
  • 宏文件是磁盘上的纯.js/.lua文件;
  • 默认目录为<AppDataLocation>/Macros/(首字母大写,与兄弟应用数据目录命名一致,首次使用时创建);文件对话框允许自由导航;
  • 未保存的编辑器文本会话内保留(QML 属性缓存),加载/清空前提示(R7)。

任务实施时进一步细化(见 tasks.md T6):由于 DialogLoader 在关闭时销毁条目,草稿实际存于app.macroDraft/app.macroDraftLanguage(main.qml),由窗口的onClosing写入、Component.onCompleted恢复。

文件对话框采用 C++QFileDialog(与既有编辑器importFile()一致),并遵守 macOS 重入规则:所有对话框后置工作通过 queued invoke 延迟出fileSelected

七、线程与热路径影响

plan.md 的专项评估结论:

  • 不触碰热路径:所有引擎工作均由用户发起,无逐帧工作,空闲的 Script 标签页零开销;宏的apiCall与终端/控制脚本一样落在 GUI 线程,不新增帧路径接触;
  • 新增跨线程信号/槽:是,且被严格控制——QML→worker 的run(queued)、worker→QML 的finished/scriptError/logMessage(queued)、apiCall编组(BlockingQueuedConnection,复用既有ControlApiMarshaller模式而非重新实现);
  • 线程加入规则MacroRunner析构函数中quit()+wait()加入工作线程,再释放 worker 资源(driver thread-join 规则),并镜像 ControlScriptWorker 的requestShutdown式关闭守卫;
  • 无缓存热路径标志的新输入,时间戳所有权不动;--benchmark-hotpath预期无变化(由 CI 门 AC10 确认)。

实际头文件印证了这一点:MacroRunner.h 中持有QThread* m_threadMacroWorker* m_workerControlApiMarshaller* m_marshaller,并具名删除了拷贝/移动构造(= delete)。

八、设计权衡:为什么这样选

plan.md 用一张决策表总结了六项关键取舍:

决策点备选方案选择及理由
JS 执行线程工作线程(MacroWorker) vs GUI 同步工作线程——GUI 事件循环被阻塞时 Stop 无从谈起;"VBA"式宏(延时、设备往返)绝不能冻结应用;marshaller/桥已存在
Lua v1 线程GUI 同步 + MASKCOUNT 截止 vs 工作线程 + 新 Lua 编组GUI 同步——Lua 工作线程需要在强制展开(forced-unwind)表面铺设新的离线程桥接管道;规格决议 Lua v1 为有界阻塞
Worker 复用新建MacroWorker兄弟 vs 复用ControlScriptWorker兄弟——控制脚本按连接强制重启的生命周期与 setup()/loop() 拆分与一次性宏相抵触;可共享部分(marshaller、桥、看门狗)本就是类而非 worker
编辑器新建MacroEditorvs 复用ControlScriptEditor新兄弟——ControlScriptEditor 读写 ProjectModel,宏与项目无关;管道复制、耦合剔除、高亮器可切换
输出路由共享回滚区 + 自动切标签 vs 每标签独立输出视图共享回滚区(规格 R4)——命令与宏共用一个输出时间线;自动切换保证可见性且不复制视图
加载/保存对话框MacroRunner 内 C++ QFileDialog vs QML FileDialogC++——与既有编辑器importFile()一致,macOSfileSelected延迟逻辑集中在单一受审地点

九、风险与缓解

plan.md 明确列出的风险清单及对策:

  • 离线程桥误用(scripting.md 具名线程缺陷):worker 引擎只装__ss_bridge,任务中设审查检查点;
  • 看门狗 lintsetInterrupted(true)只保留在JsWatchdogThread.cpp,停止被表达为立即截止时间的arm()
  • 编辑器管道回归:三个不变量从ControlScriptEditor逐字复制,任务在编辑期点名它们;
  • 阻塞队列死锁(worker 阻塞于 GUI 而 GUI 等待 worker):GUI 线程绝不等待 worker;停止是异步的,join 仅发生在析构(并带requestShutdown式守卫用于应用关闭);
  • 宏关闭自身窗口:worker 只存活到MacroRunner析构 join;信号为队列投递,迟到信号命中已销毁接收者时由 auto-disconnect 安全处理;
  • Lua GUI 冻结:由 MASKCOUNT 截止时间限制并文档化;JS 是推荐语言且为默认项;
  • macOS 文件对话框重入:所有对话框后置工作经 queued invoke 延迟。

T9 的 6 代理审查还修复了若干细节:堆上 QThread + warn-and-abandon、7 秒 join 预算(大于 5 秒看门狗)、闩锁式 teardown 标志、GUI 侧停止复位、停止门控的看门狗重武装;Lua try 块加宽以涵盖__tostring/setup 恐慌;保存改用QSaveFileverify()设为 const;SerialStudio::ScriptLanguage锚定等。

十、测试与验收:AC1–AC10

plan.md 的测试计划将验证分为四层:

  • 单元测试:无——tests/scripts/解析器无改动;
  • 集成测试(维护者运行):无新增——既有 API 集成套件仍是分发路径之网;
  • 应用内(维护者):AC1(JS 宏循环 + print)、AC2(Lua 变体)、AC3(校验语法错误 + 行号、拒绝运行)、AC4(保存/清空/加载往返)、AC5(while(true){}经 Stop/看门狗停止、应用此后健康)、AC6(抛异常宏,后续运行正常)、AC7(0045 回归)、AC8(GPL 构建);
  • 静态检查(AC9)code-verify.py --check覆盖所有新/改文件;registry-verify.py(应为 no-op——无注册表面);sanitize 管道;交接前对新 C++ 做qt-cpp-review
  • 热路径(AC10):CI--benchmark-hotpath门禁,预期无变化。

tasks.md 的 T1–T9 将上述拆为可独立验证的顺序任务,且全部标记完成,spec.md 状态已置为done

十一、实战示例:窗口内置的 Hello World 宏

打开 Macros 窗口后,编辑器会按当前语言自动填充一个"Hello world"起始宏(定义于 app/qml/Dialogs/Macros.qml)。JS 版本如下,它演示了宏的核心使用模式:

// 调用 api.getCommands const reply = apiCall("api.getCommands") if (!reply.ok) throw new Error(reply.error) // 获取命令列表 const commands = reply.result.commands console.log("Hello from Serial Studio! " + commands.length + " commands available.\n") // 打印前几条命令 for (let i = 0; i < Math.min(5, commands.length); ++i) { const cmd = commands[i] console.log(" " + cmd.name + " - " + cmd.description + "\n") }

对应的 Lua 版本结构完全平行:

local reply = apiCall("api.getCommands") if not reply.ok then error(reply.error) end local commands = reply.result.commands print("Hello from Serial Studio! " .. #commands .. " commands available.\n") for i = 1, math.min(5, #commands) do local cmd = commands[i] print(" " .. cmd.name .. " - " .. cmd.description .. "\n") end

两个版本都展示了三条要点:所有 API 命令经apiCall(method, params)可达;响应永远是{ ok: true, result: ... }{ ok: false, error: "..." }的统一对象/表;SDK 包装器已预加载,因此api.getCommands()等价于上面的apiCall。点击播放按钮后,输出会显示在 Terminal 标签页中(Execute 自动切换标签,且先回显一行> [macro] run (js))。

十二、总结

0046 脚本宏特性把 Serial Studio 从"逐条命令的终端"升级为"可编程的自动化工作台":JS 宏经工作线程运行、每次全新引擎、停止感知的MacroApiBridge保证可中断性;Lua v1 以 GUI 同步 + MASKCOUNT 截止时间提供有界阻塞的执行;Verify 只编译不执行;输出统一汇入共享回滚区。整个过程复用既有 marshaller/看门狗/编辑器管道,不新增任何 API 命令,也不触碰热路径——用 plan.md 的话说,是"以既有脚本引擎纪律复用为前提,不引入新的引擎架构"。

如果你想深入源码,推荐按此顺序阅读:先看 plan.md 与 spec.md 建立全貌,再读 MacroWorker.h(停止感知桥与 worker 生命周期)、MacroRunner.h(门面与信号面)、MacroEditor.h(编辑器不变量),最后对照 Macros.qml 查看 QML 侧的标签页、工具栏与草稿缓存接线。

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

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

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

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

立即咨询