Complex Bluetooth HID Input with iWRAP and the Bluegiga WT12

Thanks to Bluegiga’s workhorse of a class 2 Bluetooth module and the latest iWRAP5 firmware with custom HID descriptor support, I have now been able to achieve the wireless capabilities I always hoped the Keyglove would have. Keyboard, mouse with scrolling support, consumer page reports, and raw HID packets for arbitrary data transmission. It isn’t fully integrated into the Keyglove code yet, and I’ve only tested it with manual control so far, but the firmware setup is solid. It’s now just a matter of translating the manual control I’ve already done into my codebase.

The enabling factor here compared to the last time I tried this (quite some time ago) is the addition of custom descriptor support in iWRAP5. I don’t know of any other module which allows the degree of simple customizability in this area that the WT12 does. It was the best module I saw two years ago, and the new features are icing on the cake. (Yes, I have been working at Bluegiga since last October, but bear in mind I was singing this module’s praises long before that started.)

So how does this configuration actually work? There are three important pieces involved:

  1. Building the HID descriptor
  2. Configuring iWRAP
  3. Sending HID report packets

Naturally, I will explain each in order.

Building the HID descriptor

To be honest, this is the part I am least confident about. I have learned a lot about HID descriptors, but only enough to be dangerous instead of simply clueless. In short, they are sent from a device to a host during the connection establishment phase so that the host will know what kind of data to expect from the device, how it will be formatted, and what it should do with it. A HID descriptor is built using a predefined binary “language” of sorts; data bytes either denote a field or setting, or else are the byte(s) contained within the previously denoted field or setting. There are many aspects to these definitions, and I don’t fully understand them all yet. It will be much better if I point you to the resources that were most helpful for me:

Actually, the first and last links ended up being the most practically useful to me. The others are good for cross-referencing things or getting into a detailed understanding. It’s a great list to get started with if you’re looking for HID development resources. Also, USBlyzer can be amazing if you are trying to mimic the behavior of an existing wired USB device, since it can show you both the descriptor structure as well as the actual HID report traffic.

But what’s all this about USB? Wasn’t the whole point of this to be all wireless and Bluetoothy and stuff? Well, it turns out that the important bits of device-specific USB HID descriptors pretty much apply directly to Bluetooth HID, because as best as I can tell, Bluetooth HID is basically USB HID wrapped in a wireless transport layer. It isn’t exactly the same, of course, but the most important pieces—namely, the HID descriptor and corresponding HID reports—match.

Now, what about Bluegiga’s HID documentation? There is a HID application note for iWRAP which is quite good if you are interested in single-function devices, or if you know what you are doing with descriptors. But if you’re only good at following directions and you want a combo device, it’s less obvious. (I will be fixing this shortly, because now I can!) There are example descriptors supplied for a standard keyboard, standard mouse (X/Y + 3 buttons), consumer page control (e.g. calculator and music controls), and a gamepad. But what if you want a keyboard/mouse/gamepad device all in one?

It turns out that this is really not difficult at all. There are two basic rules you have to follow:

  1. Take each individual descriptor and mash them all together into one big long one.
  2. Make sure each report structure definition includes a “Report ID” byte (0x85) and unique value (0x01 or higher).

The first one is easy. The second one is not so easy if you don’t know where the Report ID byte should go. Based on my tests, it needs to go after the innermost “Collection” start field (0xA1 0xnn) somewhere. I don’t know what the rule is exactly, but I put them (when they weren’t already there) immediately after this “Collection” start field. Each report needs its own unique ID so that the host device knows which kind of report you’re sending, whenever you send one. Pretty straightforward.

Here is the actual descriptor I put together for my successful test last night, in C struct form:

