#73 #74 #76 improved gamepad support

xkb
sezanzeb 3 years ago committed by sezanzeb
parent e4e6130b70
commit 07cc8e1cc6

@ -29,7 +29,7 @@ import asyncio
import evdev
from evdev.ecodes import EV_KEY, EV_ABS, KEY_CAMERA, EV_REL, BTN_STYLUS, \
BTN_A, ABS_MT_POSITION_X, REL_X, KEY_A, BTN_LEFT
BTN_A, ABS_MT_POSITION_X, REL_X, KEY_A, BTN_LEFT, REL_Y, REL_WHEEL
from keymapper.logger import logger
@ -51,11 +51,6 @@ GRAPHICS_TABLET = 'graphics-tablet'
CAMERA = 'camera'
UNKNOWN = 'unknown'
# sort types that most devices would fall in easily to the right
PRIORITIES = [
GRAPHICS_TABLET, TOUCHPAD, MOUSE, GAMEPAD, KEYBOARD, CAMERA, UNKNOWN
]
if not hasattr(evdev.InputDevice, 'path'):
# for evdev < 1.0.0 patch the path property
@ -68,10 +63,17 @@ if not hasattr(evdev.InputDevice, 'path'):
def _is_gamepad(capabilities):
"""Check if joystick movements are available for mapping."""
if len(capabilities.get(EV_REL, [])) > 0:
return False
if BTN_A not in capabilities.get(EV_KEY, []):
# A few buttons that indicate a gamepad
buttons = {
evdev.ecodes.BTN_BASE,
evdev.ecodes.BTN_A,
evdev.ecodes.BTN_THUMB,
evdev.ecodes.BTN_TOP,
evdev.ecodes.BTN_DPAD_DOWN,
evdev.ecodes.BTN_GAMEPAD,
}
if not buttons.intersection(capabilities.get(EV_KEY, [])):
# no button is in the key capabilities
return False
# joysticks
@ -86,9 +88,20 @@ def _is_gamepad(capabilities):
def _is_mouse(capabilities):
"""Check if the capabilities represent those of a mouse."""
# Based on observation, those capabilities need to be present to get an
# UInput recognized as mouse
# mouse movements
if not REL_X in capabilities.get(EV_REL, []):
return False
if not REL_Y in capabilities.get(EV_REL, []):
return False
# at least the vertical mouse wheel
if not REL_WHEEL in capabilities.get(EV_REL, []):
return False
# and a mouse click button
if not BTN_LEFT in capabilities.get(EV_KEY, []):
return False
@ -138,12 +151,12 @@ def classify(device):
if _is_touchpad(capabilities):
return TOUCHPAD
if _is_mouse(capabilities):
return MOUSE
if _is_gamepad(capabilities):
return GAMEPAD
if _is_mouse(capabilities):
return MOUSE
if _is_camera(capabilities):
return CAMERA
@ -230,16 +243,15 @@ class _GetDevices(threading.Thread):
names = [entry[0] for entry in group]
devs = [entry[1] for entry in group]
# find the most specific type from all devices per group.
# e.g. a device with mouse and keyboard subdevices is a mouse.
types = sorted([entry[2] for entry in group], key=PRIORITIES.index)
device_type = types[0]
shortest_name = sorted(names, key=len)[0]
result[shortest_name] = {
'paths': devs,
'devices': names,
'type': device_type
# sort it alphabetically to be predictable in tests
'types': sorted(list({
item[2] for item in group
if item[2] != UNKNOWN
}))
}
self.pipe.send(result)

