各位观众,早上好/下午好/晚上好!今天咱们来聊聊一个听起来有点神秘,但实际上挺有趣的东西:WebHID Report Descriptors Parser 开发,以及如何用它来打造通用 HID 设备驱动。别担心,我会尽量用大白话把这玩意儿讲清楚,争取让各位听完之后,也能撸起袖子自己写一个出来。
一、啥是 WebHID?为啥要用它?
首先,我们得知道啥是 WebHID。简单来说,WebHID 就是 Web API 里的一个大哥,它的作用就是让浏览器可以直接和 HID (Human Interface Device) 设备进行通信。HID 设备是啥?键盘、鼠标、游戏手柄、各种奇奇怪怪的传感器,只要是用来和人交互的,基本上都可以算作 HID 设备。
那为啥要用 WebHID 呢?传统的 Web 开发,对 HID 设备的支持非常有限。你可能只能用一些内置的 API 来处理键盘事件和鼠标事件,但如果你想用一些特殊的 HID 设备,比如一个自定义的游戏手柄,或者一个压力传感器,那就没辙了。WebHID 的出现,就是为了解决这个问题。它提供了一个更底层的接口,让开发者可以自由地和 HID 设备进行通信。
二、Report Descriptor:HID 设备的“说明书”
要和 HID 设备通信,我们首先要了解它的“说明书”,也就是 Report Descriptor(报告描述符)。这玩意儿是一个二进制的数据结构,它描述了 HID 设备的数据格式、功能和用途。你可以把它想象成一个 HID 设备的“简历”,里面详细地列出了它的所有信息。
Report Descriptor 的结构非常复杂,它由一系列的“项目” (Item) 组成。每个项目都有一个类型、一个大小和一个值。这些项目按照一定的规则组织在一起,形成了一个树状的结构。
举个例子,一个简单的鼠标 Report Descriptor 可能会包含以下几个项目:
项目类型 | 大小 (字节) | 值 | 含义 |
---|---|---|---|
Usage Page | 1 | 0x01 (Generic Desktop Controls) | 指定了设备的使用场景,这里表示是通用桌面控制设备,比如鼠标、键盘等。 |
Usage | 1 | 0x02 (Mouse) | 指定了设备的具体用途,这里表示是鼠标。 |
Collection | 1 | 0x01 (Application) | 定义了一个应用程序级别的集合,表示这是一个顶级的设备集合。 |
Report ID | 1 | 0x01 | 指定了报告的 ID,用于区分不同的报告类型。 |
Usage Page | 1 | 0x09 (Button) | 指定了按钮的使用场景。 |
Usage Minimum | 1 | 0x01 (Button 1) | 指定了按钮的起始编号。 |
Usage Maximum | 1 | 0x03 (Button 3) | 指定了按钮的结束编号。 |
Logical Minimum | 1 | 0x00 | 指定了逻辑值的最小值。 |
Logical Maximum | 1 | 0x01 | 指定了逻辑值的最大值。 |
Report Count | 1 | 0x03 | 指定了报告中包含的按钮数量。 |
Report Size | 1 | 0x01 | 指定了每个按钮占用的大小,这里表示每个按钮占用 1 位。 |
Input | 1 | 0x02 (Data, Variable, Absolute) | 定义了一个输入项目,表示设备向主机发送的数据。Data 表示是数据项目,Variable 表示是可变长度的,Absolute 表示是绝对值。 |
Usage Page | 1 | 0x01 (Generic Desktop Controls) | 指定了设备的使用场景,这里表示是通用桌面控制设备。 |
Usage | 1 | 0x30 (X) | 指定了 X 轴的用途。 |
Logical Minimum | 1 | 0x81 (-127) | 指定了 X 轴逻辑值的最小值。 |
Logical Maximum | 1 | 0x7F (127) | 指定了 X 轴逻辑值的最大值。 |
Report Size | 1 | 0x08 | 指定了 X 轴占用的大小,这里表示占用 8 位,也就是一个字节。 |
Report Count | 1 | 0x01 | 指定了报告中包含的 X 轴数量。 |
Input | 1 | 0x06 (Data, Variable, Relative) | 定义了一个输入项目,表示设备向主机发送的数据。Data 表示是数据项目,Variable 表示是可变长度的,Relative 表示是相对值。 |
Usage | 1 | 0x31 (Y) | 指定了 Y 轴的用途。 |
Logical Minimum | 1 | 0x81 (-127) | 指定了 Y 轴逻辑值的最小值。 |
Logical Maximum | 1 | 0x7F (127) | 指定了 Y 轴逻辑值的最大值。 |
Report Size | 1 | 0x08 | 指定了 Y 轴占用的大小,这里表示占用 8 位,也就是一个字节。 |
Report Count | 1 | 0x01 | 指定了报告中包含的 Y 轴数量。 |
Input | 1 | 0x06 (Data, Variable, Relative) | 定义了一个输入项目,表示设备向主机发送的数据。Data 表示是数据项目,Variable 表示是可变长度的,Relative 表示是相对值。 |
End Collection | 1 | 0x00 | 结束集合的定义。 |
这只是一个非常简单的例子,实际的 Report Descriptor 可能会更加复杂。但是,理解了这些基本概念,就可以开始着手解析 Report Descriptor 了。
三、Report Descriptor Parser:把“说明书”翻译成人话
Report Descriptor Parser 的作用就是把 Report Descriptor 这个二进制的数据结构,解析成一个更易于理解的数据结构。我们可以用 JavaScript 来实现这个 Parser。
首先,我们需要定义一些常量,用来表示不同的项目类型:
const HID_ITEM_TYPE_MAIN = 0x00;
const HID_ITEM_TYPE_GLOBAL = 0x01;
const HID_ITEM_TYPE_LOCAL = 0x02;
const HID_ITEM_SIZE_SHORT = 0x00;
const HID_ITEM_SIZE_LONG = 0x02;
然后,我们需要定义一个函数,用来读取 Report Descriptor 中的一个项目:
function readItem(data, offset) {
const prefix = data[offset];
const type = (prefix >> 2) & 0x03;
const size = prefix & 0x03;
let value = 0;
let valueLength = 0;
switch (size) {
case 0x00: // No data
valueLength = 0;
break;
case 0x01: // 1 byte
valueLength = 1;
value = data[offset + 1];
break;
case 0x02: // 2 bytes
valueLength = 2;
value = data[offset + 1] | (data[offset + 2] << 8);
break;
case 0x03: // 4 bytes
valueLength = 4;
value = data[offset + 1] | (data[offset + 2] << 8) | (data[offset + 3] << 16) | (data[offset + 4] << 24);
break;
}
return {
type: type,
size: size,
value: value,
length: 1 + valueLength, // Length of the item in bytes
};
}
这个函数接收 Report Descriptor 的数据和一个偏移量作为参数,然后返回一个包含项目类型、大小和值的对象。
接下来,我们需要定义一个函数,用来解析 Report Descriptor:
function parseReportDescriptor(data) {
let offset = 0;
const items = [];
while (offset < data.length) {
const item = readItem(data, offset);
items.push(item);
offset += item.length;
}
return items;
}
这个函数接收 Report Descriptor 的数据作为参数,然后循环读取 Report Descriptor 中的每一个项目,并将它们存储在一个数组中。
最后,我们可以使用这个 Parser 来解析一个 Report Descriptor:
const reportDescriptor = new Uint8Array([
0x05, 0x01, // Usage Page (Generic Desktop)
0x09, 0x02, // Usage (Mouse)
0xA1, 0x01, // Collection (Application)
0x09, 0x01, // Usage (Pointer)
0xA1, 0x00, // Collection (Physical)
0x05, 0x09, // Usage Page (Button)
0x19, 0x01, // Usage Minimum (Button 1)
0x29, 0x03, // Usage Maximum (Button 3)
0x15, 0x00, // Logical Minimum (0)
0x25, 0x01, // Logical Maximum (1)
0x95, 0x03, // Report Count (3)
0x75, 0x01, // Report Size (1)
0x81, 0x02, // Input (Data, Variable, Absolute)
0x05, 0x01, // Usage Page (Generic Desktop)
0x09, 0x30, // Usage (X)
0x09, 0x31, // Usage (Y)
0x15, 0x81, // Logical Minimum (-127)
0x25, 0x7F, // Logical Maximum (127)
0x75, 0x08, // Report Size (8)
0x95, 0x02, // Report Count (2)
0x81, 0x06, // Input (Data, Variable, Relative)
0xC0, // End Collection
0xC0 // End Collection
]);
const parsedReportDescriptor = parseReportDescriptor(reportDescriptor);
console.log(parsedReportDescriptor);
这个代码会把上面的鼠标 Report Descriptor 解析成一个包含所有项目的数组。你可以通过 console.log
来查看解析结果。
四、从解析结果到设备控制:构建通用驱动
有了 Report Descriptor Parser,我们就可以把 HID 设备的“说明书”翻译成人话了。接下来,我们需要根据解析结果,来构建一个通用的 HID 设备驱动。
这个驱动的核心任务是:
- 识别设备的功能: 根据 Report Descriptor 中的 Usage Page 和 Usage 项目,来判断设备的功能。比如,如果 Usage Page 是
Generic Desktop Controls
,Usage 是Mouse
,那么就可以判断这是一个鼠标。 - 解析设备发送的数据: 根据 Report Descriptor 中的 Input 项目,来确定设备发送的数据的格式。比如,如果 Report Descriptor 中包含一个 X 轴和一个 Y 轴,那么就可以知道设备会发送两个字节的数据,分别表示 X 轴和 Y 轴的位移。
- 将数据转换成有意义的值: 根据 Report Descriptor 中的 Logical Minimum 和 Logical Maximum 项目,来将设备发送的数据转换成有意义的值。比如,如果 X 轴的 Logical Minimum 是 -127,Logical Maximum 是 127,那么就可以知道设备发送的 X 轴数据范围是 -127 到 127。
下面是一个简单的例子,展示了如何根据解析结果来解析鼠标数据:
function processMouseData(data, parsedReportDescriptor) {
let x = 0;
let y = 0;
let buttons = 0;
// 假设我们已经解析了 Report Descriptor,并找到了 X 轴、Y 轴和按钮的 Input 项目
// 这里只是一个简化的例子,实际情况可能会更复杂
const xItem = parsedReportDescriptor.find(item => item.usage === 0x30); // X
const yItem = parsedReportDescriptor.find(item => item.usage === 0x31); // Y
const buttonItem = parsedReportDescriptor.find(item => item.usagePage === 0x09); // Button Page
if (xItem) {
x = data[xItem.offset]; // 假设 X 轴数据在第一个字节
}
if (yItem) {
y = data[yItem.offset]; // 假设 Y 轴数据在第二个字节
}
if (buttonItem) {
buttons = data[buttonItem.offset]; // 假设按钮数据在第三个字节
}
return {
x: x,
y: y,
buttons: buttons,
};
}
这个函数接收设备发送的数据和解析后的 Report Descriptor 作为参数,然后根据 Report Descriptor 中的信息,解析出 X 轴、Y 轴和按钮的值。
五、WebHID 的实际应用:一些脑洞大开的想法
有了 WebHID 和 Report Descriptor Parser,我们可以做很多有趣的事情。比如:
- 自定义游戏手柄驱动: 可以用 WebHID 来读取自定义游戏手柄的数据,然后将其转换成游戏可以识别的输入。
- 传感器数据可视化: 可以用 WebHID 来读取各种传感器的数据,比如温度传感器、压力传感器等,然后将其可视化。
- 智能家居控制: 可以用 WebHID 来控制智能家居设备,比如灯泡、窗帘等。
总之,WebHID 的应用场景非常广泛,只要你有足够的想象力,就可以用它来做很多有趣的事情。
六、代码示例:一个简单的 WebHID 设备连接和数据读取
下面是一个简单的 HTML 文件,展示了如何使用 WebHID 连接设备并读取数据:
<!DOCTYPE html>
<html>
<head>
<title>WebHID Example</title>
</head>
<body>
<button id="connectButton">Connect HID Device</button>
<div id="dataDisplay"></div>
<script>
const connectButton = document.getElementById('connectButton');
const dataDisplay = document.getElementById('dataDisplay');
connectButton.addEventListener('click', async () => {
try {
// Request access to a HID device
const devices = await navigator.hid.requestDevice({ filters: [] });
if (devices.length > 0) {
const device = devices[0];
await device.open();
dataDisplay.textContent = `Connected to device: ${device.productName}`;
device.addEventListener('inputreport', event => {
const { data, reportId } = event;
const uint8View = new Uint8Array(data.buffer);
// Here you would parse the data based on the report descriptor
// For simplicity, we'll just display the raw data
dataDisplay.textContent = `Received data: ${uint8View}`;
});
device.addEventListener('disconnect', () => {
dataDisplay.textContent = 'Device disconnected.';
});
} else {
dataDisplay.textContent = 'No HID devices selected.';
}
} catch (error) {
dataDisplay.textContent = `Error: ${error}`;
}
});
</script>
</body>
</html>
这段代码首先会请求用户授权访问 HID 设备。如果用户授权了,就会打开设备,并监听 inputreport
事件。当设备发送数据时,inputreport
事件就会被触发,然后我们就可以在事件处理函数中读取数据。
七、总结:WebHID 的未来和挑战
WebHID 是一项非常有潜力的技术,它可以让 Web 应用直接和 HID 设备进行通信,从而实现很多有趣的功能。但是,WebHID 也面临着一些挑战:
- 安全性: WebHID 允许 Web 应用访问用户的硬件设备,这可能会带来安全风险。因此,我们需要采取一些措施来保护用户的隐私和安全。
- 兼容性: 不同的 HID 设备可能使用不同的 Report Descriptor 格式,这可能会导致兼容性问题。因此,我们需要开发一个通用的 Report Descriptor Parser,来支持不同的 HID 设备。
- 易用性: WebHID 的 API 比较底层,使用起来比较复杂。因此,我们需要开发一些更易于使用的库和工具,来简化 WebHID 的开发过程。
尽管如此,WebHID 的未来仍然值得期待。随着 Web 技术的不断发展,WebHID 将会在更多的领域得到应用,为我们带来更多的便利和乐趣。
八、进阶思考:更健壮的Parser,更智能的驱动
上面的代码只是一个非常简单的例子,实际的 Report Descriptor 可能会更加复杂,需要更健壮的解析器。例如,可以考虑使用状态机来处理 Report Descriptor 中的嵌套结构,或者使用更高级的数据结构来存储解析结果。
另外,我们还可以让驱动更加智能。例如,可以根据设备的功能,自动选择合适的解析方法,或者根据用户的偏好,自定义设备的行为。
这些都需要更深入的研究和实践,希望大家能够在这个方向上继续探索,为 WebHID 的发展做出贡献。
好了,今天的讲座就到这里。希望大家能够有所收获,也欢迎大家提出问题和建议。谢谢大家!