Experimental out-of-tree Linux HID driver for the SInput gamepad protocol.
This repository is deliberately a starter rather than a finished upstream-quality driver. The first goal is to establish a clean DKMS/module test loop against SInput devices, then add the protocol features that Linux's generic HID/gamepad path does not expose cleanly.
No dedicated SInput product is owned. Development is based on the published
SInput specification and Hand Held
Legend's SInput-HID reference
repository, cross-checked against SDL's SInput HIDAPI implementation (the same
code shipped in the SDL 3.4.x release series,
first publicly available around 3.4.6). The feature-response layout is
still best-effort, but most features have been verified against a real
SInput device (see Planned stages for what hasn't): an
ESP32 running
lemmingDev/ESP32-BLE-Gamepad
in SInput mode, paired over BLE with Raspberry Pi hardware-in-the-loop (HIL)
rigs. The HIL setup is tracked in
LeeNX/ESP32-BLE-Gamepad-HIL;
see docs/rpi-hil.md for the Pi side and
docs/research.md for each run and what it found.
See CHANGELOG.md for what each release added.
As of 2026-09-16:
- SInput is a HID format developed by Hand Held Legend and documented in the SInput HID development specification.
- The documented generic testing VID/PID is
0x2E8A:0x10C6; the specification recommends registering a device-specific PID for real products. - SDL has a native SInput HIDAPI implementation, shipped in the 3.4.x release series.
- SDL's implementation currently understands a 64-byte input report, 48-byte command/output report, report IDs 1/2/3, capability discovery, dynamic button mappings, battery/power state, IMU data, haptics and player/RGB LED commands.
- The Linux kernel already has a mature HID framework and gamepad-specific drivers
such as
hid-playstationandhid-nintendo. Those drivers are useful design references for a future in-tree driver. - DKMS is useful here as the experimental delivery mechanism while the protocol and Linux-facing API are being worked out.
See docs/research.md for sources and design notes.
The module:
- binds to the SInput generic test VID/PID (
2E8A:10C6) over USB or Bluetooth; - uses the Linux HID framework;
- cross-checks the parsed HID report descriptor's own field sizes against
every report-size constant this driver assumes, logging a warning
(never fatal) on any mismatch -- HIL-verified: all three report sizes
(state, command response, output command) matched exactly on real
hardware (see
docs/research.md); - creates an explicit evdev input device instead of relying on
hid-generic; - sends the SInput
FEATUREScommand on probe and decodes the response (protocol version, polling rate, sticks/triggers/accel/gyro/rumble/LED support) with a short timeout, falling back to "assume everything is present" if the device never answers; - decodes the SInput state report;
- exposes only the buttons, D-pad, and sticks/triggers the feature response's usage mask (or the fallback) says exist -- every SInput button bit has an evdev code (see Button mapping);
- exposes a separate IMU input device, with only the accel/gyro axes the device actually advertises, scaled to physical units and carrying the same vendor/product IDs as the gamepad so userspace can pair the two;
- exposes battery/charge state as a standard Linux
power_supplybattery device (always present in every state report, no capability bit); - exposes player LEDs (4 on/off
led_classdevs) and an RGB indicator (led_classdev_multicolor) as output commands, gated on the feature response the same way the input side is; - exposes rumble as a standard
FF_RUMBLEforce-feedback device (input_ff_create_memless()), gated oncaps.rumble-- HIL-verified against real hardware: captured BLE traffic showed the exact expected output-report bytes (seedocs/research.md); - exposes one or more touchpad input devices (one per pad, or one shared
multi-touch pad, depending on what the feature response says), rebuilt
live if a later response changes that shape -- HIL-verified against
real hardware (see
docs/research.md).
The source lives in src/, one file per subsystem (sinput_core.c for
probe/dispatch and the FEATURES/output-command plumbing, sinput_input.c,
sinput_battery.c, sinput_led.c, sinput_ff.c, sinput_touchpad.c),
Kbuild-linked into a single sinput.ko -- see src/Makefile.
The feature-response layout is reverse-derived from SDL's SInput HIDAPI
driver (see docs/research.md), not from a stable
spec, so treat the byte offsets as best-effort.
Button names and bit numbers follow SDL's SDL_hidapi_sinput.c. The table
is sinput_buttons[] in src/sinput_input.c.
| SInput button | evdev code |
|---|---|
| South / East / West / North | BTN_SOUTH / BTN_EAST / BTN_WEST / BTN_NORTH |
| D-pad | BTN_DPAD_UP / DOWN / LEFT / RIGHT |
| Left / right stick click | BTN_THUMBL / BTN_THUMBR |
| Left / right bumper | BTN_TL / BTN_TR |
| Left / right trigger (digital) | BTN_TL2 / BTN_TR2 |
| Start / Back / Guide | BTN_START / BTN_SELECT / BTN_MODE |
| Capture | BTN_MISC |
| Left / right paddle 1, left / right paddle 2 | BTN_GRIPL / BTN_GRIPR / BTN_GRIPL2 / BTN_GRIPR2 (BTN_TRIGGER_HAPPY9-12 on kernels whose headers predate them) |
| Power | BTN_TRIGGER_HAPPY1 |
| Misc 4-10 | BTN_TRIGGER_HAPPY2-8 |
| Touchpad click | BTN_LEFT, on the touchpad's own input device |
Power is deliberately not KEY_POWER: systemd-logind acts on power keys
from any input device, and a gamepad button must not be able to shut the
host down.
The driver's built-in id_table (src/sinput_core.c)
only matches the generic SInput testing VID/PID (2E8A:10C6). If you're
building your own SInput-compatible controller, do not ship it under
that VID/PID or under Raspberry Pi Foundation's 2E8A -- register a real
product ID instead. If you don't want to go through full USB-IF vendor
registration, pid.codes is a shared VID (1209) for
open-source hardware projects; the SInput spec itself recommends a
device-specific PID for real products (see Research snapshot).
There's no need to patch and rebuild the module for this. The Linux HID bus
already supports adding device IDs to a driver at runtime (new_id/bind
under /sys/bus/hid/drivers/), which this repo wraps in a udev rule so it
happens automatically:
-
Persistent (recommended): add a
vendor:productline, in hex, to/etc/sinput/ids.conf(see the comments in that file for the exact format), then unplug/replug the device (or re-pair it, for Bluetooth). The installed udev rule (udev/99-sinput.rules, viascripts/sinput-claim-id.sh) reads that file on every matching HID "add" event and rebinds the device from whatever currently claims it (hid-generic, in the common case) tosinput. No module rebuild, no reboot.- The two
.debpackages install this automatically. - For a plain
make/dkms-install.shinstall, runscripts/install-udev-support.shonce (as root) to install the rule, helper script, and config file.
- The two
-
One-off/manual test, without installing anything:
echo "0003 2e8a 1a01" | sudo tee /sys/bus/hid/drivers/sinput/new_id # find the device's current hid-generic instance name, e.g. from: ls /sys/bus/hid/drivers/hid-generic/ echo -n "0003:2E8A:1A01.000X" | sudo tee /sys/bus/hid/drivers/hid-generic/unbind echo -n "0003:2E8A:1A01.000X" | sudo tee /sys/bus/hid/drivers/sinput/bind
(bus
0003is USB,0005is Bluetooth; find your device's actual vendor/product/instance name under/sys/bus/hid/drivers/hid-generic/.)
The driver has no custom debug ABI (no module parameter, no debugfs file) --
it uses the kernel's own dynamic debug facility (CONFIG_DYNAMIC_DEBUG,
enabled on effectively every distro kernel) for every diagnostic message.
Off by default, near-zero overhead when off (jump-label gated), and doesn't
need a module reload or reboot to turn on or off.
Enable everything the driver logs:
echo 'module sinput +p' | sudo tee /sys/kernel/debug/dynamic_debug/controlOr scope it to one thing, e.g. only the raw report hex dump (the highest- signal single thing to enable when a decode looks wrong -- see exactly what's on the wire without an external BLE/USB capture):
echo 'func sinput_raw_event +p' | sudo tee /sys/kernel/debug/dynamic_debug/controldmesg -w (or journalctl -kf) to watch it live. Turn it back off the same
way, with -p instead of +p. See
Documentation/admin-guide/dynamic-debug-howto.rst
for the full <query> syntax (by function, file, line range, or module).
What's covered: probe (device identity, FEATURES request/response retries),
every raw incoming report (hex dump), decoded gamepad/touchpad state per
report, touchpad reconcile decisions (see
src/sinput_touchpad.c's top comment for why that
can happen more than once per device), and every outgoing command (rumble,
player LEDs, RGB LED).
SDL already supports SInput through HIDAPI, so a kernel driver is not required for SDL applications. The interesting reason to build one is to investigate a proper Linux-native representation:
- evdev input
- Linux force feedback
- LED class / multicolor LEDs
power_supply- sensor input
- suspend/resume
- consistent behaviour for applications that do not use SDL/HIDAPI
The project should therefore avoid simply duplicating SDL in kernel space.
On Debian/Ubuntu:
sudo apt install build-essential dkms linux-headers-$(uname -r)Build locally:
make
sudo insmod src/sinput.koInspect:
lsmod | grep sinput
dmesg | tail -n 100
cat /proc/bus/input/devicesFor an attached controller, also inspect:
udevadm info /sys/bus/hid/devices/*/hidraw*/device 2>/dev/null
evtestRun the host-side protocol decode checks (no kernel headers required, works on any machine including macOS/CI):
make checkThis only exercises the report byte-offset/flag math in
src/sinput_protocol.h against synthetic packets; it is not a substitute for
testing against real hardware.
Install through DKMS directly (scripts/dkms-install.sh;
builds against whatever kernel is currently running):
sudo ./scripts/dkms-install.shRemove (scripts/dkms-remove.sh):
sudo ./scripts/dkms-remove.sh- Source DKMS
.deb—./scripts/build-source-deb.sh[output-dir]. Ships the source under/usr/src/sinput-<version>/; itspostinsthands off to dkms's own/usr/lib/dkms/common.postinstto build and install for whatever kernel(s) are present, and keeps working across kernel upgrades. Needsbuild-essentialand matching kernel headers on the target, not the build machine. - Precompiled binary
.deb—./scripts/build-binary-deb.sh<KDIR> [output-dir]. Ships a prebuiltsinput.kofor one exact kernel release; no compiler needed on the target at all, but it will not load on any other kernel build (vermagic mismatch). Seedocs/rpi-hil.mdfor how to do this for a Raspberry Pi 3 target, including two real gotchas it took actually testing in containers to find (Raspberry Pi's kernel-headers packaging differs completely between OS releases, and a real apt-trust bootstrap failure on the current release).
Both .deb flavors also install the runtime VID/PID support (udev rule,
helper script, /etc/sinput/ids.conf) described in
Adding your own VID/PID — nothing extra to run.
Both scripts are plain POSIX sh, no dh/dpkg-buildpackage involved —
dkms mkdeb/mkbmdeb, despite being documented in Debian's own
/usr/share/doc/dkms/HOWTO.Debian, no longer exist in the dkms shipped with
current Debian (verified against 3.0.10); the doc is stale.
GitHub is the canonical public repo. GitLab is a public mirror. Gitea is a self-hosted, internal mirror (also used for local CI and for reaching self-hosted ARM64 hardware, including the Raspberry Pi HIL runner) rather than a public home for the project.
Three separate CI configs, one per platform this repo can be pushed to —
.gitea/workflows/ci.yml, .github/workflows/ci.yml, .gitlab-ci.yml —
rather than one shared file. They run the same six checks below via the
same underlying scripts, but the platform-specific plumbing genuinely
differs (see each file's header comment for specifics): action versions
that work on one platform and not another, how JavaScript actions get a
node binary inside a job's container (or, on GitLab, don't need one at
all — checkout and artifacts are native runner features there, not
actions), and runner/architecture labels. All three were dry-run locally
against real container images before being committed (act for the
GitHub file, manual container replay for GitLab since gitlab-runner exec has been removed from current versions); only a real push confirms
each platform's actual runners/registries behave the same way.
Every push/PR (and manual trigger) runs:
protocol-check—make check(the decode test above)shellcheck— lintsscripts/*.shcheckpatch— Linux kernel style check againstsrc/.LINUX_VERSION_CODEandCONSTANT_COMPARISONare ignored: those checkpatch rules assume in-tree code that targets a single kernel version, but this driver is out-of-tree and has to compile across a range of kernel versions.kernel-build— compiles the module against real kernel headers on a small matrix of Debian releases (currently bookworm/6.1 and trixie/6.12), plus aW=1extra-warnings pass and asparsepass. This is a compile check only; it cannot catch runtime/protocol bugs without a real SInput device.dkms-source-deb— builds the source.debabove and actuallydpkg -i/dpkg -rs it, i.e. runs the real dkms add/build/install/remove path, not just a compile. This caught two real bugs during development that every other check above missed (dkms.conf'sMAKEline ignoring the kernel dkms was actually targeting, and the module landing in the wrong build-output location) — worth keeping as a regression test rather than trimming down to "just build the module" again.rpi3-binary-deb— builds the binary.debfor a Raspberry Pi 3 (seedocs/rpi-hil.md) and uploads it as a downloadable CI artifact. Needs an arm64 runner (linux-headers-rpi-v8isn't published for amd64): pinned to thearm64label on Gitea (already registered on the RPi4/5 runner) andubuntu-24.04-armon GitHub (free hosted arm64 runner for public repos, unverified as of writing). The GitLab file'stags: [saas-linux-small-arm64]is a guess at GitLab.com's shared arm64 runner tag — check https://docs.gitlab.com/ci/runners/hosted_runners/linux/ for the current name/tier before relying on it, or point it at a self-hosted arm64 runner instead (the same RPi4/5 box could double as one).
To reproduce the kernel-build job locally without Docker/Gitea, install
linux-headers-$(dpkg --print-architecture) in a matching container and run
make KDIR=/lib/modules/$(ls /lib/modules)/build.
- GitHub:
.github/dependabot.ymlwatches.github/workflows/*.ymlfor newactions/*releases and opens PRs weekly. - Gitea:
.gitea/workflows/renovate.ymlruns Renovate itself weekly (self-hosted platforms don't get a hosted Dependabot/Renovate app), configured byrenovate.jsonat the repo root. Needs two one-time manual steps: create a Gitea access token (a dedicated bot account keeps PR attribution clean) with repo read/write scope as aRENOVATE_TOKENsecret, and a read-only github.com personal access token asRENOVATE_GITHUB_COM_TOKEN— confirmed by actually running Renovate locally (RENOVATE_PLATFORM=local) that the second one is genuinely required, not just nice-to-have for rate limits: without it, Renovate's github-actions manager hard-skips everyactions/*dependency (skipReason: github-token-required) no matter which platform it's opening PRs against. Until both secrets exist, the job runs and fails/no-ops at the relevant step rather than silently doing nothing.renovate.jsonalso carries the one rule that has to stay platform-specific: it capsactions/upload-artifactbelow v4 in.gitea/workflows/ci.ymlonly (the Results-API backend issue above), while leaving.github/workflows/ci.ymlfree to track latest.
See RELEASE.md. Short version: scripts/release.sh 0.1.0
bumps dkms.conf, commits, and tags locally; pushing the tag triggers the
release workflows on all three platforms to build both .deb flavors.
GitHub and GitLab attach them to a Release automatically; Gitea uploads them
as a downloadable artifact for now (a container-network DNS issue on that
runner blocks it from reaching its own release API — see RELEASE.md), so
creating the actual Gitea Release from the pushed tag is currently a manual
step. Before tagging, move the [Unreleased] entries in
CHANGELOG.md under the new version's heading.
This is an experimental kernel module. Keep a second console available and test first on a disposable Linux installation or VM. Do not use the module's current feature set as evidence that SInput is ready for upstream Linux.
- DKMS skeleton
- HID binding
- basic SInput state report decoding
- evdev buttons/D-pad/axes
- capability/feature report request + response parsing
- capability-driven axis + IMU registration (sticks, triggers, accel, gyro)
- protocol version / polling-rate logging
- host-side protocol decode test (
make check) - source DKMS
.debpackaging (scripts/build-source-deb.sh), with a real dpkg-i/dpkg-r regression test in CI - precompiled binary
.debpackaging for one exact kernel build (scripts/build-binary-deb.sh), verified for Raspberry Pi 3 (seedocs/rpi-hil.md) - capability-driven button mapping (registration and reporting both gated on the feature response's usage mask, falling back to "assume every mapped button exists" like the other capabilities), covering every SInput button bit including paddles, power and misc (see Button mapping)
- battery /
power_supply(HIL-verified against real hardware; seedocs/research.md) - force feedback / rumble output command (
FF_RUMBLEviainput_ff_create_memless(), HIL-verified against real hardware; seedocs/research.md) - player LEDs (HIL-verified against real hardware; see
docs/research.md) - RGB LED (HIL-verified against real hardware; see
docs/research.md) - touchpads (one input_dev per pad or per finger depending on the
feature response's shape, reconciled live if a late response changes
it; HIL-verified against real hardware; see
docs/research.md) - output-command serialization and locking (
output_lockmutex, added once player/RGB LED became the second and third output commands; seedocs/research.md) - runtime VID/PID registration for DIY builds, no module rebuild (udev
rule +
/etc/sinput/ids.conf; see Adding your own VID/PID) - diagnostics via the kernel's dynamic debug facility, no custom debug ABI (see Diagnostics)
- USB + Bluetooth transport testing (Bluetooth binding added and verified
against a real device over BLE; USB still untested; see
docs/research.md) - suspend/resume
- kernel version compatibility matrix (compile-verified on 6.1 and 6.12; now also
real-hardware-verified on 6.18 via
docs/rpi-hil.md) - HIL tests using ESP32 SInput firmware (real device, BLE, against
rp4b-ble-hil; seedocs/research.md) - evaluate whether an upstream Linux HID driver is justified (where would this posted/reported?)