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

Naisu Xu 2024-07-14 11:03:03 阅读 82

文章目录

目的相关资料使用说明代码与演示总结

目的

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

相关资料

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

在这里插入图片描述

使用说明

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

<code>// 全局串口设备插入事件

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">code>

<head>

<meta charset="UTF-8">code>

<meta name="viewport" content="width=device-width, initial-scale=1.0">code>

<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>code>

<button id="btnOpen">open</button><br>code>

<button id="btnClose">close</button><br>code>

<button id="btnSend">send</button><br>code>

<textarea id="iptOutput">D0 D1 D2 D3 D4 D5 D6 D7</textarea><br>code>

<textarea id="iptInput" readonly></textarea>code>

<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 访问串口非常方便,目前来说唯一的问题是这还是实验性质的功能,可能之后接口还会变动,需要根据实际情况进行调整。



声明

本文内容仅代表作者观点,或转载于其他网站,本站不以此文作为商业用途
如有涉及侵权,请联系本站进行删除
转载本站原创文章,请注明来源及作者。