Web Serial API 允许网站与串行设备通信,Web Serial API 是功能项目的一部分,在 Chrome 89 中推出。
什么是网络串行 API?
串行端口是一种双向通信接口,允许逐字节发送和接收数据。Web Serial API 为网站提供了一种使用 JavaScript 读取和写入串行设备的方法。串行设备通过用户系统上的串行端口或通过模拟串行端口的可移动 USB 和蓝牙设备连接。 换句话说,Web Serial API 通过允许网站与串行设备(例如微控制器和 3D 打印机)进行通信,从而在 Web 和物理世界之间架起了一座桥梁。 此 API 也是 WebUSB 的绝佳伴侣,因为操作系统要求应用程序使用其高级串行 API 而不是低级 USB API 与某些串行端口进行通信。
推荐用例
在教育、业余爱好者和工业部门,用户将外围设备连接到他们的计算机。这些设备通常由微控制器通过定制软件使用的串行连接进行控制。一些控制这些设备的定制软件是使用网络技术构建的:
- Arduino 创建
- Betaflight 配置器
- Espruino 网络集成开发环境
- 微软 MakeCode 在某些情况下,网站通过用户手动安装的代理应用程序与设备通信。在其他情况下,应用程序通过 Electron 等框架在打包的应用程序中交付。而在其他情况下,用户需要执行额外的步骤,例如通过 USB 闪存驱动器将已编译的应用程序复制到设备。 在所有这些情况下,通过在网站和它所控制的设备之间提供直接通信,将改善用户体验。
当前状态
阶段 | 状态 |
---|---|
创建解释器 | 已完成 |
创建规范的初始草案 | 已完成 |
收集反馈并迭代设计 | 已完成 |
测试 | 已完成 |
发布 | 已完成 |
使用网络串行 API
特征检测
要检查是否支持 Web Serial API:
if ("serial" in navigator) {
// The Web Serial API is supported.
}
打开串口
Web Serial API 在设计上是异步的。这可以防止网站 UI 在等待输入时阻塞,这很重要,因为串行数据可以随时接收,需要一种方法来收听它。 要打开串行端口,首先访问一个 SerialPort 对象.为此可以通过调用 navigator.serial.requestPort() 以响应用户对话框(例如触摸或鼠标单击)来提示用户选择单个串行端口,或者从 navigator.serial.getPorts() 中选择一个,它返回网站已被授予访问权限的串行端口列表。
document.querySelector('button').addEventListener('click', async () => {
// Prompt user to select any serial port.
const port = await navigator.serial.requestPort();
});
// Get all serial ports the user has previously granted the website access to.
const ports = await navigator.serial.getPorts();
navigator.serial.requestPort() 函数接受一个可选的对象字面量来定义过滤器.这些用于将通过 USB 连接的任何串行设备与强制性 USB 供应商 (usbVendorId) 和可选的 USB 产品标识符 (usbProductId) 相匹配。
// Filter on devices with the Arduino Uno USB Vendor/Product IDs.
const filters = [
{ usbVendorId: 0x2341, usbProductId: 0x0043 },
{ usbVendorId: 0x2341, usbProductId: 0x0001 }
];
// Prompt user to select an Arduino Uno device.
const port = await navigator.serial.requestPort({ filters });
const { usbProductId, usbVendorId } = port.getInfo();
选择 BBC micro:bit 的用户提示
调用 requestPort() 提示用户选择一个设备并返回一个 SerialPort 对象.一旦你有了一个 SerialPort 对象,以所需的波特率调用 port.open() 将打开串口. baudRate成员指定通过串行线路发送数据的速度,它以每秒位数 (bps) 为单位表示。检查您设备的文档以获取正确的值,因为如果指定不正确,发送和接收的所有数据都将是乱码.对于某些模拟串行端口的 USB 和蓝牙设备,此值可以安全地设置为任何值,因为它会被模拟忽略。
// Prompt user to select any serial port.
const port = await navigator.serial.requestPort();
// Wait for the serial port to open.
await port.open({ baudRate: 9600 });
可以在打开串行端口时指定以下任何选项,这些选项是可选的,并且具有方便的默认值:
- dataBits:每帧的数据位数(7 或 8)
- stopBits:帧末尾的停止位数(1 或 2)
- parity:奇偶校验模式(none-无、even-偶数或 odd-奇数)
- bufferSize:应创建的读写缓冲区的大小(必须小于 16MB)
- flowControl:流量控制模式(“none”或“hardware”)
从串口读取
Web Serial API 中的输入和输出流由 Streams API 处理。如果不熟悉流,请查看 Streams API 概念。本文仅涉及流和流处理的皮毛。 建立串行端口连接后,SerialPort 对象的可读和可写属性返回一个 ReadableStream 和一个 WritableStream,这些将用于从串行设备接收数据和向串行设备发送数据。两者都使用 Uint8Array 实例进行数据传输。 当新数据从串行设备到达时,port.readable.getReader().read() 异步返回两个属性:value和done布尔值,如果done为真,串口已经关闭或者没有数据进来。调用 port.readable.getReader() 创建一个reader并锁定它的可读性,可读被锁定时,串口不能关闭。
const reader = port.readable.getReader();
// Listen to data coming from the serial device.
while (true) {
const { value, done } = await reader.read();
if (done) {
// Allow the serial port to be closed later.
reader.releaseLock();
break;
}
// value is a Uint8Array.
console.log(value);
}
在缓冲区溢出、帧错误或奇偶校验错误等某些情况下,可能会发生一些非致命的串行端口读取错误。这些作为异常抛出,可以通过在前一个检查 port.readable 的循环之上添加另一个循环来捕获。这是可行的,因为只要错误不是致命的,就会自动创建一个新的 ReadableStream。如果发生致命错误,例如串行设备被移除,则 port.readable 变为 null。
while (port.readable) {
const reader = port.readable.getReader();
try {
while (true) {
const { value, done } = await reader.read();
if (done) {
// Allow the serial port to be closed later.
reader.releaseLock();
break;
}
if (value) {
console.log(value);
}
}
} catch (error) {
// TODO: Handle non-fatal read error.
}
}
如果串行设备发回文本,您可以通过 TextDecoderStream 管道 port.readable ,如下所示,TextDecoderStream 是一个转换流,它获取所有 Uint8Array 块并将它们转换为字符串。
const textDecoder = new TextDecoderStream();
const readableStreamClosed = port.readable.pipeTo(textDecoder.writable);
const reader = textDecoder.readable.getReader();
// Listen to data coming from the serial device.
while (true) {
const { value, done } = await reader.read();
if (done) {
// Allow the serial port to be closed later.
reader.releaseLock();
break;
}
// value is a string.
console.log(value);
}
当您使用“Bring Your Own Buffer”(BYOB)阅读器从流中读取时,您可以控制内存的分配方式.调用 port.readable.getReader({ mode: "byob" }) 获取 ReadableStreamBYOBReader 接口,并在调用 read() 时提供自己的 ArrayBuffer.请注意,Web Serial API 在 Chrome 106 或更高版本中支持此功能。
try {
const reader = port.readable.getReader({ mode: "byob" });
// Call reader.read() to read data into a buffer...
} catch (error) {
if (error instanceof TypeError) {
// BYOB readers are not supported.
// Fallback to port.readable.getReader()...
}
}
下面是一个如何重用 value.buffer 中的缓冲区的示例:
const bufferSize = 1024; // 1kB
let buffer = new ArrayBuffer(bufferSize);
// Set `bufferSize` on open() to at least the size of the buffer.
await port.open({ baudRate: 9600, bufferSize });
const reader = port.readable.getReader({ mode: "byob" });
while (true) {
const { value, done } = await reader.read(new Uint8Array(buffer));
if (done) {
break;
}
buffer = value.buffer;
// Handle `value`.
}
这是另一个如何从串行端口读取特定数量数据的示例:
async function readInto(reader, buffer) {
let offset = 0;
while (offset < buffer.byteLength) {
const { value, done } = await reader.read(
new Uint8Array(buffer, offset)
);
if (done) {
break;
}
buffer = value.buffer;
offset += value.byteLength;
}
return buffer;
}
const reader = port.readable.getReader({ mode: "byob" });
let buffer = new ArrayBuffer(512);
// Read the first 512 bytes.
buffer = await readInto(reader, buffer);
// Then read the next 512 bytes.
buffer = await readInto(reader, buffer);
写入串口
要将数据发送到串行设备,请将数据传递给 port.writable.getWriter().write()。需要在 port.writable.getWriter() 上调用 releaseLock() 才能稍后关闭串口。
const writer = port.writable.getWriter();
const data = new Uint8Array([104, 101, 108, 108, 111]); // hello
await writer.write(data);
// Allow the serial port to be closed later.
writer.releaseLock();
通过管道传输到 port.writable 的 TextEncoderStream 将文本发送到设备,如下所示。
const textEncoder = new TextEncoderStream();
const writableStreamClosed = textEncoder.readable.pipeTo(port.writable);
const writer = textEncoder.writable.getWriter();
await writer.write("hello");
关闭串口
如果其可读和可写成员已解锁,即reader和writer已经各自调用releaseLock(),则port.close() 会关闭串行端口。
await port.close();
但是,当使用循环从串行设备连续读取数据时,port.readable 将一直被锁定,直到遇到错误.在这种情况下,调用 reader.cancel() 将强制 reader.read() 立即解析为 { value: undefined, done: true } 并因此允许循环调用 reader.releaseLock()。
// Without transform streams.
let keepReading = true;
let reader;
async function readUntilClosed() {
while (port.readable && keepReading) {
reader = port.readable.getReader();
try {
while (true) {
const { value, done } = await reader.read();
if (done) {
// reader.cancel() has been called.
break;
}
// value is a Uint8Array.
console.log(value);
}
} catch (error) {
// Handle error...
} finally {
// Allow the serial port to be closed later.
reader.releaseLock();
}
}
await port.close();
}
const closedPromise = readUntilClosed();
document.querySelector('button').addEventListener('click', async () => {
// User clicked a button to close the serial port.
keepReading = false;
// Force reader.read() to resolve immediately and subsequently
// call reader.releaseLock() in the loop example above.
reader.cancel();
await closedPromise;
});
使用转换流(如 TextDecoderStream 和 TextEncoderStream)时,关闭串行端口会更加复杂。像以前一样调用 reader.cancel() 。然后调用 writer.close() 和 port.close()。这通过转换流将错误传播到底层串行端口。因为错误传播不会立即发生,所以您需要使用之前创建的 readableStreamClosed 和 writableStreamClosed 承诺来检测 port.readable 和 port.writable 何时被解锁。取消读取器会导致流中止;这就是为什么您必须捕获并忽略由此产生的错误。
// With transform streams.
const textDecoder = new TextDecoderStream();
const readableStreamClosed = port.readable.pipeTo(textDecoder.writable);
const reader = textDecoder.readable.getReader();
// Listen to data coming from the serial device.
while (true) {
const { value, done } = await reader.read();
if (done) {
reader.releaseLock();
break;
}
// value is a string.
console.log(value);
}
//
const textEncoder = new TextEncoderStream();
const writableStreamClosed = textEncoder.readable.pipeTo(port.writable);
reader.cancel();
await readableStreamClosed.catch(() => { /* Ignore the error */ });
writer.close();
await writableStreamClosed;
await port.close();
监听连接和断开
如果 USB 设备提供串行端口,则该设备可以连接到系统或从系统断开连接。当网站被授予访问串行端口的权限时,它应该监视连接和断开事件。
navigator.serial.addEventListener("connect", (event) => {
// TODO: Automatically open event.target or warn user a port is available.
});
navigator.serial.addEventListener("disconnect", (event) => {
// TODO: Remove |event.target| from the UI.
// If the serial port was opened, a stream error would be observed as well.
});
在 Chrome 89 之前,连接和断开连接事件会触发自定义 SerialConnectionEvent 对象,受影响的 SerialPort 接口可用作端口属性。你可能想使用 event.port || event.target 来处理转换。
处理信号
建立串口连接后,可以显式查询和设置串口暴露的信号,用于设备检测和流量控制。这些信号被定义为布尔值。例如,如果切换数据终端就绪 (DTR) 信号,某些设备(如 Arduino)将进入编程模式。 设置输出信号和获取输入信号分别通过调用 port.setSignals() 和 port.getSignals() 来完成。请参阅下面的用法示例。
/ Turn off Serial Break signal.
await port.setSignals({ break: false });
// Turn on Data Terminal Ready (DTR) signal.
await port.setSignals({ dataTerminalReady: true });
// Turn off Request To Send (RTS) signal.
await port.setSignals({ requestToSend: false });
const signals = await port.getSignals();
console.log(`Clear To Send: ${signals.clearToSend}`);
console.log(`Data Carrier Detect: ${signals.dataCarrierDetect}`);
console.log(`Data Set Ready: ${signals.dataSetReady}`);
console.log(`Ring Indicator: ${signals.ringIndicator}`);
转换流
从串行设备接收数据时,不一定会一次获得所有数据。它可以任意分块。有关详细信息,请参阅流 API 概念。
为了解决这个问题,您可以使用一些内置的转换流,例如 TextDecoderStream 或创建您自己的转换流,它允许解析传入的流并返回解析后的数据。转换流位于串行设备和使用该流的读取循环之间,它可以在使用数据之前应用任意转换。把它想象成一条装配线:当一个小部件从生产线上下来时,生产线中的每一步都会修改小部件,因此当它到达最终目的地时,它就是一个功能齐全的小部件
二战城堡布罗姆维奇飞机厂
例如,考虑如何创建一个转换流类,该类使用流并根据换行符对其进行分块.每次流接收到新数据时都会调用其 transform() 方法。它可以将数据排入队列或将其保存以备后用。流关闭时调用 flush() 方法,它处理任何尚未处理的数据。
要使用转换流类,您需要通过它传输传入流。在从串行端口读取下的第三个代码示例中,原始输入流仅通过 TextDecoderStream 进行管道传输,因此我们需要调用 pipeThrough() 将其通过我们新的管道传输LineBreakTransformer
class LineBreakTransformer {
constructor() {
// A container for holding stream data until a new line.
this.chunks = "";
}
transform(chunk, controller) {
// Append new chunks to existing chunks.
this.chunks += chunk;
// For each line breaks in chunks, send the parsed lines out.
const lines = this.chunks.split("\r\n");
this.chunks = lines.pop();
lines.forEach((line) => controller.enqueue(line));
}
flush(controller) {
// When the stream is closed, flush any remaining chunks out.
controller.enqueue(this.chunks);
}
}
const textDecoder = new TextDecoderStream();
const readableStreamClosed = port.readable.pipeTo(textDecoder.writable);
const reader = textDecoder.readable
.pipeThrough(new TransformStream(new LineBreakTransformer()))
.getReader();
要调试串行设备通信问题,请使用 port.readable 的 tee() 方法来拆分进出串行设备的流。创建的两个流可以独立使用,这允许您将一个流打印到控制台以供检查
const [appReadable, devReadable] = port.readable.tee();
// You may want to update UI with incoming data from appReadable
// and log incoming data in JS console for inspection from devReadable.
撤销对串行端口的访问
该网站可以通过在 SerialPort 实例上调用 forget() 来清除访问它不再感兴趣的串行端口的权限,例如,对于在具有许多设备的共享计算机上使用的教育 Web 应用程序,大量累积的用户生成的权限会造成糟糕的用户体验。
// Voluntarily revoke access to this serial port.
await port.forget();
由于 forget() 在 Chrome 103 或更高版本中可用,请检查以下是否支持此功能:
if ("serial" in navigator && "forget" in SerialPort.prototype) {
// forget() is supported.
}
开发技巧
使用内部页面 about://device-log 在 Chrome 中调试 Web Serial API 很容易,可以在一个地方看到所有与串行设备相关的事件。
Chrome 中用于调试 Web Serial API 的内部页面。
插上usb设备,或者拔掉设备时会输出如下日志:
USBUser[23:30:12] USB device added: vendor=1659 "Prolific Technology Inc.", product=8963 "USB-Serial Controller", serial="", guid=0e0195a0-08dd-4e21-8692-7d9c8838fc73
SerialEvent[23:30:12] Serial device added: dialin=/dev/tty.usbserial-1410 callout=/dev/cu.usbserial-1410 vid=067B pid=2303 usb_serial=(none) usb_driver=com.apple.driver.AppleUSBPLCOM
USBUser[23:29:44] USB device removed: guid=99a43714-9296-4246-901b-b988e4021314
SerialDebug[23:29:44] Failed to flush port: Device not configured (6)
SerialEvent[23:29:44] Serial device removed: path=/dev/cu.usbserial-1410
代码实验室
在 Google Developer Codelab 中,您将使用 Web Serial API 与 BBC micro:bit 板进行交互,以在其 5x5 LED 矩阵上显示图像。
浏览器支持
Web Serial API 在 Chrome 89 的所有桌面平台(ChromeOS、Linux、macOS 和 Windows)上可用。
Polyfill
在 Android 上,可以使用 WebUSB API 和 Serial API polyfill 支持基于 USB 的串行端口。此 polyfill 仅限于可通过 WebUSB API 访问设备的硬件和平台,因为它尚未被内置设备驱动程序声明。
安全和隐私
规范作者使用控制对强大 Web 平台功能的访问中定义的核心原则设计和实现了 Web 串行 API,包括用户控制、透明度和人体工程学。使用此 API 的能力主要受权限模型的限制,该模型一次仅授予对单个串行设备的访问权限。为响应用户提示,用户必须采取主动步骤来选择特定的串行设备。 要了解安全权衡,请查看 Web Serial API Explainer 的安全和隐私部分。
示例
Serial Terminal WebSerial Espruino Web IDE
本文由 至简 创作,采用 知识共享署名4.0 国际许可协议进行许可
本站文章除注明转载/出处外,均为本站原创或翻译,转载前请务必署名
最后编辑时间为:
2023/02/07 12:37