尧图网站设计 尧图网站设计YAOTU DESIGN
ARTICLE DETAIL

资讯详情

深耕网站设计与一线实操的经验洞察。

基于Web Serial API的多会话浏览器串口调试助手设计与实现

基于Web Serial API的多会话浏览器串口调试助手设计与实现 还在用串口猎人、XCOM来回切窗口还在为同时调试两块板子把USB转串口线拔来拔去今天把我折腾了快一个月的“多会话浏览器串口调试助手”完整拆开讲。这玩意儿直接用Chrome浏览器的Web Serial API把串口调试搬进了网页里而且每一路串口连接都可以独立成“会话”互不干扰实测下来无论是混用CH340还是FTDI芯片都没有出现数据串线的问题。这篇文章最适合这几类人一是平时搞嵌入式开发被各种老旧的串口助手折磨到无语的工程师二是需要在不同电脑上临时调试又不想每次都装驱动的硬件工程师三是学校实验室里管理多套单片机设备想用统一工具替代一堆杂牌串口助手的老师。我会从为什么选浏览器方案讲起到多会话的内部架构再到完整的实操过程最后把我都踩过的坑一个个排给你看。1. 先搞清楚为什么非要用“浏览器”来做串口调试1.1 传统串口助手的老大难问题先说说我为什么放着现成的工具不用非要折腾浏览器方案。做嵌入式或者硬件调试的人桌面上至少躺着两三个串口调试工具。Windows下常见的有SSCOM、XCOM、串口猎人macOS下有CoolTermLinux下用minicom的也不少。这些工具单拎出来功能都够用但一旦需要同时调试多设备、多协议问题就全冒出来了。最核心的问题是会话隔离。传统的串口助手一个窗口基本绑定一个COM口。你要同时看两块板子的打印日志就得开两个软件或者开两个窗口。这还不算最头疼的真正麻烦的是当两个设备用同一个USB转串口芯片比如两块都是CH340的板子Windows分配的COM口序号会经常乱跳今天这块板子是COM5重启一次变COM7了。一旦多个窗口开着日志混在一起看错板子定位BUG的时候简直要命。还有个很现实的问题是场景迁移。像我偶尔要在实验室不同工位的电脑上调试每台电脑都得重新装串口驱动、下载串口助手有的电脑还是公司统一管控的权限锁得死死的你连安装软件的资格都没有。这时候浏览器方案的优势就很明显了只要电脑上有Chrome或者Edge打开网页就能用。1.2 Web Serial API到底是怎么工作的浏览器能读取串口靠的是Web Serial API。这是Chrome在89版本正式推出的一项能力直接调用系统底层的串口资源。虽然起步比原生桌面工具晚但现在的API设计已经足够稳定读写、流控、参数配置都覆盖到了。从原理上讲Web Serial API把串口抽象成了两个JavaScript对象SerialPort和Serial。Serial是总控负责枚举设备、请求授权、监听设备的插拔事件SerialPort对应一条具体的物理串口链路打开之后可以拿到readable和writable两个数据流剩下的就是标准Web流式处理了。串口数据本质上是字节流没有包的概念这点和网络HTTP通信完全不同。Web Serial API也保留了这套逻辑你需要自己定义帧协议、解析数据尾帧这个灵活性反而适合做协议调试。而且这个API的设计对“多会话”天然友好。每一个SerialPort实例都是独立的只要电脑硬件资源够你可以同时打开多个设备的串口每个端口的读写、参数、日志完全隔离。这一点跟原生串口助手一字排开的做法比在代码层面就是降维打击。注意Web Serial API要求页面必须是HTTPS协议或者本地localhost环境。这个限制主要是因为串口操作权限太高浏览器出于安全考虑强制加密信道。实际部署时你可以用Nginx挂个HTTPS证书或者开发阶段直接本地起服务访问。1.3 适用场景什么时候值得用多会话方案不是所有场景都适合上浏览器串口调试但下面几种场景我用下来是真的香多设备联调一块STM32主控一块ESP32一个FPGA开发板三个串口同时看日志每个会话独立标记颜色和过滤规则修改参数时互不干扰。远程协助调试配合内网穿透工具把调试页面临时分享给远端同事同事浏览器打开就能看到实时串口数据不用同步安装任何软件。教学和演示场景给学生展示串口收发的时候直接在浏览器里演示学生用自己电脑打开页面就能做实验不用提前按头装驱动。产测脚本辅助多会话浏览器串口助手可以配合Web Worker跑数据比对逻辑一个会话负责下发指令另一个会话监听响应自动判断PASS/FAIL。这些场景的核心共同点就是多路数据需要同时看、同时操作而且环境不能依赖特定操作系统。2. 多会话架构的核心设计思路2.1 “会话”到底是什么数据隔离是灵魂我做这个项目的最大出发点就是把“会话”这个概念做扎实。这里说的会话不能只是一个皮肤外壳必须做到三个层面的隔离。第一层是设备隔离。每个会话绑定一个物理串口端口对象独立发送和接收的数据互不相通。这层隔离主要靠Web Serial API的端口管理同一条串口线不可能同时被两个会话打开避免了数据串线的物理可能。第二层是参数隔离。每个会话有独立的波特率、数据位、停止位、校验位配置。比如会话A是115200-8-N-1会话B是9600-8-E-1切换会话时不需要重设参数系统会自动记住这个会话上次的配置。这个细节在实际调试中特别省事尤其是不同设备使用不同波特率时频繁手动切换很容易出错。第三层是显示和日志隔离。每个会话维护自己独立的接收缓冲区、历史记录、导出文件。我在设计时把每个会话的数据流都单独存到一个环形队列里队列上限默认5万条超过后自动丢最旧的数据。这样即使在长时间压力测试的场景下不同会话的数据也不会互相污染日志乱掉的情况从根本上防住了。提示串口的数据流是字节流多会话最怕的就是“A会话的返回值串到了B会话的显示器上”。解决思路就是上面说的三层隔离——设备、参数、显示互不共用并且在代码层面对每个会话的数据流做独立订阅和独立解析不搞全局总线中转。2.2 串口参数配置的底层逻辑串口调试参数配置是最基础也最容易出错的一环。市面上的串口助手参数设置项大同小异但Web方案里怎么存、怎么同步踩过坑的都知道不简单。这个工具里每个会话会维护一个SerialConfig对象包含波特率、数据位、停止位、校验方式、流控方式。波特率我用的是下拉自定义组合的输入框除了常见的9600、19200、38400、57600、115200还支持手动输入非标波特率比如某些定制的传感器模块用的是4800。对于非标波特率Web Serial API其实也可以支持但要注意操作系统底层是否认得这个速率实测中Linux系统对非标波特率支持最好Windows偶尔会直接拒绝打开端口。数据位、停止位、校验位这三项分别对应SerialOptions的dataBits、stopBits、parity属性。这块API的限制在于值必须是合法的枚举组合比如数据位只能是7或8校验位只能是none、even、odd。如果你设置了错误组合打开端口时浏览器会直接抛异常。我在代码里对配置做了合法性校验不通过就不发起打开请求提前拦截错误。流控方面Web Serial API的flowControl选项目前主要支持none和hardware。硬件流控就是用RTS/CTS引脚做握手如果你的设备用了流控线这里就必须选hardware否则高频大数据量通信时容易出现数据丢失。说实话大部分开发板默认是不用硬件流控的我用下来99%的场景都是none。2.3 多会话状态管理与界面布局设计界面布局直接决定了这工具好不好用。我的方案是左侧会话列表右侧标签页的工作区布局这个设计是从IDE的标签页模式借鉴过来的。左侧列出所有已创建的会话每个会话显示设备名、当前状态打开/关闭、以及实时通信速率的小指示条。右侧是标签页点击会话就能切入对应的收发面板支持拖拽排序也支持一键展开分栏模式同时看两个会话的收发区。会话状态管理是整个项目复杂度最高的部分。一个会话至少包含这些字段会话ID、设备信息vendorId、productId、serialNumber、串口参数、发送区内容、接收区内容、过滤规则、日志开关、以及内部的环形队列句柄。为了不让多会话的状态管理变成一团乱麻我用了类似Redux的单向数据流架构。所有会话状态放在一个全局Store里组件通过Action来修改状态。每次打开或关闭串口都会触发数据的异步更新界面上的连接状态、指示灯、日志区域都会同步变化。这种架构的收益在处理多会话并发时特别明显状态变更可追踪、可回溯不会出现“点了A会话的开结果B会话收到数据”这种诡异BUG。3. 实操过程与核心环节实现3.1 环境准备与项目搭建开始之前先把环境准备好。这个项目不需要装任何串口驱动这是浏览器方案的天然优势。但你得有一台安装了Chrome 89或Edge 89的电脑最好是Windows、macOS、Linux都各备一台方便后面测试不同系统下的表现。硬件层面准备一块带串口输出的开发板CH340芯片的板子最典型FTDI芯片的也准备一块后面排查问题的时候能用得到。接线时注意USB转串口的TX接板子的RXRX接板子的TXGND要共地。我见过太多人第一次连串口没反应最后发现是把TX和RX接反了。前端搭建我用的Vite Vue 3。Vite启动快、热更新及时写这种偏工具类的项目体验很好。串口调用逻辑单独封装成一个模块组件层不直接接触navigator.serial这样以后想换框架也容易。npm create vitelatest serial-debug-tool -- --template vue cd serial-debug-tool npm install npm run dev启动之后浏览器访问http://localhost:5173。注意localhost属于安全上下文可以正常使用Web Serial API。如果用IP访问浏览器会直接报警告说不支持。3.2 核心功能1连接管理模块连接管理是整个工具的地基包含三块设备枚举、授权请求、打开端口。设备枚举有两种方式。首次连接时主动弹窗让用户选择设备用的是navigator.serial.requestPort()。这个方法必须在用户手势点击事件里调用不能页面加载时自动调否则会被浏览器拦截。另一种是查询之前授权过的设备用navigator.serial.getPorts()页面刷新后还能重新连回之前的串口。async function connectToSerial() { // 必须在用户点击等手势事件中调用 const port await navigator.serial.requestPort(); const info port.getInfo(); console.log(选择了设备: VID${info.vendorId} PID${info.productId}); return port; }打开端口时要传入详细的串口参数这一步出错的概率最大。建议在打开之前先校验参数合法性不要直接交给API去抛异常。const config { baudRate: 115200, // 波特率必须为正整数 dataBits: 8, // 数据位7或8 stopBits: 1, // 停止位1或2 parity: none, // 校验none / even / odd flowControl: none, // 流控none / hardware bufferSize: 255 // 可选参数接收缓冲区大小 }; async function openPort(port, config) { await port.open(config); console.log(串口已打开); }bufferSize这个参数容易被忽略。它控制的是内部接收缓冲区大小默认值是255如果你的设备有大量数据突发建议调大到1024以上否则读取端来不及消费内核缓冲区会溢出丢数据。这块我在第4章还会详细说。监听设备插拔事件也很关键。调试的时候经常会有用户突然拔掉USB线代码里如果没有监听disconnect事件后续的数据读写会直接抛异常界面上还会卡在“已连接”状态。3.3 核心功能2数据收发与日志记录数据的读写是通过Web Streams API实现的。port.readable是一个ReadableStream你可以用getReader()拿到读取器然后循环读取字节。port.writable对应WritableStream用getWriter()拿写入器。接收数据是最核心的逻辑。我的实现是启动一个持续运行的读取循环这个循环在串口打开期间不退出async function readLoop(port, sessionId) { const reader port.readable.getReader(); const decoder new TextDecoder(); try { while (true) { const { value, done } await reader.read(); if (done) break; // 流已关闭端口被关闭或者断开了 if (!value) continue; const text decoder.decode(value); appendToSessionBuffer(sessionId, text); // 写入该会话的环形队列 updateReceivePanel(sessionId, text); // 更新界面显示 } } catch (err) { handleSerialError(sessionId, err); } finally { reader.releaseLock(); } }这里有几个性能细节值得强调。TextDecoder不要每次循环都实例化全局复用一个实例性能要好得多。接收区的界面更新不要每收到一个字节就DOM刷新一次这样浏览器会卡死。我用的是节流更新策略数据先写入环形队列界面层用requestAnimationFrame周期性批量刷新这样即使以每秒几万字节的速度涌入界面也不会卡顿。发送数据相对简单注意写入前要await writer.ready否则写入缓冲区满了会丢数据async function sendData(port, data) { const writer port.writable.getWriter(); await writer.ready; await writer.write(new TextEncoder().encode(data)); writer.releaseLock(); }日志记录方面每个会话有独立的日志文件下载按钮支持TXT和CSV格式导出。TXT适合记录纯文本日志CSV适合后续做数据分析。记录带上毫秒级时间戳方便和协议栈里的其他日志对齐排查。多会话导出时文件名会自动加会话ID前缀避免下载后搞混是哪块板子的日志。3.4 核心功能3多会话并发管理多会话并发是整个工具的特色功能也是最容易出BUG的部分。我先说清楚整体的并发模型每个会话的读取循环是独立运行的数据在进入环形队列之后才会做统一展示处理。写入操作方面因为不同会话绑定不同端口天然互不阻塞。但并发也会带来系统层面的竞争问题最典型的就是多个会话同时发起打开操作导致USB控制器短暂高负载。实测下来同一个USB Hub上同时打开三个串口时第一个和第三个参数一致的场景下偶尔会出现端口打开慢半拍的情况。我的处理办法是加了一个全局的“打开串口异步队列”所有会话的打开操作串行执行确保不会同时向USB控制器发请求。const openQueue new AsyncQueue(); async function openSerialConcurrent(sessionId, port, config) { await openQueue.enqueue(async () { await port.open(config); // 打开成功后启动该会话的读取循环 startSessionReadLoop(sessionId); }); }并发场景下界面的实时性也很重要。左侧会话列表里我对每个会话加了速率指示条每秒统计一次收发字节数用滚动柱状图展示。实测多会话同时刷新时并没有出现明显的卡顿。注意多会话的硬上限取决于系统的串口资源浏览器层面没有硬性数量限制。但如果你的电脑用了多个USB转串口设备建议把连接的USB口分散到不同的USB控制器上避免单控制器带宽被占满导致数据丢包。我在同一USB Hub上同时接5个CH340数据吞吐开始出现抖动分散到两个控制器之后立刻恢复正常。3.5 界面功能细节与操作流整个界面我按“会话为中心”来编排。新建会话时会弹出设备选择对话框列出当前系统所有可用的串口设备。用户选好后进入会话详情页这时参数配置、发送区、接收区都在同一个页面上。接收区默认是文本模式显示成带滚动行的日志流。可以切换到Hex字节模式每个字节显示为两位十六进制分组显示这在对二进制协议时非常有帮助。过滤功能是刚需我支持关键词过滤、正则过滤和反转过滤比如只想看标有[ERROR]的日志行直接在过滤器里输入正则\[ERROR\]即可。发送区支持三块单行发送区、定时发送设置、快捷指令列表。定时发送是调试心跳包或周期指令的利器间隔单位最小可以设置到1ms。快捷指令列表是高频操作把常发的AT指令、UART协议帧存好点击即发送配合多会话的场景可以对不同会话配置不同指令集。会话的持久化设计值得一提。我用了localStorage保存每个会话的配置和发送区的历史指令。刷新页面后会话列表还在参数都在但串口需要重新打开——因为浏览器不允许页面自动重连串口必须由用户主动点击操作触发。{ sessionId: a1b2c3, device: { vid: 1A86, pid: 7523 }, config: { baudRate: 115200, dataBits: 8, stopBits: 1, parity: none, flowControl: none }, sendHistory: [ATRST, ATGMR, ATCWJAP\ssid\,\pass\] }4. 常见问题与排查技巧实录4.1 设备怎么选不到驱动和权限排查浏览器里选不到串口设备或者选设备时列表为空这是遇到最多的新手问题。原因无非下面几类。最典型的是USB转串口驱动没有正确安装。虽然浏览器方案不需要你在界面里装什么但操作系统如果不识别这块USB芯片requestPort()的列表里就永远不会出现它。CH340在Windows下需要装驱动FTDI大部分系统自带驱动但这几年Windows更新也偶尔会抽风。判断驱动是否正常最简单的办法是打开设备管理器看看端口下面有没有对应的COM口没有就是驱动没装好。还有一种情况是设备被别的进程占用了。比如你本地开着一个原生串口助手或者另一个浏览器标签页已经连着这块板子你再通过新的会话去连接打开端口时浏览器会报NetworkError底层意思是“资源已被占用”。解决办法是检查所有占用串口的软件全部关掉再试。Linux系统要多一步权限配置。当前用户如果没有串口设备所在的dialout组的权限浏览器请求端口时能看到设备但打开会失败。解决方法是把用户加进dialout组然后注销重登sudo usermod -a -G dialout $USER4.2 打开端口报错集中在哪几个原因port.open()抛异常是调试过程中经久不衰的话题。我把常见的几类错误归结如下第一类是参数不合法。上来就写baudRate: 1000000部分系统芯片并不支持这么高的速率。或者dataBits: 6这是Web Serial API不支持的枚举值。这类错误调试起来最快控制台看一眼报错信息回去检查参数。第二类是端口被占用。这个和前面说的设备占用类似但是更隐秘——有时候你上一个会话没关干净端口还处于打开状态新会话去打开同一个端口就会报错。我的代码里对这类情况做了双层保护技术上记住每个端口当前绑定的会话ID界面上在每个会话的连接按钮旁显示当前占用状态。第三类是操作时序错误。串口打开操作需要等待硬件信号稳定如果你的代码在open()完成之前就去读取数据流会拿到undefined的reader。这类问题通常表现为数据接收区域一直空白控制台也没有报错。我的处理方式是统一使用异步状态机把“打开中”“已打开”“关闭中”“已关闭”状态全梳理清楚只有已打开状态才允许触发读写操作。4.3 数据乱码和丢字节先别怪芯片数据乱码的现象多种多样但排查方向其实很统一。先看波特率是否两边一致——主机和板子有一个不一致出来的字就是一堆乱码。再看校验位和数据位两边设置必须完全一致否则偶发错位会越来越严重。还有一个经常被忽略的点不要用高波特率跑长线。我实测过用2米长的杜邦线在115200波特率下传输数据丢包率明显上升。把波特率降到38400或者换屏蔽线之后问题立刻消失。如果是长距离传输场景硬件上想办法加电平转换或者隔离器靠软件调参治标不治本。丢字节的关键主要在bufferSize的设置和读取循环的处理速度上。SerialPort的bufferSize默认值是255如果你的设备一次突发几百个字节并且读取循环里做了大量处理逻辑来不及消费的数据就会被内部缓冲区丢弃。解决措施一是调大bufferSize到1024以上二是读取循环里只做“收数据入队”这个最轻量动作数据处理放到队列的另一端慢慢消化。const config { // ...其他参数不变 bufferSize: 1024 };4.4 多会话并发数据串线的极限测试多会话最怕什么最怕看起来每个会话独立实际上数据串来串去。我用三块不同芯片CH340、FTDI、原生串口做了并发测试。三个会话同时打开同时以500ms的间隔发不同前缀的指令接收端分别打印接收到的数据。测试结果下来在纯文本模式、较低速率9600和115200混用下三路数据没有任何串线。在中断频繁插拔测试中偶发出现某一路会话报错断开但没有出现数据错流到其他会话的情况。这说明三层隔离的架构在真实硬件条件下是可靠的。但极端情况下也有需要留意的点当你用不同波特率同时接收时界面如果启用了“自动滚动到底部”高频刷新下界面渲染压力会比较大。建议高频数据场景下关闭自动滚动改用“暂停滚动”的按钮否则布局频繁重排体感会掉帧。4.5 实用排查技巧速查表问题现象优先排查路径解决方案列表里看不到设备USB驱动、连接线故障设备管理器检查COM口换线换口测试端口打不开参数配置、驱动权限、被占用检查参数枚举值关掉占用程序Linux加dialout组收到乱码波特率、校验位、接线统一双方参数TX/RX交叉检查降低波特率高流量时丢数据缓冲区和读取速度调大bufferSize读取循环只做入队操作数据串会话会话隔离失效检查唯一会话ID绑定、全局Store隔离逻辑页面刷新后连不上浏览器安全策略必须用户手势重新打开串口4.6 从工具到生产力的几个建议多会话浏览器串口调试助手做出来之后我实际工作流的改变比预想中大得多。以前调一块新板子先装驱动再开串口助手改波特率手动记日志。现在直接打开浏览器导入之前存好的会话配置点两下就连上了。我个人的经验是把这个工具和自动化脚本结合起来才是最大生产力提升。可以设计一个隐藏的“自动化”面板用JavaScript给特定会话发送指令然后基于返回结果做判断。比如给板子上电后自动发送ATSTART如果3秒内没有响应自动重发并标记失败。这种能力是传统串口助手很难提供的因为原生工具通常不开放脚本接口。最后提醒一句虽然Web Serial API已经很成熟了但浏览器版本更新偶发会带来兼容性问题。碰到奇怪的报错先去更新Chrome到最新版本很多“莫名奇妙”的问题直接就消失了。工具做出来就是给人用的我的体会是别过度追求炫技功能把数据查看、多会话隔离、日志导出这些基础能力做到极致稳当就是最适合日常开发的好工具。尤其是多会话并发这一块架构上把隔离做扎实后面加什么新功能都不慌。
返回列表