static const uint8_t hid_descriptor_combined[] = {

    // KEYBOARD

    /****/ 0x05, 0x01, /* USAGE_PAGE (Generic Desktop) */
    /****/ 0x09, 0x06, /* USAGE (Keyboard) */
    /****/ 0xa1, 0x01, /* COLLECTION (Application) */
    /******/ 0x05, 0x07, /* USAGE_PAGE (Keyboard) */
    /******/ 0x85, 0x01,   /* REPORT_ID (1) */
    /* 1 byte Modifier: Ctrl, Shift and other modifier keys, 8 in total */
    /******/ 0x19, 0xe0, /* USAGE_MINIMUM (kbd LeftControl) */
    /******/ 0x29, 0xe7, /* USAGE_MAXIMUM (kbd Right GUI) */
    /******/ 0x15, 0x00, /* LOGICAL_MINIMUM (0) */
    /******/ 0x25, 0x01, /* LOGICAL_MAXIMUM (1) */
    /******/ 0x75, 0x01, /* REPORT_SIZE (1) */
    /******/ 0x95, 0x08, /* REPORT_COUNT (8) */
    /******/ 0x81, 0x02, /* INPUT (Data,Var,Abs) */
    /* 1 Reserved byte */
    /******/ 0x95, 0x01,  /* REPORT_COUNT (1) */
    /******/ 0x75, 0x08, /* REPORT_SIZE (8) */
    /******/ 0x81, 0x01, /* INPUT (Cnst,Ary,Abs) */
    /* LEDs for num lock etc */
    /******/ 0x95, 0x05, /* REPORT_COUNT (5) */
    /******/ 0x75, 0x01, /* REPORT_SIZE (1) */
    /******/ 0x05, 0x08, /* USAGE_PAGE (LEDs) */
    /******/ 0x85, 0x01, /* REPORT_ID (1) */
    /******/ 0x19, 0x01, /* USAGE_MINIMUM (Num Lock) */
    /******/ 0x29, 0x05, /* USAGE_MAXIMUM (Kana) */
    /******/ 0x91, 0x02, /* OUTPUT (Data,Var,Abs) */
    /* Reserved 3 bits */
    /******/ 0x95, 0x01, /* REPORT_COUNT (1) */
    /******/ 0x75, 0x03, /* REPORT_SIZE (3) */
    /******/ 0x91, 0x03, /* OUTPUT (Cnst,Var,Abs) */
    /* Slots for 6 keys that can be pressed down at the same time */
    /******/ 0x95, 0x06, /* REPORT_COUNT (6) */
    /******/ 0x75, 0x08, /* REPORT_SIZE (8) */
    /******/ 0x15, 0x00, /* LOGICAL_MINIMUM (0) */
    /******/ 0x25, 0x65, /* LOGICAL_MAXIMUM (101) */
    /******/ 0x05, 0x07, /* USAGE_PAGE (Keyboard) */
    /******/ 0x19, 0x00, /* USAGE_MINIMUM (Reserved (no event indicated)) */
    /******/ 0x29, 0x65, /* USAGE_MAXIMUM (Keyboard Application) */
    /******/ 0x81, 0x00, /* INPUT (Data,Ary,Abs) */
    /****/ 0xc0, /* END_COLLECTION */

    // CONSUMER PAGE

    /****/ 0x05, 0x0c, /* USAGE_PAGE (Consumer) */
    /****/ 0x09, 0x01, /* USAGE (Consumer Control) */
    /****/ 0xa1, 0x01, /* COLLECTION (Application) */
    /******/ 0x85, 0x02, /* Report ID 2 */
    /******/ 0x05, 0x0c, /* USAGE_PAGE (Consumer) */
    /* 8 media player related keys */
    /******/ 0x15, 0x00, /* Logical Min 0 */
    /******/ 0x25, 0x01, /* Logical Max 1 */
    /******/ 0x09, 0xe9, /* Usage (8-bit), Volume Increment */
    /******/ 0x09, 0xea, /* Usage (8-bit), Volume Decrement */
    /******/ 0x09, 0xe2, /* Usage (8-bit), Mute */
    /******/ 0x09, 0xcd, /* Usage (8-bit), Play/Pause */
    /******/ 0x19, 0xb5, /* Usage Min (Scan Next Track, Scan Previous Track, Stop, Eject) */
    /******/ 0x29, 0xb8, /* Usage Max */
    /******/ 0x75, 0x01, /* Report Size */
    /******/ 0x95, 0x08, /* Report Count */
    /******/ 0x81, 0x02, /* Input type 2 */
    /* 8 application control keys */
    /******/ 0x0a, 0x8a, 0x01,/* Usage (16-bit), LSB first, e.g. this is 0x018a Email Reader */
    /******/ 0x0a, 0x21, 0x02,/* Usage (16-bit), Generic GUI Application Control Search */
    /******/ 0x0a, 0x2a, 0x02,/* Usage (16-bit), Application Control Bookmarks */
    /******/ 0x1a, 0x23, 0x02,/* Usage Min (16-bit), AC Home, Back, Forward, Stop, Refresh */
    /******/ 0x2a, 0x27, 0x02,/* Usage Max (16-bit) */
    /******/ 0x75, 0x01, /* Report Size */
    /******/ 0x95, 0x08, /* Report Count */
    /******/ 0x81, 0x02, /* Input type 2 */
    /* Application launch keys + record & rewind */
    /******/ 0x0a, 0x83, 0x01,/* Usage (16-bit), Application Launch Generic Consumer Control */
    /******/ 0x0a, 0x96, 0x01,/* Usage (16-bit), AL Internet Browser */
    /******/ 0x0a, 0x92, 0x01,/* Usage (16-bit), AL Calculator */
    /******/ 0x0a, 0x9e, 0x01,/* Usage (16-bit), AL Terminal Lock / Screensaver */
    /******/ 0x0a, 0x94, 0x01,/* Usage (16-bit), AL Local Machine Browser */
    /******/ 0x0a, 0x06, 0x02,/* Usage (16-bit), AC Minimize */
    /******/ 0x09, 0xb2, /* Usage (8-bit), Record */
    /******/ 0x09, 0xb4, /* Usage (8-bit), Rewind */
    /******/ 0x75, 0x01, /* Report Size */
    /******/ 0x95, 0x08, /* Report Count */
    /******/ 0x81, 0x02, /* Input type 2 */
    /******/ 0xc0,      /* End Collection */

    // MOUSE

    0x05, 0x01,       // USAGE_PAGE (Generic Desktop)
    0x09, 0x02,       // USAGE (Mouse)
    0xa1, 0x01,       // COLLECTION (Application)
    0x09, 0x01,       //   USAGE (Pointer)
    0xa1, 0x00,       //   COLLECTION (Physical)
    0x85, 0x03,       //     REPORT_ID (3)
    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,Var,Abs)
    0x95, 0x01,       //     REPORT_COUNT (1)
    0x75, 0x05,       //     REPORT_SIZE (5)
    0x81, 0x03,       //     INPUT (Cnst,Var,Abs)
    0x05, 0x01,       //     USAGE_PAGE (Generic Desktop)
    0x09, 0x30,       //     USAGE (X)
    0x09, 0x31,       //     USAGE (Y)
    0x09, 0x38,       //     USAGE (Wheel)
    0x15, 0x81,       //     LOGICAL_MINIMUM (-127)
    0x25, 0x7f,       //     LOGICAL_MAXIMUM (127)
    0x75, 0x08,       //     REPORT_SIZE (8)
    0x95, 0x03,       //     REPORT_COUNT (3)
    0x81, 0x06,       //     INPUT (Data,Var,Rel)
    0x05, 0x0c,       //     USAGE_PAGE (Consumer Devices)
    0x0a, 0x38, 0x02, //     USAGE (Undefined)
    0x95, 0x01,       //     REPORT_COUNT (1)
    0x81, 0x06,       //     INPUT (Data,Var,Rel)
    0xc0,             //   END_COLLECTION
    0xc0,              // END_COLLECTION

    // RAW 16-BYTE I/O

    0x06, 0xAB, 0xFF,   // Usage Page (Vendor-Defined 172)
    0x0A, 0x00, 0x02,   // Usage (Vendor-Defined 512)
    0xA1, 0x01,         // Collection (Application)
    0x85, 0x04,         /* REPORT_ID (4) */
    0x75, 0x08,         // Report Size (8)
    0x15, 0x00,         // Logical Minimum (0)
    0x26, 0xFF, 0x00,   // Logical Maximum (255)
    0x95, 0x10,         // Report Count (16)
    0x09, 0x01,         // Usage (Vendor-Defined 1)
    0x81, 0x02,         // Input (Data,Var,Abs,NWrp,Lin,Pref,NNul,Bit)
    0x95, 0x10,         // Report Count (16)
    0x09, 0x02,         // Usage (Vendor-Defined 2)
    0x91, 0x02,         // Output (Data,Var,Abs,NWrp,Lin,Pref,NNul,NVol,Bit)
    0xc0,               // End Collection
};

