improved gamepad code, more tests

flatpak
sezanzeb 3 years ago
parent 244bfc87dc
commit 49e165acbd

@ -34,6 +34,7 @@ from keymapper.logger import logger
MOUSE = 'mouse'
WHEEL = 'wheel'
BUTTONS = 'buttons'
NONE = 'none'
INITIAL_CONFIG = {
'autoload': {},

@ -47,7 +47,7 @@ def abs_max(value_1, value_2):
class EventProducer:
"""Keeps producing events at 60hz if needed.
Can debounce writes or map joysticks to mouse movements.
Can debounce arbitrary functions. Maps joysticks to mouse movements.
This class does not handle injecting macro stuff over time, that is done
by the keycode_mapper.
@ -140,6 +140,8 @@ class EventProducer:
This information is needed for abs -> rel mapping.
"""
if device is None:
# I don't think this ever happened
logger.error('Expected device to not be None')
return
max_abs = utils.get_max_abs(device)

@ -32,13 +32,14 @@ import evdev
from evdev.ecodes import EV_KEY, EV_REL
from keymapper.logger import logger
from keymapper.getdevices import get_devices, map_abs_to_rel
from keymapper.getdevices import get_devices, is_gamepad
from keymapper.dev.keycode_mapper import handle_keycode
from keymapper.dev import utils
from keymapper.dev.event_producer import EventProducer
from keymapper.dev.macros import parse, is_this_a_macro
from keymapper.state import system_mapping
from keymapper.mapping import DISABLE_CODE
from keymapper.config import NONE, MOUSE, WHEEL
DEV_NAME = 'key-mapper'
@ -58,9 +59,6 @@ STOPPED = 5
NO_GRAB = 6
# TODO joystick unmodified? forward abs_rel and abs_rel in capabilities
def is_numlock_on():
"""Get the current state of the numlock."""
try:
@ -150,13 +148,35 @@ class Injector:
mapping : Mapping
"""
self.device = device
self.mapping = mapping
self._process = None
self._msg_pipe = multiprocessing.Pipe()
self._key_to_code = self._map_keys_to_codes()
self._state = UNKNOWN
self._event_producer = None
def _forwards_joystick(self):
"""If at least one of the joysticks remains a regular joystick."""
left_purpose = self.mapping.get('gamepad.joystick.left_purpose')
right_purpose = self.mapping.get('gamepad.joystick.right_purpose')
return NONE in (left_purpose, right_purpose)
def _maps_joystick(self):
"""If at least one of the joysticks will serve a special purpose."""
left_purpose = self.mapping.get('gamepad.joystick.left_purpose')
right_purpose = self.mapping.get('gamepad.joystick.right_purpose')
return (left_purpose, right_purpose) != (NONE, NONE)
def _joystick_as_mouse(self):
"""If at least one joystick maps to an EV_REL capability."""
purposes = (
self.mapping.get('gamepad.joystick.left_purpose'),
self.mapping.get('gamepad.joystick.right_purpose')
)
return MOUSE in purposes or WHEEL in purposes
def _map_keys_to_codes(self):
"""To quickly get target keycodes during operation.
@ -219,17 +239,13 @@ class Injector:
return self._state
def _prepare_device(self, path):
"""Try to grab the device, return if not needed/possible.
Also return if ABS events are changed to REL mouse movements,
because the capabilities of the returned device are changed
so this cannot be checked later anymore.
"""
def _grab_device(self, path):
"""Try to grab the device, return None if not needed/possible."""
try:
device = evdev.InputDevice(path)
except FileNotFoundError:
return None, False
logger.error('Could not find "%s"', path)
return None
capabilities = device.capabilities(absinfo=False)
@ -239,16 +255,16 @@ class Injector:
needed = True
break
abs_to_rel = map_abs_to_rel(capabilities)
gamepad = is_gamepad(device)
if abs_to_rel:
if gamepad and self._maps_joystick():
needed = True
if not needed:
# skipping reading and checking on events from those devices
# may be beneficial for performance.
logger.debug('No need to grab %s', path)
return None, False
return None
attempts = 0
while True:
@ -270,13 +286,13 @@ class Injector:
if attempts >= 4:
logger.error('Cannot grab %s, it is possibly in use', path)
logger.error(str(error))
return None, False
return None
time.sleep(self.regrab_timeout)
return device, abs_to_rel
return device
def _modify_capabilities(self, macros, input_device, abs_to_rel):
def _modify_capabilities(self, macros, input_device, gamepad):
"""Adds all used keycodes into a copy of a devices capabilities.
Sometimes capabilities are a bit tricky and change how the system
@ -287,7 +303,7 @@ class Injector:
macros : dict
mapping of int to _Macro
input_device : evdev.InputDevice
abs_to_rel : bool
gamepad : bool
if ABS capabilities should be removed in favor of REL
"""
ecodes = evdev.ecodes
@ -311,7 +327,7 @@ class Injector:
for macro in macros.values():
capabilities[EV_KEY] += list(macro.get_capabilities())
if abs_to_rel:
if gamepad and self._joystick_as_mouse():
# REL_WHEEL was also required to recognize the gamepad
# as mouse, even if no joystick is used as wheel.
capabilities[EV_REL] = [
@ -334,15 +350,18 @@ class Injector:
del capabilities[ecodes.EV_SYN]
if ecodes.EV_FF in capabilities:
del capabilities[ecodes.EV_FF]
if ecodes.EV_ABS in capabilities:
# EV_KEY events are ignoerd by the os when EV_ABS capabilities
# are present
if gamepad and not self._forwards_joystick():
# key input to text inputs and such only works without ABS
# events in the capabilities, possibly due to some intentional
# constraints in wayland/X. So if the joysticks are not used
# as joysticks remove ABS.
del capabilities[ecodes.EV_ABS]
return capabilities
async def _msg_listener(self, loop):
async def _msg_listener(self):
"""Wait for messages from the main process to do special stuff."""
loop = asyncio.get_event_loop()
while True:
frame_available = asyncio.Event()
loop.add_reader(self._msg_pipe[0].fileno(), frame_available.set)
@ -374,7 +393,6 @@ class Injector:
numlock_state = is_numlock_on()
loop = asyncio.get_event_loop()
coroutines = []
logger.info('Starting injecting the mapping for "%s"', self.device)
@ -385,7 +403,7 @@ class Injector:
# Watch over each one of the potentially multiple devices per hardware
for path in paths:
source, abs_to_rel = self._prepare_device(path)
source = self._grab_device(path)
if source is None:
# this path doesn't need to be grabbed for injection, because
# it doesn't provide the events needed to execute the mapping
@ -409,10 +427,11 @@ class Injector:
# certain capabilities can have side effects apparently. with an
# EV_ABS capability, EV_REL won't move the mouse pointer anymore.
# so don't merge all InputDevices into one UInput device.
gamepad = is_gamepad(source)
uinput = evdev.UInput(
name=f'{DEV_NAME} {self.device}',
phys=DEV_NAME,
events=self._modify_capabilities(macros, source, abs_to_rel)
events=self._modify_capabilities(macros, source, gamepad)
)
logger.spam(
@ -429,16 +448,12 @@ class Injector:
macro.set_handler(handler)
# actual reading of events
coroutines.append(self._event_consumer(
macros,
source,
uinput
))
coroutines.append(self._event_consumer(macros, source, uinput))
# The event source of the current iteration will deliver events
# that are needed for this. It is that one that will be mapped
# to a mouse-like devnode.
if abs_to_rel:
if gamepad and self._joystick_as_mouse():
self._event_producer.set_max_abs_from(source)
self._event_producer.set_mouse_uinput(uinput)
@ -447,7 +462,7 @@ class Injector:
self._msg_pipe[0].send(NO_GRAB)
return
coroutines.append(self._msg_listener(loop))
coroutines.append(self._msg_listener())
# run besides this stuff
coroutines.append(self._event_producer.run())
@ -536,6 +551,7 @@ class Injector:
continue
# forward the rest
# TODO triggers should retain their original value if not mapped
uinput.write(event.type, event.code, event.value)
# this already includes SYN events, so need to syn here again

@ -28,7 +28,7 @@ import time
import asyncio
import evdev
from evdev.ecodes import EV_KEY, EV_ABS
from evdev.ecodes import EV_KEY, EV_ABS, KEY_CAMERA
from keymapper.logger import logger
@ -45,13 +45,14 @@ if not hasattr(evdev.InputDevice, 'path'):
evdev.InputDevice.path = path
def map_abs_to_rel(capabilities):
"""Check if joystick movements can and should be mapped."""
# mapping buttons only works without ABS events in the capabilities,
# possibly due to some intentional constraints in the os. So always
# just map those events to REL if possible and remove ABS from
# the capabilities, because ABS events prevent regular button
# mappings from working,
def is_gamepad(device):
"""Check if joystick movements are available for mapping.
Parameters
----------
device : InputDevice
"""
capabilities = device.capabilities(absinfo=False)
abs_capabilities = capabilities.get(EV_ABS)
if abs_capabilities is not None:
if evdev.ecodes.ABS_MT_TRACKING_ID in abs_capabilities:
@ -107,14 +108,20 @@ class _GetDevices(threading.Thread):
if device.name == 'Power Button':
continue
# only keyboard devices
gamepad = is_gamepad(device)
# https://www.kernel.org/doc/html/latest/input/event-codes.html
capabilities = device.capabilities(absinfo=False)
if EV_KEY not in capabilities and EV_ABS not in capabilities:
# or gamepads, because they can be mapped like a keyboard
key_capa = capabilities.get(EV_KEY)
if key_capa is None and not gamepad:
# skip devices that don't provide buttons that can be mapped
continue
is_gamepad = map_abs_to_rel(capabilities)
if len(key_capa) == 1 and key_capa[0] == KEY_CAMERA:
# skip cameras
continue
name = device.name
path = device.path
@ -129,31 +136,31 @@ class _GetDevices(threading.Thread):
if grouped.get(info) is None:
grouped[info] = []
logger.spam('Found "%s", "%s", "%s"', info, path, name)
logger.spam(
'Found "%s", "%s", "%s" %s',
info, path, name, '(gamepad)' if gamepad else ''
)
grouped[info].append((name, path, is_gamepad))
grouped[info].append((name, path, gamepad))
# now write down all the paths of that group
result = {}
for group in grouped.values():
names = [entry[0] for entry in group]
devs = [entry[1] for entry in group]
is_gamepad = True in [entry[2] for entry in group]
gamepad = True in [entry[2] for entry in group]
shortest_name = sorted(names, key=len)[0]
result[shortest_name] = {
'paths': devs,
'devices': names,
'gamepad': is_gamepad
'gamepad': gamepad
}
self.pipe.send(result)
def refresh_devices():
"""Get new devices, e.g. new ones created by key-mapper.
This should be called whenever devices in /dev are added or removed.
"""
"""This can be called to discover new devices."""
# it may take a little bit of time until devices are visible after
# changes
time.sleep(0.1)
@ -185,11 +192,7 @@ def get_devices(include_keymapper=False):
# block until devices are available
_devices = pipe[0].recv()
if len(_devices) == 0:
logger.error(
'Did not find any device. If you added yourself to the '
'needed groups (see `ls -l /dev/input`) already, make sure '
'you also logged out and back in.'
)
logger.error('Did not find any input device')
else:
names = [f'"{name}"' for name in _devices]
logger.info('Found %s', ', '.join(names))

@ -327,7 +327,6 @@ class UInput:
self.device = InputDevice('justdoit')
self.name = name
self.events = events
pass
def capabilities(self, *args, **kwargs):
return self.events

@ -23,7 +23,7 @@ import unittest
import evdev
from keymapper.getdevices import _GetDevices, get_devices, map_abs_to_rel
from keymapper.getdevices import _GetDevices, get_devices, is_gamepad
class FakePipe:
@ -96,35 +96,43 @@ class TestGetDevices(unittest.TestCase):
},
})
def test_map_abs_to_rel(self):
def test_is_gamepad(self):
# properly detects if the device is a gamepad
EV_ABS = evdev.ecodes.EV_ABS
EV_KEY = evdev.ecodes.EV_KEY
self.assertTrue(map_abs_to_rel({
class FakeDevice:
def __init__(self, capabilities):
self.c = capabilities
def capabilities(self, absinfo):
assert not absinfo
return self.c
self.assertTrue(is_gamepad(FakeDevice({
EV_ABS: [evdev.ecodes.ABS_X]
}))
self.assertTrue(map_abs_to_rel({
})))
self.assertTrue(is_gamepad(FakeDevice({
EV_ABS: [evdev.ecodes.ABS_X, evdev.ecodes.ABS_RY],
EV_KEY: [evdev.ecodes.KEY_A]
}))
self.assertFalse(map_abs_to_rel({
})))
self.assertFalse(is_gamepad(FakeDevice({
EV_ABS: [evdev.ecodes.ABS_X, evdev.ecodes.ABS_MT_TRACKING_ID]
}))
self.assertFalse(map_abs_to_rel({
})))
self.assertFalse(is_gamepad(FakeDevice({
EV_ABS: [evdev.ecodes.ABS_HAT2X]
}))
self.assertFalse(map_abs_to_rel({
})))
self.assertFalse(is_gamepad(FakeDevice({
EV_KEY: [evdev.ecodes.ABS_X] # intentionally ABS_X (0) on EV_KEY
}))
self.assertFalse(map_abs_to_rel({
})))
self.assertFalse(is_gamepad(FakeDevice({
EV_ABS: [evdev.ecodes.ABS_X],
EV_KEY: [evdev.ecodes.BTN_TOOL_BRUSH]
}))
self.assertFalse(map_abs_to_rel({
})))
self.assertFalse(is_gamepad(FakeDevice({
EV_ABS: [evdev.ecodes.ABS_X],
EV_KEY: [evdev.ecodes.BTN_STYLUS]
}))
})))
if __name__ == "__main__":

