Web Serial API 使用入门

1. 简介

上次更新日期:2022 年 9 月 19 日

构建内容

在此 Codelab 中,您将构建一个网页,通过 Web Serial API 与 BBC micro:bit 板进行交互,以便在其 5x5 LED 矩阵上显示图像。您将了解 Web Serial API,以及如何使用可读写的数据流和转换数据流通过浏览器与串行设备通信。

67543f4caaaca5de

学习内容

  • 如何打开和关闭网络串行端口
  • 如何使用读取循环处理输入流中的数据
  • 如何通过写入流发送数据

所需条件

我们选择在此 Codelab 中使用 micro:bit v1,因为它经济实惠,可以提供一些输入(按钮)和输出(5x5 LED 显示屏),并且可以提供额外的输入和输出。如需详细了解 micro:bit 的功能,请参阅 Espruino 网站上的 BBC micro:bit 页面

2. Web Serial API 简介

Web Serial API 为网站提供了一种使用脚本对串行设备执行读写操作的方式。该 API 允许网站与串行设备(例如微控制器和 3D 打印机)通信,从而将网络与现实世界联系起来。

有很多使用 Web 技术构建的控制软件示例。例如:

在某些情况下,这些网站会通过用户手动安装的原生代理应用与设备通信。在其他情况下,应用会通过 Electron 等框架以打包的原生应用形式提供。在其他情况下,用户需要执行额外的步骤,例如使用 USB 闪存盘将已编译的应用复制到设备。

通过在网站与其控制的设备之间提供直接通信,可以改善用户体验。

3. 准备工作

获取代码

我们已将您完成此 Codelab 所需的一切都放入一个 Glitch 项目中。

  1. 打开新的浏览器标签页,然后转到 https://web-serial-codelab-start.glitch.me/
  2. 点击 Remix Glitch 链接,创建您自己的起始项目版本。
  3. 点击显示按钮,然后选择在新窗口中,查看代码的实际效果。

4. 打开串行连接

检查 Web Serial API 是否受支持

首先需要检查当前浏览器是否支持 Web Serial API。为此,请检查 serial 是否在 navigator 中。

DOMContentLoaded 事件中,将以下代码添加到您的项目中:

script.js - DOMContentLoaded

// CODELAB: Add feature detection here.
const notSupported = document.getElementById('notSupported');
notSupported.classList.toggle('hidden', 'serial' in navigator);

这会检查是否支持 Web Serial。如果支持,此代码会隐藏显示“网络串行不受支持”的横幅。

试试看

  1. 加载页面。
  2. 确认页面上未显示红色横幅,其中指出不支持 Web Serial。

打开串行端口

接下来,我们需要打开串行端口。与大多数其他现代 API 一样,Web Serial API 是异步的。这可以防止界面在等待输入时阻塞,但这也很重要,因为网页可能会随时接收串行数据,而我们需要一种监听数据的方法。

由于一台计算机可能有多个串行设备,因此当浏览器尝试请求端口时,它会提示用户选择要连接的设备。

将以下代码添加到您的项目中:

script.js - connect()

// CODELAB: Add code to request & open port here.
// - Request a port and open a connection.
port = await navigator.serial.requestPort();
// - Wait for the port to open.
await port.open({ baudRate: 9600 });

requestPort 调用会提示用户选择要连接到的设备。调用 port.open 会打开端口。我们还需要提供与串行设备通信的速度。BBC micro:bit 在 USB 转串行芯片和主处理器之间使用 9600 波特的连接。

我们还要连接连接按钮,使其在用户点击它时调用 connect()

将以下代码添加到您的项目中:

script.js - clickConnect()

// CODELAB: Add connect code here.
await connect();

试试看

现在,我们的项目已经具备了开始运行所需的最低要求。点击 Connect 按钮后,系统会提示用户选择要连接的串行设备,然后连接到 micro:bit。

  1. 重新加载页面。
  2. 点击连接按钮。
  3. 在“串行端口选择器”对话框中,选择 BBC micro:bit 设备,然后点击连接
  4. 在该标签页上,您应该会看到一个表示您已连接到串行设备的图标:

e695daf2277cd3a2.png

设置输入流以监听串行端口中的数据

连接建立后,我们需要设置输入流和读取器,以便从设备读取数据。首先,我们将通过调用 port.readable 从端口获取可读数据流。由于我们知道会从设备收到文本,因此我们会通过文本解码器将其管道化。接下来,我们将获取一个读取器并开始读取循环。

将以下代码添加到您的项目中:

script.js - connect()

// CODELAB: Add code to read the stream here.
let decoder = new TextDecoderStream();
inputDone = port.readable.pipeTo(decoder.writable);
inputStream = decoder.readable;

reader = inputStream.getReader();
readLoop();

