之前介绍过一个USB键盘转蓝牙键盘的装置【参考1】,这次使用的同样的设计来实现USB游戏手柄改装为蓝牙游戏手柄。 具体硬件设计在【参考1】中:
接下来我们完全聚焦在软件设计上: 首先,检查手上USB手柄的描述符。抓到的数据如下:
Usage Page (Generic Desktop) | 05 01 | Usage (Joystick) | 09 04 | Collection (Application) | A1 01 | Collection (Logical) | A1 02 | Report Size (8) | 75 08 | Report Count (2) | 95 02 | Logical Minimum (0) | 15 00 | Logical Maximum (255) | 26 FF 00 | Physical Minimum (0) | 35 00 | Physical Maximum (255) | 46 FF 00 | Usage (X) | 09 30 | Usage (Y) | 09 31 | Input (Data,Var,Abs,NWrp,Lin,Pref,NNul,Bit) | 81 02 | Report Count (1) | 95 01 | Input (Cnst,Ary,Abs) | 81 01 | Report Count (2) | 95 02 | Usage (Z) | 09 32 | Usage (Rz) | 09 35 | Input (Data,Var,Abs,NWrp,Lin,Pref,NNul,Bit) | 81 02 | Report Size (4) | 75 04 | Report Count (1) | 95 01 | Logical Maximum (7) | 25 07 | Physical Maximum (315) | 46 3B 01 | Unit (Eng Rot: Degree) | 65 14 | Usage (Hat Switch) | 09 39 | Input (Data,Var,Abs,NWrp,Lin,Pref,Null,Bit) | 81 42 | Unit (None) | 65 00 | Report Size (1) | 75 01 | Report Count (12) | 95 0C | Logical Maximum (1) | 25 01 | Physical Maximum (1) | 45 01 | Usage Page (Button) | 05 09 | Usage Minimum (Button 1) | 19 01 | Usage Maximum (Button 12) | 29 0C | Input (Data,Var,Abs,NWrp,Lin,Pref,NNul,Bit) | 81 02 | Usage Page (Vendor-Defined 1) | 06 00 FF | Report Size (1) | 75 01 | Report Count (8) | 95 08 | Logical Maximum (1) | 25 01 | Physical Maximum (1) | 45 01 | Usage (Vendor-Defined 1) | 09 01 | Input (Data,Var,Abs,NWrp,Lin,Pref,NNul,Bit) | 81 02 | End Collection | C0 | Collection (Logical) | A1 02 | Report Size (8) | 75 08 | Report Count (7) | 95 07 | Physical Maximum (255) | 46 FF 00 | Logical Maximum (255) | 26 FF 00 | Usage (Vendor-Defined 2) | 09 02 | Output (Data,Var,Abs,NWrp,Lin,Pref,NNul,NVol,Bit) | 91 02 | End Collection | C0 | End Collection | C0 |
使用逻辑分析仪可以看到Button6按下时,数据为 7F 7F 00 80 80 0F 02 00
按下Button3时,数据为 7F 7F 00 80 80 4F 00 00
按下Button9时,数据为 7F 7F 00 80 80 0F 10 00
正常的做法是 USB Host 解析,然后从 BLE GamePad上发送出去,但是很明显这种方法非常麻烦。因此,这里研究一种直接将取得的数据转发出去的方法。 游戏手柄是 HID设备,蓝牙是在 USB协议推出之后才逐渐成熟的,因此这部分遵从了USBHID 的协议。在设备发现时,会传输一个类似 HID 描述符的东西给主机,告诉主机后面的数据格式。在 BLE 协议中,这个地方称作HIDreport map。 这里我们使用ESP32-BLE-Gamepad-master库来模拟BLE Gamepad。前面的HIDreport map代码可以在BleGamepad.cpp看到:
- uint8_t *customHidReportDescriptor = new uint8_t[hidReportDescriptorSize];
- memcpy(customHidReportDescriptor, tempHidReportDescriptor, hidReportDescriptorSize);
- BleGamepadInstance->hid->reportMap((uint8_t*)customHidReportDescriptor, hidReportDescriptorSize);
- BleGamepadInstance->hid->startServices();
复制代码
接下来就开始修改代码: 1. 用前面的 USB 手柄的 HID描述符替换库中的HIDReport Map。 - 0x05, 0x01, // Usage Page (Generic Desktop Ctrls)
- 0x09, 0x04, // Usage (Joystick)
- 0xA1, 0x01, // Collection (Application)
- 0xA1, 0x02, // Collection (Logical)
- 0x75, 0x08, // Report Size (8)
- 0x95, 0x02, // Report Count (2)
- 0x15, 0x00, // Logical Minimum (0)
- 0x26, 0xFF, 0x00, // Logical Maximum (255)
- 0x35, 0x00, // Physical Minimum (0)
- 0x46, 0xFF, 0x00, // Physical Maximum (255)
- 0x09, 0x30, // Usage (X)
- 0x09, 0x31, // Usage (Y)
- 0x81, 0x02, // Input (Data,Var,Abs,No Wrap,Linear,Preferred State,No Null Position)
- 0x95, 0x01, // Report Count (1)
- 0x81, 0x01, // Input (Const,Array,Abs,No Wrap,Linear,Preferred State,No Null Position)
- 0x95, 0x02, // Report Count (2)
- 0x09, 0x32, // Usage (Z)
- 0x09, 0x35, // Usage (Rz)
- 0x81, 0x02, // Input (Data,Var,Abs,No Wrap,Linear,Preferred State,No Null Position)
- 0x75, 0x04, // Report Size (4)
- 0x95, 0x01, // Report Count (1)
- 0x25, 0x07, // Logical Maximum (7)
- 0x46, 0x3B, 0x01, // Physical Maximum (315)
- 0x65, 0x14, // Unit (System: English Rotation, Length: Centimeter)
- 0x09, 0x39, // Usage (Hat switch)
- 0x81, 0x42, // Input (Data,Var,Abs,No Wrap,Linear,Preferred State,Null State)
- 0x65, 0x00, // Unit (None)
- 0x75, 0x01, // Report Size (1)
- 0x95, 0x0C, // Report Count (12)
- 0x25, 0x01, // Logical Maximum (1)
- 0x45, 0x01, // Physical Maximum (1)
- 0x05, 0x09, // Usage Page (Button)
- 0x19, 0x01, // Usage Minimum (0x01)
- 0x29, 0x0C, // Usage Maximum (0x0C)
- 0x81, 0x02, // Input (Data,Var,Abs,No Wrap,Linear,Preferred State,No Null Position)
- 0x06, 0x00, 0xFF, // Usage Page (Vendor Defined 0xFF00)
- 0x75, 0x01, // Report Size (1)
- 0x95, 0x08, // Report Count (8)
- 0x25, 0x01, // Logical Maximum (1)
- 0x45, 0x01, // Physical Maximum (1)
- 0x09, 0x01, // Usage (0x01)
- 0x81, 0x02, // Input (Data,Var,Abs,No Wrap,Linear,Preferred State,No Null Position)
- 0xC0, // End Collection
- 0xA1, 0x02, // Collection (Logical)
- 0x75, 0x08, // Report Size (8)
- 0x95, 0x07, // Report Count (7)
- 0x46, 0xFF, 0x00, // Physical Maximum (255)
- 0x26, 0xFF, 0x00, // Logical Maximum (255)
- 0x09, 0x02, // Usage (0x02)
- 0x91, 0x02, // Output (Data,Var,Abs,No Wrap,Linear,Preferred State,No Null Position,Non-volatile)
- 0xC0, // End Collection
- 0xC0, // End Collection
复制代码
2. 直接用上述数据替换之后无法正常工作,现象是系统能够发现手柄,但是系统自带的 Game Controller 中的手柄无法正常出现按键。经过研究发现ESP32-BLE-Gamepad能够正常工作的代码中,REPORTMAP多一个REPORTID。因此修改上面的代码如下:
- 0x05, 0x01, // Usage Page (Generic Desktop Ctrls)
- 0x85, 0x03, // Report ID
- 0x09, 0x04, // Usage (Joystick)
- 0xA1, 0x01, // Collection (Application)
- 0xA1, 0x02, // Collection (Logical)
- 0x75, 0x08, // Report Size (8)
复制代码
在英文中,有 JoyStick 和Gamepad的区别,前者可以理解为飞行摇杆这种;后者可以理解为玩拳皇这种的手柄)。
3.1. 接下来我们还要在 BLE GAMEPAD 库中添加一个转发的函数,将收到的 buffer完整的从 BLE 发送出去,代码如下: - void BleGamepad::MySendReport(uint8_t *buffer)
- {
- if (this->isConnected())
- {
- this->inputGamepad->setValue(buffer, 8);
- this->inputGamepad->notify();
- }
- }
复制代码
4.1. 最终代码如下: - #include <BleGamepad.h>
-
- #include <elapsedMillis.h>
- #include <usb/usb_host.h>
- #include "show_desc.hpp"
- #include "usbhhelp.hpp"
-
- BleGamepad bleGamepad;
-
- bool isGamepad = false;
- bool isGamepadReady = false;
- uint8_t GamepadInterval;
- bool isGamepadPolling = false;
- elapsedMillis GamepadTimer;
-
- uint8_t Button[8];
-
- const size_t Gamepad_IN_BUFFER_SIZE = 8;
- usb_transfer_t *GamepadIn = NULL;
-
- void Gamepad_transfer_cb(usb_transfer_t *transfer)
- {
- if (Device_Handle == transfer->device_handle) {
- isGamepadPolling = false;
- if (transfer->status == 0) {
- if (transfer->actual_num_bytes == 8) {
- uint8_t *const p = transfer->data_buffer;
- //ESP_LOGI("", "HID report: %02x %02x %02x %02x %02x %02x %02x %02x",
- // p[0], p[1], p[2], p[3], p[4], p[5], p[6], p[7]);
- if (bleGamepad.isConnected())
- {
- if (memcmp(p, Button, 8) != 0) {
- ESP_LOGI("", "HID report: %02x %02x %02x %02x %02x %02x %02x %02x",
- p[0], p[1], p[2], p[3], p[4], p[5], p[6], p[7]);
- memcpy(Button,p,8);
- bleGamepad.MySendReport(Button);
- }
- }
- }
- else {
- ESP_LOGI("", "Gamepad boot hid transfer too short or long");
- }
- }
- else {
- ESP_LOGI("", "transfer->status %d", transfer->status);
- }
- }
- }
-
- void check_interface_desc_boot_Gamepad(const void *p)
- {
- const usb_intf_desc_t *intf = (const usb_intf_desc_t *)p;
-
- if ((intf->bInterfaceClass == USB_CLASS_HID) &&
- (intf->bInterfaceSubClass == 0x00) &&
- (intf->bInterfaceProtocol == 0x00)) {
- isGamepad = true;
- ESP_LOGI("", "Claiming a boot Gamepad!");
- esp_err_t err = usb_host_interface_claim(Client_Handle, Device_Handle,
- intf->bInterfaceNumber, intf->bAlternateSetting);
- if (err != ESP_OK) ESP_LOGI("", "usb_host_interface_claim failed: %x", err);
- }
- }
-
- void prepare_endpoint(const void *p)
- {
- const usb_ep_desc_t *endpoint = (const usb_ep_desc_t *)p;
- esp_err_t err;
-
- // must be interrupt for HID
- if ((endpoint->bmAttributes & USB_BM_ATTRIBUTES_XFERTYPE_MASK) != USB_BM_ATTRIBUTES_XFER_INT) {
- ESP_LOGI("", "Not interrupt endpoint: 0x%02x", endpoint->bmAttributes);
- return;
- }
- if (endpoint->bEndpointAddress & USB_B_ENDPOINT_ADDRESS_EP_DIR_MASK) {
- err = usb_host_transfer_alloc(Gamepad_IN_BUFFER_SIZE, 0, &GamepadIn);
- if (err != ESP_OK) {
- GamepadIn = NULL;
- ESP_LOGI("", "usb_host_transfer_alloc In fail: %x", err);
- return;
- }
- GamepadIn->device_handle = Device_Handle;
- GamepadIn->bEndpointAddress = endpoint->bEndpointAddress;
- GamepadIn->callback = Gamepad_transfer_cb;
- GamepadIn->context = NULL;
- isGamepadReady = true;
- GamepadInterval = endpoint->bInterval;
- ESP_LOGI("", "USB boot Gamepad ready");
- }
- else {
- ESP_LOGI("", "Ignoring interrupt Out endpoint");
- }
- }
-
- void show_config_desc_full(const usb_config_desc_t *config_desc)
- {
- // Full decode of config desc.
- const uint8_t *p = &config_desc->val[0];
- static uint8_t USB_Class = 0;
- uint8_t bLength;
- for (int i = 0; i < config_desc->wTotalLength; i += bLength, p += bLength) {
- bLength = *p;
- if ((i + bLength) <= config_desc->wTotalLength) {
- const uint8_t bDescriptorType = *(p + 1);
- switch (bDescriptorType) {
- case USB_B_DESCRIPTOR_TYPE_DEVICE:
- ESP_LOGI("", "USB Device Descriptor should not appear in config");
- break;
- case USB_B_DESCRIPTOR_TYPE_CONFIGURATION:
- show_config_desc(p);
- break;
- case USB_B_DESCRIPTOR_TYPE_STRING:
- ESP_LOGI("", "USB string desc TBD");
- break;
- case USB_B_DESCRIPTOR_TYPE_INTERFACE:
- USB_Class = show_interface_desc(p);
- check_interface_desc_boot_Gamepad(p);
- break;
- case USB_B_DESCRIPTOR_TYPE_ENDPOINT:
- show_endpoint_desc(p);
- if (isGamepad && GamepadIn == NULL) prepare_endpoint(p);
- break;
- case USB_B_DESCRIPTOR_TYPE_DEVICE_QUALIFIER:
- // Should not be config config?
- ESP_LOGI("", "USB device qual desc TBD");
- break;
- case USB_B_DESCRIPTOR_TYPE_OTHER_SPEED_CONFIGURATION:
- // Should not be config config?
- ESP_LOGI("", "USB Other Speed TBD");
- break;
- case USB_B_DESCRIPTOR_TYPE_INTERFACE_POWER:
- // Should not be config config?
- ESP_LOGI("", "USB Interface Power TBD");
- break;
- case 0x21:
- if (USB_Class == USB_CLASS_HID) {
- show_hid_desc(p);
- }
- break;
- default:
- ESP_LOGI("", "Unknown USB Descriptor Type: 0x%x", bDescriptorType);
- break;
- }
- }
- else {
- ESP_LOGI("", "USB Descriptor invalid");
- return;
- }
- }
- }
-
- void setup()
- {
- usbh_setup(show_config_desc_full);
- bleGamepad.begin();
- }
-
- void loop()
- {
- usbh_task();
-
- if (isGamepadReady && !isGamepadPolling && (GamepadTimer > GamepadInterval)) {
- GamepadIn->num_bytes = 8;
- esp_err_t err = usb_host_transfer_submit(GamepadIn);
- if (err != ESP_OK) {
- ESP_LOGI("", "usb_host_transfer_submit In fail: %x", err);
- }
- isGamepadPolling = true;
- GamepadTimer = 0;
- }
- }
复制代码
参考:
|