589 lines
23 KiB
Python
Executable File
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()
|