[ESP8266/ESP32]USB 手柄改蓝牙 精华

2917浏览
查看: 2917|回复: 10

[ESP8266/ESP32] USB 手柄改蓝牙

[复制链接]
之前介绍过一个USB键盘转蓝牙键盘的装置【参考1】,这次使用的同样的设计来实现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
USB 手柄改蓝牙图2
按下Button3时,数据为 7F 7F 00 80 80 4F 00 00
USB 手柄改蓝牙图3
按下Button9时,数据为 7F 7F 00 80 80 0F 10 00
USB 手柄改蓝牙图4
正常的做法是 USB Host 解析,然后从 BLE GamePad上发送出去,但是很明显这种方法非常麻烦。因此,这里研究一种直接将取得的数据转发出去的方法。
游戏手柄是 HID设备,蓝牙是在 USB协议推出之后才逐渐成熟的,因此这部分遵从了USBHID 的协议。在设备发现时,会传输一个类似 HID 描述符的东西给主机,告诉主机后面的数据格式。在 BLE 协议中,这个地方称作HIDreport map
这里我们使用ESP32-BLE-Gamepad-master库来模拟BLE Gamepad。前面的HIDreport map代码可以在BleGamepad.cpp看到:



  1.    uint8_t *customHidReportDescriptor = new uint8_t[hidReportDescriptorSize];
  2.    memcpy(customHidReportDescriptor, tempHidReportDescriptor, hidReportDescriptorSize);
  3.   BleGamepadInstance->hid->reportMap((uint8_t*)customHidReportDescriptor, hidReportDescriptorSize);
  4.   BleGamepadInstance->hid->startServices();
复制代码
接下来就开始修改代码:
1.     用前面的 USB 手柄的 HID描述符替换库中的HIDReport Map。
  1. 0x05, 0x01,        // Usage Page (Generic Desktop Ctrls)
  2. 0x09, 0x04,        // Usage (Joystick)
  3. 0xA1, 0x01,        // Collection (Application)
  4. 0xA1, 0x02,        //   Collection (Logical)
  5. 0x75, 0x08,        //     Report Size (8)
  6. 0x95, 0x02,        //     Report Count (2)
  7. 0x15, 0x00,        //     Logical Minimum (0)
  8. 0x26, 0xFF, 0x00,  //     Logical Maximum (255)
  9. 0x35, 0x00,        //     Physical Minimum (0)
  10. 0x46, 0xFF, 0x00,  //     Physical Maximum (255)
  11. 0x09, 0x30,        //     Usage (X)
  12. 0x09, 0x31,        //     Usage (Y)
  13. 0x81, 0x02,        //     Input (Data,Var,Abs,No Wrap,Linear,Preferred State,No Null Position)
  14. 0x95, 0x01,        //     Report Count (1)
  15. 0x81, 0x01,        //     Input (Const,Array,Abs,No Wrap,Linear,Preferred State,No Null Position)
  16. 0x95, 0x02,        //     Report Count (2)
  17. 0x09, 0x32,        //     Usage (Z)
  18. 0x09, 0x35,        //     Usage (Rz)
  19. 0x81, 0x02,        //     Input (Data,Var,Abs,No Wrap,Linear,Preferred State,No Null Position)
  20. 0x75, 0x04,        //     Report Size (4)
  21. 0x95, 0x01,        //     Report Count (1)
  22. 0x25, 0x07,        //     Logical Maximum (7)
  23. 0x46, 0x3B, 0x01,  //     Physical Maximum (315)
  24. 0x65, 0x14,        //     Unit (System: English Rotation, Length: Centimeter)
  25. 0x09, 0x39,        //     Usage (Hat switch)
  26. 0x81, 0x42,        //     Input (Data,Var,Abs,No Wrap,Linear,Preferred State,Null State)
  27. 0x65, 0x00,        //     Unit (None)
  28. 0x75, 0x01,        //     Report Size (1)
  29. 0x95, 0x0C,        //     Report Count (12)
  30. 0x25, 0x01,        //     Logical Maximum (1)
  31. 0x45, 0x01,        //     Physical Maximum (1)
  32. 0x05, 0x09,        //     Usage Page (Button)
  33. 0x19, 0x01,        //     Usage Minimum (0x01)
  34. 0x29, 0x0C,        //     Usage Maximum (0x0C)
  35. 0x81, 0x02,        //     Input (Data,Var,Abs,No Wrap,Linear,Preferred State,No Null Position)
  36. 0x06, 0x00, 0xFF,  //     Usage Page (Vendor Defined 0xFF00)
  37. 0x75, 0x01,        //     Report Size (1)
  38. 0x95, 0x08,        //     Report Count (8)
  39. 0x25, 0x01,        //     Logical Maximum (1)
  40. 0x45, 0x01,        //     Physical Maximum (1)
  41. 0x09, 0x01,        //     Usage (0x01)
  42. 0x81, 0x02,        //     Input (Data,Var,Abs,No Wrap,Linear,Preferred State,No Null Position)
  43. 0xC0,              //   End Collection
  44. 0xA1, 0x02,        //   Collection (Logical)
  45. 0x75, 0x08,        //     Report Size (8)
  46. 0x95, 0x07,        //     Report Count (7)
  47. 0x46, 0xFF, 0x00,  //     Physical Maximum (255)
  48. 0x26, 0xFF, 0x00,  //     Logical Maximum (255)
  49. 0x09, 0x02,        //     Usage (0x02)
  50. 0x91, 0x02,        //     Output (Data,Var,Abs,No Wrap,Linear,Preferred State,No Null Position,Non-volatile)
  51. 0xC0,              //   End Collection
  52. 0xC0,              // End Collection
