170 lines
5.7 KiB
Python
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))
|