Pardon the mix of comment formatting. I pulled these together from multiple sources and built one of them by hand. The structure of this descriptor is that the standard keyboard report has ID 1, consumer page report has ID 2, mouse report has ID 3, and raw generic 16-byte data packet has ID 4.

However, the above isn’t directly applicable to iWRAP configuration on the WT12 module, since iWRAP doesn’t use C structs as settings. This leads us to…

Configuring iWRAP

Luckily for all of us, the factory default iWRAP5 firmware—currently v5.0.1 build 620—supports all the HID customization we need. (It is also freely user-upgradable if you have or want a different build for whatever reason.) The default settings enable only the serial port profile (SPP), so we have to turn on the HID profile specifically, and write our descriptor. The maximum descriptor length is 255 bytes at the moment. For simplicity, here’s the full set of relevant initialization commands that I’m sending for the Keyglove’s Bluetooth module:

SET BT NAME Keyglove
SET BT CLASS 00540
SET BT IDENT USB:1d50 6025 1.0.0 Keyglove Input Device
SET BT SSP 3 0
SET CONTROL CD 80 2 20
SET CONTROL ESCAPE - 40 1
SET PROFILE HID d 40 100 0 en 0409 Keyglove Input Device
HID SET F2 05010906A1010507850119E029E715002501750195088102950175088101950575010508850119012905910295017503910395067508150025650507190029658100C0050C0901A1018502050C1500250109E909EA09E209CD19B529B87501950881020A8A010A21020A2A021A23022A27027501950881020A83010A96010A92010A9E010A94010A060209B209B4750195088102C005010902A1010901A10085030509190129031500250195037501810295017505810305010930093109381581257F750895038106050C0A380295018106C0C006ABFF0A0002A10185047508150026FF00951009018102951009029102C0

