Key Mime Pi: Turn Your Raspberry Pi into a Remote Keyboard

Recent versions of the Raspberry Pi support USB on-the-go (USB OTG), which allows them to impersonate USB devices such as keyboards, thumb drives, and microphones. To take advantage of this, I made an open-source web app that turns my Pi into a fake keyboard. I call it Key Mime Pi.

This post demonstrates how Key Mime Pi works and how you can build one for yourself.

Demo ๐Ÿ”—︎

What you’ll need ๐Ÿ”—︎

Install Raspberry Pi OS Lite ๐Ÿ”—︎

To begin, install Raspberry Pi OS lite (formerly known as Raspbian) on a microSD card.

Screenshot of Rufus

I use Rufus to write my Pi micro SD cards, but any whole disk imaging tool will work.

Enable SSH access by placing a file called ssh on the microSD’s boot partition, and insert the microSD card into your Pi device. If you’re connecting over wireless, you’ll also need to create a wpa_supplicant.conf file on the boot partition.

Connecting your Pi ๐Ÿ”—︎

Connect the USB cable to your Pi’s USB OTG port. On the Pi 4, this is the USB-C port. For the Pi Zero, it’s the Micro-USB port labeled “USB.”

Connect the other end of the USB cable to the computer that you want to connect to as a keyboard. USB 3.0 ports work better because they output more power, but all the USB 2.0 ports I tested worked fine as well.

Your Pi should draw power from the computer’s USB port and power up.

Install Key Mime Pi ๐Ÿ”—︎

You have two options for installing Key Mime Pi. You can do it using plain old bash, which requires no external tools. Or, for a touch of class, install it via Ansible, my favorite open source configuration management tool.

Option 1: The pure bash way ๐Ÿ”—︎

From a bash shell, enter the following commands to connect to your Pi and configure it for USB device emulation:

# SSH into your Pi
PI_HOSTNAME="raspberrypi" # Change to your pi's hostname
PI_SSH_USERNAME="pi"      # Change to your Pi username

# Connect to the Pi (default password is "raspberry")
ssh "${PI_SSH_USERNAME}@${PI_HOSTNAME}"

# Install pre-requisites
sudo apt-get update && \
  sudo apt-get install -y \
    git \
    python-pip \
    python3-venv

# Install Key Mime Pi
git clone https://github.com/mtlynch/key-mime-pi.git
cd key-mime-pi
sudo ./enable-usb-hid
sudo reboot

Allow the Pi to reboot, then SSH in again and start the Key Mime Pi web server:

ssh "${PI_SSH_USERNAME}@${PI_HOSTNAME}"
cd key-mime-pi
python3 -m venv venv
. venv/bin/activate
pip install --requirement requirements.txt
PORT=8000 ./app/main.py

Option 2: The Ansible way ๐Ÿ”—︎

If you’re an Ansible user, you can use my Key Mime Pi Ansible role for better automation. The following commands install Key Mime Pi on your device as a systemd service:

PI_HOSTNAME="raspberrypi" # Change to your pi's hostname
PI_SSH_USERNAME="pi"      # Change to your Pi username

# Install the Key Mime Pi Ansible role
ansible-galaxy install mtlynch.keymimepi

# Create a minimal Ansible playbook to configure your Pi
echo "- hosts: $PI_HOSTNAME
  roles:
    - role: mtlynch.keymimepi" > install.yml

# Install all software (default password is "raspberry")
ansible-playbook \
  --inventory "$PI_HOSTNAME", \
  --user "$PI_SSH_USERNAME" \
  --become \
  --become-method sudo \
  --ask-pass \
  install.yml

# Reboot the Pi
ansible \
  "$PI_HOSTNAME" \
  -m reboot \
  --inventory "$PI_HOSTNAME", \
  --user "$PI_SSH_USERNAME" \
  --ask-pass \
  --become \
  --become-method sudo

Using Key Mime Pi ๐Ÿ”—︎

After you run the install script, Key Mime Pi will be available at:

Its interface looks like this:

Screenshot of Key Mime Pi web interface

Key Mime Pi web interface awaiting input

And like, magic, when you type into your browser, the keys will appear on the machine connected to the Pi.

Key Mime Pi transmitting keystrokes from the browser

Key Mime Pi allows you to send keystrokes through the browser to a remote computer.

How it works ๐Ÿ”—︎

USB device emulation ๐Ÿ”—︎

The real magic here comes from Linux’s USB Human Interface Device (HID) gadget driver. It allows user-mode applications to interact with the operating system as if they were USB devices.

The key-mime-pi configuration script creates a file path at /dev/hidg0. Any program can read or write to this path, and the OS translates the data to keyboard signals.

To mimic a keyboard, the Pi has to communicate with the OS according to the USB HID spec. At 97 pages of keycodes and tables, that document is a bit of a slog, but it turns out that the protocol for keyboards is dead simple.

Upon each keystroke, the keyboard sends an 8-byte message called a “report.”

Byte IndexPurpose
0Modifier keys (Ctrl, Alt, Shift)
1Reserved for manufacturers
2Key #1
3Key #2
4Key #3
5Key #4
6Key #5
7Key #6

Sending the keys for “Hi” looks like this:

# H (Right shift + h)
echo -ne "\x20\0\xb\0\0\0\0\0" > /dev/hidg0
# i
echo -ne "\0\0\xc\0\0\0\0\0" > /dev/hidg0
# Release all keys
echo -ne "\0\0\0\0\0\0\0\0" > /dev/hidg0

In addition to signalling key presses, keyboards must also indicate key releases. An 8-byte block of zeroes indicates that no keys are active.

The above example sent one keystroke at a time, but HID reports have space for six keys. This means you can send up to six keystrokes in a single message as long as they’re distinct keys:

echo -ne "\0\0\x1a\x0b\x04\x17\x18\x13" > /dev/hidg0 && \
  echo -ne "\0\0\0\0\0\0\0\0" > /dev/hidg0
whatup

Translating from JavaScript to HID ๐Ÿ”—︎

When you type into a browser window, JavaScript generates events for each keystroke. The website keycode.info provides an excellent demonstration of this functionality in action.

JavaScript key events include keycodes, but they’re distinct from HID keycodes. Fortunately, there’s a mostly 1:1 mapping between the two. To translate from JavaScript to HID, I created a lookup table like this:

_JS_TO_HID_KEYCODES = {
    3: 0x48,  # Pause / Break
    8: 0x2a,  # Backspace / Delete
    9: 0x2b,  # Tab
    ...
    65: 0x04,  # a
    66: 0x05,  # b
    67: 0x06,  # c
    68: 0x07,  # d

The Key Mime Pi server listens for JavaScript keycode events from the browser, translates them into HID codes, then sends them to the Pi’s HID interface at /dev/hidg0.

Here’s how it works from end to end:

  1. A user hits a key in the browser.
  2. JavaScript on the page sends the JavaScript keycode to the Key Mime Pi server on the Pi.
  3. The Key Mime Pi server translates the JavaScript keycode to its equivalent HID code.
  4. The Key Mime Pi server sends the HID code to the USB gadget interface at /dev/hidg0.
  5. The computer connected to the Pi’s USB cable receives this as keyboard input, causing a character to appear on the screen.

The power problem ๐Ÿ”—︎

In my tests, USB ports from computers produced enough electricity to power the Pi, but under-voltage warnings appeared frequently in the system log:

 $ sudo journalctl -xe | grep "Under-voltage"
Jun 05 03:46:05 keymimepi kernel: Under-voltage detected! (0x00050005)
Jun 05 03:48:29 keymimepi kernel: Under-voltage detected! (0x00050005)
Jun 05 03:54:22 keymimepi kernel: Under-voltage detected! (0x00050005)

The Pi was correctly detecting that standard USB 2.0 and USB 3.0 ports provide insufficient power to meet the Pi’s requirements.

Raspberry Pi ModelPower requirements
Pi Zero W5 V / 1.2 A
Pi 45 V / 3.0 A

Standard USB ports come up short:

USB Port TypePower output
USB 2.05 V / 0.5 A
USB 3.05 V / 0.9 A

I currently am still searching for a solution tot his problem. Here are some possible solutions I have not yet tested:

Troubleshooting ๐Ÿ”—︎

Verifying the driver is working ๐Ÿ”—︎

If Key Mime Pi fails to forward keystrokes to the target machine, the first step is to determine whether the USB gadget is working properly.

Try the following commands:

echo -ne "\0\0\xb\0\0\0\0\0" > /dev/hidg0 && \
  echo -ne "\0\0\xc\0\0\0\0\0" > /dev/hidg0 && \
  echo -ne "\0\0\0\0\0\0\0\0" > /dev/hidg0

If everything is working, you should see the following output on the machine the Pi is connected to via USB:

hi

Writes to HID interface hang ๐Ÿ”—︎

When testing Key Mime Pi on the Pi Zero W, I ran into a case where writes to /dev/hidg0 hung indefinitely. I tried a different Micro-USB to USB-A cable, and the problem went away. I suspect that the first cable was either damaged or supported power only and not data. If you run into hanging writes to /dev/hidg0, try a USB cable that supports data transfer (most USB cables do).

Next step: embedding display output ๐Ÿ”—︎

Remote typing is fun, but it’s a bit impractical. When you’re typing into a system, it generally helps to see the output too.

My next step is to capture HDMI output from the target computer and embed it in Key Mime Pi’s web interface. That way, I’ll be able to plug my Pi into a headless server and have a virtual console in the browser. It will essentially be a low-cost, hackable KVM over IP device.

I have a working prototype using ffplay and an HDMI extender, but I’m still working on a solution that puts everything in a single browser window with low latency.

Screenshot of Key Mime Pi showing the remote machine's screen

I can view the remote machine’s monitor output using an HDMI extender, but I’m still working on integrating everything into the browser.

Want a pre-configured kit? ๐Ÿ”—︎

Update: June 29, 2020

I now have a working solution that both captures video output from a target device and allows you to send keystrokes, all within a browser window.

A detailed follow-up post is coming soon, but in the meantime, you can pre-order pre-configured KVM Pi kits that include everything you need to build your own KVM Pi:

Source code ๐Ÿ”—︎

Key Mime Pi’s code is fully open source under the permissive MIT license:

  • key-mime-pi: Web server that forwards keystrokes to the Pi’s virtual keyboard.
  • ansible-role-key-mime-pi: The Ansible role for configuring the Pi’s USB gadget functionality and for installing the web server as a systemd service.

Acknowledgments ๐Ÿ”—︎

  • raspberrypisig/pizero-usb-hid-keyboard was the first sample code I found that successfully installed the virtual USB HID device on my Pi.
  • Fmstrat/diy-ipmi was an inspiration for this project and proved that it was possible to make a Pi function as a KVM over IP.
  • Rafael Medina provided the most readable explanation of the HID protocol I found.
  • Thanks to the Linux and Raspberry Pi OS developers who made USB gadget functionality possible.