mapping joysticks like a d-pad

This commit is contained in:
sezanzeb 2021-01-01 22:20:33 +01:00
parent e20f3fbbf9
commit bc0818a8e5
21 changed files with 348 additions and 87 deletions

View File

@ -645,6 +645,7 @@
<items>
<item id="mouse" translatable="yes">Mouse</item>
<item id="wheel" translatable="yes">Wheel</item>
<item id="buttons" translatable="yes">Buttons</item>
</items>
<signal name="changed" handler="on_left_joystick_purpose_changed" swapped="no"/>
</object>
@ -687,6 +688,7 @@
<items>
<item id="mouse" translatable="yes">Mouse</item>
<item id="wheel" translatable="yes">Wheel</item>
<item id="buttons" translatable="yes">Buttons</item>
</items>
<signal name="changed" handler="on_right_joystick_purpose_changed" swapped="no"/>
</object>

View File

@ -33,6 +33,7 @@ from keymapper.logger import logger
MOUSE = 'mouse'
WHEEL = 'wheel'
BUTTONS = 'buttons'
INITIAL_CONFIG = {
'autoload': {},

View File

@ -26,10 +26,11 @@ import asyncio
import time
import evdev
from evdev.ecodes import EV_ABS, EV_REL, REL_X, REL_Y, REL_WHEEL, REL_HWHEEL
from evdev.ecodes import EV_REL, REL_X, REL_Y, REL_WHEEL, REL_HWHEEL
from keymapper.logger import logger
from keymapper.config import MOUSE, WHEEL
from keymapper.dev.utils import max_abs
# other events for ABS include buttons
@ -117,20 +118,9 @@ async def ev_abs_mapper(abs_state, input_device, keymapper_device, mapping):
mapping : Mapping
the mapping object that configures the current injection
"""
# since input_device.absinfo(EV_ABS).max is too new for ubuntu,
# figure out the max value via the capabilities
absinfos = [
entry[1] for entry in
input_device.capabilities(absinfo=True)[EV_ABS]
if isinstance(entry, tuple) and isinstance(entry[1], evdev.AbsInfo)
]
max_value = max_abs(input_device)
if len(absinfos) == 0:
return
max_value = absinfos[0].max
if max_value == 0:
if max_value == 0 or max_value is None:
return
max_speed = ((max_value ** 2) * 2) ** 0.5

View File

@ -162,6 +162,11 @@ class KeycodeInjector:
continue
for permutation in key.get_permutations():
if permutation.keys[-1][-1] not in [-1, 1]:
logger.error(
'Expected values to be -1 or 1 at this point: %s',
permutation.keys
)
key_to_code[permutation.keys] = target_code
return key_to_code
@ -447,6 +452,15 @@ class KeycodeInjector:
)
async for event in source.async_read_loop():
if should_map_event_as_btn(source, event, self.mapping):
handle_keycode(
self._key_to_code,
macros,
event,
uinput
)
continue
if abs_to_rel and event.type == EV_ABS and event.code in JOYSTICK:
if event.code == evdev.ecodes.ABS_X:
self.abs_state[0] = event.value
@ -458,15 +472,6 @@ class KeycodeInjector:
self.abs_state[3] = event.value
continue
if should_map_event_as_btn(event.type, event.code):
handle_keycode(
self._key_to_code,
macros,
event,
uinput
)
continue
# forward the rest
uinput.write(event.type, event.code, event.value)
# this already includes SYN events, so need to syn here again

View File

@ -24,13 +24,16 @@
import itertools
import asyncio
import math
from evdev.ecodes import EV_KEY, EV_ABS
from evdev.ecodes import EV_KEY, EV_ABS, ABS_X, ABS_Y, ABS_RX, ABS_RY
from keymapper.logger import logger, is_debug
from keymapper.util import sign
from keymapper.mapping import DISABLE_CODE
from keymapper.config import BUTTONS
from keymapper.dev.ev_abs_mapper import JOYSTICK
from keymapper.dev.utils import max_abs
# maps mouse buttons to macro instances that have been executed. They may
@ -55,25 +58,44 @@ active_macros = {}
unreleased = {}
def should_map_event_as_btn(ev_type, code):
# a third of a quarter circle
JOYSTICK_BUTTON_THRESHOLD = math.sin((math.pi / 2) / 3 * 1)
# TODO test intuos again
def should_map_event_as_btn(device, event, mapping):
"""Does this event describe a button.
If it does, this function will make sure its value is one of [-1, 0, 1],
so that it matches the possible values in a mapping object.
Especially important for gamepad events, some of the buttons
require special rules.
Parameters
----------
ev_type : int
one of evdev.events
code : int
linux keycode
"""
if ev_type == EV_KEY:
if event.type == EV_KEY:
return True
if ev_type == EV_ABS:
is_mousepad = 47 <= code <= 61
if not is_mousepad and code not in JOYSTICK:
is_mousepad = event.type == EV_ABS and 47 <= event.code <= 61
if is_mousepad:
return False
if event.type == EV_ABS:
if event.code in JOYSTICK:
l_purpose = mapping.get('gamepad.joystick.left_purpose')
r_purpose = mapping.get('gamepad.joystick.right_purpose')
threshold = max_abs(device) * JOYSTICK_BUTTON_THRESHOLD
triggered = abs(event.value) > threshold
if event.code in [ABS_X, ABS_Y] and l_purpose == BUTTONS:
event.value = sign(event.value) if triggered else 0
return True
if event.code in [ABS_RX, ABS_RY] and r_purpose == BUTTONS:
event.value = sign(event.value) if triggered else 0
return True
else:
return True
return False
@ -217,8 +239,9 @@ def handle_keycode(key_to_code, macros, event, uinput):
else:
log(key, 'releasing %s', target_code)
write(uinput, (target_type, target_code, 0))
else:
# disabled keys can still be used in combinations btw
elif event.type != EV_ABS:
# ABS events might be spammed like crazy every time the position
# slightly changes
log(key, 'unexpected key up')
# everything that can be released is released now

View File

@ -25,13 +25,15 @@
import sys
import select
import multiprocessing
import threading
import evdev
from evdev.events import EV_KEY, EV_ABS
from evdev.ecodes import EV_KEY, EV_ABS
from keymapper.logger import logger
from keymapper.util import sign
from keymapper.key import Key
from keymapper.state import custom_mapping
from keymapper.getdevices import get_devices, refresh_devices
from keymapper.dev.keycode_mapper import should_map_event_as_btn
@ -128,13 +130,13 @@ class _KeycodeReader:
pipe = multiprocessing.Pipe()
self._pipe = pipe
self._process = multiprocessing.Process(target=self._read_worker)
self._process = threading.Thread(target=self._read_worker)
self._process.start()
def _consume_event(self, event):
def _consume_event(self, event, device):
"""Write the event code into the pipe if it is a key-down press."""
# value: 1 for down, 0 for up, 2 for hold.
if self._pipe[1].closed:
if self._pipe is None or self._pipe[1].closed:
logger.debug('Pipe closed, reader stops.')
sys.exit(0)
@ -153,9 +155,11 @@ class _KeycodeReader:
# which breaks the current workflow.
return
if not should_map_event_as_btn(event.type, event.code):
if not should_map_event_as_btn(device, event, custom_mapping):
return
if not (event.value == 0 and event.type == EV_ABS):
# avoid gamepad trigger spam
logger.spam(
'got (%s, %s, %s)',
event.type,
@ -165,8 +169,8 @@ class _KeycodeReader:
self._pipe[1].send(event)
def _read_worker(self):
"""Process that reads keycodes and buffers them into a pipe."""
# using a process that blocks instead of read_one made it easier
"""Thread that reads keycodes and buffers them into a pipe."""
# using a thread that blocks instead of read_one made it easier
# to debug via the logs, because the UI was not polling properly
# at some point which caused logs for events not to be written.
rlist = {device.fd: device for device in self.virtual_devices}
@ -175,7 +179,7 @@ class _KeycodeReader:
while True:
ready = select.select(rlist, [], [])[0]
for fd in ready:
readable = rlist[fd]
readable = rlist[fd] # a device or a pipe
if isinstance(readable, multiprocessing.connection.Connection):
msg = readable.recv()
if msg == CLOSE:
@ -185,7 +189,7 @@ class _KeycodeReader:
try:
for event in rlist[fd].read():
self._consume_event(event)
self._consume_event(event, readable)
except OSError:
logger.debug(
'Device "%s" disappeared from the reader',

50
keymapper/dev/utils.py Normal file
View File

@ -0,0 +1,50 @@
#!/usr/bin/python3
# -*- coding: utf-8 -*-
# key-mapper - GUI for device specific keyboard mappings
# Copyright (C) 2020 sezanzeb <proxima@hip70890b.de>
#
# This file is part of key-mapper.
#
# key-mapper is free software: you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
# the Free Software Foundation, either version 3 of the License, or
# (at your option) any later version.
#
# key-mapper is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with key-mapper. If not, see <https://www.gnu.org/licenses/>.
"""Utility functions for all other modules in keymapper.dev"""
import evdev
from evdev.ecodes import EV_ABS
def max_abs(device):
"""Figure out the maximum value of EV_ABS events of that device.
Like joystick movements or triggers.
"""
# since input_device.absinfo(EV_ABS).max is too new for (some?) ubuntus,
# figure out the max value via the capabilities
capabilities = device.capabilities(absinfo=True)
if EV_ABS not in capabilities:
return None
absinfos = [
entry[1] for entry in
capabilities[EV_ABS]
if isinstance(entry, tuple) and isinstance(entry[1], evdev.AbsInfo)
]
if len(absinfos) == 0:
return None
return absinfos[0].max

View File

@ -62,6 +62,7 @@ def to_string(key):
if ev_type != evdev.ecodes.EV_KEY:
direction = {
# D-Pad
(evdev.ecodes.ABS_HAT0X, -1): 'L',
(evdev.ecodes.ABS_HAT0X, 1): 'R',
(evdev.ecodes.ABS_HAT0Y, -1): 'U',
@ -74,6 +75,15 @@ def to_string(key):
(evdev.ecodes.ABS_HAT2X, 1): 'R',
(evdev.ecodes.ABS_HAT2Y, -1): 'U',
(evdev.ecodes.ABS_HAT2Y, 1): 'D',
# joystick
(evdev.ecodes.ABS_X, 1): 'R',
(evdev.ecodes.ABS_X, -1): 'L',
(evdev.ecodes.ABS_Y, 1): 'D',
(evdev.ecodes.ABS_Y, -1): 'U',
(evdev.ecodes.ABS_RX, 1): 'R',
(evdev.ecodes.ABS_RX, -1): 'L',
(evdev.ecodes.ABS_RY, 1): 'D',
(evdev.ecodes.ABS_RY, -1): 'U',
}.get((code, value))
if direction is not None:
key_name += f' {direction}'
@ -115,13 +125,15 @@ class Row(Gtk.ListBoxRow):
def release(self):
"""Tell the row that no keys are currently pressed down."""
if self.state == HOLDING:
if self.state == HOLDING and self.get_key() is not None:
# A key was pressed and then released.
# Switch to the character. idle_add this so that the
# 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.state = IDLE
def get_key(self):
"""Get the Key object from the left column.

View File

@ -170,7 +170,6 @@ class Window:
self.get('gamepad_config').hide()
self.populate_devices()
self.select_newest_preset()
self.timeouts = [

View File

@ -32,7 +32,7 @@ def verify(key):
if not isinstance(key, tuple) or len(key) != 3:
raise ValueError(f'Expected key to be a 3-tuple, but got {key}')
if sum([not isinstance(value, int) for value in key]) != 0:
raise ValueError(f'Can only use numbers, but got {key}')
raise ValueError(f'Can only use integers, but got {key}')
# having shift in combinations modifies the configured output,

View File

@ -249,3 +249,5 @@ class Mapping(ConfigBase):
existing = self._mapping.get(permutation)
if existing is not None:
return existing
return None

View File

@ -17,7 +17,7 @@
<text x="32.5" y="14">coverage</text>
</g>
<g fill="#fff" text-anchor="middle" font-family="DejaVu Sans,Verdana,Geneva,sans-serif" font-size="11">
<text x="83.0" y="15" fill="#010101" fill-opacity=".3">91%</text>
<text x="82.0" y="14">91%</text>
<text x="83.0" y="15" fill="#010101" fill-opacity=".3">92%</text>
<text x="82.0" y="14">92%</text>
</g>
</svg>

Before

Width:  |  Height:  |  Size: 1.0 KiB

After

Width:  |  Height:  |  Size: 1.0 KiB

View File

@ -27,7 +27,8 @@ requests.
- [x] start the daemon in such a way to not require usermod
- [x] mapping a combined button press to a key
- [x] add "disable" as mapping option
- [ ] mapping joystick directions as buttons, making it act like a D-Pad
- [x] mapping joystick directions as buttons, making it act like a D-Pad
- [ ] mapping mouse wheel events to buttons
- [ ] automatically load presets when devices get plugged in after login (udev)
- [ ] configure locale for preset to provide a different set of possible keys
- [ ] user-friendly way to map btn_left

View File

@ -71,6 +71,16 @@ uinput_write_history = []
uinput_write_history_pipe = multiprocessing.Pipe()
pending_events = {}
def read_write_history_pipe():
"""convert the write history from the pipe to some easier to manage list"""
history = []
while uinput_write_history_pipe[0].poll():
event = uinput_write_history_pipe[0].recv()
history.append((event.type, event.code, event.value))
return history
# key-mapper is only interested in devices that have EV_KEY, add some
# random other stuff to test that they are ignored.
phys_1 = 'usb-0000:03:00.0-1/input2'
@ -197,6 +207,9 @@ class InputEvent:
self.sec = int(timestamp)
self.usec = timestamp % 1 * 1000000
def __str__(self):
return f'InputEvent{self.t}'
def patch_paths():
from keymapper import paths
@ -222,6 +235,9 @@ def patch_select():
if len(pending_events.get(thing, [])) > 0:
ret.append(thing)
# avoid a fast iterating infinite loop in the reader
time.sleep(0.01)
return [ret, [], []]
select.select = new_select
@ -242,6 +258,13 @@ class InputDevice:
self.name = fixtures[path]['name']
self.fd = self.name
def log(self, key, msg):
print(
f'\033[90m' # color
f'{msg} "{self.name}" "{self.phys}" {key}'
'\033[0m' # end style
)
def absinfo(self, *args):
raise Exception('Ubuntus version of evdev doesn\'t support .absinfo')
@ -264,6 +287,7 @@ class InputDevice:
return None
event = pending_events[self.name].pop(0)
self.log(event, 'read_one')
return event
def read_loop(self):
@ -272,7 +296,9 @@ class InputDevice:
return
while len(pending_events[self.name]) > 0:
yield pending_events[self.name].pop(0)
result = pending_events[self.name].pop(0)
self.log(result, 'read_loop')
yield result
time.sleep(EVENT_READ_TIMEOUT)
async def async_read_loop(self):
@ -281,7 +307,9 @@ class InputDevice:
return
while len(pending_events[self.name]) > 0:
yield pending_events[self.name].pop(0)
result = pending_events[self.name].pop(0)
self.log(result, 'async_read_loop')
yield result
await asyncio.sleep(0.01)
def capabilities(self, absinfo=True):
@ -375,6 +403,9 @@ def cleanup():
task.cancel()
os.system('pkill -f key-mapper-service')
time.sleep(0.05)
if os.path.exists(tmp):
shutil.rmtree(tmp)
@ -436,7 +467,6 @@ def main():
print()
unittest.TextTestResult.startTest = start_test
unittest.TextTestRunner(verbosity=2).run(testsuite)

View File

@ -148,6 +148,7 @@ class TestDaemon(unittest.TestCase):
self.daemon = Daemon()
preset_path = get_preset_path(device, preset)
self.assertFalse(uinput_write_history_pipe[0].poll())
self.daemon.start_injecting(device, preset_path)
self.assertTrue(self.daemon.is_injecting(device))
@ -161,6 +162,13 @@ class TestDaemon(unittest.TestCase):
self.daemon.stop_injecting(device)
self.assertFalse(self.daemon.is_injecting(device))
time.sleep(0.2)
try:
self.assertFalse(uinput_write_history_pipe[0].poll())
except AssertionError:
print(uinput_write_history_pipe[0].recv())
raise
"""injection 2"""
# -1234 will be normalized to -1 by the injector
@ -168,9 +176,6 @@ class TestDaemon(unittest.TestCase):
InputEvent(*ev_2, -1234)
]
time.sleep(0.2)
self.assertFalse(uinput_write_history_pipe[0].poll())
path = get_preset_path(device, preset)
self.daemon.start_injecting(device, path)

View File

@ -27,6 +27,7 @@ from evdev.ecodes import EV_REL, REL_X, REL_Y, REL_WHEEL, REL_HWHEEL
from keymapper.dev.ev_abs_mapper import ev_abs_mapper
from keymapper.config import config
from keymapper.mapping import Mapping
from keymapper.dev.utils import max_abs
from keymapper.dev.ev_abs_mapper import MOUSE, WHEEL
from tests.test import InputDevice, UInput, MAX_ABS, clear_write_history, \
@ -59,6 +60,10 @@ class TestEvAbsMapper(unittest.TestCase):
def tearDown(self):
cleanup()
def test_max_abs(self):
self.assertEqual(max_abs(InputDevice('/dev/input/event30')), MAX_ABS)
self.assertIsNone(max_abs(InputDevice('/dev/input/event10')))
def do(self, a, b, c, d, expectation):
"""Present fake values to the loop and observe the outcome."""
clear_write_history()

View File

@ -118,5 +118,6 @@ class TestGetDevices(unittest.TestCase):
EV_KEY: [evdev.ecodes.ABS_X] # intentionally ABS_X (0) on EV_KEY
}))
if __name__ == "__main__":
unittest.main()

