265 lines
8.4 KiB
Python
Executable File
265 lines
8.4 KiB
Python
Executable File
#!/usr/bin/env python3
|
|
"""
|
|
Fake Gaming Wheel using uinput
|
|
Creates a virtual gaming wheel device for testing.
|
|
|
|
Requirements:
|
|
- Linux with uinput kernel module
|
|
- Root/sudo access or user in 'input' group
|
|
- Python 3.6+
|
|
|
|
Usage:
|
|
sudo python3 fake_wheel_uinput.py
|
|
"""
|
|
|
|
import os
|
|
import sys
|
|
import struct
|
|
import time
|
|
import fcntl
|
|
import signal
|
|
import math
|
|
|
|
# uinput constants
|
|
UINPUT_MAX_NAME_SIZE = 80
|
|
|
|
# ioctl commands
|
|
UI_DEV_CREATE = 0x5501
|
|
UI_DEV_DESTROY = 0x5502
|
|
UI_DEV_SETUP = 0x405c5503
|
|
UI_ABS_SETUP = 0x401c5504
|
|
UI_SET_EVBIT = 0x40045564
|
|
UI_SET_KEYBIT = 0x40045565
|
|
UI_SET_ABSBIT = 0x40045567
|
|
UI_SET_FFBIT = 0x4004556b
|
|
|
|
# Event types
|
|
EV_SYN = 0x00
|
|
EV_KEY = 0x01
|
|
EV_ABS = 0x03
|
|
EV_FF = 0x15
|
|
|
|
# Absolute axes
|
|
ABS_X = 0x00 # Steering wheel
|
|
ABS_Y = 0x01 # Throttle
|
|
ABS_Z = 0x02 # Brake
|
|
ABS_RZ = 0x05 # Clutch (optional)
|
|
|
|
# Button codes (BTN_JOYSTICK range)
|
|
BTN_TRIGGER = 0x120
|
|
BTN_BASE = 0x126
|
|
|
|
class FakeGamingWheel:
|
|
def __init__(self):
|
|
self.running = True
|
|
self.fd = None
|
|
|
|
# Current state
|
|
self.steering = 0 # -32768 to 32767
|
|
self.throttle = 0 # 0 to 255
|
|
self.brake = 0 # 0 to 255
|
|
self.buttons = 0 # Bitmask
|
|
|
|
def create_device(self):
|
|
"""Create the virtual gaming wheel device."""
|
|
try:
|
|
self.fd = os.open('/dev/uinput', os.O_WRONLY | os.O_NONBLOCK)
|
|
except PermissionError:
|
|
try:
|
|
self.fd = os.open('/dev/input/uinput', os.O_WRONLY | os.O_NONBLOCK)
|
|
except:
|
|
print("Error: Permission denied accessing uinput.")
|
|
print("Try: sudo chmod +0666 /dev/uinput")
|
|
print("Or run with: sudo python3 fake_wheel_uinput.py")
|
|
sys.exit(1)
|
|
except FileNotFoundError:
|
|
print("Error: uinput device not found.")
|
|
print("Try: sudo modprobe uinput")
|
|
sys.exit(1)
|
|
|
|
# Enable event types
|
|
fcntl.ioctl(self.fd, UI_SET_EVBIT, EV_KEY) # Button events
|
|
fcntl.ioctl(self.fd, UI_SET_EVBIT, EV_ABS) # Absolute axis events
|
|
fcntl.ioctl(self.fd, UI_SET_EVBIT, EV_FF) # Force feedback
|
|
|
|
# Enable buttons (16 buttons)
|
|
for i in range(16):
|
|
fcntl.ioctl(self.fd, UI_SET_KEYBIT, BTN_TRIGGER + i)
|
|
|
|
# Enable and setup axes
|
|
fcntl.ioctl(self.fd, UI_SET_ABSBIT, ABS_X) # Steering
|
|
fcntl.ioctl(self.fd, UI_SET_ABSBIT, ABS_Y) # Throttle
|
|
fcntl.ioctl(self.fd, UI_SET_ABSBIT, ABS_Z) # Brake
|
|
|
|
# Setup axis parameters using UI_ABS_SETUP (new API)
|
|
# struct uinput_abs_setup: code(u16), padding(u16), absinfo(struct input_absinfo)
|
|
# struct input_absinfo: value(s32), minimum(s32), maximum(s32), fuzz(s32), flat(s32), resolution(s32)
|
|
|
|
# ABS_X: Steering wheel (-32768 to 32767)
|
|
abs_setup_x = struct.pack('HH6i', ABS_X, 0, 0, -32768, 32767, 16, 128, 0)
|
|
fcntl.ioctl(self.fd, UI_ABS_SETUP, abs_setup_x)
|
|
|
|
# ABS_Y: Throttle (0 to 255)
|
|
abs_setup_y = struct.pack('HH6i', ABS_Y, 0, 0, 0, 255, 0, 15, 0)
|
|
fcntl.ioctl(self.fd, UI_ABS_SETUP, abs_setup_y)
|
|
|
|
# ABS_Z: Brake (0 to 255)
|
|
abs_setup_z = struct.pack('HH6i', ABS_Z, 0, 0, 0, 255, 0, 15, 0)
|
|
fcntl.ioctl(self.fd, UI_ABS_SETUP, abs_setup_z)
|
|
|
|
# Setup device info using UI_DEV_SETUP (new API)
|
|
# struct uinput_setup: id(struct input_id), name(80 bytes), ff_effects_max(u32)
|
|
# struct input_id: bustype(u16), vendor(u16), product(u16), version(u16)
|
|
device_setup = struct.pack(
|
|
'HHHH80sI',
|
|
0x03, # bustype (BUS_USB)
|
|
0x046d, # vendor (Logitech)
|
|
0xc24f, # product (G29)
|
|
0x0111, # version
|
|
b'Logitech G29 Racing Wheel\x00', # name (80 bytes, null-terminated)
|
|
0 # ff_effects_max
|
|
)
|
|
fcntl.ioctl(self.fd, UI_DEV_SETUP, device_setup)
|
|
|
|
# Create the device
|
|
fcntl.ioctl(self.fd, UI_DEV_CREATE)
|
|
|
|
print("=" * 60)
|
|
print("✓ Created virtual gaming wheel device")
|
|
print(" Name: Logitech G29 Racing Wheel")
|
|
print(" Vendor: 0x046d (Logitech)")
|
|
print(" Product: 0xc24f (G29)")
|
|
print("=" * 60)
|
|
|
|
time.sleep(0.5) # Give system time to recognize device
|
|
|
|
print("\nDevice should now appear in:")
|
|
print(" • /dev/input/by-id/ (look for Logitech)")
|
|
print(" • /proc/bus/input/devices")
|
|
print("\nTest with: evtest (select the G29 device)")
|
|
print()
|
|
|
|
def destroy_device(self):
|
|
"""Destroy the virtual device."""
|
|
if self.fd:
|
|
try:
|
|
fcntl.ioctl(self.fd, UI_DEV_DESTROY)
|
|
os.close(self.fd)
|
|
print("\n✓ Destroyed virtual gaming wheel device")
|
|
except Exception as e:
|
|
print(f"\nWarning: Error destroying device: {e}")
|
|
|
|
def send_event(self, ev_type, code, value):
|
|
"""Send a single input event."""
|
|
# struct input_event: timeval (8 bytes) + type (2) + code (2) + value (4) = 16 bytes on 32-bit, 24 on 64-bit
|
|
# We'll use the simpler 5-value format: sec, usec, type, code, value
|
|
if sys.maxsize > 2**32: # 64-bit
|
|
event = struct.pack('llHHi', 0, 0, ev_type, code, value)
|
|
else: # 32-bit
|
|
event = struct.pack('IIHHi', 0, 0, ev_type, code, value)
|
|
|
|
os.write(self.fd, event)
|
|
|
|
def sync(self):
|
|
"""Send sync event to mark end of event batch."""
|
|
self.send_event(EV_SYN, 0, 0)
|
|
|
|
def send_state(self):
|
|
"""Send current device state."""
|
|
# Send axis values
|
|
self.send_event(EV_ABS, ABS_X, self.steering)
|
|
self.send_event(EV_ABS, ABS_Y, self.throttle)
|
|
self.send_event(EV_ABS, ABS_Z, self.brake)
|
|
|
|
# Send button states
|
|
for i in range(16):
|
|
button_pressed = (self.buttons >> i) & 1
|
|
self.send_event(EV_KEY, BTN_TRIGGER + i, button_pressed)
|
|
|
|
# Sync
|
|
self.sync()
|
|
|
|
def update_state(self, elapsed_time):
|
|
"""Update the wheel state with simulated movement."""
|
|
# Steering: smooth sine wave (full range)
|
|
angle = elapsed_time * 0.5
|
|
self.steering = int(16384 * math.sin(angle))
|
|
|
|
# Throttle: pulse pattern (0-255)
|
|
throttle_wave = (math.sin(elapsed_time * 2) + 1) / 2
|
|
self.throttle = int(200 * throttle_wave)
|
|
|
|
# Brake: periodic braking
|
|
if int(elapsed_time) % 5 < 1:
|
|
self.brake = 200
|
|
else:
|
|
self.brake = 0
|
|
|
|
# Buttons: cycle through
|
|
button_index = int(elapsed_time) % 16
|
|
self.buttons = 1 << button_index
|
|
|
|
def run(self):
|
|
"""Main loop."""
|
|
print("Simulating gaming wheel...")
|
|
print(" • Steering: Auto-rotating ±450°")
|
|
print(" • Throttle: Pulsing 0-78%")
|
|
print(" • Brake: Periodic (every 5s)")
|
|
print(" • Buttons: Cycling 1-16")
|
|
print("\nPress Ctrl+C to stop\n")
|
|
|
|
start_time = time.time()
|
|
|
|
try:
|
|
while self.running:
|
|
elapsed = time.time() - start_time
|
|
|
|
# Update state
|
|
self.update_state(elapsed)
|
|
|
|
# Send to device
|
|
self.send_state()
|
|
|
|
# Display status
|
|
if int(elapsed * 10) % 10 == 0:
|
|
steering_deg = self.steering / 32768 * 450
|
|
throttle_pct = self.throttle / 255 * 100
|
|
brake_pct = self.brake / 255 * 100
|
|
|
|
pressed_button = None
|
|
for i in range(16):
|
|
if self.buttons & (1 << i):
|
|
pressed_button = i + 1
|
|
break
|
|
|
|
print(f"\rSteering: {steering_deg:+6.1f}° | "
|
|
f"Throttle: {throttle_pct:5.1f}% | "
|
|
f"Brake: {brake_pct:5.1f}% | "
|
|
f"Button: {pressed_button if pressed_button else 'None':>4}",
|
|
end='', flush=True)
|
|
|
|
time.sleep(0.01) # 100 Hz
|
|
|
|
except KeyboardInterrupt:
|
|
print("\n\nStopping...")
|
|
self.running = False
|
|
|
|
def signal_handler(sig, frame):
|
|
"""Handle shutdown signals."""
|
|
sys.exit(0)
|
|
|
|
def main():
|
|
signal.signal(signal.SIGINT, signal_handler)
|
|
signal.signal(signal.SIGTERM, signal_handler)
|
|
|
|
wheel = FakeGamingWheel()
|
|
|
|
try:
|
|
wheel.create_device()
|
|
wheel.run()
|
|
finally:
|
|
wheel.destroy_device()
|
|
|
|
if __name__ == '__main__':
|
|
main()
|