ModLink Studio
ModLink Studio 是我现在最核心的项目。它一开始就不是朝着“给某一种设备写一个单独上位机”去做的,而是想把设备搜索、连接、流描述、实时预览、采集控制和录制保存这些共性能力整理到同一套宿主 runtime 里。这样以后接新设备,新增的重点应该是 driver,而不是再复制一套新的应用骨架。
这个项目真正花时间的地方,也不是表面上看到的那些界面,而是把几条边界慢慢理顺:哪些东西应该留在 runtime,哪些东西应该属于 UI,哪些东西应该交给插件分发层处理。
0.1.0 到 0.2.0 的变化,基本也都围绕这件事展开。前一版里,很多运行时语义还和 Qt 绑得比较紧,发布方式也更像探索期结构;到了 0.2.0,重点就不再是继续往上叠功能,而是先把项目中心重新放回 runtime。
GitHub Organization: modlink-studio
主仓库: modlink-studio/ModLink-Studio
插件仓库: modlink-studio/ModLink-Studio-Plugins
现在的仓库结构(按职责)
项目现在不是单仓库结构,而是拆成了几条比较清楚的线:
modlink-studio/
apps/
modlink_studio/ # Qt Widgets 宿主入口
modlink_studio_qml/ # QML 宿主入口
modlink_server/ # FastAPI 服务宿主入口
packages/
modlink_sdk/ # driver 最小契约
modlink_core/ # 纯 Python runtime
modlink_qt_bridge/ # Qt 适配层
modlink_plugin_manager/ # 插件安装与管理 CLI
modlink_ui_qt_widgets/ # Widgets UI
modlink_ui_qt_qml/ # QML UI
tools/
modlink_plugin_scaffold/ # driver 脚手架
modlink-studio-plugins/
plugins/
index.json # 插件索引
host-camera/
host-microphone/
openbci-ganglion/
这个结构本身就对应了我后来比较明确的一种分法:
sdk/core负责运行时和接入边界bridge负责 UI 适配apps/ui负责宿主表现plugins仓库负责官方插件源码和发布资产
第一部分:先把 runtime 从 UI 里拆出来
0.1.0 时,项目虽然已经有了设备接入和界面,但很多系统语义仍然带着比较重的 Qt 影子。这样做的好处是早期推进很快,问题是后面只要界面层开始变化,backend 也会被一起拖着动。
所以到了 0.2.0,最重要的一步其实不是“多做一个新 UI”,而是先把 sdk 和 core 从 Qt 运行时语义里拆开,转成纯 Python runtime。
这一步完成之后,项目的中心就不再是某个窗口,而是统一的运行时模型。设备搜索、连接、流描述、采集、录制这些事情,都开始围绕同一套 runtime 组织,而不是散在各个界面里各自承担。
这样调整之后,Qt Widgets 还是可以继续存在,QML 也可以并行推进,后面如果要走服务化 host 或 Web UI,也不会要求把 backend 再重写一遍。
第二部分:把 driver 的异步复杂度收进 core
这个项目里,driver 开发体验是一个很核心的问题。
如果接入一个新设备时,driver 作者一开始就要先去理解 Future、回调完成时机、线程切换、生命周期收口和错误传播链路,那实际写下来很容易变成“先适应宿主框架,再处理设备本身”。
所以在现在这版结构里,driver 侧尽量保持同步接口。搜索、连接、开始采集、停止采集这些操作,从 driver 的视角看,仍然是比较直接的控制流程;而执行线程、Future、任务状态和异常传播,则留在 core 的 portal / executor 这一层处理。
这样分下来,driver 作者更接近在写设备逻辑本身,宿主则可以继续用自己的方式组织任务完成、状态更新和界面响应。
换句话说,异步并没有被拿掉,只是被尽量留在 runtime 内部。
第三部分:把线程边界先做稳
只把异步执行收进 core 还不够,后面很快就会碰到另一个更麻烦的问题:线程。
很多设备 driver 在现实里就是会碰到 worker thread、loop thread 甚至额外 helper thread。如果底层共享结构本身不稳,那么插件作者写起来就会一直围着“这个对象到底能不能跨线程碰”打转。
所以 state、stream bus、settings 这些基础能力,在 core 里都尽量按线程安全结构整理。这里的目标不是把线程模型做得多花,而是先把那些一定会被多个部分碰到的共享边界做稳。
这样之后,driver 侧即使需要自己的线程,也不至于一上来就先撞上一堆隐含前提。
第四部分:UI 不再承载系统语义
0.2.0 还有一个对我来说很重要的变化,就是把 UI 从“系统本体”这个位置上拿下来。
现在的理解更接近这样:
- runtime 负责系统语义
- bridge 负责适配
- UI 负责消费这些能力
qt-bridge 在这里的位置就很明确。它要做的是把 backend 的状态、事件和任务结果适配成 Qt 主线程可以消费的形式,而不是在 bridge 里再长出一套新的后端语义。
这件事看起来只是分层,但实际影响很大。因为一旦 bridge 的职责明确下来,Qt Widgets、QML,甚至后面可能出现的 HTML / Web 前端,都可以建立在同一套 runtime 上。
所以这个项目越来越不像一个“UI 项目”,而更像一个“runtime + 多宿主”的项目。
第五部分:发布方式的变化,其实也是边界设计
这部分在代码外面,但我觉得它同样重要。
0.1.0 的时候,项目托管在 Cloudsmith,上层包结构也更分散。那种方式在探索期不是不能用,但随着边界不断调整,它会把内部结构的不稳定直接暴露给最终用户。
到了 0.2.0,主能力先收口为一个 PyPI 主包 modlink-studio。这样做的原因很简单:在内部边界还没有完全钉死的时候,公开安装入口最好先稳定下来。否则后面只要再改一次边界,用户就要跟着理解包名变化、安装路径变化和职责变化。
插件这条线则没有继续走“逐个发布到 PyPI”的方案,而是拆成了另一层:
- 插件元数据放在文档站提供的
JSON manifest - 插件 wheel 放在 GitHub Releases
- 安装通过
modlink-plugin工具统一处理
现在的结构看上去不是最传统的分发方式,但它更适合当前这个阶段。主包和插件资产可以分开演进,兼容范围可以单独写在 manifest 里,插件仓库也可以独立更新,不必每次都和主仓库发布强绑定。
这一轮调整之后
整理完这些边界之后,项目的几个方向都比之前清楚很多:
- driver 更接近设备逻辑本身
- runtime 开始成为真正的中心
- UI 更像宿主,而不是业务本体
- 插件分发从主包发布里独立出来
这些变化未必都是最显眼的部分,但它们基本决定了这个项目后面还能不能继续长下去,以及接新设备、换宿主、调发布策略时会不会越来越重。