前言
最近在搞HID透传 《STM32 USB使用记录:HID类设备(后篇)》 。
市面上的各种测试工具都或多或少存在问题,所以就自己写一个工具进行测试。目前来说纯前端方案编写这个工具应该是最方便的,这里放上相关代码。
项目地址与代码示例
项目地址:https://github.com/NaisuXu/HID_Passthrough_Tool
下面代码保存到 index.html
文件,双击打开文件即可使用:
<!DOCTYPE html>
<html lang="en"><head><meta charset="UTF-8" /><meta name="viewport" content="width=device-width, initial-scale=1.0" /><title>HID Passthrough Tool</title><style>* {margin: 0;padding: 0;}html,body {height: 100vh;background-color: #f7f7ff;}div {height: calc(100% - 4rem);padding: 2rem;display: grid;grid-template-columns: 1fr 1fr 1fr;grid-template-rows: 2rem 1fr;row-gap: 1rem;column-gap: 2rem;}textarea {resize: none;overflow-y: scroll;overflow-x: hidden;padding: 1rem;}</style><script>if ("hid" in navigator) {// 浏览器支持hid} else {alert("Browser is not supported Web HID API.");}</script></head><body><div><button id="btnOpen">open</button><button id="btnSend">send</button><button id="btnClear">clear</button><textarea id="iptLog" readonly></textarea><textarea id="iptOutput">D0 D1 D2 D3 D4 D5 D6 D7</textarea><textarea id="iptInput" readonly></textarea></div><script>const btnOpen = document.querySelector("#btnOpen");const btnSend = document.querySelector("#btnSend");const btnClear = document.querySelector("#btnClear");const iptLog = document.querySelector("#iptLog");const iptOutput = document.querySelector("#iptOutput");const iptInput = document.querySelector("#iptInput");iptLog.value += "HID Passthrough Tool\n\n";iptLog.value += "This is an HID Passthrough device read/write Tool.\n\n";iptLog.value += "Device must have one collection with one input and one output.\n\n";iptLog.value += "For more detail see below:\n\n";iptLog.value += "https://github.com/NaisuXu/HID_Passthrough_Tool\n\n";iptLog.value += "《STM32 USB使用记录:HID类设备(后篇)》\nhttps://blog.csdn.net/Naisu_kun/article/details/131880999\n\n";iptLog.value += "《使用 Web HID API 在浏览器中进行HID设备交互(纯前端)》\nhttps://blog.csdn.net/Naisu_kun/article/details/132539918\n\n";let device; // 需要连接或已连接的设备let inputDataLength; // 发送数据包长度let outputDataLength; // 发送数据包长度// 打开设备相关操作btnOpen.onclick = async () => {try {// requestDevice方法将显示一个包含已连接设备列表的对话框,用户选择可以并授予其中一个设备访问权限const devices = await navigator.hid.requestDevice({ filters: [] });// const devices = await navigator.hid.requestDevice({// filters: [{// vendorId: 0xabcd, // 根据VID进行过滤// productId: 0x1234, // 根据PID进行过滤// usagePage: 0x0c, // 根据usagePage进行过滤// usage: 0x01, // 根据usage进行过滤// },],// });// let devices = await navigator.hid.getDevices(); // getDevices方法可以返回已连接的授权过的设备列表if (devices.length == 0) {iptLog.value += "No device selected\n\n";iptLog.scrollTop = iptLog.scrollHeight; // 滚动到底部return;}device = devices[0]; // 选择列表中第一个设备if (!device.opened) {// 检查设备是否打开await device.open(); // 打开设备// 下面几行代码和我的自定义的透传的HID设备有关// 我的设备中有一个collection,其中有一个input、一个output// inputReports和outputReports数据是Array,reportSize是8// reportCount表示一包数据的字节数,USB-FS 和 USB-HS 设置的reportCount最大值不同if (device.collections[0].inputReports[0].items[0].isArray && device.collections[0].inputReports[0].items[0].reportSize === 8) {// 发送数据包长度必须和报告描述符中描述的一致inputDataLength = device.collections[0].inputReports[0].items[0].reportCount ?? 0;}if (device.collections[0].outputReports[0].items[0].isArray && device.collections[0].outputReports[0].items[0].reportSize === 8) {// 发送数据包长度必须和报告描述符中描述的一致outputDataLength = device.collections[0].outputReports[0].items[0].reportCount ?? 0;}iptLog.value += `Open device: \n${device.productName}\nPID-${device.productId} VID-${device.vendorId}\ninputDataLength-${inputDataLength} outputDataLength-${outputDataLength}\n\n`;iptLog.scrollTop = iptLog.scrollHeight; // 滚动到底部}// await device.close(); // 关闭设备// await device.forget() // 遗忘设备// 电脑接收到来自设备的消息回调device.oninputreport = (event) => {console.log(event); // event中包含device、reportId、data等内容let array = new Uint8Array(event.data.buffer); // 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; // 滚动到底部iptLog.value += `Received ${event.data.byteLength} bytes\n\n`;iptLog.scrollTop = iptLog.scrollHeight; // 滚动到底部};} catch (error) {iptLog.value += `${error}\n\n`;iptLog.scrollTop = iptLog.scrollHeight; // 滚动到底部}};// 发送数据相关操作btnSend.onclick = async () => {try {if (!device?.opened) {throw "Device not opened";}const outputData = new Uint8Array(outputDataLength); // 要发送的数据包let outputDatastr = iptOutput.value.replace(/\s+/g, ""); // 去除所有空白字符if (outputDatastr.length % 2 == 0 && /^[0-9a-fA-F]+$/.test(outputDatastr)) {// 检查长度和字符是否正确// 一包长度不能大于报告描述符中规定的长度const byteLength = outputDatastr.length / 2 > outputDataLength ? outputDataLength : outputDatastr.length / 2;// 将字符串转成字节数组数据for (let i = 0; i < byteLength; i++) {outputData[i] = parseInt(outputDatastr.substr(i * 2, 2), 16);}} else {throw "Data is not even or 0-9、a-f、A-F";}await device.sendReport(0, outputData); // 发送数据,第一个参数为reportId,填0表示不使用reportIdiptLog.value += `Send ${outputData.length} bytes\n\n`;iptLog.scrollTop = iptLog.scrollHeight; // 滚动到底部} catch (error) {iptLog.value += `${error}\n\n`;iptLog.scrollTop = iptLog.scrollHeight; // 滚动到底部}};// 全局HID设备插入事件navigator.hid.onconnect = (event) => {console.log("HID connected: ", event.device); // device 的 collections 可以看到设备报告描述符相关信息iptLog.value += `HID connected:\n${event.device.productName}\nPID ${event.device.productId} VID ${event.device.vendorId}\n\n`;iptLog.scrollTop = iptLog.scrollHeight; // 滚动到底部};// 全局HID设备拔出事件navigator.hid.ondisconnect = (event) => {device = null; // 释放当前设备iptLog.value += `HID disconnected:\n${event.device.productName}\nPID ${event.device.productId} VID ${event.device.vendorId}\n\n`;iptLog.scrollTop = iptLog.scrollHeight; // 滚动到底部};// 清空数据接收窗口btnClear.onclick = () => {iptInput.value = "";};</script></body></html>
注意事项
Web HID API 目前还处于实验性质,只有电脑上的Chrome、Edge、Opera等浏览器支持:
另外还需要注意的是从网页操作设备是比较容易产生安全风险的,所以这个API只支持本地调用或者是HTTPS方式调用。