复制代码
2.     直接用上述数据替换之后无法正常工作,现象是系统能够发现手柄,但是系统自带的 Game Controller 中的手柄无法正常出现按键。经过研究发现ESP32-BLE-Gamepad能够正常工作的代码中,REPORTMAP多一个REPORTID。因此修改上面的代码如下:


  1. 0x05, 0x01,        // Usage Page (Generic Desktop Ctrls)
  2. 0x85, 0x03,        // Report ID
  3. 0x09, 0x04,        // Usage (Joystick)
  4. 0xA1, 0x01,        // Collection (Application)
  5. 0xA1, 0x02,        //   Collection (Logical)
  6. 0x75, 0x08,        //     Report Size (8)
复制代码
在英文中,有 JoyStick 和Gamepad的区别,前者可以理解为飞行摇杆这种;后者可以理解为玩拳皇这种的手柄)。

3.1.     接下来我们还要在 BLE GAMEPAD 库中添加一个转发的函数,将收到的 buffer完整的从 BLE 发送出去,代码如下:
  1. void BleGamepad::MySendReport(uint8_t *buffer)
  2. {
  3.         if (this->isConnected())
  4.         {
  5.                 this->inputGamepad->setValue(buffer, 8);
  6.                 this->inputGamepad->notify();
  7.         }
  8. }
复制代码

