← 返回项目 ModLink Studio

ModLink Studio


主要项目
Python架构Runtime插件生态

ModLink Studio 是我现在最核心的项目。它一开始就不是朝着“给某一种设备写一个单独上位机”去做的,而是想把设备搜索、连接、流描述、实时预览、采集控制和录制保存这些共性能力整理到同一套宿主 runtime 里。这样以后接新设备,新增的重点应该是 driver,而不是再复制一套新的应用骨架。

这个项目真正花时间的地方,也不是表面上看到的那些界面,而是把几条边界慢慢理顺:哪些东西应该留在 runtime,哪些东西应该属于 UI,哪些东西应该交给插件分发层处理。

0.1.00.2.0 的变化,基本也都围绕这件事展开。前一版里,很多运行时语义还和 Qt 绑得比较紧,发布方式也更像探索期结构;到了 0.2.0,重点就不再是继续往上叠功能,而是先把项目中心重新放回 runtime。

GitHub Organization: modlink-studio

主仓库: modlink-studio/ModLink-Studio

插件仓库: modlink-studio/ModLink-Studio-Plugins

文档站: modlink-studio.github.io

现在的仓库结构(按职责)

项目现在不是单仓库结构,而是拆成了几条比较清楚的线:

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”,而是先把 sdkcore 从 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。如果底层共享结构本身不稳,那么插件作者写起来就会一直围着“这个对象到底能不能跨线程碰”打转。

所以 statestream bussettings 这些基础能力,在 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 更像宿主,而不是业务本体
  • 插件分发从主包发布里独立出来

这些变化未必都是最显眼的部分,但它们基本决定了这个项目后面还能不能继续长下去,以及接新设备、换宿主、调发布策略时会不会越来越重。