SerialHub:把串口变成 WebSocket 字节管道,浏览器和脚本直接读写(开源工具 SerialHub 实战)

一、这些场景你大概率遇到过

写嵌入式固件的朋友,这三件事应该没少折腾:

  • 串口被一个程序独占。串口助手开着收数据,上位机脚本就打不开同一个 COM 口;想两边同时看,只能开一对虚拟串口,或者来回关开关开。
  • 设备热拔插就要全部重连。USB 线一碰、设备重新枚举,所有程序集体断线。更坑的是有的工具断线后"窗口还开着、数据早不来了",你盯着空屏排查十分钟,其实是旧句柄在设备抖动后已经失效了。
  • 想在浏览器或脚本里摸串口。无头浏览器拿不到 Web Serial 授权,网页上位机连不上设备;自动化测试想用脚本假装一台设备跟被测系统对发字节,也缺一条顺手的管道。

SerialHub 就是为这些场景做的:Apache-2.0 开源、完全免费,Rust 写的跨平台单文件,管理台网页直接内嵌在二进制里,下载解压就能用。它把串口变成一条 WebSocket 字节管道,浏览器和脚本直接读写原始字节,所有数据全程留在本机。

二、它是什么

一句话:每块串口对应一座"桥",桥的另一头是一个 WebSocket 数据网址;所有桥由一个固定的网页管理台统一管理。

复制代码
COM1 ══ 桥1 ══ ws://127.0.0.1:8101/ws ══ 网页上位机(自带协议栈)
COM3 ══ 桥2 ══ ws://127.0.0.1:8102/ws ══ Python 测试脚本
              │
     管理台 http://127.0.0.1:8080
     (建桥 / 启停 / 改配 / 删除 / 速率统计,重启自动恢复)

数据端口由程序自动分配(默认从 8101 起),桥的生命周期内不变。两个网址记住一句话就够:管理台网址管程序,数据网址管字节,程序接入永远连数据网址。

动手做这个工具之前,我把现成方案用了一圈:有的在桥里做 Modbus、对外只吐 JSON;有的用 JSON+base64 包帧,页面侧协议栈得推倒重来;有的明确单客户端、没有广播;还有的停更好几年了。SerialHub 的取舍是反过来的:桥里不做任何协议语义,只搬运字节。为什么这么选,第五部分细说。

三、快速上手

从 GitHub Releases 下载对应平台的压缩包,解压就是全部:Windows x86_64 的 zip 只有约 1.6 MB,不用装运行时、不用注册账号,双击就能跑。想从源码编译,仓库里 cargo install --path . 一条命令的事。

命令行三步(旧单桥参数 = 自动建一座桥并启动,命令与注释原样来自 README):

bash 复制代码
# 列出本机串口
serialhub --list-ports

# 一行起桥: 串口 COM1, 115200, 8N2; 管理台地址 127.0.0.1:8080
serialhub --port COM1 --baud 115200 --config 8N2 --addr 127.0.0.1:8080
# 浏览器打开 http://127.0.0.1:8080 ------ 管理台即用
# 程序接入: ws://127.0.0.1:8081/ws (本桥数据端点, 纯二进制; 裸地址 ws://127.0.0.1:8081 亦可)

它有两种形态:

  • 桌面客户端 :Windows 双击 serialhub.exe 就是原生窗口 + 系统托盘,关窗退到后台,桥继续收发;
  • 纯命令行 :脚本和 CI 场景加 --headless,无窗口无托盘:
bash 复制代码
serialhub --headless --port COM1   # 纯命令行模式 (脚本/CI)

串口参数是全覆盖的:波特率 110~2,000,000,数据位 7/8,校验 N/E/O,停止位 1/2,流控 none/rtscts/xonxoff。CLI 和管理台完全对等,界面上每个配置项在命令行都有对应物,管理台里还能一键复制与当前配置等价的启动命令。

