203 lines
7 KiB
Python
203 lines
7 KiB
Python
#!/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))
|