only considering joystick events for buttons on gamepads

This commit is contained in:
sezanzeb 2021-02-16 23:40:43 +01:00 committed by sezanzeb
parent 3344837845
commit 14bbd9f7fc
11 changed files with 118 additions and 63 deletions

View File

@ -2,8 +2,8 @@
disable=
# that is the standard way to import GTK afaik
wrong-import-position
wrong-import-position,
# using """ for comments highlights them in green for me and makes it
# a great way to separate stuff into multiple sections
pointless-string-statement
pointless-string-statement

View File

@ -23,9 +23,7 @@
import os
import sys
import json
import shutil
import copy
from keymapper.paths import CONFIG_PATH, USER, touch

View File

@ -28,8 +28,7 @@ import time
import asyncio
import evdev
from evdev.ecodes import EV_KEY, EV_ABS, KEY_CAMERA, EV_REL, ABS_PRESSURE, \
BTN_STYLUS, BTN_A
from evdev.ecodes import EV_KEY, EV_ABS, KEY_CAMERA, EV_REL, BTN_STYLUS, BTN_A
from keymapper.logger import logger

View File

@ -34,7 +34,7 @@ from evdev.ecodes import EV_KEY, EV_ABS, ABS_MISC, EV_REL
from keymapper.logger import logger
from keymapper.key import Key
from keymapper.state import custom_mapping
from keymapper.getdevices import get_devices
from keymapper.getdevices import get_devices, is_gamepad
from keymapper import utils
CLOSE = 1
@ -119,6 +119,11 @@ class _KeycodeReader:
If read is called without prior start_reading, no keycodes
will be available.
Parameters
----------
device_name : string
As indexed in get_devices()
"""
if self._pipe is not None:
self.stop_reading()
@ -126,33 +131,39 @@ class _KeycodeReader:
self.virtual_devices = []
for name, group in get_devices().items():
if device_name not in name:
group = get_devices()[device_name]
# Watch over each one of the potentially multiple devices per hardware
for path in group['paths']:
try:
device = evdev.InputDevice(path)
except FileNotFoundError:
continue
# Watch over each one of the potentially multiple devices per
# hardware
for path in group['paths']:
try:
device = evdev.InputDevice(path)
except FileNotFoundError:
continue
if evdev.ecodes.EV_KEY in device.capabilities():
self.virtual_devices.append(device)
if evdev.ecodes.EV_KEY in device.capabilities():
self.virtual_devices.append(device)
logger.debug(
'Starting reading keycodes from "%s"',
'", "'.join([device.name for device in self.virtual_devices])
)
logger.debug(
'Starting reading keycodes from "%s"',
'", "'.join([device.name for device in self.virtual_devices])
)
pipe = multiprocessing.Pipe()
self._pipe = pipe
self._process = threading.Thread(target=self._read_worker)
self._process.start()
def _pipe_event(self, event, device):
"""Write the event into the pipe to the main process."""
def _pipe_event(self, event, device, gamepad):
"""Write the event into the pipe to the main process.
Parameters
----------
event : evdev.InputEvent
device : evdev.InputDevice
gamepad : bool
If true, ABS_X and ABS_Y might be mapped to buttons as well
depending on the purpose configuration
"""
# value: 1 for down, 0 for up, 2 for hold.
if self._pipe is None or self._pipe[1].closed:
logger.debug('Pipe closed, reader stops.')
@ -173,7 +184,7 @@ class _KeycodeReader:
# which breaks the current workflow.
return
if not utils.should_map_event_as_btn(event, custom_mapping):
if not utils.should_map_event_as_btn(event, custom_mapping, gamepad):
return
max_abs = utils.get_max_abs(device)
@ -186,13 +197,18 @@ class _KeycodeReader:
# using a thread that blocks instead of read_one made it easier
# to debug via the logs, because the UI was not polling properly
# at some point which caused logs for events not to be written.
rlist = {device.fd: device for device in self.virtual_devices}
rlist = {}
gamepad = {}
for device in self.virtual_devices:
rlist[device.fd] = device
gamepad[device.fd] = is_gamepad(device)
rlist[self._pipe[1]] = self._pipe[1]
while True:
ready = select.select(rlist, [], [])[0]
for fd in ready:
readable = rlist[fd] # a device or a pipe
readable = rlist[fd] # an InputDevice or a pipe
if isinstance(readable, multiprocessing.connection.Connection):
msg = readable.recv()
if msg == CLOSE:
@ -202,7 +218,11 @@ class _KeycodeReader:
try:
for event in rlist[fd].read():
self._pipe_event(event, readable)
self._pipe_event(
event,
readable,
gamepad.get(fd, False)
)
except OSError:
logger.debug(
'Device "%s" disappeared from the reader',

View File

@ -59,6 +59,9 @@ class Context:
macros : dict
mapping of ((type, code, value),) to _Macro objects.
Combinations work similar as in key_to_code
is_gamepad : bool
if key-mapper considers this device to be a gamepad. If yes, ABS_X
and ABS_Y events can be treated as buttons.
"""
def __init__(self, mapping):
self.mapping = mapping
@ -67,6 +70,7 @@ class Context:
# might be a bit expensive
self.key_to_code = self._map_keys_to_codes()
self.macros = self._parse_macros()
self.left_purpose = None
self.right_purpose = None
self.update_purposes()

