使用 NiceGUI 与 WebSerial API 实现浏览器直连串口设备通信
使用 NiceGUI 与 WebSerial API 实现浏览器直连串口设备通信【免费下载链接】niceguiCreate web-based user interfaces with Python. The nice way.项目地址: https://gitcode.com/GitHub_Trending/ni/nicegui导读本篇文章基于 NiceGUI 仓库中的 examples/webserial 官方示例完整讲解如何在纯 Python 的 Web 界面中调用浏览器原生 WebSerial API实现无需安装驱动、无需后端串口权限的浏览器↔串口设备如 Arduino、ESP32双向通信。读完本文你将掌握 WebSerial 的浏览器兼容性检测、设备连接/断开流程、基于流式读写的数据收发、JS 事件到 Python 事件的双向桥接以及如何在 NiceGUI 中通过数据绑定实时反映硬件状态最终落地一个可运行的“控制 LED 读取按键”实战 Demo。一、示例总览WebSerial 是什么能做什么WebSerial API 是浏览器提供的一组 JavaScript 接口navigator.serial允许网页应用直接访问用户主动授权选择的串口设备。它的核心价值在于串口通信能力完全在浏览器端完成Python 后端不需要拥有设备文件权限、不需要安装 pyserial 等驱动库天然适合远程部署、跨平台访问以及安全沙箱化的 Web 应用场景。NiceGUI 仓库中的examples/webserial目录结构如下examples/webserial/ ├── README.md # 示例说明 ├── main.py # NiceGUI 页面与串口逻辑Python ├── script.js # WebSerial 连接/读写核心浏览器端 JS ├── sketch/ │ └── sketch.ino # Arduino 固件串口协议对端 └── screenshot.webp # 运行界面截图整个示例是一条完整的“浏览器 ↔ NiceGUI 服务 ↔ 串口设备”链路浏览器通过navigator.serial与硬件直连通信Python 侧只负责界面编排、状态管理和事件转发双向数据通过 JavaScript 与 NiceGUI 的桥接机制同步。适用前提与限制WebSerial 仅在支持该 API 的浏览器Chrome / Edge 等 Chromium 系中可用且必须通过 HTTPS 或 localhost 访问页面连接设备时必须由用户点击触发并手动选择设备这是浏览器安全模型的要求。该示例依赖 NiceGUI 的ui.run_javascript与ui.add_body_html能力需要 NiceGUI 3.x 环境下文会展开说明。二、浏览器端核心script.js 中的 WebSerial 全流程WebSerial 的全部底层能力由 examples/webserial/script.js 实现。它按 NiceGUI 前端模式组织为connect()、disconnect()、send()、readLoop()四个函数并通过全局事件emitEvent(read, ...)把串口数据上报给 NiceGUI 服务端。2.1 连接设备requestPort openasync function connect() { try { port await navigator.serial.requestPort(); await port.open({ baudRate: 115200 }); } catch (err) { console.log(err); return false; } // ...后续建立读写管道 return true; }navigator.serial.requestPort()会弹出浏览器的设备选择对话框只有用户显式授权后才能获得port对象随后port.open({ baudRate: 115200 })以 115200 波特率打开串口。任何一步失败用户取消授权、设备被占用等都会抛异常并被捕获函数返回false表示连接失败。2.2 建立双向数据管道TextEncoderStream / TextDecoderStreamWebSerial 暴露的是流式接口ReadableStream/WritableStream为了以文本形式收发数据示例用编码/解码流做了桥接const encoder new TextEncoderStream(); outputDone encoder.readable.pipeTo(port.writable); // 写入方向字符串 → 字节 outputStream encoder.writable; const decoder new TextDecoderStream(); inputDone port.readable.pipeTo(decoder.writable); // 读取方向字节 → 字符串 inputStream decoder.readable; reader inputStream.getReader();这样后续代码只需操作outputStream写入和reader读取两个文本级对象无需关心字节编解码细节。2.3 发送数据send()function send(message) { const writer outputStream.getWriter(); writer.write(message \n); // 以换行符作为报文边界 writer.releaseLock(); }send()向串口写入字符串并在末尾追加\n。换行符就是本示例应用层协议的分隔符——Arduino 端读取到\n才认为一条指令结束详见第五节固件分析。2.4 读取数据readLoop() 与按行解析async function readLoop() { let fullStr ; while (true) { const { value, done } await reader.read(); if (value) { fullStr value; const res fullStr.match(/(.*)\r\n/); if (res) { fullStr ; emitEvent(read, res[1]); // 将一行数据发给 Python 端 } } if (done) { reader.releaseLock(); break; } } }由于串口数据是分块到达的一行的内容可能被拆散到多次read()中因此用fullStr做缓冲累积并通过正则/(.*)\r\n/匹配完整的一行Arduino 使用\r\n行尾对应其Serial.println输出。匹配成功后调用全局事件发射器emitEvent(read, res[1])把去掉行尾符的纯文本行上报给 NiceGUI。2.5 断开连接按顺序释放所有资源async function disconnect() { if (reader) { await reader.cancel(); await inputDone.catch(() {}); ... } if (outputStream) { await outputStream.getWriter().close(); await outputDone; ... } if (port) { await port.close(); port null; } }断开顺序有讲究先取消读取流reader.cancel()并等待inputDone再关闭写入流writer.close()并等待outputDone最后才port.close()。这样能确保管道中的数据被完整冲刷、不留悬挂的异步任务避免端口未释放导致下次连接失败。三、Python 端NiceGUI 页面编排与状态管理examples/webserial/main.py 只有约 40 行却完整演示了 NiceGUI 与浏览器 JS 协作的几个核心 API。3.1 注入前端脚本ui.add_body_htmlui.add_body_html(fscript{(Path(__file__).parent / script.js).read_text(encodingutf-8)}/script)示例通过ui.add_body_html将script.js的源码以内联script标签注入页面body。从 NiceGUI 源码 nicegui/functions/html.py 可以看到add_body_html会把代码追加到当前页面的 body HTML 中若页面响应已生成则会改为通过run_javascript动态插入。除了把 JS 文件读入内联也可以直接用ui.add_body_html(script.../script)书写脚本或用sharedTrue让脚本对所有页面生效。3.2 调用浏览器能力ui.run_javascript连接、断开、发送指令都是通过ui.run_javascript完成的async def connect() - None: if not await ui.run_javascript(serial in navigator): ui.notify(WebSerial is not available in this browser.) return if not await ui.run_javascript(connect(), timeout100): ui.notify(Could not connect to the device.) return ui.run_javascript(readLoop()) state[connected] Trueui.run_javascript是 NiceGUI 提供的“在浏览器中执行任意 JavaScript 并取回结果”的关键函数定义见 nicegui/functions/javascript.py不await时只执行、不等待返回值如ui.run_javascript(readLoop())await时JavaScript 表达式的结果会作为 Python 值返回如第一句用serial in navigator检测浏览器是否支持 WebSerial第二句用connect()的布尔返回值判断连接是否成功timeout参数默认 1.0 秒用于控制等待 JS 返回的最长时间。这里connect()因为要等待用户在弹出的设备选择框中操作被放大到timeout100秒disconnect()则收紧为timeout5执行时机JS 代码只会在客户端连接就绪后执行内部会先等待client.connected()自 NiceGUI 3.0 起这段等待不计入 timeout。注意connect()中返回false时script.js里的异常已被捕获因此这里await拿到的是布尔值而非异常若浏览器不支持 WebSerial则提前用ui.notify给出中文友好的提示避免用户误以为设备问题。3.3 串口数据上报 → Python 事件ui.on(read)ui.on(read, lambda e: state.update(buttone.args LOW))emitEvent(read, ...)在浏览器侧发射的自定义事件由 NiceGUI 的通用事件订阅机制Element.on见 nicegui/element.py接收。回调拿到的事件对象e.args就是串口传来的那一行文本Arduino 上报LOW表示按键按下、HIGH表示松开因此state[button]被更新为对应的布尔值驱动界面开关。3.4 用绑定代替手动刷新bind_* API示例用三种绑定把硬件状态与界面状态“粘”在一起全部基于state字典ui.button(Connect, on_clickconnect).bind_visibility_from(state, connected, valueFalse) ui.button(Disconnect, on_clickdisconnect).bind_visibility_from(state, connected) ui.switch(LED, on_changelambda e: ui.run_javascript(fsend({e.value:d}))).bind_enabled_from(state, connected) ui.switch(Button).props(disable).bind_value_from(state, button)绑定 API作用数据方向bind_visibility_from(state, connected, valueFalse)当state[connected] False时显示“Connect”按钮visibility.py状态 → 元素bind_visibility_from(state, connected)连接成功后显示“Disconnect”按钮状态 → 元素bind_enabled_from(state, connected)只有连接后才允许操作 LED 开关disableable_element.py状态 → 元素bind_value_from(state, button)把串口读取到的按键状态实时反映到开关上value_element.py状态 → 元素这样无需任何手动刷新代码state一变化界面自动更新同时由于 Connect/Disconnect 的显隐互斥用户不可能在连接状态下重复连接交互逻辑被绑定机制天然约束住。3.5 LED 开关Python 回调转发为 JS 指令ui.switch(LED, on_changelambda e: ui.run_javascript(fsend({e.value:d})))用户拨动 LED 开关时on_change回调把开关的布尔值格式化为1或0通过send()写入串口。这就是前端 UI → 浏览器串口 → 硬件这条下行链路的闭环。四、固件对端Arduino 串口协议解析examples/webserial/sketch/sketch.ino 是配套的 Arduino 固件板载 LED 在引脚 13按键在引脚 12使用内部上拉const int BUTTON 12; const int LED 13;下行网页 → 硬件loop()中轮询Serial.available()读到一个字符后执行switch1→digitalWrite(LED, HIGH)点亮 LED对应网页开关拨到开0→digitalWrite(LED, LOW)熄灭 LED上行硬件 → 网页检测按键引脚电平变化含 50ms 消抖并通过Serial.println输出Serial.println(buttonState LOW ? LOW : HIGH);Serial.println的行尾正是\r\n与浏览器端readLoop()中/(.*)\r\n/的解析正则严格对应——双方必须就“行分隔符”达成一致这是该串口协议能工作的前提。同时固件与script.js都使用Serial.begin(115200)/baudRate: 115200相同的波特率两处不一致会导致乱码或无法通信。五、完整运行流程与实战指引准备固件将 sketch.ino 烧录到 ArduinoIDE 中选择对应板卡与端口保持 115200 波特率。启动应用在仓库根目录执行python examples/webserial/main.pyNiceGUI 默认在http://localhost:8080提供服务。打开页面使用支持 WebSerial 的 Chromium 系浏览器访问。若在 localhost 之外访问必须通过 HTTPS否则navigator.serial不可用。连接设备点击Connect按钮在浏览器弹出的选择框中选中 Arduino 串口并确认——这是浏览器强制要求用户授权的一次性操作。交互验证拨动LED开关网页通过send()发送1/0固件点亮/熄灭板载 LED按下Button开关硬件按键固件上报LOW/HIGHui.on(read)更新state[button]界面开关随之翻转点击Disconnect按序释放读写流并关闭端口两个按钮的显隐自动回到初始状态。六、从示例到生产可复用要点总结能力检测先行任何 WebSerial 应用都应先执行serial in navigator检测并给出明确的降级提示示例用ui.notify实现。超时按操作类型区分用户交互型操作等待选设备用较长的timeout纯资源释放型操作断开用较短超时避免界面长时间无响应。把二进制流抽象为文本行TextEncoderStream/TextDecoderStream 按行正则解析是处理串口“分块到达、行可能被截断”这一天然特性的通用模式生产环境可进一步做超时组帧、校验和、重传等协议层增强。事件桥接是双向通信的钥匙下行用ui.run_javascript上行用emitEventui.on再配合bind_*数据绑定即可在几乎不写 JS 的前提下完成复杂硬件交互界面。协议两端必须一致波特率、行分隔符\n还是\r\n、报文格式都要在浏览器端与固件端严格对齐这是串口通信最常见的故障源。七、小结examples/webserial用约 40 行 Python 70 行 JavaScript 30 行 Arduino 代码串起了“NiceGUI 界面 →ui.run_javascript→ WebSerial → 串口设备 → 固件事件 →emitEvent→ui.on→ 数据绑定更新界面”的完整双向链路是理解 NiceGUI 前后端桥接机制与浏览器串口编程的极佳范例。围绕ui.run_javascriptjavascript.py、ui.add_body_htmlhtml.py、Element.onelement.py与bind_*绑定体系你可以把这一模式推广到任何需要浏览器直连硬件、传感器、调试端口的真实项目中。【免费下载链接】niceguiCreate web-based user interfaces with Python. The nice way.项目地址: https://gitcode.com/GitHub_Trending/ni/nicegui创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考