#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 import evdev
from evdev.ecodes import EV_KEY, EV_ABS, KEY_CAMERA, EV_REL, BTN_STYLUS, \ 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 from keymapper.logger import logger
@ -51,11 +51,6 @@ GRAPHICS_TABLET = 'graphics-tablet'
CAMERA = 'camera' CAMERA = 'camera'
UNKNOWN = 'unknown' 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'): if not hasattr(evdev.InputDevice, 'path'):
# for evdev < 1.0.0 patch the path property # for evdev < 1.0.0 patch the path property
@ -68,10 +63,17 @@ if not hasattr(evdev.InputDevice, 'path'):
def _is_gamepad(capabilities): def _is_gamepad(capabilities):
"""Check if joystick movements are available for mapping.""" """Check if joystick movements are available for mapping."""
if len(capabilities.get(EV_REL, [])) > 0: # A few buttons that indicate a gamepad
return False buttons = {
evdev.ecodes.BTN_BASE,
if BTN_A not in capabilities.get(EV_KEY, []): 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 return False
# joysticks # joysticks
@ -86,9 +88,20 @@ def _is_gamepad(capabilities):
def _is_mouse(capabilities): def _is_mouse(capabilities):
"""Check if the capabilities represent those of a mouse.""" """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, []): if not REL_X in capabilities.get(EV_REL, []):
return False 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, []): if not BTN_LEFT in capabilities.get(EV_KEY, []):
return False return False
@ -138,12 +151,12 @@ def classify(device):
if _is_touchpad(capabilities): if _is_touchpad(capabilities):
return TOUCHPAD return TOUCHPAD
if _is_mouse(capabilities):
return MOUSE
if _is_gamepad(capabilities): if _is_gamepad(capabilities):
return GAMEPAD return GAMEPAD
if _is_mouse(capabilities):
return MOUSE
if _is_camera(capabilities): if _is_camera(capabilities):
return CAMERA return CAMERA
@ -230,16 +243,15 @@ class _GetDevices(threading.Thread):
names = [entry[0] for entry in group] names = [entry[0] for entry in group]
devs = [entry[1] 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] shortest_name = sorted(names, key=len)[0]
result[shortest_name] = { result[shortest_name] = {
'paths': devs, 'paths': devs,
'devices': names, '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) self.pipe.send(result)

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

@ -35,7 +35,7 @@ from keymapper.ipc.pipe import Pipe
from keymapper.gui.helper import TERMINATE from keymapper.gui.helper import TERMINATE
from keymapper import utils from keymapper import utils
from keymapper.state import custom_mapping from keymapper.state import custom_mapping
from keymapper.getdevices import get_devices from keymapper.getdevices import get_devices, GAMEPAD
DEBOUNCE_TICKS = 3 DEBOUNCE_TICKS = 3
@ -133,7 +133,7 @@ class Reader:
if event is None: if event is None:
continue 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): if not utils.should_map_as_btn(event, custom_mapping, gamepad):
continue continue

@ -70,6 +70,11 @@ ICON_NAMES = {
UNKNOWN: None, 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): def with_selected_device(func):
"""Decorate a function to only execute if a device is selected.""" """Decorate a function to only execute if a device is selected."""
@ -251,7 +256,7 @@ class Window:
def initialize_gamepad_config(self): def initialize_gamepad_config(self):
"""Set slider and dropdown values when a gamepad is selected.""" """Set slider and dropdown values when a gamepad is selected."""
devices = get_devices() 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_separator').show()
self.get('gamepad_config').show() self.get('gamepad_config').show()
else: else:
@ -342,7 +347,9 @@ class Window:
with HandlerDisabled(device_selection, self.on_select_device): with HandlerDisabled(device_selection, self.on_select_device):
self.device_store.clear() self.device_store.clear()
for device in devices: 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.device_store.append([icon_name, device])
self.select_newest_preset() self.select_newest_preset()

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

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

@ -338,10 +338,11 @@ class Injector(multiprocessing.Process):
# where mapped events go to. # where mapped events go to.
# See the Context docstring on why this is needed. # See the Context docstring on why this is needed.
is_gamepad = GAMEPAD in group['types']
self.context.uinput = evdev.UInput( self.context.uinput = evdev.UInput(
name=self.get_udef_name(self.device, 'mapped'), name=self.get_udef_name(self.device, 'mapped'),
phys=DEV_NAME, 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 # 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 # that are needed for this. It is that one that will be mapped
# to a mouse-like devnode. # to a mouse-like devnode.
if gamepad and self.context.joystick_as_mouse(): 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: if len(coroutines) == 0:
logger.error('Did not grab any device') logger.error('Did not grab any device')

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

@ -68,19 +68,27 @@ def sign(value):
return 0 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.""" """Fit the event value to one of 0, 1 or -1."""
if event.type == EV_ABS and event.code in JOYSTICK: if event.type == EV_ABS and event.code in JOYSTICK:
if max_abs is None: if abs_range is None:
logger.error( logger.error(
'Got %s, but max_abs is %s', '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 return event.value
threshold = max_abs * JOYSTICK_BUTTON_THRESHOLD # center is the value of the resting position
triggered = abs(event.value) > threshold center = (abs_range[1] + abs_range[0]) / 2
return sign(event.value) if triggered else 0 # 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) return sign(event.value)
@ -157,8 +165,8 @@ def should_map_as_btn(event, mapping, gamepad):
return False return False
def get_max_abs(device): def get_abs_range(device, code=ABS_X):
"""Figure out the maximum value of EV_ABS events of that device. """Figure out the max and min value of EV_ABS events of that device.
Like joystick movements or triggers. Like joystick movements or triggers.
""" """
@ -169,16 +177,31 @@ def get_max_abs(device):
if EV_ABS not in capabilities: if EV_ABS not in capabilities:
return None return None
absinfos = [ absinfo = [
entry[1] for entry in entry[1] for entry in
capabilities[EV_ABS] 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: if len(absinfo) == 0:
logger.error('Failed to get max abs of "%s"') logger.error(
'Failed to get ABS info of "%s" for key %d: %s',
device, code, capabilities
)
return None 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 ```bash
sudo pip install coverage sudo pip install coverage
pylint keymapper --extension-pkg-whitelist=evdev pylint keymapper --extension-pkg-whitelist=evdev
sudo pkill -f key-mapper
sudo pip install . && coverage run tests/test.py sudo pip install . && coverage run tests/test.py
coverage combine && coverage report -m coverage combine && coverage report -m
``` ```
@ -56,6 +57,9 @@ Single tests can be executed via
python3 tests/test.py test_paths.TestPaths.test_mkdir 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 ## Releasing
ssh/login into a debian/ubuntu environment ssh/login into a debian/ubuntu environment

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

@ -29,13 +29,13 @@ from keymapper.config import config, BUTTONS
from keymapper.mapping import Mapping from keymapper.mapping import Mapping
from keymapper import utils 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): class TestDevUtils(unittest.TestCase):
def test_max_abs(self): def test_max_abs(self):
self.assertEqual(utils.get_max_abs(InputDevice('/dev/input/event30')), MAX_ABS) self.assertEqual(utils.get_abs_range(InputDevice('/dev/input/event30'))[1], MAX_ABS)
self.assertIsNone(utils.get_max_abs(InputDevice('/dev/input/event10'))) self.assertIsNone(utils.get_abs_range(InputDevice('/dev/input/event10')))
def test_will_report_key_up(self): def test_will_report_key_up(self):
self.assertFalse( self.assertFalse(
@ -127,22 +127,66 @@ class TestDevUtils(unittest.TestCase):
self.assertFalse(do(1, 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 test_normalize_value(self):
""""""
"""0 to MAX_ABS"""
def do(event): 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) event = new_event(EV_ABS, ecodes.ABS_RX, MAX_ABS)
self.assertEqual(do(event), 1) 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) 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) 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) 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) 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) 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) event = new_event(EV_ABS, ecodes.ABS_RX, MAX_ABS)
self.assertEqual(utils.normalize_value(event, None), 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 keymapper.injection.event_producer import EventProducer, MOUSE, WHEEL
from tests.test import InputDevice, UInput, MAX_ABS, clear_write_history, \ 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] abs_state = [0, 0, 0, 0]
@ -50,7 +50,7 @@ class TestEventProducer(unittest.TestCase):
device = InputDevice('/dev/input/event30') device = InputDevice('/dev/input/event30')
self.event_producer = EventProducer(self.context) 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()) asyncio.ensure_future(self.event_producer.run())
config.set('gamepad.joystick.x_scroll_speed', 1) config.set('gamepad.joystick.x_scroll_speed', 1)
@ -112,6 +112,26 @@ class TestEventProducer(unittest.TestCase):
self.assertEqual(history[0], 1) self.assertEqual(history[0], 1)
self.assertEqual(history[1], 2) 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): def do(self, a, b, c, d, expectation):
"""Present fake values to the loop and observe the outcome.""" """Present fake values to the loop and observe the outcome."""
clear_write_history() clear_write_history()
@ -127,7 +147,11 @@ class TestEventProducer(unittest.TestCase):
# sleep long enough to test if multiple events are written # sleep long enough to test if multiple events are written
self.assertGreater(len(history), 1) self.assertGreater(len(history), 1)
self.assertIn(expectation, history) 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): def test_joystick_purpose_1(self):
speed = 20 speed = 20
@ -136,16 +160,24 @@ class TestEventProducer(unittest.TestCase):
self.mapping.set('gamepad.joystick.left_purpose', MOUSE) self.mapping.set('gamepad.joystick.left_purpose', MOUSE)
self.mapping.set('gamepad.joystick.right_purpose', WHEEL) self.mapping.set('gamepad.joystick.right_purpose', WHEEL)
self.do(MAX_ABS, 0, 0, 0, (EV_REL, REL_X, speed)) min_abs = 0
self.do(-MAX_ABS, 0, 0, 0, (EV_REL, REL_X, -speed)) # if `rest` is not exactly `max_abs / 2` decimal places might add up
self.do(0, MAX_ABS, 0, 0, (EV_REL, REL_Y, speed)) # and cause higher or lower values to be written after a few events,
self.do(0, -MAX_ABS, 0, 0, (EV_REL, REL_Y, -speed)) # 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 # vertical wheel event values are negative
self.do(0, 0, MAX_ABS, 0, (EV_REL, REL_HWHEEL, 1)) self.do(rest, rest, max_abs, rest, (EV_REL, REL_HWHEEL, 1))
self.do(0, 0, -MAX_ABS, 0, (EV_REL, REL_HWHEEL, -1)) self.do(rest, rest, min_abs, rest, (EV_REL, REL_HWHEEL, -1))
self.do(0, 0, 0, MAX_ABS, (EV_REL, REL_WHEEL, -1)) self.do(rest, rest, rest, max_abs, (EV_REL, REL_WHEEL, -1))
self.do(0, 0, 0, -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): def test_joystick_purpose_2(self):
speed = 30 speed = 30
@ -158,14 +190,14 @@ class TestEventProducer(unittest.TestCase):
# vertical wheel event values are negative # 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(-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, -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, -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, -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): def test_joystick_purpose_3(self):
speed = 40 speed = 40
@ -175,14 +207,14 @@ class TestEventProducer(unittest.TestCase):
config.set('gamepad.joystick.right_purpose', MOUSE) 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(-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, -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, -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, -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): def test_joystick_purpose_4(self):
config.set('gamepad.joystick.left_purpose', WHEEL) 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.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(-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, -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 # 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, -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, -MAX_ABS, (EV_REL, REL_WHEEL, 3)) self.do(0, 0, 0, MIN_ABS, (EV_REL, REL_WHEEL, 3))
if __name__ == "__main__": if __name__ == "__main__":

@ -57,22 +57,22 @@ class TestGetDevices(unittest.TestCase):
'device 1', 'device 1',
'device 1' 'device 1'
], ],
'type': MOUSE 'types': [KEYBOARD, MOUSE]
}, },
'device 2': { 'device 2': {
'paths': ['/dev/input/event20'], 'paths': ['/dev/input/event20'],
'devices': ['device 2'], 'devices': ['device 2'],
'type': KEYBOARD 'types': [KEYBOARD]
}, },
'gamepad': { 'gamepad': {
'paths': ['/dev/input/event30'], 'paths': ['/dev/input/event30'],
'devices': ['gamepad'], 'devices': ['gamepad'],
'type': GAMEPAD 'types': [GAMEPAD]
}, },
'key-mapper device 2': { 'key-mapper device 2': {
'paths': ['/dev/input/event40'], 'paths': ['/dev/input/event40'],
'devices': ['key-mapper device 2'], 'devices': ['key-mapper device 2'],
'type': KEYBOARD 'types': [KEYBOARD]
}, },
}) })
self.assertDictEqual(pipe.devices, get_devices(include_keymapper=True)) self.assertDictEqual(pipe.devices, get_devices(include_keymapper=True))
@ -90,17 +90,17 @@ class TestGetDevices(unittest.TestCase):
'device 1', 'device 1',
'device 1' 'device 1'
], ],
'type': MOUSE 'types': [KEYBOARD, MOUSE]
}, },
'device 2': { 'device 2': {
'paths': ['/dev/input/event20'], 'paths': ['/dev/input/event20'],
'devices': ['device 2'], 'devices': ['device 2'],
'type': KEYBOARD 'types': [KEYBOARD]
}, },
'gamepad': { 'gamepad': {
'paths': ['/dev/input/event30'], 'paths': ['/dev/input/event30'],
'devices': ['gamepad'], 'devices': ['gamepad'],
'type': GAMEPAD 'types': [GAMEPAD]
}, },
}) })
@ -169,7 +169,7 @@ class TestGetDevices(unittest.TestCase):
"""mice""" """mice"""
self.assertEqual(classify(FakeDevice({ 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] EV_KEY: [evdev.ecodes.BTN_LEFT]
})), MOUSE) })), MOUSE)
@ -186,13 +186,14 @@ class TestGetDevices(unittest.TestCase):
EV_ABS: [evdev.ecodes.ABS_MT_POSITION_X] EV_ABS: [evdev.ecodes.ABS_MT_POSITION_X]
})), TOUCHPAD) })), TOUCHPAD)
"""weird combos""" """graphics tablets"""
self.assertEqual(classify(FakeDevice({ self.assertEqual(classify(FakeDevice({
EV_ABS: [evdev.ecodes.ABS_X, evdev.ecodes.ABS_Y], EV_ABS: [evdev.ecodes.ABS_X, evdev.ecodes.ABS_Y],
EV_KEY: [evdev.ecodes.BTN_A], EV_KEY: [evdev.ecodes.BTN_STYLUS]
EV_REL: [evdev.ecodes.REL_X] })), GRAPHICS_TABLET)
})), UNKNOWN)
"""weird combos"""
self.assertEqual(classify(FakeDevice({ self.assertEqual(classify(FakeDevice({
EV_ABS: [evdev.ecodes.ABS_X, evdev.ecodes.ABS_Y], 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, \ from tests.test import new_event, push_events, fixtures, \
EVENT_READ_TIMEOUT, uinput_write_history_pipe, \ EVENT_READ_TIMEOUT, uinput_write_history_pipe, \
MAX_ABS, quick_cleanup, read_write_history_pipe, InputDevice, uinputs, \ MAX_ABS, quick_cleanup, read_write_history_pipe, InputDevice, uinputs, \
keyboard_keys keyboard_keys, MIN_ABS
class TestInjector(unittest.TestCase): class TestInjector(unittest.TestCase):
@ -418,7 +418,8 @@ class TestInjector(unittest.TestCase):
self.injector.run() self.injector.run()
# not in a process, so the event_producer state can be checked # 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.assertEqual(
self.injector.context.mapping.get('gamepad.joystick.left_purpose'), self.injector.context.mapping.get('gamepad.joystick.left_purpose'),
MOUSE MOUSE
@ -430,7 +431,7 @@ class TestInjector(unittest.TestCase):
self.injector = Injector('gamepad', custom_mapping) self.injector = Injector('gamepad', custom_mapping)
self.injector.stop_injecting() self.injector.stop_injecting()
self.injector.run() 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): def test_device1_event_producer(self):
custom_mapping.set('gamepad.joystick.left_purpose', MOUSE) custom_mapping.set('gamepad.joystick.left_purpose', MOUSE)
@ -440,7 +441,7 @@ class TestInjector(unittest.TestCase):
self.injector.run() self.injector.run()
# not a gamepad, so _event_producer is not initialized for that. # not a gamepad, so _event_producer is not initialized for that.
# it can still debounce stuff though # 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): def test_get_udef_name(self):
self.injector = Injector('device 1', custom_mapping) 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, \ from tests.test import tmp, push_events, new_event, spy, cleanup, \
uinput_write_history_pipe, MAX_ABS, EVENT_READ_TIMEOUT, \ uinput_write_history_pipe, MAX_ABS, EVENT_READ_TIMEOUT, \
send_event_to_reader send_event_to_reader, MIN_ABS
def gtk_iteration(): def gtk_iteration():
@ -1235,7 +1235,7 @@ class TestIntegration(unittest.TestCase):
time.sleep(0.1) time.sleep(0.1)
push_events('gamepad', [ 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) new_event(EV_ABS, ABS_X, MAX_ABS)
] * 100) ] * 100)

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

Loading…
Cancel
Save