AgileVR Bluetooth Firmware

AgileVR Bluetooth Firmware

Microchip's Curiosity PIC32MZEF + BM71 PICtail


https://www.microchip.com/developmenttools/ProductDetails/DM320104

https://www.microchip.com/developmenttools/ProductDetails/PartNO/BM-71-PICtail

BM71 Stopwatch Demo

The BM71 Stopwatch Demo is the only example showing how to configure/deploy a custom (private) GATT Service. This example however does not demonstrate how to implement a HID over GATT device so it will take some extra work to implement. The example was created for the PIC24 MCU / Explorer15 Development Board and MPLAB XC16 Compiler so we need to convert the example to work with the PIC32 MCU / Curiosity PIC32MZEF Development Board and the MPLAB XC32 Compiler.

When we look at the example, we can see the custom services used in it.

The example shows how to send the stopwatch state and time.

As you can see the packet size is 8 bytes with the first 3 containing the connection handle and characteristic value handle. The last 5 bytes contain the state (start, stop, lap, reset) and stopwatch time.

AgileVR

The AgileVR hardware needs to be detected as a HID device. The way to do this is to use HID over GATT Profile (HOGP).

HID over GATT Profile (HOGP)

The minimum service requirements for a HID Device:

  • Device Information Service
  • Human Interface Device Service
  • Battery Service
  • Scan Parameters Service (Optional)

HID over GATT Attribute Table (Example)

BLEDK3 UI Configuration Tool

Using the UI Configuration Tool, we can set the services to meet the HOGP service requirements. NOTE: For 16-bit values the bytes need to be swapped.

Built-In Service

Appearance (0x2A01)

This data is made up of 1 part:

  • Source (16 bits)
    • Value: 0x03C0 -- Generic HID
    • Value: 0x03C1 -- Keyboard
    • Value: 0x03C2 -- Mouse
    • Value: 0x03C3 -- Joystick
    • Value: 0x03C4 -- Gamepad
    • Value: 0x03C5 -- Tablet
    • Value: 0x03C6 -- Card Reader
    • Value: 0x03C7 -- Digital Pen
    • Value: 0x03C8 -- Barcode

Add-On Service Table

Device Information Service (0x180A)

PnP ID (0x2A50)

The PnP ID characteristic is a set of values used to create a device ID value that is unique for this device.

This data is made up of 4 parts:

  • Source (8 bits)
    • Value: 0x02 -- USB
  • Vendor ID (16 bits)
    • Value: 0x04D8 -- Microchip
  • Product ID (16 bits)
    • Value: 0xED55 -- AgileVR
  • Version (16 bits)
    • Value: 0x0000

Human Interface Device Service (0x1812)

Report (0x2A4D)

The Report characteristic value contains Input Report, Output Report or Feature Report data to be transferred between the HID Device and HID Host.

The Report characteristic is used as a vehicle for HID reports. Unlike USB, where the ID is sent as a prefix when there is more than one report per type, the ID is stored in a characteristic descriptor. This means that there will be one characteristic per report described in the Report Map.

Client Characteristic Configuration (0x2902)

The Client Characteristic Configuration declaration is an optional characteristic descriptor that defines how the characteristic may be configured by a specific client. ie. Enable or disable notifications and indications.

This data is made up of 1 part:

  • Notification (16 bits)
    • Value: 0x0000 -- Disable Notifications & Indications
    • Value: 0x0001 -- Get Notifications
    • Value: 0x0002 -- Get Indications
    • Value: 0x0003 -- Get Notifications & Indications

Report Reference (0x2908)

This data is made up of 2 parts:

  • Report ID (8 bits)
    • Value: 0x01
  • Report Type (8 bits)
    • Value: 0x01 -- Input Report
    • Value: 0x02 -- Output Report
    • Value: 0x03 -- Feature Report

Report Map (0x2A4B)

The Report Map characteristic value contains formatting and other information for Input Report, Output Report and Feature Report data transferred between a HID Device and HID Host.

Here we have the report descriptor for the AgileVR. For testing purposes, we will use the same one as the USB version of AgileVR but with the addition of a Report ID. It contains an input and output report with 64 bytes of data and looks like this:

0x06, 0x00, 0xFF,  // Usage Page (Vendor Defined 0xFF00)
0x09, 0x01,        // Usage (0x01)
0xA1, 0x01,        // Collection (Application)
0x85, 0x01,        //   Report ID (1)
0x19, 0x01,        //   Usage Minimum (0x01)
0x29, 0x40,        //   Usage Maximum (0x40)
0x15, 0x01,        //   Logical Minimum (1)
0x25, 0x40,        //   Logical Maximum (64)
0x75, 0x08,        //   Report Size (8)
0x95, 0x40,        //   Report Count (64)
0x81, 0x00,        //   Input (Data,Array,Abs,No Wrap,Linear,Preferred State,No Null Position)
0x19, 0x01,        //   Usage Minimum (0x01)
0x29, 0x40,        //   Usage Maximum (0x40)
0x91, 0x00,        //   Output (Data,Array,Abs,No Wrap,Linear,Preferred State,No Null Position,Non-volatile)
0xC0,              // End Collection

Further iterations of the firmware will require a more complex report descriptor using multiple report ID's and potentially adding a gamepad and haptic feedback descriptors.

The report size is 30 bytes but the ATT_MTU (Maximum Transmission Unit) for a Bluetooth LE attribute is 23 bytes.

0600FF0901A10185011901294015012540750895408100190129409100C0

We need to send 22 bytes and then the remaining 8 bytes in a separate chunk.

There is a setting in UI Configurator under System Setup -> BT 4.2 Features called DLE (Data Length Extension) which allows ATT_MTU larger than 23 bytes but we don't want to force BT 4.2 compatibility. If we use a larger ATT_MTU we must implement ESP_GATTS_MTU_EVT. Since we would prefer not to use a larger ATT_MTU we must send the Report Map data in multiple chunks.

Nordic Semiconductor's SDK 16.0.0 HID

There is an example in Nordic's SDK 16.0.0 called 'HID Keyboard Application' which demonstrates using HID over GATT with the nRF5 series SoC's.

https://infocenter.nordicsemi.com/topic/sdk_nrf5_v16.0.0/ble_sdk_app_hids_keyboard.html?cp=7_1_4_2_2_15