读取循环是一个异步函数,它会在循环中运行并等待内容,而不会阻塞主线程。当新数据到达时,该读取器会返回两个属性:valuedone 布尔值。如果 done 为 true,则表示端口已关闭,或者没有更多数据传入。

将以下代码添加到您的项目中:

script.js - readLoop()

// CODELAB: Add read loop here.
while (true) {
  const { value, done } = await reader.read();
  if (value) {
    log.textContent += value + '\n';
  }
  if (done) {
    console.log('[readLoop] DONE', done);
    reader.releaseLock();
    break;
  }
}

试试看

我们的项目现在可以连接到设备,并将从设备接收的所有数据附加到日志元素。

  1. 重新加载页面。
  2. 点击连接按钮。
  3. 在“串行端口选择器”对话框中,选择 BBC micro:bit 设备,然后点击连接
  4. 您应该会看到 Espruino 徽标:

dd52b5c37fc4b393.png

设置输出流以将数据发送到串行端口

串行通信通常是双向的。除了从串行端口接收数据外,我们还希望向端口发送数据。与输入流一样,我们只会通过输出流向 micro:bit 发送文本。

首先,创建一个文本编码器流,并将该流通过管道传送到 port.writeable

script.js - connect()

// CODELAB: Add code setup the output stream here.
const encoder = new TextEncoderStream();
outputDone = encoder.readable.pipeTo(port.writable);
outputStream = encoder.writable;

通过串行与 Espruino 固件连接时,BBC micro:bit 开发板充当 JavaScript 读取-评估-输出循环 (REPL),类似于您在 Node.js shell 中获取的内容。接下来,我们需要提供一种向数据流发送数据的方法。以下代码会从输出流中获取写入者,然后使用 write 发送每行。发送的每一行都包含一个新行字符 (\n),用于告知 micro:bit 对发送的命令进行评估。

script.js - writeToStream()

// CODELAB: Write to output stream
const writer = outputStream.getWriter();
lines.forEach((line) => {
  console.log('[SEND]', line);
  writer.write(line + '\n');
});
writer.releaseLock();

为了将系统置于已知状态并停止向我们发回所发送的字符,我们需要发送 CTRL-C 并关闭回显。

script.js - connect()

// CODELAB: Send CTRL-C and turn off echo on REPL
writeToStream('\x03', 'echo(false);');

试试看

现在,我们的项目可以向 micro:bit 发送和接收数据了。让我们验证一下是否可以正确发送命令:

  1. 重新加载页面。
  2. 点击连接按钮。
  3. 在串行端口选择器对话框中,选择 BBC micro:bit 设备,然后点击连接
  4. 在 Chrome 开发者工具中打开控制台标签页,然后输入 writeToStream('console.log("yes")');

您应该会在页面上看到类似以下内容的输出:

15e2df0064b5de28.png

5. 控制 LED 矩阵

构建矩阵网格字符串

如需控制 micro:bit 上的 LED 矩阵,我们需要调用 show()。此方法在内置的 5x5 LED 屏幕上显示图形。此参数接受二进制数或字符串。

我们将迭代复选框,并生成一个由 1 和 0 组成的数组,用于指示哪些复选框处于选中状态,哪些复选框处于未选中状态。然后,我们需要对数组进行反转,因为复选框的顺序与矩阵中的 LED 顺序相反。接下来,我们将数组转换为字符串并创建命令以发送到 micro:bit。

script.js - sendGrid()

// CODELAB: Generate the grid
const arr = [];
ledCBs.forEach((cb) => {
  arr.push(cb.checked === true ? 1 : 0);
});
writeToStream(`show(0b${arr.reverse().join('')})`);

连接复选框以更新矩阵