四、多桥管理台:所有桥一个网页管完

  • 多桥并存:每行一座桥,状态徽章(运行中 / 连接中 / 重连中 / 已停止)、连接数、运行时长、最近错误一眼看完;
  • 可视化:每桥一张流程图(串口 ⇄ 桥 ⇄ 数据网址),有数据流动线路就点亮;RX/TX 速率火花线和累计字节数每秒刷新;
  • 新建桥零门槛:选串口、设波特率就能跑,数据端口自动预填下一个空闲端口;表单记住上次的串口和波特率,连建多座桥很快;
  • 只读旁看:点桥名打开抽屉,「串口数据」页签只读查看串口原始流,ASCII / HEX 随时切换,最多保留 5000 行;
  • 重启自动恢复:所有桥的配置持久化在 fleet.json 里,程序重启自动恢复全部桥,不用每天早上重新建一遍;
  • 自动重连可开关:每座桥独立控制(默认开),运行中改开关即时生效;
  • 主题插件:内置浅色 / 深色 / 示例·奥利奥 / Windows 95 四套主题,exe 旁 themes/ 文件夹放一个 .css 就是一套新主题,切换不刷新页面;
  • 托盘常驻:图标颜色跟着桥状态变(绿=运行中 / 琥珀=重连中 / 灰=已停止)。

命令行形态也有截图可看:

五、关键实现思路

讲三个设计决策,素材来自仓库里的架构决策记录(ADR),想看原文的去仓库翻 decisions.md

1. 数据面只做管道:纯原始二进制帧

