Human-Interfacing Devices: The Descriptor Heist

Today, we’ll build our own input devices. And they will be easy to create and write firmware for, they will work perfectly, and they will be cross-platform. We can do that with help of the Human Interface Device (HID) standard, and by way of introduction, so that you never get confused by what a “descriptor” means, and we’ll build our own HID device — a Human Interface Device device. The way we build them won’t require reading specifications – instead, I’ll teach your how to steal HID descriptors from existing devices, tweak them for our purposes, and use them in our devices to harness the power of HID.

For decades now, it’s been possible to build a HID mouse or keyboard by using a library or two, and it’s been a godsend for hackers all around the world. However, these libraries are typically confined to a certain template and inflexible, and we hackers often go outside of what’s expected. HID allows for much more than a simple keyboard or a mouse. That’s why today we’re building a touchscreen – something not yet covered online or by libraries.

HID lets you build devices that are friendly. They don’t need drivers, they are plug and play, and they do what you expect them to do. At its core, the HID standard is as simple as is ubiquitous. You can tunnel HID over USB, Bluetooth, I2C, and modern-day operating systems support all three of these. Today, let’s go through the basics of HID, and then build a USB touchscreen out of a SPI-connected resistive touchscreen, with help of the usual RP2040+MicroPython combo. I will also give you a toolkit for how to debug a Human Interface Device device as thoroughly as possible – specifically on Linux, showing all the HID debug and introspection capabilities that Linux gives you. But it’ll work on Windows too through the beauty of standardization.

Checking The Map

There’s quite a few guides on this topic by now, but of course, I’ll go in more detail, be more succinct, and give you tools that other guides don’t have. That said, you can always benefit from multiple different guides to cross-reference on your journey, so here’s an Adafruit guide, and here’s a few other ones. In particular, they go further into HID descriptor structure and explain it in all the detail you could use when figuring out descriptors. Instead, I will give you a high-level overview of HID, and demonstrate you that you don’t need to fully understand HID descriptors to create HID devices – you can hack on them all the same.

A report descriptor is a machine-readable guide on how to parse a HID packet (“report”) from your device. The descriptor tells your OS which bits and bytes inside a HID packet correspond to what kinds of data. When a HID device is connected to your OS, the OS gets the descriptor, reads it, learns what it can expect from your device, and whenever an incoming packet of data arrives, the OS parses your device’s packet given the info in the descriptor.

As you can see, descriptors are quite verbose, and that lets devices send packets that are short and simple.  This descriptor portion was taken from here, you can see the complete descriptor there if you’re interested.

Of course, your device could be sending different kinds of packets, for different sorts of data. For instance, a mouse-plus-keyboard wireless receiver would send at least two different kinds of HID packets, and a touchscreen would send different packets based on whether you’re using a finger, or the special stylus. That’s why a report descriptor can have different sections for different types of packets, and the packets are then distinguished by the report ID. This ID has to be the first byte of the report – if you don’t get the ID first and foremost, you don’t know which section of the descriptor to use to parse the entire packet that just arrived.

It’s easy to build a HID mouse. It’s easy to build a HID keyboard. There are libraries for both of these, and if you want to build a keyboard, the Adafruit tutorial will show you how. Today, I need to build a different device for my arsenal – a small portable touchscreen display, something that’s not exactly available off the shelf. Naturally, I also want it to be plug and play, and in the future, I want to have it be accessible over I2C for all my embedded needs, too.

As a test polygon, here’s a touch-enabled HDMI screen with an SPI-connected XPT2046 resistive touchscreen controller IC, a clone of the ADS7846, with SPI pins exposed onto a Raspberry Pi-compatible header. The HDMI part of it is alright, save for it pretending to be a 1080p screen while its actual resolution is 480×320. The touchscreen part, however, is a pretty obtuse piece of tech – it only works if you have an SPI interface, you have to load the driver before anything works, and if you unplug it, you might have to reboot your Pi for the touchscreen to work again. Plus, it’s a bother to calibrate. HID support would make it universal, plug&play, and having our own firmware would let us to even put calibration settings into nonvolatile storage, something that all self-respecting touchscreen displays do.

