Appearance
Web串口通信
Web Serial API 是一套允许网站从串行设备通过脚本读取和写入的方式微控制器、3D 打印机和其他串行设备等设备进行通信的一套 API。
要注意的是, Web Serial API 比较新, 建议只在最新版谷歌浏览器使用。同时出于安全考虑,Web Serial API 只被允许运行在使用 https 的网站上,一些方法的调用必须通过用户操作执行。
支持检测
检查浏览器是否支持 Web Serial API:
if ("serial" in navigator) {
// 浏览器支持串口通信
}
1
2
3
2
3
打开串口
requestPort 方法来提示用户选择一个串口,或者使用 getPorts 方法从先前授予过访问权限的串口列表中选择一个。
// 提示用户选择一个串口
const port = await navigator.serial.requestPort();
// 获取用户之前授予该网站访问权限的所有串口
const ports = await navigator.serial.getPorts();
// 打开串口
await port.open({
dataBits: 8, // 数据位
stopBits: 1, // 停止位
parity: "none", // 奇偶校验
baudRate: 9600, // 波特率
});
1
2
3
4
5
6
7
8
9
10
11
12
2
3
4
5
6
7
8
9
10
11
12
读取数据
Web Serial API 中的输入和输出流由 streams API 处理。
串口打开成功之后,SerialPort 对象的 readable 和 writable 属性返回一个 ReadableStream 和一个 WritableStream用于串口通信交互传输数据。Web Serial API 使用 Uint8Array 实例进行数据传输。
使用 port.readable.getReader 来初始化获取一个数据读取器。初始化之后,这个数据读取器会被锁定为 readable。此时串口不能关闭,直到数据读取器的 releaseLock 方法被调用。
当接收到串口数据时,调用数据读取器的 read 方法会异步返回两个属性:
- value(Uint8Array)
- done (Boolean)
如果 done 为 true,表示此时串口没有正在接收的数据传入或串口已经关闭:
const reader = port.readable.getReader();
// 监听来自串口的数据
while (true) {
const { value, done } = await reader.read();
if (done) {
// 允许稍后关闭串口
reader.releaseLock();
break;
}
// value 是一个 Uint8Array
console.log(value);
}
1
2
3
4
5
6
7
8
9
10
11
12
2
3
4
5
6
7
8
9
10
11
12
使用 String.fromCharCode 方法可以处理 Uint8Array 为 字符串。
写入数据
和读取数据类似,写入数据也需要初始化一个数据读取器,同时要将数据转为 Uint8Array。
await writer.write(
new Uint8Array("hello".split("").map((s) => s.charCodeAt(0)))
);
1
2
3
2
3
串口关闭
使用 port 的 close 方法可以关闭串口,前提是串口的 readable 和 writable 被解锁,调用其上的 releaseLock 方法即可解锁。
当想要在在串口接受数据过程中关闭串口,需要调用 reader 的 cancel 方法将 reader.read()的返回值变为为 {value: undefined, done: true},从而允许调用 reader.releaseLock。最后调用 port 的 close 方法。
Electron
在 Electron 中使用 Web Serial API 需要在主进程监听渲染进程 webContents.session 的 select-serial-port 事件处理返回一个 portId。这一步相当于用户手动选择串口设备。
与 8051 单片机通信
这里实现了一个可以接收和发送串口数据的 8051 单片机和 Web 交互示例,实现了 Web 驱动单片机蜂鸣器和单片机独立按键状态反馈至 Web 页面效果:
C:
#include <regx52.h>
void InitPort()
{
SCON = 0X50;
TMOD = 0X20;
PCON = 0X00;
TH1 = 0Xfd;
TL1 = 0Xfd;
ES = 1;
EA = 1;
TR1 = 1;
}
void delay(unsigned int xms)
{
unsigned int i, j;
for (i = xms; i > 0; i--)
for (j = 112; j > 0; j--);
}
void main()
{
InitPort();
do
{
if (P3_4 == 0) {
delay(1);
SBUF = '1';
break;
}
if (P3_5 == 0) {
delay(1);
SBUF = '2';
break;
}
if (P3_6 == 0) {
delay(1);
SBUF = '3';
break;
}
if (P3_7 == 0) {
delay(1);
SBUF = '4';
break;
}
SBUF = '0';
} while (1);
}
void uart() interrupt 4
{
if (RI == 1)
{
RI = 0;
P2_3 = 0;
delay(50);
P2_3 = 1;
delay(50);
P2_3 = 0;
delay(50);
P2_3 = 1;
delay(50);
P2_3 = 0;
delay(50);
P2_3 = 1;
delay(50);
}
}
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
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
HTML:
<!DOCTYPE html>
<html lang="zh">
<head>
<meta charset="UTF-8" />
<meta http-equiv="X-UA-Compatible" content="IE=edge" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>Document</title>
<style>
html,
body {
height: 100%;
user-select: none;
}
.flex {
display: flex;
align-items: center;
justify-content: center;
}
.flexc {
flex-direction: column;
}
.k {
width: 66px;
height: 66px;
color: #000;
margin: 0 4px;
font-size: 30px;
font-weight: bold;
border-radius: 4px;
background: #fff;
border: 1px solid #aaa;
}
</style>
</head>
<body class="flex flexc">
<button id="open">打开串口</button>
<br />
<button id="close" style="display: none">关闭串口</button>
<br />
<button id="ring" style="display: none">蜂鸣器</button>
<br />
<div id="key" class="flex">
<div id="k1" class="flex k">K2</div>
<div id="k2" class="flex k">K3</div>
<div id="k3" class="flex k">K4</div>
<div id="k4" class="flex k">K5</div>
</div>
<script>
const open = document.getElementById("open");
const ring = document.getElementById("ring");
const close = document.getElementById("close");
const ks = Array.from(document.getElementById("key").children);
let port,
reader,
writer,
opened = false;
const render = (value) => {
ks.forEach((k) => {
k.style.color = "#000";
k.style.background = "#fff";
});
if (value && !isNaN(value)) {
ks[value - 1].style.color = "#fff";
ks[value - 1].style.background = "#f00";
}
};
const init = (_port) => {
if (!_port) {
return;
}
port = _port;
close.style.display = ring.style.display = "block";
port.open({ baudRate: 9600 }).then(async () => {
opened = true;
reader = port.readable.getReader();
writer = port.writable.getWriter();
while (port.readable && opened) {
while (true) {
let { value, done } = await reader.read();
if (done) {
reader.releaseLock();
writer.releaseLock();
port.close();
break;
}
render(+String.fromCharCode(value));
}
}
});
};
navigator.serial
.getPorts()
.then(([port]) => {
init(port);
})
.catch(console.log);
open.onclick = () => {
navigator.serial
.requestPort()
.then(init)
.catch(console.log);
};
close.onclick = () => {
opened = false;
ring.style.display = close.style.display = "none";
reader.cancel();
};
ring.onclick = () => {
writer.write(new Uint8Array("0".split("").map((s) => s.charCodeAt(0))));
};
</script>
</body>
</html>
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
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