wip reading keycodes even though the device is grabbed

This commit is contained in:
sezanzeb 2020-11-22 16:54:40 +01:00
parent c7736b15ed
commit 2afde0039f
6 changed files with 122 additions and 45 deletions

View File

@ -64,7 +64,12 @@ class _Config:
self._config['map_EV_REL_devices'] = active self._config['map_EV_REL_devices'] = active
def may_modify_movement_devices(self): def may_modify_movement_devices(self):
"""Get if devices that control movements may be modified as well.""" """Get if devices that control movements may be modified as well.
Since movement events happen quite often and fast, I'd like to
add the option to disabling mapping those if it affects their
performance. TODO figure out which devices to inject to instead?
"""
return self._config['map_EV_REL_devices'] return self._config['map_EV_REL_devices']
def load_config(self): def load_config(self):

View File

@ -23,6 +23,8 @@
import multiprocessing import multiprocessing
import time
import copy
import evdev import evdev
@ -62,10 +64,6 @@ class _GetDevicesProcess(multiprocessing.Process):
# "Logitech USB Keyboard" and "Logitech USB Keyboard Consumer Control" # "Logitech USB Keyboard" and "Logitech USB Keyboard Consumer Control"
grouped = {} grouped = {}
for device in devices: for device in devices:
if device.phys.startswith('key-mapper'):
# injector device, not really periphery
continue
# only keyboard devices # only keyboard devices
# https://www.kernel.org/doc/html/latest/input/event-codes.html # https://www.kernel.org/doc/html/latest/input/event-codes.html
capabilities = device.capabilities().keys() capabilities = device.capabilities().keys()
@ -76,8 +74,6 @@ class _GetDevicesProcess(multiprocessing.Process):
not config.may_modify_movement_devices() not config.may_modify_movement_devices()
and evdev.ecodes.EV_REL in capabilities and evdev.ecodes.EV_REL in capabilities
): ):
# skip devices that control movement to avoid affecting
# their performance due to the amount of their events.
# TODO add checkbox to automatically load # TODO add checkbox to automatically load
# a preset on login # a preset on login
logger.debug( logger.debug(
@ -90,7 +86,7 @@ class _GetDevicesProcess(multiprocessing.Process):
if grouped.get(usb) is None: if grouped.get(usb) is None:
grouped[usb] = [] grouped[usb] = []
logger.debug('Found "%s", %s, %s', device.name, device.path, usb) logger.spam('Found "%s", %s, %s', device.name, device.path, usb)
grouped[usb].append((device.name, device.path)) grouped[usb].append((device.name, device.path))
@ -109,13 +105,19 @@ class _GetDevicesProcess(multiprocessing.Process):
def refresh_devices(): def refresh_devices():
"""Get new devices, e.g. new ones created by key-mapper.""" """Get new devices, e.g. new ones created by key-mapper.
This should be called whenever devices in /dev are added or removed.
"""
# it may take a little bit of time until devices are visible after
# changes
time.sleep(0.1)
global _devices global _devices
_devices = None _devices = None
return get_devices() return get_devices()
def get_devices(): def get_devices(include_keymapper=False):
"""Group devices and get relevant infos per group. """Group devices and get relevant infos per group.
Returns a list containing mappings of Returns a list containing mappings of
@ -138,4 +140,14 @@ def get_devices():
else: else:
names = [f'"{name}"' for name in _devices] names = [f'"{name}"' for name in _devices]
logger.info('Found %s', ', '.join(names)) logger.info('Found %s', ', '.join(names))
return _devices
# filter the result
result = {}
for device in _devices.keys():
if include_keymapper and device.startswith('key-mapper'):
result[device] = _devices[device]
continue
result[device] = _devices[device]
return result

View File

@ -86,8 +86,10 @@ class Row(Gtk.ListBoxRow):
self.keycode.set_label(str(new_keycode)) self.keycode.set_label(str(new_keycode))
# switch to the character, don't require mouse input because # switch to the character, don't require mouse input because
# that would overwrite the key with the mouse-button key if # that would overwrite the key with the mouse-button key if
# the current device is a mouse # the current device is a mouse. idle_add this so that the
self.window.window.set_focus(self.character_input) # keycode event won't write into the character input as well.
window = self.window.window
GLib.idle_add(lambda: window.set_focus(self.character_input))
self.highlight() self.highlight()
# the character is empty and therefore the mapping is not complete # the character is empty and therefore the mapping is not complete

View File

@ -34,13 +34,14 @@ import multiprocessing
import evdev import evdev
from keymapper.logger import logger from keymapper.logger import logger
from keymapper.getdevices import get_devices from keymapper.getdevices import get_devices, refresh_devices
from keymapper.state import custom_mapping, system_mapping from keymapper.state import custom_mapping, system_mapping
DEV_NAME = 'key-mapper' DEV_NAME = 'key-mapper'
DEVICE_CREATED = 1 DEVICE_CREATED = 1
FAILED = 2 FAILED = 2
DEVICE_SKIPPED = 3
def _grab(path): def _grab(path):
@ -88,6 +89,24 @@ def _modify_capabilities(device):
return capabilities return capabilities
def _is_device_mapped(device, mapping):
"""Check if this device has capabilities that are being mapped.
Parameters
----------
device : evdev.InputDevice
mapping : Mapping
"""
capabilities = device.capabilities(absinfo=False)[evdev.ecodes.EV_KEY]
needed = False
for keycode, _ in custom_mapping:
if keycode in capabilities:
needed = True
if not needed:
logger.debug('No need to grab %s', device.path)
return needed
def _start_injecting_worker(path, pipe, mapping): def _start_injecting_worker(path, pipe, mapping):
"""Inject keycodes for one of the virtual devices. """Inject keycodes for one of the virtual devices.
@ -108,19 +127,21 @@ def _start_injecting_worker(path, pipe, mapping):
pipe.send(FAILED) pipe.send(FAILED)
return return
capabilities = _modify_capabilities(device) if not _is_device_mapped(device, mapping):
pipe.send(DEVICE_SKIPPED)
return
keymapper_device = evdev.UInput( keymapper_device = evdev.UInput(
name=f'key-mapper {device.name}', name=f'key-mapper {device.name}',
phys='key-mapper', phys='key-mapper',
events=capabilities events=_modify_capabilities(device)
) )
pipe.send(DEVICE_CREATED) pipe.send(DEVICE_CREATED)
logger.debug( logger.debug(
'Started injecting into %s, fd %s', 'Started injecting into %s, fd %s',
device.path, keymapper_device.fd keymapper_device.device.path, keymapper_device.fd
) )
for event in device.read_loop(): for event in device.read_loop():
@ -222,11 +243,7 @@ class KeycodeInjector:
paths = get_devices()[self.device]['paths'] paths = get_devices()[self.device]['paths']
logger.info( logger.info('Starting injecting the mapping for %s', self.device)
'Starting injecting the mapping for %s on %s',
self.device,
', '.join(paths)
)
# Watch over each one of the potentially multiple devices per hardware # Watch over each one of the potentially multiple devices per hardware
for path in paths: for path in paths:
@ -247,6 +264,9 @@ class KeycodeInjector:
if len(self.processes) == 0: if len(self.processes) == 0:
raise OSError('Could not grab any device') raise OSError('Could not grab any device')
# key-mapper devices were added, freshly scan /dev/input
refresh_devices()
@ensure_numlock @ensure_numlock
def stop_injecting(self): def stop_injecting(self):
"""Stop injecting keycodes.""" """Stop injecting keycodes."""
@ -258,3 +278,5 @@ class KeycodeInjector:
if process.is_alive(): if process.is_alive():
process.terminate() process.terminate()
self.processes[i] = None self.processes[i] = None
refresh_devices()

View File

@ -50,21 +50,32 @@ class _KeycodeReader:
If read is called without prior start_reading, no keycodes If read is called without prior start_reading, no keycodes
will be available. will be available.
Parameters
----------
device : string
The name of the device.
""" """
paths = get_devices()[device]['paths'] groups = get_devices(include_keymapper=True)
for name, group in groups.items():
# also find stuff like "key-mapper {device}"
if device not in name:
return
logger.debug( paths = group['paths']
'Starting reading keycodes for %s on %s',
device,
', '.join(paths)
)
# Watch over each one of the potentially multiple devices per hardware # Watch over each one of the potentially multiple devices per
# hardware
self.virtual_devices = [ self.virtual_devices = [
evdev.InputDevice(path) evdev.InputDevice(path)
for path in paths for path in paths
] ]
logger.debug(
'Starting reading keycodes from "%s"',
'", "'.join([device.name for device in self.virtual_devices])
)
def read(self): def read(self):
"""Get the newest keycode or None if none was pressed.""" """Get the newest keycode or None if none was pressed."""
newest_keycode = None newest_keycode = None
@ -74,12 +85,11 @@ class _KeycodeReader:
if event is None: if event is None:
break break
if event.type == evdev.ecodes.EV_KEY and event.value == 1:
logger.spam( logger.spam(
'got code:%s value:%s', 'got code:%s value:%s',
event.code + 8, event.value event.code + 8, event.value
) )
if event.type == evdev.ecodes.EV_KEY and event.value == 1:
# value: 1 for down, 0 for up, 2 for hold. # value: 1 for down, 0 for up, 2 for hold.
# this happens to report key codes that are 8 lower # this happens to report key codes that are 8 lower
# than the ones reported by evtest and used in xkb files # than the ones reported by evtest and used in xkb files

View File

@ -22,16 +22,18 @@
import unittest import unittest
from keymapper.getdevices import _GetDevicesProcess from keymapper.getdevices import _GetDevicesProcess
from keymapper.config import config
class TestGetDevices(unittest.TestCase):
def test_get_devices(self):
class FakePipe: class FakePipe:
devices = None devices = None
def send(self, devices): def send(self, devices):
self.devices = devices self.devices = devices
class TestGetDevices(unittest.TestCase):
def test_get_devices(self):
# don't actually start the process, just use the `run` function. # don't actually start the process, just use the `run` function.
# otherwise the coverage tool can't keep track. # otherwise the coverage tool can't keep track.
pipe = FakePipe() pipe = FakePipe()
@ -41,7 +43,31 @@ class TestGetDevices(unittest.TestCase):
'paths': [ 'paths': [
'/dev/input/event11', '/dev/input/event11',
'/dev/input/event10', '/dev/input/event10',
'/dev/input/event13'], '/dev/input/event13'
],
'devices': [
'device 1 foo',
'device 1',
'device 1'
]
},
'device 2': {
'paths': ['/dev/input/event20'],
'devices': ['device 2']
}
})
def test_map_movement_devices(self):
pipe = FakePipe()
config.set_modify_movement_devices(False)
_GetDevicesProcess(pipe).run()
self.assertDictEqual(pipe.devices, {
'device 1': {
'paths': [
'/dev/input/event11',
'/dev/input/event10',
'/dev/input/event13'
],
'devices': [ 'devices': [
'device 1 foo', 'device 1 foo',
'device 1', 'device 1',