joeylib2/scripts/lib/virtualPad.py
2026-10-13 14:01:46 -05:00

170 lines
5.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.
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). 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 [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
NAME_BYTES = 80
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
BUTTONS = (BTN_SOUTH, BTN_EAST, BTN_NORTH, BTN_WEST, BTN_TL, BTN_TR,
BTN_SELECT, BTN_START, BTN_MODE, BTN_THUMBL, BTN_THUMBR)
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)
AXES = {
ABS_X: (STICK_MIN, STICK_MAX, 16, 128), ABS_Y: (STICK_MIN, STICK_MAX, 16, 128),
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
LINE = {"button0": BTN_SOUTH, "button1": BTN_EAST}
class Pad:
def __init__(self, name):
self.fd = os.open(UINPUT, os.O_WRONLY | os.O_NONBLOCK)
fcntl.ioctl(self.fd, UI_SET_EVBIT, EV_KEY)
for b in 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 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, XBOX360_VENDOR, XBOX360_PRODUCT,
XBOX360_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 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):
prefix = argv[1] if len(argv) > 1 else "JoeyLib gate"
pads = {}
try:
for stick in ("A", "B"):
pads[stick] = Pad("%s pad %s" % (prefix, stick))
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))