rmed

blog

Using RPi Zero as a Keyboard Part 3: Sending and receiving reports

2017-07-13 11:40

In this third and last part of the series we will use the new keyboard to send keystrokes to the connected computer. To do this, I will provide a couple of examples using Bash and Python, although they can be easily translated to other languages.

Edited July 8, 2018: Tested on Raspbian Stretch (lite) 2018-06-27. Added null character in between the ll in Hello.


Index

  1. Setup and device definition
  2. Report descriptor
  3. Sending and receiving reports (this post)

Input reports

Input reports are those sent from the keyboard to the computer. As we saw in the previous post, input reports for keyboards are 8 bytes long:

  • 1 byte: modifier keys (Control, Shift, Alt, etc.), where each bit corresponds to a key
  • 1 byte: unused/reserved for OEM
  • 6 bytes: pressed key codes

In order to press a regular key (e.g. A or B), its code has to be included inside the 6 byte segment. Remember that this segment represents the pressed keys, so until a key stops appearing there the host will keep pressing that key constantly.

The modifier keys, however, are 1 bit each. The modifier byte has the following structure (bit 0 is on the rightmost part):

[Right Meta | Right Alt | Right Shift | Right Control | Left Meta | Left Alt | Left Shift | Left Control]

If a given bit is set to 1, then the modifier key in question is pressed.

Now, as an example, let's write Hello World! with the virtual keyboard. Note that the device, once activated as shown in the first post (and having connected the Pi to a computer through the data port), will be accessible from the path /dev/hidg0 and can be interacted with like a normal file.

For example, in bash:

#!/bin/bash

function write_report {
    echo -ne $1 > /dev/hidg0
}

# H (press shift and H)
write_report "\x20\0\xb\0\0\0\0\0"

# e
write_report "\0\0\x8\0\0\0\0\0"

# ll
write_report "\0\0\xf\0\0\0\0\0"
write_report "\0\0\0\0\0\0\0\0"
write_report "\0\0\xf\0\0\0\0\0"

# o
write_report "\0\0\x12\0\0\0\0\0"

# SPACE
write_report "\0\0\x2c\0\0\0\0\0"

# W (press shift and W)
write_report "\x20\0\x1a\0\0\0\0\0"

# o
write_report "\0\0\x12\0\0\0\0\0"

# r
write_report "\0\0\x21\0\0\0\0\0"

# l
write_report "\0\0\xf\0\0\0\0\0"

# d
write_report "\0\0\x7\0\0\0\0\0"

# ! (press shift and 1)
write_report "\x20\0\x1e\0\0\0\0\0"

# Release al keys
write_report "\0\0\0\0\0\0\0\0"

And for Python (3):

#!/usr/bin/env python3

NULL_CHAR = chr(0)

def write_report(report):
    with open('/dev/hidg0', 'rb+') as fd:
        fd.write(report.encode())


# H (press shift and H)
write_report(chr(32)+NULL_CHAR+chr(11)+NULL_CHAR*5)

# e
write_report(NULL_CHAR*2+chr(8)+NULL_CHAR*5)

# ll
write_report(NULL_CHAR*2+chr(15)+NULL_CHAR*5)
write_report(NULL_CHAR*8)
write_report(NULL_CHAR*2+chr(15)+NULL_CHAR*5)

# o
write_report(NULL_CHAR*2+chr(18)+NULL_CHAR*5)

# SPACE
write_report(NULL_CHAR*2+chr(44)+NULL_CHAR*5)

# W (press shift and W)
write_report(chr(32)+NULL_CHAR+chr(26)+NULL_CHAR*5)

# o
write_report(NULL_CHAR*2+chr(18)+NULL_CHAR*5)

# r
write_report(NULL_CHAR*2+chr(21)+NULL_CHAR*5)

# l
write_report(NULL_CHAR*2+chr(15)+NULL_CHAR*5)

# d
write_report(NULL_CHAR*2+chr(7)+NULL_CHAR*5)

# ! (press shift and 1)
write_report(chr(32)+NULL_CHAR+chr(30)+NULL_CHAR*5)

# Release all keys
write_report(NULL_CHAR*8)

Note that both examples may require superuser permissions in order to access the device file, and that the key codes must be sent in binary format. Also note that the only report that explicitly release the keys is the last one, as that is when we end writing and no more keys are going to be pressed.

Output reports

Output reports are sent from the host to the keyboard in order to set LED states. These reports are 1 byte long and each bit represents the state of a single LED. Their structure is as follows:

[CONSTANT | CONSTANT | CONSTANT | Kana | Compose | Scroll Lock | Caps Lock | Num Lock]

Take into account that these may not only be sent when pressing keys such as Num Lock. For instance, when dealing with virtual machines, the host will change the state of the LEDs depending on the state in the main operating system or the operating system inside the virtual machine.

A very simple example of reading an output report in Python (3) is as follows:

#!/usr/bin/env python3

with open('/dev/hidg0', 'rb+') as fd:
    # Read only 1 byte
    output = fd.read(1)

Final note

I hope this short series of posts was somewhat useful. Having the Raspberry Pi Zero act as a USB device, in this case a HID such as a keyboard, offers many different possibilities such as automated testing, fast shortcuts, or device prototyping. That, together with the fact that it is such a low-cost board and the multiple periferials and components available, make it a great base for many different types of projects.

Here I've only covered the basis of a keyboard (HID) gadget, but there are many types available that seem as interesting and worth looking at. This is only the beginning!

Post series index

  1. Setup and device definition
  2. Report descriptor
  3. Sending and receiving reports (this post)