This commit is contained in:
sezanzeb 2020-12-02 20:48:23 +01:00 committed by sezanzeb
parent 78692f40eb
commit 95b09259d3
7 changed files with 96 additions and 46 deletions

View File

@ -107,3 +107,5 @@ pylint keymapper --extension-pkg-whitelist=evdev
sudo pip install . && coverage run tests/test.py
coverage combine && coverage report -m
```
To read events, `evtest` is very helpful.

View File

@ -32,8 +32,20 @@ from keymapper.logger import logger
from keymapper.config import config
# other events for ABS include buttons
JOYSTICK = [
evdev.ecodes.ABS_X,
evdev.ecodes.ABS_Y,
evdev.ecodes.ABS_Z,
evdev.ecodes.ABS_RX,
evdev.ecodes.ABS_RY,
evdev.ecodes.ABS_RZ,
]
def _write(device, ev_type, keycode, value):
"""Inject."""
print('move mouse', ev_type, keycode, value)
device.write(ev_type, keycode, value)
device.syn()

View File

@ -34,8 +34,9 @@ from evdev.ecodes import EV_KEY, EV_ABS
from keymapper.logger import logger
from keymapper.getdevices import get_devices
from keymapper.state import system_mapping, KEYCODE_OFFSET
from keymapper.dev.keycode_mapper import handle_keycode
from keymapper.dev.ev_abs_mapper import ev_abs_mapper
from keymapper.dev.keycode_mapper import handle_keycode, \
should_map_event_as_btn
from keymapper.dev.ev_abs_mapper import ev_abs_mapper, JOYSTICK
from keymapper.dev.macros import parse
@ -119,7 +120,7 @@ class KeycodeInjector:
self._process = multiprocessing.Process(target=self._start_injecting)
self._process.start()
def map_ev_to_abs(self, capabilities):
def map_abs_to_rel(self, 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
@ -141,16 +142,15 @@ class KeycodeInjector:
capabilities = device.capabilities(absinfo=False)
needed = False
if capabilities.get(EV_KEY) is not None:
for (ev_type, keycode), _ in self.mapping:
# TEST ev_type
if keycode - KEYCODE_OFFSET in capabilities.get(ev_type, []):
needed = True
break
for (ev_type, keycode), _ in self.mapping:
# TODO test ev_type
if keycode - KEYCODE_OFFSET in capabilities.get(ev_type, []):
needed = True
break
map_ev_abs = self.map_ev_to_abs(capabilities)
abs_to_rel = self.map_abs_to_rel(capabilities)
if map_ev_abs:
if abs_to_rel:
needed = True
if not needed:
@ -181,15 +181,15 @@ class KeycodeInjector:
time.sleep(0.15)
return device, map_ev_abs
return device, abs_to_rel
def _modify_capabilities(self, input_device, map_ev_abs):
def _modify_capabilities(self, input_device, abs_to_rel):
"""Adds all keycode into a copy of a devices capabilities.
Prameters
---------
input_device : evdev.InputDevice
map_ev_abs : bool
abs_to_rel : bool
if ABS capabilities should be removed in favor of REL
"""
ecodes = evdev.ecodes
@ -207,7 +207,7 @@ class KeycodeInjector:
if keycode is not None:
capabilities[ev_type].append(keycode - KEYCODE_OFFSET)
if map_ev_abs:
if abs_to_rel:
del capabilities[ecodes.EV_ABS]
capabilities[ecodes.EV_REL] = [
evdev.ecodes.REL_X,
@ -222,6 +222,8 @@ class KeycodeInjector:
if ecodes.EV_FF in capabilities:
del capabilities[ecodes.EV_FF]
print(capabilities)
return capabilities
async def _msg_listener(self, loop):
@ -254,7 +256,7 @@ class KeycodeInjector:
# Watch over each one of the potentially multiple devices per hardware
for path in paths:
input_device, map_ev_abs = self._prepare_device(path)
input_device, abs_to_rel = self._prepare_device(path)
if input_device is None:
continue
@ -264,15 +266,15 @@ class KeycodeInjector:
uinput = evdev.UInput(
name=f'{DEV_NAME} {self.device}',
phys=DEV_NAME,
events=self._modify_capabilities(input_device, map_ev_abs)
events=self._modify_capabilities(input_device, abs_to_rel)
)
# keycode injection
coroutine = self._keycode_loop(input_device, uinput, map_ev_abs)
coroutine = self._keycode_loop(input_device, uinput, abs_to_rel)
coroutines.append(coroutine)
# mouse movement injection
if map_ev_abs:
if abs_to_rel:
self.abs_state[0] = 0
self.abs_state[1] = 0
coroutine = ev_abs_mapper(
@ -307,7 +309,7 @@ class KeycodeInjector:
uinput.write(EV_KEY, keycode - KEYCODE_OFFSET, value)
uinput.syn()
async def _keycode_loop(self, device, uinput, map_ev_abs):
async def _keycode_loop(self, device, uinput, abs_to_rel):
"""Inject keycodes for one of the virtual devices.
Can be stopped by stopping the asyncio loop.
@ -318,7 +320,7 @@ class KeycodeInjector:
where to read keycodes from
uinput : evdev.UInput
where to write keycodes to
map_ev_abs : bool
abs_to_rel : bool
if joystick events should be mapped to mouse movements
"""
# efficiently figure out the target keycode without taking
@ -352,7 +354,7 @@ class KeycodeInjector:
)
async for event in device.async_read_loop():
if map_ev_abs and event.type == EV_ABS:
if abs_to_rel and event.type == EV_ABS and event.code in JOYSTICK:
if event.code not in [evdev.ecodes.ABS_X, evdev.ecodes.ABS_Y]:
continue
if event.code == evdev.ecodes.ABS_X:
@ -361,16 +363,14 @@ class KeycodeInjector:
self.abs_state[1] = event.value
continue
if event.type != EV_KEY:
uinput.write(event.type, event.code, event.value)
# this already includes SYN events, so need to syn here again
if should_map_event_as_btn(event):
handle_keycode(code_code_mapping, macros, event, uinput)
continue
if event.value == 2:
# linux does them itself, no need to trigger them
continue
handle_keycode(code_code_mapping, macros, event, uinput)
# forward the rest
uinput.write(event.type, event.code, event.value)
# this already includes SYN events, so need to syn here again
continue
# this should only ever happen in tests to avoid blocking them
# forever, as soon as all events are consumed. In normal operation

View File

@ -24,12 +24,35 @@
import asyncio
import evdev
from keymapper.logger import logger
from keymapper.state import KEYCODE_OFFSET
def should_map_event_as_btn(event):
"""Does this event describe a button.
Especially important for gamepad events, some of the buttons
require special rules.
Parameters
----------
event : evdev.events.InputEvent
"""
# TODO test
if event.type == evdev.events.EV_KEY:
return True
if event.type == evdev.events.EV_ABS and event.code > 5:
# 1 - 5 seem to be joystick events
return True
return False
def handle_keycode(code_code_mapping, macros, event, uinput):
"""Write the mapped keycode.
"""Write the mapped keycode or forward unmapped ones.
Parameters
----------
@ -39,6 +62,10 @@ def handle_keycode(code_code_mapping, macros, event, uinput):
macros : dict
mapping of linux-keycode to _Macro objects
"""
if event.value == 2:
# button-hold event
return
input_keycode = event.code
# for logging purposes. It should log the same keycode as xev and gtk,
@ -63,9 +90,10 @@ def handle_keycode(code_code_mapping, macros, event, uinput):
if input_keycode in code_code_mapping:
target_keycode = code_code_mapping[input_keycode]
logger.spam(
'got code:%s value:%s, maps to code:%s',
'got code:%s value:%s event:%s, maps to code:%s',
xkb_keycode,
event.value,
evdev.ecodes.EV[event.type],
target_keycode + KEYCODE_OFFSET
)
else:
@ -76,5 +104,6 @@ def handle_keycode(code_code_mapping, macros, event, uinput):
)
target_keycode = input_keycode
print('write', event.type, target_keycode, event.value)
uinput.write(event.type, target_keycode, event.value)
uinput.syn()

View File

@ -31,6 +31,7 @@ import evdev
from keymapper.logger import logger
from keymapper.getdevices import get_devices, refresh_devices
from keymapper.state import KEYCODE_OFFSET
from keymapper.dev.keycode_mapper import should_map_event_as_btn
CLOSE = 1
@ -108,10 +109,14 @@ class _KeycodeReader:
logger.debug('Pipe closed, reader stops.')
sys.exit(0)
if event.type == evdev.ecodes.EV_KEY and event.value == 1:
# TODO write a test to map event `type 3 (EV_ABS), code 16
# (ABS_HAT0X), value 0` to a button
if should_map_event_as_btn(event):
logger.spam(
'got code:%s value:%s',
event.code + KEYCODE_OFFSET, event.value
'got code:%s value:%s type:%s',
event.code + KEYCODE_OFFSET,
event.value,
evdev.ecodes.EV[event.type]
)
self._pipe[1].send((event.type, event.code + KEYCODE_OFFSET))
@ -148,7 +153,7 @@ class _KeycodeReader:
"""Get the newest tuple of event type, keycode or None."""
if self._pipe is None:
logger.debug('No pipe available to read from')
return None
return (None, None)
newest_event = (None, None)
while self._pipe[0].poll():

