#!/usr/bin/env python3 """ Fake Gaming Wheel - Simple and Compatible Version Uses python-evdev library for reliable device creation. Install evdev if needed: sudo pip3 install evdev Usage: sudo python3 fake_wheel_simple.py [options] Examples: # Static position (no movement) sudo python3 fake_wheel_simple.py --steering 0 --throttle 128 --brake 0 # Hold button 5 sudo python3 fake_wheel_simple.py --button 5 # Auto-rotate with custom speed sudo python3 fake_wheel_simple.py --auto-rotate --rotate-speed 1.0 # Faster update rate sudo python3 fake_wheel_simple.py --rate 200 """ import sys import time import math import argparse import threading try: import evdev from evdev import UInput, AbsInfo, ecodes as e except ImportError: print("Error: python-evdev library not found.") print("\nInstall it with:") print(" sudo pip3 install evdev") print(" or: sudo apt install python3-evdev") sys.exit(1) class FakeGamingWheel: def __init__(self, args): self.running = True self.device = None self.args = args # Current state self.steering = args.steering if args.steering is not None else 0 self.throttle = args.throttle if args.throttle is not None else 0 self.brake = args.brake if args.brake is not None else 0 self.clutch = args.clutch if args.clutch is not None else 0 self.buttons = 0 # Button bitmask if args.button is not None and 1 <= args.button <= 16: self.buttons = 1 << (args.button - 1) def create_device(self): """Create the virtual gaming wheel device.""" # Define capabilities cap = { e.EV_KEY: [ e.BTN_TRIGGER, e.BTN_THUMB, e.BTN_THUMB2, e.BTN_TOP, e.BTN_TOP2, e.BTN_PINKIE, e.BTN_BASE, e.BTN_BASE2, e.BTN_BASE3, e.BTN_BASE4, e.BTN_BASE5, e.BTN_BASE6, e.BTN_DEAD, e.BTN_TRIGGER_HAPPY1, e.BTN_TRIGGER_HAPPY2, e.BTN_TRIGGER_HAPPY3, ], e.EV_ABS: [ (e.ABS_X, AbsInfo(value=0, min=-32768, max=32767, fuzz=16, flat=128, resolution=0)), # Steering wheel - Axis 0 (e.ABS_GAS, AbsInfo(value=0, min=0, max=255, fuzz=0, flat=15, resolution=0)), # Clutch pedal - Axis 1 (!) (e.ABS_BRAKE, AbsInfo(value=0, min=0, max=255, fuzz=0, flat=15, resolution=0)), # Brake pedal - Axis 2 (!) (e.ABS_RZ, AbsInfo(value=0, min=0, max=255, fuzz=0, flat=15, resolution=0)), # Throttle pedal - Axis 3 ], } # Create device with Logitech G29 identity self.device = UInput( events=cap, name='Logitech G29 Racing Wheel', vendor=0x046d, # Logitech product=0xc24f, # G29 version=0x0111, bustype=e.BUS_USB ) print("=" * 60) print("✓ Created virtual gaming wheel device") print(" Name: Logitech G29 Racing Wheel") print(" Vendor: 0x046d (Logitech)") print(" Product: 0xc24f (G29)") print(" Device: {}".format(self.device.device.path)) print("=" * 60) time.sleep(0.5) print("\nDevice is now available in:") print(" • /dev/input/by-id/ (look for Logitech)") print(" • {}".format(self.device.device.path)) print() # Send initial state to ensure axes are set correctly print("Initializing device state...") self.send_state() print(" ✓ Initial state sent") def destroy_device(self): """Destroy the virtual device.""" if self.device: self.device.close() print("\n✓ Destroyed virtual gaming wheel device") def send_state(self, debug=False): """Send current device state.""" # Send axis values directly (no conversion needed with 0-255 range) steering_raw = self.steering clutch_raw = self.clutch brake_raw = self.brake throttle_raw = self.throttle if debug: print(f"\n[DEBUG] Sending state:") print(f" Steering: {self.steering} -> raw {steering_raw}") print(f" Clutch: {self.clutch} -> raw {clutch_raw}") print(f" Brake: {self.brake} -> raw {brake_raw}") print(f" Throttle: {self.throttle} -> raw {throttle_raw}") self.device.write(e.EV_ABS, e.ABS_X, steering_raw) # Steering wheel - Axis 0 self.device.write(e.EV_ABS, e.ABS_GAS, clutch_raw) # Clutch pedal - Axis 1 self.device.write(e.EV_ABS, e.ABS_BRAKE, brake_raw) # Brake pedal - Axis 2 self.device.write(e.EV_ABS, e.ABS_RZ, throttle_raw) # Throttle pedal - Axis 3 # Send button states button_map = [ e.BTN_TRIGGER, e.BTN_THUMB, e.BTN_THUMB2, e.BTN_TOP, e.BTN_TOP2, e.BTN_PINKIE, e.BTN_BASE, e.BTN_BASE2, e.BTN_BASE3, e.BTN_BASE4, e.BTN_BASE5, e.BTN_BASE6, e.BTN_DEAD, e.BTN_TRIGGER_HAPPY1, e.BTN_TRIGGER_HAPPY2, e.BTN_TRIGGER_HAPPY3, ] for i, btn in enumerate(button_map): if i < 16: # Only 16 buttons button_pressed = (self.buttons >> i) & 1 self.device.write(e.EV_KEY, btn, button_pressed) # Sync self.device.syn() def update_state(self, elapsed_time): """Update the wheel state with simulated movement.""" # Steering: auto-rotate if enabled, otherwise use static value if self.args.auto_rotate: angle = elapsed_time * self.args.rotate_speed self.steering = int(32767 * math.sin(angle)) elif self.args.steering is None: # Default auto-rotate if nothing specified angle = elapsed_time * 0.5 self.steering = int(32767 * math.sin(angle)) # else: keep the static steering value from __init__ # Throttle: auto-pulse if enabled, otherwise use static value if self.args.auto_throttle: throttle_wave = (math.sin(elapsed_time * 2) + 1) / 2 self.throttle = int(255 * throttle_wave) elif self.args.throttle is None: # Default auto-pulse if nothing specified throttle_wave = (math.sin(elapsed_time * 2) + 1) / 2 self.throttle = int(255 * throttle_wave) # else: keep the static throttle value from __init__ # Brake: auto-brake if enabled, otherwise use static value if self.args.auto_brake: if int(elapsed_time) % 5 < 1: self.brake = 255 else: self.brake = 0 elif self.args.brake is None: # Default auto-brake if nothing specified if int(elapsed_time) % 5 < 1: self.brake = 255 else: self.brake = 0 # else: keep the static brake value from __init__ # Clutch: auto-clutch if enabled, otherwise use static value if self.args.auto_clutch: # Clutch engages/disengages in a pattern (simulating gear changes) clutch_cycle = (int(elapsed_time * 0.5) % 4) # 4 second cycle if clutch_cycle == 0: # Pressed for 1 second self.clutch = 255 else: self.clutch = 0 elif self.args.clutch is None: # Default: no clutch activity if nothing specified self.clutch = 0 # else: keep the static clutch value from __init__ # Buttons: cycle if enabled, otherwise keep static if self.args.cycle_buttons: button_index = int(elapsed_time) % 16 self.buttons = 1 << button_index elif self.args.button is None: # Default cycle if nothing specified button_index = int(elapsed_time) % 16 self.buttons = 1 << button_index # else: keep the static button value from __init__ def run(self): """Main loop.""" print("Simulating gaming wheel...") # Show current configuration if self.args.auto_rotate or self.args.steering is None: if self.args.auto_rotate: print(f" • Steering: Auto-rotating ±450° (speed: {self.args.rotate_speed}x)") else: print(f" • Steering: Auto-rotating ±450° (default)") else: steering_deg = self.steering / 32768 * 450 print(f" • Steering: Static at {steering_deg:+.1f}°") if self.args.auto_throttle or self.args.throttle is None: print(f" • Throttle: {'Auto-pulsing' if self.args.auto_throttle else 'Pulsing'} 0-100%") else: print(f" • Throttle: Static at {self.throttle / 255 * 100:.1f}%") if self.args.auto_brake or self.args.brake is None: print(f" • Brake: {'Auto-activating' if self.args.auto_brake else 'Periodic'} at 100% (every 5s)") else: print(f" • Brake: Static at {self.brake / 255 * 100:.1f}%") if self.args.auto_clutch: print(f" • Clutch: Auto-engaging (gear change pattern)") elif self.args.clutch is not None: print(f" • Clutch: Static at {self.clutch / 255 * 100:.1f}%") else: print(f" • Clutch: Released (0%)") if self.args.cycle_buttons or self.args.button is None: print(f" • Buttons: Cycling 1-16") elif self.args.button is not None: print(f" • Buttons: Holding button {self.args.button}") else: print(f" • Buttons: None pressed") print(f" • Update rate: {self.args.rate} Hz") print("\nPress Ctrl+C to stop\n") start_time = time.time() update_interval = 1.0 / self.args.rate debug_counter = 0 try: while self.running: elapsed = time.time() - start_time # Update state self.update_state(elapsed) # Send to device (with debug for first 3 iterations) if debug_counter < 3: print(f"\n=== Iteration {debug_counter + 1} ===") self.send_state(debug=True) debug_counter += 1 else: self.send_state() # Display status (shorter format to prevent line wrapping) if int(elapsed * 10) % 10 == 0: steering_deg = self.steering / 32768 * 450 throttle_pct = self.throttle / 255 * 100 brake_pct = self.brake / 255 * 100 clutch_pct = self.clutch / 255 * 100 pressed_buttons = [] for i in range(16): if self.buttons & (1 << i): pressed_buttons.append(str(i + 1)) button_str = ','.join(pressed_buttons) if pressed_buttons else '-' # Compact format print(f"\rS:{steering_deg:+6.1f}° T:{throttle_pct:5.1f}% " f"B:{brake_pct:5.1f}% C:{clutch_pct:5.1f}% Btn:{button_str:<4}", end='', flush=True) time.sleep(update_interval) except KeyboardInterrupt: print("\n\nStopping...") self.running = False def run_interactive(self): """Interactive mode - accept commands to change settings on the fly.""" print("Simulating gaming wheel in INTERACTIVE mode...") print(f" • Update rate: {self.args.rate} Hz") print("\n" + "=" * 60) print("INTERACTIVE MODE") print("=" * 60) print("\nAvailable commands:") print(" steering Set steering (-32768 to 32767, 0=center)") print(" throttle Set throttle (0 to 255)") print(" brake Set brake (0 to 255)") print(" clutch Set clutch (0 to 255)") print(" button Press button (1-16, 0=release all)") print(" status Show current values") print(" help Show this help") print(" quit / exit Stop the script") print("\nShorthand commands:") print(" s steering") print(" t throttle") print(" b brake") print(" c clutch") print(" btn button") print("\nExamples:") print(" steering 0 Center the wheel") print(" throttle 255 Full throttle") print(" button 5 Press button 5") print(" s -10000 t 200 Turn left and half throttle") print("=" * 60) print() # Start the device update loop in a separate thread update_thread = threading.Thread(target=self._update_loop, daemon=True) update_thread.start() # Main interactive input loop try: while self.running: try: user_input = input("> ").strip().lower() if not user_input: continue # Split input into commands (allows multiple commands in one line) tokens = user_input.split() i = 0 while i < len(tokens): cmd = tokens[i] # Quit commands if cmd in ['quit', 'exit', 'q']: print("Exiting...") self.running = False break # Help elif cmd in ['help', 'h', '?']: print("\nCommands: steering/s, throttle/t, brake/b, clutch/c, button/btn, status, quit") print("Example: s 0 t 128 b 0 btn 5\n") i += 1 # Status elif cmd == 'status': self._print_status() i += 1 # Commands that take a value elif cmd in ['steering', 's', 'throttle', 't', 'brake', 'b', 'clutch', 'c', 'button', 'btn']: if i + 1 >= len(tokens): print(f"Error: '{cmd}' requires a value") break try: value = int(tokens[i + 1]) if cmd in ['steering', 's']: if -32768 <= value <= 32767: self.steering = value print(f"✓ Steering set to {value} ({value / 32768 * 450:+.1f}°)") else: print("Error: Steering must be -32768 to 32767") elif cmd in ['throttle', 't']: if 0 <= value <= 255: self.throttle = value print(f"✓ Throttle set to {value} ({value / 255 * 100:.1f}%)") self.send_state(debug=True) else: print("Error: Throttle must be 0 to 255") elif cmd in ['brake', 'b']: if 0 <= value <= 255: self.brake = value print(f"✓ Brake set to {value} ({value / 255 * 100:.1f}%)") self.send_state(debug=True) else: print("Error: Brake must be 0 to 255") elif cmd in ['clutch', 'c']: if 0 <= value <= 255: self.clutch = value print(f"✓ Clutch set to {value} ({value / 255 * 100:.1f}%)") self.send_state(debug=True) else: print("Error: Clutch must be 0 to 255") elif cmd in ['button', 'btn']: if 0 <= value <= 16: if value == 0: self.buttons = 0 print("✓ All buttons released") else: self.buttons = 1 << (value - 1) print(f"✓ Button {value} pressed") else: print("Error: Button must be 0 to 16") i += 2 except ValueError: print(f"Error: Invalid value '{tokens[i + 1]}'") break else: print(f"Unknown command: '{cmd}'. Type 'help' for commands.") break except EOFError: print("\nExiting...") self.running = False break except KeyboardInterrupt: print("\n\nStopping...") self.running = False def _update_loop(self): """Background thread that continuously sends device state.""" print("[DEBUG] Background update thread started") update_interval = 1.0 / self.args.rate iteration = 0 while self.running: self.send_state() if iteration < 3: print(f"[DEBUG] Background thread iteration {iteration}: T={self.throttle} B={self.brake} C={self.clutch}") iteration += 1 time.sleep(update_interval) def _print_status(self): """Print current device state.""" steering_deg = self.steering / 32768 * 450 throttle_pct = self.throttle / 255 * 100 brake_pct = self.brake / 255 * 100 clutch_pct = self.clutch / 255 * 100 pressed_buttons = [] for i in range(16): if self.buttons & (1 << i): pressed_buttons.append(str(i + 1)) button_str = ','.join(pressed_buttons) if pressed_buttons else 'None' print(f"\n--- Current Status ---") print(f"Steering: {self.steering:6d} ({steering_deg:+6.1f}°)") print(f"Throttle: {self.throttle:3d} ({throttle_pct:5.1f}%)") print(f"Brake: {self.brake:3d} ({brake_pct:5.1f}%)") print(f"Clutch: {self.clutch:3d} ({clutch_pct:5.1f}%)") print(f"Buttons: {button_str}") print() def main(): import signal parser = argparse.ArgumentParser( description='Fake Gaming Wheel - Virtual Logitech G29 Racing Wheel', formatter_class=argparse.RawDescriptionHelpFormatter, epilog=""" Examples: # Default behavior (auto-rotating, pulsing throttle, periodic brake) sudo python3 fake_wheel_simple.py # Static centered position with no inputs sudo python3 fake_wheel_simple.py --steering 0 --throttle 0 --brake 0 --button 0 # Full throttle, half brake, steering left 45° sudo python3 fake_wheel_simple.py --steering -3640 --throttle 255 --brake 128 # Hold button 5 while auto-rotating sudo python3 fake_wheel_simple.py --button 5 --auto-rotate # Fast rotation with high update rate sudo python3 fake_wheel_simple.py --rotate-speed 2.0 --rate 200 # Static steering at center, auto throttle only sudo python3 fake_wheel_simple.py --steering 0 --auto-throttle --brake 0 Axis Ranges: Steering: -32768 to 32767 (represents ±450°, so ±72.8 per degree) Throttle: 0 to 255 (0% to 100%) Brake: 0 to 255 (0% to 100%) Clutch: 0 to 255 (0% to 100%, 0=released, 255=fully pressed) Buttons: 1 to 16 """ ) # Static values parser.add_argument('--steering', type=int, metavar='N', help='Static steering position (-32768 to 32767, 0=center)') parser.add_argument('--throttle', type=int, metavar='N', help='Static throttle position (0 to 255)') parser.add_argument('--brake', type=int, metavar='N', help='Static brake position (0 to 255)') parser.add_argument('--clutch', type=int, metavar='N', help='Static clutch position (0 to 255, 0=released, 255=fully pressed)') parser.add_argument('--button', type=int, metavar='N', help='Hold specific button (1-16, 0=none)') # Auto behaviors (override static values) parser.add_argument('--auto-rotate', action='store_true', help='Enable auto-rotating steering (overrides --steering)') parser.add_argument('--rotate-speed', type=float, default=0.5, metavar='X', help='Rotation speed multiplier (default: 0.5)') parser.add_argument('--auto-throttle', action='store_true', help='Enable auto-pulsing throttle (overrides --throttle)') parser.add_argument('--auto-brake', action='store_true', help='Enable auto-braking pattern (overrides --brake)') parser.add_argument('--auto-clutch', action='store_true', help='Enable auto-clutch pattern (simulates gear changes)') parser.add_argument('--cycle-buttons', action='store_true', help='Cycle through all buttons (overrides --button)') # System options parser.add_argument('--rate', type=int, default=100, metavar='HZ', help='Update rate in Hz (default: 100)') parser.add_argument('--interactive', '-i', action='store_true', help='Interactive mode - change settings via CLI commands') parser.add_argument('--quiet', action='store_true', help='Suppress status output (only show device info)') args = parser.parse_args() # Validate ranges if args.steering is not None and not (-32768 <= args.steering <= 32767): parser.error('--steering must be between -32768 and 32767') if args.throttle is not None and not (0 <= args.throttle <= 255): parser.error('--throttle must be between 0 and 255') if args.brake is not None and not (0 <= args.brake <= 255): parser.error('--brake must be between 0 and 255') if args.clutch is not None and not (0 <= args.clutch <= 255): parser.error('--clutch must be between 0 and 255') if args.button is not None and not (0 <= args.button <= 16): parser.error('--button must be between 0 and 16') if args.rate < 1 or args.rate > 1000: parser.error('--rate must be between 1 and 1000 Hz') def signal_handler(sig, frame): sys.exit(0) signal.signal(signal.SIGINT, signal_handler) signal.signal(signal.SIGTERM, signal_handler) wheel = FakeGamingWheel(args) try: wheel.create_device() if args.interactive: wheel.run_interactive() else: wheel.run() finally: wheel.destroy_device() if __name__ == '__main__': main()