Add | more formatter and some more Fix | type mis match sync pyglet Enhance | logger with Template add lib-not-dr as requirement sync pyglet sync pyglet Add | add lto=yes to nuitka_build just incase sync pyglet sync lib_not_dr Remove | external requirement lib-not-dr some logger sync lib-not-dr sync pyglet sync lib-not-dr sync lib-not-dr sync pyglet sync pyglet Fix | console thread been block Update DR rs and DR sdk sync lib not dr sync lib-not-dr sync lib-not-dr sync pyglet and lib-not-dr sync pyglet 0.1.8 sync lib not dr logger almost done? almost! sync pyglet (clicpboard support!) sync lib not dr sync lib not dr color code and sync pyglet do not show memory and progress building localy sync pyglet synclibs
658 lines
19 KiB
Python
658 lines
19 KiB
Python
import os
|
|
import time
|
|
import fcntl
|
|
import ctypes
|
|
import warnings
|
|
|
|
from ctypes import c_uint16 as _u16
|
|
from ctypes import c_int16 as _s16
|
|
from ctypes import c_uint32 as _u32
|
|
from ctypes import c_int32 as _s32
|
|
from ctypes import c_int64 as _s64
|
|
from concurrent.futures import ThreadPoolExecutor
|
|
|
|
from typing import List
|
|
|
|
import pyglet
|
|
|
|
from .evdev_constants import *
|
|
from pyglet.app.xlib import XlibSelectDevice
|
|
from pyglet.input.base import Device, RelativeAxis, AbsoluteAxis, Button, Joystick, Controller
|
|
from pyglet.input.base import DeviceOpenException, ControllerManager
|
|
from pyglet.input.controller import get_mapping, Relation, create_guid
|
|
|
|
c = pyglet.lib.load_library('c')
|
|
|
|
_IOC_NRBITS = 8
|
|
_IOC_TYPEBITS = 8
|
|
_IOC_SIZEBITS = 14
|
|
_IOC_DIRBITS = 2
|
|
|
|
_IOC_NRSHIFT = 0
|
|
_IOC_TYPESHIFT = (_IOC_NRSHIFT + _IOC_NRBITS)
|
|
_IOC_SIZESHIFT = (_IOC_TYPESHIFT + _IOC_TYPEBITS)
|
|
_IOC_DIRSHIFT = (_IOC_SIZESHIFT + _IOC_SIZEBITS)
|
|
|
|
_IOC_NONE = 0
|
|
_IOC_WRITE = 1
|
|
_IOC_READ = 2
|
|
|
|
|
|
def _IOC(dir, type, nr, size):
|
|
return ((dir << _IOC_DIRSHIFT) |
|
|
(type << _IOC_TYPESHIFT) |
|
|
(nr << _IOC_NRSHIFT) |
|
|
(size << _IOC_SIZESHIFT))
|
|
|
|
|
|
def _IOR(type, nr, struct):
|
|
request = _IOC(_IOC_READ, ord(type), nr, ctypes.sizeof(struct))
|
|
|
|
def f(fileno):
|
|
buffer = struct()
|
|
fcntl.ioctl(fileno, request, buffer)
|
|
return buffer
|
|
|
|
return f
|
|
|
|
|
|
def _IOR_len(type, nr):
|
|
def f(fileno, buffer):
|
|
request = _IOC(_IOC_READ, ord(type), nr, ctypes.sizeof(buffer))
|
|
fcntl.ioctl(fileno, request, buffer)
|
|
return buffer
|
|
|
|
return f
|
|
|
|
|
|
def _IOR_str(type, nr):
|
|
g = _IOR_len(type, nr)
|
|
|
|
def f(fileno, length=256):
|
|
return g(fileno, ctypes.create_string_buffer(length)).value
|
|
|
|
return f
|
|
|
|
|
|
def _IOW(type, nr):
|
|
|
|
def f(fileno, buffer):
|
|
request = _IOC(_IOC_WRITE, ord(type), nr, ctypes.sizeof(buffer))
|
|
fcntl.ioctl(fileno, request, buffer)
|
|
|
|
return f
|
|
|
|
|
|
# Structures from /linux/blob/master/include/uapi/linux/input.h
|
|
|
|
class Timeval(ctypes.Structure):
|
|
_fields_ = (
|
|
('tv_sec', _s64),
|
|
('tv_usec', _s64)
|
|
)
|
|
|
|
|
|
class InputEvent(ctypes.Structure):
|
|
_fields_ = (
|
|
('time', Timeval),
|
|
('type', _u16),
|
|
('code', _u16),
|
|
('value', _s32)
|
|
)
|
|
|
|
|
|
class InputID(ctypes.Structure):
|
|
_fields_ = (
|
|
('bustype', _u16),
|
|
('vendor', _u16),
|
|
('product', _u16),
|
|
('version', _u16),
|
|
)
|
|
|
|
|
|
class InputABSInfo(ctypes.Structure):
|
|
_fields_ = (
|
|
('value', _s32),
|
|
('minimum', _s32),
|
|
('maximum', _s32),
|
|
('fuzz', _s32),
|
|
('flat', _s32),
|
|
)
|
|
|
|
|
|
class FFReplay(ctypes.Structure):
|
|
_fields_ = (
|
|
('length', _u16),
|
|
('delay', _u16)
|
|
)
|
|
|
|
|
|
class FFTrigger(ctypes.Structure):
|
|
_fields_ = (
|
|
('button', _u16),
|
|
('interval', _u16)
|
|
)
|
|
|
|
|
|
class FFEnvelope(ctypes.Structure):
|
|
_fields_ = [
|
|
('attack_length', _u16),
|
|
('attack_level', _u16),
|
|
('fade_length', _u16),
|
|
('fade_level', _u16),
|
|
]
|
|
|
|
|
|
class FFConstantEffect(ctypes.Structure):
|
|
_fields_ = [
|
|
('level', _s16),
|
|
('ff_envelope', FFEnvelope),
|
|
]
|
|
|
|
|
|
class FFRampEffect(ctypes.Structure):
|
|
_fields_ = [
|
|
('start_level', _s16),
|
|
('end_level', _s16),
|
|
('ff_envelope', FFEnvelope),
|
|
]
|
|
|
|
|
|
class FFConditionEffect(ctypes.Structure):
|
|
_fields_ = [
|
|
('right_saturation', _u16),
|
|
('left_saturation', _u16),
|
|
('right_coeff', _s16),
|
|
('left_coeff', _s16),
|
|
('deadband', _u16),
|
|
('center', _s16),
|
|
]
|
|
|
|
|
|
class FFPeriodicEffect(ctypes.Structure):
|
|
_fields_ = [
|
|
('waveform', _u16),
|
|
('period', _u16),
|
|
('magnitude', _s16),
|
|
('offset', _s16),
|
|
('phase', _u16),
|
|
('envelope', FFEnvelope),
|
|
('custom_len', _u32),
|
|
('custom_data', ctypes.POINTER(_s16)),
|
|
]
|
|
|
|
|
|
class FFRumbleEffect(ctypes.Structure):
|
|
_fields_ = (
|
|
('strong_magnitude', _u16),
|
|
('weak_magnitude', _u16)
|
|
)
|
|
|
|
|
|
class FFEffectType(ctypes.Union):
|
|
_fields_ = (
|
|
('ff_constant_effect', FFConstantEffect),
|
|
('ff_ramp_effect', FFRampEffect),
|
|
('ff_periodic_effect', FFPeriodicEffect),
|
|
('ff_condition_effect', FFConditionEffect * 2),
|
|
('ff_rumble_effect', FFRumbleEffect),
|
|
)
|
|
|
|
|
|
class FFEvent(ctypes.Structure):
|
|
_fields_ = (
|
|
('type', _u16),
|
|
('id', _s16),
|
|
('direction', _u16),
|
|
('ff_trigger', FFTrigger),
|
|
('ff_replay', FFReplay),
|
|
('u', FFEffectType)
|
|
)
|
|
|
|
|
|
EVIOCGVERSION = _IOR('E', 0x01, ctypes.c_int)
|
|
EVIOCGID = _IOR('E', 0x02, InputID)
|
|
EVIOCGNAME = _IOR_str('E', 0x06)
|
|
EVIOCGPHYS = _IOR_str('E', 0x07)
|
|
EVIOCGUNIQ = _IOR_str('E', 0x08)
|
|
EVIOCSFF = _IOW('E', 0x80)
|
|
|
|
|
|
def EVIOCGBIT(fileno, ev, buffer):
|
|
return _IOR_len('E', 0x20 + ev)(fileno, buffer)
|
|
|
|
|
|
def EVIOCGABS(fileno, abs):
|
|
buffer = InputABSInfo()
|
|
return _IOR_len('E', 0x40 + abs)(fileno, buffer)
|
|
|
|
|
|
def get_set_bits(bytestring):
|
|
bits = set()
|
|
j = 0
|
|
for byte in bytestring:
|
|
for i in range(8):
|
|
if byte & 1:
|
|
bits.add(j + i)
|
|
byte >>= 1
|
|
j += 8
|
|
return bits
|
|
|
|
|
|
_abs_names = {
|
|
ABS_X: AbsoluteAxis.X,
|
|
ABS_Y: AbsoluteAxis.Y,
|
|
ABS_Z: AbsoluteAxis.Z,
|
|
ABS_RX: AbsoluteAxis.RX,
|
|
ABS_RY: AbsoluteAxis.RY,
|
|
ABS_RZ: AbsoluteAxis.RZ,
|
|
ABS_HAT0X: AbsoluteAxis.HAT_X,
|
|
ABS_HAT0Y: AbsoluteAxis.HAT_Y,
|
|
}
|
|
|
|
_rel_names = {
|
|
REL_X: RelativeAxis.X,
|
|
REL_Y: RelativeAxis.Y,
|
|
REL_Z: RelativeAxis.Z,
|
|
REL_RX: RelativeAxis.RX,
|
|
REL_RY: RelativeAxis.RY,
|
|
REL_RZ: RelativeAxis.RZ,
|
|
REL_WHEEL: RelativeAxis.WHEEL,
|
|
}
|
|
|
|
|
|
def _create_control(fileno, event_type, event_code):
|
|
if event_type == EV_ABS:
|
|
raw_name = abs_raw_names.get(event_code, 'EV_ABS(%x)' % event_code)
|
|
name = _abs_names.get(event_code)
|
|
absinfo = EVIOCGABS(fileno, event_code)
|
|
value = absinfo.value
|
|
minimum = absinfo.minimum
|
|
maximum = absinfo.maximum
|
|
control = AbsoluteAxis(name, minimum, maximum, raw_name)
|
|
control.value = value
|
|
if name == 'hat_y':
|
|
control.inverted = True
|
|
elif event_type == EV_REL:
|
|
raw_name = rel_raw_names.get(event_code, 'EV_REL(%x)' % event_code)
|
|
name = _rel_names.get(event_code)
|
|
# TODO min/max?
|
|
control = RelativeAxis(name, raw_name)
|
|
elif event_type == EV_KEY:
|
|
raw_name = key_raw_names.get(event_code, 'EV_KEY(%x)' % event_code)
|
|
name = None
|
|
control = Button(name, raw_name)
|
|
else:
|
|
value = minimum = maximum = 0 # TODO
|
|
return None
|
|
control._event_type = event_type
|
|
control._event_code = event_code
|
|
return control
|
|
|
|
|
|
event_types = {
|
|
EV_KEY: KEY_MAX,
|
|
EV_REL: REL_MAX,
|
|
EV_ABS: ABS_MAX,
|
|
EV_MSC: MSC_MAX,
|
|
EV_LED: LED_MAX,
|
|
EV_SND: SND_MAX,
|
|
EV_FF: FF_MAX,
|
|
}
|
|
|
|
|
|
class EvdevDevice(XlibSelectDevice, Device):
|
|
_fileno = None
|
|
|
|
def __init__(self, display, filename):
|
|
self._filename = filename
|
|
|
|
fileno = os.open(filename, os.O_RDONLY)
|
|
|
|
self._id = EVIOCGID(fileno)
|
|
self.id_bustype = self._id.bustype
|
|
self.id_vendor = hex(self._id.vendor)
|
|
self.id_product = hex(self._id.product)
|
|
self.id_version = self._id.version
|
|
|
|
name = EVIOCGNAME(fileno)
|
|
try:
|
|
name = name.decode('utf-8')
|
|
except UnicodeDecodeError:
|
|
try:
|
|
name = name.decode('latin-1')
|
|
except UnicodeDecodeError:
|
|
pass
|
|
|
|
try:
|
|
self.phys = EVIOCGPHYS(fileno)
|
|
except OSError:
|
|
self.phys = ''
|
|
try:
|
|
self.uniq = EVIOCGUNIQ(fileno)
|
|
except OSError:
|
|
self.uniq = ''
|
|
|
|
self.controls = []
|
|
self.control_map = {}
|
|
self.ff_types = []
|
|
|
|
event_types_bits = (ctypes.c_byte * 4)()
|
|
EVIOCGBIT(fileno, 0, event_types_bits)
|
|
for event_type in get_set_bits(event_types_bits):
|
|
if event_type not in event_types:
|
|
continue
|
|
max_code = event_types[event_type]
|
|
nbytes = max_code // 8 + 1
|
|
event_codes_bits = (ctypes.c_byte * nbytes)()
|
|
EVIOCGBIT(fileno, event_type, event_codes_bits)
|
|
if event_type == EV_FF:
|
|
self.ff_types.extend(get_set_bits(event_codes_bits))
|
|
else:
|
|
for event_code in get_set_bits(event_codes_bits):
|
|
control = _create_control(fileno, event_type, event_code)
|
|
if control:
|
|
self.control_map[(event_type, event_code)] = control
|
|
self.controls.append(control)
|
|
|
|
self.controls.sort(key=lambda c: c._event_code)
|
|
os.close(fileno)
|
|
|
|
self._event_size = ctypes.sizeof(InputEvent)
|
|
self._event_buffer = (InputEvent * 64)()
|
|
|
|
super().__init__(display, name)
|
|
|
|
def get_guid(self):
|
|
"""Get the device's SDL2 style GUID string"""
|
|
_id = self._id
|
|
return create_guid(_id.bustype, _id.vendor, _id.product, _id.version, self.name, 0, 0)
|
|
|
|
def open(self, window=None, exclusive=False):
|
|
super().open(window, exclusive)
|
|
|
|
try:
|
|
self._fileno = os.open(self._filename, os.O_RDWR | os.O_NONBLOCK)
|
|
except OSError as e:
|
|
raise DeviceOpenException(e)
|
|
|
|
pyglet.app.platform_event_loop.select_devices.add(self)
|
|
|
|
def close(self):
|
|
super().close()
|
|
|
|
if not self._fileno:
|
|
return
|
|
|
|
pyglet.app.platform_event_loop.select_devices.remove(self)
|
|
os.close(self._fileno)
|
|
self._fileno = None
|
|
|
|
def get_controls(self):
|
|
return self.controls
|
|
|
|
# Force Feedback methods
|
|
|
|
def ff_upload_effect(self, structure):
|
|
os.write(self._fileno, structure)
|
|
|
|
# XlibSelectDevice interface
|
|
|
|
def fileno(self):
|
|
return self._fileno
|
|
|
|
def poll(self):
|
|
return False
|
|
|
|
def select(self):
|
|
if not self._fileno:
|
|
return
|
|
|
|
try:
|
|
bytes_read = c.read(self._fileno, self._event_buffer, self._event_size)
|
|
except OSError:
|
|
self.close()
|
|
return
|
|
|
|
n_events = bytes_read // self._event_size
|
|
|
|
for event in self._event_buffer[:n_events]:
|
|
try:
|
|
self.control_map[(event.type, event.code)].value = event.value
|
|
except KeyError:
|
|
pass
|
|
|
|
|
|
class FFController(Controller):
|
|
"""Controller that supports force-feedback"""
|
|
_fileno = None
|
|
_weak_effect = None
|
|
_play_weak_event = None
|
|
_stop_weak_event = None
|
|
_strong_effect = None
|
|
_play_strong_event = None
|
|
_stop_strong_event = None
|
|
|
|
def open(self, window=None, exclusive=False):
|
|
super().open(window, exclusive)
|
|
self._fileno = self.device.fileno()
|
|
# Create Force Feedback effects & events when opened:
|
|
# https://www.kernel.org/doc/html/latest/input/ff.html
|
|
self._weak_effect = FFEvent(FF_RUMBLE, -1)
|
|
EVIOCSFF(self._fileno, self._weak_effect)
|
|
self._play_weak_event = InputEvent(Timeval(), EV_FF, self._weak_effect.id, 1)
|
|
self._stop_weak_event = InputEvent(Timeval(), EV_FF, self._weak_effect.id, 0)
|
|
|
|
self._strong_effect = FFEvent(FF_RUMBLE, -1)
|
|
EVIOCSFF(self._fileno, self._strong_effect)
|
|
self._play_strong_event = InputEvent(Timeval(), EV_FF, self._strong_effect.id, 1)
|
|
self._stop_strong_event = InputEvent(Timeval(), EV_FF, self._strong_effect.id, 0)
|
|
|
|
def rumble_play_weak(self, strength=1.0, duration=0.5):
|
|
effect = self._weak_effect
|
|
effect.u.ff_rumble_effect.weak_magnitude = int(max(min(1.0, strength), 0) * 0xFFFF)
|
|
effect.ff_replay.length = int(duration * 1000)
|
|
EVIOCSFF(self._fileno, effect)
|
|
self.device.ff_upload_effect(self._play_weak_event)
|
|
|
|
def rumble_play_strong(self, strength=1.0, duration=0.5):
|
|
effect = self._strong_effect
|
|
effect.u.ff_rumble_effect.strong_magnitude = int(max(min(1.0, strength), 0) * 0xFFFF)
|
|
effect.ff_replay.length = int(duration * 1000)
|
|
EVIOCSFF(self._fileno, effect)
|
|
self.device.ff_upload_effect(self._play_strong_event)
|
|
|
|
def rumble_stop_weak(self):
|
|
self.device.ff_upload_effect(self._stop_weak_event)
|
|
|
|
def rumble_stop_strong(self):
|
|
self.device.ff_upload_effect(self._stop_strong_event)
|
|
|
|
|
|
class EvdevControllerManager(ControllerManager, XlibSelectDevice):
|
|
|
|
def __init__(self, display=None):
|
|
super().__init__()
|
|
self._display = display
|
|
self._devices_file = open('/proc/bus/input/devices')
|
|
self._device_names = self._get_device_names()
|
|
self._controllers = {}
|
|
self._thread_pool = ThreadPoolExecutor(max_workers=1)
|
|
|
|
for name in self._device_names:
|
|
path = os.path.join('/dev/input', name)
|
|
try:
|
|
device = EvdevDevice(self._display, path)
|
|
except OSError:
|
|
continue
|
|
controller = _create_controller(device)
|
|
if controller:
|
|
self._controllers[name] = controller
|
|
|
|
pyglet.app.platform_event_loop.select_devices.add(self)
|
|
|
|
def __del__(self):
|
|
self._devices_file.close()
|
|
|
|
def fileno(self):
|
|
"""Allow this class to be Selectable"""
|
|
return self._devices_file.fileno()
|
|
|
|
@staticmethod
|
|
def _get_device_names():
|
|
return {name for name in os.listdir('/dev/input') if name.startswith('event')}
|
|
|
|
def _make_device_callback(self, future):
|
|
name, device = future.result()
|
|
if not device:
|
|
return
|
|
|
|
if name in self._controllers:
|
|
controller = self._controllers.get(name)
|
|
else:
|
|
controller = _create_controller(device)
|
|
self._controllers[name] = controller
|
|
|
|
if controller:
|
|
# Dispatch event in main thread:
|
|
pyglet.app.platform_event_loop.post_event(self, 'on_connect', controller)
|
|
|
|
def _make_device(self, name, count=1):
|
|
path = os.path.join('/dev/input', name)
|
|
while count > 0:
|
|
try:
|
|
return name, EvdevDevice(self._display, path)
|
|
except OSError:
|
|
if count > 0:
|
|
time.sleep(0.1)
|
|
count -= 1
|
|
return None, None
|
|
|
|
def select(self):
|
|
"""Triggered whenever the devices_file changes."""
|
|
new_device_files = self._get_device_names()
|
|
appeared = new_device_files - self._device_names
|
|
disappeared = self._device_names - new_device_files
|
|
self._device_names = new_device_files
|
|
|
|
for name in appeared:
|
|
future = self._thread_pool.submit(self._make_device, name, count=10)
|
|
future.add_done_callback(self._make_device_callback)
|
|
|
|
for name in disappeared:
|
|
controller = self._controllers.get(name)
|
|
if controller:
|
|
self.dispatch_event('on_disconnect', controller)
|
|
|
|
def get_controllers(self) -> List[Controller]:
|
|
return list(self._controllers.values())
|
|
|
|
|
|
def get_devices(display=None):
|
|
_devices = {}
|
|
base = '/dev/input'
|
|
for filename in os.listdir(base):
|
|
if filename.startswith('event'):
|
|
path = os.path.join(base, filename)
|
|
if path in _devices:
|
|
continue
|
|
|
|
try:
|
|
_devices[path] = EvdevDevice(display, path)
|
|
except OSError:
|
|
pass
|
|
|
|
return list(_devices.values())
|
|
|
|
|
|
def _create_joystick(device):
|
|
# Look for something with an ABS X and ABS Y axis, and a joystick 0 button
|
|
have_x = False
|
|
have_y = False
|
|
have_button = False
|
|
for control in device.controls:
|
|
if control._event_type == EV_ABS and control._event_code == ABS_X:
|
|
have_x = True
|
|
elif control._event_type == EV_ABS and control._event_code == ABS_Y:
|
|
have_y = True
|
|
elif control._event_type == EV_KEY and control._event_code in (BTN_JOYSTICK, BTN_GAMEPAD):
|
|
have_button = True
|
|
if not (have_x and have_y and have_button):
|
|
return
|
|
|
|
return Joystick(device)
|
|
|
|
|
|
def get_joysticks(display=None):
|
|
return [joystick for joystick in
|
|
[_create_joystick(device) for device in get_devices(display)]
|
|
if joystick is not None]
|
|
|
|
|
|
def _detect_controller_mapping(device):
|
|
# If no explicit mapping is available, we can
|
|
# detect it from the Linux gamepad specification:
|
|
# https://www.kernel.org/doc/html/v4.13/input/gamepad.html
|
|
# Note: legacy device drivers don't always adhere to this.
|
|
mapping = dict(guid=device.get_guid(), name=device.name)
|
|
|
|
_aliases = {BTN_MODE: 'guide', BTN_SELECT: 'back', BTN_START: 'start',
|
|
BTN_SOUTH: 'a', BTN_EAST: 'b', BTN_WEST: 'x', BTN_NORTH: 'y',
|
|
BTN_TL: 'leftshoulder', BTN_TR: 'rightshoulder',
|
|
BTN_TL2: 'lefttrigger', BTN_TR2: 'righttrigger',
|
|
BTN_THUMBL: 'leftstick', BTN_THUMBR: 'rightstick',
|
|
BTN_DPAD_UP: 'dpup', BTN_DPAD_DOWN: 'dpdown',
|
|
BTN_DPAD_LEFT: 'dpleft', BTN_DPAD_RIGHT: 'dpright',
|
|
|
|
ABS_HAT0X: 'dpleft', # 'dpright',
|
|
ABS_HAT0Y: 'dpup', # 'dpdown',
|
|
ABS_Z: 'lefttrigger', ABS_RZ: 'righttrigger',
|
|
ABS_X: 'leftx', ABS_Y: 'lefty', ABS_RX: 'rightx', ABS_RY: 'righty'}
|
|
|
|
button_controls = [control for control in device.controls if isinstance(control, Button)]
|
|
axis_controls = [control for control in device.controls if isinstance(control, AbsoluteAxis)]
|
|
hat_controls = [control for control in device.controls if control.name in ('hat_x', 'hat_y')]
|
|
|
|
for i, control in enumerate(button_controls):
|
|
name = _aliases.get(control._event_code)
|
|
if name:
|
|
mapping[name] = Relation('button', i)
|
|
|
|
for i, control in enumerate(axis_controls):
|
|
name = _aliases.get(control._event_code)
|
|
if name:
|
|
mapping[name] = Relation('axis', i)
|
|
|
|
for i, control in enumerate(hat_controls):
|
|
name = _aliases.get(control._event_code)
|
|
if name:
|
|
index = 1 + i << 1
|
|
mapping[name] = Relation('hat0', index)
|
|
|
|
return mapping
|
|
|
|
|
|
def _create_controller(device):
|
|
for control in device.controls:
|
|
if control._event_type == EV_KEY and control._event_code == BTN_GAMEPAD:
|
|
break
|
|
else:
|
|
return None # Game Controllers must have a BTN_GAMEPAD
|
|
|
|
mapping = get_mapping(device.get_guid())
|
|
if not mapping:
|
|
warnings.warn(f"Warning: {device} (GUID: {device.get_guid()}) "
|
|
f"has no controller mappings. Update the mappings in the Controller DB.\n"
|
|
f"Auto-detecting as defined by the 'Linux gamepad specification'")
|
|
mapping = _detect_controller_mapping(device)
|
|
|
|
if FF_RUMBLE in device.ff_types:
|
|
return FFController(device, mapping)
|
|
else:
|
|
return Controller(device, mapping)
|
|
|
|
|
|
def get_controllers(display=None):
|
|
return [controller for controller in
|
|
[_create_controller(device) for device in get_devices(display)] if controller]
|