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

589 lines
23 KiB
Python
Executable File

#!/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 <value> Set steering (-32768 to 32767, 0=center)")
print(" throttle <value> Set throttle (0 to 255)")
print(" brake <value> Set brake (0 to 255)")
print(" clutch <value> Set clutch (0 to 255)")
print(" button <num> 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 <value> steering")
print(" t <value> throttle")
print(" b <value> brake")
print(" c <value> clutch")
print(" btn <num> 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()