Looking at the communication using Wireshark (nRF Sniffer) we can see the entire HID Report Map (consisting of 76 bytes in the report_map_data[] array) is transferred in 4 chunks (segments, at packets # 1614, 1618, 1622 and 1626) and then reassembled:

From this we can conclude that it's possible to send a larger Report Map with the ATT_MTU set to 23 bytes.

Using nRF Sniffer to examine a Report Map (0x2A4B) comms:

Using nRF Sniffer with the Flydigi Apex Gamepad which uses the nRF51 Series SoC. Instructions are available here.

Frame 820 starts with a Read Request (0x0a) for the Report Map (0x2A4B):

Frame 820: 33 bytes on wire (264 bits), 33 bytes captured (264 bits) on interface wireshark_extcap2912, id 0
...
Bluetooth Attribute Protocol
   Opcode: Read Request (0x0a)
       0... .... = Authentication Signature: False
       .0.. .... = Command: False
       ..00 1010 = Method: Read Request (0x0a)
   Handle: 0x0028 (Human Interface Device: Report Map)
       [Service UUID: Human Interface Device (0x1812)]
       [UUID: Report Map (0x2a4b)]

0000 06 1a 00 02 2c 0c 06 0a 0f 06 2c b9 00 64 2a 00  ....,.....,..d*.
0010 00 ea 7a 65 50 02 07 03 00 04 00 0a 28 00 b2 ee  ..zeP.......(...
0020 df

Frame 823 is the Read Response (0x0b). You will notice we receive the Value and that the ATT_MTU (Maximum Transmission Unit) has been reached:

Frame 823: 53 bytes on wire (424 bits), 53 bytes captured (424 bits) on interface wireshark_extcap2912, id 0
....
Bluetooth Attribute Protocol
   Opcode: Read Response (0x0b)
       0... .... = Authentication Signature: False
       .0.. .... = Command: False
       ..00 1011 = Method: Read Response (0x0b)
   [Handle: 0x0028 (Human Interface Device: Report Map)]
       [Service UUID: Human Interface Device (0x1812)]
       [UUID: Report Map (0x2a4b)]
   Value: 05010905a1018501a10209300931093209351580257f
       [Expert Info (Note/Protocol): Reached ATT_MTU. Attribute value may be longer.]
           [Reached ATT_MTU. Attribute value may be longer.]
           [Severity level: Note]
           [Group: Protocol]
   [Request in Frame: 820]

0000 06 2e 00 02 2f 0c 06 0a 0d 0c 2d ba 00 95 00 00  ..../.....-.....
0010 00 ea 7a 65 50 0a 1b 17 00 04 00 0b 05 01 09 05  ..zeP...........
0020 a1 01 85 01 a1 02 09 30 09 31 09 32 09 35 15 80  .......0.1.2.5..
0030 25 7f c6 ee 25                                   %...%

In Frame 824 you can see there is a Read Blob Request (0x0c) at Offset 22:

Frame 824: 35 bytes on wire (280 bits), 35 bytes captured (280 bits) on interface wireshark_extcap2912, id 0
...
Bluetooth Attribute Protocol
   Opcode: Read Blob Request (0x0c)
       0... .... = Authentication Signature: False
       .0.. .... = Command: False
       ..00 1100 = Method: Read Blob Request (0x0c)
   Handle: 0x0028 (Human Interface Device: Report Map)
       [Service UUID: Human Interface Device (0x1812)]
       [UUID: Report Map (0x2a4b)]
   Offset: 22

0000 06 1c 00 02 30 0c 06 0a 0f 12 2c bb 00 ea 37 00  ....0.....,...7.
0010 00 ea 7a 65 50 02 09 05 00 04 00 0c 28 00 16 00  ..zeP.......(...
0020 01 f5 ba                                         ...

Frame 827 we receive the Read Blob Response (0x0d) and the Value at Offset 22:

Frame 827: 53 bytes on wire (424 bits), 53 bytes captured (424 bits) on interface wireshark_extcap2912, id 0
...
Bluetooth Attribute Protocol
   Opcode: Read Blob Response (0x0d)
       0... .... = Authentication Signature: False
       .0.. .... = Command: False
       ..00 1101 = Method: Read Blob Response (0x0d)
   [Handle: 0x0028 (Human Interface Device: Report Map)]
       [Service UUID: Human Interface Device (0x1812)]
       [UUID: Report Map (0x2a4b)]
   Value: 3580457f7508950481020940094109420943150026ff
       [Expert Info (Note/Protocol): Reached ATT_MTU. Attribute value may be longer.]
           [Reached ATT_MTU. Attribute value may be longer.]
           [Severity level: Note]
           [Group: Protocol]
   [Request in Frame: 824]

0000 06 2e 00 02 33 0c 06 0a 0d 18 2c bc 00 96 00 00  ....3.....,.....
0010 00 ea 7a 65 50 0a 1b 17 00 04 00 0d 35 80 45 7f  ..zeP.......5.E.
0020 75 08 95 04 81 02 09 40 09 41 09 42 09 43 15 00  u......@.A.B.C..
0030 26 ff 2c 39 4a                                   &.,9J

In conclusion:

  1. Master will send a Read Request (0x0a) for the Report Map (0x2A4B)
  2. Slave will send a Read Response (0x0b) (if Value has reached ATT_MTU the attribute value may be longer)
  3. Master will send a Read Blob Request (0x0c) for the Report Map (0x2A4B) at Offset(s) 22, 44, 66, 88, 110 etc.
  4. Slave will send a Read Blob Response (0x0d) with the Report Map (0x2A4B) Value at the Offset until the entire attribute Value can be reassembled

External Report Reference (0x2907)

The External Report Reference characteristic descriptor allows a HID Host to map information from the Report Map characteristic value for Input Report, Output Report or Feature Report data to the Characteristic UUID of external service characteristics used to transfer the associated data.

HID Information (0x2A4A)

The HID Information characteristic is used to hold a set of values known as the HID Device\'s HID Attributes.

This data is made up of 3 parts:

  • HID Version (16 bits)
    • Value: 0x0101 -- 1.01
  • Country Code (8 bits)
    • Value: 0x00 -- Not Supported
  • Flags (8 bits)
    • Value: 0x03 -- Normal Connect | Remote Wake

HID Control Point (0x2A4C)

The HID Control Point characteristic is a control-point attribute that defines the following HID Commands when written. Refer to Section 7.4.2, Bluetooth HID Profile Specification 1.0.

This data is made up of 1 part:

  • Suspend (8 bits)
    • Value: 0x00 -- Suspend
    • Value: 0x01 -- Exit Suspend

Battery Service (0x180F)

The Battery Service exposes the Battery State and Battery Level of a single battery or set of batteries in a device.

Battery Level (0x2A19)

The Battery Level characteristic is read using the GATT Read Characteristic Value sub-procedure and returns the current battery level as a percentage from 0% to 100%; 0% represents a battery that is fully discharged, 100% represents a battery that is fully charged.

This data is made up of 1 part:

  • Battery Level (8 bits)
    • Value: 0x00 to 0x40 -- 0% to 100%

Flydigi\'s Apex Gamepad (BT LE / 2.4GHz WiFi)

The 'Apex' gamepad from China's Flydigi employs Nordic Semiconductor's nRF51 Series SoCs. It uses Bluetooth LE and we can examine its GATT service table to learn how to implement such a device.

For more information please read the following article:

https://www.nordicsemi.com/News/2017/12/The-Apex-gamepad-from-Chinas-Flydigi-employs-Nordics-nRF51-Series-SoC

Apex Controller Operation Modes

One of the most interesting aspects of the Apex controller is its 3 modes of operation:

  • Bluetooth (LE)
  • Wireless Android (USB Wireless Receiver)
  • Xinput (USB Wireless Receiver)

The nRF51 SoCs are multiprotocol devices that also support 2.4GHz proprietary protocols, non-Bluetooth technology TVs and PCs hosting games can be controlled using a 2.4GHz USB dongle. Why does Flydigi require the 2.4GHz mode to use Xinput? Perhaps Xinput mode is not possible using Bluetooth LE? Or perhaps they're using the two modes to hide the fact they're using the Microsoft Vendor ID?

Apex Battery Life

There is substantial battery saving by the use of Bluetooth LE technology. Here are the stats for the Apex controller:

  • Bluetooth LE Mode -- 160 hours
  • Wireless Android Mode -- 40 hours
  • Xinput Mode -- 60 hours

Apex Xinput Mode (USB Wireless Receiver)

This mode of operation uses a 2.4Ghz USB dongle.

When paired it has the following PnP data:

  • Product Name: Controller (Flydigi 2.4G x360)
  • Vendor ID: 0x045E (Microsoft)
  • Product ID: 0x028E (Xbox360 Controller)
  • Input Report Length: 15 Bytes

Running XInput Test we can see that it is indeed detected as an Xbox Controller.

Using Thesycon USB Descriptor Dumper we can get the Device Descriptor and Configuration Descriptor.

Information for device Flydigi 2.4G x360 (VID=0x045E PID=0x028E):

Connection Information:
------------------------------
Device current bus speed: FullSpeed
Device supports USB 1.1 specification
Device supports USB 2.0 specification
Device address: 0x001B
Current configuration value: 0x01
Number of open pipes: 5

Device Descriptor:
------------------------------
0x12    bLength
0x01    bDescriptorType
0x0200  bcdUSB
0xFF    bDeviceClass      (Vendor specific)
0xFF    bDeviceSubClass
0xFF    bDeviceProtocol
0x40    bMaxPacketSize0   (64 bytes)
0x045E  idVendor
0x028E  idProduct
0x0104  bcdDevice
0x01    iManufacturer   "Flydigi"
0x02    iProduct   "Flydigi 2.4G x360"
0x03    iSerialNumber
0x01    bNumConfigurations
Hex dump:
0x12 0x01 0x00 0x02 0xFF 0xFF 0xFF 0x40 0x5E 0x04
0x8E 0x02 0x04 0x01 0x01 0x02 0x03 0x01

Configuration Descriptor:
------------------------------
0x09    bLength
0x02    bDescriptorType
0x008B  wTotalLength   (139 bytes)
0x04    bNumInterfaces
0x01    bConfigurationValue
0x00    iConfiguration
0xA0    bmAttributes   (Bus-powered Device, Remote-Wakeup)
0xFA    bMaxPower      (500 mA)
Hex dump:
0x09 0x02 0x8B 0x00 0x04 0x01 0x00 0xA0 0xFA

Interface Descriptor:
------------------------------
0x09    bLength
0x04    bDescriptorType
0x00    bInterfaceNumber
0x00    bAlternateSetting
0x02    bNumEndPoints
0xFF    bInterfaceClass      (Vendor specific)
0x5D    bInterfaceSubClass
0x01    bInterfaceProtocol
0x00    iInterface
Hex dump:
0x09 0x04 0x00 0x00 0x02 0xFF 0x5D 0x01 0x00

Unknown Descriptor:
------------------------------
0x11    bLength
0x21    bDescriptorType
Hex dump:
0x11 0x21 0x10 0x01 0x01 0x25 0x81 0x14 0x03 0x03
0x03 0x04 0x13 0x02 0x08 0x03 0x03

Endpoint Descriptor:
------------------------------
0x07    bLength
0x05    bDescriptorType
0x81    bEndpointAddress  (IN endpoint 1)
0x03    bmAttributes      (Transfer: Interrupt / Synch: None / Usage: Data)
0x0040  wMaxPacketSize    (1 x 64 bytes)
0x0A    bInterval         (10 frames)
Hex dump:
0x07 0x05 0x81 0x03 0x40 0x00 0x0A

Endpoint Descriptor:
------------------------------
0x07    bLength
0x05    bDescriptorType
0x02    bEndpointAddress  (OUT endpoint 2)
0x03    bmAttributes      (Transfer: Interrupt / Synch: None / Usage: Data)
0x0040  wMaxPacketSize    (1 x 64 bytes)
0x0A    bInterval         (10 frames)
Hex dump:
0x07 0x05 0x02 0x03 0x40 0x00 0x0A

Interface Descriptor:
------------------------------
0x09    bLength
0x04    bDescriptorType
0x01    bInterfaceNumber
0x00    bAlternateSetting
0x02    bNumEndPoints
0xFF    bInterfaceClass      (Vendor specific)
0x5D    bInterfaceSubClass
0x03    bInterfaceProtocol
0x00    iInterface
Hex dump:
0x09 0x04 0x01 0x00 0x02 0xFF 0x5D 0x03 0x00

Unknown Descriptor:
------------------------------
0x1B    bLength
0x21    bDescriptorType
Hex dump:
0x1B 0x21 0x00 0x01 0x01 0x01 0x83 0x40 0x01 0x04
0x20 0x16 0x85 0x00 0x00 0x00 0x00 0x00 0x00 0x16
0x05 0x00 0x00 0x00 0x00 0x00 0x00

Endpoint Descriptor:
------------------------------
0x07    bLength
0x05    bDescriptorType
0x83    bEndpointAddress  (IN endpoint 3)
0x03    bmAttributes      (Transfer: Interrupt / Synch: None / Usage: Data)
0x0020  wMaxPacketSize    (1 x 32 bytes)
0x02    bInterval         (2 frames)
Hex dump:
0x07 0x05 0x83 0x03 0x20 0x00 0x02

Endpoint Descriptor:
------------------------------
0x07    bLength
0x05    bDescriptorType
0x04    bEndpointAddress  (OUT endpoint 4)
0x03    bmAttributes      (Transfer: Interrupt / Synch: None / Usage: Data)
0x0020  wMaxPacketSize    (1 x 32 bytes)
0x04    bInterval         (4 frames)
Hex dump:
0x07 0x05 0x04 0x03 0x20 0x00 0x04

Interface Descriptor:
------------------------------
0x09    bLength
0x04    bDescriptorType
0x02    bInterfaceNumber
0x00    bAlternateSetting
0x01    bNumEndPoints
0xFF    bInterfaceClass      (Vendor specific)
0x5D    bInterfaceSubClass
0x02    bInterfaceProtocol
0x00    iInterface
Hex dump:
0x09 0x04 0x02 0x00 0x01 0xFF 0x5D 0x02 0x00

Unknown Descriptor:
------------------------------
0x09    bLength
0x21    bDescriptorType
Hex dump:
0x09 0x21 0x00 0x01 0x01 0x22 0x86 0x07 0x00

Endpoint Descriptor:
------------------------------
0x07    bLength
0x05    bDescriptorType
0x85    bEndpointAddress  (IN endpoint 5)
0x03    bmAttributes      (Transfer: Interrupt / Synch: None / Usage: Data)
0x0020  wMaxPacketSize    (1 x 32 bytes)
0x10    bInterval         (16 frames)
Hex dump:
0x07 0x05 0x85 0x03 0x20 0x00 0x10

Interface Descriptor:
------------------------------
0x09    bLength
0x04    bDescriptorType
0x03    bInterfaceNumber
0x00    bAlternateSetting
0x00    bNumEndPoints
0xFF    bInterfaceClass      (Vendor specific)
0xFD    bInterfaceSubClass
0x13    bInterfaceProtocol
0x04    iInterface
Hex dump:
0x09 0x04 0x03 0x00 0x00 0xFF 0xFD 0x13 0x04

Unknown Descriptor:
------------------------------
0x06    bLength
0x41    bDescriptorType
Hex dump:
0x06 0x41 0x00 0x01 0x01 0x03

Microsoft OS Descriptor is not available. Error code: 0x0000001F

String Descriptor Table
--------------------------------
Index  LANGID  String
0x00   0x0000  0x0409
Hex dump:
0x04 0x03 0x09 0x04

0x01   0x0409  "Flydigi"
Hex dump:
0x10 0x03 0x46 0x00 0x6C 0x00 0x79 0x00 0x64 0x00
0x69 0x00 0x67 0x00 0x69 0x00

0x02   0x0409  "Flydigi 2.4G x360"
Hex dump:
0x24 0x03 0x46 0x00 0x6C 0x00 0x79 0x00 0x64 0x00
0x69 0x00 0x67 0x00 0x69 0x00 0x20 0x00 0x32 0x00
0x2E 0x00 0x34 0x00 0x47 0x00 0x20 0x00 0x78 0x00
0x33 0x00 0x36 0x00 0x30 0x00

0x03   0x0409  Request failed with 0x0000001F
Hex dump:

0x04   0x0409  Request failed with 0x0000001F
Hex dump:

------------------------------

Whole Device Descriptor as hex dump:
0x12, 0x01, 0x00, 0x02, 0xFF, 0xFF, 0xFF, 0x40, 0x5E, 0x04,
0x8E, 0x02, 0x04, 0x01, 0x01, 0x02, 0x03, 0x01

Whole Configuration Descriptor as hex dump:
0x09, 0x02, 0x8B, 0x00, 0x04, 0x01, 0x00, 0xA0, 0xFA, 0x09,
0x04, 0x00, 0x00, 0x02, 0xFF, 0x5D, 0x01, 0x00, 0x11, 0x21,
0x10, 0x01, 0x01, 0x25, 0x81, 0x14, 0x03, 0x03, 0x03, 0x04,
0x13, 0x02, 0x08, 0x03, 0x03, 0x07, 0x05, 0x81, 0x03, 0x40,
0x00, 0x0A, 0x07, 0x05, 0x02, 0x03, 0x40, 0x00, 0x0A, 0x09,
0x04, 0x01, 0x00, 0x02, 0xFF, 0x5D, 0x03, 0x00, 0x1B, 0x21,
0x00, 0x01, 0x01, 0x01, 0x83, 0x40, 0x01, 0x04, 0x20, 0x16,
0x85, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x16, 0x05, 0x00,
0x00, 0x00, 0x00, 0x00, 0x00, 0x07, 0x05, 0x83, 0x03, 0x20,
0x00, 0x02, 0x07, 0x05, 0x04, 0x03, 0x20, 0x00, 0x04, 0x09,
0x04, 0x02, 0x00, 0x01, 0xFF, 0x5D, 0x02, 0x00, 0x09, 0x21,
0x00, 0x01, 0x01, 0x22, 0x86, 0x07, 0x00, 0x07, 0x05, 0x85,
0x03, 0x20, 0x00, 0x10, 0x09, 0x04, 0x03, 0x00, 0x00, 0xFF,
0xFD, 0x13, 0x04, 0x06, 0x41, 0x00, 0x01, 0x01, 0x03

------------------------------

Connection path for device:
USB xHCI Compliant Host Controller
Root Hub
Flydigi 2.4G x360 (VID=0x045E PID=0x028E) Port: 2

Running on: Windows 10 or greater (Build Version 18363)

Brought to you by TDD v2.12.0, Aug  2 2019, 16:35:05

Apex Bluetooth LE Mode

This mode of operation uses Bluetooth LE.

When paired it has the following PnP data:

  • Product Name: 10 axis 12 button gamepad with hat switch
  • Vendor ID: 0x1915 (Nordic Semiconductor)
  • Product ID: 0x0040
  • Input Report Length: 13 Bytes

Tools

There are several tools available which can help us decode Bluetooth LE GATT Services and Characteristics.

Windows 10

The problem with the Windows 10 Bluetooth LE API is it does not allow decoding of HID Service Characteristics.

  • Bluetooth LE Explorer
  • Bluetooth LE Lab

Android

Android apps can decode the HID Service Characteristic names but it cannot read any values. This is due to the BLUETOOTH_PRIVILEGED permission requirement which is not available to third-party applications.

  • nRF Connect

I 13:18:27.645 Connected to C8:66:9D:B9:08:CA
D 13:18:27.647 wait(1600ms)
I 13:18:27.684 Connection parameters updated (interval: 15.0ms, latency: 1, timeout: 3000ms)
I 13:18:28.285 Connection parameters updated (interval: 15.0ms, latency: 1, timeout: 3000ms)
V 13:18:29.248 Discovering services...
D 13:18:29.248 gatt.discoverServices()
D 13:18:29.269 [Callback] Services discovered with status: 0
I 13:18:29.269 Services discovered
V 13:18:29.308 Generic Access (0x1800)
- Device Name [R W] (0x2A00)
- Appearance [R] (0x2A01)
- Peripheral Preferred Connection Parameters [R] (0x2A04)
Generic Attribute (0x1801)
- Service Changed [I] (0x2A05)
   Client Characteristic Configuration (0x2902)
Device Information (0x180A)
- Manufacturer Name String [R] (0x2A29)
- Firmware Revision String [R] (0x2A26)
- PnP ID [R] (0x2A50)
Unknown Service (00001000-e619-419b-bc43-821e71a409b7)
- Unknown Characteristic [N] (00001002-e619-419b-bc43-821e71a409b7)
   Client Characteristic Configuration (0x2902)
- Unknown Characteristic [W WNR] (00001004-e619-419b-bc43-821e71a409b7)
- Unknown Characteristic [N W WNR] (00001001-e619-419b-bc43-821e71a409b7)
   Client Characteristic Configuration (0x2902)
Nordic UART Service (6e400001-b5a3-f393-e0a9-e50e24dcca9e)
- TX Characteristic [N] (6e400003-b5a3-f393-e0a9-e50e24dcca9e)
   Client Characteristic Configuration (0x2902)
- RX Characteristic [W WNR] (6e400002-b5a3-f393-e0a9-e50e24dcca9e)
Human Interface Device (0x1812)
- Report [N R W] (0x2A4D)
   Client Characteristic Configuration (0x2902)
   Report Reference (0x2908)
- Report Map [R] (0x2A4B)
- HID Information [R] (0x2A4A)
- HID Control Point [WNR] (0x2A4C)
D 13:18:29.308 gatt.setCharacteristicNotification(00002a05-0000-1000-8000-00805f9b34fb, true)
D 13:18:29.310 gatt.setCharacteristicNotification(00001002-e619-419b-bc43-821e71a409b7, true)
D 13:18:29.311 gatt.setCharacteristicNotification(00001001-e619-419b-bc43-821e71a409b7, true)
D 13:18:29.314 gatt.setCharacteristicNotification(6e400003-b5a3-f393-e0a9-e50e24dcca9e, true)
D 13:18:29.315 gatt.setCharacteristicNotification(00002a4d-0000-1000-8000-00805f9b34fb, true)
V 13:28:03.967 Reading all characteristics...
V 13:28:03.967 Reading characteristic 00002a00-0000-1000-8000-00805f9b34fb
D 13:28:03.967 gatt.readCharacteristic(00002a00-0000-1000-8000-00805f9b34fb)
I 13:28:04.026 Read Response received from 00002a00-0000-1000-8000-00805f9b34fb, value: (0x) 46-65-69-5A-68-69-41-50-45-58, "FeiZhiAPEX"
A 13:28:04.026 "FeiZhiAPEX" received
V 13:28:04.032 Reading characteristic 00002a01-0000-1000-8000-00805f9b34fb
D 13:28:04.032 gatt.readCharacteristic(00002a01-0000-1000-8000-00805f9b34fb)
I 13:28:04.053 Read Response received from 00002a01-0000-1000-8000-00805f9b34fb, value: (0x) C2-03
A 13:28:04.053 "[962] Mouse (HID subtype)" received
V 13:28:04.066 Reading characteristic 00002a04-0000-1000-8000-00805f9b34fb
D 13:28:04.066 gatt.readCharacteristic(00002a04-0000-1000-8000-00805f9b34fb)
I 13:28:04.113 Read Response received from 00002a04-0000-1000-8000-00805f9b34fb, value: (0x) 0C-00-0C-00-01-00-2C-01
A 13:28:04.113 "Connection Interval: 15.00ms - 15.00ms,
Slave Latency: 1,
Supervision Timeout Multiplier: 300" received
V 13:28:04.120 Reading characteristic 00002a29-0000-1000-8000-00805f9b34fb
D 13:28:04.120 gatt.readCharacteristic(00002a29-0000-1000-8000-00805f9b34fb)
I 13:28:04.143 Read Response received from 00002a29-0000-1000-8000-00805f9b34fb, value: (0x) 66-6C-79-64-69-67-69, "flydigi"
A 13:28:04.143 "flydigi" received
V 13:28:04.150 Reading characteristic 00002a26-0000-1000-8000-00805f9b34fb
D 13:28:04.150 gatt.readCharacteristic(00002a26-0000-1000-8000-00805f9b34fb)
I 13:28:04.173 Read Response received from 00002a26-0000-1000-8000-00805f9b34fb, value: (0x) 35-2E-38-2E-32-2E-30, "5.8.2.0"
A 13:28:04.173 "5.8.2.0" received
V 13:28:04.182 Reading characteristic 00002a50-0000-1000-8000-00805f9b34fb
D 13:28:04.182 gatt.readCharacteristic(00002a50-0000-1000-8000-00805f9b34fb)
I 13:28:04.204 Read Response received from 00002a50-0000-1000-8000-00805f9b34fb, value: (0x) 02-15-19-40-00-01-00
A 13:28:04.204 "USB Implementer's Forum Vendor ID: 6421 (0x1915)
Product Id: 64
Product Version: 1" received
V 13:28:04.213 Reading characteristic 00002a4d-0000-1000-8000-00805f9b34fb
D 13:28:04.213 gatt.readCharacteristic(00002a4d-0000-1000-8000-00805f9b34fb)
E 13:28:04.213 Exception occurred (BLUETOOTH_PRIVILEGED permission required)
V 13:28:04.220 Reading characteristic 00002a4b-0000-1000-8000-00805f9b34fb
D 13:28:04.220 gatt.readCharacteristic(00002a4b-0000-1000-8000-00805f9b34fb)
E 13:28:04.220 Exception occurred (BLUETOOTH_PRIVILEGED permission required)
V 13:28:04.226 Reading characteristic 00002a4a-0000-1000-8000-00805f9b34fb
D 13:28:04.226 gatt.readCharacteristic(00002a4a-0000-1000-8000-00805f9b34fb)
E 13:28:04.226 Exception occurred (BLUETOOTH_PRIVILEGED permission required)
V 13:28:04.232 9 characteristics read

Raspberry Pi (3 B+)

This is a useful device for decoding GATT Service information as it has full access to all GATT Service Characteristics and values. Once we have installed BlueZ on the Raspberry Pi we can use an app called 'gatttool' to decode the data.

  • BlueZ / gatttool

Using gatttool to Decode GATT Services

Once you pair the 'Apex' gamepad to the Raspberry Pi you should first check to see if there is an existing connection that has been established:

$ hcitool con
Connections:
    < Unknown C8:66:9D:B9:08:CA handle 64 state 1 lm MASTER

If there is a connection you will need to disconnect:

$ sudo hcitool ledc 64

Next run the gatttool as follows:

$ sudo gatttool -t random -b c8:66:9d:b9:08:ca -I
[c8:66:9d:b9:08:ca][LE]> connect
Attempting to connect to c8:66:9d:b9:08:ca
Connection successful
[c8:66:9d:b9:08:ca][LE]> primary
attr handle: 0x0001, end grp handle: 0x0007 uuid: 00001800-0000-1000-8000-00805f9b34fb
attr handle: 0x0008, end grp handle: 0x000b uuid: 00001801-0000-1000-8000-00805f9b34fb
attr handle: 0x000c, end grp handle: 0x0012 uuid: 0000180a-0000-1000-8000-00805f9b34fb
attr handle: 0x0013, end grp handle: 0x001b uuid: 00001000-e619-419b-bc43-821e71a409b7
attr handle: 0x001c, end grp handle: 0x0021 uuid: 6e400001-b5a3-f393-e0a9-e50e24dcca9e
attr handle: 0x0022, end grp handle: 0xffff uuid: 00001812-0000-1000-8000-00805f9b34fb
[c8:66:9d:b9:08:ca][LE]> characteristics
handle: 0x0002, char properties: 0x0a, char value handle: 0x0003, uuid: 00002a00-0000-1000-8000-00805f9b34fb
handle: 0x0004, char properties: 0x02, char value handle: 0x0005, uuid: 00002a01-0000-1000-8000-00805f9b34fb
handle: 0x0006, char properties: 0x02, char value handle: 0x0007, uuid: 00002a04-0000-1000-8000-00805f9b34fb
handle: 0x0009, char properties: 0x20, char value handle: 0x000a, uuid: 00002a05-0000-1000-8000-00805f9b34fb
handle: 0x000d, char properties: 0x02, char value handle: 0x000e, uuid: 00002a29-0000-1000-8000-00805f9b34fb
handle: 0x000f, char properties: 0x02, char value handle: 0x0010, uuid: 00002a26-0000-1000-8000-00805f9b34fb
handle: 0x0011, char properties: 0x02, char value handle: 0x0012, uuid: 00002a50-0000-1000-8000-00805f9b34fb
handle: 0x0014, char properties: 0x10, char value handle: 0x0015, uuid: 00001002-e619-419b-bc43-821e71a409b7
handle: 0x0017, char properties: 0x0c, char value handle: 0x0018, uuid: 00001004-e619-419b-bc43-821e71a409b7
handle: 0x0019, char properties: 0x1c, char value handle: 0x001a, uuid: 00001001-e619-419b-bc43-821e71a409b7
handle: 0x001d, char properties: 0x10, char value handle: 0x001e, uuid: 6e400003-b5a3-f393-e0a9-e50e24dcca9e
handle: 0x0020, char properties: 0x0c, char value handle: 0x0021, uuid: 6e400002-b5a3-f393-e0a9-e50e24dcca9e
handle: 0x0023, char properties: 0x1a, char value handle: 0x0024, uuid: 00002a4d-0000-1000-8000-00805f9b34fb  Report
handle: 0x0027, char properties: 0x02, char value handle: 0x0028, uuid: 00002a4b-0000-1000-8000-00805f9b34fb  Report Map
handle: 0x0029, char properties: 0x02, char value handle: 0x002a, uuid: 00002a4a-0000-1000-8000-00805f9b34fb  HID Information
handle: 0x002b, char properties: 0x04, char value handle: 0x002c, uuid: 00002a4c-0000-1000-8000-00805f9b34fb  HID Control Point
[c8:66:9d:b9:08:ca][LE]> char-read-hnd 24
Characteristic value/descriptor:
[c8:66:9d:b9:08:ca][LE]> char-read-hnd 28
Characteristic value/descriptor: 05 01 09 05 a1 01 85 01 a1 02 09 30 09 31 09 32 09 35 15 80 25 7f 35 80 45 7f 75 08 95 04 81 02 09 40 09 41 09 42 09 43 15 00 26 ff 00 75 08 95 04 81 02 75 04 95 01 15 01 25 08 46 3b 01 65 14 09 39 81 42 65 00 05 09 95 0c 75 01 25 01 15 00 09 01 09 02 09 04 09 05 09 07 09 08 09 09 09 0a 09 0b 09 0c 09 0e 09 0f 81 02 05 02 15 00 26 ff 00 09 c5 09 c4 95 02 75 08 81 02 c0 c0
[c8:66:9d:b9:08:ca][LE]> char-read-hnd 2a
Characteristic value/descriptor: 01 01 00 03
[c8:66:9d:b9:08:ca][LE]> char-read-hnd 2c
Error: Characteristic value/descriptor read failed: Attribute can't be read
[c8:66:9d:b9:08:ca][LE]> quit

Nordic Semiconductor's nRF51 DK / nRF51 Dongle

Using nRF Sniffer + Wireshark (PC)

The nRF Sniffer for Bluetooth LE allows near real-time display of
Bluetooth LE packets.

https://www.nordicsemi.com/Software-and-Tools/Development-Tools/nRF-Sniffer-for-Bluetooth-LE

Now that we have dumped the HID Service Characteristics, we can take a look at what it all means. The 'Report Map' is the HID report descriptor. We can run it through the following website to decode it:

http://eleccelerator.com/usbdescreqparser/

0x05, 0x01,        // Usage Page (Generic Desktop Ctrls)
0x09, 0x05,        // Usage (Game Pad)
0xA1, 0x01,        // Collection (Application)
0x85, 0x01,        //   Report ID (1)
0xA1, 0x02,        //   Collection (Logical)
0x09, 0x30,        //     Usage (X)
0x09, 0x31,        //     Usage (Y)
0x09, 0x32,        //     Usage (Z)
0x09, 0x35,        //     Usage (Rz)
0x15, 0x80,        //     Logical Minimum (-128)
0x25, 0x7F,        //     Logical Maximum (127)
0x35, 0x80,        //     Physical Minimum (-128)
0x45, 0x7F,        //     Physical Maximum (127)
0x75, 0x08,        //     Report Size (8)
0x95, 0x04,        //     Report Count (4)
0x81, 0x02,        //     Input (Data,Var,Abs,No Wrap,Linear,Preferred State,No Null Position)
0x09, 0x40,        //     Usage (Vx)
0x09, 0x41,        //     Usage (Vy)
0x09, 0x42,        //     Usage (Vz)
0x09, 0x43,        //     Usage (Vbrx)
0x15, 0x00,        //     Logical Minimum (0)
0x26, 0xFF, 0x00,  //     Logical Maximum (255)
0x75, 0x08,        //     Report Size (8)
0x95, 0x04,        //     Report Count (4)
0x81, 0x02,        //     Input (Data,Var,Abs,No Wrap,Linear,Preferred State,No Null Position)
0x75, 0x04,        //     Report Size (4)
0x95, 0x01,        //     Report Count (1)
0x15, 0x01,        //     Logical Minimum (1)
0x25, 0x08,        //     Logical Maximum (8)
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)
0x05, 0x09,        //     Usage Page (Button)
0x95, 0x0C,        //     Report Count (12)
0x75, 0x01,        //     Report Size (1)
0x25, 0x01,        //     Logical Maximum (1)
0x15, 0x00,        //     Logical Minimum (0)
0x09, 0x01,        //     Usage (0x01)
0x09, 0x02,        //     Usage (0x02)
0x09, 0x04,        //     Usage (0x04)
0x09, 0x05,        //     Usage (0x05)
0x09, 0x07,        //     Usage (0x07)
0x09, 0x08,        //     Usage (0x08)
0x09, 0x09,        //     Usage (0x09)
0x09, 0x0A,        //     Usage (0x0A)
0x09, 0x0B,        //     Usage (0x0B)
0x09, 0x0C,        //     Usage (0x0C)
0x09, 0x0E,        //     Usage (0x0E)
0x09, 0x0F,        //     Usage (0x0F)
0x81, 0x02,        //     Input (Data,Var,Abs,No Wrap,Linear,Preferred State,No Null Position)
0x05, 0x02,        //     Usage Page (Sim Ctrls)
0x15, 0x00,        //     Logical Minimum (0)
0x26, 0xFF, 0x00,  //     Logical Maximum (255)
0x09, 0xC5,        //     Usage (Brake)
0x09, 0xC4,        //     Usage (Accelerator)
0x95, 0x02,        //     Report Count (2)
0x75, 0x08,        //     Report Size (8)
0x81, 0x02,        //     Input (Data,Var,Abs,No Wrap,Linear,Preferred State,No Null Position)
0xC0,              //   End Collection
0xC0,              // End Collection

Reading Raw HID Data

Since a HID over GATT device is treated like a regular HID device once paired and connected to a PC we can use a tool such as SimpleHIDWrite.

You can download the utility from here:

http://janaxelson.com/files/SimpleHIDWrite3.zip

The result of pressing the A button on the controller will generate a button down and up report respectively.

Microsoft Xbox One Bluetooth Controller (BT Classic)

It's worth taking a look at a Bluetooth Classic Device and take a look at how it differs from a Bluetooth LE device. For this we're going to look at the Microsoft One Bluetooth Controller.

Classic Bluetooth Devices use SDP (Service Discovery Protocol) to inform the client what services it provides. The Bluetooth support for this controller is for Windows 10 only.

The wired Xbox One Controller Protocol is documented here:

https://github.com/quantus/xbox-one-controller-protocol

The Microsoft One Bluetooth Controller exposes 3 services:

  1. PnP Information (0x1200)
  • Protocol info (L2CAP / SDP)
  • Language info (0x656e / 0x006a / 0x0100)
  • Vendor ID (0x0201) / Product ID (0x0202) / Serial (0x0203)
  1. Human Interface Device Service (0x1124)
  • Protocol info (L2CAP / HIDP)
  • Language info (0x656e / 0x006a / 0x0100)
  • Name (0x0100) / Provider (0x0102) / Description (0x0101)
  • Report descriptor (0x0206)
  1. Handsfree (0x111e) / Generic Audio (0x1203)
  • Protocol info (L2CAP / RFCOMM)
  • Language info (0x656e / 0x006a / 0x0100)

We can use a Raspberry Pi and BlueZ to examine the SDP communication but firstly we must enable backward compatibility:

sudo nano /lib/systemd/system/bluetooth.service

Add the --compat flag to the following line:

ExecStart=/usr/lib/bluetooth/bluetoothd --compat

Now we can run 'btmon' to view the SDP handshake during the pairing process.

$ sudo btmon

          Attribute list: [len 130] {position 0}
            Attribute: Service Record Handle (0x0000) [len 2]
              0x00010000
            Attribute: Service Class ID List (0x0001) [len 2]
              UUID (3) with 2 bytes [0 extra bits] len 3
                PnP Information (0x1200)
            Attribute: Service Record State (0x0002) [len 2]
              0x00000304
            Attribute: Protocol Descriptor List (0x0004) [len 2]
              Sequence (6) with 6 bytes [8 extra bits] len 8
                UUID (3) with 2 bytes [0 extra bits] len 3
                  L2CAP (0x0100)
                Unsigned Integer (1) with 2 bytes [0 extra bits] len 3
                  0x0001
              Sequence (6) with 3 bytes [8 extra bits] len 5
                UUID (3) with 2 bytes [0 extra bits] len 3
                  SDP (0x0001)
            Attribute: Browse Group List (0x0005) [len 2]
              UUID (3) with 2 bytes [0 extra bits] len 3
                Public Browse Root (0x1002)
            Attribute: Language Base Attribute ID List (0x0006) [len 2]
              Unsigned Integer (1) with 2 bytes [0 extra bits] len 3
                0x656e
              Unsigned Integer (1) with 2 bytes [0 extra bits] len 3
                0x006a
              Unsigned Integer (1) with 2 bytes [0 extra bits] len 3
                0x0100
            Attribute: Bluetooth Profile Descriptor List (0x0009) [len 2]
              Sequence (6) with 6 bytes [8 extra bits] len 8
                UUID (3) with 2 bytes [0 extra bits] len 3
                  PnP Information (0x1200)
                Unsigned Integer (1) with 2 bytes [0 extra bits] len 3
                  0x0100
            Attribute: Unknown (0x0100) [len 2]
               [len 1]
            Attribute: Unknown (0x0101) [len 2]
               [len 1]
            Attribute: Unknown (0x0102) [len 2]
               [len 1]
            Attribute: Unknown (0x0200) [len 2]
              0x0103
            Attribute: Unknown (0x0201) [len 2]
              0x045e
            Attribute: Unknown (0x0202) [len 2]
              0x02e0
            Attribute: Unknown (0x0203) [len 2]
              0x0903
            Attribute: Unknown (0x0204) [len 2]
              true
            Attribute: Unknown (0x0205) [len 2]
              0x0002

          Attribute list: [len 567] {position 1}
            Attribute: Service Record Handle (0x0000) [len 2]
              0x00010001
            Attribute: Service Class ID List (0x0001) [len 2]
              UUID (3) with 2 bytes [0 extra bits] len 3
                Human Interface Device Service (0x1124)
            Attribute: Service Record State (0x0002) [len 2]
              0x00000311
            Attribute: Protocol Descriptor List (0x0004) [len 2]
              Sequence (6) with 6 bytes [8 extra bits] len 8
                UUID (3) with 2 bytes [0 extra bits] len 3
                  L2CAP (0x0100)
                Unsigned Integer (1) with 2 bytes [0 extra bits] len 3
                  0x0011
              Sequence (6) with 3 bytes [8 extra bits] len 5
                UUID (3) with 2 bytes [0 extra bits] len 3
                  HIDP (0x0011)
            Attribute: Browse Group List (0x0005) [len 2]
              UUID (3) with 2 bytes [0 extra bits] len 3
                Public Browse Root (0x1002)
            Attribute: Language Base Attribute ID List (0x0006) [len 2]
              Unsigned Integer (1) with 2 bytes [0 extra bits] len 3
                0x656e
              Unsigned Integer (1) with 2 bytes [0 extra bits] len 3
                0x006a
              Unsigned Integer (1) with 2 bytes [0 extra bits] len 3
                0x0100
            Attribute: Bluetooth Profile Descriptor List (0x0009) [len 2]
              Sequence (6) with 6 bytes [8 extra bits] len 8
                UUID (3) with 2 bytes [0 extra bits] len 3
                  Human Interface Device Service (0x1124)
                Unsigned Integer (1) with 2 bytes [0 extra bits] len 3
                  0x0100
            Attribute: Additional Protocol Descriptor List (0x000d) [len 2]
              Sequence (6) with 13 bytes [8 extra bits] len 15
                Sequence (6) with 6 bytes [8 extra bits] len 8
                  UUID (3) with 2 bytes [0 extra bits] len 3
                    L2CAP (0x0100)
                  Unsigned Integer (1) with 2 bytes [0 extra bits] len 3
                    0x0013
                Sequence (6) with 3 bytes [8 extra bits] len 5
                  UUID (3) with 2 bytes [0 extra bits] len 3
                    HIDP (0x0011)
            Attribute: Unknown (0x0100) [len 2]
              Xbox Bluetooth Gamepad [len 23]
            Attribute: Unknown (0x0101) [len 2]
              Gamepad [len 8]
            Attribute: Unknown (0x0102) [len 2]
              Microsoft Corporation [len 22]
            Attribute: Unknown (0x0200) [len 2]
              0x0100
            Attribute: Unknown (0x0201) [len 2]
              0x0111
            Attribute: Unknown (0x0202) [len 2]
              0x40
            Attribute: Unknown (0x0203) [len 2]
              0x21
            Attribute: Unknown (0x0204) [len 2]
              true
            Attribute: Unknown (0x0205) [len 2]
              true
            Attribute: Unknown (0x0206) [len 2]
              Sequence (6) with 311 bytes [16 extra bits] len 314
                Unsigned Integer (1) with 1 byte [0 extra bits] len 2
                  0x22
                String (4) with 306 bytes [16 extra bits] len 309
                  05 01 09 05 A1 01 85 01 09 01 A1 00 09 30 09 31
                  15 00 27 FF FF 00 00 95 02 75 10 81 02 C0 09 01
                  A1 00 09 33 09 34 15 00 27 FF FF 00 00 95 02 75
                  10 81 02 C0 05 01 09 32 15 00 26 FF 03 95 01 75
                  0A 81 02 15 00 25 00 75 06 95 01 81 03 05 01 09
                  35 15 00 26 FF 03 95 01 75 0A 81 02 15 00 25 00
                  75 06 95 01 81 03 05 01 09 39 15 01 25 08 35 00
                  46 3B 01 66 14 00 75 04 95 01 81 42 75 04 95 01
                  15 00 25 00 35 00 45 00 65 00 81 03 05 09 19 01
                  29 0A 15 00 25 01 75 01 95 0A 81 02 15 00 25 00
                  75 06 95 01 81 03 05 01 09 80 85 02 A1 00 09 85
                  15 00 25 01 95 01 75 01 81 02 15 00 25 00 75 07
                  95 01 81 03 C0 05 0F 09 21 85 03 A1 02 09 97 15
                  00 25 01 75 04 95 01 91 02 15 00 25 00 75 04 95
                  01 91 03 09 70 15 00 25 64 75 08 95 04 91 02 09
                  50 66 01 10 55 0E 15 00 26 FF 00 75 08 95 01 91
                  02 09 A7 15 00 26 FF 00 75 08 95 01 91 02 65 00
                  55 00 09 7C 15 00 26 FF 00 75 08 95 01 91 02 C0
                  85 04 05 06 09 20 15 00 26 FF 00 75 08 95 01 81
                  02 C0 [len 306]
            Attribute: Unknown (0x0207) [len 2]
              Sequence (6) with 6 bytes [8 extra bits] len 8
                Unsigned Integer (1) with 2 bytes [0 extra bits] len 3
                  0x0409
                Unsigned Integer (1) with 2 bytes [0 extra bits] len 3
                  0x0100
            Attribute: Unknown (0x0208) [len 2]
              false
            Attribute: Unknown (0x0209) [len 2]
              true
            Attribute: Unknown (0x020a) [len 2]
              false
            Attribute: Unknown (0x020b) [len 2]
              0x0100
            Attribute: Unknown (0x020c) [len 2]
              0x0c80
            Attribute: Unknown (0x020d) [len 2]
              true
            Attribute: Unknown (0x020e) [len 2]
              false

          Attribute list: [len 106] {position 2}
            Attribute: Service Record Handle (0x0000) [len 2]
              0x00010002
            Attribute: Service Class ID List (0x0001) [len 2]
              UUID (3) with 2 bytes [0 extra bits] len 3
                Handsfree (0x111e)
              UUID (3) with 2 bytes [0 extra bits] len 3
                Generic Audio (0x1203)
            Attribute: Service Record State (0x0002) [len 2]
              0x00000312
            Attribute: Protocol Descriptor List (0x0004) [len 2]
              Sequence (6) with 3 bytes [8 extra bits] len 5
                UUID (3) with 2 bytes [0 extra bits] len 3
                  L2CAP (0x0100)
              Sequence (6) with 5 bytes [8 extra bits] len 7
                UUID (3) with 2 bytes [0 extra bits] len 3
                  RFCOMM (0x0003)
                Unsigned Integer (1) with 1 byte [0 extra bits] len 2
                  0x04
            Attribute: Browse Group List (0x0005) [len 2]
              UUID (3) with 2 bytes [0 extra bits] len 3
                Public Browse Root (0x1002)
            Attribute: Language Base Attribute ID List (0x0006) [len 2]
              Unsigned Integer (1) with 2 bytes [0 extra bits] len 3
                0x656e
              Unsigned Integer (1) with 2 bytes [0 extra bits] len 3
                0x006a
              Unsigned Integer (1) with 2 bytes [0 extra bits] len 3
                0x0100
            Attribute: Bluetooth Profile Descriptor List (0x0009) [len 2]
              Sequence (6) with 6 bytes [8 extra bits] len 8
                UUID (3) with 2 bytes [0 extra bits] len 3
                  Handsfree (0x111e)
                Unsigned Integer (1) with 2 bytes [0 extra bits] len 3
                  0x0106
            Attribute: Unknown (0x0100) [len 2]
              Hands-Free unit [len 16]
            Attribute: Unknown (0x0311) [len 2]
              0x003f

We can use the data dump from attribute 0x0206 (SDP_ATTR_HID_DESCRIPTOR_LIST) to get the HID descriptor using the USB Descriptor and Request Parser from:

http://eleccelerator.com/usbdescreqparser/

Microchip's BM78

https://www.microchip.com/DevelopmentTools/ProductDetails/BM-78-pictail

It might be worth considering the BM78 chip due to its support for Bluetooth Classic.

Nordic Semiconductor's nRF51 DK

The nRF51 DK is a low-cost, versatile single-board development kit for Bluetooth Low Energy, ANT and 2.4 GHz proprietary applications on the nRF51822, nRF51422, nRF51824 and nRF51802 SoCs.

https://www.nordicsemi.com/Software-and-Tools/Development-Kits/nRF51-DK

There are several reasons why the nRF51 series of SoCs make for a compelling choice.

  • Bluetooth 4.0 (LE) and 2.4 GHz support
  • HID over GATT examples in the SDK
  • SEGGER J-Link debugger
  • Well established in the industry with active support forums
  • Easy OTA firmware updating using the nRF Toolbox application

Haptic Feedback

Force Feedback devices exert a normal reaction to the direction of your movements. Rumble on the other hand is just a collection of vibrating motors.

Xinput Force Feedback

If we take a look at Xinput and its XINPUT_VIBRATION structure the only control we have is over the left and right motor speed.

typedef struct _XINPUT_VIBRATION {
  WORD wLeftMotorSpeed;
  WORD wRightMotorSpeed;
} XINPUT_VIBRATION, *PXINPUT_VIBRATION;

Here we can see the HID Report Descriptor for the Xbox One Controller Device Class Definition for Physical Interface Devices (PID).

0x05, 0x0F,        // Usage Page (PID Page)
0x09, 0x21,        // Usage (0x21)
0x85, 0x03,        // Report ID (3)
0xA1, 0x02,        // Collection (Logical)
0x09, 0x97,        //   Usage (0x97)
0x15, 0x00,        //   Logical Minimum (0)
0x25, 0x01,        //   Logical Maximum (1)
0x75, 0x04,        //   Report Size (4)
0x95, 0x01,        //   Report Count (1)
0x91, 0x02,        //   Output (Data,Var,Abs,No Wrap,Linear,Preferred State,No Null Position,Non-volatile)
0x15, 0x00,        //   Logical Minimum (0)
0x25, 0x00,        //   Logical Maximum (0)
0x75, 0x04,        //   Report Size (4)
0x95, 0x01,        //   Report Count (1)
0x91, 0x03,        //   Output (Const,Var,Abs,No Wrap,Linear,Preferred State,No Null Position,Non-volatile)
0x09, 0x70,        //   Usage (0x70)
0x15, 0x00,        //   Logical Minimum (0)
0x25, 0x64,        //   Logical Maximum (100)
0x75, 0x08,        //   Report Size (8)
0x95, 0x04,        //   Report Count (4)
0x91, 0x02,        //   Output (Data,Var,Abs,No Wrap,Linear,Preferred State,No Null Position,Non-volatile)
0x09, 0x50,        //   Usage (0x50)
0x66, 0x01, 0x10,  //   Unit (System: SI Linear, Time: Seconds)
0x55, 0x0E,        //   Unit Exponent (-2)
0x15, 0x00,        //   Logical Minimum (0)
0x26, 0xFF, 0x00,  //   Logical Maximum (255)
0x75, 0x08,        //   Report Size (8)
0x95, 0x01,        //   Report Count (1)
0x91, 0x02,        //   Output (Data,Var,Abs,No Wrap,Linear,Preferred State,No Null Position,Non-volatile)
0x09, 0xA7,        //   Usage (0xA7)
0x15, 0x00,        //   Logical Minimum (0)
0x26, 0xFF, 0x00,  //   Logical Maximum (255)
0x75, 0x08,        //   Report Size (8)
0x95, 0x01,        //   Report Count (1)
0x91, 0x02,        //   Output (Data,Var,Abs,No Wrap,Linear,Preferred State,No Null Position,Non-volatile)
0x65, 0x00,        //   Unit (None)
0x55, 0x00,        //   Unit Exponent (0)
0x09, 0x7C,        //   Usage (0x7C)
0x15, 0x00,        //   Logical Minimum (0)
0x26, 0xFF, 0x00,  //   Logical Maximum (255)
0x75, 0x08,        //   Report Size (8)
0x95, 0x01,        //   Report Count (1)
0x91, 0x02,        //   Output (Data,Var,Abs,No Wrap,Linear,Preferred State,No Null Position,Non-volatile)
0xC0,              // End Collection

// 91 bytes

DirectInput Force Feedback

To be DirectInput compliant the joystick report requires a rather large hid report descriptor for haptic feedback.

You can view an example Force Feedback PID Descriptor:

https://forums.vigem.org/topic/294/pid-1-0-example-descriptor

It has a number of effect types:

  • Constant Force
  • Ramp
  • Square
  • Sine
  • Triangle
  • Sawtooth Up
  • Sawtooth Down
  • Spring
  • Damper
  • Inertia
  • Friction
  • Custom Force Data

You can also apply a directional force. Here is some example code from the DirectInput SDK (ffconst.cpp)

//-----------------------------------------------------------------------------
// Name: SetDeviceForcesXY()
// Desc: Apply the X and Y forces to the effect we prepared.
//-----------------------------------------------------------------------------
HRESULT SetDeviceForcesXY()
{
    // Modifying an effect is basically the same as creating a new one, except
    // you need only specify the parameters you are modifying
    LONG rglDirection[2] = { 0, 0 };

    DICONSTANTFORCE cf;

    if( g_dwNumForceFeedbackAxis == 1 )
    {
        // If only one force feedback axis, then apply only one direction and
        // keep the direction at zero
        cf.lMagnitude = g_nXForce;
        rglDirection[0] = 0;
    }
    else
    {
        // If two force feedback axis, then apply magnitude from both directions
        rglDirection[0] = g_nXForce;
        rglDirection[1] = g_nYForce;
        cf.lMagnitude = (DWORD)sqrt( (double)g_nXForce * (double)g_nXForce +
                                     (double)g_nYForce * (double)g_nYForce );
    }

    DIEFFECT eff;
    ZeroMemory( &eff, sizeof(eff) );
    eff.dwSize                = sizeof(DIEFFECT);
    eff.dwFlags               = DIEFF_CARTESIAN | DIEFF_OBJECTOFFSETS;
    eff.cAxes                 = g_dwNumForceFeedbackAxis;
    eff.rglDirection          = rglDirection;
    eff.lpEnvelope            = 0;
    eff.cbTypeSpecificParams  = sizeof(DICONSTANTFORCE);
    eff.lpvTypeSpecificParams = &cf;
    eff.dwStartDelay            = 0;

    // Now set the new parameters and start the effect immediately.
    return g_pEffect->SetParameters( &eff, DIEP_DIRECTION |
                                           DIEP_TYPESPECIFICPARAMS |
                                           DIEP_START );
}

The DirectX 9 SDK includes an application called Force Editor (fedit.exe) that allows you to create custom force feedback effects.

OpenVR Haptic Vibration

The problem with implementing Force Feedback on the AgileVR is that very few games will support it out-of-the-box. VR controllers in general only support rumble.

Let's take a look at OpenVR's function for trigging haptic vibration:

EVRInputError IVRInput::TriggerHapticVibrationAction( VRActionHandle_t action, float fStartSecondsFromNow, float fDurationSeconds, float fFrequency, float fAmplitude )

Triggers a haptic vibration action.

  • action - The action to trigger
  • fStartSecondsFromNow - When to start the haptic event
  • fDurationSeconds - How long to trigger the haptic event for
  • fFrequency - The frequency in cycles per second of the haptic event
  • fAmplitude - The magnitude of the haptic event. This value must be between 0.0 and 1.0.

The best solution for implementing haptic support for the AgileVR is to pass the left / right haptic data from the controllers to the AgileVR hardware.

Until developers directly add support for the AgileVR haptics into their games this is the only viable solution for now.

To do this we add a check for the VREvent_Input_HapticVibration event in PollNextEvent and get access to the following data struct:

struct VREvent_HapticVibration_t
{
    uint64_t containerHandle; // property container handle of the device with the haptic component
    uint64_t componentHandle; // Which haptic component needs to vibrate
    float fDurationSeconds;
    float fFrequency;
    float fAmplitude;
};

From here we can convert and pass this data onto the AgileVR to trigger haptics.

Conclusions

  • Xinput support does not appear possible with Bluetooth LE
  • If we add PSVR support we will need to implement a gamepad report descriptor
  • Force feedback is most likely overkill for this device due to lack of game support
  • Wireless technologies:
    • Bluetooth LE
    • Requires a Bluetooth 4.2 USB dongle
    • Bluetooth Classic
    • Requires a Bluetooth 4.2 USB dongle
    • WiFi 2.4GHz
    • Requires a 2.4Ghz USB dongle
  • SoC's:
    • Microchip BM71 (Bluetooth LE)
    • Microchip BM78 (Bluetooth Classic / LE)
    • Nordic Semiconductor nRF51 (Bluetooth Classic? / LE / WiFi 2.4GHz)
  • How to send Report Map's larger than ATT_MTU of 23 bytes?
    • This appears to be automated by the Nordic's nRF5 series SoC as demonstrated by the 'HID Keyboard Application' example.
    • We don't know how using Microchip's BM71 as the 'UI Configuration Tool' does not allow entry of more than 23 bytes. Most likely we include the first 23 bytes of the report using the tool and send the rest of the data as 'blobs' in the GATT Read Characteristic Value sub-procedure.

Recommendations

Even though the Nordic's nRF5 series seems like a great choice for the AgileVR version 2 firmware, we're already using Microchip's MCU, so it does make sense for us to give the BM71 a try. Although if we have too many issues with the BM71, as it does lack documentation and examples, or we decide that the nRF51 chipset has features desirable for the product (such as 2.4Ghz support), then it's most definitely a worthwhile SoC to consider. I also think it would interface quite easily with the PIC32.

For the haptic feedback I think we should not bother with directional force feedback but simply go with motors such as those found in the Xbox and VR controllers. Unless developers add support for the AgileVR hardware I think it will be overkill to implement anything more than simple rumble effects. For now, I think it's best we just forward haptic feedback data being sent to the VR controllers to the AgileVR.