JavaScript内核与高级编程之:`JavaScript` 的 `WebHID` API:其在 `JavaScript` 中与 `HID` 设备通信。

大家好,欢迎来到今天的“奇妙的WebHID世界”讲座!

今天咱们要聊的是一个相当硬核,但又充满可能性的东西——WebHID API。 别被“HID”这个缩写吓到,它可不是什么神秘组织,而是“Human Interface Device”的缩写,翻译过来就是“人机接口设备”。 简单说,就是那些让你能跟电脑互动的玩意儿,比如键盘、鼠标、游戏手柄,甚至是更奇葩的东西,比如自定义的按钮盒、扫描仪等等。

WebHID API,简单来说,就是让你的JavaScript代码可以直接跟这些HID设备“对话”。 以前,这事儿只能是桌面应用才能干的,现在浏览器也能插一脚了! 这就打开了很多扇脑洞大开的大门。

为什么要用WebHID?

你可能会问,键盘鼠标不都能用JS监听事件了吗?干嘛还要WebHID? 好问题!

  1. 标准事件不够用啊! 键盘鼠标的事件,Browser已经定义好了,比如keydown, click,但如果你的HID设备不是标准的键盘鼠标,事件就没法对应了。比如一个自定义的按钮盒,你按下一个特殊的按钮,JS就没法直接知道。WebHID 允许你直接读取设备发来的原始数据,自己解析。
  2. 独占设备。 有些设备,你希望浏览器独占使用,不要被操作系统拦截。比如,一个专业的游戏手柄,你希望所有的按键和摇杆数据都直接交给游戏处理,而不是被操作系统的一些快捷键给截胡了。
  3. 性能! 对于一些高频率的设备,比如VR/AR设备,WebHID可以直接读取设备的原始数据,绕过操作系统的驱动层,降低延迟,提高性能。

WebHID 的基本流程

要想用WebHID,大致需要经历以下几个步骤:

  1. 请求权限: 先跟用户打个招呼,问问能不能访问他们的HID设备。
  2. 选择设备: 列出所有可用的HID设备,让用户选择要连接哪个。
  3. 连接设备: 建立连接,准备收发数据。
  4. 收发数据: 从设备接收数据,或者向设备发送控制指令。
  5. 断开连接: 用完了,记得礼貌地断开连接。

代码时间!

光说不练假把式,咱们来点实际的。

1. 请求权限和选择设备

async function requestHID() {
  try {
    const devices = await navigator.hid.requestDevice({
      filters: [
        { usagePage: 0x0001, usage: 0x0006 } // 过滤条件:只显示键盘
        // 可以添加多个filter,比如
        // { vendorId: 0x1234, productId: 0x5678 } // 根据厂商ID和产品ID过滤
      ]
    });

    if (devices.length > 0) {
      const device = devices[0]; // 选择第一个设备
      console.log("已选择设备:", device);
      await connectDevice(device); // 连接设备
    } else {
      console.log("未选择任何设备");
    }
  } catch (error) {
    console.error("请求HID设备失败:", error);
  }
}

// 按钮点击事件,触发请求设备
const requestButton = document.getElementById('requestButton');
requestButton.addEventListener('click', requestHID);

这段代码做了几件事:

  • navigator.hid.requestDevice():这是WebHID的核心函数,会弹出一个对话框,让用户选择HID设备。
  • filters:这是一个过滤器,可以指定要显示的设备类型。 usagePageusage 是HID的规范,用来描述设备类型。 0x0001 代表通用桌面设备,0x0006 代表键盘。 你也可以根据 vendorId (厂商ID) 和 productId (产品ID) 来过滤特定设备。
  • await:注意这里用了await,因为requestDevice()是异步的,需要等待用户选择设备。
  • connectDevice(device):用户选择设备后,调用connectDevice()函数来连接设备。

2. 连接设备和接收数据

