Raspberry Pi as USB gadget – part 1

Today we will make our RPi a slave device connected to PC via USB. And this RPi will be detected as the keyboard.
We need Raspberry Pi Zero or Zero W with the micro USB connector.

Configuration

First, we have to enable ConfigFS for USB gadgets:

echo "dtoverlay=dwc2" | sudo tee -a /boot/config.txt
echo "dwc2" | sudo tee -a /etc/modules
sudo echo "libcomposite" | sudo tee -a /etc/modules

Shutdown Raspberry and plug USB from computer to USB socket not power in. Windows should cry that it cannot configure the device but do not worry.

After boot, we have to create a description for our USB device. Thanks to the Linux fs system it is relatively easy. We need to create directories and files with required values. Just like that. If you want some more detailed information read links from the references section.
In short, we have to declare device name, vendor, config, functions and descriptor.

The most important thing is report_desc. This file describes our device and its capabilities. It is a binary structured file.
Currently, the best tutorial I found is here: https://eleccelerator.com/tutorial-about-usb-hid-report-descriptors/

Our first goal: use RPi as a keyboard. To make it simple let’s create a python setup script. We will use default keyboard descriptor.

import os
import sys
import shutil

vendor_id = "0x1d6b"  # Linux Foundation
product_id = "0x0104"  # Multifunction Composite Gadget
serialnumber = "rpi0w_000004"
manufacturer = "Kosci"
product = "Hane control device"
lang = "0x409"  # english
path = "/sys/kernel/config/usb_gadget/" +product.lower().replace(" ", "_")

functions = [
    {
        "name": "hid.usb0",
        "protocol": "1",
        "subclass": "1",
        "report_length": "8",
        "report_desc": [
            0x05, 0x01,  # USAGE_PAGE (Generic Desktop)
            0x09, 0x06,  # USAGE (Keyboard)
            0xa1, 0x01,  # COLLECTION (Application)
            0x05, 0x07,  # USAGE_PAGE (Keyboard)
            0x19, 0xe0,  # USAGE_MINIMUM (Keyboard LeftControl)
            0x29, 0xe7,  # USAGE_MAXIMUM (Keyboard 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)
            0x95, 0x01,  # REPORT_COUNT (1)
            0x75, 0x08,  # REPORT_SIZE (8)
            0x81, 0x01,  # INPUT (Cnst,Var,Abs) // 0x03
            0x95, 0x05,
            0x75, 0x01,
            0x05, 0x08,
            0x19, 0x01,
            0x29, 0x05,
            0x91, 0x02,
            0x95, 0x01,
            0x75, 0x03,
            0x91, 0x01,  # 0x03
            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
        ],
    }
]

The protocol and subclass variables set to “1” define the device as standard keyboard available during boot.

def fileputcontent(filename, content, mode="w"):
    with open(filename, mode) as fp:
        if type(content) == str:
            fp.write(content)
        if type(content) == list:
            fp.write(bytearray(content))


def create_dirs(path):
    if not os.path.exists(path):
        os.makedirs(path)

    if not os.path.exists(path + "/strings/" + lang):
        os.makedirs(path + "/strings/" + lang)

    if not os.path.exists(path + "/configs/c.1/strings/" + lang):
        os.makedirs(path + "/configs/c.1/strings/" + lang)


def create_functions(path, functions):
    for function in functions:
        if not os.path.exists(path + "/functions/" + function["name"]):
            os.makedirs(path + "/functions/" + function["name"])
        fileputcontent(path + "/functions/" + function["name"] + "/protocol", function['protocol'])
        fileputcontent(path + "/functions/" + function["name"] + "/subclass", function['subclass'])
        fileputcontent(path + "/functions/" + function["name"] + "/report_length", function['report_length'])
        fileputcontent(path + "/functions/" + function["name"] + "/report_desc", function['report_desc'], "wb")
        if not os.path.exists(path + "/configs/c.1/" + function["name"]):
            os.symlink(path + "/functions/" + function["name"], path + "/configs/c.1/" + function["name"], True)


def enable():
    tmp = os.listdir("/sys/class/udc")
    print(tmp)
    fileputcontent(path + "/UDC", tmp[0])


def disable():
    fileputcontent(path + "/UDC", "")


def install():
    create_dirs(path)
    fileputcontent(path + "/idVendor", vendor_id)
    fileputcontent(path + "/idProduct", product_id)
    fileputcontent(path + "/bcdDevice", "0x0100")
    fileputcontent(path + "/bcdUSB", "0x0200")

    fileputcontent(path + "/strings/" + lang + "/serialnumber", serialnumber)
    fileputcontent(path + "/strings/" + lang + "/manufacturer", manufacturer)
    fileputcontent(path + "/strings/" + lang + "/product", product)

    fileputcontent(path + "/configs/c.1/strings/" + lang + "/configuration", "Config 1: ECM network")
    fileputcontent(path + "/configs/c.1/MaxPower", "250")

    create_functions(path, functions)

    enable()


def remove():
    shutil.rmtree(path)


if len(sys.argv) == 1:
    print("Usage: install, remove, enable, disable")
elif sys.argv[1] == "install":
    install()
elif sys.argv[1] == "remove":
    remove()
elif sys.argv[1] == "disable":
    disable()
elif sys.argv[1] == "enable":
    enable()
else:
    print("No idea")

And that is our script.
We have to run it via sudo:

sudo python3 setup.py install

And after a second Windows showed a popup with “Installing new device..” and I can see it in devices:

Good, we have it but how to use it?

Click a key

How can we send char to PC?
First, we have a new entry in /dev dir. Type:

ls /dev/hid*

You should see something like:

/dev/hidg0

This is all we need.

We will write to this file something called report. It is a packet that was described in report_desc. We are using a keyboard so it is predefined in USB specification.

#!/usr/bin/env python3

import time

time.sleep(0.5)
NULL_CHAR = chr(0)

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

def press(key, mod=0x00):
    try:
        write_report(chr(mod) + NULL_CHAR + chr(key) + NULL_CHAR * 5)
        write_report(NULL_CHAR * 8)
    except:
        write_report(NULL_CHAR * 8)
        raise

press(0x04)  # key a according to spec
press(0x04, 32)  # key A, a + shift mod

The delay function at the top is for me to change the active window to editor’s one.

We have pressed a and A key with software 🙂 Nice.

Reference

http://isticktoit.net/?p=1383
https://www.kernel.org/doc/Documentation/usb/gadget_hid.txt
https://www.rmedgar.com/blog/using-rpi-zero-as-keyboard-setup-and-device-definition
http://www.raspibo.org/wiki/index.php/Chip:_USB_hid_on_libcomposite
Raspberry Pi Zero as Multiple USB Gadgets
https://eleccelerator.com/tutorial-about-usb-hid-report-descriptors/

Advertisements

One comment

Leave a Reply

Fill in your details below or click an icon to log in:

WordPress.com Logo

You are commenting using your WordPress.com account. Log Out /  Change )

Google+ photo

You are commenting using your Google+ account. Log Out /  Change )

Twitter picture

You are commenting using your Twitter account. Log Out /  Change )

Facebook photo

You are commenting using your Facebook account. Log Out /  Change )

Connecting to %s

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