That ridiculously long “HID SET” line at the end is the descriptor itself. The lone preceding “F2” value is the length (in hex) of the whole thing, which is 242 bytes. I’m right at the edge of the size limit with this one.

Here’s a quick breakdown of these settings (see the iWRAP5 User Guide from Bluegiga for more detail):

  • SET BT NAME Keyglove

    – Sets the Bluetooth device friendly name

  • SET BT CLASS 00540

    – Sets the device class to “keyboard” (important for iOS compatibility)

  • SET BT IDENT USB:1d50 6025 1.0.0 Keyglove Input Device

    – Sets the VID/PID, version, and self-identifying description

  • SET BT SSP 3 0

    – Enables PIN-less “just works” secure simple pairing

  • SET CONTROL CD 80 2 20

    – Enables GPIO output signals for active link and DATA mode status

  • SET CONTROL ESCAPE - 40 1

    – Enable GPIO input control for exiting DATA mode

  • SET PROFILE HID d 40 100 0 en 0409 Keyglove Input Device

    – Sets the HID options to be a keyboard class device, English layout, no localization, version 1.0.0

  • HID SET F2 ...

    – Explained above. Sets the actual HID descriptor.

Not all of these are strictly necessary for the HID functionality I’m going for (e.g. the “SET CONTROL …” commands and BT NAME), but the rest are. Note also that all of these persist through power cycles, since all of them are stored in non-volatile memory. Once these settings are applied the first time, you must power-cycle the module or issue a “RESET” command to make them take effect—particularly the newly enabled HID profile and HID descriptor.

