Skip to main content

SerialForge

写给那些只想在 Qt 界面上点个按钮读写串口,却被多线程、分包断句、乱码和界面卡死折磨到怀疑人生的开发者。

开发上位机串口程序,大概是每个写 GUI 的人迟早都要趟一遍的浑水。

如果用原生方案写,你的代码通常会长成这样:开一个 QThread,在死循环里用 pyserial.read() 啃字节;因为串口本质上是流水,一口气读出来的 50 个字节可能前半截是上一条回复的尾巴,中间混着设备被踢掉线时的乱码,后半截才是刚刚发出的回包;你得手写状态机拼接 \r\n,再把文本拆开转成浮点数;如果设备还喜欢复读你的命令(回显),或者冷不丁主动报个警,你还得区分哪个是回复、哪个是上报;最后还要通过自定义的 Qt Signal 辛辛苦苦把数据丢回主线程更新界面。

一旦有人拔掉 USB 线,或者遇上一个打不开的虚拟蓝牙串口,整个程序直接在 open() 里面假死,Windows 标题栏挂上灰色的“未响应”。

SerialForge 就是为了消灭这几百行脏代码而生的。

你只管声明业务逻辑:“发什么文本、按什么正则匹配、帮我转成什么类型”。至于底层的后台 I/O 线程、断句切帧、排队限流、超时兜底、断线重连,以及把解析好的数据直接送到 Qt 主线程闭包,全部由库包办。

uv add serialforge              # 项目里已经有 Qt(PySide6 / PyQt6 / PyQt5),装本体就够
uv add "serialforge[pyside6]"   # 环境里一个 Qt 都没有?让它顺手带一份 PySide6 或者 serialforge[pyqt6] / serialforge[pyqt5]

SerialForge 本体不带 Qt,只借用你环境里已有的 QtCore。所以你原本用哪套绑定,装完 SerialForge 就继续用哪套,不会被悄悄换掉或升级;只有当你手上连 Qt 都没有时,才需要 extra 代劳。


10 秒钟看懂它是怎么工作的

假设你的设备协议很简单:发 PING,回 PONG。

from serialforge import SerialForge

forge = SerialForge()

def read_ping(forge: SerialForge) -> list[str]:
    """扫描、连接、发一条命令,返回设备回复。"""
    PING = forge.add(request="PING", pattern=r"PONG")
    devices = forge.scan_sync(baudrates=[115200], probe=PING)
    if not devices:
        return []
    forge.connect_sync(devices)
    try:
        return forge.send_sync(PING).data
    finally:
        forge.disconnect()

print(read_ping(forge))

纯控制台脚本传 SerialForge(application="core") 即可自动托管事件循环;GUI 程序在已有 QApplication 的主线程直接创建 forge = SerialForge()。

如果是在 Qt GUI 界面里,不要在主线程用 _sync 堵路,换成对应的 _async 方法加闭包回调即可。你的回调函数百分之百在 Qt 主线程里执行,拿到了数据直接调 label.setText(),绝不卡界面。


几个让你省下大把时间的设计

1. 正则匹配 + 自动转类型,告别手撕字符串

很多协议长成 TEMP: 24.8 这样。以前你得拿正则匹配,取出 group(1),再用 float() 强转,还要套 try-except 防炸。

在 SerialForge 里,支持直接绑定数值或 Python @dataclass:

from dataclasses import dataclass
from serialforge import SerialForge

@dataclass
class SensorData:
    channel: int
    voltage: float

def declare_commands(forge: SerialForge):
    # 想要浮点数:正则捕获的第一组数字自动转 float
    GET_TEMP = forge.add("GET_TEMP", r"TEMP:\s*(?P<val>[\d\.]+)", result_type=float)

    # 想要强类型结构体:命名捕获组自动映射到 dataclass 字段
    READ_ADC = forge.add(
        "READ_ADC",
        r"CH(?P<channel>\d+):(?P<voltage>[\d\.]+)V",
        result_type=SensorData,
    )

forge = SerialForge()
declare_commands(forge)

2. 推荐的异步写法:闭包就地改界面,不卡主线程

在桌面上位机里,最怕的就是点击一个按钮后整个界面直接“未响应”。