4.1.     最终代码如下:
  1. #include <BleGamepad.h>
  2. #include <elapsedMillis.h>
  3. #include <usb/usb_host.h>
  4. #include "show_desc.hpp"
  5. #include "usbhhelp.hpp"
  6. BleGamepad bleGamepad;
  7. bool isGamepad = false;
  8. bool isGamepadReady = false;
  9. uint8_t GamepadInterval;
  10. bool isGamepadPolling = false;
  11. elapsedMillis GamepadTimer;
  12. uint8_t Button[8];
  13. const size_t Gamepad_IN_BUFFER_SIZE = 8;
  14. usb_transfer_t *GamepadIn = NULL;
  15. void Gamepad_transfer_cb(usb_transfer_t *transfer)
  16. {
  17.   if (Device_Handle == transfer->device_handle) {
  18.     isGamepadPolling = false;
  19.     if (transfer->status == 0) {
  20.       if (transfer->actual_num_bytes == 8) {
  21.         uint8_t *const p = transfer->data_buffer;
  22.         //ESP_LOGI("", "HID report: %02x %02x %02x %02x %02x %02x %02x %02x",
  23.         //         p[0], p[1], p[2], p[3], p[4], p[5], p[6], p[7]);
  24.         if (bleGamepad.isConnected())
  25.         {
  26.           if (memcmp(p, Button, 8) != 0) {
  27.             ESP_LOGI("", "HID report: %02x %02x %02x %02x %02x %02x %02x %02x",
  28.                      p[0], p[1], p[2], p[3], p[4], p[5], p[6], p[7]);
  29.             memcpy(Button,p,8);
  30.             bleGamepad.MySendReport(Button);
  31.           }
  32.         }
  33.       }
  34.       else {
  35.         ESP_LOGI("", "Gamepad boot hid transfer too short or long");
  36.       }
  37.     }
  38.     else {
  39.       ESP_LOGI("", "transfer->status %d", transfer->status);
  40.     }
  41.   }
  42. }
  43. void check_interface_desc_boot_Gamepad(const void *p)
  44. {
  45.   const usb_intf_desc_t *intf = (const usb_intf_desc_t *)p;
  46.   if ((intf->bInterfaceClass == USB_CLASS_HID) &&
  47.       (intf->bInterfaceSubClass == 0x00) &&
  48.       (intf->bInterfaceProtocol == 0x00)) {
  49.     isGamepad = true;
  50.     ESP_LOGI("", "Claiming a boot Gamepad!");
  51.     esp_err_t err = usb_host_interface_claim(Client_Handle, Device_Handle,
  52.                     intf->bInterfaceNumber, intf->bAlternateSetting);
  53.     if (err != ESP_OK) ESP_LOGI("", "usb_host_interface_claim failed: %x", err);
  54.   }
  55. }
  56. void prepare_endpoint(const void *p)
  57. {
  58.   const usb_ep_desc_t *endpoint = (const usb_ep_desc_t *)p;
  59.   esp_err_t err;
  60.   // must be interrupt for HID
  61.   if ((endpoint->bmAttributes & USB_BM_ATTRIBUTES_XFERTYPE_MASK) != USB_BM_ATTRIBUTES_XFER_INT) {
  62.     ESP_LOGI("", "Not interrupt endpoint: 0x%02x", endpoint->bmAttributes);
  63.     return;
  64.   }
  65.   if (endpoint->bEndpointAddress & USB_B_ENDPOINT_ADDRESS_EP_DIR_MASK) {
  66.     err = usb_host_transfer_alloc(Gamepad_IN_BUFFER_SIZE, 0, &GamepadIn);
  67.     if (err != ESP_OK) {
  68.       GamepadIn = NULL;
  69.       ESP_LOGI("", "usb_host_transfer_alloc In fail: %x", err);
  70.       return;
  71.     }
  72.     GamepadIn->device_handle = Device_Handle;
  73.     GamepadIn->bEndpointAddress = endpoint->bEndpointAddress;
  74.     GamepadIn->callback = Gamepad_transfer_cb;
  75.     GamepadIn->context = NULL;
  76.     isGamepadReady = true;
  77.     GamepadInterval = endpoint->bInterval;
  78.     ESP_LOGI("", "USB boot Gamepad ready");
  79.   }
  80.   else {
  81.     ESP_LOGI("", "Ignoring interrupt Out endpoint");
  82.   }
  83. }
  84. void show_config_desc_full(const usb_config_desc_t *config_desc)
  85. {
  86.   // Full decode of config desc.
  87.   const uint8_t *p = &config_desc->val[0];
  88.   static uint8_t USB_Class = 0;
  89.   uint8_t bLength;
  90.   for (int i = 0; i < config_desc->wTotalLength; i += bLength, p += bLength) {
  91.     bLength = *p;
  92.     if ((i + bLength) <= config_desc->wTotalLength) {
  93.       const uint8_t bDescriptorType = *(p + 1);
  94.       switch (bDescriptorType) {
  95.         case USB_B_DESCRIPTOR_TYPE_DEVICE:
  96.           ESP_LOGI("", "USB Device Descriptor should not appear in config");
  97.           break;
  98.         case USB_B_DESCRIPTOR_TYPE_CONFIGURATION:
  99.           show_config_desc(p);
  100.           break;
  101.         case USB_B_DESCRIPTOR_TYPE_STRING:
  102.           ESP_LOGI("", "USB string desc TBD");
  103.           break;
  104.         case USB_B_DESCRIPTOR_TYPE_INTERFACE:
  105.           USB_Class = show_interface_desc(p);
  106.           check_interface_desc_boot_Gamepad(p);
  107.           break;
  108.         case USB_B_DESCRIPTOR_TYPE_ENDPOINT:
  109.           show_endpoint_desc(p);
  110.           if (isGamepad && GamepadIn == NULL) prepare_endpoint(p);
  111.           break;
  112.         case USB_B_DESCRIPTOR_TYPE_DEVICE_QUALIFIER:
  113.           // Should not be config config?
  114.           ESP_LOGI("", "USB device qual desc TBD");
  115.           break;
  116.         case USB_B_DESCRIPTOR_TYPE_OTHER_SPEED_CONFIGURATION:
  117.           // Should not be config config?
  118.           ESP_LOGI("", "USB Other Speed TBD");
  119.           break;
  120.         case USB_B_DESCRIPTOR_TYPE_INTERFACE_POWER:
  121.           // Should not be config config?
  122.           ESP_LOGI("", "USB Interface Power TBD");
  123.           break;
  124.         case 0x21:
  125.           if (USB_Class == USB_CLASS_HID) {
  126.             show_hid_desc(p);
  127.           }
  128.           break;
  129.         default:
  130.           ESP_LOGI("", "Unknown USB Descriptor Type: 0x%x", bDescriptorType);
  131.           break;
  132.       }
  133.     }
  134.     else {
  135.       ESP_LOGI("", "USB Descriptor invalid");
  136.       return;
  137.     }
  138.   }
  139. }
  140. void setup()
  141. {
  142.   usbh_setup(show_config_desc_full);
  143.   bleGamepad.begin();
  144. }
  145. void loop()
  146. {
  147.   usbh_task();
  148.   if (isGamepadReady && !isGamepadPolling && (GamepadTimer > GamepadInterval)) {
  149.     GamepadIn->num_bytes = 8;
  150.     esp_err_t err = usb_host_transfer_submit(GamepadIn);
  151.     if (err != ESP_OK) {
  152.       ESP_LOGI("", "usb_host_transfer_submit In fail: %x", err);
  153.     }
  154.     isGamepadPolling = true;
  155.     GamepadTimer = 0;
  156.   }
  157. }