Now we’re ready to actually send some data.

Sending HID report packets

iWRAP uses a specific encapsulation format for sending raw HID packets. It will actually let you send basic ASCII keyboard characters as plain bytes over UART—so the byte ‘a’ (0x61) sends the keyboard HID reports for pressing and releasing the ‘a’ key—but only if you have the standard keyboard descriptor included at the beginning of your full HID descriptor. As it happens, we do. But full control only comes through the use of raw HID packets, so that’s what we’ll be using here. The raw HID format, as described in the iWRAP HID app note, is always of this form:

0x9F [length] [data ... ... ...]

Where 0x9F is the start-of-frame byte, [length] is how many data bytes follow, and [data …] is the actual data itself. So, for a 6-byte packet example, it might be formatted like this:

0x9F 0x06 0xAA 0xBB 0xCC 0xDD 0xEE 0xFF

The above packet is invalid given our descriptors, but it illustrates the point. Remember that we have four unique reports in our combo descriptor above, so there are four possible kinds of raw reports we might send:

  1. Keyboard
    0x9F 0x0A 0xA1 0x01 [modifier] 0x00 [key1] [key2] [key3] [key4] [key5] [key6]
  2. Consumer page
    0x9F 0x05 0xA1 0x02 [field1] [field2] [field3]
  3. Mouse
    0x9F 0x07 0xA1 0x03 [buttons] [x-move] [y-move] [v-scroll] [h-scroll]
  4. Raw
    0x9F 0x12 0xA1 0x04 [data1 ... data16]

Note the Report ID byte in red. For testing purposes, I like using Realterm because (1) it’s free and (2) it makes sending or displaying binary data in hex format easier than anything else I’ve seen. The UI is definitely engineer-ish, but it doesn’t take too much getting used to, and it has some features that are very hard to do without in cases like these. Realterm has a “Send” tab where you can paste bytes in hex notation (either 0xAA or $AA) and then send them as binary data, which is excellent here.

One important thing to point out here is that for many operations that we tend to think of as a single action, like a keypress or mouse click, there are actually two things going on. One is the press and the other is the release. It is critical that you keep this in mind when sending raw reports. Pressing the ‘a’ key is actually sending one report with the key down and another with no keys down. Here are some examples of each kind of descriptor:

Press and release the ‘a’ key

0x9F 0x0A 0xA1 0x01 0x00 0x00 0x04 0x00 0x00 0x00 0x00 0x00
0x9F 0x0A 0xA1 0x01 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00

Press and release the calculator button

0x9F 0x05 0xA1 0x02 0x00 0x00 0x04
0x9F 0x05 0xA1 0x02 0x00 0x00 0x00

Press and release the left mouse button

0x9F 0x07 0xA1 0x03 0x01 0x00 0x00 0x00 0x00
0x9F 0x07 0xA1 0x03 0x00 0x00 0x00 0x00 0x00

Send 16 bytes of raw data

0x9F 0x12 0xA1 0x04 0x00 0x11 0x22 0x33 0x44 0x55 0x66 0x77 0x88 0x99 0xAA 0xBB 0xCC 0xDD 0xEE 0xFF

(This is a single action and doesn’t require a “release” report)

This successful implementation of complex Bluetooth HID is an excellent next step in the Keyglove development. While the raw HID (as far as I know) is not accessible on iOS devices, this same combo descriptor and iWRAP configuration allows you to connect to a PC, Mac, Linux machine, iPhone, and Android phone—and, I assume and very much hope, Google Glass. We shall see in approximately 10 days.