接下来,我们需要监听复选框的更改,如果发生更改,则将相关信息发送到 micro:bit。在特征检测代码 (// CODELAB: Add feature detection here.) 中,添加以下代码行:

script.js - DOMContentLoaded

initCheckboxes();

我们还可以在 micro:bit 首次连接时重置网格,以便它显示笑脸。drawGrid() 函数已提供。此函数的工作方式与 sendGrid() 类似;它接受由 1 和 0 组成的数组,并视情况选中复选框。

script.js - clickConnect()

// CODELAB: Reset the grid on connect here.
drawGrid(GRID_HAPPY);
sendGrid();

试试看

现在,当该页面打开与 micro:bit 的连接时,它会发送一个笑脸。点击复选框将更新 LED 矩阵上的显示内容。

  1. 重新加载页面。
  2. 点击连接按钮。
  3. 在串行端口选择器对话框中,选择 BBC micro:bit 设备,然后点击连接
  4. 您应该会在 micro:bit LED 矩阵上看到一个笑脸。
  5. 通过更改复选框,在 LED 矩阵上绘制不同的图案。

6. 连接 micro:bit 按钮

在 micro:bit 按钮上添加 watch 事件

micro:bit 上有两个按钮,LED 矩阵的两侧各一个。Espruino 提供了一个 setWatch 函数,用于在按下按钮时发送事件/回调。由于我们要监听这两个按钮,因此要将函数设为泛型,并让它输出事件的详细信息。

script.js - watchButton()

// CODELAB: Hook up the micro:bit buttons to print a string.
const cmd = `
  setWatch(function(e) {
    print('{"button": "${btnId}", "pressed": ' + e.state + '}');
  }, ${btnId}, {repeat:true, debounce:20, edge:"both"});
`;
writeToStream(cmd);

接下来,每次将串行端口连接到设备时,我们都需要连接 micro:bit 板上名为 BTN1 和 BTN2 的两个按钮。

script.js - clickConnect()

// CODELAB: Initialize micro:bit buttons.
watchButton('BTN1');
watchButton('BTN2');

试试看

按下 micro:bit 上的任一按钮时,您不仅会在连接时显示笑脸,还会向页面添加说明按下的按钮的文字。每个字符都可能各占一行。

  1. 重新加载页面。
  2. 点击连接按钮。
  3. 在“串行端口选择器”对话框中,选择 BBC micro:bit 设备,然后点击连接
  4. 您应该会在 micro:bit 的 LED 矩阵上看到一个笑脸。
  5. 按下 micro:bit 上的按钮,验证是否向按下按钮详细信息的页面附加了新文本。

7. 使用转换流解析传入数据

基本数据流处理

当按下其中一个 micro:bit 按钮时,micro:bit 会通过流将数据发送到串行端口。数据流非常有用,但也可能带来挑战,因为您不一定会一次性获得所有数据,并且数据可能会被任意分块。

目前,应用会在传入的流到达时输出它(在 readLoop 中)。在大多数情况下,每个字符各占一行,但这样做不是很有用。理想情况下,应将数据流解析为单独的行,并将每条消息显示为单独的行。

使用 TransformStream 转换数据流

为此,我们可以使用转换流 (TransformStream),以便解析传入流并返回解析后的数据。转换流可以位于流源(在本例中为 micro:bit)与使用该流的任何资源(在本例中为 readLoop)之间,并且可以在最终使用之前应用任意转换。不妨将其想象成一条装配线:随着微件沿着装配线移动,装配线中的每个步骤都会修改微件,以便在微件到达最终目的地时,它已成为一个功能完善的微件。

如需了解详情,请参阅 MDN 的 Streams API 概念

使用 LineBreakTransformer 转换数据流

我们来创建一个 LineBreakTransformer 类,该类将接受一个数据流,并根据换行符 (\r\n) 将其分块。该类需要两个方法,即 transformflush。每当数据流收到新数据时,系统都会调用 transform 方法。它可以将数据加入队列,也可以保存数据以备后用。关闭流时,系统会调用 flush 方法,该方法会处理任何尚未处理的数据。

transform 方法中,我们要将新数据添加到 container,然后检查 container 中是否有任何换行符。如果有,请将其拆分为数组,然后迭代行,调用 controller.enqueue() 以发送解析的行。

script.js - LineBreakTransformer.transform()

// CODELAB: Handle incoming chunk
this.container += chunk;
const lines = this.container.split('\r\n');
this.container = lines.pop();
lines.forEach(line => controller.enqueue(line));

当数据流关闭时,我们只需使用 enqueue 清空容器中任何剩余的数据。

script.js - LineBreakTransformer.flush()

// CODELAB: Flush the stream.
controller.enqueue(this.container);

最后,我们需要通过新的 LineBreakTransformer 来对传入的流进行管道处理。我们的原始输入流仅通过 TextDecoderStream 传递,因此我们需要添加额外的 pipeThrough,以通过新的 LineBreakTransformer 传递。

script.js - connect()

// CODELAB: Add code to read the stream here.
let decoder = new TextDecoderStream();
inputDone = port.readable.pipeTo(decoder.writable);
inputStream = decoder.readable
  .pipeThrough(new TransformStream(new LineBreakTransformer()));

试试看

现在,当您按下 micro:bit 的任意按钮时,系统应返回一行数据。

  1. 重新加载页面。
  2. 点击连接按钮。
  3. 在串行端口选择器对话框中,选择 BBC micro:bit 设备,然后点击连接
  4. 您应该会在 micro:bit LED 矩阵上看到一个笑脸。
  5. 按 micro:bit 上的按钮,然后验证您是否看到如下内容:

eead3553d29ee581.png

使用 JSONTransformer 转换数据流

我们可以尝试在 readLoop 中将字符串解析为 JSON,但我们不妨创建一个非常简单的 JSON 转换器,将数据转换为 JSON 对象。如果数据不是有效的 JSON,则仅返回传入的内容。

script.js - JSONTransformer.transform

// CODELAB: Attempt to parse JSON content
try {
  controller.enqueue(JSON.parse(chunk));
} catch (e) {
  controller.enqueue(chunk);
}

接下来,在流经过 LineBreakTransformer 后,通过 JSONTransformer 来传输流。这样,我们可以使 JSONTransformer 变得简单,因为我们知道 JSON 只会在一行中发送。

script.js - connect

// CODELAB: Add code to read the stream here.
let decoder = new TextDecoderStream();
inputDone = port.readable.pipeTo(decoder.writable);
inputStream = decoder.readable
  .pipeThrough(new TransformStream(new LineBreakTransformer()))
  .pipeThrough(new TransformStream(new JSONTransformer()));

试试看

现在,当您按下其中一个 micro:bit 按钮时,您应该会看到页面上输出了 [object Object]

  1. 重新加载页面。
  2. 点击连接按钮。
  3. 在串行端口选择器对话框中,选择 BBC micro:bit 设备,然后点击连接
  4. 您应该会在 micro:bit LED 矩阵上看到一个微笑。
  5. 按 micro:bit 上的按钮,然后验证您是否看到如下内容:

响应按钮按下动作

如需响应 micro:bit 按钮按下操作,请更新 readLoop 以检查它收到的数据是否为具有 button 属性的 object。然后,调用 buttonPushed 来处理按钮按下动作。

script.js - readLoop()

const { value, done } = await reader.read();
if (value && value.button) {
  buttonPushed(value);
} else {
  log.textContent += value + '\n';
}

按下 micro:bit 按钮后,LED 矩阵上的显示屏应会发生变化。使用以下代码设置矩阵:

script.js - buttonPushed()

// CODELAB: micro:bit button press handler
if (butEvt.button === 'BTN1') {
  divLeftBut.classList.toggle('pressed', butEvt.pressed);
  if (butEvt.pressed) {
    drawGrid(GRID_HAPPY);
    sendGrid();
  }
  return;
}
if (butEvt.button === 'BTN2') {
  divRightBut.classList.toggle('pressed', butEvt.pressed);
  if (butEvt.pressed) {
    drawGrid(GRID_SAD);
    sendGrid();
  }
}

试试看

现在,当您按下 micro:bit 的任意一个按钮时,LED 矩阵应该会变为笑脸或哭脸。

  1. 重新加载页面。
  2. 点击连接按钮。
  3. 在“串行端口选择器”对话框中,选择 BBC micro:bit 设备,然后点击连接
  4. 您应该会在 micro:bits LED 矩阵上看到微笑。
  5. 按 micro:bit 上的按钮,并验证 LED 矩阵是否发生变化。

8. 关闭串行端口

最后一步是连接断开连接功能,以便在用户完成操作后关闭端口。

在用户点击“连接/断开连接”按钮时关闭端口

当用户点击 Connect/Disconnect 按钮时,我们需要关闭连接。如果端口已打开,请调用 disconnect() 并更新界面,以表明该网页已不再连接到串行设备。

script.js - clickConnect()

// CODELAB: Add disconnect code here.
if (port) {
  await disconnect();
  toggleUIConnected(false);
  return;
}

关闭流和端口

disconnect 函数中,我们需要关闭输入流、关闭输出流并关闭端口。如需关闭输入流,请调用 reader.cancel()。对 cancel 的调用是异步进行的,因此我们需要使用 await 来等待调用完成:

script.js - disconnect()

// CODELAB: Close the input stream (reader).
if (reader) {
  await reader.cancel();
  await inputDone.catch(() => {});
  reader = null;
  inputDone = null;
}

如需关闭输出流,请获取 writer,调用 close(),并等待 outputDone 对象关闭:

script.js - disconnect()

// CODELAB: Close the output stream.
if (outputStream) {
  await outputStream.getWriter().close();
  await outputDone;
  outputStream = null;
  outputDone = null;
}

最后,关闭串行端口并等待其关闭:

script.js - disconnect()

// CODELAB: Close the port.
await port.close();
port = null;

试试看

现在,您可以随意打开和关闭串行端口。

  1. 重新加载页面。
  2. 点击连接按钮。
  3. 在“串行端口选择器”对话框中,选择 BBC micro:bit 设备,然后点击连接
  4. 您应该会在 micro:bit LED 矩阵上看到一个微笑
  5. Disconnect(断开连接)按钮,验证 LED 矩阵关闭且控制台中没有错误。

9. 恭喜

恭喜!您已成功构建您的第一个使用 Web Serial API 的 Web 应用。

请关注 https://goo.gle/fugu-api-tracker,及时了解 Web Serial API 以及 Chrome 团队正在开发的所有其他令人兴奋的新 Web 功能的最新动态。