All that our firmware has to do is to store a descriptor, and then send descriptor-matching HID packets to the OS whenever we want to send touch coordinates. As long as the OS is okay with our descriptor and the values in our packets make sense, the OS is going to create touchscreen events from our packets. It’s really this simple.

A Vault Full Of Descriptors

One way to create a touchscreen descriptor is to read the HID specification, figure out all the things you need to say in your descriptor to be a valid touchscreen, carefully put those things together, and then debug it until it works. If this doesn’t really sound fun for you, don’t worry, it doesn’t sound fun for me either.

Here’s a hacker-friendly plan instead. We steal a HID descriptor from a touchscreen, see how the touchscreen events are parsed by an OS, then make our own touchscreen descriptor out of a mouse one, send packets that correspond to this descriptor, and see how our OS reacts. I have a capacitive touchscreen panel that I’ve salvaged from a laptop – it connects over USB, it works on Linux without trouble, and that’s very much enough to extract a descriptor from.

In the meantime, I’ve modified this screen to power from HDMI instead of using a separate microUSB power input

We won’t be using super abstract libraries today, either – let’s build raw packets and see how they work. MicroPython still doesn’t have HID support, somehow, so here’s a build of MicroPython, where [elpekenin] has added HID device support with only a few extra commits (plus, undoubtedly, hours of blood, sweat and tears). There’s no “send ‘A’ keypress” API to speak of, you must make your packets even for keyboard use – but it’s very simple, and, that’s perfect for our goal! This build was made by [Pablo Martínez (elpekenin)], and I’m eternally grateful to them for sharing it – go follow them on GitHub, they make cool stuff! For the XPT2046 driver, I’ve used a XPT2046 MicroPython library by the ever-prolific [robert-hh], who is also the hacker to turn to if you want your fix of high-quality MicroPython libraries and tools!

I suggest cloning this repository like git clone --branch peke-devel --recursive URL, since the main branch of the repository was rebased onto CircuitPython and the recursive clone required for the main branch, will also pull in an absolute ton of Adafruit and other code that was added into CircuitPython. Cloning only the specific branch we need, that still relies only on MicroPython libraries, will save you a ton of bandwidth, time, and disk space. Once you do that, however, compiling this firmware and loading this onto a friendly RP2040 will give you a good few new HID devices, and a library to import:

import time
import usb_hid

report = bytearray(8)

report[2] = 0x04 # register 'a' keycode
usb_hid.report(usb_hid.KEYBOARD, report) # send event

time.sleep(2)

report[2] = 0x00 # unregister 'a' keycode
usb_hid.report(usb_hid.KEYBOARD, report) # send event

As you can see, a report is 8 bytes (64 bits) long, and different bits in the report respond to different characters you can send. If you’re wondering how is it that 0x04 corresponds to a, that’s a default bit-to-character mapping that can be found here and here and more info here, here’s more tips on how you can mod that mapping. Most of this firmware’s HID descriptors come from TUSB library, tried and true, and, there’s also the MOUSE_ABS descriptor added in the code itself, that you can change if you simply recompile the firmware!

With the MOUSE_ABS descriptor helpfully included and easily editable in particular, you have everything you could need to make your own descriptor quickly, be it a custom keyboard, a touchscreen, or a Braille display. In CircuitPython, you don’t even have to recompile the firmware to change a descriptor, but it’d be harder to use CircuitPython for tinkering here, for reasons we might just pinpoint and alleviate in a future article! Until then, this is a MicroPython library that lets you play with HID directly, on the lowest level comfortable. In the next article, I want to give you a Linux-compatible toolkit for playing with both device descriptors and HID packets, showing you how to quickly and easily debug all sides of the equation, and then, I’ll show you how to modify the descriptor to create your own USB touchscreen!

