用 Firefox 直接刷板子:Web Serial API 把浏览器变成硬件开发工具

2026-05-25 19 预计阅读时间:1 分钟
来源:oschina.net AI 摘要 原文链接

免责声明:本文为 AI 摘要整理,建议结合原文阅读。摘要可能省略上下文、版本差异或边界条件,不作为官方说明。

预计阅读时间:8 分钟

过去给一块 CircuitPython 板子刷固件、读传感器数据,你得先装个串口终端——macOS 上是 screenminicom,Windows 上找 PuTTY,Linux 上折腾 stty 参数。现在 Firefox 和 Adafruit 联手把这件事搬进了浏览器:打开网页,选端口,直接读写串口,零桌面依赖。

Web Serial API:从 USB 到网页的一步直达

Web Serial API 是 Chromium 社区几年前提出的标准,允许网页通过 navigator.serial 直接与 USB/蓝牙串口设备通信。Firefox 此前一直没跟进,这次合作标志着它正式加入支持行列。

核心流程只有三步:

  1. 请求端口navigator.serial.requestPort() 弹出系统端口选择器,用户手动授权。
  2. 打开连接 — 设定波特率等参数,调用 port.open()
  3. 读写数据 — 通过 ReadableStream / WritableStream 收发字节流。

安全模型是关键:端口不会自动暴露给网页,必须由用户主动点击触发选择,且只对当前 origin 有效。这比桌面串口工具那种"谁都能读"的模型要严格得多。

Adafruit 的落地场景

Adafruit 的硬件生态以 CircuitPython 板子为主——Feather、ItsyBitsy、Trinket 系列。这些板子插上 USB 后,会以串口设备的形式出现在系统中,用户通常通过 REPL 交互。

合作带来的直接变化:

  • Web-based REPL — 在 Adafruit 的网页工具里直接打开板子的 Python REPL,敲代码、看回显,不用装任何本地软件。
  • 固件刷写 — 某些板子支持串口刷入 .uf2.bin 固件,网页可以直接完成这个流程。
  • 传感器数据可视化 — 板子持续往外吐数据,网页用 ReadableStream 持续读取,实时画图表。

这对教育场景尤其友好——学校机房不让随便装软件,但浏览器是现成的。

实战:在 Firefox 里连一块 CircuitPython 板子

下面这段代码可以在任何支持 Web Serial 的浏览器里运行。你只需要一块 CircuitPython 板子(或任何输出文本的串口设备),插上 USB,然后打开这个 HTML 文件。

<!DOCTYPE html>
<html lang="zh-CN">
<head>
  <meta charset="UTF-8">
  <title>Web Serial REPL</title>
  <style>
    body { font-family: monospace; max-width: 720px; margin: 2rem auto; }
    #output { background: #1e1e1e; color: #d4d4d4; padding: 1rem;
              height: 300px; overflow-y: auto; white-space: pre-wrap; }
    #input-line { display: flex; gap: 0.5rem; margin-top: 0.5rem; }
    #cmd { flex: 1; }
    button { padding: 0.4rem 1rem; }
  </style>
