各位观众老爷,大家好!今天咱们来聊聊一个听起来有点高大上,但实际上挺有趣的技术——WebUSB,以及如何用它来解析USB设备描述符,并最终与你的自定义USB设备谈笑风生(进行通信)。
一、WebUSB:浏览器里的USB魔法
WebUSB,顾名思义,就是让你的Web应用直接和USB设备交流的API。想想看,以前要搞定USB设备,要么得装驱动,要么得写C/C++代码,现在好了,浏览器直接上,简直是懒人福音啊!
WebUSB的优势:
- 跨平台: 只要浏览器支持,你的代码就能在Windows、macOS、Linux上跑。
- 无需驱动: 对于符合WebUSB规范的设备,不需要安装额外的驱动程序。
- 安全: 浏览器会询问用户是否允许Web应用访问USB设备,安全性有保障。
- 方便: 直接在浏览器里调试,省去了很多麻烦。
但是,WebUSB也有局限性:
- 浏览器支持: 目前主流浏览器都支持,但还是要注意兼容性。
- 设备支持: 并非所有USB设备都支持WebUSB。设备需要声明支持WebUSB协议。
- 安全性: 虽然有安全机制,但开发者仍需注意代码安全,防止恶意利用。
二、USB设备描述符:设备的身份证
想要和USB设备通信,首先你得了解它。USB设备描述符就像是设备的身份证,它包含了设备的各种信息,比如厂商ID(Vendor ID)、产品ID(Product ID)、设备类别等等。
常见的USB描述符:
描述符类型 | 描述 |
---|---|
Device Descriptor | 设备总体信息,如VID、PID等。 |
Configuration Descriptor | 设备配置信息,如接口数量等。 |
Interface Descriptor | 接口信息,如接口类别、端点数量等。 |
Endpoint Descriptor | 端点信息,如端点地址、传输类型等。 |
Device Descriptor (设备描述符) 结构体:
typedef struct {
uint8_t bLength; // 描述符长度,固定为0x12
uint8_t bDescriptorType; // 描述符类型,Device Descriptor为0x01
uint16_t bcdUSB; // USB版本号,如0x0200表示USB 2.0
uint8_t bDeviceClass; // 设备类别,如0x00表示由接口描述符定义
uint8_t bDeviceSubClass; // 设备子类别
uint8_t bDeviceProtocol; // 设备协议
uint8_t bMaxPacketSize0; // 端点0的最大包大小
uint16_t idVendor; // 厂商ID (Vendor ID)
uint16_t idProduct; // 产品ID (Product ID)
uint16_t bcdDevice; // 设备版本号
uint8_t iManufacturer; // 厂商字符串索引
uint8_t iProduct; // 产品字符串索引
uint8_t iSerialNumber; // 序列号字符串索引
uint8_t bNumConfigurations; // 配置数量
} USB_DeviceDescriptor;
Configuration Descriptor (配置描述符) 结构体:
typedef struct {
uint8_t bLength; // 描述符长度,固定为0x09
uint8_t bDescriptorType; // 描述符类型,Configuration Descriptor为0x02
uint16_t wTotalLength; // 描述符总长度(包括所有接口、端点等描述符)
uint8_t bNumInterfaces; // 接口数量
uint8_t bConfigurationValue; // 配置值,用于选择配置
uint8_t iConfiguration; // 配置字符串索引
uint8_t bmAttributes; // 属性,如是否支持远程唤醒、供电方式等
uint8_t bMaxPower; // 最大功耗,单位为2mA
} USB_ConfigurationDescriptor;
Interface Descriptor (接口描述符) 结构体:
typedef struct {
uint8_t bLength; // 描述符长度,固定为0x09
uint8_t bDescriptorType; // 描述符类型,Interface Descriptor为0x04
uint8_t bInterfaceNumber; // 接口编号,从0开始
uint8_t bAlternateSetting; // 备用接口设置
uint8_t bNumEndpoints; // 端点数量
uint8_t bInterfaceClass; // 接口类别,如0x03表示HID设备
uint8_t bInterfaceSubClass; // 接口子类别
uint8_t bInterfaceProtocol; // 接口协议
uint8_t iInterface; // 接口字符串索引
} USB_InterfaceDescriptor;
Endpoint Descriptor (端点描述符) 结构体:
typedef struct {
uint8_t bLength; // 描述符长度,固定为0x07
uint8_t bDescriptorType; // 描述符类型,Endpoint Descriptor为0x05
uint8_t bEndpointAddress; // 端点地址,bit7表示方向(0: OUT, 1: IN)
uint8_t bmAttributes; // 属性,如传输类型(控制、批量、中断、等时)
uint16_t wMaxPacketSize; // 最大包大小
uint8_t bInterval; // 轮询间隔,单位取决于传输类型
} USB_EndpointDescriptor;
三、WebUSB实战:解析描述符并通信
1. HTML准备:
首先,我们需要一个简单的HTML页面,包含一个按钮,点击后触发WebUSB连接:
<!DOCTYPE html>
<html>
<head>
<title>WebUSB Demo</title>
</head>
<body>
<button id="connectButton">Connect to USB Device</button>
<pre id="output"></pre>
<script src="script.js"></script>
</body>
</html>
2. JavaScript代码:
接下来,是核心的JavaScript代码 script.js
。
const connectButton = document.getElementById('connectButton');
const output = document.getElementById('output');
let device;
connectButton.addEventListener('click', async () => {
try {
// 请求设备
device = await navigator.usb.requestDevice({ filters: [] });
// 连接设备
await device.open();
// 选择配置
await device.selectConfiguration(1); // 假设选择第一个配置
// 声明接口(如果需要)
await device.claimInterface(0); // 假设声明第一个接口
log('Connected to USB device!');
// 读取设备描述符
const deviceDescriptor = await getDeviceDescriptor(device);
log('Device Descriptor:', deviceDescriptor);
// 读取配置描述符
const configurationDescriptor = await getConfigurationDescriptor(device);
log('Configuration Descriptor:', configurationDescriptor);
// 假设设备是 HID 设备,发送一个特征报告请求
if (deviceDescriptor.bDeviceClass === 0x00 && configurationDescriptor.interfaces[0].interfaceClass === 0x03) {
try {
const featureReport = await getFeatureReport(device, 0x01, 64); // 假设报告ID为 0x01,长度为 64
log('Feature Report:', featureReport);
} catch (error) {
log('Error getting feature report:', error);
}
}
// 发送数据 (OUT endpoint)
const dataToSend = new Uint8Array([0x01, 0x02, 0x03, 0x04]); // 要发送的数据
try {
await device.transferOut(1, dataToSend); // 假设 OUT 端点地址为 1
log('Data sent successfully!');
} catch (error) {
log('Error sending data:', error);
}
// 接收数据 (IN endpoint)
try {
const result = await device.transferIn(2, 64); // 假设 IN 端点地址为 2, 最大包大小为 64
const receivedData = new Uint8Array(result.data.buffer);
log('Received data:', receivedData);
} catch (error) {
log('Error receiving data:', error);
}
} catch (error) {
log('Error:', error);
} finally {
// 关闭设备 (可选,根据需求)
// if (device && device.opened) {
// await device.close();
// log('Device closed.');
// }
}
});
// 日志函数
function log(message, data = null) {
if (data) {
output.textContent += message + JSON.stringify(data, null, 2) + 'n';
} else {
output.textContent += message + 'n';
}
}
// 获取设备描述符
async function getDeviceDescriptor(device) {
const buffer = await device.controlTransferIn({
requestType: 'standard',
recipient: 'device',
request: 0x06, // GET_DESCRIPTOR
value: (0x01 << 8) | 0x00, // Device Descriptor type
index: 0
}, 18); // 设备描述符长度为18字节
const dataView = new DataView(buffer.data.buffer);
return {
bLength: dataView.getUint8(0),
bDescriptorType: dataView.getUint8(1),
bcdUSB: dataView.getUint16(2, true),
bDeviceClass: dataView.getUint8(4),
bDeviceSubClass: dataView.getUint8(5),
bDeviceProtocol: dataView.getUint8(6),
bMaxPacketSize0: dataView.getUint8(7),
idVendor: dataView.getUint16(8, true),
idProduct: dataView.getUint16(10, true),
bcdDevice: dataView.getUint16(12, true),
iManufacturer: dataView.getUint8(14),
iProduct: dataView.getUint8(15),
iSerialNumber: dataView.getUint8(16),
bNumConfigurations: dataView.getUint8(17)
};
}
// 获取配置描述符
async function getConfigurationDescriptor(device) {
const buffer = await device.controlTransferIn({
requestType: 'standard',
recipient: 'device',
request: 0x06, // GET_DESCRIPTOR
value: (0x02 << 8) | 0x00, // Configuration Descriptor type, index 0
index: 0
}, 9); // 先读取前9个字节,获取wTotalLength
const dataView = new DataView(buffer.data.buffer);
const wTotalLength = dataView.getUint16(2, true);
const fullBuffer = await device.controlTransferIn({
requestType: 'standard',
recipient: 'device',
request: 0x06, // GET_DESCRIPTOR
value: (0x02 << 8) | 0x00, // Configuration Descriptor type, index 0
index: 0
}, wTotalLength); // 读取完整配置描述符
const fullDataView = new DataView(fullBuffer.data.buffer);
const numInterfaces = fullDataView.getUint8(4);
const interfaces = [];
let offset = 9; // 跳过Configuration Descriptor
for (let i = 0; i < numInterfaces; i++) {
const interfaceLength = fullDataView.getUint8(offset);
const interfaceDescriptorType = fullDataView.getUint8(offset + 1);
if (interfaceDescriptorType === 0x04) { // Interface Descriptor
const interfaceDescriptor = {
bLength: fullDataView.getUint8(offset),
bDescriptorType: fullDataView.getUint8(offset + 1),
bInterfaceNumber: fullDataView.getUint8(offset + 2),
bAlternateSetting: fullDataView.getUint8(offset + 3),
bNumEndpoints: fullDataView.getUint8(offset + 4),
bInterfaceClass: fullDataView.getUint8(offset + 5),
bInterfaceSubClass: fullDataView.getUint8(offset + 6),
bInterfaceProtocol: fullDataView.getUint8(offset + 7),
iInterface: fullDataView.getUint8(offset + 8),
endpoints: []
};
offset += interfaceLength;
for (let j = 0; j < interfaceDescriptor.bNumEndpoints; j++) {
const endpointLength = fullDataView.getUint8(offset);
const endpointDescriptorType = fullDataView.getUint8(offset + 1);
if (endpointDescriptorType === 0x05) { // Endpoint Descriptor
const endpointDescriptor = {
bLength: fullDataView.getUint8(offset),
bDescriptorType: fullDataView.getUint8(offset + 1),
bEndpointAddress: fullDataView.getUint8(offset + 2),
bmAttributes: fullDataView.getUint8(offset + 3),
wMaxPacketSize: fullDataView.getUint16(offset + 4, true),
bInterval: fullDataView.getUint8(offset + 6)
};
interfaceDescriptor.endpoints.push(endpointDescriptor);
offset += endpointLength;
} else {
// 如果不是 Endpoint Descriptor,则跳过
offset += fullDataView.getUint8(offset);
j--; // 修正循环计数器,因为没有找到端点
}
}
interfaces.push(interfaceDescriptor);
} else {
// 如果不是 Interface Descriptor,则跳过
offset += fullDataView.getUint8(offset);
i--; // 修正循环计数器,因为没有找到接口
}
}
return {
bLength: fullDataView.getUint8(0),
bDescriptorType: fullDataView.getUint8(1),
wTotalLength: wTotalLength,
bNumInterfaces: numInterfaces,
bConfigurationValue: fullDataView.getUint8(5),
iConfiguration: fullDataView.getUint8(6),
bmAttributes: fullDataView.getUint8(7),
bMaxPower: fullDataView.getUint8(8),
interfaces: interfaces
};
}
// 获取 Feature Report (HID 设备)
async function getFeatureReport(device, reportId, length) {
const buffer = await device.controlTransferIn({
requestType: 'class',
recipient: 'interface',
request: 0x01, // GET_REPORT
value: (0x03 << 8) | reportId, // Feature Report type and Report ID
index: 0
}, length);
return new Uint8Array(buffer.data.buffer);
}
代码解释:
navigator.usb.requestDevice()
: 弹出设备选择框,让用户选择USB设备。device.open()
: 打开设备连接。device.selectConfiguration()
: 选择设备配置。device.claimInterface()
: 声明接口,告诉系统我们要使用这个接口。device.controlTransferIn()
: 发送控制传输请求,读取设备描述符。注意,这里的参数需要根据USB规范来设置,比如requestType
、recipient
、request
、value
、index
和length
。device.transferOut()
: 发送数据到设备的OUT端点。device.transferIn()
: 从设备的IN端点接收数据。getDeviceDescriptor()
和getConfigurationDescriptor()
: 解析设备描述符和配置描述符,将二进制数据转换为JavaScript对象。getFeatureReport()
: 获取HID设备的Feature Report。
3. USB设备准备:
要让WebUSB工作,你的USB设备需要支持WebUSB协议。简单来说,就是在设备的固件里添加一个WebUSB描述符,告诉浏览器这个设备支持WebUSB。 这个过程因设备而异,通常需要在设备固件中进行配置。你可以参考WebUSB官方文档和相关示例代码。
重要提示:
idVendor
和idProduct
: 在navigator.usb.requestDevice()
中,你可以使用filters
选项来指定你想要连接的设备的idVendor
和idProduct
,这样可以避免弹出设备选择框,直接连接到指定的设备。- 错误处理: 代码中包含了简单的错误处理,但在实际应用中,你需要更完善的错误处理机制,以便更好地调试和处理异常情况。
- 安全: WebUSB涉及到硬件访问,所以要特别注意安全问题。不要连接来历不明的USB设备,并仔细审查代码,防止恶意利用。
- Endpoint 地址:
transferIn
和transferOut
函数中的 endpoint 地址至关重要。 需要从 configuration descriptor 中解析出来,确保使用正确的 IN 和 OUT endpoint 地址。 地址的最高位表示方向(IN 或 OUT)。
4. 运行:
将HTML文件和JavaScript文件放在同一个目录下,用支持WebUSB的浏览器打开HTML文件,点击按钮,如果一切顺利,你就可以在浏览器控制台看到设备描述符的信息了。
四、进阶:自定义USB设备通信
有了设备描述符,你就可以根据设备的协议,发送和接收数据了。
通信步骤:
- 了解设备协议: 你需要了解你的USB设备使用的协议,比如是HID、CDC还是自定义协议。
- 查找端点: 通过解析配置描述符,找到用于数据传输的IN和OUT端点。
- 发送数据: 使用
device.transferOut()
发送数据到OUT端点。 - 接收数据: 使用
device.transferIn()
从IN端点接收数据。 - 解析数据: 根据设备协议,解析接收到的数据。
示例:
假设你的设备是一个简单的串口设备,使用CDC-ACM协议。你可以这样发送和接收数据:
// 发送数据
async function sendData(data) {
await device.transferOut(OUT_ENDPOINT_ADDRESS, data); // OUT_ENDPOINT_ADDRESS 是你的OUT端点地址
}
// 接收数据
async function receiveData() {
const result = await device.transferIn(IN_ENDPOINT_ADDRESS, 64); // IN_ENDPOINT_ADDRESS 是你的IN端点地址
const receivedData = new Uint8Array(result.data.buffer);
return receivedData;
}
// 使用示例
const dataToSend = new Uint8Array([0x48, 0x65, 0x6c, 0x6c, 0x6f]); // "Hello"的ASCII码
await sendData(dataToSend);
const receivedData = await receiveData();
log('Received:', new TextDecoder().decode(receivedData)); // 将Uint8Array转换为字符串
五、总结
WebUSB为Web应用打开了与USB设备通信的大门。通过解析设备描述符,我们可以了解设备的特性,并根据设备的协议进行数据交互。虽然WebUSB还有一些限制,但随着技术的不断发展,它必将在Web开发中发挥越来越重要的作用。
希望今天的讲解能帮助你入门WebUSB,并开始探索USB设备通信的乐趣。 记住,实践是检验真理的唯一标准,多动手尝试,你才能真正掌握这项技术。 祝你玩得开心!