I had a problem with my dual T16000M joysticks being detected as a single device, rather than as two separate joysticks.
On Windows I used VJoy to setup a single virtual joystick, but now I've moved to Linux (windows 10 won't run on my functional yet old PC), I ran into the same issue.
To work around it, I created a script which creates two virtual joysticks with a unique name.
Here it is for those of you who want or need it.
I execute it via "sudo python3 joystick_emulator.py"
Package Dependencies:
python3-evdev → lets Python read real joystick events (from /dev/input/event*).
python3-uinput → lets Python create virtual joystick devices.
jstest-gtk → GUI tool to test/calibrate joysticks.
joystick (sometimes installed with jstest-gtk) → provides jstest and other low-level joystick tools.
libsdl2-dev (attempted, not always needed) → development headers for SDL2, useful if you want to rebuild joystick GUIDs for games.
Code: Select all
#!/usr/bin/env python3
# Requires: pip install evdev
import asyncio
from evdev import InputDevice, list_devices, ecodes, UInput, AbsInfo
# -----------------------------
# Configuration
# -----------------------------
TARGET_NAME = 'Thrustmaster T.16000M'
# Virtual axis full range for games
VIRTUAL_MIN = -32768
VIRTUAL_MAX = 32767
# -----------------------------
# Helper functions
# -----------------------------
def find_joysticks(name):
"""
Return all input devices matching the given name.
Lesson: Must find both sticks separately; using order is safer than relying on /dev/input/eventX numbering.
"""
devices = [InputDevice(path) for path in list_devices()]
return [dev for dev in devices if dev.name == name]
def build_abs_map(dev: InputDevice):
"""
Build a dictionary of absolute axes for UInput.
Lesson: Must use positional args for AbsInfo in Python 3.12+; keyword args cause TypeError.
"""
abs_map = {}
for code, absinfo in dev.capabilities().get(ecodes.EV_ABS, []):
# Initialize virtual axis with min/max range, starting value is irrelevant because we forward scaled events
abs_map[code] = AbsInfo(
0, # Starting value ignored
VIRTUAL_MIN, # Virtual min for full range mapping
VIRTUAL_MAX, # Virtual max
absinfo.fuzz, # Keep device fuzz/noise filtering
absinfo.flat, # Preserve physical dead zone
absinfo.resolution # Preserve resolution
)
return abs_map
def build_key_map(dev: InputDevice):
"""
Build a list of button codes for UInput.
Lesson: evdev.capabilities() may return ints or tuples; must handle both to avoid TypeError.
"""
keys = dev.capabilities().get(ecodes.EV_KEY, [])
key_codes = []
for k in keys:
if isinstance(k, tuple):
key_codes.append(k[0])
else:
key_codes.append(k)
return key_codes
def scale_axis(value, phys_min, phys_max):
"""
Scale the physical axis to the virtual axis range.
Lesson: T.16000M raw axes are nonlinear or partial range; virtual device must remap full travel.
"""
if phys_max == phys_min:
return VIRTUAL_MIN
scaled = int((value - phys_min) / (phys_max - phys_min) * (VIRTUAL_MAX - VIRTUAL_MIN) + VIRTUAL_MIN)
return max(VIRTUAL_MIN, min(VIRTUAL_MAX, scaled))
async def forward_events(dev: InputDevice, ui: UInput):
"""
Forward events from the physical stick to the virtual UInput device.
Lessons learned:
- Must forward both EV_KEY and EV_ABS.
- Use ui.write_event(event) for keys; for axes, scale values.
- Always call ui.syn() after each event to update device state.
"""
# Read axis info once for scaling
abs_infos = {code: dev.absinfo(code) for code, _ in dev.capabilities().get(ecodes.EV_ABS, [])}
async for event in dev.async_read_loop():
if event.type == ecodes.EV_KEY:
# Forward button presses exactly
ui.write_event(event)
ui.syn()
elif event.type == ecodes.EV_ABS:
# Scale axis values to full virtual range
info = abs_infos[event.code]
scaled = scale_axis(event.value, info.min, info.max)
ui.write(ecodes.EV_ABS, event.code, scaled)
ui.syn()
# -----------------------------
# Main
# -----------------------------
async def main():
# Detect both sticks
sticks = find_joysticks(TARGET_NAME)
if len(sticks) < 2:
print(f"Error: Found {len(sticks)} '{TARGET_NAME}' sticks, need 2.")
return
# Assign left/right based on detection order
dev_left, dev_right = sticks[1], sticks[0]
"""
Create virtual devices for left/right sticks
Lessons learned:
- Need a unique Product code so as not to conflict with the physical joysticks.
- The OOTB product code is 0xb10a
- The codes I chose might conflict with other devices.
"""
# Read axis info once for scaling
ui_left = UInput(
events={ecodes.EV_KEY: build_key_map(dev_left), ecodes.EV_ABS: build_abs_map(dev_left)},
name='T16000M Left',
vendor=0x044f, # Thrustmaster VID
product=0xb10b # Modified T.16000M PID
)
ui_right = UInput(
events={ecodes.EV_KEY: build_key_map(dev_right), ecodes.EV_ABS: build_abs_map(dev_right)},
name='T16000M Right',
vendor=0x044f,
product=0xb10c
)
print(f"Forwarding {dev_left.path} → T16000M Left")
print(f"Forwarding {dev_right.path} → T16000M Right")
# Forward events concurrently for both sticks
await asyncio.gather(
forward_events(dev_left, ui_left),
forward_events(dev_right, ui_right)
)
if __name__ == "__main__":
asyncio.run(main())