什么,网页也能直接与硬件通信?Web Serial API!|8月更文挑战

13,771 阅读9分钟

8月更文挑战第二篇,冲冲冲

往期文章

  1. 更强大的文件接口,File System Access API|8月更文挑战 (juejin.cn)

前言

上一篇更强大的文件接口,File System Access API|8月更文挑战 (juejin.cn)介绍了WICGFile-System-Access,这次再来介绍另一个好玩的接口 Web Serial API。它能够使得我们网站拥有读取和写入串行设备的能力,例如单片机、蓝牙、打印机等等。总之就是帮我们搭起了网络和物理世界的桥梁。

来自官方文档的介绍:

The Serial API provides a way for websites to read and write from a serial device through script. Such an API would bridge the web and the physical world, by allowing documents to communicate with devices such as microcontrollers, 3D printers, and other serial devices.

Web Serial API 为网站提供了一种通过脚本从串行设备读取和写入的方法。 通过允许文档与微控制器、3D 打印机和其他串行设备等设备进行通信,这样的 API 将连接网络和物理世界。

玩过单片机或者开发过单片机设备的朋友都知道串口调试,一般都是用串口线或者串口转usb线,一头连板子一头连主机,然后主机上通过串口调试软件来进行通信,而串口调试软件需要使用系统底层能力,一般使用c/c++/c#或者electron来编写,而有了Web Serial API,使得网页串口调试唾手可得!

想尝鲜的可以登录这个网站 Serial Terminal,这是谷歌chrome实验室基于Web Serial API开发的Demo,一个网页版串口调试助手,但功能不多。

8-2-1.png

下面我将逐个使用该API提供的方法实现类似的功能,并且结合Stm32F407开发板来实现一个实时光照强度可视化的demo

开始

环境搭建

在开发过程中我们需要真实串口设备来提供数据,没有设备的话可以使用虚拟设备仿真,串口使用Virtual Serial Port Driver软件来创建虚拟串口,再利用串口调试助手来模拟设备发送数据。

第一次使用我们还要看下浏览器是否支持此功能,不支持的话升级到最新版本就行了,仅限Chrome以及Edge。控制台输入navigator.serial,只要不是undefined就没问题。

打开串口

通过调用navigator.serial.requestPort()方法来选择我们要打开的串口,由于串行数据可以随时接受,为了避免阻塞页面,API被设计成异步的,因此它的方法都被Promise化。

// 选择要打开的串口
const port = await navigator.serial.requestPort();

8-2-2.png

调用后会弹出一个对话框来让我们选择串口设备,之后会返回一个SerialPort 对象,里面包括连接状态以及一系列数据交换的方法。

8-2-3.png

如果我们之前在当前浏览器已经打开过一些串口设备,下次使用时无需再次选择,可以直接获得

// 返回一个数组,里面是之前访问过的串口
const ports = await navigator.serial.getPorts();

有了串口对象,调用port.open(),并传入相应的串口设置参数就能连接上了。这里参数主要有波特率、停止位、数据位、奇偶校验等,和串口调试助手的差不多。

8-2-4.png

其中波特率特别重要,设置错的话收到的数据就是一堆乱码,所以要与单片机串口波特率一致,但是对于模拟串口等仿真设备来说,可以是任意值。其他参数字段如下

  • dataBits: 数据位,值为7或8.
  • stopBits: 停止位,值为1或2.
  • parity: 是否进行奇偶校验, "none"不使用, "even"偶校验 、 "odd"奇校验.
  • bufferSize: 缓冲区大小 ,必须小于16MB.
  • flowControl: 流控制, "none" 或者 "hardware".
// 选择串口
const port = await navigator.serial.requestPort();
// 打开串口,这里我的设备波特率是57600
await port.open({ baudRate: 57600 });

读取数据

串口连接成功后,SerialPort对象的readablewritable分别是一个ReadableStreamWritableStream对象,要了解这2个对象,可以参阅 (Streams API 概念 - Web API 接口参考 | MDN (mozilla.org)

当监听到设备发送了数据时,readable.getReader().read()会异步返回2个属性:value done,分别代表数据值以及发送状态,跟ES6 的迭代器对象返回值差不多,这里的value使用Uint8Array对象来存储数据,如果donetrue,说明数据传输完毕。

调用port.readable.getReader()将串口变成锁定读写状态,当readable对象的lockedtrue时,串行端口不能被关闭。

8-2-5.png

异步IO存在很多不稳定因素,例如这里可能会发生缓冲区溢出、数据帧错误、奇偶校验失败或者设备溢出等等,因此我们加上错误处理,并且使用死循环来轮询数据接收,

下面是一个异步监听串口数据的栗子:

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) {
    // 错误处理
  }
}

