#!/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()