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

304 lines
10 KiB
Python
Executable File

#!/usr/bin/env python3
"""
Fake Gaming Wheel USB Device Emulator
Creates a virtual USB HID gaming wheel device for testing purposes.
Simulates a Logitech G29 Racing Wheel.
Requirements:
- Linux with uhid kernel module
- Root/sudo access to access /dev/uhid
- Python 3.6+
Usage:
sudo python3 fake_wheel.py
"""
import os
import sys
import struct
import time
import select
import signal
# UHID event types
UHID_CREATE2 = 11
UHID_DESTROY = 1
UHID_INPUT2 = 12
UHID_OUTPUT = 6
UHID_GET_REPORT = 9
UHID_SET_REPORT = 13
class FakeGamingWheel:
def __init__(self):
self.running = True
self.fd = None
# HID Report Descriptor for a gaming wheel with:
# - Steering wheel axis (X axis)
# - Throttle (Y axis)
# - Brake (Z axis)
# - Multiple buttons
self.hid_descriptor = bytes([
0x05, 0x01, # Usage Page (Generic Desktop)
0x09, 0x04, # Usage (Joystick)
0xA1, 0x01, # Collection (Application)
0x09, 0x01, # Usage (Pointer)
0xA1, 0x00, # Collection (Physical)
# Steering wheel (X axis) - 16 bit
0x09, 0x30, # Usage (X)
0x15, 0x00, # Logical Minimum (0)
0x27, 0xFF, 0xFF, 0x00, 0x00, # Logical Maximum (65535)
0x35, 0x00, # Physical Minimum (0)
0x47, 0xFF, 0xFF, 0x00, 0x00, # Physical Maximum (65535)
0x75, 0x10, # Report Size (16 bits)
0x95, 0x01, # Report Count (1)
0x81, 0x02, # Input (Data, Variable, Absolute)
# Throttle (Y axis) - 16 bit
0x09, 0x31, # Usage (Y)
0x15, 0x00, # Logical Minimum (0)
0x27, 0xFF, 0xFF, 0x00, 0x00, # Logical Maximum (65535)
0x75, 0x10, # Report Size (16 bits)
0x95, 0x01, # Report Count (1)
0x81, 0x02, # Input (Data, Variable, Absolute)
# Brake (Z axis) - 16 bit
0x09, 0x32, # Usage (Z)
0x15, 0x00, # Logical Minimum (0)
0x27, 0xFF, 0xFF, 0x00, 0x00, # Logical Maximum (65535)
0x75, 0x10, # Report Size (16 bits)
0x95, 0x01, # Report Count (1)
0x81, 0x02, # Input (Data, Variable, Absolute)
# Buttons (16 buttons)
0x05, 0x09, # Usage Page (Button)
0x19, 0x01, # Usage Minimum (Button 1)
0x29, 0x10, # Usage Maximum (Button 16)
0x15, 0x00, # Logical Minimum (0)
0x25, 0x01, # Logical Maximum (1)
0x75, 0x01, # Report Size (1 bit)
0x95, 0x10, # Report Count (16)
0x81, 0x02, # Input (Data, Variable, Absolute)
0xC0, # End Collection
0xC0 # End Collection
])
# Current state
self.steering = 32768 # Center position (0-65535)
self.throttle = 0 # No throttle
self.brake = 0 # No brake
self.buttons = 0 # No buttons pressed
def handle_uhid_event(self):
"""Read and handle events from uhid."""
try:
# Use select to check if data is available (non-blocking)
readable, _, _ = select.select([self.fd], [], [], 0)
if not readable:
return
data = os.read(self.fd, 4096)
if len(data) < 4:
return
event_type = struct.unpack('<I', data[:4])[0]
# We can ignore most events, but logging them helps with debugging
if event_type == UHID_OUTPUT:
pass # Output report from host
elif event_type == UHID_GET_REPORT:
pass # Host requesting report
elif event_type == UHID_SET_REPORT:
pass # Host setting report
except BlockingIOError:
pass
except Exception as e:
print(f"Warning: Error handling uhid event: {e}")
def create_device(self):
"""Create the virtual HID device."""
try:
self.fd = os.open('/dev/uhid', os.O_RDWR | os.O_NONBLOCK)
except PermissionError:
print("Error: Permission denied. Please run with sudo.")
sys.exit(1)
except FileNotFoundError:
print("Error: /dev/uhid not found. Make sure uhid module is loaded.")
print("Try: sudo modprobe uhid")
sys.exit(1)
# Create UHID_CREATE2 event
name = b"Fake Logitech G29 Wheel"
phys = b"fake-wheel-phys"
uniq = b"fake-wheel-001"
# Logitech G29 vendor/product IDs
bus = 0x03 # USB
vid = 0x046d # Logitech
pid = 0xc24f # G29 Racing Wheel
# Prepare the create2 structure matching kernel's uhid_event
# struct uhid_create2_req is what we need to match
rd_size = len(self.hid_descriptor)
# Pack: name(128), phys(64), uniq(64), rd_size(u16), bus(u16), vid(u32), pid(u32), version(u32), country(u32), rd_data
create2_data = struct.pack(
'<I', # event type
UHID_CREATE2
) + struct.pack(
'<128s64s64sHHIIII',
name.ljust(128, b'\x00'),
phys.ljust(64, b'\x00'),
uniq.ljust(64, b'\x00'),
rd_size,
bus,
vid,
pid,
0, # version
0 # country
) + self.hid_descriptor
# Pad to 4096 bytes (kernel's UHID_DATA_MAX)
if len(create2_data) < 4096:
create2_data += b'\x00' * (4096 - len(create2_data))
bytes_written = os.write(self.fd, create2_data)
print(f"✓ Sent UHID_CREATE2 event ({bytes_written} bytes)")
print(f" Name: {name.decode().strip()}")
print(f" Vendor ID: 0x{vid:04x} (Logitech)")
print(f" Product ID: 0x{pid:04x} (G29 Racing Wheel)")
print(f" HID Descriptor: {rd_size} bytes")
# Give kernel time to process and create device
time.sleep(0.5)
# Check for any response from kernel
self.handle_uhid_event()
print(f" Device should appear in /dev/input/")
print("\nCheck with: ls -l /dev/input/by-id/ | grep -i logitech")
def destroy_device(self):
"""Destroy the virtual HID device."""
if self.fd:
event = struct.pack('<I', UHID_DESTROY)
payload = b'\x00' * 4096
os.write(self.fd, event + payload)
os.close(self.fd)
print("\n✓ Destroyed virtual gaming wheel device")
def send_report(self):
"""Send a HID input report with current state."""
# Handle any pending uhid events from kernel
self.handle_uhid_event()
# Pack the report: steering (16-bit), throttle (16-bit), brake (16-bit), buttons (16-bit)
report = struct.pack('<HHHH',
self.steering,
self.throttle,
self.brake,
self.buttons)
# Create UHID_INPUT2 event: type(u32) + size(u16) + data
input_data = struct.pack('<IH', UHID_INPUT2, len(report)) + report
# Pad to 4096 bytes
if len(input_data) < 4096:
input_data += b'\x00' * (4096 - len(input_data))
os.write(self.fd, input_data)
def update_state(self, elapsed_time):
"""Update the wheel state (simulate movement)."""
import math
# Simulate steering wheel movement (slow sine wave)
# Range: 0-65535, center at 32768
angle = elapsed_time * 0.5 # Slow rotation
self.steering = int(32768 + 16384 * math.sin(angle))
# Simulate throttle (pulse pattern)
throttle_wave = (math.sin(elapsed_time * 2) + 1) / 2
self.throttle = int(65535 * throttle_wave * 0.5)
# Simulate brake (occasional braking)
if int(elapsed_time) % 5 < 1: # Brake for 1s every 5s
self.brake = int(65535 * 0.8)
else:
self.brake = 0
# Simulate button presses (cycle through buttons)
button_index = int(elapsed_time) % 16
self.buttons = 1 << button_index
def run(self):
"""Main loop - send periodic updates."""
print("\nSimulating gaming wheel...")
print("Features:")
print(" • Steering wheel: Auto-rotating left/right")
print(" • Throttle: Pulsing 0-50%")
print(" • Brake: Activates every 5 seconds")
print(" • Buttons: Cycling through buttons 1-16")
print("\nPress Ctrl+C to stop\n")
start_time = time.time()
try:
while self.running:
elapsed = time.time() - start_time
# Update state with simulated movement
self.update_state(elapsed)
# Send the report
self.send_report()
# Print status every second
if int(elapsed * 10) % 10 == 0:
steering_deg = (self.steering - 32768) / 32768 * 900 # G29 has 900° rotation
throttle_pct = self.throttle / 65535 * 100
brake_pct = self.brake / 65535 * 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:2d}", end='', flush=True)
time.sleep(0.01) # 100 Hz update rate
except KeyboardInterrupt:
print("\n\nStopping...")
self.running = False
def signal_handler(sig, frame):
"""Handle Ctrl+C gracefully."""
print("\n\nReceived signal, stopping...")
sys.exit(0)
def main():
print("=" * 60)
print("Fake Gaming Wheel USB Device Emulator")
print("=" * 60)
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()