基于 Web HID API 的HID透传测试工具(纯前端)

前言

最近在搞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表示不使用reportId
          iptLog.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方式调用。

  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

Naisu Xu

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值