串口桥最容易做偏的地方,是"顺手"帮你解析协议。我调研时见过的反例就是 JSON+base64 包帧、桥内做 Modbus:桥一旦做语义,页面侧协议栈就得跟着它的帧格式重写,接入那天就是推翻自家代码的一天。SerialHub 把定位钉死在"管道":/ws 数据通道只传原始二进制帧 ,双向;组帧、解析、校验全部留给页面和脚本自己的协议栈。配置与状态走 /api/*(JSON),控制面与数据面彻底分离,管理台页面挂了也不影响字节流动。

这个设计换来两条使用前必须心里有数的规则(手册「程序接入」章原文要点):

  • WebSocket 消息边界 ≠ 串口帧边界:设备一次 write 可能被拆成多条消息到达,多次 write 也可能合并成一条;请按自己的协议组帧,别依赖消息边界;
  • 慢客户端丢旧帧:下行缓冲 1024 条,消费跟不上时丢最旧帧保连接,不反压、不踢人。高波特率加慢消费端的组合,请确保消费端跟得上。

2. 多客户端:下行广播,上行 FIFO 单写者

串口本质是单用户设备,多个客户端同时写就是灾难。SerialHub 的仲裁很朴素:

  • 下行(串口 → 客户端)用广播通道:收到的字节推给所有客户端,测试脚本和网页可以同时围观同一条设备流;
  • 上行(客户端 → 串口)单写者:所有客户端的帧进一个 mpsc 队列,由唯一写者按 FIFO 顺序写入串口,先到先发,文档明示不做客户端优先级。

对比有的实现让每个会话各自轮询同一端口、互相抢字节,这条路至少保证了三件事:不抢字节、不乱序、不 panic。

3. 自动重连:独立监督任务,重试全程可见

这个功能来自真实的教训:CH340 抖动一次,旧串口句柄可能永久失效,"桥活着却不转发"是极难排查的静默故障。所以 SerialHub 没有把重开写成 try-catch 兜底,而是做成一等公民:

  • 一个独立于数据面的监督任务按 1 秒节奏重试,永不阻塞数据转发;
  • 状态机 Closed → Opening → Open → Retry 全程在状态接口可见;
  • 管理台明示「串口已断开,正在自动重连 (第 n 次)...」,第几次都数给你看。

对你的脚本来说什么都没发生:期间 WebSocket 连接保持着,设备插回来数秒内恢复,数据自动续传,客户端零动作。每座桥也可以独立关掉这个开关(关了掉线就直接转「已停止」)。

顺带一组仓库更新日志里记录的自测基线(v1.0.0,仅供参考):双向吞吐 7.3 / 10.4 Mbps,串口收到字节到 WebSocket 客户端收到的延迟 p95 约 0.61 ms。工程侧仓库里有 83 条 Rust 单元测试和 63 条跑在 COM1↔COM2 虚拟串口对上的集成测试,回归是有保障的。

六、客户端接入示例

Python(摘自 examples/python-client.py,先 pip install websockets

python 复制代码
DATA_URL = "ws://127.0.0.1:8090/ws"  # 改成你的桥数据网址


async def main():
    async with websockets.connect(DATA_URL) as ws:
        async def rx():  # 下行: 串口收到的原始字节, 打 HEX
            async for data in ws:
                print("收", data.hex(" "))

        async def tx():  # 上行: 每秒一行 "ping" (UTF-8 原始字节)
            while True:
                await asyncio.sleep(1)
                await ws.send(b"ping\n")

        await asyncio.gather(rx(), tx())

浏览器(摘自 examples/web-client.html,零依赖单文件,双击即用)

js 复制代码
ws = new WebSocket($('url').value);      // 数据端点: ws://<桥网址>/ws
ws.binaryType = 'arraybuffer';           // 铁律 1: 拿到的就是原始字节
ws.onmessage = e => log('收', new Uint8Array(e.data));   // 铁律 2: 只展示, 不组帧

发送侧:

js 复制代码
ws.send(new TextEncoder().encode(s));    // 二进制帧 → 桥串行写入串口

两个示例都在仓库 examples/ 目录,拿来改改就能用。注意管理台端口 ≠ 数据端口,真实数据网址以桥卡片或启动输出为准。

七、限制与诚实声明

  • 无 TLS、无鉴权(v1.x 现状) :任何连得上管理台或数据网址的人都能读写字节、改配置。只在本机或可信内网使用,勿暴露公网;需要远程访问请自己套 SSH 隧道。
  • macOS 安装包未签名:首次打开若被系统拦下,右键点文件选「打开」,再点一次「打开」即可放行。
  • Linux/macOS 桌面形态 需要 webkit2gtk / WKWebView 运行库;服务器脚本场景直接用 --headless 纯命令行。
  • Windows 发布版没有控制台黑窗 (双击即用),代价是 --headless 模式 stdout 不回显到终端------脚本判活请轮询 /api/status
  • 它不是串口终端仿真器:不做 VT100/xterm 终端;管理台「串口数据」只能看不能发,发数据是客户端程序(网页/脚本)的职责。

八、链接

SerialHub 按 Apache-2.0 完全开源,没有付费墙,也没有功能锁定。如果你也被"一个串口,全家排队"折磨过,下载试一试,顺手去仓库点个 Star,就是对它最好的支持;用得不顺手,欢迎直接提 issue。

相关推荐
传奇开心果编程2 小时前
【xilem0.4基础语法学与练】第48课 lists组件官方示例代码深度解析
学习·rust·前端框架
传奇开心果编程6 小时前
【Rust入门知识点学与练】第24课:Trait 基础
开发语言·学习·rust
susplus18 小时前
【51单片机通信协议】串口
51单片机·串口·嵌入式·通信协议
传奇开心果编程19 小时前
【dioxus0.7基础语法学与练】第8课:RSX 宏 — Dioxus 声明式 UI 语法
开发语言·学习·rust
柯南466820 小时前
【AI开发之Rust】第 5 课:引用与生命周期 —— 借用能活多久?
rust
只睡四小时20 小时前
内网穿透+WebSocket:Node 零依赖远程桌面实战
网络·websocket·网络协议
码农哈丁21 小时前
从 JVM 到 Cargo:把整个 Kotlin 项目移植到 Rust 的踩坑实录
jvm·rust·kotlin
梦想平凡21 小时前
百游棋牌源代码开发搭建教程(七):WebSocket房间同步与断线恢复
网络·websocket·网络协议
PC2005-cloud1 天前
Rust学习笔记:所有权、借用与生命周期——Rust的核心机制
笔记·学习·rust