#!/usr/bin/env python3 # Part of JoeyLib, Copyright (c) 2026 Scott Duensing, under the MIT License. # See LICENSE.md for license information. # SPDX-License-Identifier: MIT """virtualPad.py - two virtual gamepads for the joystick gates of the ports that read the host's gamepads (SDL3, Win32 under Wine, the web port in Chromium). Makes two Linux uinput devices laid out as Xbox 360 pads (the xpad driver's buttons and axes, Microsoft's USB ids), which SDL's, Wine's, and Chromium's gamepad mappings all know: stick A is the first pad, stick B the second. With --generic they are plain joysticks instead -- an X and a Y axis, a trigger and a thumb button, and pid.codes' test ids -- which no gamepad mapping knows, so SDL offers them only as raw joysticks. Then reads lines on stdin and holds or lets go of the controls named: press CONTROL... CONTROL as joyGate.sh names them: A-left A-right release CONTROL... A-up A-down A-button0 A-button1, and the same for B quit remove the pads (as does end of input) The left stick moves to the end of its travel (left/up -32767, right/down +32767; holding both of an axis's directions is the positive one), button 0 is the south button (A) and button 1 the east (B) -- on a generic stick, the trigger and the thumb button. Prints "ready" once both devices exist, and "ok" after each line it has applied. Needs write access to /dev/uinput (a desktop session's user usually has it through udev's uaccess rule). The pads are the whole machine's while they exist, not only the gate's. virtualPad.py [--generic] [NAME] NAME prefixes the devices' names """ import fcntl import os import struct import sys import time UINPUT = "/dev/uinput" # linux/uinput.h and linux/input.h. UI_DEV_CREATE = 0x5501 UI_DEV_DESTROY = 0x5502 UI_DEV_SETUP = 0x405C5503 # _IOW('U', 3, struct uinput_setup), 92 bytes UI_ABS_SETUP = 0x401C5504 # _IOW('U', 4, struct uinput_abs_setup), 28 bytes UI_SET_EVBIT = 0x40045564 UI_SET_KEYBIT = 0x40045565 UI_SET_ABSBIT = 0x40045567 EV_SYN = 0x00 EV_KEY = 0x01 EV_ABS = 0x03 SYN_REPORT = 0 BUS_USB = 0x03 XBOX360_VENDOR = 0x045E XBOX360_PRODUCT = 0x028E XBOX360_VERSION = 0x0110 # pid.codes' vendor id and its test product, in no gamepad database. GENERIC_VENDOR = 0x1209 GENERIC_PRODUCT = 0x0001 GENERIC_VERSION = 0x0100 NAME_BYTES = 80 BTN_TRIGGER = 0x120 BTN_THUMB = 0x121 BTN_SOUTH = 0x130 BTN_EAST = 0x131 BTN_NORTH = 0x133 BTN_WEST = 0x134 BTN_TL = 0x136 BTN_TR = 0x137 BTN_SELECT = 0x13A BTN_START = 0x13B BTN_MODE = 0x13C BTN_THUMBL = 0x13D BTN_THUMBR = 0x13E XBOX360_BUTTONS = (BTN_SOUTH, BTN_EAST, BTN_NORTH, BTN_WEST, BTN_TL, BTN_TR, BTN_SELECT, BTN_START, BTN_MODE, BTN_THUMBL, BTN_THUMBR) GENERIC_BUTTONS = (BTN_TRIGGER, BTN_THUMB) ABS_X = 0x00 ABS_Y = 0x01 ABS_Z = 0x02 ABS_RX = 0x03 ABS_RY = 0x04 ABS_RZ = 0x05 ABS_HAT0X = 0x10 ABS_HAT0Y = 0x11 STICK_MAX = 32767 STICK_MIN = -32768 TRIGGER_MAX = 255 # code: (min, max, fuzz, flat) STICK_AXES = {ABS_X: (STICK_MIN, STICK_MAX, 16, 128), ABS_Y: (STICK_MIN, STICK_MAX, 16, 128)} XBOX360_AXES = { **STICK_AXES, ABS_RX: (STICK_MIN, STICK_MAX, 16, 128), ABS_RY: (STICK_MIN, STICK_MAX, 16, 128), ABS_Z: (0, TRIGGER_MAX, 0, 0), ABS_RZ: (0, TRIGGER_MAX, 0, 0), ABS_HAT0X: (-1, 1, 0, 0), ABS_HAT0Y: (-1, 1, 0, 0), } # How long the kernel and the readers get to see a new device. SETTLE_SECONDS = 1.0 FULL = STICK_MAX class Layout: def __init__(self, vendor, product, version, buttons, axes, line): self.vendor = vendor self.product = product self.version = version self.buttons = buttons self.axes = axes self.line = line XBOX360 = Layout(XBOX360_VENDOR, XBOX360_PRODUCT, XBOX360_VERSION, XBOX360_BUTTONS, XBOX360_AXES, {"button0": BTN_SOUTH, "button1": BTN_EAST}) GENERIC = Layout(GENERIC_VENDOR, GENERIC_PRODUCT, GENERIC_VERSION, GENERIC_BUTTONS, STICK_AXES, {"button0": BTN_TRIGGER, "button1": BTN_THUMB}) class Pad: def __init__(self, name, layout): self.fd = os.open(UINPUT, os.O_WRONLY | os.O_NONBLOCK) self.line = layout.line fcntl.ioctl(self.fd, UI_SET_EVBIT, EV_KEY) for b in layout.buttons: fcntl.ioctl(self.fd, UI_SET_KEYBIT, b) fcntl.ioctl(self.fd, UI_SET_EVBIT, EV_ABS) for code, (lo, hi, fuzz, flat) in layout.axes.items(): fcntl.ioctl(self.fd, UI_SET_ABSBIT, code) fcntl.ioctl(self.fd, UI_ABS_SETUP, struct.pack("HH6i", code, 0, 0, lo, hi, fuzz, flat, 0)) setup = struct.pack("HHHH%dsI" % NAME_BYTES, BUS_USB, layout.vendor, layout.product, layout.version, name.encode()[:NAME_BYTES - 1], 0) fcntl.ioctl(self.fd, UI_DEV_SETUP, setup) fcntl.ioctl(self.fd, UI_DEV_CREATE) self.held = set() def emit(self, kind, code, value): now = time.time() os.write(self.fd, struct.pack("qqHHi", int(now), int((now % 1) * 1e6), kind, code, value)) def apply(self): """Send the axes and buttons the held lines make, then a report.""" x = FULL if "right" in self.held else (-FULL if "left" in self.held else 0) y = FULL if "down" in self.held else (-FULL if "up" in self.held else 0) self.emit(EV_ABS, ABS_X, x) self.emit(EV_ABS, ABS_Y, y) for line, code in self.line.items(): self.emit(EV_KEY, code, 1 if line in self.held else 0) self.emit(EV_SYN, SYN_REPORT, 0) def close(self): fcntl.ioctl(self.fd, UI_DEV_DESTROY) os.close(self.fd) def main(argv): args = argv[1:] layout = XBOX360 if args and args[0] == "--generic": layout = GENERIC args = args[1:] prefix = args[0] if args else "JoeyLib gate" pads = {} try: for stick in ("A", "B"): pads[stick] = Pad("%s pad %s" % (prefix, stick), layout) time.sleep(SETTLE_SECONDS) for pad in pads.values(): pad.apply() print("ready", flush=True) for raw in sys.stdin: words = raw.split() if not words: continue if words[0] == "quit": break if words[0] not in ("press", "release"): print("error: unknown command %s" % words[0], flush=True) continue touched = set() for control in words[1:]: stick, _, line = control.partition("-") if stick not in pads or line not in ("left", "right", "up", "down", "button0", "button1"): print("error: unknown control %s" % control, flush=True) continue if words[0] == "press": pads[stick].held.add(line) else: pads[stick].held.discard(line) touched.add(stick) for stick in touched: pads[stick].apply() print("ok", flush=True) finally: for pad in pads.values(): pad.close() return 0 if __name__ == "__main__": sys.exit(main(sys.argv))