@ -34,7 +34,7 @@ import multiprocessing
import subprocess
import evdev
from evdev.ecodes import EV_KEY
from evdev.ecodes import EV_KEY, EV_ABS
from keymapper.ipc.pipe import Pipe
from keymapper.logger import logger
@ -159,7 +159,8 @@ class RootHelper:
try:
event = device.read_one()
self._send_event(event, device)
if event:
self._send_event(event, device)
except OSError:
logger.debug('Device "%s" disappeared', device.path)
return
@ -188,8 +189,11 @@ class RootHelper:
# which breaks the current workflow.
return
max_abs = utils.get_max_abs(device)
event.value = utils.normalize_value(event, max_abs)
if event.type == EV_ABS:
abs_range = utils.get_abs_range(device, event.code)
event.value = utils.normalize_value(event, abs_range)
else:
event.value = utils.normalize_value(event)
self._results.send({
'type': 'event',

@ -35,7 +35,7 @@ from keymapper.ipc.pipe import Pipe
from keymapper.gui.helper import TERMINATE
from keymapper import utils
from keymapper.state import custom_mapping
from keymapper.getdevices import get_devices
from keymapper.getdevices import get_devices, GAMEPAD
DEBOUNCE_TICKS = 3
@ -133,7 +133,7 @@ class Reader:
if event is None:
continue
gamepad = get_devices()[self.device_name]['type'] == 'gamepad'
gamepad = GAMEPAD in get_devices()[self.device_name]['types']
if not utils.should_map_as_btn(event, custom_mapping, gamepad):
continue

@ -70,6 +70,11 @@ ICON_NAMES = {
UNKNOWN: None,
}
# sort types that most devices would fall in easily to the right.
ICON_PRIORITIES = [
GRAPHICS_TABLET, TOUCHPAD, GAMEPAD, MOUSE, KEYBOARD, UNKNOWN
]
def with_selected_device(func):
"""Decorate a function to only execute if a device is selected."""
@ -251,7 +256,7 @@ class Window:
def initialize_gamepad_config(self):
"""Set slider and dropdown values when a gamepad is selected."""
devices = get_devices()
if devices[self.selected_device]['type'] == 'gamepad':
if GAMEPAD in devices[self.selected_device]['types']:
self.get('gamepad_separator').show()
self.get('gamepad_config').show()
else:
@ -342,7 +347,9 @@ class Window:
with HandlerDisabled(device_selection, self.on_select_device):
self.device_store.clear()
for device in devices:
icon_name = ICON_NAMES[devices[device].get('type')]
types = devices[device]['types']
significant_type = sorted(types, key=ICON_PRIORITIES.index)[0]
icon_name = ICON_NAMES[significant_type]
self.device_store.append([icon_name, device])
self.select_newest_preset()

@ -124,7 +124,7 @@ class Context:
target_code = system_mapping.get(output)
if target_code is None:
logger.error('Don\'t know what %s is', output)
logger.error('Don\'t know what "%s" is', output)
continue
for permutation in key.get_permutations():

@ -55,7 +55,7 @@ class EventProducer:
"""Construct the event producer without it doing anything yet."""
self.context = context
self.max_abs = None
self.abs_range = None
# events only take ints, so a movement of 0.3 needs to add
# up to 1.2 to affect the cursor, with 0.2 remaining
self.pending_rel = {REL_X: 0, REL_Y: 0, REL_WHEEL: 0, REL_HWHEEL: 0}
@ -112,8 +112,8 @@ class EventProducer:
self.pending_rel[code] -= output_value
return output_value
def set_max_abs_from(self, device):
"""Update the maximum value joysticks will report.
def set_abs_range_from(self, device):
"""Update the min and max values joysticks will report.
This information is needed for abs -> rel mapping.
"""
@ -122,38 +122,72 @@ class EventProducer:
logger.error('Expected device to not be None')
return
max_abs = utils.get_max_abs(device)
if max_abs in [0, 1, None]:
# max_abs of joysticks is usually a much higher number
abs_range = utils.get_abs_range(device)
if abs_range is None:
return
self.max_abs = max_abs
logger.debug('Max abs of "%s": %s', device.name, max_abs)
if abs_range[1] in [0, 1, None]:
# max abs_range of joysticks is usually a much higher number
return
self.set_abs_range(*abs_range)
logger.debug('ABS range of "%s": %s', device.name, abs_range)
def set_abs_range(self, min_abs, max_abs):
"""Update the min and max values joysticks will report.
This information is needed for abs -> rel mapping.
"""
self.abs_range = (min_abs, max_abs)
# all joysticks in resting position by default
center = (self.abs_range[1] + self.abs_range[0]) / 2
self.abs_state = {
ABS_X: center,
ABS_Y: center,
ABS_RX: center,
ABS_RY: center
}
def get_abs_values(self):
"""Get the raw values for wheel and mouse movement.
Returned values center around 0 and are normalized into -1 and 1.
If two joysticks have the same purpose, the one that reports higher
absolute values takes over the control.
"""
mouse_x, mouse_y, wheel_x, wheel_y = 0, 0, 0, 0
# center is the value of the resting position
center = (self.abs_range[1] + self.abs_range[0]) / 2
# normalizer is the maximum possible value after centering
normalizer = (self.abs_range[1] - self.abs_range[0]) / 2
mouse_x = 0
mouse_y = 0
wheel_x = 0
wheel_y = 0
def standardize(value):
return (value - center) / normalizer
if self.context.left_purpose == MOUSE:
mouse_x = abs_max(mouse_x, self.abs_state[ABS_X])
mouse_y = abs_max(mouse_y, self.abs_state[ABS_Y])
mouse_x = abs_max(mouse_x, standardize(self.abs_state[ABS_X]))
mouse_y = abs_max(mouse_y, standardize(self.abs_state[ABS_Y]))
if self.context.left_purpose == WHEEL:
wheel_x = abs_max(wheel_x, self.abs_state[ABS_X])
wheel_y = abs_max(wheel_y, self.abs_state[ABS_Y])
wheel_x = abs_max(wheel_x, standardize(self.abs_state[ABS_X]))
wheel_y = abs_max(wheel_y, standardize(self.abs_state[ABS_Y]))
if self.context.right_purpose == MOUSE:
mouse_x = abs_max(mouse_x, self.abs_state[ABS_RX])
mouse_y = abs_max(mouse_y, self.abs_state[ABS_RY])
mouse_x = abs_max(mouse_x, standardize(self.abs_state[ABS_RX]))
mouse_y = abs_max(mouse_y, standardize(self.abs_state[ABS_RY]))
if self.context.right_purpose == WHEEL:
wheel_x = abs_max(wheel_x, self.abs_state[ABS_RX])
wheel_y = abs_max(wheel_y, self.abs_state[ABS_RY])
wheel_x = abs_max(wheel_x, standardize(self.abs_state[ABS_RX]))
wheel_y = abs_max(wheel_y, standardize(self.abs_state[ABS_RY]))
# Some joysticks report from 0 to 255 (EMV101),
# others from -32768 to 32767 (X-Box 360 Pad)
return mouse_x, mouse_y, wheel_x, wheel_y
def is_handled(self, event):
@ -161,7 +195,7 @@ class EventProducer:
if event.type != EV_ABS or event.code not in utils.JOYSTICK:
return False
if self.max_abs is None:
if self.abs_range is None:
return False
purposes = [MOUSE, WHEEL]
@ -182,14 +216,15 @@ class EventProducer:
Even if no new input event arrived because the joystick remained at
its position, this will keep injecting the mouse movement events.
"""
max_abs = self.max_abs
abs_range = self.abs_range
mapping = self.context.mapping
pointer_speed = mapping.get('gamepad.joystick.pointer_speed')
non_linearity = mapping.get('gamepad.joystick.non_linearity')
x_scroll_speed = mapping.get('gamepad.joystick.x_scroll_speed')
y_scroll_speed = mapping.get('gamepad.joystick.y_scroll_speed')
max_speed = 2 ** 0.5 # for normalized abs event values
if max_abs is not None:
if abs_range is not None:
logger.info(
'Left joystick as %s, right joystick as %s',
self.context.left_purpose,
@ -217,20 +252,15 @@ class EventProducer:
"""mouse movement production"""
if max_abs is None:
if abs_range is None:
# no ev_abs events will be mapped to ev_rel
continue
max_speed = ((max_abs ** 2) * 2) ** 0.5
abs_values = self.get_abs_values()
if len([val for val in abs_values if val > max_abs]) > 0:
logger.error(
'Inconsistent values: %s, max_abs: %s',
abs_values, max_abs
)
return
if len([val for val in abs_values if not (-1 <= val <= 1)]) > 0:
logger.error('Inconsistent values: %s', abs_values)
continue
mouse_x, mouse_y, wheel_x, wheel_y = abs_values
@ -238,13 +268,13 @@ class EventProducer:
if abs(mouse_x) > 0 or abs(mouse_y) > 0:
if non_linearity != 1:
# to make small movements smaller for more precision
speed = (mouse_x ** 2 + mouse_y ** 2) ** 0.5
speed = (mouse_x ** 2 + mouse_y ** 2) ** 0.5 # pythagoras
factor = (speed / max_speed) ** non_linearity
else:
factor = 1
rel_x = (mouse_x / max_abs) * factor * pointer_speed
rel_y = (mouse_y / max_abs) * factor * pointer_speed
rel_x = mouse_x * factor * pointer_speed
rel_y = mouse_y * factor * pointer_speed
rel_x = self.accumulate(REL_X, rel_x)
rel_y = self.accumulate(REL_Y, rel_y)
if rel_x != 0:
@ -254,13 +284,13 @@ class EventProducer:
# wheel movements
if abs(wheel_x) > 0:
change = wheel_x * x_scroll_speed / max_abs
change = wheel_x * x_scroll_speed
value = self.accumulate(REL_WHEEL, change)
if abs(change) > WHEEL_THRESHOLD * x_scroll_speed:
self._write(EV_REL, REL_HWHEEL, value)
if abs(wheel_y) > 0:
change = wheel_y * y_scroll_speed / max_abs
change = wheel_y * y_scroll_speed
value = self.accumulate(REL_HWHEEL, change)
if abs(change) > WHEEL_THRESHOLD * y_scroll_speed:
self._write(EV_REL, REL_WHEEL, -value)

@ -338,10 +338,11 @@ class Injector(multiprocessing.Process):
# where mapped events go to.
# See the Context docstring on why this is needed.
is_gamepad = GAMEPAD in group['types']
self.context.uinput = evdev.UInput(
name=self.get_udef_name(self.device, 'mapped'),
phys=DEV_NAME,
events=self._construct_capabilities(group['type'] == 'gamepad')
events=self._construct_capabilities(is_gamepad)
)
# Watch over each one of the potentially multiple devices per hardware
@ -369,7 +370,7 @@ class Injector(multiprocessing.Process):
# that are needed for this. It is that one that will be mapped
# to a mouse-like devnode.
if gamepad and self.context.joystick_as_mouse():
self._event_producer.set_max_abs_from(source)
self._event_producer.set_abs_range_from(source)
if len(coroutines) == 0:
logger.error('Did not grab any device')

@ -216,7 +216,7 @@ class KeycodeMapper:
where forwarded/unhandled events should be written to
"""
self.source = source
self.max_abs = utils.get_max_abs(source)
self.abs_range = utils.get_abs_range(source)
self.context = context
self.forward_to = forward_to
@ -337,7 +337,7 @@ class KeycodeMapper:
# possible, because they might skip the 1 when pressed fast
# enough.
original_tuple = (event.type, event.code, event.value)
event.value = utils.normalize_value(event, self.max_abs)
event.value = utils.normalize_value(event, self.abs_range)
# the tuple of the actual input event. Used to forward the event if
# it is not mapped, and to index unreleased and active_macros. stays

@ -68,19 +68,27 @@ def sign(value):
return 0
def normalize_value(event, max_abs):
def normalize_value(event, abs_range=None):
"""Fit the event value to one of 0, 1 or -1."""
if event.type == EV_ABS and event.code in JOYSTICK:
if max_abs is None:
if abs_range is None:
logger.error(
'Got %s, but max_abs is %s',
(event.type, event.code, event.value), max_abs
(event.type, event.code, event.value), abs_range
)
return event.value
threshold = max_abs * JOYSTICK_BUTTON_THRESHOLD
triggered = abs(event.value) > threshold
return sign(event.value) if triggered else 0
# center is the value of the resting position
center = (abs_range[1] + abs_range[0]) / 2
# normalizer is the maximum possible value after centering
normalizer = (abs_range[1] - abs_range[0]) / 2
threshold = normalizer * JOYSTICK_BUTTON_THRESHOLD
triggered = abs(event.value - center) > threshold
return sign(event.value - center) if triggered else 0
# non-joystick abs events (triggers) usually start at 0 and go up to 255,
# but anything that is > 0 was safe to be treated as pressed so far
return sign(event.value)
@ -157,8 +165,8 @@ def should_map_as_btn(event, mapping, gamepad):
return False
def get_max_abs(device):
"""Figure out the maximum value of EV_ABS events of that device.
def get_abs_range(device, code=ABS_X):
"""Figure out the max and min value of EV_ABS events of that device.
Like joystick movements or triggers.
"""
@ -169,16 +177,31 @@ def get_max_abs(device):
if EV_ABS not in capabilities:
return None
absinfos = [
absinfo = [
entry[1] for entry in
capabilities[EV_ABS]
if isinstance(entry, tuple) and isinstance(entry[1], evdev.AbsInfo)
if (
entry[0] == code
and isinstance(entry, tuple)
and isinstance(entry[1], evdev.AbsInfo)
)
]
if len(absinfos) == 0:
logger.error('Failed to get max abs of "%s"')
if len(absinfo) == 0:
logger.error(
'Failed to get ABS info of "%s" for key %d: %s',
device, code, capabilities
)
return None
max_abs = absinfos[0].max
absinfo = absinfo[0]
return absinfo.min, absinfo.max
return max_abs
def get_max_abs(device, code=ABS_X):
"""Figure out the max value of EV_ABS events of that device.
Like joystick movements or triggers.
"""
abs_range = get_abs_range(device, code)
return abs_range and abs_range[1]

@ -43,6 +43,7 @@ file should give an overview about some internals of key-mapper.
```bash
sudo pip install coverage
pylint keymapper --extension-pkg-whitelist=evdev
sudo pkill -f key-mapper
sudo pip install . && coverage run tests/test.py
coverage combine && coverage report -m
```
@ -56,6 +57,9 @@ Single tests can be executed via
python3 tests/test.py test_paths.TestPaths.test_mkdir
```
Don't use your computer during integration tests to avoid interacting
with the gui, which might make tests fail.
## Releasing
ssh/login into a debian/ubuntu environment

@ -94,6 +94,8 @@ EVENT_READ_TIMEOUT = 0.01
# call to start_reading
START_READING_DELAY = 0.05
# for joysticks
MIN_ABS = -2 ** 15
MAX_ABS = 2 ** 15
@ -375,7 +377,7 @@ class InputDevice:
if absinfo and evdev.ecodes.EV_ABS in result:
absinfo_obj = evdev.AbsInfo(
value=None, min=None, fuzz=None, flat=None,
value=None, min=MIN_ABS, fuzz=None, flat=None,
resolution=None, max=MAX_ABS
)
result[evdev.ecodes.EV_ABS] = [
@ -517,9 +519,15 @@ def quick_cleanup(log=True):
if log:
print('quick cleanup')
for key in list(pending_events.keys()):
while pending_events[key][1].poll():
pending_events[key][1].recv()
for device in list(pending_events.keys()):
try:
while pending_events[device][1].poll():
pending_events[device][1].recv()
except EOFError:
# it broke, set up a new pipe
pending_events[device] = None
setup_pipe(device)
pass
try:
reader.terminate()
@ -547,10 +555,10 @@ def quick_cleanup(log=True):
for name in list(uinputs.keys()):
del uinputs[name]
for key in list(active_macros.keys()):
del active_macros[key]
for key in list(unreleased.keys()):
del unreleased[key]
for device in list(active_macros.keys()):
del active_macros[device]
for device in list(unreleased.keys()):
del unreleased[device]
for path in list(fixtures.keys()):
if path not in _fixture_copy:
@ -560,9 +568,9 @@ def quick_cleanup(log=True):
fixtures[path] = _fixture_copy[path]
os.environ.update(environ_copy)
for key in list(os.environ.keys()):
if key not in environ_copy:
del os.environ[key]
for device in list(os.environ.keys()):
if device not in environ_copy:
del os.environ[device]
join_children()

@ -29,13 +29,13 @@ from keymapper.config import config, BUTTONS
from keymapper.mapping import Mapping
from keymapper import utils
from tests.test import new_event, InputDevice, MAX_ABS
from tests.test import new_event, InputDevice, MAX_ABS, MIN_ABS
class TestDevUtils(unittest.TestCase):
def test_max_abs(self):
self.assertEqual(utils.get_max_abs(InputDevice('/dev/input/event30')), MAX_ABS)
self.assertIsNone(utils.get_max_abs(InputDevice('/dev/input/event10')))
self.assertEqual(utils.get_abs_range(InputDevice('/dev/input/event30'))[1], MAX_ABS)
self.assertIsNone(utils.get_abs_range(InputDevice('/dev/input/event10')))
def test_will_report_key_up(self):
self.assertFalse(
@ -127,22 +127,66 @@ class TestDevUtils(unittest.TestCase):
self.assertFalse(do(1, new_event(EV_ABS, ecodes.ABS_MISC, -1)))
def test_normalize_value(self):
""""""
"""0 to MAX_ABS"""
def do(event):
return utils.normalize_value(event, MAX_ABS)
return utils.normalize_value(event, (0, MAX_ABS))
event = new_event(EV_ABS, ecodes.ABS_RX, MAX_ABS)
self.assertEqual(do(event), 1)
event = new_event(EV_ABS, ecodes.ABS_Y, -MAX_ABS)
event = new_event(EV_ABS, ecodes.ABS_Y, MAX_ABS)
self.assertEqual(do(event), 1)
event = new_event(EV_ABS, ecodes.ABS_Y, 0)
self.assertEqual(do(event), -1)
event = new_event(EV_ABS, ecodes.ABS_X, MAX_ABS // 4)
self.assertEqual(do(event), -1)
event = new_event(EV_ABS, ecodes.ABS_X, -MAX_ABS // 4)
event = new_event(EV_ABS, ecodes.ABS_X, MAX_ABS // 2)
self.assertEqual(do(event), 0)
"""MIN_ABS to MAX_ABS"""
def do2(event):
return utils.normalize_value(event, (MIN_ABS, MAX_ABS))
event = new_event(EV_ABS, ecodes.ABS_RX, MAX_ABS)
self.assertEqual(do(event), 1)
self.assertEqual(do2(event), 1)
event = new_event(EV_ABS, ecodes.ABS_Y, MIN_ABS)
self.assertEqual(do2(event), -1)
event = new_event(EV_ABS, ecodes.ABS_X, MIN_ABS // 4)
self.assertEqual(do2(event), 0)
event = new_event(EV_ABS, ecodes.ABS_RX, MAX_ABS)
self.assertEqual(do2(event), 1)
event = new_event(EV_ABS, ecodes.ABS_Y, MAX_ABS)
self.assertEqual(do(event), 1)
self.assertEqual(do2(event), 1)
event = new_event(EV_ABS, ecodes.ABS_X, MAX_ABS // 4)
self.assertEqual(do(event), 0)
self.assertEqual(do2(event), 0)
"""None"""
# if none, it just forwards the value
# it just forwards the value
event = new_event(EV_ABS, ecodes.ABS_RX, MAX_ABS)
self.assertEqual(utils.normalize_value(event, None), MAX_ABS)
"""Not a joystick"""
event = new_event(EV_ABS, ecodes.ABS_Z, 1234)
self.assertEqual(do(event), 1)
self.assertEqual(do2(event), 1)
event = new_event(EV_ABS, ecodes.ABS_Z, 0)
self.assertEqual(do(event), 0)
self.assertEqual(do2(event), 0)
event = new_event(EV_ABS, ecodes.ABS_Z, -1234)
self.assertEqual(do(event), -1)
self.assertEqual(do2(event), -1)
event = new_event(EV_KEY, ecodes.KEY_A, 1)
self.assertEqual(do(event), 1)
self.assertEqual(do2(event), 1)
event = new_event(EV_ABS, ecodes.ABS_HAT0X, 0)
self.assertEqual(do(event), 0)
self.assertEqual(do2(event), 0)
event = new_event(EV_ABS, ecodes.ABS_HAT0X, -1)
self.assertEqual(do(event), -1)
self.assertEqual(do2(event), -1)

@ -31,7 +31,7 @@ from keymapper.injection.context import Context
from keymapper.injection.event_producer import EventProducer, MOUSE, WHEEL
from tests.test import InputDevice, UInput, MAX_ABS, clear_write_history, \
uinput_write_history, quick_cleanup, new_event
uinput_write_history, quick_cleanup, new_event, MIN_ABS
abs_state = [0, 0, 0, 0]
@ -50,7 +50,7 @@ class TestEventProducer(unittest.TestCase):
device = InputDevice('/dev/input/event30')
self.event_producer = EventProducer(self.context)
self.event_producer.set_max_abs_from(device)
self.event_producer.set_abs_range_from(device)
asyncio.ensure_future(self.event_producer.run())
config.set('gamepad.joystick.x_scroll_speed', 1)
@ -112,6 +112,26 @@ class TestEventProducer(unittest.TestCase):
self.assertEqual(history[0], 1)
self.assertEqual(history[1], 2)
def assertClose(self, a, b, within):
"""a has to be within b - b * within, b + b * within."""
self.assertLess(a - abs(a) * within, b)
self.assertGreater(a + abs(a) * within, b)
def test_assertClose(self):
self.assertClose(5, 5, 0.1)
self.assertClose(5, 5, 1)
self.assertClose(6, 5, 0.2)
self.assertClose(4, 5, 0.3)
self.assertRaises(AssertionError, lambda: self.assertClose(6, 5, 0.1))
self.assertRaises(AssertionError, lambda: self.assertClose(4, 5, 0.1))
self.assertClose(-5, -5, 0.1)
self.assertClose(-5, -5, 1)
self.assertClose(-6, -5, 0.2)
self.assertClose(-4, -5, 0.3)
self.assertRaises(AssertionError, lambda: self.assertClose(-6, -5, 0.1))
self.assertRaises(AssertionError, lambda: self.assertClose(-4, -5, 0.1))
def do(self, a, b, c, d, expectation):
"""Present fake values to the loop and observe the outcome."""
clear_write_history()
@ -127,7 +147,11 @@ class TestEventProducer(unittest.TestCase):
# sleep long enough to test if multiple events are written
self.assertGreater(len(history), 1)
self.assertIn(expectation, history)
self.assertEqual(history.count(expectation), len(history))
for history_entry in history:
self.assertEqual(history_entry[:2], expectation[:2])
# if the injected cursor movement is 19 or 20 doesn't really matter
self.assertClose(history_entry[2], expectation[2], 0.1)
def test_joystick_purpose_1(self):
speed = 20
@ -136,16 +160,24 @@ class TestEventProducer(unittest.TestCase):
self.mapping.set('gamepad.joystick.left_purpose', MOUSE)
self.mapping.set('gamepad.joystick.right_purpose', WHEEL)
self.do(MAX_ABS, 0, 0, 0, (EV_REL, REL_X, speed))
self.do(-MAX_ABS, 0, 0, 0, (EV_REL, REL_X, -speed))
self.do(0, MAX_ABS, 0, 0, (EV_REL, REL_Y, speed))
self.do(0, -MAX_ABS, 0, 0, (EV_REL, REL_Y, -speed))
min_abs = 0
# if `rest` is not exactly `max_abs / 2` decimal places might add up
# and cause higher or lower values to be written after a few events,
# which might be difficult to test.
max_abs = 256
rest = 128 # resting position of the cursor
self.event_producer.set_abs_range(min_abs, max_abs)
self.do(max_abs, rest, rest, rest, (EV_REL, REL_X, speed))
self.do(min_abs, rest, rest, rest, (EV_REL, REL_X, -speed))
self.do(rest, max_abs, rest, rest, (EV_REL, REL_Y, speed))
self.do(rest, min_abs, rest, rest, (EV_REL, REL_Y, -speed))
# vertical wheel event values are negative
self.do(0, 0, MAX_ABS, 0, (EV_REL, REL_HWHEEL, 1))
self.do(0, 0, -MAX_ABS, 0, (EV_REL, REL_HWHEEL, -1))
self.do(0, 0, 0, MAX_ABS, (EV_REL, REL_WHEEL, -1))
self.do(0, 0, 0, -MAX_ABS, (EV_REL, REL_WHEEL, 1))
self.do(rest, rest, max_abs, rest, (EV_REL, REL_HWHEEL, 1))
self.do(rest, rest, min_abs, rest, (EV_REL, REL_HWHEEL, -1))
self.do(rest, rest, rest, max_abs, (EV_REL, REL_WHEEL, -1))
self.do(rest, rest, rest, min_abs, (EV_REL, REL_WHEEL, 1))
def test_joystick_purpose_2(self):
speed = 30
@ -158,14 +190,14 @@ class TestEventProducer(unittest.TestCase):
# vertical wheel event values are negative
self.do(MAX_ABS, 0, 0, 0, (EV_REL, REL_HWHEEL, 1))
self.do(-MAX_ABS, 0, 0, 0, (EV_REL, REL_HWHEEL, -1))
self.do(MIN_ABS, 0, 0, 0, (EV_REL, REL_HWHEEL, -1))
self.do(0, MAX_ABS, 0, 0, (EV_REL, REL_WHEEL, -2))
self.do(0, -MAX_ABS, 0, 0, (EV_REL, REL_WHEEL, 2))
self.do(0, MIN_ABS, 0, 0, (EV_REL, REL_WHEEL, 2))
self.do(0, 0, MAX_ABS, 0, (EV_REL, REL_X, speed))
self.do(0, 0, -MAX_ABS, 0, (EV_REL, REL_X, -speed))
self.do(0, 0, MIN_ABS, 0, (EV_REL, REL_X, -speed))
self.do(0, 0, 0, MAX_ABS, (EV_REL, REL_Y, speed))
self.do(0, 0, 0, -MAX_ABS, (EV_REL, REL_Y, -speed))
self.do(0, 0, 0, MIN_ABS, (EV_REL, REL_Y, -speed))
def test_joystick_purpose_3(self):
speed = 40
@ -175,14 +207,14 @@ class TestEventProducer(unittest.TestCase):
config.set('gamepad.joystick.right_purpose', MOUSE)
self.do(MAX_ABS, 0, 0, 0, (EV_REL, REL_X, speed))
self.do(-MAX_ABS, 0, 0, 0, (EV_REL, REL_X, -speed))
self.do(MIN_ABS, 0, 0, 0, (EV_REL, REL_X, -speed))
self.do(0, MAX_ABS, 0, 0, (EV_REL, REL_Y, speed))
self.do(0, -MAX_ABS, 0, 0, (EV_REL, REL_Y, -speed))
self.do(0, MIN_ABS, 0, 0, (EV_REL, REL_Y, -speed))
self.do(0, 0, MAX_ABS, 0, (EV_REL, REL_X, speed))
self.do(0, 0, -MAX_ABS, 0, (EV_REL, REL_X, -speed))
self.do(0, 0, MIN_ABS, 0, (EV_REL, REL_X, -speed))
self.do(0, 0, 0, MAX_ABS, (EV_REL, REL_Y, speed))
self.do(0, 0, 0, -MAX_ABS, (EV_REL, REL_Y, -speed))
self.do(0, 0, 0, MIN_ABS, (EV_REL, REL_Y, -speed))
def test_joystick_purpose_4(self):
config.set('gamepad.joystick.left_purpose', WHEEL)
@ -191,15 +223,15 @@ class TestEventProducer(unittest.TestCase):
self.mapping.set('gamepad.joystick.y_scroll_speed', 3)
self.do(MAX_ABS, 0, 0, 0, (EV_REL, REL_HWHEEL, 2))
self.do(-MAX_ABS, 0, 0, 0, (EV_REL, REL_HWHEEL, -2))
self.do(MIN_ABS, 0, 0, 0, (EV_REL, REL_HWHEEL, -2))
self.do(0, MAX_ABS, 0, 0, (EV_REL, REL_WHEEL, -3))
self.do(0, -MAX_ABS, 0, 0, (EV_REL, REL_WHEEL, 3))
self.do(0, MIN_ABS, 0, 0, (EV_REL, REL_WHEEL, 3))
# vertical wheel event values are negative
self.do(0, 0, MAX_ABS, 0, (EV_REL, REL_HWHEEL, 2))
self.do(0, 0, -MAX_ABS, 0, (EV_REL, REL_HWHEEL, -2))
self.do(0, 0, MIN_ABS, 0, (EV_REL, REL_HWHEEL, -2))
self.do(0, 0, 0, MAX_ABS, (EV_REL, REL_WHEEL, -3))
self.do(0, 0, 0, -MAX_ABS, (EV_REL, REL_WHEEL, 3))
self.do(0, 0, 0, MIN_ABS, (EV_REL, REL_WHEEL, 3))
if __name__ == "__main__":

@ -57,22 +57,22 @@ class TestGetDevices(unittest.TestCase):
'device 1',
'device 1'
],
'type': MOUSE
'types': [KEYBOARD, MOUSE]
},
'device 2': {
'paths': ['/dev/input/event20'],
'devices': ['device 2'],
'type': KEYBOARD
'types': [KEYBOARD]
},
'gamepad': {
'paths': ['/dev/input/event30'],
'devices': ['gamepad'],
'type': GAMEPAD
'types': [GAMEPAD]
},
'key-mapper device 2': {
'paths': ['/dev/input/event40'],
'devices': ['key-mapper device 2'],
'type': KEYBOARD
'types': [KEYBOARD]
},
})
self.assertDictEqual(pipe.devices, get_devices(include_keymapper=True))
@ -90,17 +90,17 @@ class TestGetDevices(unittest.TestCase):
'device 1',
'device 1'
],
'type': MOUSE
'types': [KEYBOARD, MOUSE]
},
'device 2': {
'paths': ['/dev/input/event20'],
'devices': ['device 2'],
'type': KEYBOARD
'types': [KEYBOARD]
},
'gamepad': {
'paths': ['/dev/input/event30'],
'devices': ['gamepad'],
'type': GAMEPAD
'types': [GAMEPAD]
},
})
@ -169,7 +169,7 @@ class TestGetDevices(unittest.TestCase):
"""mice"""
self.assertEqual(classify(FakeDevice({
EV_REL: [evdev.ecodes.REL_X, evdev.ecodes.REL_Y],
EV_REL: [evdev.ecodes.REL_X, evdev.ecodes.REL_Y, evdev.ecodes.REL_WHEEL],
EV_KEY: [evdev.ecodes.BTN_LEFT]
})), MOUSE)
@ -186,13 +186,14 @@ class TestGetDevices(unittest.TestCase):
EV_ABS: [evdev.ecodes.ABS_MT_POSITION_X]
})), TOUCHPAD)
"""weird combos"""
"""graphics tablets"""
self.assertEqual(classify(FakeDevice({
EV_ABS: [evdev.ecodes.ABS_X, evdev.ecodes.ABS_Y],
EV_KEY: [evdev.ecodes.BTN_A],
EV_REL: [evdev.ecodes.REL_X]
})), UNKNOWN)
EV_KEY: [evdev.ecodes.BTN_STYLUS]
})), GRAPHICS_TABLET)
"""weird combos"""
self.assertEqual(classify(FakeDevice({
EV_ABS: [evdev.ecodes.ABS_X, evdev.ecodes.ABS_Y],

@ -44,7 +44,7 @@ from keymapper.getdevices import get_devices, classify, GAMEPAD
from tests.test import new_event, push_events, fixtures, \
EVENT_READ_TIMEOUT, uinput_write_history_pipe, \
MAX_ABS, quick_cleanup, read_write_history_pipe, InputDevice, uinputs, \
keyboard_keys
keyboard_keys, MIN_ABS
class TestInjector(unittest.TestCase):
@ -418,7 +418,8 @@ class TestInjector(unittest.TestCase):
self.injector.run()
# not in a process, so the event_producer state can be checked
self.assertEqual(self.injector._event_producer.max_abs, MAX_ABS)
self.assertEqual(self.injector._event_producer.abs_range[0], MIN_ABS)
self.assertEqual(self.injector._event_producer.abs_range[1], MAX_ABS)
self.assertEqual(
self.injector.context.mapping.get('gamepad.joystick.left_purpose'),
MOUSE
@ -430,7 +431,7 @@ class TestInjector(unittest.TestCase):
self.injector = Injector('gamepad', custom_mapping)
self.injector.stop_injecting()
self.injector.run()
self.assertIsNone(self.injector._event_producer.max_abs, MAX_ABS)
self.assertIsNone(self.injector._event_producer.abs_range)
def test_device1_event_producer(self):
custom_mapping.set('gamepad.joystick.left_purpose', MOUSE)
@ -440,7 +441,7 @@ class TestInjector(unittest.TestCase):
self.injector.run()
# 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.abs_range)
def test_get_udef_name(self):
self.injector = Injector('device 1', custom_mapping)

@ -51,7 +51,7 @@ from keymapper.gui.helper import RootHelper
from tests.test import tmp, push_events, new_event, spy, cleanup, \
uinput_write_history_pipe, MAX_ABS, EVENT_READ_TIMEOUT, \
send_event_to_reader
send_event_to_reader, MIN_ABS
def gtk_iteration():
@ -1235,7 +1235,7 @@ class TestIntegration(unittest.TestCase):
time.sleep(0.1)
push_events('gamepad', [
new_event(EV_ABS, ABS_RX, -MAX_ABS),
new_event(EV_ABS, ABS_RX, MIN_ABS),
new_event(EV_ABS, ABS_X, MAX_ABS)
] * 100)

@ -35,7 +35,7 @@ from keymapper.config import config, BUTTONS
from keymapper.mapping import Mapping, DISABLE_CODE
from tests.test import new_event, UInput, uinput_write_history, \
quick_cleanup, InputDevice, MAX_ABS
quick_cleanup, InputDevice, MAX_ABS, MIN_ABS
def wait(func, timeout=1.0):
@ -210,7 +210,7 @@ class TestKeycodeMapper(unittest.TestCase):
# with the left joystick mapped as button, it will release the mapped
# key when it goes back to close to its resting position
ev_1 = (3, 0, MAX_ABS // 10) # release
ev_3 = (3, 0, -MAX_ABS) # press
ev_3 = (3, 0, MIN_ABS) # press
uinput = UInput()
@ -422,7 +422,7 @@ class TestKeycodeMapper(unittest.TestCase):
def test_combination_keycode_2(self):
combination_1 = (
(EV_KEY, 1, 1),
(EV_ABS, ABS_Y, -MAX_ABS),
(EV_ABS, ABS_Y, MIN_ABS),
(EV_KEY, 3, 1),
(EV_KEY, 4, 1)
)

Loading…
Cancel
Save