控制台源源不断地打印数据,这里是我的板子每隔200ms会通过串口发送一次光照强度。

8-2-6.png 串口调试助手界面:

8-2-7.png

现在接收到的都是二进制流数据,我们得对它加以转换。使用Steam API下的对象都有建立管道的方法,通过管道我们就能将二进制流转换成字符串了,像在Nodejs中使用fs模块的管道读写操作差不多。

这里需要创建一个TextDecoderStream对象,通过管道连接串口的读取流和textDecoder的写入流来完成Uint8Array到字符串的转换。

关于TextDecoderStream 对象,它负责将二进制流进行转码,默认是转换成UTF-8的格式,通过传入构造参数来设置编码类型,有效的编码类型参考Encoding API Encodings - Web APIs | MDN (mozilla.org)

// 如果串口显示中文乱码,多半是编码格式错误的问题,这里编码格式选择’gb2312‘
const textDecoder = new TextDecoderStream('gb2312');
// 接收数据前先连接管道,这样就能边接收边转换了
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;
  }
  console.log(value);
}

Awesome!和串口调试助手一致。

8-2-8.png

发送数据

发送数据给设备也很简单,调用port.writable.getWriter()创建一个发射器,然后构建我们要发送的数据即可,数据必须是Uint8Array对象。

const writer = port.writable.getWriter();
// 下面数据分别对应hello的ascall码
const data = new Uint8Array([104, 101, 108, 108, 111]); 
// 发射!!
await writer.write(data);

// 与读取一样,发送数据也会锁定串口,所以发送数据后需要解锁
writer.releaseLock();

当然了,咱们不能啥数据都用ascall码表示,太麻烦了,咱们要所输即所得,在发送器前使用TextEncoderStream来帮助我们转码。

// 默认使用utf8
const textEncoder = new TextEncoderStream();
const writableStreamClosed = textEncoder.readable.pipeTo(port.writable);
const writer = textEncoder.writable.getWriter();
await writer.write("hello web serial!");

状态监听

我们可以监听设备的连接和断开来进行下一步操作。

// 监听设备连接
navigator.serial.addEventListener("connect", (event) => {
	console.log('设备已连接')
});
// 监听设备断开
navigator.serial.addEventListener("disconnect", (event) => {
	console.log('设备已断开')
});

关闭串口

当串口处于未锁定状态时,可以关闭它。

await port.close();

小demo

上面已经把Web Serial API的大致使用介绍的差不多了,更多高级用法请参考官方文档。

目前可以做一个小demo啦:基于Web Serial的实时光照强度可视化展示。使用一块STM32F407开发板,由于板子通过一枚usb转串口的芯片连接usb接口和处理器的USART引脚,因此使用usb数据线来代替串口线就能实现串口连接。相关光敏AD转换的代码已经烧录进去,只需完成网页部分即可。

8-2-9.jpg

板子原本是我用来搞指纹锁的,临时被用来演示,线太乱,请忽略哈😁😁。

网页网页分成两个部分,左边显示图表,右边显示串口数据,使用vue + echart搭建。界面代码就一百来行,因此就不贴代码了,使用上述介绍的接口功能完成。下面是演示效果:

8-2-10.gif

光敏电阻分别在正常光照、闪光灯、遮盖的情况下进行采样,网页也展示了相应的效果。

总结

总的来说,Web Serial API的是一个非常不错的设计,使得网页不再束缚于沙箱环境下运行,在得到用户的授权下,与现实物理设备进行双向通信,虽然在这之前浏览器已经能调用摄像头、麦克风以及生物特征设备等,但仍存在一定局限性。

目前,该API标准还在孵化中,但并不能说它不能运用在生产环境,正如Atwood's Law的一句话:

Any application that can be written in JavaScript, will eventually be written in JavaScript.

任何可以用JavaScript编写的应用程序,最终都会用JavaScript编写。

著名的Espruino - JavaScript for Microcontrollers 是一个微处理器的 JavaScript 解释器,使得在嵌入式设备上可以使用JavaScript来控制硬件,它的Espruino Web IDE可以在线编辑代码并下载到板子上,其中就使用了Web Serial API

8-2-11.png

除此之外,微软、Arduino也分别在他们的嵌入式设备在线编程网站上有所应用。

对于我们个人开发者,我们也可以利用它来做些有趣的事情,除了能来数据展示外,我们也能利用API现有能力开发一个较谷歌demo功能更完善的网页版串口调试工具。以往要根据需求定制化一个串口调试工具一般采用其他语言开发或electron,现在只需像普通网页一样,打包部署后,利用网页天然跨平台的特性进行多平台分发,亦或者利用PWA技术或者浏览器提供的网页应用安装功能作为个人和团队的小工具。

希望JavaScript不仅在前端,在嵌入式物联网领域也能开花结果。

参考