Add game controller emulation (HID, Xbox 360, Switch Pro, DualSense)#218
Draft
anagnorisis2peripeteia wants to merge 6 commits into
Draft
Add game controller emulation (HID, Xbox 360, Switch Pro, DualSense)#218anagnorisis2peripeteia wants to merge 6 commits into
anagnorisis2peripeteia wants to merge 6 commits into
Conversation
Adds a USB HID gamepad alongside the keyboard and mouse using the same f_hid gadget mechanism. Disabled by default; enable by setting kvmd.hid.gamepad.device and otg.devices.hid.gamepad.start. - apps/otg/hid/gamepad.py: 16-button report descriptor with two analog sticks, two analog triggers and a D-pad hat (9-byte report) - plugins/hid/otg/gamepad.py: GamepadProcess streaming full-state reports - plugins/hid: BaseHid.send_gamepad_event() with a no-op default for non-OTG backends - apps/kvmd/api/hid.py: WS 'gamepad' event and binary opcode 6 - _scheme.py, validators/hid.py and udev rules (hidg3 -> kvmd-hid-gamepad)
Adds web/share/js/kvm/gamepad.js, a Gamepad module that polls the browser Gamepad API (navigator.getGamepads()) and sends full-state 'gamepad' HID events while the target exposes an online gamepad device. Wired into hid.js alongside Keyboard/Mouse (construct, setSocket, state sync, releaseAll).
XInput controllers are USB vendor-specific (class 0xFF/0x5D/0x01), not HID, so they cannot use f_hid. This adds an XInput mode for the optional gamepad, implemented as a FunctionFS function serviced from user space. - plugins/hid/otg/xinput.py: XInputProcess streams the 20-byte 360 report, services the ffs endpoints and binds the UDC. A FunctionFS function has no endpoints until its descriptors are written, so the bind is deferred from kvmd-otg to the servicer. Same send_*/get_state interface as GamepadProcess. - apps/otg: add_xinput() creates and mounts the ffs.xinput function; in XInput mode the gadget presents as 045E:028E (so xpad/consoles bind it) and the UDC bind is deferred. - plugins/hid/otg: kvmd.hid.gamepad.mode (hid|xinput) selects the backend. Validated standalone on dummy_hcd (enumerates as an Xbox 360 pad, xpad binds, live reports via jstest). Full in-daemon test pending. Requires python-functionfs.
Two new FunctionFS-based gamepad backends alongside the existing generic HID and XInput modes: - switchpro: Nintendo Switch Pro Controller (057E:2009). Full hid-nintendo handshake (0x80 USB commands + 0x01 subcommands with SPI flash calibration reads). Works on Switch 1, Switch 2 (backwards compat), and PC. - dualsense: Sony DualSense / PS5 controller (054C:0CE6). Responds to hid-playstation feature report probes (0x09 MAC, 0x05 cal, 0x20 firmware). PC/Steam only (PS5 console auth requires Sony signing IC). Both use the same send_state_event/get_state interface as XInputProcess, selected via gamepad.mode config.
Refactors the single gamepad slot into an array of up to 4 indexed gamepad processes. Each is a separate FunctionFS instance with its own USB endpoints (2 per controller: IN + OUT). Config: gamepad.count (default 2, max 4) + gamepad.ffs_base path. Slots 0-1 start enabled by default; 2-3 start disabled (can be toggled at runtime via kvmd-otgconf). Endpoint budget (9 total on Pi): - kbd(1) + mouse(1) + rel_mouse(1) + MSD(2) + 2 gamepads(4) = 9 - Disable kbd/mouse/MSD via kvmd-otgconf for 4 gamepads (couch co-op) Binary WS opcode 6 extended: 9-byte legacy (index=0) or 10-byte (index prefix, 0-3). JSON "gamepad" event gains "index" field. Backward compatible: existing 9-byte payloads still work as index 0.
Two input-path optimizations: 1. USB endpoint bInterval=1 (1ms/1000Hz polling) for XInput and DualSense modes. The host reads gamepad reports up to 1000 times per second instead of the previous 125Hz (8ms) or 250Hz (4ms). Switch Pro stays at bInterval=8 to match real hardware behavior. 2. Browser gamepad poller switched from requestAnimationFrame (16ms, tied to display refresh) to setInterval(4) (4ms, ~250Hz). This is the minimum reliable timer interval browsers allow. Combined with change-detection, reports are only sent when state actually changes. Also: the poller now reads ALL connected gamepads (up to 4) per tick and sends each with the 10-byte indexed binary format (opcode 6 with index prefix), matching the multi-controller backend from the previous commit. Net effect: worst-case input latency drops from ~24ms (16ms browser poll + 8ms USB poll) to ~5ms (4ms browser poll + 1ms USB poll).
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Game Controller Emulation for PiKVM
Adds four gamepad emulation modes with multi-controller support (up to 4 simultaneous controllers) to the OTG backend.
Controller Modes
hidxinputswitchprodualsenseMulti-Controller Support
Up to 4 gamepad slots, each a separate FunctionFS instance with its own USB endpoints (2 per controller).
Endpoint budget (9 total on Pi):
kvmd-otgconf): 8/9Slots 0-1 start enabled by default; 2-3 disabled. Toggle at runtime:
Browser Gamepad API (
navigator.getGamepads()[0..3]) maps to slots via the existing HID WebSocket. Binary opcode 6 extended: 9 bytes (legacy, index=0) or 10 bytes (index prefix 0-3). Backward compatible.Architecture
gamepad.py): standard f_hid gadgetxinput.py): FunctionFS, vendor class 0xFF/0x5D/0x01, 20-byte reportsswitchpro.py): FunctionFS, HID class, full 0x80 handshake + SPI flash calibrationdualsense.py): FunctionFS, HID class, GET_REPORT feature repliesConsole auth notes
What is NOT tested