使用 Web Serial API 在浏览器中实现串口通讯(纯前端)

目的

串口是非常常用的一种电脑与设备交互的接口。目前在浏览器上直接使用电脑上的串口设备了,这篇文章将介绍相关内容。

相关资料

Web Serial API 相关内容参考如下:

https://developer.mozilla.org/en-US/docs/Web/API/Serial
https://developer.mozilla.org/en-US/docs/Web/API/SerialPort
https://wicg.github.io/serial/

这个API目前还处于实验性质,只有电脑上的Chrome、Edge、Opera等浏览器支持:

另外还需要注意的是从网页操作设备是比较容易产生安全风险的,所以这个API只支持本地调用或者是HTTPS方式调用。

对于这个API谷歌有提供示例工程:

在线使用:https://googlechromelabs.github.io/serial-terminal/
项目地址:https://github.com/GoogleChromeLabs/serial-terminal

下面这个项目做的挺不错的,直接拿来用也很好:

在线使用:https://itldg.github.io/web-serial-debug/
项目地址:https://gitee.com/itldg/web-serial-debug or https://github.com/itldg/web-serial-debug

使用说明

使用下面方法可以侦测电脑上串口设备插入与拔出:

1
2
3
4
5
6
7
8
9
10
11
12
// 全局串口设备插入事件
navigator.serial.onconnect = (event) => {
console.log("Serial connected: ", event.target);
};

// 全局串口设备拔出事件
navigator.serial.ondisconnect = (event) => {
console.log("Serial disconnected: ", event.target);
};

// 也可以对单个的串口设备设置插入与拔出事件

使用下面方法可以显示电脑上的串口设备选择授权,或者显示已授权的串口设备列表:

1
2
3
4
5
6
7
8
9
// requestPort方法将显示一个包含已连接设备列表的对话框,用户选择可以并授予其中一个设备访问权限
// 对于USB虚拟串口而言该方法还可以传入一个过滤器,指定PID&VID的串口
const port = await navigator.serial.requestPort();
// port.forget(); // 取消授权
// port.getInfo() // 获取PID&VID (对于蓝牙串口好像是显示服务号)

// getDevices方法可以返回已连接的授权过的设备列表
const ports = await navigator.serial.getPorts();

使用 open 方法打开选中的串口设备后就可以进行数据交互了:

1
2
3
4
5
6
7
8
9
10
// open时可以传入串口参数
await port.open({
baudRate: 115200,
// bufferSize: 255, // 读写缓存,默认255
// dataBits: 8, // 数据位,默认8
// flowControl: none, // 流控制,默认无
// parity: none, // 校验,默认无
// stopBits: 1, // 停止位,默认1
});

打开后就可以发送数据了:

1
2
3
4
5
6
7
const encoder = new TextEncoder();
// const data= new Uint8Array(length);
const writer = port.writable.getWriter();
await writer.write(encoder.encode("PING"));
// await writer.write(data);
writer.releaseLock();

同样可以设置数据接收:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
while (port.readable) {
const reader = port.readable.getReader();
try {
while (true) {
const { value, done } = await reader.read();
if (done) {
// |reader| has been canceled.
break;
}
// Do something with |value|…
}
} catch (error) {
// Handle |error|…
} finally {
reader.releaseLock();
}
}

数据接收本身很简单,但需要注意的是在关闭串口前需要释放 reader 对象。

下面是关闭串口操作:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
// 使用 await port.close(); 即可关闭串口,如果正在读写数据,需要先释放相关资源

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| has been canceled.
break;
}
// Do something with |value|...
}
} catch (error) {
// Handle |error|...
} finally {
reader.releaseLock();
}
}

await port.close();
}

const closed = readUntilClosed();

// Sometime later...
keepReading = false;
reader.cancel();
await closed;

除了上面内容外还可以使用 setSignals 和 getSignals 来设置和获取流控制情况。

代码与演示

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
<!DOCTYPE html>
<html lang="en">

<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Web Serial API Test</title>
<style>
* {
margin: 0;
padding: 0;
}

button,textarea {
margin: 1rem;
margin-bottom: 0;
padding: 0.5rem;
width: 20rem;
}

textarea {
resize: none;
overflow-y: scroll;
overflow-x: hidden;
height: 5rem;
}
</style>
<script>
if ("serial" in navigator) {
// alert("Your browser support Web Serial API."); // 浏览器不支持 Web Serial API
} else {
alert("Your browser is not support Web Serial API.");
}

// 全局串口设备插入事件
navigator.serial.onconnect = (event) => {
console.log("Serial port connected: ", event.target);
};