async function connectDevice(device) {
  try {
    await device.open(); // 打开设备连接
    console.log("设备已连接");

    device.addEventListener("inputreport", event => {
      const { data, device, reportId } = event;
      const uint8Array = new Uint8Array(data.buffer); // 将数据转换为 Uint8Array
      console.log("收到数据:", uint8Array, "Report ID:", reportId);
      processInputReport(uint8Array, reportId); // 处理收到的数据
    });

    device.addEventListener("disconnect", () => {
      console.log("设备已断开连接");
    });

  } catch (error) {
    console.error("连接设备失败:", error);
  }
}

function processInputReport(data, reportId) {
    // 根据 reportId 和 data 的内容,解析HID设备发来的数据
    // 具体的解析逻辑需要根据你的设备来定
    console.log("处理数据,reportId:", reportId, "data:", data);
    // 举例:如果 reportId 是 1, 并且 data[0] 是 0x01,则表示按钮A被按下
    if (reportId === 1 && data[0] === 0x01) {
        console.log("按钮A被按下");
    }
}

这段代码做了这些事:

  • device.open():打开设备连接,这是建立通信的关键一步。
  • device.addEventListener("inputreport", ...):监听inputreport事件,当设备发送数据过来时,这个事件会被触发。
  • event.data:包含了设备发来的原始数据,是一个DataView对象。
  • Uint8Array:为了方便处理,通常会将DataView转换为Uint8Array
  • reportId:有些HID设备会使用reportId来区分不同的数据类型。
  • processInputReport(data, reportId):这是一个自定义的函数,用来解析收到的数据。 具体的解析逻辑需要根据你的HID设备来定。 这也是最麻烦,也是最有意思的地方!
  • device.addEventListener("disconnect", ...):监听disconnect事件,当设备断开连接时,这个事件会被触发。

3. 发送数据到设备

async function sendOutputReport(device, reportId, data) {
  try {
    await device.sendReport(reportId, data);
    console.log("发送数据成功,Report ID:", reportId, "Data:", data);
  } catch (error) {
    console.error("发送数据失败:", error);
  }
}

// 举例:发送数据给设备,控制LED灯的亮度
const sendButton = document.getElementById('sendButton');
sendButton.addEventListener('click', async () => {
  if (device) { // 确保设备已连接
    const reportId = 0x02; // Report ID
    const brightness = 0x80; // 亮度值 (0-255)
    const data = new Uint8Array([brightness]); // 将亮度值转换为 Uint8Array
    await sendOutputReport(device, reportId, data);
  } else {
    console.log("请先连接设备");
  }
});

这段代码做了这些事:

  • device.sendReport(reportId, data):向设备发送数据,reportIddata 的含义取决于你的HID设备。
  • Uint8Array:发送的数据必须是Uint8Array类型的。

HID Report 描述符

要想真正玩转WebHID,你需要了解HID Report 描述符。 这玩意儿就像HID设备的“说明书”,描述了设备的功能、数据格式等等。

HID Report 描述符是用一种特殊的语言描述的,叫做“HID Report Descriptor Language”。 听起来很吓人,但其实就是一些预定义的关键字和数值。

举个例子,下面是一个简单的键盘的Report 描述符:

0x05, 0x01,        // Usage Page (Generic Desktop)
0x09, 0x06,        // Usage (Keyboard)
0xA1, 0x01,        // Collection (Application)
0x05, 0x07,        //   Usage Page (Key Codes)
0x19, 0xE0,        //   Usage Minimum (Keyboard LeftControl)
0x29, 0xE7,        //   Usage Maximum (Keyboard Right GUI)
0x15, 0x00,        //   Logical Minimum (0)
0x25, 0x01,        //   Logical Maximum (1)
0x75, 0x01,        //   Report Size (1)
0x95, 0x08,        //   Report Count (8)
0x81, 0x02,        //   Input (Data,Var,Abs,No Wrap,Linear,Preferred State,No Null Position)
0x95, 0x01,        //   Report Count (1)
0x75, 0x08,        //   Report Size (8)
0x81, 0x03,        //   Input (Const,Var,Abs,No Wrap,Linear,Preferred State,No Null Position)
0x95, 0x06,        //   Report Count (6)
0x75, 0x08,        //   Report Size (8)
0x15, 0x00,        //   Logical Minimum (0)
0x25, 0x65,        //   Logical Maximum (101)
0x05, 0x07,        //   Usage Page (Key Codes)
0x19, 0x00,        //   Usage Minimum (Reserved (no event indicated))
0x29, 0x65,        //   Usage Maximum (Keyboard Application)
0x81, 0x00,        //   Input (Data,Ary,Abs,No Wrap,Linear,Preferred State,No Null Position)
0xC0              // End Collection