复制代码
参考:
2.     在线 HID Descriptor 分析工具 http://eleccelerator.com/usbdescreqparser/

zoologist  高级技匠
 楼主|

发表于 2022-9-26 16:03:13

修改后的库

完整的代码:

回复

使用道具 举报

glwz007  初级技匠

发表于 2022-10-16 15:22:16

我有一个2.4G的无线键盘,接收器丢了,能不能反向改装为有线键盘?
回复

使用道具 举报

zoologist  高级技匠
 楼主|

发表于 2022-10-16 19:09:47

glwz007 发表于 2022-10-16 15:22
我有一个2.4G的无线键盘,接收器丢了,能不能反向改装为有线键盘?

这个应该可以,只是比较折腾,拆开之后重新焊接解析键盘矩阵

更简单的方法是直接买个同型号的接收器,配对一下就能用
回复

使用道具 举报

赤星三春牛!  初级技神

发表于 2022-10-23 09:36:15

厉害厉害
回复

使用道具 举报

赤星三春牛!  初级技神

发表于 2022-10-23 09:37:18

回复

使用道具 举报

赤星三春牛!  初级技神

发表于 2022-10-23 09:38:20

点赞!!
回复

使用道具 举报

zoologist  高级技匠
 楼主|

发表于 2023-6-24 10:12:41

修改后的库

Modified——ESP32-BLE-Gamepad-master.zip

26.04 KB, 下载次数: 62

回复

使用道具 举报

SamToT  学徒

发表于 2024-4-16 17:24:27

为什么不用代码直接获取设备描述符然后替换到HIDReport Map中呢
回复

使用道具 举报

zoologist  高级技匠
 楼主|

发表于 2024-4-18 11:08:00

SamToT 发表于 2024-4-16 17:24
为什么不用代码直接获取设备描述符然后替换到HIDReport Map中呢

可以,应该没问题
回复

使用道具 举报

_深蓝_  中级技师

发表于 7 天前

很显然,我们小白是看不懂的,膜拜
回复

使用道具 举报

您需要登录后才可以回帖 登录 | 立即注册

本版积分规则

为本项目制作心愿单
购买心愿单
心愿单 编辑
[[wsData.name]]

硬件清单

  • [[d.name]]
btnicon
我也要做!
点击进入购买页面
上海智位机器人股份有限公司 沪ICP备09038501号-4

© 2013-2024 Comsenz Inc. Powered by Discuz! X3.4 Licensed

mail