View File

@ -406,6 +406,8 @@ class Injector(multiprocessing.Process):
source.path, source.fd
)
gamepad = is_gamepad(source)
keycode_handler = KeycodeMapper(self.context, source, uinput)
async for event in source.async_read_loop():
@ -415,7 +417,7 @@ class Injector(multiprocessing.Process):
continue
# for mapped stuff
if utils.should_map_event_as_btn(event, self.context.mapping):
if utils.should_map_event_as_btn(event, self.context.mapping, gamepad):
will_report_key_up = utils.will_report_key_up(event)
keycode_handler.handle_keycode(event)

View File

@ -54,6 +54,7 @@ def key_spam(self, key, msg, *args):
anything that can be string formatted, but usually a tuple of
(type, code, value) tuples
"""
# pylint: disable=protected-access
if not self.isEnabledFor(SPAM):
return

View File

@ -95,7 +95,7 @@ def will_report_key_up(event):
return not is_wheel(event)
def should_map_event_as_btn(event, mapping):
def should_map_event_as_btn(event, mapping, gamepad):
"""Does this event describe a button.
If it does, this function will make sure its value is one of [-1, 0, 1],
@ -106,6 +106,13 @@ def should_map_event_as_btn(event, mapping):
Especially important for gamepad events, some of the buttons
require special rules.
Parameters
----------
event : evdev.InputEvent
mapping : Mapping
gamepad : bool
If the device is treated as gamepad
"""
if (event.type, event.code) in STYLUS:
return False
@ -121,6 +128,9 @@ def should_map_event_as_btn(event, mapping):
return False
if event.code in JOYSTICK:
if not gamepad:
return False
l_purpose = mapping.get('gamepad.joystick.left_purpose')
r_purpose = mapping.get('gamepad.joystick.right_purpose')
@ -130,6 +140,8 @@ def should_map_event_as_btn(event, mapping):
if event.code in [ABS_RX, ABS_RY] and r_purpose == BUTTONS:
return True
else:
# for non-joystick buttons just always offer mapping them to
# buttons
return True
if is_wheel(event):

View File

@ -33,7 +33,7 @@ requests.
- [x] map keys using a `modifier + modifier + ... + key` syntax
- [ ] injecting keys that aren't available in the systems keyboard layout
- [ ] injecting keys while abs capabilities are present. e.g. stylus buttons
- [ ] ship with a list of all keys known to xkb and validate input in gui
- [ ] ship with a list of all keys known to xkb and validate input in the gui
## Tests

View File

@ -17,7 +17,7 @@
<text x="22.0" y="14">pylint</text>
</g>
<g fill="#fff" text-anchor="middle" font-family="DejaVu Sans,Verdana,Geneva,sans-serif" font-size="11">
<text x="63.0" y="15" fill="#010101" fill-opacity=".3">9.81</text>
<text x="62.0" y="14">9.81</text>
<text x="63.0" y="15" fill="#010101" fill-opacity=".3">9.84</text>
<text x="62.0" y="14">9.84</text>
</g>
</svg>

Before

Width:  |  Height:  |  Size: 1.0 KiB

After

Width:  |  Height:  |  Size: 1.0 KiB

View File

@ -56,57 +56,76 @@ class TestDevUtils(unittest.TestCase):
mapping = Mapping()
# the function name is so horribly long
def do(event):
return utils.should_map_event_as_btn(event, mapping)
def do(gamepad, event):
return utils.should_map_event_as_btn(event, mapping, gamepad)
"""D-Pad"""
self.assertTrue(do(new_event(EV_ABS, ABS_HAT0X, 1)))
self.assertTrue(do(new_event(EV_ABS, ABS_HAT0X, -1)))
self.assertTrue(do(1, new_event(EV_ABS, ABS_HAT0X, 1)))
self.assertTrue(do(0, new_event(EV_ABS, ABS_HAT0X, -1)))
"""Mouse movements"""
self.assertTrue(do(new_event(EV_REL, REL_WHEEL, 1)))
self.assertTrue(do(new_event(EV_REL, REL_WHEEL, -1)))
self.assertTrue(do(new_event(EV_REL, REL_HWHEEL, 1)))
self.assertTrue(do(new_event(EV_REL, REL_HWHEEL, -1)))
self.assertFalse(do(new_event(EV_REL, REL_X, -1)))
self.assertTrue(do(1, new_event(EV_REL, REL_WHEEL, 1)))
self.assertTrue(do(0, new_event(EV_REL, REL_WHEEL, -1)))
self.assertTrue(do(1, new_event(EV_REL, REL_HWHEEL, 1)))
self.assertTrue(do(0, new_event(EV_REL, REL_HWHEEL, -1)))
self.assertFalse(do(1, new_event(EV_REL, REL_X, -1)))
"""regular keys and buttons"""
self.assertTrue(do(new_event(EV_KEY, KEY_A, 1)))
self.assertTrue(do(new_event(EV_ABS, ABS_HAT0X, -1)))
self.assertTrue(do(1, new_event(EV_KEY, KEY_A, 1)))
self.assertTrue(do(0, new_event(EV_KEY, KEY_A, 1)))
self.assertTrue(do(1, new_event(EV_ABS, ABS_HAT0X, -1)))
self.assertTrue(do(0, new_event(EV_ABS, ABS_HAT0X, -1)))
"""mousepad events"""
self.assertFalse(do(new_event(EV_ABS, ecodes.ABS_MT_SLOT, 1)))
self.assertFalse(do(new_event(EV_ABS, ecodes.ABS_MT_TOOL_Y, 1)))
self.assertFalse(do(new_event(EV_ABS, ecodes.ABS_MT_POSITION_X, 1)))
self.assertFalse(do(new_event(EV_KEY, ecodes.BTN_TOUCH, 1)))
self.assertFalse(do(1, new_event(EV_ABS, ecodes.ABS_MT_SLOT, 1)))
self.assertFalse(do(0, new_event(EV_ABS, ecodes.ABS_MT_SLOT, 1)))
self.assertFalse(do(1, new_event(EV_ABS, ecodes.ABS_MT_TOOL_Y, 1)))
self.assertFalse(do(0, new_event(EV_ABS, ecodes.ABS_MT_TOOL_Y, 1)))
self.assertFalse(do(1, new_event(EV_ABS, ecodes.ABS_MT_POSITION_X, 1)))
self.assertFalse(do(0, new_event(EV_ABS, ecodes.ABS_MT_POSITION_X, 1)))
self.assertFalse(do(1, new_event(EV_KEY, ecodes.BTN_TOUCH, 1)))
self.assertFalse(do(0, new_event(EV_KEY, ecodes.BTN_TOUCH, 1)))
"""stylus movements"""
self.assertFalse(do(new_event(EV_KEY, ecodes.BTN_DIGI, 1)))
self.assertFalse(do(new_event(EV_ABS, ecodes.ABS_TILT_X, 1)))
self.assertFalse(do(new_event(EV_ABS, ecodes.ABS_TILT_Y, 1)))
self.assertFalse(do(new_event(EV_ABS, ecodes.ABS_DISTANCE, 1)))
self.assertFalse(do(new_event(EV_ABS, ecodes.ABS_PRESSURE, 1)))
self.assertFalse(do(0, new_event(EV_KEY, ecodes.BTN_DIGI, 1)))
self.assertFalse(do(1, new_event(EV_KEY, ecodes.BTN_DIGI, 1)))
self.assertFalse(do(0, new_event(EV_ABS, ecodes.ABS_TILT_X, 1)))
self.assertFalse(do(1, new_event(EV_ABS, ecodes.ABS_TILT_X, 1)))
self.assertFalse(do(0, new_event(EV_ABS, ecodes.ABS_TILT_Y, 1)))
self.assertFalse(do(1, new_event(EV_ABS, ecodes.ABS_TILT_Y, 1)))
self.assertFalse(do(0, new_event(EV_ABS, ecodes.ABS_DISTANCE, 1)))
self.assertFalse(do(1, new_event(EV_ABS, ecodes.ABS_DISTANCE, 1)))
self.assertFalse(do(0, new_event(EV_ABS, ecodes.ABS_PRESSURE, 1)))
self.assertFalse(do(1, new_event(EV_ABS, ecodes.ABS_PRESSURE, 1)))
"""joysticks"""
self.assertFalse(do(new_event(EV_ABS, ecodes.ABS_RX, 1234)))
self.assertFalse(do(new_event(EV_ABS, ecodes.ABS_Y, -1)))
self.assertFalse(do(new_event(EV_ABS, ecodes.ABS_RY, -1)))
"""weird events"""
self.assertFalse(do(new_event(EV_ABS, ecodes.ABS_MISC, -1)))
# without a purpose of BUTTONS it won't map any button, even for
# gamepads
self.assertFalse(do(0, new_event(EV_ABS, ecodes.ABS_RX, 1234)))
self.assertFalse(do(1, new_event(EV_ABS, ecodes.ABS_RX, 1234)))
self.assertFalse(do(0, new_event(EV_ABS, ecodes.ABS_Y, -1)))
self.assertFalse(do(1, new_event(EV_ABS, ecodes.ABS_Y, -1)))
self.assertFalse(do(0, new_event(EV_ABS, ecodes.ABS_RY, -1)))
self.assertFalse(do(1, new_event(EV_ABS, ecodes.ABS_RY, -1)))
mapping.set('gamepad.joystick.right_purpose', BUTTONS)
config.set('gamepad.joystick.left_purpose', BUTTONS)
# but only for gamepads
self.assertFalse(do(0, new_event(EV_ABS, ecodes.ABS_Y, -1)))
self.assertTrue(do(1, new_event(EV_ABS, ecodes.ABS_Y, -1)))
self.assertFalse(do(0, new_event(EV_ABS, ecodes.ABS_RY, -1)))
self.assertTrue(do(1, new_event(EV_ABS, ecodes.ABS_RY, -1)))
self.assertTrue(do(new_event(EV_ABS, ecodes.ABS_Y, -1)))
self.assertTrue(do(new_event(EV_ABS, ecodes.ABS_RY, -1)))
"""weird events"""
self.assertFalse(do(0, new_event(EV_ABS, ecodes.ABS_MISC, -1)))
self.assertFalse(do(1, new_event(EV_ABS, ecodes.ABS_MISC, -1)))
def test_normalize_value(self):
def do(event):