View File

@ -24,19 +24,19 @@ import time
import copy
import evdev
from evdev.ecodes import EV_REL, EV_KEY, EV_ABS, ABS_HAT0X
from evdev.ecodes import EV_REL, EV_KEY, EV_ABS, ABS_HAT0X, ABS_RX, ABS_Y
from keymapper.dev.injector import is_numlock_on, set_numlock, \
ensure_numlock, KeycodeInjector, is_in_capabilities
from keymapper.state import custom_mapping, system_mapping
from keymapper.mapping import Mapping, DISABLE_CODE, DISABLE_NAME
from keymapper.config import config
from keymapper.config import config, BUTTONS
from keymapper.key import Key
from keymapper.dev.macros import parse
from tests.test import InputEvent, pending_events, fixtures, \
EVENT_READ_TIMEOUT, uinput_write_history_pipe, \
MAX_ABS, cleanup
MAX_ABS, cleanup, read_write_history_pipe
class TestInjector(unittest.TestCase):
@ -393,10 +393,7 @@ class TestInjector(unittest.TestCase):
self.injector._msg_pipe[1].send(1234)
# convert the write history to some easier to manage list
history = []
while uinput_write_history_pipe[0].poll():
event = uinput_write_history_pipe[0].recv()
history.append((event.type, event.code, event.value))
history = read_write_history_pipe()
# 1 event before the combination was triggered (+1 for release)
# 4 events for the macro
@ -448,6 +445,69 @@ class TestInjector(unittest.TestCase):
numlock_after = is_numlock_on()
self.assertEqual(numlock_before, numlock_after)
def test_joysticks_as_buttons(self):
y_up = (EV_ABS, ABS_Y, -MAX_ABS)
y_release = (EV_ABS, ABS_Y, MAX_ABS // 4)
rx_right = (EV_ABS, ABS_RX, MAX_ABS)
rx_release = (EV_ABS, ABS_RX, -MAX_ABS // 4)
custom_mapping.change(Key(*y_up[:2], -1), 'w')
custom_mapping.change(Key(*rx_right[:2], 1), 'k(d)')
system_mapping.clear()
code_w = 71
code_d = 74
system_mapping._set('w', code_w)
system_mapping._set('d', code_d)
def do_stuff():
if self.injector is not None:
self.injector.stop_injecting()
time.sleep(0.1)
while uinput_write_history_pipe[0].poll():
uinput_write_history_pipe[0].recv()
pending_events['gamepad'] = [
InputEvent(*y_up),
InputEvent(*rx_right),
InputEvent(*y_release),
InputEvent(*rx_release),
]
self.injector = KeycodeInjector('gamepad', custom_mapping)
self.injector.start_injecting()
uinput_write_history_pipe[0].poll(timeout=1)
time.sleep(EVENT_READ_TIMEOUT * 10)
return read_write_history_pipe()
"""purpose != buttons"""
history = do_stuff()
self.assertEqual(history.count((EV_KEY, code_w, 1)), 0)
self.assertEqual(history.count((EV_KEY, code_d, 1)), 0)
self.assertEqual(history.count((EV_KEY, code_w, 0)), 0)
self.assertEqual(history.count((EV_KEY, code_d, 0)), 0)
"""left purpose buttons"""
custom_mapping.set('gamepad.joystick.left_purpose', BUTTONS)
history = do_stuff()
self.assertEqual(history.count((EV_KEY, code_w, 1)), 1)
self.assertEqual(history.count((EV_KEY, code_d, 1)), 0)
self.assertEqual(history.count((EV_KEY, code_w, 0)), 1)
self.assertEqual(history.count((EV_KEY, code_d, 0)), 0)
"""right purpose buttons"""
custom_mapping.remove('gamepad.joystick.right_purpose')
config.set('gamepad.joystick.right_purpose', BUTTONS)
history = do_stuff()
self.assertEqual(history.count((EV_KEY, code_w, 1)), 1)
self.assertEqual(history.count((EV_KEY, code_d, 1)), 1)
self.assertEqual(history.count((EV_KEY, code_w, 0)), 1)
self.assertEqual(history.count((EV_KEY, code_d, 0)), 1)
def test_store_permutations_for_macros(self):
mapping = Mapping()
ev_1 = (EV_KEY, 41, 1)

View File

@ -231,9 +231,11 @@ class TestIntegration(unittest.TestCase):
self.assertEqual(to_string(Key(EV_KEY, evdev.ecodes.KEY_9, 1)), '9')
self.assertEqual(to_string(Key(EV_KEY, evdev.ecodes.KEY_SEMICOLON, 1)), 'SEMICOLON')
self.assertEqual(to_string(Key(EV_ABS, evdev.ecodes.ABS_HAT0X, -1)), 'ABS_HAT0X L')
self.assertEqual(to_string(Key(EV_ABS, evdev.ecodes.ABS_HAT0X, 1)), 'ABS_HAT0X R')
self.assertEqual(to_string(Key(EV_ABS, evdev.ecodes.ABS_HAT0Y, -1)), 'ABS_HAT0Y U')
self.assertEqual(to_string(Key(EV_KEY, evdev.ecodes.BTN_A, 1)), 'BTN_A')
self.assertEqual(to_string(Key(EV_KEY, 1234, 1)), 'unknown')
self.assertEqual(to_string(Key(EV_ABS, evdev.ecodes.ABS_X, 1)), 'ABS_X R')
self.assertEqual(to_string(Key(EV_ABS, evdev.ecodes.ABS_RY, 1)), 'ABS_RY D')
# combinations
self.assertEqual(to_string(Key(
@ -334,7 +336,7 @@ class TestIntegration(unittest.TestCase):
keycode_reader._pipe[1].send(InputEvent(*sub_key))
# make the window consume the keycode
time.sleep(0.05)
time.sleep(0.06)
gtk_iteration()
# holding down
@ -347,7 +349,7 @@ class TestIntegration(unittest.TestCase):
keycode_reader._pipe[1].send(InputEvent(*sub_key[:2], 0))
# make the window consume the keycode
time.sleep(0.05)
time.sleep(0.06)
gtk_iteration()
# released
@ -367,6 +369,8 @@ class TestIntegration(unittest.TestCase):
css_classes = row.get_style_context().list_classes()
self.assertNotIn('changed', css_classes)
self.assertEqual(row.state, IDLE)
# it won't switch the focus to the character input
self.assertTrue(row.keycode_input.is_focus())
return row
if char and code_first:

View File

@ -31,11 +31,11 @@ from keymapper.dev.keycode_mapper import should_map_event_as_btn, \
active_macros, handle_keycode, unreleased, subsets, log
from keymapper.state import system_mapping
from keymapper.dev.macros import parse
from keymapper.config import config
from keymapper.config import config, BUTTONS
from keymapper.mapping import Mapping, DISABLE_CODE
from tests.test import InputEvent, UInput, uinput_write_history, \
cleanup
cleanup, InputDevice, MAX_ABS
def wait(func, timeout=1.0):
@ -192,14 +192,56 @@ class TestKeycodeMapper(unittest.TestCase):
self.assertEqual(uinput_write_history[3].t, (EV_KEY, 51, 0))
def test_should_map_event_as_btn(self):
self.assertTrue(should_map_event_as_btn(EV_ABS, ABS_HAT0X))
self.assertTrue(should_map_event_as_btn(EV_KEY, KEY_A))
self.assertFalse(should_map_event_as_btn(EV_ABS, ABS_X))
self.assertFalse(should_map_event_as_btn(EV_REL, REL_X))
device = InputDevice('/dev/input/event30')
mapping = Mapping()
self.assertFalse(should_map_event_as_btn(EV_ABS, ecodes.ABS_MT_SLOT))
self.assertFalse(should_map_event_as_btn(EV_ABS, ecodes.ABS_MT_TOOL_Y))
self.assertFalse(should_map_event_as_btn(EV_ABS, ecodes.ABS_MT_POSITION_X))
# the function name is so horribly long
do = should_map_event_as_btn
"""D-Pad"""
self.assertTrue(do(device, InputEvent(EV_ABS, ABS_HAT0X, 1), mapping))
self.assertTrue(do(device, InputEvent(EV_ABS, ABS_HAT0X, -1), mapping))
"""regular keys"""
self.assertTrue(do(device, InputEvent(EV_KEY, KEY_A, 1), mapping))
self.assertFalse(do(device, InputEvent(EV_ABS, ABS_X, 1), mapping))
self.assertFalse(do(device, InputEvent(EV_REL, REL_X, 1), mapping))
"""mousepad events"""
self.assertFalse(do(device, InputEvent(EV_ABS, ecodes.ABS_MT_SLOT, 1), mapping))
self.assertFalse(do(device, InputEvent(EV_ABS, ecodes.ABS_MT_TOOL_Y, 1), mapping))
self.assertFalse(do(device, InputEvent(EV_ABS, ecodes.ABS_MT_POSITION_X, 1), mapping))
"""joysticks"""
self.assertFalse(do(device, InputEvent(EV_ABS, ecodes.ABS_RX, 1234), mapping))
self.assertFalse(do(device, InputEvent(EV_ABS, ecodes.ABS_Y, -1), mapping))
mapping.set('gamepad.joystick.left_purpose', BUTTONS)
event = InputEvent(EV_ABS, ecodes.ABS_RX, MAX_ABS)
self.assertFalse(do(device, event, mapping))
self.assertEqual(event.value, MAX_ABS)
event = InputEvent(EV_ABS, ecodes.ABS_Y, -MAX_ABS)
self.assertTrue(do(device, event, mapping))
self.assertEqual(event.value, -1)
event = InputEvent(EV_ABS, ecodes.ABS_X, -MAX_ABS / 4)
self.assertTrue(do(device, event, mapping))
self.assertEqual(event.value, 0)
config.set('gamepad.joystick.right_purpose', BUTTONS)
event = InputEvent(EV_ABS, ecodes.ABS_RX, MAX_ABS)
self.assertTrue(do(device, event, mapping))
self.assertEqual(event.value, 1)
event = InputEvent(EV_ABS, ecodes.ABS_Y, MAX_ABS)
self.assertTrue(do(device, event, mapping))
self.assertEqual(event.value, 1)
event = InputEvent(EV_ABS, ecodes.ABS_X, MAX_ABS / 4)
self.assertTrue(do(device, event, mapping))
self.assertEqual(event.value, 0)
def test_handle_keycode(self):
_key_to_code = {

View File

@ -24,11 +24,14 @@ import time
import multiprocessing
from evdev.ecodes import EV_KEY, EV_ABS, ABS_HAT0X, ABS_HAT0Y, KEY_COMMA, \
BTN_LEFT, BTN_TOOL_DOUBLETAP, ABS_Z
BTN_LEFT, BTN_TOOL_DOUBLETAP, ABS_Z, ABS_Y
from keymapper.dev.reader import keycode_reader
from keymapper.state import custom_mapping
from keymapper.config import BUTTONS, MOUSE
from tests.test import InputEvent, pending_events, EVENT_READ_TIMEOUT, cleanup
from tests.test import InputEvent, pending_events, EVENT_READ_TIMEOUT, \
cleanup, MAX_ABS
CODE_1 = 100
@ -88,6 +91,28 @@ class TestReader(unittest.TestCase):
self.assertEqual(keycode_reader.read(), None)
self.assertEqual(len(keycode_reader._unreleased), 3)
def test_reads_joysticks(self):
# if their purpose is "buttons"
custom_mapping.set('gamepad.joystick.left_purpose', BUTTONS)
pending_events['gamepad'] = [
InputEvent(EV_ABS, ABS_Y, MAX_ABS)
]
keycode_reader.start_reading('gamepad')
wait(keycode_reader._pipe[0].poll, 0.5)
self.assertEqual(keycode_reader.read(), (EV_ABS, ABS_Y, 1))
self.assertEqual(keycode_reader.read(), None)
self.assertEqual(len(keycode_reader._unreleased), 1)
keycode_reader._unreleased = {}
custom_mapping.set('gamepad.joystick.left_purpose', MOUSE)
pending_events['gamepad'] = [
InputEvent(EV_ABS, ABS_Y, MAX_ABS)
]
keycode_reader.start_reading('gamepad')
time.sleep(0.1)
self.assertEqual(keycode_reader.read(), None)
self.assertEqual(len(keycode_reader._unreleased), 0)
def test_ignore_btn_left(self):
# click events are ignored because overwriting them would render the
# mouse useless, but a mouse is needed to stop the injection