Web Serial API
本文主要是翻译:https://web.dev/serial/
该博客内容,用于学习该API的使用,并且用于项目需求开发。关注我公众号:小笑残虹,了解更多关于Web Serial API
的踩坑经验分享,视频分享版:https://www.bilibili.com/video/BV1N54y1L7Wz/
。
Web Serial API注意事项
有好多同学加我微信请教关于该API的问题,所以特此在此罗列一下大家遇到的问题:
- 该API是JS本身
navigator
对象上就独有的,所以与任何框架开发都没有太大的关系,不管你是用Vue还是React开发。 - 要使用该API需要服务器使用
https
协议,这也是为什么很多同学问我本地开发明明没有问题,部署到线上这个API就不管用的原因。同时呢,本地开发建议使用http://localhost:端口号。 - 遇到问题了,大家一定要先冷静,学着去分析原因,不要不思考,多读 web.dev/serial/ 这篇文章,我当时遇到问题就是读了好多遍这篇文章才解决问题的,相信大家也可以。
- 最后我的wx:xxch-168,或者公众号:小笑残虹,有问题大家一起交流。
什么是Web串行API?
串口是一个双向通信接口,允许字节发送和接收数据。
Web Serial API
为网站提供了一种使用JavaScript
对串行设备进行读写的方法。串行设备可以通过用户系统上的串行端口连接,也可以通过模拟串行端口的可移动USB和蓝牙设备连接。
换句话说,Web Serial API
通过允许网站与串行设备(如微控制器和3D打印机)通信来连接网络和物理世界。
这个API也是WebUSB
的好伙伴,因为操作系统要求应用程序使用它们的高级串行API而不是低级的USB API
与一些串行端口通信。
建议用例
在教育、业余爱好者和工业部门,用户连接外围设备到他们的计算机。这些设备通常由微控制器通过定制软件使用的串行连接来控制。一些控制这些设备的定制软件是通过网络技术构建的:
- Arduino Create
- Betaflight Configurator
- Espruino Web IDE
- Microsoft MakeCode
在某些情况下,网站通过用户手动安装的代理应用程序与设备通信。在其他情况下,应用程序是通过诸如Electron
这样的框架以打包应用程序的形式交付的。在其他情况下,用户需要执行额外的步骤,例如通过USB闪存驱动器将编译后的应用程序复制到设备上。
在所有这些情况下,通过提供网站与其控制的设备之间的直接交流,用户体验将得到改善。
如何使用Web Serial 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()
中选择一个,该方法返回一个先前授予该网站访问权限的串行端口列表。
// 提示用户选择一个串口。
const port = await navigator.serial.requestPort();
// 获取用户之前授予该网站访问权限的所有串口。
const ports = await navigator.serial.getPorts();
requestport()
函数接受一个可选的定义过滤器的对象字面量。它们用于将任何通过USB
连接的串行设备与强制USB
厂商(usbVendorId
)和可选USB产品标识符(usbProductId
)匹配。
// 过滤设备与Arduino Uno USB供应商/产品id。
const filters = [
{ usbVendorId: 0x2341, usbProductId: 0x0043 },
{ usbVendorId: 0x2341, usbProductId: 0x0001 }
];
// 提示用户选择Arduino Uno设备。
const port = await navigator.serial.requestPort({ filters });
const { usbProductId, usbVendorId } = port.getInfo();
调用requestPort()
提示用户选择一个设备并返回一个SerialPort
对象。一旦你有了一个SerialPort
对象,用期望的波特率调用port.open()
将打开串口。baudRate
字典成员指定通过串行线发送数据的速度。它以每秒比特(bps
)为单位表示。检查您的设备的文档为正确的值,因为所有的数据发送和接收将是乱码,如果这是指定不正确。对于一些模拟串行端口的USB和蓝牙设备,这个值可以安全地设置为任何值,因为它会被模拟忽略。
// 提示用户选择一个串口
const port = await navigator.serial.requestPort();
// 等待串口打开
await port.open({ baudRate: 9600 });
您还可以在打开串行端口时指定下面的任何选项。这些选项是可选的,并且有方便的默认值。
dataBits
:每帧的数据位数(7或8)。stopBits
:一帧结束时的停止位数(1或2)。parity
:校验模式,可以是none,偶数,奇数。bufferSize
:应该创建的读写缓冲区大小(必须小于16MB)。flowControl
:流控模式(none或hardware)。
从串口读取
Web Serial API
中的输入和输出流由streams API
处理。
串口连接建立之后,SerialPort
对象的readable
和writable
属性返回一个ReadableStream
和一个WritableStream
。这些将用于从串行设备接收数据并将数据发送到串行设备。两者都使用Uint8Array
实例进行数据传输。
当新数据从串行设备到达时,port.readable.getReader().read()
异步返回两个属性:value
和一个done
的布尔值。如果done
为真,则串行端口已经关闭,或者没有更多的数据输入。调用port.readable.getReader()
创建一个读取器并将其锁定为readable
。当可读被锁定时,串口不能被关闭。
const reader = port.readable.getReader();
// 监听来自串行设备的数据
while (true) {
const { value, done } = await reader.read();
if (done) {
// 允许稍后关闭串口。
reader.releaseLock();
break;
}
// value 是一个 Uint8Array
console.log(value);
}
在某些情况下可能会发生一些非致命的串行端口读错误,如缓冲区溢出、帧错误或奇偶校验错误。这些是作为异常抛出的,可以通过在检查port.readable
的前一个循环之上添加另一个循环来捕获。这是可行的,因为只要错误是非致命的,一个新的ReadableStream
就会自动创建。如果发生致命错误,如串行设备被删除,则端口。可读的变成了零。
while (port.readable) {
const reader = port.readable.getReader();
try {
while (true) {
const { value, done } = await reader.read();
if (done) {
// 允许稍后关闭串口。
reader.releaseLock();
break;
}
if (value) {
console.log(value);
}
}
} catch (error) {
// TODO: 处理非致命的读错误。
}
}
如果串行设备发送文本返回,您可以管道端口。可通过TextDecoderStream
读取,如下所示。TextDecoderStream
是一个转换流,抓取所有的Uint8Array
块并将其转换为字符串。
const textDecoder = new TextDecoderStream();
const readableStreamClosed = port.readable.pipeTo(textDecoder.writable);
const reader = textDecoder.readable.getReader();
// 监听来自串行设备的数据。
while (true) {
const { value, done } = await reader.read();
if (done) {
// 允许稍后关闭串口。
reader.releaseLock();
break;
}
// value 是一个 string.
console.log(value);
}
写入串口
要将数据发送到串行设备,请将数据传递到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);
// 允许稍后关闭串口。
writer.releaseLock();
通过管道传输到端口的TextEncoderStream
向设备发送文本。port.writable
如下所示。
const textEncoder = new TextEncoderStream();
const writableStreamClosed = textEncoder.readable.pipeTo(port.writable);
const writer = textEncoder.writable.getWriter();
await writer.write("hello");
关闭串口
port.close()
如果串行端口的readable
和writable
被解锁,则关闭该串行端口,这意味着已经为其各自的读写成员调用了releaseLock()
。
await port.close();
但是,当使用循环从串行设备连续读取数据时,端口Readable
将一直被锁定,直到遇到错误。在这种情况下,调用reader.cancel()
将强制reader.read()
立即解析为{value: undefined, done: true}
,从而允许循环调用reader.releaseLock()
。
// 没有变换流。
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()。
break;
}
// value 是一个 Uint8Array.
console.log(value);
}
} catch (error) {
// 处理错误...
} finally {
// 允许稍后关闭串口。
reader.releaseLock();
}
}
await port.close();
}
const closedPromise = readUntilClosed();
document.querySelector('button').addEventListener('click', async () => {
// 用户单击按钮关闭串口。
keepReading = false;
// 强制reader.read()立即并随后解析
// 在上面的循环例子中调用reader.releaseLock()。
reader.cancel();
await closedPromise;
});
当使用转换流(如TextDecoderStream
和texttencoderstream
)时,关闭串口更复杂。像以前一样调用reader.cancel()
。然后调用writer.close()
和port.close()
。这会通过转换流将错误传播到底层串行端口。因为错误传播不会立即发生,所以需要使用前面创建的readableStreamClosed
和writableStreamClosed promise
来检测何时端口。可读的和端口。可写已解锁。取消读取器将导致流中止;这就是为什么您必须捕获并忽略产生的错误。
// 流与变换。
const textDecoder = new TextDecoderStream();
const readableStreamClosed = port.readable.pipeTo(textDecoder.writable);
const reader = textDecoder.readable.getReader();
// 监听来自串行设备的数据。
while (true) {
const { value, done } = await reader.read();
if (done) {
reader.releaseLock();
break;
}
// value 是一个 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: 自动打开事件。目标器或警告用户端口可用。
});
navigator.serial.addEventListener("disconnect", (event) => {
// TODO: Remove |event.target| from the UI.
// 如果打开了串行端口,还会观察到流错误。
});
在Chrome 89之前,连接和断开事件触发了一个自定义的SerialConnectionEvent
对象,受影响的SerialPort
接口作为端口属性可用。你可能想要使用event.port || event.target
事件。目标来处理转换。
处理信号
串口连接建立后,可以显式查询和设置串口暴露的信号,用于设备检测和流量控制。这些信号被定义为布尔值。例如,当DTR (Data Terminal Ready)
信号被切换时,一些设备(如Arduino)会进入编程模式。
设置输出信号和获取输入信号分别通过调用port.setSignals()
和port.getSignals()
来完成。参见下面的用法示例。
// 关闭串行中断信号。
await port.setSignals({ break: false });
// 打开DTR (Data Terminal Ready)信号。
await port.setSignals({ dataTerminalReady: true });
// 关闭发送请求(RTS)信号。
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
管道传输,因此我们需要调用pipe through()
将其通过新的LineBreakTransformer
管道传输。
class LineBreakTransformer {
constructor() {
// 保存流数据直到新行出现的容器。
this.chunks = "";
}
transform(chunk, controller) {
// 将新块追加到现有块。
this.chunks += chunk;
// 对于每一行分段,将解析后的行发送出去。
const lines = this.chunks.split("\r\n");
this.chunks = lines.pop();
lines.forEach((line) => controller.enqueue(line));
}
flush(controller) {
// 当流关闭时,清除所有剩余的块。
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
的tee()
方法。port.readable
,用于分割进出串行设备的流。创建的两个流可以独立使用,这允许您将其中一个打印到控制台以供检查。
const [appReadable, devReadable] = port.readable.tee();
// 你可能想用来自可理解的数据更新UI
// 并将传入的数据记录在JS控制台中,以便从devReadable检查。
Dev小贴士
在Chrome
中调试Web Serial API
很容易,有一个内部页面,Chrome://device-log
,你可以在一个地方看到所有串行设备相关的事件。
Codelab
在谷歌Developer
代码实验室中,您将使用Web Serial API
与BBC micro:bit
板交互,在其5x5 LED
矩阵上显示图像。
浏览器支持
Web Serial API
适用于Chrome 89
的所有桌面平台(Chrome OS、Linux、macOS和Windows)。
Polyfill
在Android
上,可以使用WebUSB API
和serial API polyfill
来支持基于usb
的串口。这个腻子仅限于通过WebUSB API
访问设备的硬件和平台,因为它没有被内置的设备驱动程序声明。
安全和隐私
该规范的作者使用控制对强大Web
平台特性的访问的核心原则设计并实现了Web Serial API
,这些核心原则包括用户控制、透明性和人体工程学。使用此API
的能力主要是由一次只授予访问单个串行设备的权限模型决定的。为了响应用户提示,用户必须采取主动步骤来选择特定的串行设备。
要了解安全性权衡,请查看Web Serial API
解释器的安全性和隐私部分。
今天的文章Web Serial API,web端通过串口与硬件通信分享到此就结束了,感谢您的阅读。
版权声明:本文内容由互联网用户自发贡献,该文观点仅代表作者本人。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如发现本站有涉嫌侵权/违法违规的内容, 请发送邮件至 举报,一经查实,本站将立刻删除。
如需转载请保留出处:https://bianchenghao.cn/18394.html