文章目录
目的
串口是非常常用的一种电脑与设备交互的接口。目前在浏览器上直接使用电脑上的串口设备了,这篇文章将介绍相关内容。
相关资料
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
使用说明
使用下面方法可以侦测电脑上串口设备插入与拔出:
// 全局串口设备插入事件 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 访问串口非常方便,目前来说唯一的问题是这还是实验性质的功能,可能之后接口还会变动,需要根据实际情况进行调整。