padctl
padctl maps gamepad HID reports to Linux uinput events using a declarative TOML device config.
Overview
- TOML device config — describe report layout, field offsets, button groups, and output capabilities without writing code.
- padctl-capture — record HID traffic and generate a TOML skeleton automatically.
- padctl --validate — static config checker for CI and community contributions.
- padctl --doc-gen — generate device reference pages from TOML.
Source
https://github.com/BANANASJIM/padctl
Getting Started
Prerequisites
- Zig 0.15+ (build from source)
- Linux kernel ≥ 5.10 (uinput + hidraw support)
- libusb-1.0 (system package, optional — pass
-Dlibusb=falseto build without) - A HID gamepad accessible via
/dev/hidraw*
Build from Source
git clone https://github.com/BANANASJIM/padctl
cd padctl
zig build -Doptimize=ReleaseSafe
Optional build flags:
-Dlibusb=false— disable libusb linkage (uses hidraw-only path)-Dwasm=false— disable WASM plugin runtime
Install
sudo ./zig-out/bin/padctl install
This copies the binary, systemd service, device configs, and udev rules into /usr. It also runs systemctl daemon-reload and udevadm trigger automatically.
Custom prefix (e.g. for packaging):
sudo ./zig-out/bin/padctl install --prefix /usr --destdir "$DESTDIR"
Verify
padctl scan
Lists all connected HID devices and shows whether a matching device config was found for each.
Run with a Device
# Single config
sudo padctl --config /usr/share/padctl/devices/sony/dualsense.toml
# All configs in a directory (auto multi-device)
sudo padctl --config-dir /usr/share/padctl/devices/
Run as Service
sudo systemctl enable --now padctl.service
The service runs padctl in daemon mode, managing all matched devices with hotplug support.
Validate a Config
padctl --validate devices/sony/dualsense.toml
Exit 0 = valid. Exit 1 = validation errors printed to stderr. Exit 2 = file not found or parse failure.
Generate Device Docs
padctl --doc-gen --config devices/sony/dualsense.toml
udev Permissions
padctl needs access to /dev/hidraw* and /dev/uinput. The padctl install command installs udev rules automatically. For manual installs:
sudo cp install/99-padctl.rules /etc/udev/rules.d/
sudo udevadm control --reload-rules
sudo udevadm trigger
The udev rules use TAG+="uaccess" to grant the logged-in user access to supported devices without requiring root.
Device Config Reference
Device configs are TOML files in devices/<vendor>/<model>.toml.
[device]
| Field | Type | Required | Description |
|---|---|---|---|
name | string | yes | Human-readable device name |
vid | integer | yes | USB vendor ID (hex literal ok: 0x054c) |
pid | integer | yes | USB product ID |
mode | string | no | Device mode identifier |
[[device.interface]]
| Field | Type | Required | Description |
|---|---|---|---|
id | integer | yes | USB interface number |
class | string | yes | "hid" or "vendor" |
ep_in | integer | no | IN endpoint number |
ep_out | integer | no | OUT endpoint number |
[device.init]
Optional initialization sequence sent after device open.
| Field | Type | Required | Description |
|---|---|---|---|
commands | string[] | yes | Hex byte strings sent in order |
response_prefix | integer[] | yes | Expected response prefix bytes |
enable | string | no | Hex byte string sent to activate extended mode (e.g. BT mode switch) |
disable | string | no | Hex byte string sent on shutdown |
interface | integer | no | Interface to send init commands on |
report_size | integer | no | Expected report size after init |
[[report]]
Describes one incoming HID report.
| Field | Type | Required | Description |
|---|---|---|---|
name | string | yes | Report name (unique within device) |
interface | integer | yes | Which interface this report arrives on |
size | integer | yes | Report byte length |
[report.match]
Disambiguates reports when multiple share an interface.
| Field | Type | Description |
|---|---|---|
offset | integer | Byte position to inspect |
expect | integer[] | Expected bytes at that offset |
[report.fields]
Inline table mapping field names to their layout:
[report.fields]
left_x = { offset = 1, type = "u8", transform = "scale(-32768, 32767)" }
gyro_x = { offset = 16, type = "i16le" }
battery_level = { bits = [53, 0, 4] }
| Field | Type | Description |
|---|---|---|
offset | integer | Byte offset in report |
type | string | Data type (see below) |
bits | integer[3] | Sub-byte extraction: [byte_offset, bit_offset, bit_count] |
transform | string | Comma-separated transform chain |
Use offset + type for whole-byte fields. Use bits for sub-byte bit extraction (e.g. a 4-bit battery level packed within a byte).
Note: When using
bits, thetypefield must benull,"unsigned", or"signed"— standard type strings like"u8"or"i16le"are not valid.
Data Types
u8 i8 u16le i16le u16be i16be u32le i32le u32be i32be
Transform DSL
Transforms are applied left-to-right as a comma-separated chain:
| Transform | Description |
|---|---|
scale(min, max) | Linearly scale the raw value to the target range |
negate | Negate the value (multiply by -1) |
abs | Take the absolute value |
clamp | Clamp to the output axis range |
deadzone | Apply deadzone filtering |
Example: transform = "scale(-32768, 32767), negate" — scales a u8 (0–255) to -32768..32767, then negates the result.
[report.button_group]
Maps a contiguous byte range to named buttons via bit index.
[report.button_group]
source = { offset = 8, size = 3 }
map = { A = 5, B = 6, X = 4, Y = 7, LB = 8, RB = 9 }
Button names must be valid ButtonId values:
A B X Y LB RB LT RT Start Select Home Capture LS RS DPadUp DPadDown DPadLeft DPadRight M1 M2 M3 M4 Paddle1 Paddle2 Paddle3 Paddle4 TouchPad Mic C Z LM RM O
[report.checksum]
Optional integrity check on the report.
| Field | Type | Description |
|---|---|---|
algo | string | crc32 crc8 sum8 xor none |
range | integer[2] | [start, end] byte range to checksum |
seed | integer | Initial seed value prepended to CRC calculation (e.g. 0xa1 for DualSense BT) |
expect.offset | integer | Where the checksum is stored in the report |
expect.type | string | Storage type of the checksum field |
[commands.<name>]
Output command templates (rumble, LED, adaptive triggers, etc.). Template placeholders use {name:type} syntax.
[commands.rumble]
interface = 3
template = "02 01 00 {weak:u8} {strong:u8} 00 ..."
Adaptive Trigger Commands
DualSense-style adaptive triggers use a naming convention of adaptive_trigger_<mode>:
[commands.adaptive_trigger_off]
interface = 3
template = "02 0c 00 ..."
[commands.adaptive_trigger_feedback]
interface = 3
template = "02 0c 00 ... 01 {r_position:u8} {r_strength:u8} ... 01 {l_position:u8} {l_strength:u8} ..."
[commands.adaptive_trigger_weapon]
interface = 3
template = "02 0c 00 ... 02 {r_start:u8} {r_end:u8} {r_strength:u8} ... 02 {l_start:u8} {l_end:u8} {l_strength:u8} ..."
[commands.adaptive_trigger_vibration]
interface = 3
template = "02 0c 00 ... 06 {r_position:u8} {r_amplitude:u8} {r_frequency:u8} ... 06 {l_position:u8} {l_amplitude:u8} {l_frequency:u8} ..."
[output]
Declares the uinput device emitted by padctl.
| Field | Type | Description |
|---|---|---|
emulate | string | Preset emulation profile |
name | string | uinput device name |
vid | integer | Emulated vendor ID |
pid | integer | Emulated product ID |
[output.axes]
[output.axes]
left_x = { code = "ABS_X", min = -32768, max = 32767, fuzz = 16, flat = 128 }
[output.buttons]
[output.buttons]
A = "BTN_SOUTH"
B = "BTN_EAST"
[output.dpad]
[output.dpad]
type = "hat" # or "buttons"
[output.force_feedback]
[output.force_feedback]
type = "rumble"
max_effects = 16
[output.aux]
Auxiliary output device (mouse or keyboard).
| Field | Type | Description |
|---|---|---|
type | string | "mouse" or "keyboard" |
name | string | uinput device name |
keyboard | bool | Create keyboard capability |
buttons | table | Button-to-event mapping |
[output.touchpad]
Touchpad output device.
| Field | Type | Description |
|---|---|---|
name | string | uinput device name |
x_min / x_max | integer | X axis range |
y_min / y_max | integer | Y axis range |
max_slots | integer | Maximum multitouch slots |
[wasm]
WASM plugin for stateful/custom protocols (Phase 4+).
| Field | Type | Description |
|---|---|---|
plugin | string | Path to .wasm plugin file |
[wasm.overrides]
| Field | Type | Description |
|---|---|---|
process_report | bool | Plugin handles report processing |
Mapping Config Reference
An optional --mapping TOML file overrides the default button/axis pass-through with remapping, gyro mouse, stick modes, layers, and macros.
Top-level Fields
name = "fps"
| Field | Type | Description |
|---|---|---|
name | string | Mapping profile name |
[remap]
Top-level button remapping (active when no layer overrides). Keys are ButtonId names, values are target button names, KEY_* codes, mouse_left/mouse_right/mouse_middle/mouse_side, disabled, or macro:<name>.
[remap]
M1 = "KEY_F13"
M2 = "mouse_side"
M3 = "disabled"
A = "B"
M4 = "macro:dodge_roll"
[gyro]
Global gyro-to-mouse configuration.
[gyro]
mode = "mouse"
activate = "L3"
sensitivity = 2.0
deadzone = 300
smoothing = 0.4
curve = 1.0
invert_y = true
| Field | Type | Default | Description |
|---|---|---|---|
mode | string | "off" | "off" or "mouse" |
activate | string | — | Button name to hold for activation (e.g. "L3", "hold_RB") |
sensitivity | float | — | Overall sensitivity multiplier |
sensitivity_x | float | — | X-axis sensitivity override |
sensitivity_y | float | — | Y-axis sensitivity override |
deadzone | integer | — | Raw gyro deadzone threshold |
smoothing | float | — | Smoothing factor (0–1) |
curve | float | — | Acceleration curve exponent |
max_val | float | — | Maximum output value cap |
invert_x | bool | — | Invert X axis |
invert_y | bool | — | Invert Y axis |
[stick.left] / [stick.right]
Per-stick mode configuration.
[stick.left]
mode = "gamepad"
deadzone = 128
sensitivity = 1.0
[stick.right]
mode = "mouse"
sensitivity = 2.5
deadzone = 100
suppress_gamepad = true
| Field | Type | Default | Description |
|---|---|---|---|
mode | string | "gamepad" | "gamepad", "mouse", or "scroll" |
deadzone | integer | — | Stick deadzone threshold |
sensitivity | float | — | Sensitivity multiplier |
suppress_gamepad | bool | — | Suppress gamepad axis output when in mouse/scroll mode |
[dpad]
D-pad mode configuration.
[dpad]
mode = "gamepad"
| Field | Type | Default | Description |
|---|---|---|---|
mode | string | "gamepad" | "gamepad" or "arrows" (emits arrow keys) |
suppress_gamepad | bool | — | Suppress gamepad d-pad output when in arrows mode |
[[layer]]
Each layer defines an activation condition and overrides for remap, gyro, sticks, and d-pad. Layers are evaluated in declaration order.
[[layer]]
name = "fps"
trigger = "LM"
activation = "hold"
tap = "mouse_side"
hold_timeout = 200
| Field | Type | Required | Description |
|---|---|---|---|
name | string | yes | Unique layer identifier |
trigger | string | yes | Button name that activates this layer |
activation | string | no | "hold" (default) or "toggle" |
tap | string | no | Button/key emitted on short press (when using hold activation) |
hold_timeout | integer | no | Hold detection threshold in ms (1–5000) |
[layer.remap]
Per-layer button remapping. Same syntax as top-level [remap].
[layer.remap]
RT = "mouse_left"
A = "KEY_R"
[layer.gyro]
Per-layer gyro override. Same fields as [gyro].
[layer.gyro]
mode = "mouse"
sensitivity = 8.0
deadzone = 40
smoothing = 0.4
invert_y = true
[layer.stick_left] / [layer.stick_right]
Per-layer stick overrides. Same fields as [stick.left]/[stick.right].
[layer.stick_right]
mode = "mouse"
sensitivity = 2.5
deadzone = 100
suppress_gamepad = true
[layer.dpad]
Per-layer d-pad override. Same fields as [dpad].
[layer.dpad]
mode = "arrows"
suppress_gamepad = true
[layer.adaptive_trigger]
Per-layer adaptive trigger override. Same fields as top-level [adaptive_trigger].
[adaptive_trigger]
DualSense adaptive trigger configuration.
[adaptive_trigger]
mode = "feedback"
[adaptive_trigger.left]
position = 70
strength = 200
[adaptive_trigger.right]
position = 40
strength = 180
| Field | Type | Default | Description |
|---|---|---|---|
mode | string | "off" | "off", "feedback", "weapon", or "vibration" |
command_prefix | string | "adaptive_trigger_" | Command template prefix in device config |
[adaptive_trigger.left] / [adaptive_trigger.right]
| Field | Type | Description |
|---|---|---|
position | integer | Trigger position threshold |
strength | integer | Resistance strength |
start | integer | Start position (weapon mode) |
end | integer | End position (weapon mode) |
amplitude | integer | Vibration amplitude |
frequency | integer | Vibration frequency |
[[macro]]
Named sequences of input steps bound via macro:<name> in remap values.
[[macro]]
name = "dodge_roll"
steps = [
{ tap = "B" },
{ delay = 50 },
{ tap = "LEFT" },
]
[[macro]]
name = "shift_hold"
steps = [
{ down = "KEY_LEFTSHIFT" },
"pause_for_release",
{ up = "KEY_LEFTSHIFT" },
]
Step types:
| Step | Description |
|---|---|
{ tap = "KEY" } | Press and release a key |
{ down = "KEY" } | Press and hold a key |
{ up = "KEY" } | Release a key |
{ delay = N } | Wait N milliseconds |
"pause_for_release" | Wait until the trigger button is released |
Bind a macro in remap: M1 = "macro:dodge_roll"
Device Reference
Device reference pages are generated from TOML configs via padctl --doc-gen.
Run padctl --doc-gen devices/**/*.toml to regenerate all pages.
8BitDo Ultimate Controller
VID:PID 0x2dc8:0x6003
Vendor 8bitdo
Interfaces
| ID | Class | EP IN | EP OUT |
|---|---|---|---|
| 0 | hid | — | — |
Report: usb (64 bytes, interface 0)
Match: byte[0] = 0x01
Fields
| Name | Offset | Type | Transform |
|---|---|---|---|
lt | 9 | u8 | — |
left_x | 1 | i16le | — |
rt | 10 | u8 | — |
right_x | 5 | i16le | — |
left_y | 3 | i16le | negate |
right_y | 7 | i16le | negate |
Button Map
Source: offset 11, size 2 byte(s)
| Button | Bit Index |
|---|---|
LS | 9 |
RS | 10 |
X | 2 |
LB | 4 |
RB | 5 |
A | 0 |
Select | 6 |
Home | 8 |
Start | 7 |
Y | 3 |
B | 1 |
Output Capabilities
uinput device name: 8BitDo Ultimate Controller | VID 0x2dc8 | PID 0x6003
Axes
| Field | Code | Min | Max | Fuzz | Flat |
|---|---|---|---|---|---|
lt | ABS_Z | 0 | 255 | 0 | 0 |
left_x | ABS_X | -32768 | 32767 | 16 | 128 |
rt | ABS_RZ | 0 | 255 | 0 | 0 |
right_x | ABS_RX | -32768 | 32767 | 16 | 128 |
left_y | ABS_Y | -32768 | 32767 | 16 | 128 |
right_y | ABS_RY | -32768 | 32767 | 16 | 128 |
Buttons
| Button | Event Code |
|---|---|
LS | BTN_THUMBL |
RS | BTN_THUMBR |
X | BTN_WEST |
LB | BTN_TL |
RB | BTN_TR |
A | BTN_SOUTH |
Select | BTN_SELECT |
Home | BTN_MODE |
Start | BTN_START |
Y | BTN_NORTH |
B | BTN_EAST |
Flydigi Vader 4 Pro
VID:PID 0x37d7:0x3001
Vendor flydigi
Interfaces
| ID | Class | EP IN | EP OUT |
|---|---|---|---|
| 0 | hid | — | — |
Report: extended (32 bytes, interface 0)
Match: byte[0] = 0x04
Fields
| Name | Offset | Type | Transform |
|---|---|---|---|
right_y | 22 | u8 | scale(-32768, 32767), negate |
accel_y | 13 | i16le | — |
gyro_z | 29 | i16le | — |
gyro_x | 26 | i16le | — |
accel_z | 15 | i16le | — |
accel_x | 11 | i16le | — |
left_x | 17 | u8 | scale(-32768, 32767) |
rt | 24 | u8 | — |
right_x | 21 | u8 | scale(-32768, 32767) |
lt | 23 | u8 | — |
left_y | 19 | u8 | scale(-32768, 32767), negate |
Button Map
Source: offset 7, size 4 byte(s)
| Button | Bit Index |
|---|---|
M2 | 3 |
M1 | 2 |
LT | 27 |
RT | 26 |
B | 18 |
LS | 25 |
RS | 24 |
X | 31 |
LB | 29 |
Home | 8 |
Select | 9 |
A | 19 |
RB | 28 |
M4 | 1 |
M3 | 0 |
Y | 16 |
Start | 30 |
Output Capabilities
uinput device name: Flydigi Vader 4 Pro | VID 0x37d7 | PID 0x3001
Axes
| Field | Code | Min | Max | Fuzz | Flat |
|---|---|---|---|---|---|
lt | ABS_Z | 0 | 255 | 0 | 0 |
left_x | ABS_X | -32768 | 32767 | 16 | 128 |
rt | ABS_RZ | 0 | 255 | 0 | 0 |
right_x | ABS_RX | -32768 | 32767 | 16 | 128 |
left_y | ABS_Y | -32768 | 32767 | 16 | 128 |
right_y | ABS_RY | -32768 | 32767 | 16 | 128 |
Buttons
| Button | Event Code |
|---|---|
M2 | BTN_TRIGGER_HAPPY2 |
LT | BTN_TL2 |
M1 | BTN_TRIGGER_HAPPY1 |
RT | BTN_TR2 |
B | BTN_EAST |
LS | BTN_THUMBL |
RS | BTN_THUMBR |
X | BTN_WEST |
LB | BTN_TL |
RB | BTN_TR |
A | BTN_SOUTH |
Select | BTN_SELECT |
Home | BTN_MODE |
Start | BTN_START |
Y | BTN_NORTH |
M3 | BTN_TRIGGER_HAPPY3 |
M4 | BTN_TRIGGER_HAPPY4 |
Flydigi Vader 5 Pro
VID:PID 0x37d7:0x2401
Vendor flydigi
Interfaces
| ID | Class | EP IN | EP OUT |
|---|---|---|---|
| 1 | hid | — | — |
Report: extended (32 bytes, interface 1)
Match: byte[0] = 0x5a, 0xa5, 0xef
Fields
| Name | Offset | Type | Transform |
|---|---|---|---|
left_y | 5 | i16le | negate |
right_y | 9 | i16le | negate |
accel_x | 23 | i16le | — |
gyro_z | 21 | i16le | — |
gyro_x | 17 | i16le | — |
gyro_y | 19 | i16le | — |
accel_z | 27 | i16le | — |
accel_y | 25 | i16le | — |
left_x | 3 | i16le | — |
rt | 16 | u8 | — |
lt | 15 | u8 | — |
right_x | 7 | i16le | — |
Button Map
Source: offset 11, size 4 byte(s)
| Button | Bit Index |
|---|---|
M2 | 19 |
M4 | 21 |
M1 | 18 |
DPadUp | 0 |
DPadRight | 1 |
RM | 23 |
LM | 22 |
B | 5 |
DPadDown | 2 |
DPadLeft | 3 |
X | 7 |
LS | 14 |
RS | 15 |
LB | 10 |
A | 4 |
Select | 6 |
RB | 11 |
C | 16 |
Z | 17 |
Home | 27 |
Start | 9 |
O | 24 |
Y | 8 |
M3 | 20 |
Commands
| Name | Interface | Template |
|---|---|---|
rumble | 1 | 5aa5 1206 {strong:u8} {weak:u8} 0000 0000... |
Output Capabilities
uinput device name: Xbox Elite Series 2 | VID 0x045e | PID 0x0b00
Axes
| Field | Code | Min | Max | Fuzz | Flat |
|---|---|---|---|---|---|
lt | ABS_Z | 0 | 255 | 0 | 0 |
left_x | ABS_X | -32768 | 32767 | 16 | 128 |
rt | ABS_RZ | 0 | 255 | 0 | 0 |
right_x | ABS_RX | -32768 | 32767 | 16 | 128 |
left_y | ABS_Y | -32768 | 32767 | 16 | 128 |
right_y | ABS_RY | -32768 | 32767 | 16 | 128 |
Buttons
| Button | Event Code |
|---|---|
M2 | BTN_TRIGGER_HAPPY2 |
M4 | BTN_TRIGGER_HAPPY4 |
M1 | BTN_TRIGGER_HAPPY1 |
RM | BTN_TRIGGER_HAPPY8 |
LM | BTN_TRIGGER_HAPPY7 |
B | BTN_EAST |
LS | BTN_THUMBL |
RS | BTN_THUMBR |
X | BTN_NORTH |
Z | BTN_TRIGGER_HAPPY6 |
LB | BTN_TL |
RB | BTN_TR |
A | BTN_SOUTH |
Select | BTN_SELECT |
Home | BTN_MODE |
C | BTN_TRIGGER_HAPPY5 |
Start | BTN_START |
O | BTN_TRIGGER_HAPPY9 |
Y | BTN_WEST |
M3 | BTN_TRIGGER_HAPPY3 |
Force feedback: type=rumble, max_effects=16
HORI Horipad Steam
VID:PID 0x0f0d:0x00c5
Vendor hori
Interfaces
| ID | Class | EP IN | EP OUT |
|---|---|---|---|
| 0 | hid | — | — |
Report: bt (287 bytes, interface 0)
Match: byte[0] = 0x07
Fields
| Name | Offset | Type | Transform |
|---|---|---|---|
left_y | 2 | u8 | scale(-32768, 32767), negate |
right_y | 4 | u8 | scale(-32768, 32767), negate |
accel_x | 22 | i16le | — |
gyro_z | 16 | i16le | — |
gyro_x | 12 | i16le | — |
gyro_y | 14 | i16le | — |
accel_z | 18 | i16le | — |
accel_y | 20 | i16le | — |
left_x | 1 | u8 | scale(-32768, 32767) |
rt | 8 | u8 | — |
lt | 9 | u8 | — |
right_x | 3 | u8 | scale(-32768, 32767) |
Button Map
Source: offset 5, size 3 byte(s)
| Button | Bit Index |
|---|---|
M2 | 20 |
LT | 11 |
M1 | 14 |
RT | 10 |
B | 2 |
LS | 22 |
RS | 21 |
X | 0 |
LB | 13 |
A | 3 |
Select | 9 |
RB | 12 |
Home | 23 |
Start | 8 |
Y | 15 |
M4 | 16 |
M3 | 17 |
Output Capabilities
uinput device name: HORI Horipad Steam | VID 0x0f0d | PID 0x00c5
Axes
| Field | Code | Min | Max | Fuzz | Flat |
|---|---|---|---|---|---|
lt | ABS_Z | 0 | 255 | 0 | 0 |
left_x | ABS_X | -32768 | 32767 | 16 | 128 |
rt | ABS_RZ | 0 | 255 | 0 | 0 |
right_x | ABS_RX | -32768 | 32767 | 16 | 128 |
left_y | ABS_Y | -32768 | 32767 | 16 | 128 |
right_y | ABS_RY | -32768 | 32767 | 16 | 128 |
Buttons
| Button | Event Code |
|---|---|
M2 | BTN_TRIGGER_HAPPY2 |
LT | BTN_TL2 |
M1 | BTN_TRIGGER_HAPPY1 |
RT | BTN_TR2 |
B | BTN_EAST |
LS | BTN_THUMBL |
RS | BTN_THUMBR |
X | BTN_WEST |
LB | BTN_TL |
RB | BTN_TR |
A | BTN_SOUTH |
Select | BTN_SELECT |
Home | BTN_MODE |
Start | BTN_START |
Y | BTN_NORTH |
M3 | BTN_TRIGGER_HAPPY3 |
M4 | BTN_TRIGGER_HAPPY4 |
Lenovo Legion Go
VID:PID 0x17ef:0x6182
Vendor lenovo
Interfaces
| ID | Class | EP IN | EP OUT |
|---|---|---|---|
| 0 | hid | — | — |
Report: xinput (60 bytes, interface 0)
Match: byte[0] = 0x04
Fields
| Name | Offset | Type | Transform |
|---|---|---|---|
lt | 22 | u8 | — |
left_x | 14 | u8 | scale(-32768, 32767) |
rt | 23 | u8 | — |
right_x | 16 | u8 | scale(-32768, 32767) |
left_y | 15 | u8 | scale(-32768, 32767), negate |
right_y | 17 | u8 | scale(-32768, 32767), negate |
Button Map
Source: offset 18, size 3 byte(s)
| Button | Bit Index |
|---|---|
M2 | 20 |
M1 | 1 |
LT | 14 |
DPadUp | 4 |
DPadRight | 7 |
RT | 15 |
B | 9 |
LS | 2 |
RS | 3 |
DPadDown | 5 |
DPadLeft | 6 |
X | 10 |
LB | 12 |
Home | 0 |
A | 8 |
RB | 13 |
Select | 22 |
Start | 23 |
Y | 11 |
M3 | 21 |
Output Capabilities
uinput device name: Lenovo Legion Go | VID 0x17ef | PID 0x6182
Axes
| Field | Code | Min | Max | Fuzz | Flat |
|---|---|---|---|---|---|
lt | ABS_Z | 0 | 255 | 0 | 0 |
left_x | ABS_X | -32768 | 32767 | 16 | 128 |
rt | ABS_RZ | 0 | 255 | 0 | 0 |
right_x | ABS_RX | -32768 | 32767 | 16 | 128 |
left_y | ABS_Y | -32768 | 32767 | 16 | 128 |
right_y | ABS_RY | -32768 | 32767 | 16 | 128 |
Buttons
| Button | Event Code |
|---|---|
M2 | BTN_TRIGGER_HAPPY2 |
LT | BTN_TL2 |
M1 | BTN_TRIGGER_HAPPY1 |
RT | BTN_TR2 |
B | BTN_EAST |
LS | BTN_THUMBL |
RS | BTN_THUMBR |
X | BTN_WEST |
LB | BTN_TL |
RB | BTN_TR |
A | BTN_SOUTH |
Select | BTN_SELECT |
Home | BTN_MODE |
Start | BTN_START |
Y | BTN_NORTH |
M3 | BTN_TRIGGER_HAPPY3 |
Lenovo Legion Go S
VID:PID 0x1a86:0xe310
Vendor lenovo
Interfaces
| ID | Class | EP IN | EP OUT |
|---|---|---|---|
| 6 | hid | — | — |
Report: gamepad (32 bytes, interface 6)
Match: byte[0] = 0x06
Fields
| Name | Offset | Type | Transform |
|---|---|---|---|
lt | 12 | u8 | — |
left_x | 4 | i8 | scale(-32768, 32767) |
rt | 13 | u8 | — |
right_x | 6 | i8 | scale(-32768, 32767) |
left_y | 5 | i8 | scale(-32768, 32767), negate |
right_y | 7 | i8 | scale(-32768, 32767), negate |
Button Map
Source: offset 0, size 4 byte(s)
| Button | Bit Index |
|---|---|
M2 | 20 |
M1 | 1 |
LT | 9 |
DPadUp | 4 |
DPadRight | 7 |
RT | 8 |
B | 14 |
LS | 3 |
RS | 2 |
DPadDown | 5 |
DPadLeft | 6 |
X | 13 |
LB | 11 |
Home | 0 |
RB | 10 |
A | 15 |
Select | 23 |
Start | 22 |
Y | 12 |
M3 | 21 |
Commands
| Name | Interface | Template |
|---|---|---|
rumble | 6 | 04 00 08 00 {strong:u8} {weak:u8} 00 00 00... |
Output Capabilities
uinput device name: Xbox Elite Series 2 | VID 0x045e | PID 0x0b00
Axes
| Field | Code | Min | Max | Fuzz | Flat |
|---|---|---|---|---|---|
lt | ABS_Z | 0 | 255 | 0 | 0 |
left_x | ABS_X | -32768 | 32767 | 16 | 128 |
rt | ABS_RZ | 0 | 255 | 0 | 0 |
right_x | ABS_RX | -32768 | 32767 | 16 | 128 |
left_y | ABS_Y | -32768 | 32767 | 16 | 128 |
right_y | ABS_RY | -32768 | 32767 | 16 | 128 |
Buttons
| Button | Event Code |
|---|---|
M2 | BTN_TRIGGER_HAPPY2 |
M1 | BTN_TRIGGER_HAPPY1 |
B | BTN_EAST |
LS | BTN_THUMBL |
RS | BTN_THUMBR |
X | BTN_NORTH |
LB | BTN_TL |
RB | BTN_TR |
A | BTN_SOUTH |
Select | BTN_SELECT |
Home | BTN_MODE |
Start | BTN_START |
Y | BTN_WEST |
M3 | BTN_TRIGGER_HAPPY3 |
Force feedback: type=rumble, max_effects=16
Xbox Elite Series 2
VID:PID 0x045e:0x0b00
Vendor microsoft
Interfaces
| ID | Class | EP IN | EP OUT |
|---|---|---|---|
| 0 | hid | — | — |
Report: usb (64 bytes, interface 0)
Match: byte[0] = 0x01
Fields
| Name | Offset | Type | Transform |
|---|---|---|---|
lt | 9 | u16le | scale(0, 255) |
left_x | 1 | i16le | — |
rt | 11 | u16le | scale(0, 255) |
right_x | 5 | i16le | — |
left_y | 3 | i16le | negate |
right_y | 7 | i16le | negate |
Button Map
Source: offset 13, size 3 byte(s)
| Button | Bit Index |
|---|---|
M2 | 1 |
M1 | 0 |
B | 9 |
LS | 16 |
RS | 17 |
X | 10 |
LB | 12 |
A | 8 |
RB | 13 |
Select | 14 |
Home | 18 |
Start | 15 |
M4 | 3 |
M3 | 2 |
Y | 11 |
Commands
| Name | Interface | Template |
|---|---|---|
rumble | 0 | 09 00 00 09 00 0f {strong:u8} {weak:u8} {left_trigger:u8} {r... |
Output Capabilities
uinput device name: Xbox Elite Series 2 | VID 0x045e | PID 0x0b00
Axes
| Field | Code | Min | Max | Fuzz | Flat |
|---|---|---|---|---|---|
lt | ABS_Z | 0 | 255 | 0 | 0 |
left_x | ABS_X | -32768 | 32767 | 16 | 128 |
rt | ABS_RZ | 0 | 255 | 0 | 0 |
right_x | ABS_RX | -32768 | 32767 | 16 | 128 |
left_y | ABS_Y | -32768 | 32767 | 16 | 128 |
right_y | ABS_RY | -32768 | 32767 | 16 | 128 |
Buttons
| Button | Event Code |
|---|---|
M2 | BTN_TRIGGER_HAPPY2 |
M1 | BTN_TRIGGER_HAPPY1 |
B | BTN_EAST |
LS | BTN_THUMBL |
RS | BTN_THUMBR |
X | BTN_WEST |
LB | BTN_TL |
RB | BTN_TR |
A | BTN_SOUTH |
Select | BTN_SELECT |
Home | BTN_MODE |
Start | BTN_START |
Y | BTN_NORTH |
M3 | BTN_TRIGGER_HAPPY3 |
M4 | BTN_TRIGGER_HAPPY4 |
Force feedback: type=rumble, max_effects=4
Nintendo Switch Pro Controller
VID:PID 0x057e:0x2009
Vendor nintendo
Interfaces
| ID | Class | EP IN | EP OUT |
|---|---|---|---|
| 0 | hid | — | — |
Report: bt_standard (49 bytes, interface 0)
Match: byte[0] = 0x30
Fields
| Name | Offset | Type | Transform |
|---|---|---|---|
left_y_raw | 7 | u8 | — |
right_x_raw | 9 | u8 | — |
right_y_raw | 10 | u8 | — |
left_x_raw | 6 | u8 | — |
Button Map
Source: offset 3, size 3 byte(s)
| Button | Bit Index |
|---|---|
Capture | 13 |
LT | 23 |
DPadUp | 17 |
RT | 7 |
DPadRight | 18 |
B | 2 |
LS | 11 |
RS | 10 |
X | 1 |
DPadDown | 16 |
DPadLeft | 19 |
LB | 22 |
RB | 6 |
A | 3 |
Select | 8 |
Home | 12 |
Start | 9 |
Y | 0 |
Output Capabilities
uinput device name: Nintendo Switch Pro Controller | VID 0x057e | PID 0x2009
Axes
| Field | Code | Min | Max | Fuzz | Flat |
|---|---|---|---|---|---|
left_x | ABS_X | -32768 | 32767 | 16 | 128 |
right_x | ABS_RX | -32768 | 32767 | 16 | 128 |
left_y | ABS_Y | -32768 | 32767 | 16 | 128 |
right_y | ABS_RY | -32768 | 32767 | 16 | 128 |
Buttons
| Button | Event Code |
|---|---|
Capture | BTN_MISC |
LT | BTN_TL2 |
RT | BTN_TR2 |
B | BTN_SOUTH |
LS | BTN_THUMBL |
RS | BTN_THUMBR |
X | BTN_NORTH |
LB | BTN_TL |
RB | BTN_TR |
A | BTN_EAST |
Select | BTN_SELECT |
Home | BTN_MODE |
Start | BTN_START |
Y | BTN_WEST |
Sony DualSense
VID:PID 0x054c:0x0ce6
Vendor sony
Interfaces
| ID | Class | EP IN | EP OUT |
|---|---|---|---|
| 3 | hid | — | — |
Report: usb (64 bytes, interface 3)
Match: byte[0] = 0x01
Fields
| Name | Offset | Type | Transform |
|---|---|---|---|
touch1_contact | 37 | u8 | — |
accel_x | 22 | i16le | — |
sensor_timestamp | 28 | u32le | — |
left_x | 1 | u8 | scale(-32768, 32767) |
touch0_contact | 33 | u8 | — |
lt | 5 | u8 | — |
accel_y | 24 | i16le | — |
accel_z | 26 | i16le | — |
gyro_z | 20 | i16le | — |
gyro_y | 18 | i16le | — |
battery_level | bits[53,0,4] | unsigned | — |
right_x | 3 | u8 | scale(-32768, 32767) |
gyro_x | 16 | i16le | — |
rt | 6 | u8 | — |
left_y | 2 | u8 | scale(-32768, 32767), negate |
right_y | 4 | u8 | scale(-32768, 32767), negate |
Button Map
Source: offset 8, size 3 byte(s)
| Button | Bit Index |
|---|---|
LT | 10 |
Mic | 18 |
RT | 11 |
B | 6 |
LS | 14 |
RS | 15 |
X | 4 |
LB | 8 |
RB | 9 |
A | 5 |
Select | 12 |
Home | 16 |
Start | 13 |
Y | 7 |
TouchPad | 17 |
Report: bt (78 bytes, interface 3)
Match: byte[0] = 0x31
Fields
| Name | Offset | Type | Transform |
|---|---|---|---|
touch1_contact | 38 | u8 | — |
accel_x | 23 | i16le | — |
sensor_timestamp | 29 | u32le | — |
left_x | 2 | u8 | scale(-32768, 32767) |
touch0_contact | 34 | u8 | — |
lt | 6 | u8 | — |
accel_y | 25 | i16le | — |
accel_z | 27 | i16le | — |
gyro_z | 21 | i16le | — |
gyro_y | 19 | i16le | — |
battery_level | bits[54,0,4] | unsigned | — |
right_x | 4 | u8 | scale(-32768, 32767) |
gyro_x | 17 | i16le | — |
rt | 7 | u8 | — |
left_y | 3 | u8 | scale(-32768, 32767), negate |
right_y | 5 | u8 | scale(-32768, 32767), negate |
Button Map
Source: offset 9, size 3 byte(s)
| Button | Bit Index |
|---|---|
LT | 10 |
Mic | 18 |
RT | 11 |
B | 6 |
LS | 14 |
RS | 15 |
X | 4 |
LB | 8 |
RB | 9 |
A | 5 |
Select | 12 |
Home | 16 |
Start | 13 |
Y | 7 |
TouchPad | 17 |
Commands
| Name | Interface | Template |
|---|---|---|
led | 3 | 02 00 04 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 ... |
rumble | 3 | 02 01 00 {weak:u8} {strong:u8} 00 00 00 00 00 00 00 00 00 00... |
adaptive_trigger_feedback | 3 | 02 0c 00 00 00 00 00 00 00 00 00 01 {r_position:u8} {r_stren... |
adaptive_trigger_vibration | 3 | 02 0c 00 00 00 00 00 00 00 00 00 06 {r_position:u8} {r_ampli... |
adaptive_trigger_weapon | 3 | 02 0c 00 00 00 00 00 00 00 00 00 02 {r_start:u8} {r_end:u8} ... |
adaptive_trigger_off | 3 | 02 0c 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 ... |
Output Capabilities
uinput device name: Sony DualSense | VID 0x054c | PID 0x0ce6
Axes
| Field | Code | Min | Max | Fuzz | Flat |
|---|---|---|---|---|---|
lt | ABS_Z | 0 | 255 | 0 | 0 |
left_x | ABS_X | -32768 | 32767 | 16 | 128 |
rt | ABS_RZ | 0 | 255 | 0 | 0 |
right_x | ABS_RX | -32768 | 32767 | 16 | 128 |
left_y | ABS_Y | -32768 | 32767 | 16 | 128 |
right_y | ABS_RY | -32768 | 32767 | 16 | 128 |
Buttons
| Button | Event Code |
|---|---|
Mic | BTN_MISC |
B | BTN_EAST |
LS | BTN_THUMBL |
RS | BTN_THUMBR |
X | BTN_WEST |
LB | BTN_TL |
RB | BTN_TR |
A | BTN_SOUTH |
Select | BTN_SELECT |
Home | BTN_MODE |
Start | BTN_START |
Y | BTN_NORTH |
TouchPad | BTN_TOUCH |
Force feedback: type=rumble, max_effects=16
Sony DualShock 4
VID:PID 0x054c:0x05c4
Vendor sony
Interfaces
| ID | Class | EP IN | EP OUT |
|---|---|---|---|
| 0 | hid | — | — |
Report: usb (64 bytes, interface 0)
Match: byte[0] = 0x01
Fields
| Name | Offset | Type | Transform |
|---|---|---|---|
touch1_contact | 37 | u8 | — |
accel_x | 20 | i16le | — |
left_x | 1 | u8 | scale(-32768, 32767) |
touch0_contact | 33 | u8 | — |
lt | 9 | u8 | — |
accel_y | 22 | i16le | — |
accel_z | 24 | i16le | — |
gyro_z | 18 | i16le | — |
gyro_x | 14 | i16le | — |
battery_level | bits[30,0,4] | unsigned | — |
right_x | 3 | u8 | scale(-32768, 32767) |
rt | 10 | u8 | — |
gyro_y | 16 | i16le | — |
left_y | 2 | u8 | scale(-32768, 32767), negate |
right_y | 4 | u8 | scale(-32768, 32767), negate |
Button Map
Source: offset 5, size 3 byte(s)
| Button | Bit Index |
|---|---|
LT | 10 |
RT | 11 |
B | 6 |
LS | 14 |
RS | 15 |
X | 4 |
LB | 8 |
RB | 9 |
A | 5 |
Select | 12 |
Home | 16 |
Start | 13 |
Y | 7 |
TouchPad | 17 |
Report: bt (78 bytes, interface 0)
Match: byte[0] = 0x11
Fields
| Name | Offset | Type | Transform |
|---|---|---|---|
touch1_contact | 39 | u8 | — |
accel_x | 22 | i16le | — |
left_x | 3 | u8 | scale(-32768, 32767) |
touch0_contact | 35 | u8 | — |
lt | 11 | u8 | — |
accel_y | 24 | i16le | — |
accel_z | 26 | i16le | — |
gyro_z | 20 | i16le | — |
gyro_x | 16 | i16le | — |
battery_level | bits[32,0,4] | unsigned | — |
right_x | 5 | u8 | scale(-32768, 32767) |
rt | 12 | u8 | — |
gyro_y | 18 | i16le | — |
left_y | 4 | u8 | scale(-32768, 32767), negate |
right_y | 6 | u8 | scale(-32768, 32767), negate |
Button Map
Source: offset 7, size 3 byte(s)
| Button | Bit Index |
|---|---|
LT | 10 |
RT | 11 |
B | 6 |
LS | 14 |
RS | 15 |
X | 4 |
LB | 8 |
RB | 9 |
A | 5 |
Select | 12 |
Home | 16 |
Start | 13 |
Y | 7 |
TouchPad | 17 |
Commands
| Name | Interface | Template |
|---|---|---|
led | 0 | 05 ff 00 00 00 00 {r:u8} {g:u8} {b:u8} 00 00 00 00 00 00 00 ... |
rumble | 0 | 05 ff 00 00 {weak:u8} {strong:u8} 00 00 00 00 00 00 00 00 00... |
Output Capabilities
uinput device name: Sony DualShock 4 | VID 0x054c | PID 0x05c4
Axes
| Field | Code | Min | Max | Fuzz | Flat |
|---|---|---|---|---|---|
lt | ABS_Z | 0 | 255 | 0 | 0 |
left_x | ABS_X | -32768 | 32767 | 16 | 128 |
rt | ABS_RZ | 0 | 255 | 0 | 0 |
right_x | ABS_RX | -32768 | 32767 | 16 | 128 |
left_y | ABS_Y | -32768 | 32767 | 16 | 128 |
right_y | ABS_RY | -32768 | 32767 | 16 | 128 |
Buttons
| Button | Event Code |
|---|---|
LS | BTN_THUMBL |
RS | BTN_THUMBR |
X | BTN_WEST |
B | BTN_EAST |
LB | BTN_TL |
RB | BTN_TR |
A | BTN_SOUTH |
Select | BTN_SELECT |
Home | BTN_MODE |
Start | BTN_START |
Y | BTN_NORTH |
TouchPad | BTN_TOUCH |
Force feedback: type=rumble, max_effects=16
Sony DualShock 4 v2
VID:PID 0x054c:0x09cc
Vendor sony
Interfaces
| ID | Class | EP IN | EP OUT |
|---|---|---|---|
| 0 | hid | — | — |
Report: usb (64 bytes, interface 0)
Match: byte[0] = 0x01
Fields
| Name | Offset | Type | Transform |
|---|---|---|---|
touch1_contact | 37 | u8 | — |
accel_x | 20 | i16le | — |
left_x | 1 | u8 | scale(-32768, 32767) |
touch0_contact | 33 | u8 | — |
lt | 9 | u8 | — |
accel_y | 22 | i16le | — |
accel_z | 24 | i16le | — |
gyro_z | 18 | i16le | — |
gyro_x | 14 | i16le | — |
battery_level | bits[30,0,4] | unsigned | — |
right_x | 3 | u8 | scale(-32768, 32767) |
rt | 10 | u8 | — |
gyro_y | 16 | i16le | — |
left_y | 2 | u8 | scale(-32768, 32767), negate |
right_y | 4 | u8 | scale(-32768, 32767), negate |
Button Map
Source: offset 5, size 3 byte(s)
| Button | Bit Index |
|---|---|
LT | 10 |
RT | 11 |
B | 6 |
LS | 14 |
RS | 15 |
X | 4 |
LB | 8 |
RB | 9 |
A | 5 |
Select | 12 |
Home | 16 |
Start | 13 |
Y | 7 |
TouchPad | 17 |
Report: bt (78 bytes, interface 0)
Match: byte[0] = 0x11
Fields
| Name | Offset | Type | Transform |
|---|---|---|---|
touch1_contact | 39 | u8 | — |
accel_x | 22 | i16le | — |
left_x | 3 | u8 | scale(-32768, 32767) |
touch0_contact | 35 | u8 | — |
lt | 11 | u8 | — |
accel_y | 24 | i16le | — |
accel_z | 26 | i16le | — |
gyro_z | 20 | i16le | — |
gyro_x | 16 | i16le | — |
battery_level | bits[32,0,4] | unsigned | — |
right_x | 5 | u8 | scale(-32768, 32767) |
rt | 12 | u8 | — |
gyro_y | 18 | i16le | — |
left_y | 4 | u8 | scale(-32768, 32767), negate |
right_y | 6 | u8 | scale(-32768, 32767), negate |
Button Map
Source: offset 7, size 3 byte(s)
| Button | Bit Index |
|---|---|
LT | 10 |
RT | 11 |
B | 6 |
LS | 14 |
RS | 15 |
X | 4 |
LB | 8 |
RB | 9 |
A | 5 |
Select | 12 |
Home | 16 |
Start | 13 |
Y | 7 |
TouchPad | 17 |
Commands
| Name | Interface | Template |
|---|---|---|
led | 0 | 05 ff 00 00 00 00 {r:u8} {g:u8} {b:u8} 00 00 00 00 00 00 00 ... |
rumble | 0 | 05 ff 00 00 {weak:u8} {strong:u8} 00 00 00 00 00 00 00 00 00... |
Output Capabilities
uinput device name: Sony DualShock 4 v2 | VID 0x054c | PID 0x09cc
Axes
| Field | Code | Min | Max | Fuzz | Flat |
|---|---|---|---|---|---|
lt | ABS_Z | 0 | 255 | 0 | 0 |
left_x | ABS_X | -32768 | 32767 | 16 | 128 |
rt | ABS_RZ | 0 | 255 | 0 | 0 |
right_x | ABS_RX | -32768 | 32767 | 16 | 128 |
left_y | ABS_Y | -32768 | 32767 | 16 | 128 |
right_y | ABS_RY | -32768 | 32767 | 16 | 128 |
Buttons
| Button | Event Code |
|---|---|
LS | BTN_THUMBL |
RS | BTN_THUMBR |
X | BTN_WEST |
B | BTN_EAST |
LB | BTN_TL |
RB | BTN_TR |
A | BTN_SOUTH |
Select | BTN_SELECT |
Home | BTN_MODE |
Start | BTN_START |
Y | BTN_NORTH |
TouchPad | BTN_TOUCH |
Force feedback: type=rumble, max_effects=16
Valve Steam Deck
VID:PID 0x28de:0x1205
Vendor valve
Interfaces
| ID | Class | EP IN | EP OUT |
|---|---|---|---|
| 0 | hid | — | — |
Report: main (64 bytes, interface 0)
Match: byte[1] = 0x09
Fields
| Name | Offset | Type | Transform |
|---|---|---|---|
touch1_y | 22 | i16le | — |
touch0_y | 18 | i16le | — |
accel_x | 24 | i16le | — |
touch0_x | 16 | i16le | — |
left_x | 48 | i16le | — |
lt | 44 | u16le | scale(0, 255) |
touch1_x | 20 | i16le | — |
touch1_active | bits[10,4,1] | unsigned | — |
touch0_active | bits[10,3,1] | unsigned | — |
accel_y | 26 | i16le | — |
accel_z | 28 | i16le | — |
gyro_z | 34 | i16le | — |
gyro_x | 30 | i16le | — |
gyro_y | 32 | i16le | — |
right_x | 52 | i16le | — |
rt | 46 | u16le | scale(0, 255) |
left_y | 50 | i16le | negate |
right_y | 54 | i16le | negate |
Button Map
Source: offset 8, size 4 byte(s)
| Button | Bit Index |
|---|---|
M2 | 13 |
LT | 1 |
M1 | 12 |
DPadUp | 8 |
RT | 0 |
DPadRight | 9 |
B | 5 |
LS | 3 |
RS | 2 |
X | 6 |
DPadLeft | 10 |
DPadDown | 11 |
A | 7 |
Home | 17 |
Select | 18 |
M4 | 15 |
Y | 4 |
M3 | 14 |
Start | 16 |
Commands
| Name | Interface | Template |
|---|---|---|
rumble | 0 | 8f 00 {strong:u8} 00 00 10 00 01 00... |
Output Capabilities
uinput device name: Valve Steam Deck | VID 0x28de | PID 0x1205
Axes
| Field | Code | Min | Max | Fuzz | Flat |
|---|---|---|---|---|---|
lt | ABS_Z | 0 | 255 | 0 | 0 |
left_x | ABS_X | -32768 | 32767 | 16 | 128 |
rt | ABS_RZ | 0 | 255 | 0 | 0 |
right_x | ABS_RX | -32768 | 32767 | 16 | 128 |
left_y | ABS_Y | -32768 | 32767 | 16 | 128 |
right_y | ABS_RY | -32768 | 32767 | 16 | 128 |
Buttons
| Button | Event Code |
|---|---|
M2 | BTN_TRIGGER_HAPPY2 |
LT | BTN_TL2 |
M1 | BTN_TRIGGER_HAPPY1 |
RT | BTN_TR2 |
B | BTN_EAST |
LS | BTN_THUMBL |
RS | BTN_THUMBR |
X | BTN_WEST |
A | BTN_SOUTH |
Select | BTN_SELECT |
Home | BTN_MODE |
Start | BTN_START |
Y | BTN_NORTH |
M3 | BTN_TRIGGER_HAPPY3 |
M4 | BTN_TRIGGER_HAPPY4 |
Force feedback: type=rumble, max_effects=4
Contributing
There are several ways to contribute to padctl:
Guides
- Device Config Guide — Write a device TOML config from capture data to working controller
- HID Reverse Engineering Guide — Identify, capture, and analyze a gamepad's HID protocol with Wireshark and raw hex tools
- Reference Tables — Type mapping, MSB0→LSB0 conversion, ButtonId enum, Linux event codes, transform DSL
- Code Contributions — Fork workflow, code style, test commands, build flags
- Device TOML from InputPlumber — Convert InputPlumber configs to padctl format
Device Config Guide
This guide covers writing a padctl TOML device config from your HID capture analysis. For how to capture and analyze HID reports, see the Reverse Engineering Guide.
Adding a new device requires only one file: devices/<vendor>/<device>.toml. No source code changes needed.
TOML Config Structure
With your analysis complete, translate it to padctl format:
[device]
name = "Acme Gamepad Pro"
vid = 0x1234
pid = 0x5678
[[device.interface]]
id = 0 # from HID_PHYS output
class = "hid"
[[report]]
name = "usb"
interface = 0
size = 64 # report byte count
[report.match]
offset = 0
expect = [0x01] # report ID
[report.fields]
left_x = { offset = 1, type = "u8", transform = "scale(-32768, 32767)" }
left_y = { offset = 2, type = "u8", transform = "scale(-32768, 32767), negate" }
right_x = { offset = 3, type = "u8", transform = "scale(-32768, 32767)" }
right_y = { offset = 4, type = "u8", transform = "scale(-32768, 32767), negate" }
lt = { offset = 5, type = "u8" }
rt = { offset = 6, type = "u8" }
[report.button_group]
source = { offset = 8, size = 3 }
map = { X = 4, A = 5, B = 6, Y = 7, LB = 8, RB = 9, LT = 10, RT = 11, Select = 12, Start = 13, LS = 14, RS = 15, Home = 16 }
[output]
name = "Acme Gamepad Pro"
vid = 0x1234
pid = 0x5678
[output.axes]
left_x = { code = "ABS_X", min = -32768, max = 32767, fuzz = 16, flat = 128 }
left_y = { code = "ABS_Y", min = -32768, max = 32767, fuzz = 16, flat = 128 }
right_x = { code = "ABS_RX", min = -32768, max = 32767, fuzz = 16, flat = 128 }
right_y = { code = "ABS_RY", min = -32768, max = 32767, fuzz = 16, flat = 128 }
lt = { code = "ABS_Z", min = 0, max = 255 }
rt = { code = "ABS_RZ", min = 0, max = 255 }
[output.buttons]
A = "BTN_SOUTH"
B = "BTN_EAST"
X = "BTN_WEST"
Y = "BTN_NORTH"
LB = "BTN_TL"
RB = "BTN_TR"
Select = "BTN_SELECT"
Start = "BTN_START"
Home = "BTN_MODE"
LS = "BTN_THUMBL"
RS = "BTN_THUMBR"
[output.dpad]
type = "hat"
Key Decisions
Y axis negate: HID reports almost always use +Y = down. padctl convention negates Y axes. Always add negate to Y axis transforms.
Axis type and transform:
| Raw type | Transform needed |
|---|---|
u8 centered at 0x80 | scale(-32768, 32767) |
i8 centered at 0 | scale(-32768, 32767) |
i16le centered at 0 | none (already full range) |
u8 trigger (0-255) | none |
Output emulation: For maximum game compatibility, emulate Xbox Elite Series 2 (vid = 0x045e, pid = 0x0b00). See devices/flydigi/vader5.toml for an example. If the device is well-known (like DualSense), use its real VID/PID.
Multiple Report Types
Some gamepads send different report IDs for different data:
- Report
0x01= buttons and axes - Report
0x02= touchpad data - Report
0x11= IMU data
Each needs its own [[report]] block. Use [report.match] to disambiguate:
[[report]]
name = "gamepad"
interface = 0
size = 32
[report.match]
offset = 0
expect = [0x01]
[[report]]
name = "imu"
interface = 0
size = 16
[report.match]
offset = 0
expect = [0x02]
Bluetooth vs USB
The same device often has different report formats over Bluetooth:
- Extra header byte(s): all USB offsets shift by 1 or 2 (see DualSense BT: +1 offset)
- Different report ID: DualSense USB =
0x01, BT extended =0x31 - Checksum appended: DualSense BT has CRC32 at the end, USB does not
- Different report size: DualSense USB = 64 bytes, BT = 78 bytes
You need separate [[report]] blocks for each. See devices/sony/dualsense.toml for a dual USB/BT config.
Test and Iterate
# Parse check — does the config load without errors?
padctl-debug devices/vendor/model.toml
# Live test — run padctl and verify with evtest
padctl --config devices/vendor/model.toml &
evtest /dev/input/eventNN
What to verify:
- Each axis moves full range (min to max) and centers correctly
- No axis is inverted (push right = positive value)
- Every button triggers the correct event
- D-pad works in all 8 directions
- Triggers ramp smoothly from 0 to max
Common issues:
- Axis inverted: add or remove
negatein the transform - Axis stuck at 0: wrong offset — recheck your capture analysis
- Wrong buttons fire: bit index is off — recount from the button_group source offset
- Garbage data: wrong report ID or wrong interface
Validation and Submission
-
Validate locally:
zig build && ./zig-out/bin/padctl --validate devices/<vendor>/<model>.tomlExit 0 = valid. Exit 1 = validation errors. Exit 2 = file not found or parse failure.
-
Test: Run
zig build test— the test framework auto-discovers all.tomlfiles indevices/. -
Submit: Open a pull request. CI runs the same auto-discovery tests automatically.
Directory Layout
devices/
├── 8bitdo/ 8BitDo (Ultimate Controller)
├── flydigi/ Flydigi (Vader 4 Pro, Vader 5 Pro)
├── hori/ HORI (Horipad Steam)
├── lenovo/ Lenovo (Legion Go, Legion Go S)
├── microsoft/ Microsoft (Xbox Elite Series 2)
├── nintendo/ Nintendo (Switch Pro Controller)
├── sony/ Sony (DualSense, DualShock 4, DualShock 4 v2)
└── valve/ Valve (Steam Deck)
Add a new vendor directory if the manufacturer is not listed.
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
Method 1: padctl-capture (recommended)
padctl-capture --device /dev/hidraw3 --duration 30 --output capture.bin
While capturing, do each action one at a time with a pause between:
- Leave controller idle for 3 seconds (this is your baseline)
- Press and release each face button (A, B, X, Y) one at a time
- Press and release each shoulder button (LB, RB, LT, RT)
- Move left stick to full left, full right, full up, full down
- Move right stick the same way
- Press each D-pad direction
- 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:
| Pattern | Type | Center | Range |
|---|---|---|---|
Single byte, idle = 0x80 | u8 | 128 | 0-255 |
Single byte, idle = 0x00 | i8 | 0 | -128 to 127 |
Two bytes, idle = 0x00 0x00 | i16le | 0 | -32768 to 32767 |
Two bytes, idle = 0x00 0x80 | u16le centered | 32768 | 0-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:
| Button | Byte | Bit (in byte) | Bit (in group) |
|---|---|---|---|
| Square/X | 8 | 4 | 4 |
| Cross/A | 8 | 5 | 5 |
| Circle/B | 8 | 6 | 6 |
| Triangle/Y | 8 | 7 | 7 |
| L1/LB | 9 | 0 | 8 |
| R1/RB | 9 | 1 | 9 |
| L3/LS | 9 | 6 | 14 |
| R3/RS | 9 | 7 | 15 |
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.
Reference Tables
Quick reference for writing padctl device configs.
Type Mapping
| Common HID representation | padctl type |
|---|---|
| unsigned byte | "u8" |
| signed byte | "i8" |
| 16-bit unsigned, little-endian | "u16le" |
| 16-bit signed, little-endian | "i16le" |
| 16-bit unsigned, big-endian | "u16be" |
| 16-bit signed, big-endian | "i16be" |
| single bit (boolean) | button_group entry |
| multi-bit enum (e.g. hat switch) | button_group per variant or hat field |
Axis Transform
| Raw type | Transform needed |
|---|---|
u8 centered at 0x80 | scale(-32768, 32767) |
i8 centered at 0 | scale(-32768, 32767) |
i16le centered at 0 | none (already full range) |
u8 trigger (0-255) | none |
Linux Input Event Codes
| padctl button | Linux code | Notes |
|---|---|---|
| A | BTN_SOUTH | Cross on PlayStation |
| B | BTN_EAST | Circle on PlayStation |
| X | BTN_WEST | Square on PlayStation |
| Y | BTN_NORTH | Triangle on PlayStation |
| LB | BTN_TL | L1 |
| RB | BTN_TR | R1 |
| Select | BTN_SELECT | Share/Create/View |
| Start | BTN_START | Options/Menu |
| Home | BTN_MODE | PS/Xbox/Guide |
| LS | BTN_THUMBL | L3 (stick click) |
| RS | BTN_THUMBR | R3 (stick click) |
| M1-M4 | BTN_TRIGGER_HAPPY1-4 | Back paddles / extra buttons |
MSB0 to LSB0 Bit Conversion
Some HID documentation and driver source code number bits in MSB0 order (bit 0 = most-significant bit). padctl button_group indices use LSB0 (bit 0 = least-significant bit).
Single byte:
lsb_bit = 7 - msb_bit (msb_bit in 0..=7)
Multi-byte group (source window of N bytes):
lsb_bit = (msb_bit / 8) * 8 + (7 - (msb_bit % 8))
Example — a 2-byte button field starting at offset 0:
| Button | MSB0 bit | byte | bit-in-byte | LSB0 index |
|---|---|---|---|---|
| DPadRight | 0 | 0 | 7 | 7 |
| DPadLeft | 1 | 0 | 6 | 6 |
| DPadDown | 2 | 0 | 5 | 5 |
| DPadUp | 3 | 0 | 4 | 4 |
| L3 | 4 | 0 | 3 | 3 |
| R3 | 5 | 0 | 2 | 2 |
| Btn6 | 6 | 0 | 1 | 1 |
| Btn7 | 7 | 0 | 0 | 0 |
| A | 8 | 1 | 7 | 15 |
| B | 9 | 1 | 6 | 14 |
| X | 10 | 1 | 5 | 13 |
| Y | 11 | 1 | 4 | 12 |
| LB | 12 | 1 | 3 | 11 |
| RB | 13 | 1 | 2 | 10 |
| LT | 14 | 1 | 1 | 9 |
| RT | 15 | 1 | 0 | 8 |
Common Pitfalls
- MSB0 vs LSB0 bit order — many HID reference drivers use MSB0 bit numbering. Copying bit indices directly without converting will cause buttons to trigger on wrong inputs. Always apply the conversion formula.
- Padding bytes — if the report is larger than the sum of declared fields, the extra bytes are padding. Do not declare fields for those offsets.
- Multiple report IDs on one interface — each report ID needs its own
[[report]]block with a[report.match]section. Withoutmatch, padctl tries to parse every incoming buffer with every report definition. - Split / non-contiguous fields — some devices store a single logical value across non-adjacent bytes (e.g., gyro_y with low byte at offset 18 and high byte at offset 20). padctl does not support split fields; these require a WASM plugin or a firmware mode that provides a contiguous layout.
- Endianness of multi-byte scalars — when referencing driver source code, check whether multi-byte fields are little-endian or big-endian. The default in many packed-struct frameworks is big-endian. Use
"i16be"/"u16be"for big-endian fields. - Analog stick center value — devices use either
u8(center = 0x80) ori8(center = 0). Both benefit fromtransform = "scale(-32768, 32767)"to fill the full axis range expected by uinput.
Code Contributions
Workflow
- Fork the repository and create a feature branch
- Make your changes
- Run all checks before submitting
- Open a pull request
Code Style
All Zig code must pass zig fmt:
zig build check-fmt
Testing
# Run all tests (Layer 0+1, no privileges required)
zig build test
# Run all checks (test + tsan + safe + fmt)
zig build check-all
Build Flags
| Flag | Default | Description |
|---|---|---|
-Dlibusb=false | true | Disable libusb-1.0 linkage (hidraw-only path) |
-Dwasm=false | true | Disable WASM plugin runtime |
-Dtest-coverage=true | false | Run tests with kcov coverage |
CI Auto-Validation
zig build test automatically validates every device TOML in the repository:
- TOML parse + semantic validation: syntax correctness, field value legality
- FieldTag coverage: all field names map to known FieldTag values
- ButtonId coverage: all button_group keys are valid ButtonId enum values
- VID/PID validity: all device configs contain valid VID/PID