View File

@ -76,7 +76,9 @@ class Row(Gtk.ListBoxRow):
"""Check if a keycode has been pressed and if so, display it."""
# the newest_keycode is populated since the ui regularly polls it
# in order to display it in the status bar.
previous_keycode = self.get_keycode()
key = self.get_keycode()
previous_keycode = key[1] if key else None
character = self.get_character()
# no input
@ -158,7 +160,7 @@ class Row(Gtk.ListBoxRow):
keycode_input.set_size_request(50, -1)
if keycode is not None:
keycode_input.set_label(f'{ev_type},{keycode})')
keycode_input.set_label(f'{ev_type},{keycode}')
# make the togglebutton go back to its normal state when doing
# something else in the UI

View File

@ -87,7 +87,7 @@ class TestInjector(unittest.TestCase):
fake_device = FakeDevice()
capabilities = self.injector._modify_capabilities(
fake_device,
map_ev_abs=False
abs_to_rel=False
)
self.assertIn(EV_KEY, capabilities)
@ -105,8 +105,8 @@ class TestInjector(unittest.TestCase):
path = '/dev/input/event10'
# this test needs to pass around all other constraints of
# _prepare_device
device, map_ev_abs = self.injector._prepare_device(path)
self.assertFalse(map_ev_abs)
device, abs_to_rel = self.injector._prepare_device(path)
self.assertFalse(abs_to_rel)
self.assertEqual(self.failed, 2)
# success on the third try
device.name = fixtures[path]['name']
@ -115,10 +115,10 @@ class TestInjector(unittest.TestCase):
self.injector = KeycodeInjector('gamepad', custom_mapping)
path = '/dev/input/event30'
device, map_ev_abs = self.injector._prepare_device(path)
self.assertTrue(map_ev_abs)
device, abs_to_rel = self.injector._prepare_device(path)
self.assertTrue(abs_to_rel)
capabilities = self.injector._modify_capabilities(device, map_ev_abs)
capabilities = self.injector._modify_capabilities(device, abs_to_rel)
self.assertNotIn(evdev.ecodes.EV_ABS, capabilities)
self.assertIn(evdev.ecodes.EV_REL, capabilities)
@ -127,8 +127,8 @@ class TestInjector(unittest.TestCase):
custom_mapping.change(EV_KEY, 10, 'a')
self.injector = KeycodeInjector('device 1', custom_mapping)
path = '/dev/input/event11'
device, map_ev_abs = self.injector._prepare_device(path)
self.assertFalse(map_ev_abs)
device, abs_to_rel = self.injector._prepare_device(path)
self.assertFalse(abs_to_rel)
self.assertEqual(self.failed, 0)
self.assertIsNone(device)