// 全局串口设备拔出事件
navigator.serial.ondisconnect = (event) => {
console.log("Serial port disconnected: ", event.target);
};
</script>
</head>

<body>
<button id="btnSelect">select</button><br>
<button id="btnOpen">open</button><br>
<button id="btnClose">close</button><br>
<button id="btnSend">send</button><br>
<textarea id="iptOutput">D0 D1 D2 D3 D4 D5 D6 D7</textarea><br>
<textarea id="iptInput" readonly></textarea>
<script>
const btnSelect = document.querySelector("#btnSelect");
const btnOpen = document.querySelector("#btnOpen");
const btnClose = document.querySelector("#btnClose");
const btnSend = document.querySelector("#btnSend");
const iptOutput = document.querySelector("#iptOutput");
const iptInput = document.querySelector("#iptInput");

let port = null;
let reader = null;
let reading = false;

// 选择串口
btnSelect.onclick = async () => {
try {
port = await navigator.serial.requestPort(); // 弹出系统串口列表对话框,选择一个串口进行连接

let ports = await navigator.serial.getPorts(); // 获取已连接的授权过的设备列表
console.log(ports);

// await port.forget(); // 取消授权

// console.log(port.getInfo()); // 打印PID&VID (对于蓝牙串口好像是显示服务号)

} catch (e) {
console.log(e); // The prompt has been dismissed without selecting a device.
}
};

function updateInputData(data) {
let array = new Uint8Array(data); // event.data.buffer就是接收到的inputreport包数据了
let hexstr = "";
for (const data of array) {
hexstr += (Array(2).join(0) + data.toString(16).toUpperCase()).slice(-2) + " "; // 将字节数据转换成(XX )形式字符串
}
iptInput.value += hexstr;
iptInput.scrollTop = iptInput.scrollHeight; // 滚动到底部
}

// 读取数据
async function listenReceived() {
if (reading) {
console.log("On reading.");
return;
}
reading = true;

while (port.readable && reading) {
reader = port.readable.getReader();
try {
while (true) {
const { value, done } = await reader.read();
if (done) {
// |reader| has been canceled.
break;
}
// 需要特别注意的是:实际使用中即使对端是按一个个包发送的串口数据,接收时收到的也可能是分多段收到的
updateInputData(value);
}
} catch (e) {
console.log(e);
} finally {
reader.releaseLock();
}
}

await port.close(); // 关闭串口
port = null;
console.log("Port closed.");
}

// 打开串口
btnOpen.onclick = async () => {
if (port === null) {
console.log("Not selected.");
return;
}
await port.open({
baudRate: 115200,
// bufferSize: 255, // 读写缓存,默认255
// dataBits: 8, // 数据位,默认8
// flowControl: none, // 流控制,默认无
// parity: none, // 校验,默认无
// stopBits: 1, // 停止位,默认1
});
listenReceived();
console.log("Port opened.");
}

// 关闭串口
btnClose.onclick = async () => {
if ((port === null) || (!port.writable)) {
console.log("Not opened.");
return;
}

if (reading) {
reading = false;
reader?.cancel();
}
}

// 获取发送窗口十六进制字符串转换为字节数组
function getOutputData() {
let outputDatastr = iptOutput.value.replace(/\s+/g, ""); // 去除所有空白字符
if (outputDatastr.length % 2 == 0 && /^[0-9a-fA-F]+$/.test(outputDatastr)) {
// 获取字节数组长度
const byteLength = outputDatastr.length / 2;
// 创建字节数组
const outputData = new Uint8Array(byteLength);
// 将字符串转成字节数组数据
for (let i = 0; i < byteLength; i++) {
outputData[i] = parseInt(outputDatastr.substr(i * 2, 2), 16);
}
// 返回数据
return outputData;
} else {
throw "Data is not even or 0-9、a-f、A-F";
}
}

// 发送数据
btnSend.onclick = async () => {
if ((port === null) || (!port.writable)) {
console.log("Not opened.");
return;
}
const writer = port.writable.getWriter();
await writer.write(getOutputData()); // 发送数据
writer.releaseLock();
}

</script>
</body>

</html>

下面测试时我将串口的TX/RT短接在一起,发送什么数据就会收到什么数据:

总结

使用 Web Serial API 访问串口非常方便,目前来说唯一的问题是这还是实验性质的功能,可能之后接口还会变动,需要根据实际情况进行调整。


相关链接(侵删)

  1. 使用 Web Serial API 在浏览器中实现串口通讯(纯前端)

=================我是分割线=================

欢迎到公众号来唠嗑: