HID Reverse Engineering Guide

This guide walks through reverse engineering a gamepad's HID protocol from scratch. No prior HID experience needed — just basic hex literacy. Once you have identified all fields, proceed to the Device Config Guide to write the TOML config.

Prerequisites

Install these tools before starting:

# Wireshark + USB monitor kernel module
sudo pacman -S wireshark-qt    # or apt install wireshark
sudo modprobe usbmon

# Raw hex tools (usually pre-installed)
which xxd hexdump

# Input device testing
sudo pacman -S evtest          # or apt install evtest

# padctl's own capture tool
padctl-capture --help

You need read access to /dev/hidraw* and /dev/usbmon*. Either run as root or add your user to the appropriate groups:

sudo usermod -aG input $USER   # for hidraw
sudo usermod -aG wireshark $USER

Step 1: Identify the Device

Plug in your gamepad and find it:

$ lsusb
Bus 001 Device 012: ID 054c:0ce6 Sony Corp. DualSense Wireless Controller

The hex pair 054c:0ce6 is your VID:PID. Write these down — they go directly into the TOML config.

Find the hidraw node

$ ls /dev/hidraw*
/dev/hidraw0  /dev/hidraw1  /dev/hidraw2  /dev/hidraw3

$ cat /sys/class/hidraw/hidraw3/device/uevent
HID_ID=0003:0000054C:00000CE6
HID_NAME=Sony Interactive Entertainment Wireless Controller
HID_PHYS=usb-0000:08:00.3-2/input3

The HID_ID confirms VID/PID. The input3 at the end of HID_PHYS tells you this is interface 3.

Multiple interfaces

Many devices expose several USB interfaces. A DualSense has interfaces 0-3 (audio + HID). You need the one that carries gamepad data. Quick way to find it:

# Read a few bytes from each hidraw node while pressing buttons
for i in /dev/hidraw*; do
    echo "=== $i ==="
    sudo timeout 1 xxd -l 64 -c 32 "$i" 2>/dev/null || echo "(no data)"
done

The node that produces continuous output when you press buttons or move sticks is your target.


Step 2: Capture Raw HID Reports

padctl-capture --device /dev/hidraw3 --duration 30 --output capture.bin

While capturing, do each action one at a time with a pause between:

  1. Leave controller idle for 3 seconds (this is your baseline)
  2. Press and release each face button (A, B, X, Y) one at a time
  3. Press and release each shoulder button (LB, RB, LT, RT)
  4. Move left stick to full left, full right, full up, full down
  5. Move right stick the same way
  6. Press each D-pad direction
  7. Press Start, Select, Home

Write down the order and approximate timing. You will cross-reference this with the capture data.

Method 2: Wireshark USB capture

sudo modprobe usbmon

Open Wireshark, select the usbmonN interface matching your USB bus (from lsusb output). Apply this display filter:

usb.transfer_type == 0x01 && usb.dst == "host"

This shows only interrupt IN transfers (device-to-host) — which is how gamepads send input reports.

Start capture, perform the same systematic button/axis sequence, then stop.

Method 3: Quick and dirty with xxd

For a fast look without any special tools:

sudo xxd -c 64 -g 1 /dev/hidraw3 | head -20

This prints raw reports in hex as they arrive. Move a stick or press a button to see bytes change.


Step 3: Analyze the Protocol

This is the core skill. You are looking at raw bytes and figuring out what each one means.

Determine report size and report ID

Look at the raw data. Every read from hidraw returns one complete report. Check the length — common sizes are 10, 20, 32, 49, 64, or 78 bytes.

If the first byte is constant across all reports, it is likely a report ID. For example, DualSense USB reports always start with 0x01:

01 80 80 80 80 00 00 08 00 00 ...
^^
Report ID 0x01

Some devices (like Flydigi Vader 5) use multi-byte magic headers:

5a a5 ef 00 00 00 00 00 00 ...
^^^^^^^^
3-byte magic prefix

Find the idle baseline

With nothing pressed and sticks centered, capture several reports. This is your baseline:

Idle DualSense USB report (64 bytes):
01 80 80 80 80 00 00 08 00 00 00 00 00 00 00 00
00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00
00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00
00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00

Note bytes 1-4 are 80 80 80 80 — that is four axes centered at 0x80 (128).

Identify analog axes

Move only the left stick fully left, and compare with idle:

Idle:       01 [80] 80 80 80 00 00 08 ...
Left full:  01 [00] 80 80 80 00 00 08 ...
                ^^
                Byte 1 changed: 0x80 → 0x00

Now fully right:

Right full: 01 [ff] 80 80 80 00 00 08 ...
                ^^
                Byte 1: 0x80 → 0xFF

This tells you:

  • Byte 1 = left stick X axis
  • Type: u8 (unsigned, 0x00 = left, 0x80 = center, 0xFF = right)
  • Needs transform = "scale(-32768, 32767)" to map to standard axis range

Repeat for left stick Y (byte 2), right stick X (byte 3), right stick Y (byte 4).

How to tell u8 vs i16le:

PatternTypeCenterRange
Single byte, idle = 0x80u81280-255
Single byte, idle = 0x00i80-128 to 127
Two bytes, idle = 0x00 0x00i16le0-32768 to 32767
Two bytes, idle = 0x00 0x80u16le centered327680-65535

For i16le, you will see two adjacent bytes change together. Move the stick fully right:

8BitDo Ultimate (i16le axes):
Idle:       01 [00 00] [00 00] [00 00] [00 00] ...
Right full: 01 [ff 7f] [00 00] [00 00] [00 00] ...
                ^^^^^
                0x7FFF = 32767 in little-endian = i16le max

Identify triggers

Triggers are usually u8 (0 = released, 0xFF = fully pressed). Slowly squeeze a trigger and watch which byte ramps from 0x00 to 0xFF:

LT released: ... 00 00 08 ...
LT half:     ... 80 00 08 ...
LT full:     ... ff 00 08 ...
                  ^^
                  Byte 5 = LT, type u8

Identify buttons

Press one button at a time and XOR with the idle frame to find changed bits:

Idle byte 8:    08  = 0000 1000
Press Cross:    28  = 0010 1000
XOR:            20  = 0010 0000  → bit 5 changed

So the Cross/A button is bit 5 of byte 8.

Do this for every button. Build a table:

ButtonByteBit (in byte)Bit (in group)
Square/X844
Cross/A855
Circle/B866
Triangle/Y877
L1/LB908
R1/RB919
L3/LS9614
R3/RS9715

The "bit in group" is calculated from the button_group source offset. If source = { offset = 8, size = 3 }, then bit indices are: byte 8 bits 0-7, byte 9 bits 8-15, byte 10 bits 16-23.

Identify D-pad

D-pads come in two flavors:

Hat switch (most common): A single nibble (4 bits) encodes direction as a number 0-8:

0=N  1=NE  2=E  3=SE  4=S  5=SW  6=W  7=NW  8=neutral

Look for a nibble in the button bytes that cycles through these values as you press D-pad directions. On DualSense, bits [3:0] of byte 8 are the hat:

Idle:    08 (1000) → hat = 8 (neutral)
Up:      00 (0000) → hat = 0 (north)
Right:   02 (0010) → hat = 2 (east)
Down:    04 (0100) → hat = 4 (south)
Left:    06 (0110) → hat = 6 (west)

Button bits: Four separate bits, one for each direction. Flydigi Vader 5 uses this:

map = { DPadUp = 0, DPadRight = 1, DPadDown = 2, DPadLeft = 3, ... }

Spot checksums

If the last 1-4 bytes change with every report even when nothing else changes, that is likely a checksum or sequence counter. DualSense Bluetooth has a CRC32 in the last 4 bytes:

Report bytes 74-77 change every frame, even when idle
→ CRC32 checksum over bytes 0-73

A single byte that increments by 1 each report is a sequence counter (common, usually ignored).


Tips and Tricks

Compare with similar devices

Devices from the same vendor often share report layouts. DualShock 4 and DualSense share the same structure with minor offset shifts (see devices/sony/dualshock4.toml vs devices/sony/dualsense.toml). If your device is a newer revision of a known one, start from the existing config and adjust offsets.

Finding output commands (rumble, LED)

In Wireshark, look for host-to-device interrupt or control transfers:

usb.transfer_type == 0x01 && usb.dst != "host"

Or look for SET_REPORT control transfers:

usb.setup.bRequest == 0x09

Trigger rumble from another driver or app and capture the outgoing bytes. The structure is usually: report ID + flags + motor values + padding.

Vendor-specific magic

Some devices (like Flydigi Vader 5) require an init sequence to enter extended mode. Signs that you need this:

  • Reports are very short (< 10 bytes) and missing axes
  • Reports change format after you send a specific command
  • A reference driver sends a series of vendor commands on open

Look at how existing Linux drivers handle the device. Protocol facts (byte sequences, report formats) are not copyrightable (Feist v. Rural, 1991) — you may freely use byte offsets, field types, VID/PID, and bit positions found in any open-source driver. Do not copy source code or comment text verbatim.