文章目录
目的
串口是非常常用的一种电脑与设备交互的接口。目前在浏览器上直接使用电脑上的串口设备了,这篇文章将介绍相关内容。
相关资料
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方式调用。
使用说明
使用下面方法可以侦测电脑上串口设备插入与拔出:
// 全局串口设备插入事件
navigator.serial.onconnect = (event) => {
console.log("Serial connected: ", event.target);
};
// 全局串口设备拔出事件
navigator.serial.ondisconnect = (event) => {
console.log("Serial disconnected: ", event.target);
};
// 也可以对单个的串口设备设置插入与拔出事件
使用下面方法可以显示电脑上的串口设备选择授权,或者显示已授权的串口设备列表:
// requestPort方法将显示一个包含已连接设备列表的对话框,用户选择可以并授予其中一个设备访问权限
// 对于USB虚拟串口而言该方法还可以传入一个过滤器,指定PID&VID的串口
const port = await navigator.serial.requestPort();
// port.forget(); // 取消授权
// port.getInfo() // 获取PID&VID (对于蓝牙串口好像是显示服务号)
// getDevices方法可以返回已连接的授权过的设备列表
const ports = await navigator.serial.getPorts();
使用 open 方法打开选中的串口设备后就可以进行数据交互了:
// open时可以传入串口参数
await port.open({
baudRate: 115200,
// bufferSize: 255, // 读写缓存,默认255
// dataBits: 8, // 数据位,默认8
// flowControl: none, // 流控制,默认无
// parity: none, // 校验,默认无
// stopBits: 1, // 停止位,默认1
});
打开后就可以发送数据了:
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();
同样可以设置数据接收:
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 对象。
下面是关闭串口操作:
// 使用 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 来设置和获取流控制情况。
代码与演示
<!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 访问串口非常方便,目前来说唯一的问题是这还是实验性质的功能,可能之后接口还会变动,需要根据实际情况进行调整。