Until then, have fun with your own Bluetooth HID projects, if you are so inclined!

13 Comments
  1. Great job! Nice write-up on HID for WT-12 module. This is great information for anyone starting a HID project. Thank you for taking the time to write this up. The detail in this has helped me out tremendously. 🙂

    • Thanks, Andrew! I’m glad it was helpful. There is a lot to grasp when working with BT HID stuff, and it took me a while before I understood it well enough to write up even this much of a tutorial. I wish it were more straightforward…but I guess “straightforward” is a relative term, and many people who have worked with it for years would say that it is. 🙂

  2. Hi Jeff. Like Andrew H said, awesome job! I’ve been reading your write ups trying to learn as much as I can about the WT12 and HID. I’ve been working on a project for some time and I just recently had to switch to the WT12, so your work is insanely helpful.

    One question I have though, is how to update the firmware to iWrap 5. The iWrap 4 chip is cheaper (on Mouser, the new chip is out of stock), but all I have at my disposal are a couple of USB ports. I was considering adding programming pins onto the finished PCB for this purpose but I don’t know what’s really required to upgrade the firmware.

  3. Jeff,
    Thanks for publishing this, very helpful information about WT-12 and HID reports.
    Could you please post the result of your “SET” command, once the WT12 is configured as you have described here?
    I am having trouble pairing to the WT-12 to OSX Host with this configuration and I’m i’d like to make sure i’m not missing something simple.
    Thanks!

  4. Hi Jeff!

    Hope you are well.

    I am trying to use BLE113 and connect an Arduino to BLE113 through UART and send HID commands.

    I found your Arduino code for BLE here– thanks!
    http://forum.arduino.cc/index.php?topic=132487.0

    I was wondering if you have tried to use Arduino host to send HID keyboard commands through BLE113?

    Thanks!
    P.K.

  5. Hi Jeff!
    I’m quite interested in Raw IO in HID. Did you find any way to send and receive data with it on computers?

  6. Hello
    Dear Jeff, i have Some expairence with wt11 as HID in windows as Gamepad , but it did not work , the pairing is ok
    The connecting not, Couleur You please Tell me way!!!???

  7. Hi jeff

    I miss configure pskeys of the firmware of my wt41 module kit. and now i am unable to connect to my kit through terminal software nor by pstool software to reconfigure it. what i have to do for reconfiguration ?

    waiting for your informative replay.

  8. in raw reports, 0xA1 meaning is a some kind of important tag?

    following was wrong because 0xA1 was missing:
    0x9F [length] [data … … …]
    0x9F 0x06 0xAA 0xBB 0xCC 0xDD 0xEE 0xFF

    after inserting 0xA1:
    0x9F [length+1] >>A1<>A1<< 0xAA 0xBB 0xCC 0xDD 0xEE 0xFF

    • after inserting 0xA1:
      0x9F [length+1] A1 [data … … …]
      0x9F 0x06+1 A1 0xAA 0xBB 0xCC 0xDD 0xEE 0xFF

      • 0x9F [length+1] 0xA1 [data … … …]
        0x9F 0x06+1 0xA1 0xAA 0xBB 0xCC 0xDD 0xEE 0xFF

  9. Greetings Jeff,. Your work is really amazing. Inspired by your quest i’m pursuing dream of making glove mouse. Sir I’ve done half part by making the mouse work by means of wired connection with hepl of accelerometer and teensy++ 2.0. Now the biggest challenge for me is to establish wireless connection between host pc and my glove mouse. Sir can you guide me in using “RAW HID” concept.Because no one has posted any help on that.I’m using HC-05 bluetooth module.But I have downloaded BTHID library from github…but don’t know how to use it sir…A small loop hole from you will be so encouragive sir..kindly Looking forward for your answer sir.

Leave a Reply