传统的异步写法往往极其繁杂:在发命令的地方把请求扔出,在别处定义各种自定义 Qt Signal,或者在总体的接收槽函数里用一大长串 if-elif 逐个判断属于哪条指令的回包,还得在类里维护凌乱的字典或全局变量来记住“刚刚点了哪个控件”。一旦业务复杂,改个逻辑要在好几个分散的方法甚至文件之间来回跳跃排查。

SerialForge 推荐使用“异步发送 + 主线程闭包”的模式:

from serialforge import Message, SerialForge

class MainWindow:
    def __init__(self):
        self.forge = SerialForge()
        # 集中声明业务命令(定义一次,多次复用)
        self.GET_TEMP = self.forge.add("GET_TEMP", r"TEMP:\s*(?P<val>[\d\.]+)", result_type=float)

    def on_refresh_clicked(self):
        """点击刷新按钮:请求发起与界面更新完整收拢在这一个函数内"""
        def on_done(message: Message[float]) -> None:
            # ① 回调统一排队回 Qt 主线程,在闭包里直接写操作界面的代码,安全且直观
            self.temp_label.setText(f"{message.data[0]:.1f} ℃")
            self.refresh_btn.setEnabled(True)

        # ② 异步发起请求,后台处理串口 I/O 与解析,绝不卡住主线程
        self.refresh_btn.setEnabled(False)
        self.forge.send_async(self.GET_TEMP).add_done_callback(on_done)

这种写法的优势非常直观:

  • 绝不卡住主线程:所有串口 I/O、切帧定界、超时检测都在后台线程执行,界面操作丝滑不卡顿。
  • 直接在回调里写界面代码:add_done_callback 的闭包统一由 Qt 事件循环调度回主线程运行,拿到了数据直接调用控件方法(如 setText、setEnabled),无需自己手写跨线程信号与槽。
  • 逻辑高内聚,方便封装和调试:从“点击按钮禁用控件”、“异步发送”到“回包后更新界面并恢复控件”,完整的业务逻辑全收拢在同一个函数里面。闭包能顺手捕获控件和当前局部变量,不用在类里记凌乱的中间状态;未来无论是封装新功能还是定位调试,看这一个函数就能看懂全貌。

3. 急事可以插队(URGENT 优先级)

队列里排了 10 条轮询传感器数据的指令,操作员突然按了红色的“紧急停机”按钮。如果按传统的 FIFO 队列慢慢排,等轮到停机命令时黄花菜都凉了。

给紧急命令加个高优先级,它立刻插到队列最前头:

from serialforge import ResponseMode, SerialForge
from serialforge.advanced import CommandPriority

def emergency_stop(forge: SerialForge):
    # 停机指令直接加塞到队列最前头,且不等回复
    STOP = forge.add("STOP", mode=ResponseMode.NO_REPLY)
    forge.send_async(STOP, priority=CommandPriority.URGENT)

4. 知道 USB 身份时,扫描快到飞起

很多人扫描串口很慢,是因为电脑上插了各种蓝牙转串口、虚拟调制解调器,或者驱动稀烂的设备,打开一次就要等一两秒。

如果你知道自己设备的 USB 芯片型号(VID/PID),直接告诉扫描器:

from serialforge import SerialForge, Spec

def fast_scan(forge: SerialForge, ping_probe: Spec[str]):
    # 传入芯片 VID/PID,跳过无关端口,扫描极速完成
    call = forge.scan_async(
        baudrates=[115200],
        probe=ping_probe,
        vid_pid=[(0x1A86, 0x7523)],  # 比如只敲 CH340 的门
    )
    call.add_done_callback(lambda devices: print(f"找到 {len(devices)} 台设备"))

扫描时间直接从 5 秒跌到几十毫秒。

5. 失败不调成功回调

许多库喜欢不管成功失败都调同一个回调,让用户在回调里写一堆 if result.is_error(): return。

SerialForge 的策略很干脆:add_done_callback 注册的闭包只在真正成功收到预期回复时触发。如果超时、设备掉线或者通信出错,成功回调压根不会执行。想看异常细节,去调 call.result(),它会把现场原原本本地重抛给你。


示例代码目录

仓库 examples/ 目录下带了一台内建的虚拟串口设备,不需要任何硬件,敲命令就能跑:

# 简单入门
uv run python examples/simple/first_command.py      # 同步阻塞最小脚本
uv run python examples/simple/async_callback.py     # 异步回调最小脚本