9 thoughts on “Human-Interfacing Devices: The Descriptor Heist

  1. The HID usage tables are surprisingly comprehensive. Sadly most of the codes are never used or supported by operating systems. For example barcode scanners just disguise as 0x07 keyboards instead of using 0x8C Barcode Scanner Page. Maybe one day someone will build a Magic Carpet Simulation Device.

  2. i guess it’s probably just a quirk of how i work with these things, but i don’t think HID is such a complicated standard that you can’t just skim the standard and fill in the blanks. maybe it’s handy to have reference packets that you can try to decode in your head while you’re reading the standard? i just don’t think ‘stealing’ is really relevant to this challenge.

    it’s not really the same thing but this reminds me of a flashquark keyboard i have. they stole the USB vendor/device id from apple. i don’t know if they felt like they couldn’t get their own vendor id (when i looked briefly, i did have a little trouble finding a blessed catch-all garbage vendor id for one-off hacks, maybe the situation is worse for small vendors). or maybe they were afraid that OS drivers would not accept their keyboard if it wasn’t already on a whitelist.

    anyways, it was a mild nuissance for me, as it loaded up hid-apple which remapped all my function keys against my desires. the fix is easy, “options hid_apple fnmode=0” in /etc/moprobe.d/flashquark.conf

    i guess mostly it’s disappointing that HID is not self-contained, that the interpretation of HID is still dependent on the USB vendor/device ID. i would have hoped that HID would make it so that the USB IDs simply don’t matter to how the packets are interpretted.

    but of course the real problem with the keyboard (and the reason i was using its USB interface in the first place) is that somehow all these years later, bluetooth is still DOA. given the enormous bandwidth and DSP capacity, i am just astonished that bluetooth still can’t maintain a reliable 100bps over 10ft. and the protocol layers are so poorly suited to the flaws of the physical implementation that if you drop a single packet, that failure is passed through to the user experience instead of compensated for with retransmission. it’s just unbelievably bad and for no reason. bespoke one-off hacks work better than the official protocol. why?!

  3. God I wish the number format on report descriptors was less insane. Give me a hex editor and I can read the structure of a certificate or key in DER, but report descriptors I need two spec documents and a copy of notepad open just to parse it.

  4. I’m still looking for an example of how to parse and generate USB and/or SATA data streams. I *really* miss the days when drives had physical write-protect mechanisms. Yes, there are a few specialized USB drives that have this; yes, there are expensive “forensic” devices that can do it, but there are enough small processors now that can handle those data rates that it really shouldn’t be hard to build a cheap firewall for these protocols

  5. The biggest problem with HID report descriptors is that they are too flexible, so while it’s easy to concoct many different types of data structures and logic hierarchy with it, it will ultimately come down to what the host OS driver actually can parse. This is why the strategy done in this post is entirely justifiable.

    The HID usage table is also a failure, they only provide a basic description on the carried value, but do nothing to explain in what usage hierarchy they can be supported, or how a practical complete report descriptor might look like. They have mismanaged this table for a decade, leading to some IDs having conflicting usages assigned, a problem that still pops up these days, after the table being finally consolidated.

    What I find the craziest is that even though Microsoft has the biggest influence on the HID working group, they still created custom protocols for XBOX controllers instead of using already established HID usages (and extending them where necessary), and Windows assigning a first-class XInput driver only to such gamepads. (Well, later they also backtracked somewhat, there exists an xinputhid driver, that Windows only assigns to devices with Microsoft’s vendor ID…)

    I have a C++ library that makes HID report descriptors a lot more manageable, it not only helps write them with syntactic help, but also does compile-time verification of the layout (requires exception support), and can extract meta-information from it in compile time (e.g. report IDs used, max report sizes): https://github.com/IntergatedCircuits/hid-rp

  6. So, I look with interest, my hacking skills are pretty low but I do play.
    I want, really want, a mouse, a standard mouse but not a ball mouse that I can turn off, not generate or block the “Y” axis data so the PC has no sense of “Y”:
    Why? well games of course, yes, I can play with autohotkey, it works unless the game bypasses it, (it does) the was/is other good programs that can do this, the game has bypassed all of them to date. So,I Need to Hack. I will watch, thank you for what I hope will help me make the hack happen .

Leave a Reply

Please be kind and respectful to help make the comments section excellent. (Comment Policy)

This site uses Akismet to reduce spam. Learn how your comment data is processed.