Files
Tools/fake-wheel/fake_wheel_uinput.py
T
2026-06-01 01:25:58 +01:00

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()