@ -25,18 +25,18 @@ import copy
import evdev
from evdev.ecodes import EV_REL, EV_KEY, EV_ABS, ABS_HAT0X, BTN_LEFT, KEY_A, \
REL_X, REL_Y, REL_WHEEL, REL_HWHEEL
REL_X, REL_Y, REL_WHEEL, REL_HWHEEL, BTN_A, ABS_X, ABS_Y
from keymapper.dev.injector import is_numlock_on, set_numlock, \
ensure_numlock, Injector, is_in_capabilities, \
STARTING, RUNNING, STOPPED, NO_GRAB, UNKNOWN
from keymapper.state import custom_mapping, system_mapping
from keymapper.mapping import Mapping, DISABLE_CODE, DISABLE_NAME
from keymapper.config import config
from keymapper.config import config, NONE, MOUSE, WHEEL, BUTTONS
from keymapper.key import Key
from keymapper.dev.macros import parse
from keymapper.dev import utils
from keymapper.getdevices import get_devices
from keymapper.getdevices import get_devices, is_gamepad
from tests.test import new_event, pending_events, fixtures, \
EVENT_READ_TIMEOUT, uinput_write_history_pipe, \
@ -76,71 +76,6 @@ class TestInjector(unittest.TestCase):
cleanup()
def test_modify_capabilities(self):
class FakeDevice:
def capabilities(self, absinfo=True):
assert absinfo is False
return {
evdev.ecodes.EV_SYN: [1, 2, 3],
evdev.ecodes.EV_FF: [1, 2, 3],
evdev.ecodes.EV_ABS: [1, 2, 3]
}
mapping = Mapping()
mapping.change(Key(EV_KEY, 80, 1), 'a')
mapping.change(Key(EV_KEY, 81, 1), DISABLE_NAME)
macro_code = 'r(2, m(sHiFt_l, r(2, k(1).k(2))))'
macro = parse(macro_code, mapping)
mapping.change(Key(EV_KEY, 60, 111), macro_code)
# going to be ignored, because EV_REL cannot be mapped, that's
# mouse movements.
mapping.change(Key(EV_REL, 1234, 3), 'b')
a = system_mapping.get('a')
shift_l = system_mapping.get('ShIfT_L')
one = system_mapping.get(1)
two = system_mapping.get('2')
btn_left = system_mapping.get('BtN_lEfT')
self.injector = Injector('foo', mapping)
fake_device = FakeDevice()
capabilities_1 = self.injector._modify_capabilities(
{60: macro},
fake_device,
abs_to_rel=False
)
self.assertIn(EV_KEY, capabilities_1)
keys = capabilities_1[EV_KEY]
self.assertIn(a, keys)
self.assertIn(one, keys)
self.assertIn(two, keys)
self.assertIn(shift_l, keys)
self.assertNotIn(DISABLE_CODE, keys)
# abs_to_rel is false, so mouse capabilities are not needed
self.assertNotIn(btn_left, keys)
self.assertNotIn(evdev.ecodes.EV_SYN, capabilities_1)
self.assertNotIn(evdev.ecodes.EV_FF, capabilities_1)
self.assertNotIn(evdev.ecodes.EV_REL, capabilities_1)
self.assertNotIn(evdev.ecodes.EV_ABS, capabilities_1)
# abs_to_rel makes sure that BTN_LEFT is present
capabilities_2 = self.injector._modify_capabilities(
{60: macro},
fake_device,
abs_to_rel=True
)
keys = capabilities_2[EV_KEY]
self.assertIn(a, keys)
self.assertIn(one, keys)
self.assertIn(two, keys)
self.assertIn(shift_l, keys)
self.assertIn(btn_left, keys)
def test_grab(self):
# path is from the fixtures
custom_mapping.change(Key(EV_KEY, 10, 1), 'a')
@ -148,8 +83,9 @@ class TestInjector(unittest.TestCase):
self.injector = Injector('device 1', custom_mapping)
path = '/dev/input/event10'
# this test needs to pass around all other constraints of
# _prepare_device
device, abs_to_rel = self.injector._prepare_device(path)
# _grab_device
device = self.injector._grab_device(path)
abs_to_rel = is_gamepad(device)
self.assertFalse(abs_to_rel)
self.assertEqual(self.failed, 2)
# success on the third try
@ -161,10 +97,9 @@ class TestInjector(unittest.TestCase):
self.injector = Injector('device 1', custom_mapping)
path = '/dev/input/event10'
device, abs_to_rel = self.injector._prepare_device(path)
self.assertFalse(abs_to_rel)
self.assertGreaterEqual(self.failed, 1)
device = self.injector._grab_device(path)
self.assertIsNone(device)
self.assertGreaterEqual(self.failed, 1)
self.assertEqual(self.injector.get_state(), UNKNOWN)
self.injector.start_injecting()
@ -175,27 +110,29 @@ class TestInjector(unittest.TestCase):
self.assertFalse(self.injector._process.is_alive())
self.assertEqual(self.injector.get_state(), NO_GRAB)
def test_prepare_device_1(self):
def test_grab_device_1(self):
# according to the fixtures, /dev/input/event30 can do ABS_HAT0X
custom_mapping.change(Key(EV_ABS, ABS_HAT0X, 1), 'a')
self.injector = Injector('foobar', custom_mapping)
_prepare_device = self.injector._prepare_device
self.assertIsNone(_prepare_device('/dev/input/event10')[0])
self.assertIsNotNone(_prepare_device('/dev/input/event30')[0])
_grab_device = self.injector._grab_device
self.assertIsNone(_grab_device('/dev/input/event10'))
self.assertIsNotNone(_grab_device('/dev/input/event30'))
def test_prepare_device_non_existing(self):
def test_grab_device_non_existing(self):
custom_mapping.change(Key(EV_ABS, ABS_HAT0X, 1), 'a')
self.injector = Injector('foobar', custom_mapping)
_prepare_device = self.injector._prepare_device
self.assertIsNone(_prepare_device('/dev/input/event1234')[0])
_grab_device = self.injector._grab_device
self.assertIsNone(_grab_device('/dev/input/event1234'))
def test_gamepad_capabilities(self):
self.injector = Injector('gamepad', custom_mapping)
path = '/dev/input/event30'
device, abs_to_rel = self.injector._prepare_device(path)
device = self.injector._grab_device(path)
abs_to_rel = is_gamepad(device)
self.assertIsNotNone(device)
self.assertTrue(abs_to_rel)
capabilities = self.injector._modify_capabilities(
@ -214,6 +151,65 @@ class TestInjector(unittest.TestCase):
self.assertIn(EV_KEY, capabilities)
self.assertIn(evdev.ecodes.BTN_LEFT, capabilities[EV_KEY])
def test_gamepad_purpose_none(self):
# forward abs joystick events
custom_mapping.set('gamepad.joystick.left_purpose', NONE)
config.set('gamepad.joystick.right_purpose', NONE)
self.injector = Injector('gamepad', custom_mapping)
path = '/dev/input/event30'
device = self.injector._grab_device(path)
self.assertIsNone(device) # no capability is used, so it won't grab
custom_mapping.change(Key(EV_KEY, BTN_A, 1), 'a')
device = self.injector._grab_device(path)
self.assertIsNotNone(device)
gamepad = is_gamepad(device)
self.assertTrue(gamepad)
capabilities = self.injector._modify_capabilities(
{},
device,
gamepad
)
self.assertIn(evdev.ecodes.EV_ABS, capabilities)
def test_gamepad_purpose_none_2(self):
# forward abs joystick events for the left joystick only
custom_mapping.set('gamepad.joystick.left_purpose', NONE)
config.set('gamepad.joystick.right_purpose', MOUSE)
self.injector = Injector('gamepad', custom_mapping)
path = '/dev/input/event30'
device = self.injector._grab_device(path)
# the right joystick maps as mouse, so it is grabbed
# even with an empty mapping
self.assertIsNotNone(device)
gamepad = is_gamepad(device)
self.assertTrue(gamepad)
capabilities = self.injector._modify_capabilities(
{},
device,
gamepad
)
self.assertIn(evdev.ecodes.EV_ABS, capabilities)
self.assertIn(evdev.ecodes.EV_REL, capabilities)
custom_mapping.change(Key(EV_KEY, BTN_A, 1), 'a')
device = self.injector._grab_device(path)
gamepad = is_gamepad(device)
self.assertIsNotNone(device)
self.assertTrue(gamepad)
capabilities = self.injector._modify_capabilities(
{},
device,
gamepad
)
self.assertIn(evdev.ecodes.EV_ABS, capabilities)
self.assertIn(evdev.ecodes.EV_REL, capabilities)
self.assertIn(evdev.ecodes.EV_KEY, capabilities)
def test_adds_ev_key(self):
# for some reason, having any EV_KEY capability is needed to
# be able to control the mouse. it probably wants the mouse click.
@ -228,10 +224,11 @@ class TestInjector(unittest.TestCase):
'capabilities': gamepad_template['capabilities']
}
del fixtures[path]['capabilities'][EV_KEY]
device, abs_to_rel = self.injector._prepare_device(path)
device = self.injector._grab_device(path)
gamepad = is_gamepad(device)
self.assertNotIn(EV_KEY, device.capabilities())
capabilities = self.injector._modify_capabilities(
{}, device, abs_to_rel
{}, device, gamepad
)
self.assertIn(EV_KEY, capabilities)
self.assertIn(evdev.ecodes.BTN_MOUSE, capabilities[EV_KEY])
@ -246,9 +243,10 @@ class TestInjector(unittest.TestCase):
}
fixtures[path]['capabilities'][EV_KEY].append(BTN_LEFT)
fixtures[path]['capabilities'][EV_KEY].append(KEY_A)
device, abs_to_rel = self.injector._prepare_device(path)
device = self.injector._grab_device(path)
gamepad = is_gamepad(device)
capabilities = self.injector._modify_capabilities(
{}, device, abs_to_rel
{}, device, gamepad
)
self.assertIn(EV_KEY, capabilities)
self.assertIn(evdev.ecodes.BTN_MOUSE, capabilities[EV_KEY])
@ -257,11 +255,12 @@ class TestInjector(unittest.TestCase):
"""gamepad with existing key capabilities, but not btn_mouse"""
path = '/dev/input/event30'
device, abs_to_rel = self.injector._prepare_device(path)
device = self.injector._grab_device(path)
gamepad = is_gamepad(device)
self.assertIn(EV_KEY, device.capabilities())
self.assertNotIn(evdev.ecodes.BTN_MOUSE, device.capabilities()[EV_KEY])
capabilities = self.injector._modify_capabilities(
{}, device, abs_to_rel
{}, device, gamepad
)
self.assertIn(EV_KEY, capabilities)
self.assertGreater(len(capabilities), 1)
@ -272,16 +271,15 @@ class TestInjector(unittest.TestCase):
custom_mapping.change(Key(EV_KEY, 10, 1), 'a')
self.injector = Injector('device 1', custom_mapping)
path = '/dev/input/event11'
device, abs_to_rel = self.injector._prepare_device(path)
self.assertFalse(abs_to_rel)
self.assertEqual(self.failed, 0)
device = self.injector._grab_device(path)
self.assertIsNone(device)
self.assertEqual(self.failed, 0)
def test_skip_unknown_device(self):
# skips a device because its capabilities are not used in the mapping
self.injector = Injector('device 1', custom_mapping)
path = '/dev/input/event11'
device, _ = self.injector._prepare_device(path)
device = self.injector._grab_device(path)
# make sure the test uses a fixture without interesting capabilities
capabilities = evdev.InputDevice(path).capabilities()
@ -316,7 +314,7 @@ class TestInjector(unittest.TestCase):
set_numlock(before)
self.assertEqual(before, is_numlock_on())
def test_abs_to_rel(self):
def test_gamepad_to_mouse(self):
# maps gamepad joystick events to mouse events
config.set('gamepad.joystick.non_linearity', 1)
pointer_speed = 80
@ -327,10 +325,10 @@ class TestInjector(unittest.TestCase):
x = MAX_ABS / pointer_speed / divisor
y = MAX_ABS / pointer_speed / divisor
pending_events['gamepad'] = [
new_event(EV_ABS, REL_X, x),
new_event(EV_ABS, REL_Y, y),
new_event(EV_ABS, REL_X, -x),
new_event(EV_ABS, REL_Y, -y),
new_event(EV_ABS, ABS_X, x),
new_event(EV_ABS, ABS_Y, y),
new_event(EV_ABS, ABS_X, -x),
new_event(EV_ABS, ABS_Y, -y),
]
self.injector = Injector('gamepad', custom_mapping)
@ -344,10 +342,7 @@ class TestInjector(unittest.TestCase):
time.sleep(sleep)
# convert the write history to some easier to manage list
history = []
while uinput_write_history_pipe[0].poll():
event = uinput_write_history_pipe[0].recv()
history.append((event.type, event.code, event.value))
history = read_write_history_pipe()
if history[0][0] == EV_ABS:
raise AssertionError(
@ -368,6 +363,72 @@ class TestInjector(unittest.TestCase):
# only those two types of events were written
self.assertEqual(len(history), count_x + count_y)
def test_gamepad_forward_joysticks(self):
pending_events['gamepad'] = [
# should forward them unmodified
new_event(EV_ABS, ABS_X, 10),
new_event(EV_ABS, ABS_Y, 20),
new_event(EV_ABS, ABS_X, -30),
new_event(EV_ABS, ABS_Y, -40),
new_event(EV_KEY, BTN_A, 1),
new_event(EV_KEY, BTN_A, 0)
] * 2
custom_mapping.set('gamepad.joystick.left_purpose', NONE)
custom_mapping.set('gamepad.joystick.right_purpose', NONE)
# BTN_A -> 77
custom_mapping.change(Key((1, BTN_A, 1)), 'b')
system_mapping._set('b', 77)
self.injector = Injector('gamepad', custom_mapping)
self.injector.start_injecting()
# wait for the injector to start sending, at most 1s
uinput_write_history_pipe[0].poll(1)
time.sleep(0.2)
# convert the write history to some easier to manage list
history = read_write_history_pipe()
self.assertEqual(history.count((EV_ABS, ABS_X, 10)), 2)
self.assertEqual(history.count((EV_ABS, ABS_Y, 20)), 2)
self.assertEqual(history.count((EV_ABS, ABS_X, -30)), 2)
self.assertEqual(history.count((EV_ABS, ABS_Y, -40)), 2)
self.assertEqual(history.count((EV_KEY, 77, 1)), 2)
self.assertEqual(history.count((EV_KEY, 77, 0)), 2)
def test_gamepad_to_mouse_event_producer(self):
custom_mapping.set('gamepad.joystick.left_purpose', MOUSE)
custom_mapping.set('gamepad.joystick.right_purpose', NONE)
self.injector = Injector('gamepad', custom_mapping)
# the stop message will be available in the pipe right away,
# so _start_injecting won't block and just stop. all the stuff
# will be initialized though, so that stuff can be tested
self.injector.stop_injecting()
self.injector._start_injecting()
# not in a process, so the event_producer state can be checked
self.assertEqual(self.injector._event_producer.max_abs, MAX_ABS)
self.assertIsNotNone(self.injector._event_producer.mouse_uinput)
def test_gamepad_to_buttons_event_producer(self):
custom_mapping.set('gamepad.joystick.left_purpose', BUTTONS)
custom_mapping.set('gamepad.joystick.right_purpose', BUTTONS)
self.injector = Injector('gamepad', custom_mapping)
self.injector.stop_injecting()
self.injector._start_injecting()
self.assertIsNone(self.injector._event_producer.max_abs, MAX_ABS)
self.assertIsNone(self.injector._event_producer.mouse_uinput)
def test_device1_event_producer(self):
custom_mapping.set('gamepad.joystick.left_purpose', MOUSE)
custom_mapping.set('gamepad.joystick.right_purpose', WHEEL)
self.injector = Injector('device 1', custom_mapping)
self.injector.stop_injecting()
self.injector._start_injecting()
# not a gamepad, so _event_producer is not initialized for that.
# it can still debounce stuff though
self.assertIsNone(self.injector._event_producer.max_abs)
self.assertIsNone(self.injector._event_producer.mouse_uinput)
def test_injector(self):
# the tests in test_keycode_mapper.py test this stuff in detail
@ -513,7 +574,7 @@ class TestInjector(unittest.TestCase):
# the injector will otherwise skip the device because
# the capabilities don't contain EV_TYPE
input = InputDevice('/dev/input/event30')
self.injector._prepare_device = lambda *args: (input, False)
self.injector._grab_device = lambda *args: input
self.injector.start_injecting()
uinput_write_history_pipe[0].poll(timeout=1)
@ -692,5 +753,156 @@ class TestInjector(unittest.TestCase):
self.assertTrue(is_in_capabilities(key, capabilities))
class TestModifyCapabilities(unittest.TestCase):
def setUp(self):
class FakeDevice:
def capabilities(self, absinfo=True):
assert absinfo is False
return {
evdev.ecodes.EV_SYN: [1, 2, 3],
evdev.ecodes.EV_FF: [1, 2, 3],
evdev.ecodes.EV_ABS: [1, 2, 3]
}
mapping = Mapping()
mapping.change(Key(EV_KEY, 80, 1), 'a')
mapping.change(Key(EV_KEY, 81, 1), DISABLE_NAME)
macro_code = 'r(2, m(sHiFt_l, r(2, k(1).k(2))))'
macro = parse(macro_code, mapping)
mapping.change(Key(EV_KEY, 60, 111), macro_code)
# going to be ignored, because EV_REL cannot be mapped, that's
# mouse movements.
mapping.change(Key(EV_REL, 1234, 3), 'b')
self.a = system_mapping.get('a')
self.shift_l = system_mapping.get('ShIfT_L')
self.one = system_mapping.get(1)
self.two = system_mapping.get('2')
self.left = system_mapping.get('BtN_lEfT')
self.fake_device = FakeDevice()
self.mapping = mapping
self.macro = macro
def check_keys(self, capabilities):
"""No matter the configuration, EV_KEY will be mapped to EV_KEY."""
self.assertIn(EV_KEY, capabilities)
keys = capabilities[EV_KEY]
self.assertIn(self.a, keys)
self.assertIn(self.one, keys)
self.assertIn(self.two, keys)
self.assertIn(self.shift_l, keys)
self.assertNotIn(DISABLE_CODE, keys)
def tearDown(self):
cleanup()
def test_modify_capabilities(self):
self.injector = Injector('foo', self.mapping)
capabilities = self.injector._modify_capabilities(
{60: self.macro},
self.fake_device,
gamepad=False
)
self.assertIn(EV_ABS, capabilities)
self.check_keys(capabilities)
keys = capabilities[EV_KEY]
# mouse capabilities are not needed
self.assertNotIn(self.left, keys)
self.assertNotIn(evdev.ecodes.EV_SYN, capabilities)
self.assertNotIn(evdev.ecodes.EV_FF, capabilities)
self.assertNotIn(evdev.ecodes.EV_REL, capabilities)
# keeps that stuff since modify_capabilities is told that it is not
# a gamepad, so it probably serves some special purpose for that
# device type.
self.assertIn(evdev.ecodes.EV_ABS, capabilities)
def test_modify_capabilities_gamepad(self):
config.set('gamepad.joystick.left_purpose', MOUSE)
self.mapping.set('gamepad.joystick.right_purpose', WHEEL)
self.injector = Injector('foo', self.mapping)
self.assertFalse(self.injector._forwards_joystick())
self.assertTrue(self.injector._maps_joystick())
self.assertTrue(self.injector._joystick_as_mouse())
capabilities = self.injector._modify_capabilities(
{60: self.macro},
self.fake_device,
gamepad=True
)
# because ABS is translated to REL, ABS is not a capability anymore
self.assertNotIn(EV_ABS, capabilities)
self.check_keys(capabilities)
keys = capabilities[EV_KEY]
# now that it is told that it is a gamepad, btn_left is inserted
# to ensure the operating system interprets it as mouse.
self.assertIn(self.left, keys)
def test_modify_capabilities_gamepad_none_none(self):
config.set('gamepad.joystick.left_purpose', NONE)
self.mapping.set('gamepad.joystick.right_purpose', NONE)
self.injector = Injector('foo', self.mapping)
self.assertTrue(self.injector._forwards_joystick())
self.assertFalse(self.injector._maps_joystick())
self.assertFalse(self.injector._joystick_as_mouse())
capabilities = self.injector._modify_capabilities(
{60: self.macro},
self.fake_device,
gamepad=True
)
self.check_keys(capabilities)
self.assertIn(EV_ABS, capabilities)
def test_modify_capabilities_gamepad_buttons_buttons(self):
config.set('gamepad.joystick.left_purpose', BUTTONS)
self.mapping.set('gamepad.joystick.right_purpose', BUTTONS)
self.injector = Injector('foo', self.mapping)
self.assertFalse(self.injector._forwards_joystick())
self.assertTrue(self.injector._maps_joystick())
self.assertFalse(self.injector._joystick_as_mouse())
capabilities = self.injector._modify_capabilities(
{60: self.macro},
self.fake_device,
gamepad=True
)
self.check_keys(capabilities)
self.assertNotIn(EV_ABS, capabilities)
self.assertNotIn(EV_REL, capabilities)
def test_modify_capabilities_buttons_buttons(self):
# those settings shouldn't have an effect with gamepad=False
config.set('gamepad.joystick.left_purpose', BUTTONS)
self.mapping.set('gamepad.joystick.right_purpose', BUTTONS)
self.injector = Injector('foo', self.mapping)
capabilities = self.injector._modify_capabilities(
{60: self.macro},
self.fake_device,
gamepad=False
)
self.check_keys(capabilities)
# not a gamepad, keeps EV_ABS because it probably has some special
# purpose
self.assertIn(EV_ABS, capabilities)
self.assertNotIn(EV_REL, capabilities)
if __name__ == "__main__":
unittest.main()

@ -241,7 +241,6 @@ class TestReader(unittest.TestCase):
pipe[1].send(new_event(EV_KEY, CODE_1, 0, 1004))
read = keycode_reader.read()
print(read)
self.assertEqual(read, None)
pipe[1].send(new_event(EV_ABS, ABS_Y, 0, 1007))

Loading…
Cancel
Save