</head>
<body>
  <h2>串口 REPL</h2>
  <button id="connect">连接设备</button>
  <button id="disconnect" disabled>断开</button>
  <div id="output"></div>
  <div id="input-line">
    <input id="cmd" placeholder="输入命令,回车发送" disabled>
    <button id="send" disabled>发送</button>
  </div>

  <script>
    const connectBtn  = document.getElementById('connect');
    const disconnectBtn = document.getElementById('disconnect');
    const outputDiv   = document.getElementById('output');
    const cmdInput    = document.getElementById('cmd');
    const sendBtn     = document.getElementById('send');

    let port = null;
    let reader = null;
    let writer = null;
    let readableClosed = false;
    let writableClosed = false;

    // 连接串口
    connectBtn.addEventListener('click', async () => {
      if (!('serial' in navigator)) {
        alert('当前浏览器不支持 Web Serial API');
        return;
      }
      try {
        port = await navigator.serial.requestPort();
        // CircuitPython 板子默认 115200 波特率
        await port.open({ baudRate: 115200 });

        connectBtn.disabled = true;
        disconnectBtn.disabled = false;
        cmdInput.disabled = false;
        sendBtn.disabled = false;

        // 持续读取板子输出
        const decoder = new TextDecoderStream();
        readableClosed = port.readable.pipeTo(decoder.writable);
        reader = decoder.readable.getReader();
        readLoop();

        // 准备写入通道
        const encoder = new TextEncoderStream();
        writableClosed = encoder.readable.pipeTo(port.writable);
        writer = encoder.writable.getWriter();
      } catch (e) {
        outputDiv.textContent += `\n[错误] ${e.message}`;
      }
    });

    // 持续读数据并显示
    async function readLoop() {
      try {
        while (true) {
          const { value, done } = await reader.read();
          if (done) break;
          outputDiv.textContent += value;
          outputDiv.scrollTop = outputDiv.scrollHeight;
        }
      } catch (e) {
        outputDiv.textContent += `\n[读取中断] ${e.message}`;
      }
    }

    // 发送命令到板子
    async function sendCommand(text) {
      if (!writer) return;
      await writer.write(text + '\r');  // REPL 需要 \r 而非 \n
    }

    sendBtn.addEventListener('click', () => {
      sendCommand(cmdInput.value);
      cmdInput.value = '';
    });

    cmdInput.addEventListener('keydown', (e) => {
      if (e.key === 'Enter') {
        sendCommand(cmdInput.value);
        cmdInput.value = '';
      }
    });

    // 断开连接
    disconnectBtn.addEventListener('click', async () => {
      reader.cancel();
      await readableClosed.catch(() => {});
      writer.close();
      await writableClosed;
      await port.close();

      port = null; reader = null; writer = null;
      connectBtn.disabled = false;
      disconnectBtn.disabled = true;
      cmdInput.disabled = true;
      sendBtn.disabled = true;
      outputDiv.textContent += '\n[已断开]';
    });
  </script>
</body>
</html>

使用前注意几点:

  • Firefox 需要在设置中启用 Web Serial(目前可能仍在逐步 rollout,检查 dom.serial.enabled 配置项)。
  • 波特率 115200 是 CircuitPython 的默认值;Arduino Uno 通常用 9600,根据你的板子修改。
  • REPL 交互用 \r(CR)而非 \n(LF),这是 CircuitPython REPL 的约定,搞错了板子不会执行命令。
  • 每次连接必须由用户点击"连接设备"按钮触发,这是安全模型的要求——网页不能静默连串口。

还能做什么:超出 REPL 的玩法

Web Serial 给出的只是字节流,怎么用是网页的事。几个值得尝试的方向:

  • 实时数据仪表盘 — 板子上跑一段每秒输出 JSON 的代码,网页 JSON.parse 后喂给 Chart.js,零延迟可视化。
  • 批量固件部署 — 教室里有 20 块板子,学生逐个点击连接、网页自动刷入统一固件,比逐台装软件快得多。
  • 自定义协议 — 不用 REPL,板子和网页约定二进制帧格式(头 + 长度 + 校验),网页侧用 DataView 解析,适合传感器高频采样。

采纳前的权衡

优势 限制
零安装,任何有浏览器的机器都能用 浏览器必须支持 Web Serial,Safari 目前不支持
用户主动授权端口,安全边界清晰 每次刷新页面需重新连接(端口不持久化)
流式 API 天然适合持续数据读取 大吞吐场景下流控需自行处理,没有硬件流控 RTS/CTS 的 API
教育和工坊场景极度友好 复杂调试(断点、单步)仍需桌面 IDE

如果你在做硬件工坊、STEM 课程,或者只是想快速读一块板子的传感器数据——现在可以直接打开 Firefox,不用再翻箱倒柜找串口驱动了。


相关推荐