# 进阶特性
uv run python examples/advanced/async_callbacks.py  # 生产推荐写法:异步发现 + 闭包处理 + STREAM
uv run python examples/advanced/auto_connect.py     # 扫描模式与三种连接姿势
uv run python examples/advanced/quick_start.py      # 单发单收、多收、流式、上报、无回复五种模式
uv run python examples/advanced/interactive.py      # 带命令行界面的交互控制台

纯洁与边界

  • 只依赖 QtCore:绝不导入 QtWidgets 或 QtGui。你用它写无界面的后台服务、控制台小工具,还是搭配庞大的桌面 UI,全凭你心意。
  • 导入无副作用:import serialforge 不会擅自起线程、不会开串口、不会动文件系统,更不会弹窗。
  • 支持三套绑定:PySide6、PyQt6、PyQt5 都可以。如果你同时装了多个,设置环境变量 $env:QT_API="pyside6" 指定即可。
  • 环境要求:Windows 10/11 x64,Python 3.11–3.14(64位)。

完整文档

更详细的指南在在线文档站点:https://271374667.github.io/serialforge/

Metadata

Release files for serialforge 1.1.0

For a detailed explanation of source distributions (sdists) and built distributions (wheels), please see the package formats documentation.

Source distribution (sdist)

Source distribution for serialforge 1.1.0
File Size Uploaded Provenance Actions
serialforge-1.1.0.tar.gz 73.9 kB Without provenance Details for serialforge-1.1.0.tar.gz

Built distribution (wheel)

Table of built distributions (wheels) for serialforge 1.1.0
File Interpreter ABI Platform Actions
serialforge-1.1.0-py3-none-any.whl Python 3 none any Details for serialforge-1.1.0-py3-none-any.whl

Total release size: 165.3 kB

Release files / serialforge-1.1.0.tar.gz

Download URL serialforge-1.1.0.tar.gz
Size 73.9 kB
Tags Source
SHA-256 checksum
How to use checksums
561949f837a18d7919c269470461fe1c2107f8004ee7db5fc3b6021ccbef3fe4
BLAKE2b-256 checksum
How to use checksums
3d9577c2f56933bf70cced533e914860cde7969746a64bc8761e765ceb4655c6
Upload date
Uploaded using Trusted Publishing?
What is trusted publishing?
No
Uploaded via uv/0.13.5 {"installer":{"name":"uv","version":"0.13.5","subcommand":["publish"]},"python":null,"implementation":{"name":null,"version":null},"distro":null,"system":{"name":null,"release":null},"cpu":null,"openssl_version":null,"setuptools_version":null,"rustc_version":null,"ci":null}

Release files / serialforge-1.1.0-py3-none-any.whl

Download URL serialforge-1.1.0-py3-none-any.whl
Size 91.4 kB
Tags Python 3
SHA-256 checksum
How to use checksums
ae8f54a29764d8ac537baf7e31867ca71fa2458a452ad57a3d7f619e0957a940
BLAKE2b-256 checksum
How to use checksums
a86dae1c3853ab3bd5334bb5b342c0f0b56e33cb235f18641a1edea55449ed3c
Upload date
Uploaded using Trusted Publishing?
What is trusted publishing?
No
Uploaded via uv/0.13.5 {"installer":{"name":"uv","version":"0.13.5","subcommand":["publish"]},"python":null,"implementation":{"name":null,"version":null},"distro":null,"system":{"name":null,"release":null},"cpu":null,"openssl_version":null,"setuptools_version":null,"rustc_version":null,"ci":null}

Security for serialforge 1.1.0

Provenance

Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.

No provenance found

None of the files in this release were uploaded with provenance metadata.

A note on package security

PyPI is a repository for distributing packages, not a service for auditing them. While third-party security researchers and our staff do their best to detect and remove malicious packages, PyPI cannot guarantee the quality, security or suitability of any given package.

To learn more about protecting yourself from malicious actors, visit our package security documentation.

Anthropic, PBC Visionary sponsor Bloomberg Visionary sponsor Hudson River Trading Visionary sponsor Meta Visionary sponsor NVIDIA Visionary sponsor Microsoft Sustainability sponsor Depot Continuous Integration AWS Cloud computing and Security Sponsor Datadog Monitoring Fastly CDN Google Download Analytics Sentry Error logging StatusPage Status page