是不是看得头皮发麻? 没关系,你不需要记住所有的细节。 你只需要知道,这个描述符告诉了你键盘的按键数量、数据格式等等。

怎么找到HID Report 描述符?

  • 设备文档: 有些设备会提供详细的文档,里面包含了Report 描述符。
  • 操作系统工具: Windows 下可以用 HIDView 工具,Mac 下可以用 IOJones 工具来查看设备的Report 描述符。
  • 在线数据库: 有一些在线的HID Report 描述符数据库,比如 usb.org

实战案例

咱们来几个实战案例,让你感受一下WebHID的威力。

1. 自定义游戏手柄

假设你有一个自定义的游戏手柄,上面有一些特殊的按钮和旋钮。 你可以用WebHID来读取这些按钮和旋钮的状态,然后控制游戏中的角色。

// 假设手柄的 Report 描述符如下:
// Report ID: 0x01
// Button 1: data[0] & 0x01
// Button 2: data[0] & 0x02
// Knob: data[1] (0-255)

function processInputReport(data, reportId) {
  if (reportId === 0x01) {
    const button1 = data[0] & 0x01;
    const button2 = data[0] & 0x02;
    const knob = data[1];

    console.log("Button 1:", button1 ? "Pressed" : "Released");
    console.log("Button 2:", button2 ? "Pressed" : "Released");
    console.log("Knob:", knob);

    // 根据按钮和旋钮的状态,更新游戏中的角色
    updateGameCharacter(button1, button2, knob);
  }
}

2. LED 控制器

假设你有一个LED控制器,可以用WebHID来控制LED的颜色和亮度。

// 假设LED控制器的 Report 描述符如下:
// Report ID: 0x02
// Red: data[0] (0-255)
// Green: data[1] (0-255)
// Blue: data[2] (0-255)

async function setLedColor(device, red, green, blue) {
  const reportId = 0x02;
  const data = new Uint8Array([red, green, blue]);
  await sendOutputReport(device, reportId, data);
}

// 设置LED颜色为红色
setLedColor(device, 255, 0, 0);

WebHID 的一些坑

WebHID 虽然强大,但也有些需要注意的地方:

  • 兼容性: WebHID 还在发展中,不是所有的浏览器都支持。 目前 Chrome 和 Edge 的支持比较好。
  • 安全性: 访问HID设备需要用户授权,浏览器会弹出对话框,询问用户是否允许访问。
  • 跨域: 如果你的网页是通过 HTTPS 访问的,那么你的HID设备也必须支持HTTPS,否则可能会出现跨域问题。
  • Report 描述符: 解析HID Report 描述符需要一定的功底,需要花时间学习和调试。

WebHID 的未来

WebHID 的潜力是巨大的。 它可以用于各种各样的应用,比如:

  • 游戏: 自定义游戏手柄、VR/AR 设备
  • 工业控制: 机器人、传感器
  • 医疗: 医疗设备
  • 教育: STEM 教育

随着WebHID 的不断发展,相信会有越来越多的创新应用涌现出来。

总结

今天咱们聊了WebHID API的基本概念、使用方法和一些实战案例。 希望你能从中受益,开启你的WebHID 探索之旅!

记住,WebHID 是一个强大的工具,但也是一个需要耐心和技巧的工具。 多尝试,多实践,你就能掌握它,创造出属于你的奇妙应用!

好了,今天的讲座就到这里。 谢谢大家! 别忘了,编程的乐趣在于探索和创造! 祝大家编程愉快!

发表回复

您的邮箱地址不会被公开。 必填项已用 * 标注