From 7bb08bca0734f1f6f14f77af4105a682ef07ab95 Mon Sep 17 00:00:00 2001
From: Fabio Manganiello <fabio@manganiello.tech>
Date: Thu, 16 Nov 2023 21:42:57 +0100
Subject: [PATCH] [joystick] Rewritten `joystick` integration as a plugin.

And removed legacy `joystick*` backends and `inputs` plugin.

Closes: #290
---
 platypush/backend/joystick/__init__.py        |   42 -
 platypush/backend/joystick/jstest/__init__.py |  283 --
 .../backend/joystick/jstest/manifest.yaml     |   19 -
 platypush/backend/joystick/linux/__init__.py  |  206 -
 .../backend/joystick/linux/manifest.yaml      |   16 -
 platypush/message/event/joystick.py           |   98 +-
 platypush/plugins/inputs/__init__.py          |  156 -
 platypush/plugins/inputs/manifest.yaml        |    7 -
 platypush/plugins/joystick/__init__.py        |  124 +
 platypush/plugins/joystick/_inputs.py         | 3660 +++++++++++++++++
 platypush/plugins/joystick/_manager.py        |  175 +
 platypush/plugins/joystick/_state.py          |   50 +
 .../joystick/manifest.yaml                    |    4 +-
 .../plugins/media/chromecast/__init__.py      |    3 +
 platypush/schemas/joystick.py                 |  199 +
 15 files changed, 4230 insertions(+), 812 deletions(-)
 delete mode 100644 platypush/backend/joystick/__init__.py
 delete mode 100644 platypush/backend/joystick/jstest/__init__.py
 delete mode 100644 platypush/backend/joystick/jstest/manifest.yaml
 delete mode 100644 platypush/backend/joystick/linux/__init__.py
 delete mode 100644 platypush/backend/joystick/linux/manifest.yaml
 delete mode 100644 platypush/plugins/inputs/__init__.py
 delete mode 100644 platypush/plugins/inputs/manifest.yaml
 create mode 100644 platypush/plugins/joystick/__init__.py
 create mode 100644 platypush/plugins/joystick/_inputs.py
 create mode 100644 platypush/plugins/joystick/_manager.py
 create mode 100644 platypush/plugins/joystick/_state.py
 rename platypush/{backend => plugins}/joystick/manifest.yaml (83%)
 create mode 100644 platypush/schemas/joystick.py

diff --git a/platypush/backend/joystick/__init__.py b/platypush/backend/joystick/__init__.py
deleted file mode 100644
index 247f9a014..000000000
--- a/platypush/backend/joystick/__init__.py
+++ /dev/null
@@ -1,42 +0,0 @@
-import time
-
-from platypush.backend import Backend
-from platypush.message.event.joystick import JoystickEvent
-
-
-class JoystickBackend(Backend):
-    """
-    This backend will listen for events from a joystick device and post a
-    JoystickEvent whenever a new event is captured.
-    """
-
-    def __init__(self, device, *args, **kwargs):
-        """
-        :param device: Path to the joystick device (e.g. `/dev/input/js0`)
-        :type device_name: str
-        """
-
-        super().__init__(*args, **kwargs)
-
-        self.device = device
-
-    def run(self):
-        import inputs
-
-        super().run()
-        self.logger.info(
-            'Initialized joystick backend on device {}'.format(self.device)
-        )
-
-        while not self.should_stop():
-            try:
-                events = inputs.get_gamepad()
-                for event in events:
-                    if event.ev_type == 'Key' or event.ev_type == 'Absolute':
-                        self.bus.post(JoystickEvent(code=event.code, state=event.state))
-            except Exception as e:
-                self.logger.exception(e)
-                time.sleep(1)
-
-
-# vim:sw=4:ts=4:et:
diff --git a/platypush/backend/joystick/jstest/__init__.py b/platypush/backend/joystick/jstest/__init__.py
deleted file mode 100644
index 79bb1a714..000000000
--- a/platypush/backend/joystick/jstest/__init__.py
+++ /dev/null
@@ -1,283 +0,0 @@
-import json
-import os
-import re
-import subprocess
-import time
-from typing import Optional, List
-
-from platypush.backend import Backend
-from platypush.message.event.joystick import (
-    JoystickConnectedEvent,
-    JoystickDisconnectedEvent,
-    JoystickStateEvent,
-    JoystickButtonPressedEvent,
-    JoystickButtonReleasedEvent,
-    JoystickAxisEvent,
-)
-
-
-class JoystickState:
-    def __init__(self, axes: List[int], buttons: List[bool]):
-        self.axes = axes
-        self.buttons = buttons
-
-    def __str__(self):
-        return json.dumps(self.__dict__)
-
-    def __eq__(self, obj):
-        return obj.axes == self.axes and obj.buttons == self.buttons
-
-    def __sub__(self, obj) -> dict:
-        if len(obj.axes) < len(self.axes) or len(obj.buttons) < len(self.buttons):
-            return {}
-
-        diff = {
-            'axes': {
-                axis: obj.axes[axis]
-                for axis in range(len(self.axes))
-                if self.axes[axis] != obj.axes[axis]
-            },
-            'buttons': {
-                button: obj.buttons[button]
-                for button in range(len(self.buttons))
-                if self.buttons[button] != obj.buttons[button]
-            },
-        }
-
-        return {k: v for k, v in diff.items() if v}
-
-
-class JoystickJstestBackend(Backend):
-    """
-    This backend can be used to intercept events from a joystick device if the device does not work with the standard
-    :class:`platypush.backend.joystick.JoystickBackend` backend (this may especially happen with some Bluetooth
-    joysticks that don't support the ``ioctl`` requests used by ``inputs``).
-
-    This backend only works on Linux, and it requires the ``joystick`` package to be installed.
-
-    **NOTE**: This backend can be quite slow, since it has to run another program (``jstest``) and parse its output.
-    Consider it as a last resort if your joystick works with neither :class:`platypush.backend.joystick.JoystickBackend`
-    nor :class:`platypush.backend.joystick.JoystickLinuxBackend`.
-
-    To test if your joystick is compatible, connect it to your device, check for its path (usually under
-    ``/dev/input/js*``) and run::
-
-        $ jstest /dev/input/js[n]
-
-    """
-
-    js_axes_regex = re.compile(r'Axes:\s+(((\d+):\s*([\-\d]+)\s*)+)')
-    js_buttons_regex = re.compile(r'Buttons:\s+(((\d+):\s*(on|off)\s*)+)')
-    js_axis_regex = re.compile(r'^\s*(\d+):\s*([\-\d]+)\s*(.*)')
-    js_button_regex = re.compile(r'^\s*(\d+):\s*(on|off)\s*(.*)')
-
-    def __init__(
-        self,
-        device: str = '/dev/input/js0',
-        jstest_path: str = '/usr/bin/jstest',
-        **kwargs,
-    ):
-        """
-        :param device: Path to the joystick device (default: ``/dev/input/js0``).
-        :param jstest_path: Path to the ``jstest`` executable that comes with the ``joystick`` system package
-            (default: ``/usr/bin/jstest``).
-        """
-        super().__init__(device=device, **kwargs)
-
-        self.device = device
-        self.jstest_path = jstest_path
-        self._process: Optional[subprocess.Popen] = None
-        self._state: Optional[JoystickState] = None
-
-    def _wait_ready(self):
-        self.logger.info(f'Waiting for joystick device on {self.device}')
-
-        while not self.should_stop():
-            if not os.path.exists(self.device):
-                time.sleep(1)
-
-            try:
-                with open(self.device, 'rb'):
-                    break
-            except Exception as e:
-                self.logger.debug(e)
-                time.sleep(0.1)
-                continue
-
-        self.bus.post(JoystickConnectedEvent(device=self.device))
-
-    def _read_states(self):
-        while not self.should_stop():
-            yield self._get_state()
-
-    def _get_state(self) -> JoystickState:
-        axes = []
-        buttons = []
-        line = ''
-
-        while os.path.exists(self.device) and not self.should_stop():
-            ch = self._process.stdout.read(1).decode()
-            if not ch:
-                continue
-
-            if ch in ['\r', '\n']:
-                line = ''
-                continue
-
-            line += ch
-            if line.endswith('Axes:  '):
-                break
-
-        while (
-            os.path.exists(self.device)
-            and not self.should_stop()
-            and len(axes) < len(self._state.axes)
-        ):
-            ch = ' '
-            while ch == ' ':
-                ch = self._process.stdout.read(1).decode()
-
-            self._process.stdout.read(len(f'{len(axes)}'))
-            value = ''
-
-            while os.path.exists(self.device) and not self.should_stop():
-                ch = self._process.stdout.read(1).decode()
-                if ch == ' ':
-                    if not value:
-                        continue
-                    break
-
-                if ch == ':':
-                    break
-
-                value += ch
-
-            if value:
-                axes.append(int(value))
-
-        line = ''
-
-        while os.path.exists(self.device) and not self.should_stop():
-            ch = self._process.stdout.read(1).decode()
-            if not ch:
-                continue
-
-            line += ch
-            if line.endswith('Buttons:  '):
-                break
-
-        while (
-            os.path.exists(self.device)
-            and not self.should_stop()
-            and len(buttons) < len(self._state.buttons)
-        ):
-            ch = ' '
-            while ch == ' ':
-                ch = self._process.stdout.read(1).decode()
-
-            self._process.stdout.read(len(f'{len(buttons)}'))
-            value = ''
-
-            while os.path.exists(self.device) and not self.should_stop():
-                ch = self._process.stdout.read(1).decode()
-                if ch == ' ':
-                    continue
-
-                value += ch
-                if value in ['on', 'off']:
-                    buttons.append(value == 'on')
-                    break
-
-        return JoystickState(axes=axes, buttons=buttons)
-
-    def _initialize(self):
-        while (
-            self._process.poll() is None
-            and os.path.exists(self.device)
-            and not self.should_stop()
-            and not self._state
-        ):
-            line = b''
-            ch = None
-
-            while ch not in [b'\r', b'\n']:
-                ch = self._process.stdout.read(1)
-                line += ch
-
-            line = line.decode().strip()
-            if not (line and line.startswith('Axes:')):
-                continue
-
-            re_axes = self.js_axes_regex.search(line)
-            re_buttons = self.js_buttons_regex.search(line)
-
-            if not (re_axes and re_buttons):
-                return
-
-            state = {
-                'axes': [],
-                'buttons': [],
-            }
-
-            axes = re_axes.group(1)
-            while axes:
-                m = self.js_axis_regex.search(axes)
-                state['axes'].append(int(m.group(2)))
-                axes = m.group(3)
-
-            buttons = re_buttons.group(1)
-            while buttons:
-                m = self.js_button_regex.search(buttons)
-                state['buttons'].append(m.group(2) == 'on')
-                buttons = m.group(3)
-
-            self._state = JoystickState(**state)
-
-    def _process_state(self, state: JoystickState):
-        diff = self._state - state
-        if not diff:
-            return
-
-        self.bus.post(JoystickStateEvent(device=self.device, **state.__dict__))
-
-        for button, pressed in diff.get('buttons', {}).items():
-            evt_class = (
-                JoystickButtonPressedEvent if pressed else JoystickButtonReleasedEvent
-            )
-            self.bus.post(evt_class(device=self.device, button=button))
-
-        for axis, value in diff.get('axes', {}).items():
-            self.bus.post(JoystickAxisEvent(device=self.device, axis=axis, value=value))
-
-        self._state = state
-
-    def run(self):
-        super().run()
-
-        try:
-            while not self.should_stop():
-                self._wait_ready()
-
-                with subprocess.Popen(
-                    [self.jstest_path, '--normal', self.device], stdout=subprocess.PIPE
-                ) as self._process:
-                    self.logger.info('Device opened')
-                    self._initialize()
-
-                    if self._process.poll() is not None:
-                        break
-
-                    for state in self._read_states():
-                        if self._process.poll() is not None or not os.path.exists(
-                            self.device
-                        ):
-                            self.logger.warning(f'Connection to {self.device} lost')
-                            self.bus.post(JoystickDisconnectedEvent(self.device))
-                            break
-
-                        self._process_state(state)
-        finally:
-            self._process = None
-
-
-# vim:sw=4:ts=4:et:
diff --git a/platypush/backend/joystick/jstest/manifest.yaml b/platypush/backend/joystick/jstest/manifest.yaml
deleted file mode 100644
index d0269b769..000000000
--- a/platypush/backend/joystick/jstest/manifest.yaml
+++ /dev/null
@@ -1,19 +0,0 @@
-manifest:
-  events:
-    - platypush.message.event.joystick.JoystickAxisEvent
-    - platypush.message.event.joystick.JoystickButtonPressedEvent
-    - platypush.message.event.joystick.JoystickButtonReleasedEvent
-    - platypush.message.event.joystick.JoystickConnectedEvent
-    - platypush.message.event.joystick.JoystickDisconnectedEvent
-    - platypush.message.event.joystick.JoystickStateEvent
-  install:
-    apk:
-      - linuxconsoletools
-    apt:
-      - joystick
-    dnf:
-      - joystick
-    pacman:
-      - joyutils
-  package: platypush.backend.joystick.jstest
-  type: backend
diff --git a/platypush/backend/joystick/linux/__init__.py b/platypush/backend/joystick/linux/__init__.py
deleted file mode 100644
index 61ed80c54..000000000
--- a/platypush/backend/joystick/linux/__init__.py
+++ /dev/null
@@ -1,206 +0,0 @@
-import array
-import struct
-import time
-from fcntl import ioctl
-from typing import IO
-
-from platypush.backend import Backend
-from platypush.message.event.joystick import (
-    JoystickConnectedEvent,
-    JoystickDisconnectedEvent,
-    JoystickButtonPressedEvent,
-    JoystickButtonReleasedEvent,
-    JoystickAxisEvent,
-)
-
-
-class JoystickLinuxBackend(Backend):
-    """
-    This backend intercepts events from joystick devices through the native Linux API implementation.
-
-    It is loosely based on https://gist.github.com/rdb/8864666, which itself uses the
-    `Linux kernel joystick API <https://www.kernel.org/doc/Documentation/input/joystick-api.txt>`_ to interact with
-    the devices.
-    """
-
-    # These constants were borrowed from linux/input.h
-    axis_names = {
-        0x00: 'x',
-        0x01: 'y',
-        0x02: 'z',
-        0x03: 'rx',
-        0x04: 'ry',
-        0x05: 'rz',
-        0x06: 'throttle',
-        0x07: 'rudder',
-        0x08: 'wheel',
-        0x09: 'gas',
-        0x0A: 'brake',
-        0x10: 'hat0x',
-        0x11: 'hat0y',
-        0x12: 'hat1x',
-        0x13: 'hat1y',
-        0x14: 'hat2x',
-        0x15: 'hat2y',
-        0x16: 'hat3x',
-        0x17: 'hat3y',
-        0x18: 'pressure',
-        0x19: 'distance',
-        0x1A: 'tilt_x',
-        0x1B: 'tilt_y',
-        0x1C: 'tool_width',
-        0x20: 'volume',
-        0x28: 'misc',
-    }
-
-    button_names = {
-        0x120: 'trigger',
-        0x121: 'thumb',
-        0x122: 'thumb2',
-        0x123: 'top',
-        0x124: 'top2',
-        0x125: 'pinkie',
-        0x126: 'base',
-        0x127: 'base2',
-        0x128: 'base3',
-        0x129: 'base4',
-        0x12A: 'base5',
-        0x12B: 'base6',
-        0x12F: 'dead',
-        0x130: 'a',
-        0x131: 'b',
-        0x132: 'c',
-        0x133: 'x',
-        0x134: 'y',
-        0x135: 'z',
-        0x136: 'tl',
-        0x137: 'tr',
-        0x138: 'tl2',
-        0x139: 'tr2',
-        0x13A: 'select',
-        0x13B: 'start',
-        0x13C: 'mode',
-        0x13D: 'thumbl',
-        0x13E: 'thumbr',
-        0x220: 'dpad_up',
-        0x221: 'dpad_down',
-        0x222: 'dpad_left',
-        0x223: 'dpad_right',
-        # XBox 360 controller uses these codes.
-        0x2C0: 'dpad_left',
-        0x2C1: 'dpad_right',
-        0x2C2: 'dpad_up',
-        0x2C3: 'dpad_down',
-    }
-
-    def __init__(self, device: str = '/dev/input/js0', *args, **kwargs):
-        """
-        :param device: Joystick device to monitor (default: ``/dev/input/js0``).
-        """
-        super().__init__(*args, **kwargs)
-        self.device = device
-        self._axis_states = {}
-        self._button_states = {}
-        self._axis_map = []
-        self._button_map = []
-
-    def _init_joystick(self, dev: IO):
-        # Get the device name.
-        buf = array.array('B', [0] * 64)
-        ioctl(dev, 0x80006A13 + (0x10000 * len(buf)), buf)  # JSIOCGNAME(len)
-        js_name = buf.tobytes().rstrip(b'\x00').decode('utf-8')
-
-        # Get number of axes and buttons.
-        buf = array.array('B', [0])
-        ioctl(dev, 0x80016A11, buf)  # JSIOCGAXES
-        num_axes = buf[0]
-
-        buf = array.array('B', [0])
-        ioctl(dev, 0x80016A12, buf)  # JSIOCGBUTTONS
-        num_buttons = buf[0]
-
-        # Get the axis map.
-        buf = array.array('B', [0] * 0x40)
-        ioctl(dev, 0x80406A32, buf)  # JSIOCGAXMAP
-
-        for axis in buf[:num_axes]:
-            axis_name = self.axis_names.get(axis, 'unknown(0x%02x)' % axis)
-            self._axis_map.append(axis_name)
-            self._axis_states[axis_name] = 0.0
-
-        # Get the button map.
-        buf = array.array('H', [0] * 200)
-        ioctl(dev, 0x80406A34, buf)  # JSIOCGBTNMAP
-
-        for btn in buf[:num_buttons]:
-            btn_name = self.button_names.get(btn, 'unknown(0x%03x)' % btn)
-            self._button_map.append(btn_name)
-            self._button_states[btn_name] = 0
-
-        self.bus.post(
-            JoystickConnectedEvent(
-                device=self.device,
-                name=js_name,
-                axes=self._axis_map,
-                buttons=self._button_map,
-            )
-        )
-
-    def run(self):
-        super().run()
-        self.logger.info(f'Opening {self.device}...')
-
-        while not self.should_stop():
-            # Open the joystick device.
-            try:
-                jsdev = open(self.device, 'rb')  # noqa
-                self._init_joystick(jsdev)
-            except Exception as e:
-                self.logger.debug(
-                    'Joystick device on %s not available: %s', self.device, e
-                )
-                time.sleep(5)
-                continue
-
-            try:
-                # Joystick event loop
-                while not self.should_stop():
-                    try:
-                        evbuf = jsdev.read(8)
-                        if evbuf:
-                            _, value, evt_type, number = struct.unpack('IhBB', evbuf)
-
-                            if evt_type & 0x80:  # Initial state notification
-                                continue
-
-                            if evt_type & 0x01:
-                                button = self._button_map[number]
-                                if button:
-                                    self._button_states[button] = value
-                                    evt_class = (
-                                        JoystickButtonPressedEvent
-                                        if value
-                                        else JoystickButtonReleasedEvent
-                                    )
-                                    # noinspection PyTypeChecker
-                                    self.bus.post(
-                                        evt_class(device=self.device, button=button)
-                                    )
-
-                            if evt_type & 0x02:
-                                axis = self._axis_map[number]
-                                if axis:
-                                    fvalue = value / 32767.0
-                                    self._axis_states[axis] = fvalue
-                                    # noinspection PyTypeChecker
-                                    self.bus.post(
-                                        JoystickAxisEvent(
-                                            device=self.device, axis=axis, value=fvalue
-                                        )
-                                    )
-                    except OSError as e:
-                        self.logger.warning(f'Connection to {self.device} lost: {e}')
-                        self.bus.post(JoystickDisconnectedEvent(device=self.device))
-                        break
-            finally:
-                jsdev.close()
diff --git a/platypush/backend/joystick/linux/manifest.yaml b/platypush/backend/joystick/linux/manifest.yaml
deleted file mode 100644
index 227b328a5..000000000
--- a/platypush/backend/joystick/linux/manifest.yaml
+++ /dev/null
@@ -1,16 +0,0 @@
-manifest:
-  events:
-    platypush.message.event.joystick.JoystickAxisEvent: when an axis value of the
-      joystick changes.
-    platypush.message.event.joystick.JoystickButtonPressedEvent: when a joystick button
-      is pressed.
-    platypush.message.event.joystick.JoystickButtonReleasedEvent: when a joystick
-      button is released.
-    platypush.message.event.joystick.JoystickConnectedEvent: when the joystick is
-      connected.
-    platypush.message.event.joystick.JoystickDisconnectedEvent: when the joystick
-      is disconnected.
-  install:
-    pip: []
-  package: platypush.backend.joystick.linux
-  type: backend
diff --git a/platypush/message/event/joystick.py b/platypush/message/event/joystick.py
index 57bf10f82..44a718414 100644
--- a/platypush/message/event/joystick.py
+++ b/platypush/message/event/joystick.py
@@ -1,108 +1,46 @@
-from abc import ABCMeta
-from typing import Optional, Iterable, Union
+from abc import ABC
 
 from platypush.message.event import Event
 
 
-class JoystickEvent(Event):
-    """
-    Generic joystick event.
-    """
-
-    def __init__(self, code, state, *args, **kwargs):
-        """
-        :param code: Event code, usually the code of the source key/handle
-        :type code: str
-
-        :param state: State of the triggering element. Can be 0/1 for a button, -1/0/1 for an axis, a discrete integer
-            for an analog input etc.
-        :type state: int
-        """
-
-        super().__init__(*args, code=code, state=state, **kwargs)
-
-
-class _JoystickEvent(Event, ABCMeta):
+class JoystickEvent(Event, ABC):
     """
     Base joystick event class.
     """
 
-    def __init__(self, device: str, **kwargs):
-        super().__init__(device=device, **kwargs)
+    def __init__(self, device: dict, *args, **kwargs):
+        """
+        :param device: Joystick device info as a dictionary:
+
+            .. schema:: joystick.JoystickDeviceSchema
+        """
+        super().__init__(*args, device=device, **kwargs)
 
 
-class JoystickConnectedEvent(_JoystickEvent):
+class JoystickConnectedEvent(JoystickEvent):
     """
     Event triggered upon joystick connection.
     """
-    def __init__(self,
-                 *args,
-                 name: Optional[str] = None,
-                 axes: Optional[Iterable[Union[int, str]]] = None,
-                 buttons: Optional[Iterable[Union[int, str]]] = None,
-                 **kwargs):
-        """
-        :param name: Device name.
-        :param axes: List of the device axes, as indices or names.
-        :param buttons: List of the device buttons, as indices or names.
-        """
-        super().__init__(*args, name=name, axes=axes, buttons=buttons, **kwargs)
 
 
-class JoystickDisconnectedEvent(_JoystickEvent):
+class JoystickDisconnectedEvent(JoystickEvent):
     """
     Event triggered upon joystick disconnection.
     """
 
 
-class JoystickStateEvent(_JoystickEvent):
+class JoystickStateEvent(JoystickEvent):
     """
-    Event triggered when the state of the joystick changes.
+    Base joystick state event class.
     """
 
-    def __init__(self, *args, axes: Iterable[int], buttons: Iterable[bool], **kwargs):
+    def __init__(self, *args, state: dict, **kwargs):
         """
-        :param axes: Joystick axes values, as a list of integer values.
-        :param buttons: Joystick buttons values, as a list of boolean values (True for pressed, False for released).
+        :param state: Joystick state as a dictionary:
+
+            .. schema:: joystick.JoystickStateSchema
         """
-        super().__init__(*args, axes=axes, buttons=buttons, **kwargs)
-
-
-class JoystickButtonPressedEvent(_JoystickEvent):
-    """
-    Event triggered when a joystick button is pressed.
-    """
-
-    def __init__(self, *args, button: Union[int, str], **kwargs):
-        """
-        :param button: Button index or name.
-        """
-        super().__init__(*args, button=button, **kwargs)
-
-
-class JoystickButtonReleasedEvent(_JoystickEvent):
-    """
-    Event triggered when a joystick button is released.
-    """
-
-    def __init__(self, *args, button: Union[int, str], **kwargs):
-        """
-        :param button: Button index or name.
-        """
-        super().__init__(*args, button=button, **kwargs)
-
-
-class JoystickAxisEvent(_JoystickEvent):
-    """
-    Event triggered when an axis value of the joystick changes.
-    """
-
-    def __init__(self, *args, axis: Union[int, str], value: int, **kwargs):
-        """
-        :param axis: Axis index or name.
-        :param value: Axis value.
-        """
-        super().__init__(*args, axis=axis, value=value, **kwargs)
+        super().__init__(*args, state=state, **kwargs)
 
 
 # vim:sw=4:ts=4:et:
diff --git a/platypush/plugins/inputs/__init__.py b/platypush/plugins/inputs/__init__.py
deleted file mode 100644
index 7919f3f46..000000000
--- a/platypush/plugins/inputs/__init__.py
+++ /dev/null
@@ -1,156 +0,0 @@
-from typing import List
-
-from platypush.plugins import Plugin, action
-
-
-class InputsPlugin(Plugin):
-    """
-    This plugin emulates user input on a keyboard/mouse. It requires the a graphical server (X server or Mac/Win
-    interface) to be running - it won't work in console mode.
-    """
-
-    @staticmethod
-    def _get_keyboard():
-        # noinspection PyPackageRequirements
-        from pykeyboard import PyKeyboard
-
-        return PyKeyboard()
-
-    @staticmethod
-    def _get_mouse():
-        # noinspection PyPackageRequirements
-        from pymouse import PyMouse
-
-        return PyMouse()
-
-    @classmethod
-    def _parse_key(cls, key: str):
-        k = cls._get_keyboard()
-        keymap = {
-            'ctrl': k.control_key,
-            'alt': k.alt_key,
-            'meta': k.windows_l_key,
-            'shift': k.shift_key,
-            'enter': k.enter_key,
-            'tab': k.tab_key,
-            'home': k.home_key,
-            'end': k.end_key,
-            'capslock': k.caps_lock_key,
-            'back': k.backspace_key,
-            'del': k.delete_key,
-            'up': k.up_key,
-            'down': k.down_key,
-            'left': k.left_key,
-            'right': k.right_key,
-            'pageup': k.page_up_key,
-            'pagedown': k.page_down_key,
-            'esc': k.escape_key,
-            'find': k.find_key,
-            'f1': k.function_keys[1],
-            'f2': k.function_keys[2],
-            'f3': k.function_keys[3],
-            'f4': k.function_keys[4],
-            'f5': k.function_keys[5],
-            'f6': k.function_keys[6],
-            'f7': k.function_keys[7],
-            'f8': k.function_keys[8],
-            'f9': k.function_keys[9],
-            'f10': k.function_keys[10],
-            'f11': k.function_keys[11],
-            'f12': k.function_keys[12],
-            'help': k.help_key,
-            'media_next': k.media_next_track_key,
-            'media_prev': k.media_prev_track_key,
-            'media_play': k.media_play_pause_key,
-            'menu': k.menu_key,
-            'numlock': k.num_lock_key,
-            'print': k.print_key,
-            'print_screen': k.print_screen_key,
-            'sleep': k.sleep_key,
-            'space': k.space_key,
-            'voldown': k.volume_down_key,
-            'volup': k.volume_up_key,
-            'volmute': k.volume_mute_key,
-            'zoom': k.zoom_key,
-        }
-
-        lkey = key.lower()
-        if lkey in keymap:
-            return keymap[lkey]
-
-        return key
-
-    @action
-    def press_key(self, key: str):
-        """
-        Emulate the pressure of a key.
-        :param key: Key to be pressed
-        """
-        kbd = self._get_keyboard()
-        key = self._parse_key(key)
-        kbd.press_key(key)
-
-    @action
-    def release_key(self, key: str):
-        """
-        Release a pressed key.
-        :param key: Key to be released
-        """
-        kbd = self._get_keyboard()
-        key = self._parse_key(key)
-        kbd.release_key(key)
-
-    @action
-    def press_keys(self, keys: List[str]):
-        """
-        Emulate the pressure of multiple keys.
-        :param keys: List of keys to be pressed.
-        """
-        kbd = self._get_keyboard()
-        keys = [self._parse_key(k) for k in keys]
-        kbd.press_keys(keys)
-
-    @action
-    def tap_key(self, key: str, repeat: int = 1, interval: float = 0):
-        """
-        Emulate a key tap.
-        :param key: Key to be pressed
-        :param repeat: Number of iterations (default: 1)
-        :param interval: Repeat interval in seconds (default: 0)
-        """
-        kbd = self._get_keyboard()
-        key = self._parse_key(key)
-        kbd.tap_key(key, n=repeat, interval=interval)
-
-    @action
-    def type_string(self, string: str, interval: float = 0):
-        """
-        Type a string.
-        :param string: String to be typed
-        :param interval: Interval between key strokes in seconds (default: 0)
-        """
-        kbd = self._get_keyboard()
-        kbd.type_string(string, interval=interval)
-
-    @action
-    def get_screen_size(self) -> List[int]:
-        """
-        Get the size of the screen in pixels.
-        """
-        m = self._get_mouse()
-        return [m.screen_size()]
-
-    @action
-    def mouse_click(self, x: int, y: int, btn: int, repeat: int = 1):
-        """
-        Mouse click.
-        :param x: x screen position
-        :param y: y screen position
-        :param btn: Button number (1 for left, 2 for right, 3 for middle)
-        :param repeat: Number of clicks (default: 1)
-        """
-        m = self._get_mouse()
-        m.click(x, y, btn, n=repeat)
-
-
-# vim:sw=4:ts=4:et:
diff --git a/platypush/plugins/inputs/manifest.yaml b/platypush/plugins/inputs/manifest.yaml
deleted file mode 100644
index 332f693ff..000000000
--- a/platypush/plugins/inputs/manifest.yaml
+++ /dev/null
@@ -1,7 +0,0 @@
-manifest:
-  events: {}
-  install:
-    pip:
-    - pyuserinput
-  package: platypush.plugins.inputs
-  type: plugin
diff --git a/platypush/plugins/joystick/__init__.py b/platypush/plugins/joystick/__init__.py
new file mode 100644
index 000000000..6504e82c1
--- /dev/null
+++ b/platypush/plugins/joystick/__init__.py
@@ -0,0 +1,124 @@
+import multiprocessing
+import threading
+from queue import Empty
+from typing import Dict, List, Optional
+
+from platypush.plugins import RunnablePlugin, action
+from platypush.schemas.joystick import JoystickStatusSchema
+
+from ._inputs import DeviceManager, GamePad
+from ._manager import JoystickManager
+
+
+class JoystickPlugin(RunnablePlugin):
+    """
+    A plugin to monitor joystick events.
+    """
+
+    def __init__(self, poll_interval: float = 0.025, **kwargs):
+        """
+        :param poll_interval: Polling interval in seconds (default: 0.025)
+        """
+
+        super().__init__(poll_interval=poll_interval, **kwargs)
+        self._dev_managers: Dict[str, JoystickManager] = {}
+        self._state_queue = multiprocessing.Queue()
+        self._state_monitor_thread: Optional[threading.Thread] = None
+
+    @staticmethod
+    def _key(device: GamePad) -> str:
+        return device.get_char_device_path()
+
+    def _start_manager(self, device: GamePad):
+        if device in self._dev_managers:
+            return
+
+        dev_manager = JoystickManager(
+            device=device,
+            poll_interval=self.poll_interval,
+            state_queue=self._state_queue,
+        )
+
+        dev_manager.start()
+        self._dev_managers[self._key(device)] = dev_manager
+
+    def _stop_manager(self, device: str):
+        dev_manager = self._dev_managers.get(device)
+        if not dev_manager:
+            return
+
+        dev_manager.stop()
+        dev_manager.join(1)
+        if dev_manager.is_alive():
+            dev_manager.kill()
+
+        del self._dev_managers[device]
+
+    def _state_monitor(self):
+        while not self.should_stop():
+            try:
+                state = self._state_queue.get(timeout=0.5)
+                device = state.device
+                if device not in self._dev_managers:
+                    continue
+
+                self._dev_managers[device].state = state.state
+            except Empty:
+                pass
+            except Exception as e:
+                self.logger.exception(e)
+
+    def main(self):
+        if not self._state_monitor_thread:
+            self._state_monitor_thread = threading.Thread(
+                target=self._state_monitor, daemon=True
+            )
+            self._state_monitor_thread.start()
+
+        while not self.should_stop():
+            try:
+                devices = DeviceManager().gamepads
+                missing_devices_keys = set(self._dev_managers.keys()) - {
+                    self._key(dev) for dev in devices
+                }
+
+                new_devices = {
+                    dev for dev in devices if self._key(dev) not in self._dev_managers
+                }
+
+                # Stop managers for devices that are no longer connected
+                for dev in missing_devices_keys:
+                    self._stop_manager(dev)
+
+                # Start managers for new devices
+                for dev in new_devices:
+                    self._start_manager(dev)
+            except Exception as e:
+                self.logger.exception(e)
+            finally:
+                self.wait_stop(max(0.5, min(10, (self.poll_interval or 0) * 10)))
+
+    def stop(self):
+        for dev in list(self._dev_managers.keys()):
+            self._stop_manager(dev)
+
+        super().stop()
+
+    @action
+    def status(self) -> List[dict]:
+        """
+        :return: .. schema:: joystick.JoystickStatusSchema(many=True)
+        """
+        return JoystickStatusSchema().dump(
+            [
+                {
+                    'device': dev.device,
+                    'state': dev.state,
+                }
+                for dev in self._dev_managers.values()
+            ],
+            many=True,
+        )
+
+
+# vim:sw=4:ts=4:et:
diff --git a/platypush/plugins/joystick/_inputs.py b/platypush/plugins/joystick/_inputs.py
new file mode 100644
index 000000000..19060f3be
--- /dev/null
+++ b/platypush/plugins/joystick/_inputs.py
@@ -0,0 +1,3660 @@
+"""Inputs - user input for humans.
+
+Inputs aims to provide easy to use, cross-platform, user input device
+support for Python. I.e. keyboards, mice, gamepads, etc.
+
+Currently supported platforms are the Raspberry Pi, Linux, Windows and
+Mac OS X.
+
+"""
+
+# Forked by Fabio Manganiello <fabio@manganiello.tech> from:
+# Copyright (c) 2016, 2018: Zeth
+# All rights reserved.
+#
+# BSD Licence
+#
+# Redistribution and use in source and binary forms, with or without
+# modification, are permitted provided that the following conditions are met:
+#
+#     * Redistributions of source code must retain the above copyright
+#       notice, this list of conditions and the following disclaimer.
+#
+#     * Redistributions in binary form must reproduce the above copyright
+#       notice, this list of conditions and the following disclaimer in the
+#       documentation and/or other materials provided with the distribution.
+#
+#     * Neither the name of the copyright holder nor the
+#       names of its contributors may be used to endorse or promote products
+#       derived from this software without specific prior written permission.
+#
+# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
+# AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
+# IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
+# ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE
+# LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR
+# CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF
+# SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
+# INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN
+# CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
+# ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
+# POSSIBILITY OF SUCH DAMAGE.
+
+from __future__ import print_function
+from __future__ import division
+
+import os
+import sys
+import io
+import glob
+import struct
+import platform
+import math
+import time
+import codecs
+from warnings import warn
+from itertools import count
+from operator import itemgetter
+from multiprocessing import Process, Pipe
+import ctypes
+
+__version__ = "0.5"
+
+WIN = platform.system() == 'Windows'
+MAC = platform.system() == 'Darwin'
+NIX = platform.system() == 'Linux'
+
+if WIN:
+    # pylint: disable=wrong-import-position
+    import ctypes.wintypes
+
+    DWORD = ctypes.wintypes.DWORD
+    HANDLE = ctypes.wintypes.HANDLE
+    WPARAM = ctypes.wintypes.WPARAM
+    LPARAM = ctypes.wintypes.WPARAM
+    MSG = ctypes.wintypes.MSG
+else:
+    DWORD = ctypes.c_ulong
+    HANDLE = ctypes.c_void_p
+    WPARAM = ctypes.c_ulonglong
+    LPARAM = ctypes.c_ulonglong
+    MSG = ctypes.Structure
+
+if NIX:
+    from fcntl import ioctl
+
+OLD = sys.version_info < (3, 4)
+
+PERMISSIONS_ERROR_TEXT = (
+    "The user (that this program is being run as) does "
+    "not have permission to access the input events, "
+    "check groups and permissions, for example, on "
+    "Debian, the user needs to be in the input group."
+)
+
+# Standard event format for most devices.
+# long, long, unsigned short, unsigned short, int
+EVENT_FORMAT = str('llHHi')
+
+EVENT_SIZE = struct.calcsize(EVENT_FORMAT)
+
+
+def chunks(raw):
+    """Yield successive EVENT_SIZE sized chunks from raw."""
+    for i in range(0, len(raw), EVENT_SIZE):
+        yield struct.unpack(EVENT_FORMAT, raw[i : i + EVENT_SIZE])
+
+
+if OLD:
+
+    def iter_unpack(raw):
+        """Yield successive EVENT_SIZE chunks from message."""
+        return chunks(raw)
+
+else:
+
+    def iter_unpack(raw):
+        """Yield successive EVENT_SIZE chunks from message."""
+        return struct.iter_unpack(EVENT_FORMAT, raw)
+
+
+def convert_timeval(seconds_since_epoch):
+    """Convert time into C style timeval."""
+    frac, whole = math.modf(seconds_since_epoch)
+    microseconds = math.floor(frac * 1000000)
+    seconds = math.floor(whole)
+    return seconds, microseconds
+
+
+SPECIAL_DEVICES = (
+    (
+        "Raspberry Pi Sense HAT Joystick",
+        "/dev/input/by-id/gpio-Raspberry_Pi_Sense_HAT_Joystick-event-kbd",
+    ),
+    (
+        "Nintendo Wii Remote",
+        "/dev/input/by-id/bluetooth-Nintendo_Wii_Remote-event-joystick",
+    ),
+    (
+        "FT5406 memory based driver",
+        "/dev/input/by-id/gpio-Raspberry_Pi_Touchscreen_Display-event-mouse",
+    ),
+)
+
+XINPUT_MAPPING = (
+    (1, 0x11),
+    (2, 0x11),
+    (3, 0x10),
+    (4, 0x10),
+    (5, 0x13A),
+    (6, 0x13B),
+    (7, 0x13D),
+    (8, 0x13E),
+    (9, 0x136),
+    (10, 0x137),
+    (13, 0x130),
+    (14, 0x131),
+    (15, 0x134),
+    (16, 0x133),
+    (17, 0x11),
+    ('l_thumb_x', 0x00),
+    ('l_thumb_y', 0x01),
+    ('left_trigger', 0x02),
+    ('r_thumb_x', 0x03),
+    ('r_thumb_y', 0x04),
+    ('right_trigger', 0x05),
+)
+
+XINPUT_DLL_NAMES = (
+    "XInput1_4.dll",
+    "XInput9_1_0.dll",
+    "XInput1_3.dll",
+    "XInput1_2.dll",
+    "XInput1_1.dll",
+)
+
+XINPUT_ERROR_DEVICE_NOT_CONNECTED = 1167
+XINPUT_ERROR_SUCCESS = 0
+
+XBOX_STYLE_LED_CONTROL = {
+    0: 'off',
+    1: 'all blink, then previous setting',
+    2: '1/top-left blink, then on',
+    3: '2/top-right blink, then on',
+    4: '3/bottom-left blink, then on',
+    5: '4/bottom-right blink, then on',
+    6: '1/top-left on',
+    7: '2/top-right on',
+    8: '3/bottom-left on',
+    9: '4/bottom-right on',
+    10: 'rotate',
+    11: 'blink, based on previous setting',
+    12: 'slow blink, based on previous setting',
+    13: 'rotate with two lights',
+    14: 'persistent slow all blink',
+    15: 'blink once, then previous setting',
+}
+
+DEVICE_PROPERTIES = (
+    (0x00, "INPUT_PROP_POINTER"),  # needs a pointer
+    (0x01, "INPUT_PROP_DIRECT"),  # direct input devices
+    (0x02, "INPUT_PROP_BUTTONPAD"),  # has button(s) under pad
+    (0x03, "INPUT_PROP_SEMI_MT"),  # touch rectangle only
+    (0x04, "INPUT_PROP_TOPBUTTONPAD"),  # softbuttons at top of pad
+    (0x05, "INPUT_PROP_POINTING_STICK"),  # is a pointing stick
+    (0x06, "INPUT_PROP_ACCELEROMETER"),  # has accelerometer
+    (0x1F, "INPUT_PROP_MAX"),
+    (0x1F + 1, "INPUT_PROP_CNT"),
+)
+
+EVENT_TYPES = (
+    (0x00, "Sync"),
+    (0x01, "Key"),
+    (0x02, "Relative"),
+    (0x03, "Absolute"),
+    (0x04, "Misc"),
+    (0x05, "Switch"),
+    (0x11, "LED"),
+    (0x12, "Sound"),
+    (0x14, "Repeat"),
+    (0x15, "ForceFeedback"),
+    (0x16, "Power"),
+    (0x17, "ForceFeedbackStatus"),
+    (0x1F, "Max"),
+    (0x1F + 1, "Current"),
+)
+
+SYNCHRONIZATION_EVENTS = (
+    (0, "SYN_REPORT"),
+    (1, "SYN_CONFIG"),
+    (2, "SYN_MT_REPORT"),
+    (3, "SYN_DROPPED"),
+    (0xF, "SYN_MAX"),
+    (0xF + 1, "SYN_CNT"),
+)
+
+KEYS_AND_BUTTONS = (
+    (0, "KEY_RESERVED"),
+    (1, "KEY_ESC"),
+    (2, "KEY_1"),
+    (3, "KEY_2"),
+    (4, "KEY_3"),
+    (5, "KEY_4"),
+    (6, "KEY_5"),
+    (7, "KEY_6"),
+    (8, "KEY_7"),
+    (9, "KEY_8"),
+    (10, "KEY_9"),
+    (11, "KEY_0"),
+    (12, "KEY_MINUS"),
+    (13, "KEY_EQUAL"),
+    (14, "KEY_BACKSPACE"),
+    (15, "KEY_TAB"),
+    (16, "KEY_Q"),
+    (17, "KEY_W"),
+    (18, "KEY_E"),
+    (19, "KEY_R"),
+    (20, "KEY_T"),
+    (21, "KEY_Y"),
+    (22, "KEY_U"),
+    (23, "KEY_I"),
+    (24, "KEY_O"),
+    (25, "KEY_P"),
+    (26, "KEY_LEFTBRACE"),
+    (27, "KEY_RIGHTBRACE"),
+    (28, "KEY_ENTER"),
+    (29, "KEY_LEFTCTRL"),
+    (30, "KEY_A"),
+    (31, "KEY_S"),
+    (32, "KEY_D"),
+    (33, "KEY_F"),
+    (34, "KEY_G"),
+    (35, "KEY_H"),
+    (36, "KEY_J"),
+    (37, "KEY_K"),
+    (38, "KEY_L"),
+    (39, "KEY_SEMICOLON"),
+    (40, "KEY_APOSTROPHE"),
+    (41, "KEY_GRAVE"),
+    (42, "KEY_LEFTSHIFT"),
+    (43, "KEY_BACKSLASH"),
+    (44, "KEY_Z"),
+    (45, "KEY_X"),
+    (46, "KEY_C"),
+    (47, "KEY_V"),
+    (48, "KEY_B"),
+    (49, "KEY_N"),
+    (50, "KEY_M"),
+    (51, "KEY_COMMA"),
+    (52, "KEY_DOT"),
+    (53, "KEY_SLASH"),
+    (54, "KEY_RIGHTSHIFT"),
+    (55, "KEY_KPASTERISK"),
+    (56, "KEY_LEFTALT"),
+    (57, "KEY_SPACE"),
+    (58, "KEY_CAPSLOCK"),
+    (59, "KEY_F1"),
+    (60, "KEY_F2"),
+    (61, "KEY_F3"),
+    (62, "KEY_F4"),
+    (63, "KEY_F5"),
+    (64, "KEY_F6"),
+    (65, "KEY_F7"),
+    (66, "KEY_F8"),
+    (67, "KEY_F9"),
+    (68, "KEY_F10"),
+    (69, "KEY_NUMLOCK"),
+    (70, "KEY_SCROLLLOCK"),
+    (71, "KEY_KP7"),
+    (72, "KEY_KP8"),
+    (73, "KEY_KP9"),
+    (74, "KEY_KPMINUS"),
+    (75, "KEY_KP4"),
+    (76, "KEY_KP5"),
+    (77, "KEY_KP6"),
+    (78, "KEY_KPPLUS"),
+    (79, "KEY_KP1"),
+    (80, "KEY_KP2"),
+    (81, "KEY_KP3"),
+    (82, "KEY_KP0"),
+    (83, "KEY_KPDOT"),
+    (85, "KEY_ZENKAKUHANKAKU"),
+    (86, "KEY_102ND"),
+    (87, "KEY_F11"),
+    (88, "KEY_F12"),
+    (89, "KEY_RO"),
+    (90, "KEY_KATAKANA"),
+    (91, "KEY_HIRAGANA"),
+    (92, "KEY_HENKAN"),
+    (93, "KEY_KATAKANAHIRAGANA"),
+    (94, "KEY_MUHENKAN"),
+    (95, "KEY_KPJPCOMMA"),
+    (96, "KEY_KPENTER"),
+    (97, "KEY_RIGHTCTRL"),
+    (98, "KEY_KPSLASH"),
+    (99, "KEY_SYSRQ"),
+    (100, "KEY_RIGHTALT"),
+    (101, "KEY_LINEFEED"),
+    (102, "KEY_HOME"),
+    (103, "KEY_UP"),
+    (104, "KEY_PAGEUP"),
+    (105, "KEY_LEFT"),
+    (106, "KEY_RIGHT"),
+    (107, "KEY_END"),
+    (108, "KEY_DOWN"),
+    (109, "KEY_PAGEDOWN"),
+    (110, "KEY_INSERT"),
+    (111, "KEY_DELETE"),
+    (112, "KEY_MACRO"),
+    (113, "KEY_MUTE"),
+    (114, "KEY_VOLUMEDOWN"),
+    (115, "KEY_VOLUMEUP"),
+    (116, "KEY_POWER"),  # SC System Power Down
+    (117, "KEY_KPEQUAL"),
+    (118, "KEY_KPPLUSMINUS"),
+    (119, "KEY_PAUSE"),
+    (120, "KEY_SCALE"),  # AL Compiz Scale (Expose)
+    (121, "KEY_KPCOMMA"),
+    (122, "KEY_HANGEUL"),
+    (123, "KEY_HANJA"),
+    (124, "KEY_YEN"),
+    (125, "KEY_LEFTMETA"),
+    (126, "KEY_RIGHTMETA"),
+    (127, "KEY_COMPOSE"),
+    (128, "KEY_STOP"),  # AC Stop
+    (129, "KEY_AGAIN"),
+    (130, "KEY_PROPS"),  # AC Properties
+    (131, "KEY_UNDO"),  # AC Undo
+    (132, "KEY_FRONT"),
+    (133, "KEY_COPY"),  # AC Copy
+    (134, "KEY_OPEN"),  # AC Open
+    (135, "KEY_PASTE"),  # AC Paste
+    (136, "KEY_FIND"),  # AC Search
+    (137, "KEY_CUT"),  # AC Cut
+    (138, "KEY_HELP"),  # AL Integrated Help Center
+    (139, "KEY_MENU"),  # Menu (show menu)
+    (140, "KEY_CALC"),  # AL Calculator
+    (141, "KEY_SETUP"),
+    (142, "KEY_SLEEP"),  # SC System Sleep
+    (143, "KEY_WAKEUP"),  # System Wake Up
+    (144, "KEY_FILE"),  # AL Local Machine Browser
+    (145, "KEY_SENDFILE"),
+    (146, "KEY_DELETEFILE"),
+    (147, "KEY_XFER"),
+    (148, "KEY_PROG1"),
+    (149, "KEY_PROG2"),
+    (150, "KEY_WWW"),  # AL Internet Browser
+    (151, "KEY_MSDOS"),
+    (152, "KEY_COFFEE"),  # AL Terminal Lock/Screensaver
+    (153, "KEY_ROTATE_DISPLAY"),  # Display orientation for e.g. tablets
+    (154, "KEY_CYCLEWINDOWS"),
+    (155, "KEY_MAIL"),
+    (156, "KEY_BOOKMARKS"),  # AC Bookmarks
+    (157, "KEY_COMPUTER"),
+    (158, "KEY_BACK"),  # AC Back
+    (159, "KEY_FORWARD"),  # AC Forward
+    (160, "KEY_CLOSECD"),
+    (161, "KEY_EJECTCD"),
+    (162, "KEY_EJECTCLOSECD"),
+    (163, "KEY_NEXTSONG"),
+    (164, "KEY_PLAYPAUSE"),
+    (165, "KEY_PREVIOUSSONG"),
+    (166, "KEY_STOPCD"),
+    (167, "KEY_RECORD"),
+    (168, "KEY_REWIND"),
+    (169, "KEY_PHONE"),  # Media Select Telephone
+    (170, "KEY_ISO"),
+    (171, "KEY_CONFIG"),  # AL Consumer Control Configuration
+    (172, "KEY_HOMEPAGE"),  # AC Home
+    (173, "KEY_REFRESH"),  # AC Refresh
+    (174, "KEY_EXIT"),  # AC Exit
+    (175, "KEY_MOVE"),
+    (176, "KEY_EDIT"),
+    (177, "KEY_SCROLLUP"),
+    (178, "KEY_SCROLLDOWN"),
+    (179, "KEY_KPLEFTPAREN"),
+    (180, "KEY_KPRIGHTPAREN"),
+    (181, "KEY_NEW"),  # AC New
+    (182, "KEY_REDO"),  # AC Redo/Repeat
+    (183, "KEY_F13"),
+    (184, "KEY_F14"),
+    (185, "KEY_F15"),
+    (186, "KEY_F16"),
+    (187, "KEY_F17"),
+    (188, "KEY_F18"),
+    (189, "KEY_F19"),
+    (190, "KEY_F20"),
+    (191, "KEY_F21"),
+    (192, "KEY_F22"),
+    (193, "KEY_F23"),
+    (194, "KEY_F24"),
+    (200, "KEY_PLAYCD"),
+    (201, "KEY_PAUSECD"),
+    (202, "KEY_PROG3"),
+    (203, "KEY_PROG4"),
+    (204, "KEY_DASHBOARD"),  # AL Dashboard
+    (205, "KEY_SUSPEND"),
+    (206, "KEY_CLOSE"),  # AC Close
+    (207, "KEY_PLAY"),
+    (208, "KEY_FASTFORWARD"),
+    (209, "KEY_BASSBOOST"),
+    (210, "KEY_PRINT"),  # AC Print
+    (211, "KEY_HP"),
+    (212, "KEY_CAMERA"),
+    (213, "KEY_SOUND"),
+    (214, "KEY_QUESTION"),
+    (215, "KEY_EMAIL"),
+    (216, "KEY_CHAT"),
+    (217, "KEY_SEARCH"),
+    (218, "KEY_CONNECT"),
+    (219, "KEY_FINANCE"),  # AL Checkbook/Finance
+    (220, "KEY_SPORT"),
+    (221, "KEY_SHOP"),
+    (222, "KEY_ALTERASE"),
+    (223, "KEY_CANCEL"),  # AC Cancel
+    (224, "KEY_BRIGHTNESSDOWN"),
+    (225, "KEY_BRIGHTNESSUP"),
+    (226, "KEY_MEDIA"),
+    (227, "KEY_SWITCHVIDEOMODE"),  # Cycle between available video
+    (228, "KEY_KBDILLUMTOGGLE"),
+    (229, "KEY_KBDILLUMDOWN"),
+    (230, "KEY_KBDILLUMUP"),
+    (231, "KEY_SEND"),  # AC Send
+    (232, "KEY_REPLY"),  # AC Reply
+    (233, "KEY_FORWARDMAIL"),  # AC Forward Msg
+    (234, "KEY_SAVE"),  # AC Save
+    (235, "KEY_DOCUMENTS"),
+    (236, "KEY_BATTERY"),
+    (237, "KEY_BLUETOOTH"),
+    (238, "KEY_WLAN"),
+    (239, "KEY_UWB"),
+    (240, "KEY_UNKNOWN"),
+    (241, "KEY_VIDEO_NEXT"),  # drive next video source
+    (242, "KEY_VIDEO_PREV"),  # drive previous video source
+    (243, "KEY_BRIGHTNESS_CYCLE"),  # brightness up, after max is min
+    (244, "KEY_BRIGHTNESS_AUTO"),  # Set Auto Brightness: manual
+    (245, "KEY_DISPLAY_OFF"),  # display device to off state
+    (246, "KEY_WWAN"),  # Wireless WAN (LTE, UMTS, GSM, etc.)
+    (247, "KEY_RFKILL"),  # Key that controls all radios
+    (248, "KEY_MICMUTE"),  # Mute / unmute the microphone
+    (0x100, "BTN_MISC"),
+    (0x100, "BTN_0"),
+    (0x101, "BTN_1"),
+    (0x102, "BTN_2"),
+    (0x103, "BTN_3"),
+    (0x104, "BTN_4"),
+    (0x105, "BTN_5"),
+    (0x106, "BTN_6"),
+    (0x107, "BTN_7"),
+    (0x108, "BTN_8"),
+    (0x109, "BTN_9"),
+    (0x110, "BTN_MOUSE"),
+    (0x110, "BTN_LEFT"),
+    (0x111, "BTN_RIGHT"),
+    (0x112, "BTN_MIDDLE"),
+    (0x113, "BTN_SIDE"),
+    (0x114, "BTN_EXTRA"),
+    (0x115, "BTN_FORWARD"),
+    (0x116, "BTN_BACK"),
+    (0x117, "BTN_TASK"),
+    (0x120, "BTN_JOYSTICK"),
+    (0x120, "BTN_TRIGGER"),
+    (0x121, "BTN_THUMB"),
+    (0x122, "BTN_THUMB2"),
+    (0x123, "BTN_TOP"),
+    (0x124, "BTN_TOP2"),
+    (0x125, "BTN_PINKIE"),
+    (0x126, "BTN_BASE"),
+    (0x127, "BTN_BASE2"),
+    (0x128, "BTN_BASE3"),
+    (0x129, "BTN_BASE4"),
+    (0x12A, "BTN_BASE5"),
+    (0x12B, "BTN_BASE6"),
+    (0x12F, "BTN_DEAD"),
+    (0x130, "BTN_GAMEPAD"),
+    (0x130, "BTN_SOUTH"),
+    (0x131, "BTN_EAST"),
+    (0x132, "BTN_C"),
+    (0x133, "BTN_NORTH"),
+    (0x134, "BTN_WEST"),
+    (0x135, "BTN_Z"),
+    (0x136, "BTN_TL"),
+    (0x137, "BTN_TR"),
+    (0x138, "BTN_TL2"),
+    (0x139, "BTN_TR2"),
+    (0x13A, "BTN_SELECT"),
+    (0x13B, "BTN_START"),
+    (0x13C, "BTN_MODE"),
+    (0x13D, "BTN_THUMBL"),
+    (0x13E, "BTN_THUMBR"),
+    (0x140, "BTN_DIGI"),
+    (0x140, "BTN_TOOL_PEN"),
+    (0x141, "BTN_TOOL_RUBBER"),
+    (0x142, "BTN_TOOL_BRUSH"),
+    (0x143, "BTN_TOOL_PENCIL"),
+    (0x144, "BTN_TOOL_AIRBRUSH"),
+    (0x145, "BTN_TOOL_FINGER"),
+    (0x146, "BTN_TOOL_MOUSE"),
+    (0x147, "BTN_TOOL_LENS"),
+    (0x148, "BTN_TOOL_QUINTTAP"),  # Five fingers on trackpad
+    (0x14A, "BTN_TOUCH"),
+    (0x14B, "BTN_STYLUS"),
+    (0x14C, "BTN_STYLUS2"),
+    (0x14D, "BTN_TOOL_DOUBLETAP"),
+    (0x14E, "BTN_TOOL_TRIPLETAP"),
+    (0x14F, "BTN_TOOL_QUADTAP"),  # Four fingers on trackpad
+    (0x150, "BTN_WHEEL"),
+    (0x150, "BTN_GEAR_DOWN"),
+    (0x151, "BTN_GEAR_UP"),
+    (0x160, "KEY_OK"),
+    (0x161, "KEY_SELECT"),
+    (0x162, "KEY_GOTO"),
+    (0x163, "KEY_CLEAR"),
+    (0x164, "KEY_POWER2"),
+    (0x165, "KEY_OPTION"),
+    (0x166, "KEY_INFO"),  # AL OEM Features/Tips/Tutorial
+    (0x167, "KEY_TIME"),
+    (0x168, "KEY_VENDOR"),
+    (0x169, "KEY_ARCHIVE"),
+    (0x16A, "KEY_PROGRAM"),  # Media Select Program Guide
+    (0x16B, "KEY_CHANNEL"),
+    (0x16C, "KEY_FAVORITES"),
+    (0x16D, "KEY_EPG"),
+    (0x16E, "KEY_PVR"),  # Media Select Home
+    (0x16F, "KEY_MHP"),
+    (0x170, "KEY_LANGUAGE"),
+    (0x171, "KEY_TITLE"),
+    (0x172, "KEY_SUBTITLE"),
+    (0x173, "KEY_ANGLE"),
+    (0x174, "KEY_ZOOM"),
+    (0x175, "KEY_MODE"),
+    (0x176, "KEY_KEYBOARD"),
+    (0x177, "KEY_SCREEN"),
+    (0x178, "KEY_PC"),  # Media Select Computer
+    (0x179, "KEY_TV"),  # Media Select TV
+    (0x17A, "KEY_TV2"),  # Media Select Cable
+    (0x17B, "KEY_VCR"),  # Media Select VCR
+    (0x17C, "KEY_VCR2"),  # VCR Plus
+    (0x17D, "KEY_SAT"),  # Media Select Satellite
+    (0x17E, "KEY_SAT2"),
+    (0x17F, "KEY_CD"),  # Media Select CD
+    (0x180, "KEY_TAPE"),  # Media Select Tape
+    (0x181, "KEY_RADIO"),
+    (0x182, "KEY_TUNER"),  # Media Select Tuner
+    (0x183, "KEY_PLAYER"),
+    (0x184, "KEY_TEXT"),
+    (0x185, "KEY_DVD"),  # Media Select DVD
+    (0x186, "KEY_AUX"),
+    (0x187, "KEY_MP3"),
+    (0x188, "KEY_AUDIO"),  # AL Audio Browser
+    (0x189, "KEY_VIDEO"),  # AL Movie Browser
+    (0x18A, "KEY_DIRECTORY"),
+    (0x18B, "KEY_LIST"),
+    (0x18C, "KEY_MEMO"),  # Media Select Messages
+    (0x18D, "KEY_CALENDAR"),
+    (0x18E, "KEY_RED"),
+    (0x18F, "KEY_GREEN"),
+    (0x190, "KEY_YELLOW"),
+    (0x191, "KEY_BLUE"),
+    (0x192, "KEY_CHANNELUP"),  # Channel Increment
+    (0x193, "KEY_CHANNELDOWN"),  # Channel Decrement
+    (0x194, "KEY_FIRST"),
+    (0x195, "KEY_LAST"),  # Recall Last
+    (0x196, "KEY_AB"),
+    (0x197, "KEY_NEXT"),
+    (0x198, "KEY_RESTART"),
+    (0x199, "KEY_SLOW"),
+    (0x19A, "KEY_SHUFFLE"),
+    (0x19B, "KEY_BREAK"),
+    (0x19C, "KEY_PREVIOUS"),
+    (0x19D, "KEY_DIGITS"),
+    (0x19E, "KEY_TEEN"),
+    (0x19F, "KEY_TWEN"),
+    (0x1A0, "KEY_VIDEOPHONE"),  # Media Select Video Phone
+    (0x1A1, "KEY_GAMES"),  # Media Select Games
+    (0x1A2, "KEY_ZOOMIN"),  # AC Zoom In
+    (0x1A3, "KEY_ZOOMOUT"),  # AC Zoom Out
+    (0x1A4, "KEY_ZOOMRESET"),  # AC Zoom
+    (0x1A5, "KEY_WORDPROCESSOR"),  # AL Word Processor
+    (0x1A6, "KEY_EDITOR"),  # AL Text Editor
+    (0x1A7, "KEY_SPREADSHEET"),  # AL Spreadsheet
+    (0x1A8, "KEY_GRAPHICSEDITOR"),  # AL Graphics Editor
+    (0x1A9, "KEY_PRESENTATION"),  # AL Presentation App
+    (0x1AA, "KEY_DATABASE"),  # AL Database App
+    (0x1AB, "KEY_NEWS"),  # AL Newsreader
+    (0x1AC, "KEY_VOICEMAIL"),  # AL Voicemail
+    (0x1AD, "KEY_ADDRESSBOOK"),  # AL Contacts/Address Book
+    (0x1AE, "KEY_MESSENGER"),  # AL Instant Messaging
+    (0x1AF, "KEY_DISPLAYTOGGLE"),  # Turn display (LCD) on and off
+    (0x1B0, "KEY_SPELLCHECK"),  # AL Spell Check
+    (0x1B1, "KEY_LOGOFF"),  # AL Logoff
+    (0x1B2, "KEY_DOLLAR"),
+    (0x1B3, "KEY_EURO"),
+    (0x1B4, "KEY_FRAMEBACK"),  # Consumer - transport controls
+    (0x1B5, "KEY_FRAMEFORWARD"),
+    (0x1B6, "KEY_CONTEXT_MENU"),  # GenDesc - system context menu
+    (0x1B7, "KEY_MEDIA_REPEAT"),  # Consumer - transport control
+    (0x1B8, "KEY_10CHANNELSUP"),  # 10 channels up (10+)
+    (0x1B9, "KEY_10CHANNELSDOWN"),  # 10 channels down (10-)
+    (0x1BA, "KEY_IMAGES"),  # AL Image Browser
+    (0x1C0, "KEY_DEL_EOL"),
+    (0x1C1, "KEY_DEL_EOS"),
+    (0x1C2, "KEY_INS_LINE"),
+    (0x1C3, "KEY_DEL_LINE"),
+    (0x1D0, "KEY_FN"),
+    (0x1D1, "KEY_FN_ESC"),
+    (0x1D2, "KEY_FN_F1"),
+    (0x1D3, "KEY_FN_F2"),
+    (0x1D4, "KEY_FN_F3"),
+    (0x1D5, "KEY_FN_F4"),
+    (0x1D6, "KEY_FN_F5"),
+    (0x1D7, "KEY_FN_F6"),
+    (0x1D8, "KEY_FN_F7"),
+    (0x1D9, "KEY_FN_F8"),
+    (0x1DA, "KEY_FN_F9"),
+    (0x1DB, "KEY_FN_F10"),
+    (0x1DC, "KEY_FN_F11"),
+    (0x1DD, "KEY_FN_F12"),
+    (0x1DE, "KEY_FN_1"),
+    (0x1DF, "KEY_FN_2"),
+    (0x1E0, "KEY_FN_D"),
+    (0x1E1, "KEY_FN_E"),
+    (0x1E2, "KEY_FN_F"),
+    (0x1E3, "KEY_FN_S"),
+    (0x1E4, "KEY_FN_B"),
+    (0x1F1, "KEY_BRL_DOT1"),
+    (0x1F2, "KEY_BRL_DOT2"),
+    (0x1F3, "KEY_BRL_DOT3"),
+    (0x1F4, "KEY_BRL_DOT4"),
+    (0x1F5, "KEY_BRL_DOT5"),
+    (0x1F6, "KEY_BRL_DOT6"),
+    (0x1F7, "KEY_BRL_DOT7"),
+    (0x1F8, "KEY_BRL_DOT8"),
+    (0x1F9, "KEY_BRL_DOT9"),
+    (0x1FA, "KEY_BRL_DOT10"),
+    (0x200, "KEY_NUMERIC_0"),  # used by phones, remote controls,
+    (0x201, "KEY_NUMERIC_1"),  # and other keypads
+    (0x202, "KEY_NUMERIC_2"),
+    (0x203, "KEY_NUMERIC_3"),
+    (0x204, "KEY_NUMERIC_4"),
+    (0x205, "KEY_NUMERIC_5"),
+    (0x206, "KEY_NUMERIC_6"),
+    (0x207, "KEY_NUMERIC_7"),
+    (0x208, "KEY_NUMERIC_8"),
+    (0x209, "KEY_NUMERIC_9"),
+    (0x20A, "KEY_NUMERIC_STAR"),
+    (0x20B, "KEY_NUMERIC_POUND"),
+    (0x20C, "KEY_NUMERIC_A"),  # Phone key A - HUT Telephony 0xb9
+    (0x20D, "KEY_NUMERIC_B"),
+    (0x20E, "KEY_NUMERIC_C"),
+    (0x20F, "KEY_NUMERIC_D"),
+    (0x210, "KEY_CAMERA_FOCUS"),
+    (0x211, "KEY_WPS_BUTTON"),  # WiFi Protected Setup key
+    (0x212, "KEY_TOUCHPAD_TOGGLE"),  # Request switch touchpad on or off
+    (0x213, "KEY_TOUCHPAD_ON"),
+    (0x214, "KEY_TOUCHPAD_OFF"),
+    (0x215, "KEY_CAMERA_ZOOMIN"),
+    (0x216, "KEY_CAMERA_ZOOMOUT"),
+    (0x217, "KEY_CAMERA_UP"),
+    (0x218, "KEY_CAMERA_DOWN"),
+    (0x219, "KEY_CAMERA_LEFT"),
+    (0x21A, "KEY_CAMERA_RIGHT"),
+    (0x21B, "KEY_ATTENDANT_ON"),
+    (0x21C, "KEY_ATTENDANT_OFF"),
+    (0x21D, "KEY_ATTENDANT_TOGGLE"),  # Attendant call on or off
+    (0x21E, "KEY_LIGHTS_TOGGLE"),  # Reading light on or off
+    (0x220, "BTN_DPAD_UP"),
+    (0x221, "BTN_DPAD_DOWN"),
+    (0x222, "BTN_DPAD_LEFT"),
+    (0x223, "BTN_DPAD_RIGHT"),
+    (0x230, "KEY_ALS_TOGGLE"),  # Ambient light sensor
+    (0x240, "KEY_BUTTONCONFIG"),  # AL Button Configuration
+    (0x241, "KEY_TASKMANAGER"),  # AL Task/Project Manager
+    (0x242, "KEY_JOURNAL"),  # AL Log/Journal/Timecard
+    (0x243, "KEY_CONTROLPANEL"),  # AL Control Panel
+    (0x244, "KEY_APPSELECT"),  # AL Select Task/Application
+    (0x245, "KEY_SCREENSAVER"),  # AL Screen Saver
+    (0x246, "KEY_VOICECOMMAND"),  # Listening Voice Command
+    (0x250, "KEY_BRIGHTNESS_MIN"),  # Set Brightness to Minimum
+    (0x251, "KEY_BRIGHTNESS_MAX"),  # Set Brightness to Maximum
+    (0x260, "KEY_KBDINPUTASSIST_PREV"),
+    (0x261, "KEY_KBDINPUTASSIST_NEXT"),
+    (0x262, "KEY_KBDINPUTASSIST_PREVGROUP"),
+    (0x263, "KEY_KBDINPUTASSIST_NEXTGROUP"),
+    (0x264, "KEY_KBDINPUTASSIST_ACCEPT"),
+    (0x265, "KEY_KBDINPUTASSIST_CANCEL"),
+    (0x2C0, "BTN_TRIGGER_HAPPY"),
+    (0x2C0, "BTN_TRIGGER_HAPPY1"),
+    (0x2C1, "BTN_TRIGGER_HAPPY2"),
+    (0x2C2, "BTN_TRIGGER_HAPPY3"),
+    (0x2C3, "BTN_TRIGGER_HAPPY4"),
+    (0x2C4, "BTN_TRIGGER_HAPPY5"),
+    (0x2C5, "BTN_TRIGGER_HAPPY6"),
+    (0x2C6, "BTN_TRIGGER_HAPPY7"),
+    (0x2C7, "BTN_TRIGGER_HAPPY8"),
+    (0x2C8, "BTN_TRIGGER_HAPPY9"),
+    (0x2C9, "BTN_TRIGGER_HAPPY10"),
+    (0x2CA, "BTN_TRIGGER_HAPPY11"),
+    (0x2CB, "BTN_TRIGGER_HAPPY12"),
+    (0x2CC, "BTN_TRIGGER_HAPPY13"),
+    (0x2CD, "BTN_TRIGGER_HAPPY14"),
+    (0x2CE, "BTN_TRIGGER_HAPPY15"),
+    (0x2CF, "BTN_TRIGGER_HAPPY16"),
+    (0x2D0, "BTN_TRIGGER_HAPPY17"),
+    (0x2D1, "BTN_TRIGGER_HAPPY18"),
+    (0x2D2, "BTN_TRIGGER_HAPPY19"),
+    (0x2D3, "BTN_TRIGGER_HAPPY20"),
+    (0x2D4, "BTN_TRIGGER_HAPPY21"),
+    (0x2D5, "BTN_TRIGGER_HAPPY22"),
+    (0x2D6, "BTN_TRIGGER_HAPPY23"),
+    (0x2D7, "BTN_TRIGGER_HAPPY24"),
+    (0x2D8, "BTN_TRIGGER_HAPPY25"),
+    (0x2D9, "BTN_TRIGGER_HAPPY26"),
+    (0x2DA, "BTN_TRIGGER_HAPPY27"),
+    (0x2DB, "BTN_TRIGGER_HAPPY28"),
+    (0x2DC, "BTN_TRIGGER_HAPPY29"),
+    (0x2DD, "BTN_TRIGGER_HAPPY30"),
+    (0x2DE, "BTN_TRIGGER_HAPPY31"),
+    (0x2DF, "BTN_TRIGGER_HAPPY32"),
+    (0x2E0, "BTN_TRIGGER_HAPPY33"),
+    (0x2E1, "BTN_TRIGGER_HAPPY34"),
+    (0x2E2, "BTN_TRIGGER_HAPPY35"),
+    (0x2E3, "BTN_TRIGGER_HAPPY36"),
+    (0x2E4, "BTN_TRIGGER_HAPPY37"),
+    (0x2E5, "BTN_TRIGGER_HAPPY38"),
+    (0x2E6, "BTN_TRIGGER_HAPPY39"),
+    (0x2E7, "BTN_TRIGGER_HAPPY40"),
+    (0x2FF, "KEY_MAX"),
+    (0x2FF + 1, "KEY_CNT"),
+)
+
+RELATIVE_AXES = (
+    (0x00, "REL_X"),
+    (0x01, "REL_Y"),
+    (0x02, "REL_Z"),
+    (0x03, "REL_RX"),
+    (0x04, "REL_RY"),
+    (0x05, "REL_RZ"),
+    (0x06, "REL_HWHEEL"),
+    (0x07, "REL_DIAL"),
+    (0x08, "REL_WHEEL"),
+    (0x09, "REL_MISC"),
+    (0x0F, "REL_MAX"),
+    (0x0F + 1, "REL_CNT"),
+)
+
+ABSOLUTE_AXES = (
+    (0x00, "ABS_X"),
+    (0x01, "ABS_Y"),
+    (0x02, "ABS_Z"),
+    (0x03, "ABS_RX"),
+    (0x04, "ABS_RY"),
+    (0x05, "ABS_RZ"),
+    (0x06, "ABS_THROTTLE"),
+    (0x07, "ABS_RUDDER"),
+    (0x08, "ABS_WHEEL"),
+    (0x09, "ABS_GAS"),
+    (0x0A, "ABS_BRAKE"),
+    (0x10, "ABS_HAT0X"),
+    (0x11, "ABS_HAT0Y"),
+    (0x12, "ABS_HAT1X"),
+    (0x13, "ABS_HAT1Y"),
+    (0x14, "ABS_HAT2X"),
+    (0x15, "ABS_HAT2Y"),
+    (0x16, "ABS_HAT3X"),
+    (0x17, "ABS_HAT3Y"),
+    (0x18, "ABS_PRESSURE"),
+    (0x19, "ABS_DISTANCE"),
+    (0x1A, "ABS_TILT_X"),
+    (0x1B, "ABS_TILT_Y"),
+    (0x1C, "ABS_TOOL_WIDTH"),
+    (0x20, "ABS_VOLUME"),
+    (0x28, "ABS_MISC"),
+    (0x2F, "ABS_MT_SLOT"),  # MT slot being modified
+    (0x30, "ABS_MT_TOUCH_MAJOR"),  # Major axis of touching ellipse
+    (0x31, "ABS_MT_TOUCH_MINOR"),  # Minor axis (omit if circular)
+    (0x32, "ABS_MT_WIDTH_MAJOR"),  # Major axis of approaching ellipse
+    (0x33, "ABS_MT_WIDTH_MINOR"),  # Minor axis (omit if circular)
+    (0x34, "ABS_MT_ORIENTATION"),  # Ellipse orientation
+    (0x35, "ABS_MT_POSITION_X"),  # Center X touch position
+    (0x36, "ABS_MT_POSITION_Y"),  # Center Y touch position
+    (0x37, "ABS_MT_TOOL_TYPE"),  # Type of touching device
+    (0x38, "ABS_MT_BLOB_ID"),  # Group a set of packets as a blob
+    (0x39, "ABS_MT_TRACKING_ID"),  # Unique ID of initiated contact
+    (0x3A, "ABS_MT_PRESSURE"),  # Pressure on contact area
+    (0x3B, "ABS_MT_DISTANCE"),  # Contact hover distance
+    (0x3C, "ABS_MT_TOOL_X"),  # Center X tool position
+    (0x3D, "ABS_MT_TOOL_Y"),  # Center Y tool position
+    (0x3F, "ABS_MAX"),
+    (0x3F + 1, "ABS_CNT"),
+)
+
+SWITCH_EVENTS = (
+    (0x00, "SW_LID"),  # set = lid shut
+    (0x01, "SW_TABLET_MODE"),  # set = tablet mode
+    (0x02, "SW_HEADPHONE_INSERT"),  # set = inserted
+    (0x03, "SW_RFKILL_ALL"),  # rfkill master switch, type "any"
+    (0x04, "SW_MICROPHONE_INSERT"),  # set = inserted
+    (0x05, "SW_DOCK"),  # set = plugged into dock
+    (0x06, "SW_LINEOUT_INSERT"),  # set = inserted
+    (0x07, "SW_JACK_PHYSICAL_INSERT"),  # set = mechanical switch set
+    (0x08, "SW_VIDEOOUT_INSERT"),  # set = inserted
+    (0x09, "SW_CAMERA_LENS_COVER"),  # set = lens covered
+    (0x0A, "SW_KEYPAD_SLIDE"),  # set = keypad slide out
+    (0x0B, "SW_FRONT_PROXIMITY"),  # set = front proximity sensor active
+    (0x0C, "SW_ROTATE_LOCK"),  # set = rotate locked/disabled
+    (0x0D, "SW_LINEIN_INSERT"),  # set = inserted
+    (0x0E, "SW_MUTE_DEVICE"),  # set = device disabled
+    (0x0F, "SW_MAX"),
+    (0x0F + 1, "SW_CNT"),
+)
+
+MISC_EVENTS = (
+    (0x00, "MSC_SERIAL"),
+    (0x01, "MSC_PULSELED"),
+    (0x02, "MSC_GESTURE"),
+    (0x03, "MSC_RAW"),
+    (0x04, "MSC_SCAN"),
+    (0x05, "MSC_TIMESTAMP"),
+    (0x07, "MSC_MAX"),
+    (0x07 + 1, "MSC_CNT"),
+)
+
+LEDS = (
+    (0x00, "LED_NUML"),
+    (0x01, "LED_CAPSL"),
+    (0x02, "LED_SCROLLL"),
+    (0x03, "LED_COMPOSE"),
+    (0x04, "LED_KANA"),
+    (0x05, "LED_SLEEP"),
+    (0x06, "LED_SUSPEND"),
+    (0x07, "LED_MUTE"),
+    (0x08, "LED_MISC"),
+    (0x09, "LED_MAIL"),
+    (0x0A, "LED_CHARGING"),
+    (0x0F, "LED_MAX"),
+    (0x0F + 1, "LED_CNT"),
+)
+
+LED_TYPE_CODES = (
+    ('numlock', 0x00),
+    ('capslock', 0x01),
+    ('scrolllock', 0x02),
+    ('compose', 0x03),
+    ('kana', 0x04),
+    ('sleep', 0x05),
+    ('suspend', 0x06),
+    ('mute', 0x07),
+    ('misc', 0x08),
+    ('mail', 0x09),
+    ('charging', 0x0A),
+    ('max', 0x0F),
+    ('cnt', 0x0F + 1),
+)
+
+AUTOREPEAT_VALUES = (
+    (0x00, "REP_DELAY"),
+    (0x01, "REP_PERIOD"),
+    (0x01, "REP_MAX"),
+    (0x01 + 1, "REP_CNT"),
+)
+
+SOUNDS = (
+    (0x00, "SND_CLICK"),
+    (0x01, "SND_BELL"),
+    (0x02, "SND_TONE"),
+    (0x07, "SND_MAX"),
+    (0x07 + 1, "SND_CNT"),
+)
+
+WIN_KEYBOARD_CODES = {
+    0x0100: 1,
+    0x0101: 0,
+    0x104: 1,
+    0x105: 0,
+}
+
+WIN_MOUSE_CODES = {
+    0x0201: (0x110, 1, 589825),  # WM_LBUTTONDOWN --> BTN_LEFT
+    0x0202: (0x110, 0, 589825),  # WM_LBUTTONUP   --> BTN_LEFT
+    0x0204: (0x111, 1, 589826),  # WM_RBUTTONDOWN --> BTN_RIGHT
+    0x0205: (0x111, 0, 589826),  # WM_RBUTTONUP   --> BTN_RIGHT
+    0x0207: (0x112, 1, 589827),  # WM_MBUTTONDOWN --> BTN_MIDDLE
+    0x0208: (0x112, 0, 589827),  # WM_MBUTTONU    --> BTN_MIDDLE
+    0x020B: (0x113, 1, 589828),  # WM_XBUTTONDOWN --> BTN_SIDE
+    0x020C: (0x113, 0, 589828),  # WM_XBUTTONUP   --> BTN_SIDE
+    0x020B2: (0x114, 1, 589829),  # WM_XBUTTONDOWN --> BTN_EXTRA
+    0x020C2: (0x114, 0, 589829),  # WM_XBUTTONUP   --> BTN_EXTRA
+}
+
+# THING SING That thing can sing!
+# SONG LONG A long, long song.
+# Good-bye, Thing. You sing too long.
+# pylint: disable=too-many-lines
+
+WINCODES = (
+    (0x01, 0x110),  # Left mouse button
+    (0x02, 0x111),  # Right mouse button
+    (0x03, 0),  # Control-break processing
+    (0x04, 0x112),  # Middle mouse button (three-button mouse)
+    (0x05, 0x113),  # X1 mouse button
+    (0x06, 0x114),  # X2 mouse button
+    (0x07, 0),  # Undefined
+    (0x08, 14),  # BACKSPACE key
+    (0x09, 15),  # TAB key
+    (0x0A, 0),  # Reserved
+    (0x0B, 0),  # Reserved
+    (0x0C, 0x163),  # CLEAR key
+    (0x0D, 28),  # ENTER key
+    (0x0E, 0),  # Undefined
+    (0x0F, 0),  # Undefined
+    (0x10, 42),  # SHIFT key
+    (0x11, 29),  # CTRL key
+    (0x12, 56),  # ALT key
+    (0x13, 119),  # PAUSE key
+    (0x14, 58),  # CAPS LOCK key
+    (0x15, 90),  # IME Kana mode
+    (0x15, 91),  # IME Hanguel mode (maintained for compatibility; use
+    # VK_HANGUL)
+    (0x15, 91),  # IME Hangul mode
+    (0x16, 0),  # Undefined
+    (0x17, 92),  # IME Junja mode - These all need to be fixed
+    (0x18, 93),  # IME final mode - By someone who
+    (0x19, 94),  # IME Hanja mode - Knows how
+    (0x19, 95),  # IME Kanji mode - Japanese Keyboards work
+    (0x1A, 0),  # Undefined
+    (0x1B, 1),  # ESC key
+    (0x1C, 0),  # IME convert
+    (0x1D, 0),  # IME nonconvert
+    (0x1E, 0),  # IME accept
+    (0x1F, 0),  # IME mode change request
+    (0x20, 57),  # SPACEBAR
+    (0x21, 104),  # PAGE UP key
+    (0x22, 109),  # PAGE DOWN key
+    (0x23, 107),  # END key
+    (0x24, 102),  # HOME key
+    (0x25, 105),  # LEFT ARROW key
+    (0x26, 103),  # UP ARROW key
+    (0x27, 106),  # RIGHT ARROW key
+    (0x28, 108),  # DOWN ARROW key
+    (0x29, 0x161),  # SELECT key
+    (0x2A, 210),  # PRINT key
+    (0x2B, 28),  # EXECUTE key
+    (0x2C, 99),  # PRINT SCREEN key
+    (0x2D, 110),  # INS key
+    (0x2E, 111),  # DEL key
+    (0x2F, 138),  # HELP key
+    (0x30, 11),  # 0 key
+    (0x31, 2),  # 1 key
+    (0x32, 3),  # 2 key
+    (0x33, 4),  # 3 key
+    (0x34, 5),  # 4 key
+    (0x35, 6),  # 5 key
+    (0x36, 7),  # 6 key
+    (0x37, 8),  # 7 key
+    (0x38, 9),  # 8 key
+    (0x39, 10),  # 9 key
+    #  (0x3A-40, 0),  # Undefined
+    (0x41, 30),  # A key
+    (0x42, 48),  # B key
+    (0x43, 46),  # C key
+    (0x44, 32),  # D key
+    (0x45, 18),  # E key
+    (0x46, 33),  # F key
+    (0x47, 34),  # G key
+    (0x48, 35),  # H key
+    (0x49, 23),  # I key
+    (0x4A, 36),  # J key
+    (0x4B, 37),  # K key
+    (0x4C, 38),  # L key
+    (0x4D, 50),  # M key
+    (0x4E, 49),  # N key
+    (0x4F, 24),  # O key
+    (0x50, 25),  # P key
+    (0x51, 16),  # Q key
+    (0x52, 19),  # R key
+    (0x53, 31),  # S key
+    (0x54, 20),  # T key
+    (0x55, 22),  # U key
+    (0x56, 47),  # V key
+    (0x57, 17),  # W key
+    (0x58, 45),  # X key
+    (0x59, 21),  # Y key
+    (0x5A, 44),  # Z key
+    (0x5B, 125),  # Left Windows key (Natural keyboard)
+    (0x5C, 126),  # Right Windows key (Natural keyboard)
+    (0x5D, 139),  # Applications key (Natural keyboard)
+    (0x5E, 0),  # Reserved
+    (0x5F, 142),  # Computer Sleep key
+    (0x60, 82),  # Numeric keypad 0 key
+    (0x61, 79),  # Numeric keypad 1 key
+    (0x62, 80),  # Numeric keypad 2 key
+    (0x63, 81),  # Numeric keypad 3 key
+    (0x64, 75),  # Numeric keypad 4 key
+    (0x65, 76),  # Numeric keypad 5 key
+    (0x66, 77),  # Numeric keypad 6 key
+    (0x67, 71),  # Numeric keypad 7 key
+    (0x68, 72),  # Numeric keypad 8 key
+    (0x69, 73),  # Numeric keypad 9 key
+    (0x6A, 55),  # Multiply key
+    (0x6B, 78),  # Add key
+    (0x6C, 96),  # Separator key
+    (0x6D, 74),  # Subtract key
+    (0x6E, 83),  # Decimal key
+    (0x6F, 98),  # Divide key
+    (0x70, 59),  # F1 key
+    (0x71, 60),  # F2 key
+    (0x72, 61),  # F3 key
+    (0x73, 62),  # F4 key
+    (0x74, 63),  # F5 key
+    (0x75, 64),  # F6 key
+    (0x76, 65),  # F7 key
+    (0x77, 66),  # F8 key
+    (0x78, 67),  # F9 key
+    (0x79, 68),  # F10 key
+    (0x7A, 87),  # F11 key
+    (0x7B, 88),  # F12 key
+    (0x7C, 183),  # F13 key
+    (0x7D, 184),  # F14 key
+    (0x7E, 185),  # F15 key
+    (0x7F, 186),  # F16 key
+    (0x80, 187),  # F17 key
+    (0x81, 188),  # F18 key
+    (0x82, 189),  # F19 key
+    (0x83, 190),  # F20 key
+    (0x84, 191),  # F21 key
+    (0x85, 192),  # F22 key
+    (0x86, 192),  # F23 key
+    (0x87, 194),  # F24 key
+    #  (0x88-8F, 0),  # Unassigned
+    (0x90, 69),  # NUM LOCK key
+    (0x91, 70),  # SCROLL LOCK key
+    #  (0x92-96, 0),  # OEM specific
+    #  (0x97-9F, 0),  # Unassigned
+    (0xA0, 42),  # Left SHIFT key
+    (0xA1, 54),  # Right SHIFT key
+    (0xA2, 29),  # Left CONTROL key
+    (0xA3, 97),  # Right CONTROL key
+    (0xA4, 125),  # Left MENU key
+    (0xA5, 126),  # Right MENU key
+    (0xA6, 158),  # Browser Back key
+    (0xA7, 159),  # Browser Forward key
+    (0xA8, 173),  # Browser Refresh key
+    (0xA9, 128),  # Browser Stop key
+    (0xAA, 217),  # Browser Search key
+    (0xAB, 0x16C),  # Browser Favorites key
+    (0xAC, 150),  # Browser Start and Home key
+    (0xAD, 113),  # Volume Mute key
+    (0xAE, 114),  # Volume Down key
+    (0xAF, 115),  # Volume Up key
+    (0xB0, 163),  # Next Track key
+    (0xB1, 165),  # Previous Track key
+    (0xB2, 166),  # Stop Media key
+    (0xB3, 164),  # Play/Pause Media key
+    (0xB4, 155),  # Start Mail key
+    (0xB5, 0x161),  # Select Media key
+    (0xB6, 148),  # Start Application 1 key
+    (0xB7, 149),  # Start Application 2 key
+    #  (0xB8-B9, 0),  # Reserved
+    (0xBA, 39),  # Used for miscellaneous characters; it can vary by keyboard.
+    (0xBB, 13),  # For any country/region, the '+' key
+    (0xBC, 51),  # For any country/region, the ',' key
+    (0xBD, 12),  # For any country/region, the '-' key
+    (0xBE, 52),  # For any country/region, the '.' key
+    (0xBF, 53),  # Slash
+    (0xC0, 40),  # Apostrophe
+    #  (0xC1-D7, 0),  # Reserved
+    #  (0xD8-DA, 0),  # Unassigned
+    (0xDB, 26),  # [
+    (0xDC, 86),  # \
+    (0xDD, 27),  # ]
+    (0xDE, 43),  # '
+    (0xDF, 119),  # VK_OFF - What's that?
+    (0xE0, 0),  # Reserved
+    (0xE1, 0),  # OEM Specific
+    (0xE2, 43),  # Either the angle bracket key or the backslash key
+    # on the RT 102-key keyboard (0xE3-E4, 0), # OEM
+    # specific
+    (0xE5, 0),  # IME PROCESS key
+    (0xE6, 0),  # OEM specific
+    (0xE7, 0),  # Used to pass Unicode characters as if they were
+    # keystrokes. The VK_PACKET key is the low word of a
+    # 32-bit Virtual Key value used for non-keyboard input
+    # methods. For more information, see Remark in
+    # KEYBDINPUT, SendInput, WM_KEYDOWN, and WM_KEYUP
+    (0xE8, 0),  # Unassigned
+    #  (0xE9-F5, 0),  # OEM specific
+    (0xF6, 0),  # Attn key
+    (0xF7, 0),  # CrSel key
+    (0xF8, 0),  # ExSel key
+    (0xF9, 222),  # Erase EOF key
+    (0xFA, 207),  # Play key
+    (0xFB, 0x174),  # Zoom key
+    (0xFC, 0),  # Reserved
+    (0xFD, 0x19B),  # PA1 key
+    (0xFE, 0x163),  # Clear key
+    (0xFF, 185),
+)
+
+MAC_EVENT_CODES = (
+    # NSLeftMouseDown Quartz.kCGEventLeftMouseDown
+    (1, ("Key", 0x110, 1, 589825)),
+    # NSLeftMouseUp Quartz.kCGEventLeftMouseUp
+    (2, ("Key", 0x110, 0, 589825)),
+    # NSRightMouseDown Quartz.kCGEventRightMouseDown
+    (3, ("Key", 0x111, 1, 589826)),
+    # NSRightMouseUp Quartz.kCGEventRightMouseUp
+    (4, ("Key", 0x111, 0, 589826)),
+    (5, (None, 0, 0, 0)),  # NSMouseMoved Quartz.kCGEventMouseMoved
+    (6, (None, 0, 0, 0)),  # NSLeftMouseDragged Quartz.kCGEventLeftMouseDragged
+    # NSRightMouseDragged Quartz.kCGEventRightMouseDragged
+    (7, (None, 0, 0, 0)),
+    (8, (None, 0, 0, 0)),  # NSMouseEntered
+    (9, (None, 0, 0, 0)),  # NSMouseExited
+    (10, (None, 0, 0, 0)),  # NSKeyDown
+    (11, (None, 0, 0, 0)),  # NSKeyUp
+    (12, (None, 0, 0, 0)),  # NSFlagsChanged
+    (13, (None, 0, 0, 0)),  # NSAppKitDefined
+    (14, (None, 0, 0, 0)),  # NSSystemDefined
+    (15, (None, 0, 0, 0)),  # NSApplicationDefined
+    (16, (None, 0, 0, 0)),  # NSPeriodic
+    (17, (None, 0, 0, 0)),  # NSCursorUpdate
+    (22, (None, 0, 0, 0)),  # NSScrollWheel Quartz.kCGEventScrollWheel
+    (23, (None, 0, 0, 0)),  # NSTabletPoint Quartz.kCGEventTabletPointer
+    (24, (None, 0, 0, 0)),  # NSTabletProximity Quartz.kCGEventTabletProximity
+    (25, (None, 0, 0, 0)),  # NSOtherMouseDown Quartz.kCGEventOtherMouseDown
+    (25.2, ("Key", 0x112, 1, 589827)),  # BTN_MIDDLE
+    (25.3, ("Key", 0x113, 1, 589828)),  # BTN_SIDE
+    (25.4, ("Key", 0x114, 1, 589829)),  # BTN_EXTRA
+    (26, (None, 0, 0, 0)),  # NSOtherMouseUp Quartz.kCGEventOtherMouseUp
+    (26.2, ("Key", 0x112, 0, 589827)),  # BTN_MIDDLE
+    (26.3, ("Key", 0x113, 0, 589828)),  # BTN_SIDE
+    (26.4, ("Key", 0x114, 0, 589829)),  # BTN_EXTRA
+    (27, (None, 0, 0, 0)),  # NSOtherMouseDragged
+    (29, (None, 0, 0, 0)),  # NSEventTypeGesture
+    (30, (None, 0, 0, 0)),  # NSEventTypeMagnify
+    (31, (None, 0, 0, 0)),  # NSEventTypeSwipe
+    (18, (None, 0, 0, 0)),  # NSEventTypeRotate
+    (19, (None, 0, 0, 0)),  # NSEventTypeBeginGesture
+    (20, (None, 0, 0, 0)),  # NSEventTypeEndGesture
+    (27, (None, 0, 0, 0)),  # Quartz.kCGEventOtherMouseDragged
+    (32, (None, 0, 0, 0)),  # NSEventTypeSmartMagnify
+    (33, (None, 0, 0, 0)),  # NSEventTypeQuickLook
+    (34, (None, 0, 0, 0)),  # NSEventTypePressure
+)
+
+MAC_KEYS = (
+    (0x00, 30),  # kVK_ANSI_A
+    (0x01, 31),  # kVK_ANSI_S    (0x02, 32),  # kVK_ANSI_D
+    (0x03, 33),  # kVK_ANSI_F
+    (0x04, 35),  # kVK_ANSI_H
+    (0x05, 34),  # kVK_ANSI_G
+    (0x06, 44),  # kVK_ANSI_Z
+    (0x07, 45),  # kVK_ANSI_X
+    (0x08, 46),  # kVK_ANSI_C
+    (0x09, 47),  # kVK_ANSI_V
+    (0x0B, 48),  # kVK_ANSI_B
+    (0x0C, 16),  # kVK_ANSI_Q
+    (0x0D, 17),  # kVK_ANSI_W
+    (0x0E, 18),  # kVK_ANSI_E
+    (0x0F, 33),  # kVK_ANSI_R
+    (0x10, 21),  # kVK_ANSI_Y
+    (0x11, 20),  # kVK_ANSI_T
+    (0x12, 2),  # kVK_ANSI_1
+    (0x13, 3),  # kVK_ANSI_2
+    (0x14, 4),  # kVK_ANSI_3
+    (0x15, 5),  # kVK_ANSI_4
+    (0x16, 7),  # kVK_ANSI_6
+    (0x17, 6),  # kVK_ANSI_5
+    (0x18, 13),  # kVK_ANSI_Equal
+    (0x19, 10),  # kVK_ANSI_9
+    (0x1A, 8),  # kVK_ANSI_7
+    (0x1B, 12),  # kVK_ANSI_Minus
+    (0x1C, 9),  # kVK_ANSI_8
+    (0x1D, 11),  # kVK_ANSI_0
+    (0x1E, 27),  # kVK_ANSI_RightBracket
+    (0x1F, 24),  # kVK_ANSI_O
+    (0x20, 22),  # kVK_ANSI_U
+    (0x21, 26),  # kVK_ANSI_LeftBracket
+    (0x22, 23),  # kVK_ANSI_I
+    (0x23, 25),  # kVK_ANSI_P
+    (0x25, 38),  # kVK_ANSI_L
+    (0x26, 36),  # kVK_ANSI_J
+    (0x27, 40),  # kVK_ANSI_Quote
+    (0x28, 37),  # kVK_ANSI_K
+    (0x29, 39),  # kVK_ANSI_Semicolon
+    (0x2A, 43),  # kVK_ANSI_Backslash
+    (0x2B, 51),  # kVK_ANSI_Comma
+    (0x2C, 53),  # kVK_ANSI_Slash
+    (0x2D, 49),  # kVK_ANSI_N
+    (0x2E, 50),  # kVK_ANSI_M
+    (0x2F, 52),  # kVK_ANSI_Period
+    (0x32, 41),  # kVK_ANSI_Grave
+    (0x41, 83),  # kVK_ANSI_KeypadDecimal
+    (0x43, 55),  # kVK_ANSI_KeypadMultiply
+    (0x45, 78),  # kVK_ANSI_KeypadPlus
+    (0x47, 69),  # kVK_ANSI_KeypadClear
+    (0x4B, 98),  # kVK_ANSI_KeypadDivide
+    (0x4C, 96),  # kVK_ANSI_KeypadEnter
+    (0x4E, 74),  # kVK_ANSI_KeypadMinus
+    (0x51, 117),  # kVK_ANSI_KeypadEquals
+    (0x52, 82),  # kVK_ANSI_Keypad0
+    (0x53, 79),  # kVK_ANSI_Keypad1
+    (0x54, 80),  # kVK_ANSI_Keypad2
+    (0x55, 81),  # kVK_ANSI_Keypad3
+    (0x56, 75),  # kVK_ANSI_Keypad4
+    (0x57, 76),  # kVK_ANSI_Keypad5
+    (0x58, 77),  # kVK_ANSI_Keypad6
+    (0x59, 71),  # kVK_ANSI_Keypad7
+    (0x5B, 72),  # kVK_ANSI_Keypad8
+    (0x5C, 73),  # kVK_ANSI_Keypad9
+    (0x24, 28),  # kVK_Return
+    (0x30, 15),  # kVK_Tab
+    (0x31, 57),  # kVK_Space
+    (0x33, 111),  # kVK_Delete
+    (0x35, 1),  # kVK_Escape
+    (0x37, 125),  # kVK_Command
+    (0x38, 42),  # kVK_Shift
+    (0x39, 58),  # kVK_CapsLock
+    (0x3A, 56),  # kVK_Option
+    (0x3B, 29),  # kVK_Control
+    (0x3C, 54),  # kVK_RightShift
+    (0x3D, 100),  # kVK_RightOption
+    (0x3E, 126),  # kVK_RightControl
+    (0x36, 126),  # Right Meta
+    (0x3F, 0x1D0),  # kVK_Function
+    (0x40, 187),  # kVK_F17
+    (0x48, 115),  # kVK_VolumeUp
+    (0x49, 114),  # kVK_VolumeDown
+    (0x4A, 113),  # kVK_Mute
+    (0x4F, 188),  # kVK_F18
+    (0x50, 189),  # kVK_F19
+    (0x5A, 190),  # kVK_F20
+    (0x60, 63),  # kVK_F5
+    (0x61, 64),  # kVK_F6
+    (0x62, 65),  # kVK_F7
+    (0x63, 61),  # kVK_F3
+    (0x64, 66),  # kVK_F8
+    (0x65, 67),  # kVK_F9
+    (0x67, 87),  # kVK_F11
+    (0x69, 183),  # kVK_F13
+    (0x6A, 186),  # kVK_F16
+    (0x6B, 184),  # kVK_F14
+    (0x6D, 68),  # kVK_F10
+    (0x6F, 88),  # kVK_F12
+    (0x71, 185),  # kVK_F15
+    (0x72, 138),  # kVK_Help
+    (0x73, 102),  # kVK_Home
+    (0x74, 104),  # kVK_PageUp
+    (0x75, 111),  # kVK_ForwardDelete
+    (0x76, 62),  # kVK_F4
+    (0x77, 107),  # kVK_End
+    (0x78, 60),  # kVK_F2
+    (0x79, 109),  # kVK_PageDown
+    (0x7A, 59),  # kVK_F1
+    (0x7B, 105),  # kVK_LeftArrow
+    (0x7C, 106),  # kVK_RightArrow
+    (0x7D, 108),  # kVK_DownArrow
+    (0x7E, 103),  # kVK_UpArrow
+    (0x0A, 170),  # kVK_ISO_Section
+    (0x5D, 124),  # kVK_JIS_Yen
+    (0x5E, 92),  # kVK_JIS_Underscore
+    (0x5F, 95),  # kVK_JIS_KeypadComma
+    (0x66, 94),  # kVK_JIS_Eisu
+    (0x68, 90),  # kVK_JIS_Kana
+)
+
+
+# We have yet to support force feedback but probably should
+# eventually:
+
+FORCE_FEEDBACK = ()  # Motor in gamepad
+FORCE_FEEDBACK_STATUS = ()  # Status of motor
+
+POWER = ()  # Power switch
+
+# These two are internal workings of evdev we probably will never care
+# about.
+
+MAX = ()
+CURRENT = ()
+
+
+EVENT_MAP = (
+    ('types', EVENT_TYPES),
+    ('type_codes', ((value, key) for key, value in EVENT_TYPES)),
+    ('wincodes', WINCODES),
+    ('specials', SPECIAL_DEVICES),
+    ('xpad', XINPUT_MAPPING),
+    ('Sync', SYNCHRONIZATION_EVENTS),
+    ('Key', KEYS_AND_BUTTONS),
+    ('Relative', RELATIVE_AXES),
+    ('Absolute', ABSOLUTE_AXES),
+    ('Misc', MISC_EVENTS),
+    ('Switch', SWITCH_EVENTS),
+    ('LED', LEDS),
+    ('LED_type_codes', LED_TYPE_CODES),
+    ('Sound', SOUNDS),
+    ('Repeat', AUTOREPEAT_VALUES),
+    ('ForceFeedback', FORCE_FEEDBACK),
+    ('Power', POWER),
+    ('ForceFeedbackStatus', FORCE_FEEDBACK_STATUS),
+    ('Max', MAX),
+    ('Current', CURRENT),
+)
+
+# Evdev style paths for the Mac
+
+APPKIT_KB_PATH = "/dev/input/by-id/usb-AppKit_Keyboard-event-kbd"
+QUARTZ_MOUSE_PATH = "/dev/input/by-id/usb-Quartz_Mouse-event-mouse"
+APPKIT_MOUSE_PATH = "/dev/input/by-id/usb-AppKit_Mouse-event-mouse"
+
+
+# Now comes all the structs we need to parse the infomation coming
+# from Windows.
+
+
+class KBDLLHookStruct(ctypes.Structure):
+    """Contains information about a low-level keyboard input event.
+
+    For full details see Microsoft's documentation:
+
+    https://msdn.microsoft.com/en-us/library/windows/desktop/
+    ms644967%28v=vs.85%29.aspx
+    """
+
+    # pylint: disable=too-few-public-methods
+    _fields_ = [
+        ("vk_code", DWORD),
+        ("scan_code", DWORD),
+        ("flags", DWORD),
+        ("time", ctypes.c_int),
+    ]
+
+
+class MSLLHookStruct(ctypes.Structure):
+    """Contains information about a low-level mouse input event.
+
+    For full details see Microsoft's documentation:
+
+    https://msdn.microsoft.com/en-us/library/windows/desktop/
+    ms644970%28v=vs.85%29.aspx
+    """
+
+    # pylint: disable=too-few-public-methods
+    _fields_ = [
+        ("x_pos", ctypes.c_long),
+        ("y_pos", ctypes.c_long),
+        ('reserved', ctypes.c_short),
+        ('mousedata', ctypes.c_short),
+        ("flags", DWORD),
+        ("time", DWORD),
+        ("extrainfo", ctypes.c_ulong),
+    ]
+
+
+class XinputGamepad(ctypes.Structure):
+    """Describes the current state of the Xbox 360 Controller.
+
+    For full details see Microsoft's documentation:
+
+    https://msdn.microsoft.com/en-us/library/windows/desktop/
+    microsoft.directx_sdk.reference.xinput_gamepad%28v=vs.85%29.aspx
+
+    """
+
+    # pylint: disable=too-few-public-methods
+    _fields_ = [
+        ('buttons', ctypes.c_ushort),  # wButtons
+        ('left_trigger', ctypes.c_ubyte),  # bLeftTrigger
+        ('right_trigger', ctypes.c_ubyte),  # bLeftTrigger
+        ('l_thumb_x', ctypes.c_short),  # sThumbLX
+        ('l_thumb_y', ctypes.c_short),  # sThumbLY
+        ('r_thumb_x', ctypes.c_short),  # sThumbRx
+        ('r_thumb_y', ctypes.c_short),  # sThumbRy
+    ]
+
+
+class XinputState(ctypes.Structure):
+    """Represents the state of a controller.
+
+    For full details see Microsoft's documentation:
+
+    https://msdn.microsoft.com/en-us/library/windows/desktop/
+    microsoft.directx_sdk.reference.xinput_state%28v=vs.85%29.aspx
+
+    """
+
+    # pylint: disable=too-few-public-methods
+    _fields_ = [
+        ('packet_number', ctypes.c_ulong),  # dwPacketNumber
+        ('gamepad', XinputGamepad),  # Gamepad
+    ]
+
+
+class XinputVibration(ctypes.Structure):
+    """Specifies motor speed levels for the vibration function of a
+    controller.
+
+    For full details see Microsoft's documentation:
+
+    https://msdn.microsoft.com/en-us/library/windows/desktop/
+    microsoft.directx_sdk.reference.xinput_vibration%28v=vs.85%29.aspx
+
+    """
+
+    # pylint: disable=too-few-public-methods
+    _fields_ = [
+        ("wLeftMotorSpeed", ctypes.c_ushort),
+        ("wRightMotorSpeed", ctypes.c_ushort),
+    ]
+
+
+if sys.version_info.major == 2:
+    # pylint: disable=redefined-builtin
+    class PermissionError(IOError):
+        """Raised when trying to run an operation without the adequate access
+        rights - for example filesystem permissions. Corresponds to errno
+        EACCES and EPERM."""
+
+
+class UnpluggedError(RuntimeError):
+    """The device requested is not plugged in."""
+
+    pass
+
+
+class NoDevicePath(RuntimeError):
+    """No evdev device path was given."""
+
+    pass
+
+
+class UnknownEventType(IndexError):
+    """We don't know what this event is."""
+
+    pass
+
+
+class UnknownEventCode(IndexError):
+    """We don't know what this event is."""
+
+    pass
+
+
+class InputEvent:
+    """A user event."""
+
+    # pylint: disable=too-few-public-methods
+    def __init__(self, device, event_info):
+        self.device = device
+        self.timestamp = event_info["timestamp"]
+        self.code = event_info["code"]
+        self.state = event_info["state"]
+        self.ev_type = event_info["ev_type"]
+
+
+class BaseListener:
+    """Loosely emulate Evdev keyboard behaviour on other platforms.
+    Listen (hook in Windows terminology) for key events then buffer
+    them in a pipe.
+    """
+
+    def __init__(self, pipe, events=None, codes=None):
+        self.pipe = pipe
+        self.events = events if events else []
+        self.codes = codes if codes else None
+        self.app = None
+        self.timeval = None
+        self.type_codes = dict(EVENT_TYPES)
+
+        self.install_handle_input()
+
+    def install_handle_input(self):
+        """Install the input handler."""
+        pass
+
+    def uninstall_handle_input(self):
+        """Un-install the input handler."""
+        pass
+
+    def __del__(self):
+        """Clean up when deleted."""
+        self.uninstall_handle_input()
+
+    @staticmethod
+    def get_timeval():
+        """Get the time in seconds and microseconds."""
+        return convert_timeval(time.time())
+
+    def update_timeval(self):
+        """Update the timeval with the current time."""
+        self.timeval = self.get_timeval()
+
+    def create_event_object(self, event_type, code, value, timeval=None):
+        """Create an evdev style structure."""
+        if not timeval:
+            self.update_timeval()
+            timeval = self.timeval
+        try:
+            event_code = self.type_codes[event_type]
+        except KeyError:
+            raise UnknownEventType(
+                "We don't know what kind of event a %s is." % event_type
+            )
+
+        event = struct.pack(
+            EVENT_FORMAT, timeval[0], timeval[1], event_code, code, value
+        )
+        return event
+
+    def write_to_pipe(self, event_list):
+        """Send event back to the mouse object."""
+        self.pipe.send_bytes(b''.join(event_list))
+
+    def emulate_wheel(self, data, direction, timeval):
+        """Emulate rel values for the mouse wheel.
+
+        In evdev, a single click forwards of the mouse wheel is 1 and
+        a click back is -1. Windows uses 120 and -120. We floor divide
+        the Windows number by 120. This is fine for the digital scroll
+        wheels found on the vast majority of mice. It also works on
+        the analogue ball on the top of the Apple mouse.
+
+        What do the analogue scroll wheels found on 200 quid high end
+        gaming mice do? If the lowest unit is 120 then we are okay. If
+        they report changes of less than 120 units Windows, then this
+        might be an unacceptable loss of precision. Needless to say, I
+        don't have such a mouse to test one way or the other.
+
+        """
+        if direction == 'x':
+            code = 0x06
+        elif direction == 'z':
+            # Not enitely sure if this exists
+            code = 0x07
+        else:
+            code = 0x08
+
+        if WIN:
+            data = data // 120
+
+        return self.create_event_object("Relative", code, data, timeval)
+
+    def emulate_rel(self, key_code, value, timeval):
+        """Emulate the relative changes of the mouse cursor."""
+        return self.create_event_object("Relative", key_code, value, timeval)
+
+    def emulate_press(self, key_code, scan_code, value, timeval):
+        """Emulate a button press.
+
+        Currently supports 5 buttons.
+
+        The Microsoft documentation does not define what happens with
+        a mouse with more than five buttons, and I don't have such a
+        mouse.
+
+        From reading the Linux sources, I guess evdev can support up
+        to 255 buttons.
+
+        Therefore, I guess we could support more buttons quite easily,
+        if we had any useful hardware.
+        """
+        scan_event = self.create_event_object("Misc", 0x04, scan_code, timeval)
+        key_event = self.create_event_object("Key", key_code, value, timeval)
+        return scan_event, key_event
+
+    def emulate_repeat(self, value, timeval):
+        """The repeat press of a key/mouse button, e.g. double click."""
+        repeat_event = self.create_event_object("Repeat", 2, value, timeval)
+        return repeat_event
+
+    def sync_marker(self, timeval):
+        """Separate groups of events."""
+        return self.create_event_object("Sync", 0, 0, timeval)
+
+    def emulate_abs(self, x_val, y_val, timeval):
+        """Emulate the absolute co-ordinates of the mouse cursor."""
+        x_event = self.create_event_object("Absolute", 0x00, x_val, timeval)
+        y_event = self.create_event_object("Absolute", 0x01, y_val, timeval)
+        return x_event, y_event
+
+
+class WindowsKeyboardListener(BaseListener):
+    """Loosely emulate Evdev keyboard behaviour on Windows.  Listen (hook
+    in Windows terminology) for key events then buffer them in a pipe.
+    """
+
+    def __init__(self, pipe, codes=None):
+        self.pipe = pipe
+        self.hooked = None
+        self.pointer = None
+        super(WindowsKeyboardListener, self).__init__(pipe, codes)
+
+    @staticmethod
+    def listen():
+        """Listen for keyboard input."""
+        msg = MSG()
+        ctypes.windll.user32.GetMessageA(ctypes.byref(msg), 0, 0, 0)
+
+    def get_fptr(self):
+        """Get the function pointer."""
+        cmpfunc = ctypes.CFUNCTYPE(
+            ctypes.c_int, WPARAM, LPARAM, ctypes.POINTER(KBDLLHookStruct)
+        )
+        return cmpfunc(self.handle_input)
+
+    def install_handle_input(self):
+        """Install the hook."""
+        self.pointer = self.get_fptr()
+
+        self.hooked = ctypes.windll.user32.SetWindowsHookExA(
+            13, self.pointer, ctypes.windll.kernel32.GetModuleHandleW(None), 0
+        )
+        if not self.hooked:
+            return False
+        return True
+
+    def uninstall_handle_input(self):
+        """Remove the hook."""
+        if self.hooked is None:
+            return
+        ctypes.windll.user32.UnhookWindowsHookEx(self.hooked)
+        self.hooked = None
+
+    def handle_input(self, ncode, wparam, lparam):
+        """Process the key input."""
+        value = WIN_KEYBOARD_CODES[wparam]
+        scan_code = lparam.contents.scan_code
+        vk_code = lparam.contents.vk_code
+        self.update_timeval()
+
+        events = []
+        # Add key event
+        scan_key, key_event = self.emulate_press(
+            vk_code, scan_code, value, self.timeval
+        )
+        events.append(scan_key)
+        events.append(key_event)
+
+        # End with a sync marker
+        events.append(self.sync_marker(self.timeval))
+
+        # We are done
+        self.write_to_pipe(events)
+
+        return ctypes.windll.user32.CallNextHookEx(self.hooked, ncode, wparam, lparam)
+
+
+def keyboard_process(pipe):
+    """Single subprocess for reading keyboard events on Windows."""
+    keyboard = WindowsKeyboardListener(pipe)
+    keyboard.listen()
+
+
+class WindowsMouseListener(BaseListener):
+    """Loosely emulate Evdev mouse behaviour on Windows.  Listen (hook
+    in Windows terminology) for key events then buffer them in a pipe.
+    """
+
+    def __init__(self, pipe):
+        self.pipe = pipe
+        self.hooked = None
+        self.pointer = None
+        self.mouse_codes = WIN_MOUSE_CODES
+        super(WindowsMouseListener, self).__init__(pipe)
+
+    @staticmethod
+    def listen():
+        """Listen for mouse input."""
+        msg = MSG()
+        ctypes.windll.user32.GetMessageA(ctypes.byref(msg), 0, 0, 0)
+
+    def get_fptr(self):
+        """Get the function pointer."""
+        cmpfunc = ctypes.CFUNCTYPE(
+            ctypes.c_int, WPARAM, LPARAM, ctypes.POINTER(MSLLHookStruct)
+        )
+        return cmpfunc(self.handle_input)
+
+    def install_handle_input(self):
+        """Install the hook."""
+        self.pointer = self.get_fptr()
+
+        self.hooked = ctypes.windll.user32.SetWindowsHookExA(
+            14, self.pointer, ctypes.windll.kernel32.GetModuleHandleW(None), 0
+        )
+        if not self.hooked:
+            return False
+        return True
+
+    def uninstall_handle_input(self):
+        """Remove the hook."""
+        if self.hooked is None:
+            return
+        ctypes.windll.user32.UnhookWindowsHookEx(self.hooked)
+        self.hooked = None
+
+    def handle_input(self, ncode, wparam, lparam):
+        """Process the key input."""
+        x_pos = lparam.contents.x_pos
+        y_pos = lparam.contents.y_pos
+        data = lparam.contents.mousedata
+
+        # This is how we can distinguish mouse 1 from mouse 2
+        # extrainfo = lparam.contents.extrainfo
+        # The way windows seems to do it is there is primary mouse
+        # and all other mouses report as mouse 2
+
+        # Also useful later will be to support the flags field
+        # flags = lparam.contents.flags
+        # This shows if the event was from a real device or whether it
+        # was injected somehow via software
+
+        self.emulate_mouse(wparam, x_pos, y_pos, data)
+
+        # Give back control to Windows to wait for and process the
+        # next event
+        return ctypes.windll.user32.CallNextHookEx(self.hooked, ncode, wparam, lparam)
+
+    def emulate_mouse(self, key_code, x_val, y_val, data):
+        """Emulate the ev codes using the data Windows has given us.
+
+        Note that by default in Windows, to recognise a double click,
+        you just notice two clicks in a row within a reasonablely
+        short time period.
+
+        However, if the application developer sets the application
+        window's class style to CS_DBLCLKS, the operating system will
+        notice the four button events (down, up, down, up), intercept
+        them and then send a single key code instead.
+
+        There are no such special double click codes on other
+        platforms, so not obvious what to do with them. It might be
+        best to just convert them back to four events.
+
+        Currently we do nothing.
+
+        ((0x0203, 'WM_LBUTTONDBLCLK'),
+         (0x0206, 'WM_RBUTTONDBLCLK'),
+         (0x0209, 'WM_MBUTTONDBLCLK'),
+         (0x020D, 'WM_XBUTTONDBLCLK'))
+
+        """
+        # Once again ignore Windows' relative time (since system
+        # startup) and use the absolute time (since epoch i.e. 1st Jan
+        # 1970).
+        self.update_timeval()
+
+        events = []
+
+        if key_code == 0x0200:
+            # We have a mouse move alone.
+            # So just pass through to below
+            pass
+        elif key_code == 0x020A:
+            # We have a vertical mouse wheel turn
+            events.append(self.emulate_wheel(data, 'y', self.timeval))
+        elif key_code == 0x020E:
+            # We have a horizontal mouse wheel turn
+            # https://msdn.microsoft.com/en-us/library/windows/desktop/
+            # ms645614%28v=vs.85%29.aspx
+            events.append(self.emulate_wheel(data, 'x', self.timeval))
+        else:
+            # We have a button press.
+
+            # Distinguish the second extra button
+            if key_code == 0x020B and data == 2:
+                key_code = 0x020B2
+            elif key_code == 0x020C and data == 2:
+                key_code = 0x020C2
+
+            # Get the mouse codes
+            code, value, scan_code = self.mouse_codes[key_code]
+            # Add in the press events
+            scan_event, key_event = self.emulate_press(
+                code, scan_code, value, self.timeval
+            )
+            events.append(scan_event)
+            events.append(key_event)
+
+        # Add in the absolute position of the mouse cursor
+        x_event, y_event = self.emulate_abs(x_val, y_val, self.timeval)
+        events.append(x_event)
+        events.append(y_event)
+
+        # End with a sync marker
+        events.append(self.sync_marker(self.timeval))
+
+        # We are done
+        self.write_to_pipe(events)
+
+
+def mouse_process(pipe):
+    """Single subprocess for reading mouse events on Windows."""
+    mouse = WindowsMouseListener(pipe)
+    mouse.listen()
+
+
+class QuartzMouseBaseListener(BaseListener):
+    """Emulate evdev mouse behaviour on mac."""
+
+    def __init__(self, pipe):
+        super(QuartzMouseBaseListener, self).__init__(pipe, codes=dict(MAC_EVENT_CODES))
+        self.active = True
+        self.events = []
+
+    def _get_mouse_button_number(self, event):
+        """Get the mouse button number from an event."""
+        raise NotImplementedError
+
+    def _get_click_state(self, event):
+        """The click state from an event."""
+        raise NotImplementedError
+
+    def _get_scroll(self, event):
+        """The scroll values from an event."""
+        raise NotImplementedError
+
+    def _get_absolute(self, event):
+        """Get abolute cursor location."""
+        raise NotImplementedError
+
+    def _get_relative(self, event):
+        """Get the relative mouse movement."""
+        raise NotImplementedError
+
+    def handle_button(self, event, event_type):
+        """Convert the button information from quartz into evdev format."""
+        # 0 for left
+        # 1 for right
+        # 2 for middle/center
+        # 3 for side
+        mouse_button_number = self._get_mouse_button_number(event)
+
+        # Identify buttons 3,4,5
+        if event_type in (25, 26):
+            event_type = event_type + (mouse_button_number * 0.1)
+
+        # Add buttons to events
+        event_type_string, event_code, value, scan = self.codes[event_type]
+        if event_type_string == "Key":
+            scan_event, key_event = self.emulate_press(
+                event_code, scan, value, self.timeval
+            )
+            self.events.append(scan_event)
+            self.events.append(key_event)
+
+        # doubleclick/n-click of button
+        click_state = self._get_click_state(event)
+
+        repeat = self.emulate_repeat(click_state, self.timeval)
+        self.events.append(repeat)
+
+    def handle_scrollwheel(self, event):
+        """Handle the scrollwheel (it is a ball on the mighty mouse)."""
+        # relative Scrollwheel
+        scroll_x, scroll_y = self._get_scroll(event)
+
+        if scroll_x:
+            self.events.append(self.emulate_wheel(scroll_x, 'x', self.timeval))
+
+        if scroll_y:
+            self.events.append(self.emulate_wheel(scroll_y, 'y', self.timeval))
+
+    def handle_absolute(self, event):
+        """Absolute mouse position on the screen."""
+        (x_val, y_val) = self._get_absolute(event)
+        x_event, y_event = self.emulate_abs(int(x_val), int(y_val), self.timeval)
+        self.events.append(x_event)
+        self.events.append(y_event)
+
+    def handle_relative(self, event):
+        """Relative mouse movement."""
+        delta_x, delta_y = self._get_relative(event)
+        if delta_x:
+            self.events.append(self.emulate_rel(0x00, delta_x, self.timeval))
+        if delta_y:
+            self.events.append(self.emulate_rel(0x01, delta_y, self.timeval))
+
+    # pylint: disable=unused-argument
+    def handle_input(self, proxy, event_type, event, refcon):
+        """Handle an input event."""
+        self.update_timeval()
+        self.events = []
+
+        if event_type in (1, 2, 3, 4, 25, 26, 27):
+            self.handle_button(event, event_type)
+
+        if event_type == 22:
+            self.handle_scrollwheel(event)
+
+        # Add in the absolute position of the mouse cursor
+        self.handle_absolute(event)
+
+        # Add in the relative position of the mouse cursor
+        self.handle_relative(event)
+
+        # End with a sync marker
+        self.events.append(self.sync_marker(self.timeval))
+
+        # We are done
+        self.write_to_pipe(self.events)
+
+
+def quartz_mouse_process(pipe):
+    """Single subprocess for reading mouse events on Mac using newer Quartz."""
+    # Quartz only on the mac, so don't warn about Quartz
+    # pylint: disable=import-error
+    import Quartz
+
+    # pylint: disable=no-member
+
+    class QuartzMouseListener(QuartzMouseBaseListener):
+        """Loosely emulate Evdev mouse behaviour on the Macs.
+        Listen for key events then buffer them in a pipe.
+        """
+
+        def install_handle_input(self):
+            """Constants below listed at:
+            https://developer.apple.com/documentation/coregraphics/
+            cgeventtype?language=objc#topics
+            """
+            # Keep Mac Names to make it easy to find the documentation
+            # pylint: disable=invalid-name
+
+            NSMachPort = Quartz.CGEventTapCreate(
+                Quartz.kCGSessionEventTap,
+                Quartz.kCGHeadInsertEventTap,
+                Quartz.kCGEventTapOptionDefault,
+                Quartz.CGEventMaskBit(Quartz.kCGEventLeftMouseDown)
+                | Quartz.CGEventMaskBit(Quartz.kCGEventLeftMouseUp)
+                | Quartz.CGEventMaskBit(Quartz.kCGEventRightMouseDown)
+                | Quartz.CGEventMaskBit(Quartz.kCGEventRightMouseUp)
+                | Quartz.CGEventMaskBit(Quartz.kCGEventMouseMoved)
+                | Quartz.CGEventMaskBit(Quartz.kCGEventLeftMouseDragged)
+                | Quartz.CGEventMaskBit(Quartz.kCGEventRightMouseDragged)
+                | Quartz.CGEventMaskBit(Quartz.kCGEventScrollWheel)
+                | Quartz.CGEventMaskBit(Quartz.kCGEventTabletPointer)
+                | Quartz.CGEventMaskBit(Quartz.kCGEventTabletProximity)
+                | Quartz.CGEventMaskBit(Quartz.kCGEventOtherMouseDown)
+                | Quartz.CGEventMaskBit(Quartz.kCGEventOtherMouseUp)
+                | Quartz.CGEventMaskBit(Quartz.kCGEventOtherMouseDragged),
+                self.handle_input,
+                None,
+            )
+
+            CFRunLoopSourceRef = Quartz.CFMachPortCreateRunLoopSource(
+                None, NSMachPort, 0
+            )
+            CFRunLoopRef = Quartz.CFRunLoopGetCurrent()
+            Quartz.CFRunLoopAddSource(
+                CFRunLoopRef, CFRunLoopSourceRef, Quartz.kCFRunLoopDefaultMode
+            )
+            Quartz.CGEventTapEnable(NSMachPort, True)
+
+        def listen(self):
+            """Listen for quartz events."""
+            while self.active:
+                Quartz.CFRunLoopRunInMode(Quartz.kCFRunLoopDefaultMode, 5, False)
+
+        def uninstall_handle_input(self):
+            self.active = False
+
+        def _get_mouse_button_number(self, event):
+            """Get the mouse button number from an event."""
+            return Quartz.CGEventGetIntegerValueField(
+                event, Quartz.kCGMouseEventButtonNumber
+            )
+
+        def _get_click_state(self, event):
+            """The click state from an event."""
+            return Quartz.CGEventGetIntegerValueField(
+                event, Quartz.kCGMouseEventClickState
+            )
+
+        def _get_scroll(self, event):
+            """The scroll values from an event."""
+            scroll_y = Quartz.CGEventGetIntegerValueField(
+                event, Quartz.kCGScrollWheelEventDeltaAxis1
+            )
+            scroll_x = Quartz.CGEventGetIntegerValueField(
+                event, Quartz.kCGScrollWheelEventDeltaAxis2
+            )
+            return scroll_x, scroll_y
+
+        def _get_absolute(self, event):
+            """Get abolute cursor location."""
+            return Quartz.CGEventGetLocation(event)
+
+        def _get_relative(self, event):
+            """Get the relative mouse movement."""
+            delta_x = Quartz.CGEventGetIntegerValueField(
+                event, Quartz.kCGMouseEventDeltaX
+            )
+            delta_y = Quartz.CGEventGetIntegerValueField(
+                event, Quartz.kCGMouseEventDeltaY
+            )
+            return delta_x, delta_y
+
+    mouse = QuartzMouseListener(pipe)
+    mouse.listen()
+
+
+class AppKitMouseBaseListener(BaseListener):
+    """Emulate evdev behaviour on the the Mac."""
+
+    def __init__(self, pipe, events=None):
+        super(AppKitMouseBaseListener, self).__init__(
+            pipe, events, codes=dict(MAC_EVENT_CODES)
+        )
+
+    @staticmethod
+    def _get_mouse_button_number(event):
+        """Get the button number."""
+        return event.buttonNumber()
+
+    @staticmethod
+    def _get_absolute(event):
+        """Get the absolute (pixel) location of the mouse cursor."""
+        return event.locationInWindow()
+
+    @staticmethod
+    def _get_event_type(event):
+        """Get the appkit event type of the event."""
+        return event.type()
+
+    @staticmethod
+    def _get_deltas(event):
+        """Get the changes from the appkit event."""
+        delta_x = round(event.deltaX())
+        delta_y = round(event.deltaY())
+        delta_z = round(event.deltaZ())
+        return delta_x, delta_y, delta_z
+
+    def handle_button(self, event, event_type):
+        """Handle mouse click."""
+        mouse_button_number = self._get_mouse_button_number(event)
+        # Identify buttons 3,4,5
+        if event_type in (25, 26):
+            event_type = event_type + (mouse_button_number * 0.1)
+        # Add buttons to events
+        event_type_name, event_code, value, scan = self.codes[event_type]
+        if event_type_name == "Key":
+            scan_event, key_event = self.emulate_press(
+                event_code, scan, value, self.timeval
+            )
+            self.events.append(scan_event)
+            self.events.append(key_event)
+
+    def handle_absolute(self, event):
+        """Absolute mouse position on the screen."""
+        point = self._get_absolute(event)
+        x_pos = round(point.x)
+        y_pos = round(point.y)
+        x_event, y_event = self.emulate_abs(x_pos, y_pos, self.timeval)
+        self.events.append(x_event)
+        self.events.append(y_event)
+
+    def handle_scrollwheel(self, event):
+        """Make endev from appkit scroll wheel event."""
+        delta_x, delta_y, delta_z = self._get_deltas(event)
+        if delta_x:
+            self.events.append(self.emulate_wheel(delta_x, 'x', self.timeval))
+        if delta_y:
+            self.events.append(self.emulate_wheel(delta_y, 'y', self.timeval))
+        if delta_z:
+            self.events.append(self.emulate_wheel(delta_z, 'z', self.timeval))
+
+    def handle_relative(self, event):
+        """Get the position of the mouse on the screen."""
+        delta_x, delta_y, delta_z = self._get_deltas(event)
+        if delta_x:
+            self.events.append(self.emulate_rel(0x00, delta_x, self.timeval))
+        if delta_y:
+            self.events.append(self.emulate_rel(0x01, delta_y, self.timeval))
+        if delta_z:
+            self.events.append(self.emulate_rel(0x02, delta_z, self.timeval))
+
+    def handle_input(self, event):
+        """Process the mouse event."""
+        self.update_timeval()
+        self.events = []
+        code = self._get_event_type(event)
+
+        # Deal with buttons
+        self.handle_button(event, code)
+
+        # Mouse wheel
+        if code == 22:
+            self.handle_scrollwheel(event)
+        # Other relative mouse movements
+        else:
+            self.handle_relative(event)
+
+        # Add in the absolute position of the mouse cursor
+        self.handle_absolute(event)
+
+        # End with a sync marker
+        self.events.append(self.sync_marker(self.timeval))
+
+        # We are done
+        self.write_to_pipe(self.events)
+
+
+def appkit_mouse_process(pipe):
+    """Single subprocess for reading mouse events on Mac using older AppKit."""
+    # pylint: disable=import-error,too-many-locals
+
+    # Note Objective C does not support a Unix style fork.
+    # So these imports have to be inside the child subprocess since
+    # otherwise the child process cannot use them.
+
+    # pylint: disable=no-member, no-name-in-module
+    from Foundation import NSObject
+    from AppKit import NSApplication, NSApp
+    from Cocoa import (
+        NSEvent,
+        NSLeftMouseDownMask,
+        NSLeftMouseUpMask,
+        NSRightMouseDownMask,
+        NSRightMouseUpMask,
+        NSMouseMovedMask,
+        NSLeftMouseDraggedMask,
+        NSRightMouseDraggedMask,
+        NSMouseEnteredMask,
+        NSMouseExitedMask,
+        NSScrollWheelMask,
+        NSOtherMouseDownMask,
+        NSOtherMouseUpMask,
+    )
+    from PyObjCTools import AppHelper
+    import objc
+
+    class MacMouseSetup(NSObject):
+        """Setup the handler."""
+
+        @objc.python_method
+        def init_with_handler(self, handler):
+            """
+            Init method that receives the write end of the pipe.
+            """
+            # ALWAYS call the super's designated initializer.
+            # Also, make sure to re-bind "self" just in case it
+            # returns something else!
+            # pylint: disable=self-cls-assignment
+            self = super(MacMouseSetup, self).init()
+            self.handler = handler
+            # Unlike Python's __init__, initializers MUST return self,
+            # because they are allowed to return any object!
+            return self
+
+        # pylint: disable=invalid-name, unused-argument
+        def applicationDidFinishLaunching_(self, notification):
+            """Bind the listen method as the handler for mouse events."""
+
+            mask = (
+                NSLeftMouseDownMask
+                | NSLeftMouseUpMask
+                | NSRightMouseDownMask
+                | NSRightMouseUpMask
+                | NSMouseMovedMask
+                | NSLeftMouseDraggedMask
+                | NSRightMouseDraggedMask
+                | NSScrollWheelMask
+                | NSMouseEnteredMask
+                | NSMouseExitedMask
+                | NSOtherMouseDownMask
+                | NSOtherMouseUpMask
+            )
+            NSEvent.addGlobalMonitorForEventsMatchingMask_handler_(mask, self.handler)
+
+    class MacMouseListener(AppKitMouseBaseListener):
+        """Loosely emulate Evdev mouse behaviour on the Macs.
+        Listen for key events then buffer them in a pipe.
+        """
+
+        def install_handle_input(self):
+            """Install the hook."""
+            self.app = NSApplication.sharedApplication()
+            # pylint: disable=no-member
+            delegate = MacMouseSetup.alloc().init_with_handler(self.handle_input)
+            NSApp().setDelegate_(delegate)
+            AppHelper.runEventLoop()
+
+        def __del__(self):
+            """Stop the listener on deletion."""
+            AppHelper.stopEventLoop()
+
+    MacMouseListener(pipe, events=[])
+
+
+class AppKitKeyboardListener(BaseListener):
+    """Emulate an evdev keyboard on the Mac."""
+
+    def __init__(self, pipe):
+        super(AppKitKeyboardListener, self).__init__(pipe, codes=dict(MAC_KEYS))
+
+    @staticmethod
+    def _get_event_key_code(event):
+        """Get the key code."""
+        return event.keyCode()
+
+    @staticmethod
+    def _get_event_type(event):
+        """Get the event type."""
+        return event.type()
+
+    @staticmethod
+    def _get_flag_value(event):
+        """Note, this may be able to be made more accurate,
+        i.e. handle two modifier keys at once."""
+        flags = event.modifierFlags()
+        if flags == 0x100:
+            value = 0
+        else:
+            value = 1
+        return value
+
+    def _get_key_value(self, event, event_type):
+        """Get the key value."""
+        if event_type == 10:
+            value = 1
+        elif event_type == 11:
+            value = 0
+        elif event_type == 12:
+            value = self._get_flag_value(event)
+        else:
+            value = -1
+        return value
+
+    def handle_input(self, event):
+        """Process they keyboard input."""
+        self.update_timeval()
+        self.events = []
+        code = self._get_event_key_code(event)
+        new_code = (self.codes or {}).get(code, 0)
+        event_type = self._get_event_type(event)
+        value = self._get_key_value(event, event_type)
+        scan_event, key_event = self.emulate_press(new_code, code, value, self.timeval)
+
+        self.events.append(scan_event)
+        self.events.append(key_event)
+        # End with a sync marker
+        self.events.append(self.sync_marker(self.timeval))
+        # We are done
+        self.write_to_pipe(self.events)
+
+
+def mac_keyboard_process(pipe):
+    """Single subprocesses for reading keyboard on Mac."""
+    # pylint: disable=import-error,too-many-locals
+    # Note Objective C does not support a Unix style fork.
+    # So these imports have to be inside the child subprocess since
+    # otherwise the child process cannot use them.
+
+    # pylint: disable=no-member, no-name-in-module
+    from AppKit import NSApplication, NSApp
+    from Foundation import NSObject
+    from Cocoa import NSEvent, NSKeyDownMask, NSKeyUpMask, NSFlagsChangedMask
+    from PyObjCTools import AppHelper
+    import objc
+
+    class MacKeyboardSetup(NSObject):
+        """Setup the handler."""
+
+        @objc.python_method
+        def init_with_handler(self, handler):
+            """
+            Init method that receives the write end of the pipe.
+            """
+            # ALWAYS call the super's designated initializer.
+            # Also, make sure to re-bind "self" just in case it
+            # returns something else!
+
+            # pylint: disable=self-cls-assignment
+            self = super(MacKeyboardSetup, self).init()
+
+            self.handler = handler
+
+            # Unlike Python's __init__, initializers MUST return self,
+            # because they are allowed to return any object!
+            return self
+
+        # pylint: disable=invalid-name, unused-argument
+        def applicationDidFinishLaunching_(self, notification):
+            """Bind the handler to listen to keyboard events."""
+            mask = NSKeyDownMask | NSKeyUpMask | NSFlagsChangedMask
+            NSEvent.addGlobalMonitorForEventsMatchingMask_handler_(mask, self.handler)
+
+    class MacKeyboardListener(AppKitKeyboardListener):
+        """Loosely emulate Evdev keyboard behaviour on the Mac.
+        Listen for key events then buffer them in a pipe.
+        """
+
+        def install_handle_input(self):
+            """Install the hook."""
+            self.app = NSApplication.sharedApplication()
+            # pylint: disable=no-member
+            delegate = MacKeyboardSetup.alloc().init_with_handler(self.handle_input)
+            NSApp().setDelegate_(delegate)
+            AppHelper.runEventLoop()
+
+        def __del__(self):
+            """Stop the listener on deletion."""
+            AppHelper.stopEventLoop()
+
+    MacKeyboardListener(pipe)
+
+
+class InputDevice:
+    """A user input device."""
+
+    # pylint: disable=too-many-instance-attributes
+    def __init__(self, manager, device_path=None, char_path_override=None, read_size=1):
+        self.read_size = read_size
+        self.manager = manager
+        self.__pipe = None
+        self._listener = None
+        self.leds = None
+        if device_path:
+            self._device_path = device_path
+        else:
+            self._set_device_path()
+        # We should by now have a device_path
+
+        try:
+            if not self._device_path:
+                raise NoDevicePath
+        except AttributeError:
+            raise NoDevicePath
+
+        self.protocol, _, self.device_type = self._get_path_infomation()
+        if char_path_override:
+            self._character_device_path = char_path_override
+        else:
+            self._character_device_path = os.path.realpath(self._device_path)
+
+        self._character_file = None
+
+        self._evdev = False
+        self._set_evdev_state()
+
+        self.name = "Unknown Device"
+        self._set_name()
+
+    def _set_device_path(self):
+        """Set the device path, overridden on the MAC and Windows."""
+        pass
+
+    def _set_evdev_state(self):
+        """Set whether the device is a real evdev device."""
+        if NIX:
+            self._evdev = True
+
+    def _set_name(self):
+        if NIX:
+            with open(
+                "/sys/class/input/%s/device/name" % self.get_char_name()
+            ) as name_file:
+                self.name = name_file.read().strip()
+            self.leds = []
+
+    def _get_path_infomation(self):
+        """Get useful infomation from the device path."""
+        long_identifier = self._device_path.split('/')[4]
+        protocol, remainder = long_identifier.split('-', 1)
+        identifier, _, device_type = remainder.rsplit('-', 2)
+        return (protocol, identifier, device_type)
+
+    def get_char_name(self):
+        """Get short version of char device name."""
+        return self._character_device_path.split('/')[-1]
+
+    def get_char_device_path(self):
+        """Get the char device path."""
+        return self._character_device_path
+
+    def __str__(self):
+        try:
+            return self.name
+        except AttributeError:
+            return "Unknown Device"
+
+    def __repr__(self):
+        return '%s.%s("%s")' % (
+            self.__module__,
+            self.__class__.__name__,
+            self._device_path,
+        )
+
+    @property
+    def _character_device(self):
+        if not self._character_file:
+            if WIN:
+                self._character_file = io.BytesIO()
+                return self._character_file
+            try:
+                self._character_file = io.open(self._character_device_path, 'rb')
+            except PermissionError:
+                # Python 3
+                raise PermissionError(PERMISSIONS_ERROR_TEXT)
+            except IOError as err:
+                # Python 2
+                if err.errno == 13:
+                    raise PermissionError(PERMISSIONS_ERROR_TEXT)
+                else:
+                    raise
+
+        return self._character_file
+
+    def __iter__(self):
+        while True:
+            event = self._do_iter()
+            if event:
+                yield event
+
+    def _get_data(self, read_size):
+        """Get data from the character device."""
+        return self._character_device.read(read_size)
+
+    @staticmethod
+    def _get_target_function():
+        """Get the correct target function. This is only used by Windows
+        subclasses."""
+        return False
+
+    def _get_total_read_size(self):
+        """How much event data to process at once."""
+        if self.read_size:
+            read_size = EVENT_SIZE * self.read_size
+        else:
+            read_size = EVENT_SIZE
+        return read_size
+
+    def _do_iter(self):
+        read_size = self._get_total_read_size()
+        data = self._get_data(read_size)
+        if not data:
+            return None
+        evdev_objects = iter_unpack(data)
+        events = [self._make_event(*event) for event in evdev_objects]
+        return events
+
+    # pylint: disable=too-many-arguments
+    def _make_event(self, tv_sec, tv_usec, ev_type, code, value):
+        """Create a friendly Python object from an evdev style event."""
+        event_type = self.manager.get_event_type(ev_type)
+        eventinfo = {
+            "ev_type": event_type,
+            "state": value,
+            "timestamp": tv_sec + (tv_usec / 1000000),
+            "code": self.manager.get_event_string(event_type, code),
+        }
+
+        return InputEvent(self, eventinfo)
+
+    def read(self):
+        """Read the next input event."""
+        return next(iter(self))
+
+    @property
+    def _pipe(self):
+        """On Windows we use a pipe to emulate a Linux style character
+        buffer."""
+        if self._evdev:
+            return None
+
+        if not self.__pipe:
+            target_function = self._get_target_function()
+            if not target_function:
+                return None
+
+            self.__pipe, child_conn = Pipe(duplex=False)
+            self._listener = Process(target=target_function, args=(child_conn,))
+            self._listener.start()
+        return self.__pipe
+
+    def __del__(self):
+        if ('WIN' in globals() or 'MAC' in globals()) and (WIN or MAC) and self.__pipe:
+            self._listener.terminate()
+
+
+class Keyboard(InputDevice):
+    """A keyboard or other key-like device.
+
+    Original umapped scan code, followed by the important key info
+    followed by a sync.
+    """
+
+    def _set_device_path(self):
+        super(Keyboard, self)._set_device_path()
+        if MAC:
+            self._device_path = APPKIT_KB_PATH
+
+    def _set_name(self):
+        super(Keyboard, self)._set_name()
+        if WIN:
+            self.name = "Microsoft Keyboard"
+        elif MAC:
+            self.name = "AppKit Keyboard"
+
+    @staticmethod
+    def _get_target_function():
+        """Get the correct target function."""
+        if WIN:
+            return keyboard_process
+        if MAC:
+            return mac_keyboard_process
+        return None
+
+    def _get_data(self, read_size):
+        """Get data from the character device."""
+        if NIX:
+            return super(Keyboard, self)._get_data(read_size)
+        return self._pipe.recv_bytes()
+
+
+class Mouse(InputDevice):
+    """A mouse or other pointing-like device."""
+
+    def _set_device_path(self):
+        super(Mouse, self)._set_device_path()
+        if MAC:
+            self._device_path = APPKIT_MOUSE_PATH
+
+    def _set_name(self):
+        super(Mouse, self)._set_name()
+        if WIN:
+            self.name = "Microsoft Mouse"
+        elif MAC:
+            self.name = "AppKit Mouse"
+
+    @staticmethod
+    def _get_target_function():
+        """Get the correct target function."""
+        if WIN:
+            return mouse_process
+        if MAC:
+            return appkit_mouse_process
+        return None
+
+    def _get_data(self, read_size):
+        """Get data from the character device."""
+        if NIX:
+            return super(Mouse, self)._get_data(read_size)
+        return self._pipe.recv_bytes()
+
+
+class MightyMouse(Mouse):
+    """A mouse or other pointing device on the Mac."""
+
+    def _set_device_path(self):
+        super(MightyMouse, self)._set_device_path()
+        if MAC:
+            self._device_path = QUARTZ_MOUSE_PATH
+
+    def _set_name(self):
+        self.name = "Quartz Mouse"
+
+    @staticmethod
+    def _get_target_function():
+        """Get the correct target function."""
+        return quartz_mouse_process
+
+
+def delay_and_stop(duration, dll, device_number):
+    """Stop vibration aka force feedback aka rumble on
+    Windows after duration miliseconds."""
+    xinput = getattr(ctypes.windll, dll)
+    time.sleep(duration / 1000)
+    xinput_set_state = xinput.XInputSetState
+    xinput_set_state.argtypes = [ctypes.c_uint, ctypes.POINTER(XinputVibration)]
+    xinput_set_state.restype = ctypes.c_uint
+    vibration = XinputVibration(0, 0)
+    xinput_set_state(device_number, ctypes.byref(vibration))
+
+
+# I made this GamePad class before Mouse and Keyboard above, and have
+# learned a lot about Windows in the process.  This can probably be
+# simplified massively and made to match Mouse and Keyboard more.
+
+
+class GamePad(InputDevice):
+    """A gamepad or other joystick-like device."""
+
+    def __init__(self, manager, device_path, char_path_override=None):
+        super(GamePad, self).__init__(manager, device_path, char_path_override)
+        self._write_file = None
+        self.__device_number = None
+        if WIN and "Microsoft_Corporation_Controller" in self._device_path:
+            self.name = "Microsoft X-Box 360 pad"
+            identifier = self._get_path_infomation()[1]
+            self.__device_number = int(identifier.split('_')[-1])
+            self.__received_packets = 0
+            self.__missed_packets = 0
+            self.__last_state = self.__read_device()
+        if NIX:
+            self._number_xpad()
+
+    def _number_xpad(self):
+        """Get the number of the joystick."""
+        js_path = self._device_path.replace('-event', '')
+        js_chardev = os.path.realpath(js_path)
+        try:
+            number_text = js_chardev.split('js')[1]
+        except IndexError:
+            return
+        try:
+            number = int(number_text)
+        except ValueError:
+            return
+        self.__device_number = number
+
+    def get_number(self):
+        """Return the joystick number of the gamepad."""
+        return self.__device_number
+
+    def __iter__(self):
+        while True:
+            if WIN:
+                self.__check_state()
+            event = self._do_iter()
+            if event:
+                yield event
+
+    def __check_state(self):
+        """On Windows, check the state and fill the event character device."""
+        state = self.__read_device()
+        if not state:
+            raise UnpluggedError("Gamepad %d is not connected" % self.__device_number)
+        if state.packet_number != self.__last_state.packet_number:
+            # state has changed, handle the change
+            self.__handle_changed_state(state)
+            self.__last_state = state
+
+    @staticmethod
+    def __get_timeval():
+        """Get the time and make it into C style timeval."""
+        return convert_timeval(time.time())
+
+    def create_event_object(self, event_type, code, value, timeval=None):
+        """Create an evdev style object."""
+        if not timeval:
+            timeval = self.__get_timeval()
+        try:
+            event_code = self.manager.codes['type_codes'][event_type]
+        except KeyError:
+            raise UnknownEventType(
+                "We don't know what kind of event a %s is." % event_type
+            )
+        event = struct.pack(
+            EVENT_FORMAT, timeval[0], timeval[1], event_code, code, value
+        )
+        return event
+
+    def __write_to_character_device(self, event_list, timeval=None):
+        """Emulate the Linux character device on other platforms such as
+        Windows."""
+        # Remember the position of the stream
+        pos = self._character_device.tell()
+        # Go to the end of the stream
+        self._character_device.seek(0, 2)
+        # Write the new data to the end
+        for event in event_list:
+            self._character_device.write(event)
+        # Add a sync marker
+        sync = self.create_event_object("Sync", 0, 0, timeval)
+        self._character_device.write(sync)
+        # Put the stream back to its original position
+        self._character_device.seek(pos)
+
+    def __handle_changed_state(self, state):
+        """
+        we need to pack a struct with the following five numbers:
+        tv_sec, tv_usec, ev_type, code, value
+
+        then write it using __write_to_character_device
+
+        seconds, mircroseconds, ev_type, code, value
+        time we just use now
+        ev_type we look up
+        code we look up
+        value is 0 or 1 for the buttons
+        axis value is maybe the same as Linux? Hope so!
+        """
+        timeval = self.__get_timeval()
+        events = self.__get_button_events(state, timeval)
+        events.extend(self.__get_axis_events(state, timeval))
+        if events:
+            self.__write_to_character_device(events, timeval)
+
+    def __map_button(self, button):
+        """Get the linux xpad code from the Windows xinput code."""
+        _, start_code, start_value = button
+        value = start_value
+        ev_type = "Key"
+        code = self.manager.codes['xpad'][start_code]
+        if 1 <= start_code <= 4:
+            ev_type = "Absolute"
+        if (start_code == 1 and start_value == 1) or (
+            start_code == 3 and start_value == 1
+        ):
+            value = -1
+        return code, value, ev_type
+
+    def __map_axis(self, axis):
+        """Get the linux xpad code from the Windows xinput code."""
+        start_code, start_value = axis
+        value = start_value
+        code = self.manager.codes['xpad'][start_code]
+        return code, value
+
+    def __get_button_events(self, state, timeval=None):
+        """Get the button events from xinput."""
+        changed_buttons = self.__detect_button_events(state)
+        events = self.__emulate_buttons(changed_buttons, timeval)
+        return events
+
+    def __get_axis_events(self, state, timeval=None):
+        """Get the stick events from xinput."""
+        axis_changes = self.__detect_axis_events(state)
+        events = self.__emulate_axis(axis_changes, timeval)
+        return events
+
+    def __emulate_axis(self, axis_changes, timeval=None):
+        """Make the axis events use the Linux style format."""
+        events = []
+        for axis in axis_changes:
+            code, value = self.__map_axis(axis)
+            event = self.create_event_object("Absolute", code, value, timeval=timeval)
+            events.append(event)
+        return events
+
+    def __emulate_buttons(self, changed_buttons, timeval=None):
+        """Make the button events use the Linux style format."""
+        events = []
+        for button in changed_buttons:
+            code, value, ev_type = self.__map_button(button)
+            event = self.create_event_object(ev_type, code, value, timeval=timeval)
+            events.append(event)
+        return events
+
+    @staticmethod
+    def __gen_bit_values(number):
+        """
+        Return a zero or one for each bit of a numeric value up to the most
+        significant 1 bit, beginning with the least significant bit.
+        """
+        number = int(number)
+        while number:
+            yield number & 0x1
+            number >>= 1
+
+    def __get_bit_values(self, number, size=32):
+        """Get bit values as a list for a given number
+
+        >>> get_bit_values(1) == [0]*31 + [1]
+        True
+
+        >>> get_bit_values(0xDEADBEEF)
+        [1L, 1L, 0L, 1L, 1L, 1L, 1L,
+        0L, 1L, 0L, 1L, 0L, 1L, 1L, 0L, 1L, 1L, 0L, 1L, 1L, 1L, 1L,
+        1L, 0L, 1L, 1L, 1L, 0L, 1L, 1L, 1L, 1L]
+
+        You may override the default word size of 32-bits to match your actual
+        application.
+        >>> get_bit_values(0x3, 2)
+        [1L, 1L]
+
+        >>> get_bit_values(0x3, 4)
+        [0L, 0L, 1L, 1L]
+
+        """
+        res = list(self.__gen_bit_values(number))
+        res.reverse()
+        # 0-pad the most significant bit
+        res = [0] * (size - len(res)) + res
+        return res
+
+    def __detect_button_events(self, state):
+        changed = state.gamepad.buttons ^ self.__last_state.gamepad.buttons
+        changed = self.__get_bit_values(changed, 16)
+        buttons_state = self.__get_bit_values(state.gamepad.buttons, 16)
+        changed.reverse()
+        buttons_state.reverse()
+        button_numbers = count(1)
+        changed_buttons = list(
+            filter(itemgetter(0), list(zip(changed, button_numbers, buttons_state)))
+        )
+        # returns for example [(1,15,1)] type, code, value?
+        return changed_buttons
+
+    def __detect_axis_events(self, state):
+        # axis fields are everything but the buttons
+        # pylint: disable=protected-access
+        # Attribute name _fields_ is special name set by ctypes
+        axis_fields = dict(XinputGamepad._fields_)
+        axis_fields.pop('buttons')
+        changed_axes = []
+
+        # Ax_type might be useful when we support high-level deadzone
+        # methods.
+        # pylint: disable=unused-variable
+        for axis, _ in list(axis_fields.items()):
+            old_val = getattr(self.__last_state.gamepad, axis)
+            new_val = getattr(state.gamepad, axis)
+            if old_val != new_val:
+                changed_axes.append((axis, new_val))
+        return changed_axes
+
+    def __read_device(self):
+        """Read the state of the gamepad."""
+        state = XinputState()
+        res = self.manager.xinput.XInputGetState(
+            self.__device_number, ctypes.byref(state)
+        )
+        if res == XINPUT_ERROR_SUCCESS:
+            return state
+        if res != XINPUT_ERROR_DEVICE_NOT_CONNECTED:
+            raise RuntimeError(
+                "Unknown error %d attempting to get state of device %d"
+                % (res, self.__device_number)
+            )
+        # else (device is not connected)
+        return None
+
+    @property
+    def _write_device(self):
+        if not self._write_file:
+            if not NIX:
+                return None
+            try:
+                self._write_file = io.open(self._character_device_path, 'wb')
+            except PermissionError:
+                # Python 3
+                raise PermissionError(PERMISSIONS_ERROR_TEXT)
+            except IOError as err:
+                # Python 2
+                if err.errno == 13:
+                    raise PermissionError(PERMISSIONS_ERROR_TEXT)
+                else:
+                    raise
+
+        return self._write_file
+
+    def _start_vibration_win(self, left_motor, right_motor):
+        """Start the vibration, which will run until stopped."""
+        xinput_set_state = self.manager.xinput.XInputSetState
+        xinput_set_state.argtypes = [ctypes.c_uint, ctypes.POINTER(XinputVibration)]
+        xinput_set_state.restype = ctypes.c_uint
+        vibration = XinputVibration(int(left_motor * 65535), int(right_motor * 65535))
+        xinput_set_state(self.__device_number, ctypes.byref(vibration))
+
+    def _stop_vibration_win(self):
+        """Stop the vibration."""
+        xinput_set_state = self.manager.xinput.XInputSetState
+        xinput_set_state.argtypes = [ctypes.c_uint, ctypes.POINTER(XinputVibration)]
+        xinput_set_state.restype = ctypes.c_uint
+        stop_vibration = ctypes.byref(XinputVibration(0, 0))
+        xinput_set_state(self.__device_number, stop_vibration)
+
+    def _set_vibration_win(self, left_motor, right_motor, duration):
+        """Control the motors on Windows."""
+        self._start_vibration_win(left_motor, right_motor)
+        stop_process = Process(
+            target=delay_and_stop,
+            args=(duration, self.manager.xinput_dll, self.__device_number),
+        )
+        stop_process.start()
+
+    def __get_vibration_code(self, left_motor, right_motor, duration):
+        """This is some crazy voodoo, if you can simplify it, please do."""
+        inner_event = struct.pack(
+            '2h6x2h2x2H28x',
+            0x50,
+            -1,
+            duration,
+            0,
+            int(left_motor * 65535),
+            int(right_motor * 65535),
+        )
+        buf_conts = ioctl(self._write_device, 1076905344, inner_event)
+        return int(codecs.encode(buf_conts[1:3], 'hex'), 16)
+
+    def _set_vibration_nix(self, left_motor, right_motor, duration):
+        """Control the motors on Linux.
+        Duration is in miliseconds."""
+        code = self.__get_vibration_code(left_motor, right_motor, duration)
+        secs, msecs = convert_timeval(time.time())
+        outer_event = struct.pack(EVENT_FORMAT, secs, msecs, 0x15, code, 1)
+        self._write_device.write(outer_event)
+        self._write_device.flush()
+
+    def set_vibration(self, left_motor, right_motor, duration):
+        """Control the speed of both motors seperately or together.
+        left_motor and right_motor arguments require a number between
+        0 (off) and 1 (full).
+        duration is miliseconds, e.g. 1000 for a second."""
+        if WIN:
+            self._set_vibration_win(left_motor, right_motor, duration)
+        elif NIX:
+            self._set_vibration_nix(left_motor, right_motor, duration)
+        else:
+            raise NotImplementedError
+
+
+class OtherDevice(InputDevice):
+    """A device of which its is type is either undetectable or has not
+    been implemented yet.
+    """
+
+    pass
+
+
+class LED:
+    """A light source."""
+
+    def __init__(self, manager, path, name):
+        self.manager = manager
+        self.path = path
+        self.name = name
+        self._write_file = None
+        self._character_device_path = None
+        self._post_init()
+
+    def _post_init(self):
+        """Post init setup."""
+        pass
+
+    def __str__(self):
+        return self.name
+
+    def __repr__(self):
+        return '%s.%s("%s")' % (self.__module__, self.__class__.__name__, self.path)
+
+    def status(self):
+        """Get the device status, i.e. the brightness level."""
+        status_filename = os.path.join(self.path, 'brightness')
+        with open(status_filename) as status_fp:
+            result = status_fp.read()
+        status_text = result.strip()
+        try:
+            status = int(status_text)
+        except ValueError:
+            return status_text
+        return status
+
+    def max_brightness(self):
+        """Get the device's maximum brightness level."""
+        status_filename = os.path.join(self.path, 'max_brightness')
+        with open(status_filename) as status_fp:
+            result = status_fp.read()
+        status_text = result.strip()
+        try:
+            status = int(status_text)
+        except ValueError:
+            return status_text
+        return status
+
+    @property
+    def _write_device(self):
+        """The output device."""
+        if not self._write_file:
+            if not NIX:
+                return None
+            try:
+                self._write_file = io.open(self._character_device_path, 'wb')
+            except PermissionError:
+                # Python 3
+                raise PermissionError(PERMISSIONS_ERROR_TEXT)
+            except IOError as err:
+                # Python 2 only
+                if err.errno == 13:  # pragma: no cover
+                    raise PermissionError(PERMISSIONS_ERROR_TEXT)
+                else:
+                    raise
+
+        return self._write_file
+
+    def _make_event(self, event_type, code, value):
+        """Make a new event and send it to the character device."""
+        secs, msecs = convert_timeval(time.time())
+        data = struct.pack(EVENT_FORMAT, secs, msecs, event_type, code, value)
+        self._write_device.write(data)
+        self._write_device.flush()
+
+
+class SystemLED(LED):
+    """An LED on your system e.g. caps lock."""
+
+    def __init__(self, manager, path, name):
+        self.code = None
+        self.device_path = None
+        self.device = None
+        super(SystemLED, self).__init__(manager, path, name)
+
+    def _post_init(self):
+        """Set up the device path and type code."""
+        self._led_type_code = self.manager.get_typecode('LED')
+        self.device_path = os.path.realpath(os.path.join(self.path, 'device'))
+        if '::' in self.name:
+            chardev, code_name = self.name.split('::')
+            if code_name in self.manager.codes['LED_type_codes']:
+                self.code = self.manager.codes['LED_type_codes'][code_name]
+            try:
+                event_number = chardev.split('input')[1]
+            except IndexError:
+                print("Failed with", self.name)
+                raise
+            else:
+                self._character_device_path = '/dev/input/event' + event_number
+                self._match_device()
+
+    def on(self):  # pylint: disable=invalid-name
+        """Turn the light on."""
+        self._make_event(1)
+
+    def off(self):
+        """Turn the light off."""
+        self._make_event(0)
+
+    def _make_event(self, value):  # pylint: disable=arguments-differ
+        """Make a new event and send it to the character device."""
+        super(SystemLED, self)._make_event(self._led_type_code, self.code, value)
+
+    def _match_device(self):
+        """If the LED is connected to an input device,
+        associate the objects."""
+        for device in self.manager.all_devices:
+            if device.get_char_device_path() == self._character_device_path:
+                self.device = device
+                device.leds.append(self)
+                break
+
+
+class GamepadLED(LED):
+    """A light source on a gamepad."""
+
+    def __init__(self, manager, path, name):
+        self.code = None
+        self.device = None
+        self.gamepad = None
+        super(GamepadLED, self).__init__(manager, path, name)
+
+    def _post_init(self):
+        self._match_device()
+        self._character_device_path = (
+            self.gamepad.get_char_device_path() if self.gamepad else None
+        )
+
+    def _match_device(self):
+        number = int(self.name.split('xpad')[1])
+        for gamepad in self.manager.gamepads:
+            if number == gamepad.get_number():
+                self.gamepad = gamepad
+                gamepad.leds.append(self)
+                break
+
+
+class RawInputDeviceList(ctypes.Structure):
+    """
+    Contains information about a raw input device.
+
+    For full details see Microsoft's documentation:
+
+    http://msdn.microsoft.com/en-us/library/windows/desktop/
+    ms645568(v=vs.85).aspx
+    """
+
+    # pylint: disable=too-few-public-methods
+    _fields_ = [("hDevice", HANDLE), ("dwType", DWORD)]
+
+
+class DeviceManager:
+    """Provides access to all connected and detectible user input
+    devices."""
+
+    # pylint: disable=too-many-instance-attributes
+
+    def __init__(self):
+        self.codes = {key: dict(value) for key, value in EVENT_MAP}
+        self._raw = []
+        self.keyboards = []
+        self.mice = []
+        self.gamepads = []
+        self.other_devices = []
+        self.all_devices = []
+        self.leds = []
+        self.microbits = []
+        self.xinput = None
+        self.xinput_dll = None
+        if WIN:
+            self._raw_device_counts = {
+                'mice': 0,
+                'keyboards': 0,
+                'otherhid': 0,
+                'unknown': 0,
+            }
+        self._post_init()
+
+    def _post_init(self):
+        """Call the find devices method for the relevant platform."""
+        if WIN:
+            self._find_devices_win()
+        elif MAC:
+            self._find_devices_mac()
+        else:
+            self._find_devices()
+        self._update_all_devices()
+        if NIX:
+            self._find_leds()
+
+    def _update_all_devices(self):
+        """Update the all_devices list."""
+        self.all_devices = []
+        self.all_devices.extend(self.keyboards)
+        self.all_devices.extend(self.mice)
+        self.all_devices.extend(self.gamepads)
+        self.all_devices.extend(self.other_devices)
+
+    def _parse_device_path(self, device_path, char_path_override=None):
+        """Parse each device and add to the approriate list."""
+
+        # 1. Make sure that we can parse the device path.
+        try:
+            device_type = device_path.rsplit('-', 1)[1]
+        except IndexError:
+            warn(
+                "The following device path was skipped as it could "
+                "not be parsed: %s" % device_path,
+                RuntimeWarning,
+            )
+            return
+
+        # 2. Make sure each device is only added once.
+        realpath = os.path.realpath(device_path)
+        if realpath in self._raw:
+            return
+        self._raw.append(realpath)
+
+        # 3. All seems good, append the device to the relevant list.
+        if device_type == 'kbd':
+            self.keyboards.append(Keyboard(self, device_path, char_path_override))
+        elif device_type == 'mouse':
+            self.mice.append(Mouse(self, device_path, char_path_override))
+        elif device_type == 'joystick':
+            self.gamepads.append(GamePad(self, device_path, char_path_override))
+        else:
+            self.other_devices.append(
+                OtherDevice(self, device_path, char_path_override)
+            )
+
+    def _find_xinput(self):
+        """Find most recent xinput library."""
+        for dll in XINPUT_DLL_NAMES:
+            try:
+                self.xinput = getattr(ctypes.windll, dll)
+            except OSError:
+                pass
+            else:
+                # We found an xinput driver
+                self.xinput_dll = dll
+                break
+        else:
+            # We didn't find an xinput library
+            warn("No xinput driver dll found, gamepads not supported.", RuntimeWarning)
+
+    def _find_devices_win(self):
+        """Find devices on Windows."""
+        self._find_xinput()
+        self._detect_gamepads()
+        self._count_devices()
+        if self._raw_device_counts['keyboards'] > 0:
+            self.keyboards.append(
+                Keyboard(self, "/dev/input/by-id/usb-A_Nice_Keyboard-event-kbd")
+            )
+
+        if self._raw_device_counts['mice'] > 0:
+            self.mice.append(
+                Mouse(
+                    self, "/dev/input/by-id/usb-A_Nice_Mouse_called_Arthur-event-mouse"
+                )
+            )
+
+    def _find_devices_mac(self):
+        """Find devices on Mac."""
+        self.keyboards.append(Keyboard(self))
+        self.mice.append(MightyMouse(self))
+        self.mice.append(Mouse(self))
+
+    def _detect_gamepads(self):
+        """Find gamepads."""
+        state = XinputState()
+        # Windows allows up to 4 gamepads.
+        for device_number in range(4):
+            res = self.xinput.XInputGetState(device_number, ctypes.byref(state))
+            if res == XINPUT_ERROR_SUCCESS:
+                # We found a gamepad
+                device_path = (
+                    "/dev/input/by_id/"
+                    + "usb-Microsoft_Corporation_Controller_%s-event-joystick"
+                    % device_number
+                )
+                self.gamepads.append(GamePad(self, device_path))
+                continue
+            if res != XINPUT_ERROR_DEVICE_NOT_CONNECTED:
+                raise RuntimeError(
+                    "Unknown error %d attempting to get state of device %d"
+                    % (res, device_number)
+                )
+
+    def _count_devices(self):
+        """See what Windows' GetRawInputDeviceList wants to tell us.
+
+        For now, we are just seeing if there is at least one keyboard
+        and/or mouse attached.
+
+        GetRawInputDeviceList could be used to help distinguish between
+        different keyboards and mice on the system in the way Linux
+        can. However, Roma uno die non est condita.
+
+        """
+        number_of_devices = ctypes.c_uint()
+
+        if (
+            ctypes.windll.user32.GetRawInputDeviceList(
+                ctypes.POINTER(ctypes.c_int)(),
+                ctypes.byref(number_of_devices),
+                ctypes.sizeof(RawInputDeviceList),
+            )
+            == -1
+        ):
+            warn(
+                "Call to GetRawInputDeviceList was unsuccessful."
+                "We have no idea if a mouse or keyboard is attached.",
+                RuntimeWarning,
+            )
+            return
+
+        devices_found = (RawInputDeviceList * number_of_devices.value)()
+
+        if (
+            ctypes.windll.user32.GetRawInputDeviceList(
+                devices_found,
+                ctypes.byref(number_of_devices),
+                ctypes.sizeof(RawInputDeviceList),
+            )
+            == -1
+        ):
+            warn(
+                "Call to GetRawInputDeviceList was unsuccessful."
+                "We have no idea if a mouse or keyboard is attached.",
+                RuntimeWarning,
+            )
+            return
+
+        for device in devices_found:
+            if device.dwType == 0:
+                self._raw_device_counts['mice'] += 1
+            elif device.dwType == 1:
+                self._raw_device_counts['keyboards'] += 1
+            elif device.dwType == 2:
+                self._raw_device_counts['otherhid'] += 1
+            else:
+                self._raw_device_counts['unknown'] += 1
+
+    def _find_devices(self):
+        """Find available devices."""
+        self._find_by('id')
+        self._find_by('path')
+        self._find_special()
+
+    def _find_by(self, key):
+        """Find devices."""
+        by_path = glob.glob('/dev/input/by-{key}/*-event-*'.format(key=key))
+        for device_path in by_path:
+            self._parse_device_path(device_path)
+
+    def _find_leds(self):
+        """Find LED devices, Linux-only so far."""
+        for path in glob.glob('/sys/class/leds/*'):
+            self._parse_led_path(path)
+
+    def _parse_led_path(self, path):
+        name = path.rsplit('/', 1)[1]
+        if name.startswith('xpad'):
+            self.leds.append(GamepadLED(self, path, name))
+        elif name.startswith('input'):
+            self.leds.append(SystemLED(self, path, name))
+        else:
+            self.leds.append(LED(self, path, name))
+
+    def _get_char_names(self):
+        """Get a list of already found devices."""
+        return [device.get_char_name() for device in self.all_devices]
+
+    def _find_special(self):
+        """Look for special devices."""
+        charnames = self._get_char_names()
+        for eventdir in glob.glob('/sys/class/input/event*'):
+            char_name = os.path.split(eventdir)[1]
+            if char_name in charnames:
+                continue
+            name_file = os.path.join(eventdir, 'device', 'name')
+            with open(name_file) as name_file:
+                device_name = name_file.read().strip()
+                if device_name in self.codes['specials']:
+                    self._parse_device_path(
+                        self.codes['specials'][device_name],
+                        os.path.join('/dev/input', char_name),
+                    )
+
+    def __iter__(self):
+        return iter(self.all_devices)
+
+    def __getitem__(self, index):
+        try:
+            return self.all_devices[index]
+        except IndexError:
+            raise IndexError("list index out of range")
+
+    def get_event_type(self, raw_type):
+        """Convert the code to a useful string name."""
+        try:
+            return self.codes['types'][raw_type]
+        except KeyError:
+            raise UnknownEventType("We don't know this event type")
+
+    def get_event_string(self, evtype, code):
+        """Get the string name of the event."""
+        if WIN and evtype == 'Key':
+            # If we can map the code to a common one then do it
+            try:
+                code = self.codes['wincodes'][code]
+            except KeyError:
+                pass
+        try:
+            return self.codes[evtype][code]
+        except KeyError:
+            raise UnknownEventCode("We don't know this event.", evtype, code)
+
+    def get_typecode(self, name):
+        """Returns type code for `name`."""
+        return self.codes['type_codes'].get(name)
+
+    def detect_microbit(self):
+        """Detect a microbit."""
+        try:
+            gpad = MicroBitPad(self)
+        except ModuleNotFoundError:
+            warn(
+                "The microbit library could not be found in the pythonpath. \n"
+                "For more information, please visit \n"
+                "https://inputs.readthedocs.io/en/latest/user/microbit.html",
+                RuntimeWarning,
+            )
+        else:
+            self.microbits.append(gpad)
+            self.gamepads.append(gpad)
+
+
+SPIN_UP_MOTOR = (
+    '00000',
+    '00001',
+    '00011',
+    '00111',
+    '01111',
+    '11111',
+    '01111',
+    '00011',
+    '00001',
+    '00000',
+    '00001',
+    '00011',
+    '00111',
+    '01111',
+    '11111',
+    '00000',
+    '11111',
+    '00000',
+    '11111',
+    '00000',
+)
+
+
+class MicroBitPad(GamePad):
+    """A BBC Micro:bit flashed with bitio."""
+
+    def __init__(self, manager, device_path=None, char_path_override=None):
+        if not device_path:
+            device_path = '/dev/input/by-id/dialup-BBC_MicroBit-event-joystick'
+            if not char_path_override:
+                char_path_override = '/dev/input/microbit0'
+
+        super(MicroBitPad, self).__init__(manager, device_path, char_path_override)
+
+        # pylint: disable=no-member,import-error
+        import microbit
+
+        self.microbit = microbit
+        self.default_image = microbit.Image("00500:00500:00500:00500:00500")
+        self._setup_rumble()
+        self.set_display()
+
+    def set_display(self, index=None):
+        """Show an image on the display."""
+        # pylint: disable=no-member
+        if index:
+            image = self.microbit.Image.STD_IMAGES[index]
+        else:
+            image = self.default_image
+        self.microbit.display.show(image)
+
+    def _setup_rumble(self):
+        """Setup the three animations which simulate a rumble."""
+        self.left_rumble = self._get_ready_to('99500')
+        self.right_rumble = self._get_ready_to('00599')
+        self.double_rumble = self._get_ready_to('99599')
+
+    def _set_name(self):
+        self.name = "BBC microbit Gamepad"
+
+    def _set_evdev_state(self):
+        self._evdev = False
+
+    @staticmethod
+    def _get_target_function():
+        return microbit_process
+
+    def _get_data(self, read_size):
+        """Get data from the character device."""
+        return self._pipe.recv_bytes()
+
+    def _get_ready_to(self, rumble):
+        """Watch us wreck the mike!
+        PSYCHE!"""
+        # pylint: disable=no-member
+        return [
+            self.microbit.Image(
+                ':'.join([rumble if char == '1' else '00500' for char in code])
+            )
+            for code in SPIN_UP_MOTOR
+        ]
+
+    def _full_speed_rumble(self, images, duration):
+        """Simulate the motors running at full."""
+        while duration > 0:
+            self.microbit.display.show(images[0])  # pylint: disable=no-member
+            time.sleep(0.04)
+            self.microbit.display.show(images[1])  # pylint: disable=no-member
+            time.sleep(0.04)
+            duration -= 0.08
+
+    def _spin_up(self, images, duration):
+        """Simulate the motors getting warmed up."""
+        total = 0
+        # pylint: disable=no-member
+
+        for image in images:
+            self.microbit.display.show(image)
+            time.sleep(0.05)
+            total += 0.05
+            if total >= duration:
+                return
+        remaining = duration - total
+        self._full_speed_rumble(images[-2:], remaining)
+        self.set_display()
+
+    def set_vibration(self, left_motor, right_motor, duration):
+        """Control the speed of both motors seperately or together.
+        left_motor and right_motor arguments require a number:
+        0 (off) or 1 (full).
+        duration is miliseconds, e.g. 1000 for a second."""
+        if left_motor and right_motor:
+            return self._spin_up(self.double_rumble, duration / 1000)
+        if left_motor:
+            return self._spin_up(self.left_rumble, duration / 1000)
+        if right_motor:
+            return self._spin_up(self.right_rumble, duration / 1000)
+        return -1
+
+
+def microbit_process(pipe):
+    """Simple subprocess for reading mouse events on the microbit."""
+    gamepad_listener = MicroBitListener(pipe)
+    gamepad_listener.listen()
+
+
+class MicroBitListener(BaseListener):
+    """Tracks the current state and sends changes to the MicroBitPad
+    device class."""
+
+    def __init__(self, pipe):
+        super(MicroBitListener, self).__init__(pipe)
+        self.active = True
+        self.events = []
+        self.state = {
+            ('Absolute', 0x10, 0),
+            ('Absolute', 0x11, 0),
+            ('Key', 0x130, 0),
+            ('Key', 0x131, 0),
+            ('Key', 0x13A, 0),
+            ('Key', 0x133, 0),
+            ('Key', 0x134, 0),
+        }
+        self.dpad = True
+        self.sensitivity = 300
+        # pylint: disable=import-error
+        import microbit
+
+        self.microbit = microbit
+
+    def listen(self):
+        """Listen while the device is active."""
+        while self.active:
+            self.handle_input()
+
+    def uninstall_handle_input(self):
+        """Stop listing when active is false."""
+        self.active = False
+
+    def handle_new_events(self, events):
+        """Add each new events to the event queue."""
+        for event in events:
+            self.events.append(
+                self.create_event_object(event[0], event[1], int(event[2]))
+            )
+
+    def handle_abs(self):
+        """Gets the state as the raw abolute numbers."""
+        # pylint: disable=no-member
+        x_raw = self.microbit.accelerometer.get_x()
+        y_raw = self.microbit.accelerometer.get_y()
+        x_abs = ('Absolute', 0x00, x_raw)
+        y_abs = ('Absolute', 0x01, y_raw)
+        return x_abs, y_abs
+
+    def handle_dpad(self):
+        """Gets the state of the virtual dpad."""
+        # pylint: disable=no-member
+        x_raw = self.microbit.accelerometer.get_x()
+        y_raw = self.microbit.accelerometer.get_y()
+        minus_sens = self.sensitivity * -1
+        if x_raw < minus_sens:
+            x_state = ('Absolute', 0x10, -1)
+        elif x_raw > self.sensitivity:
+            x_state = ('Absolute', 0x10, 1)
+        else:
+            x_state = ('Absolute', 0x10, 0)
+
+        if y_raw < minus_sens:
+            y_state = ('Absolute', 0x11, -1)
+        elif y_raw > self.sensitivity:
+            y_state = ('Absolute', 0x11, 1)
+        else:
+            y_state = ('Absolute', 0x11, 1)
+
+        return x_state, y_state
+
+    def check_state(self):
+        """Tracks differences in the device state."""
+        if self.dpad:
+            x_state, y_state = self.handle_dpad()
+        else:
+            x_state, y_state = self.handle_abs()
+
+        new_state = {
+            x_state,
+            y_state,
+            ('Key', 0x130, int(self.microbit.button_a.is_pressed())),
+            ('Key', 0x131, int(self.microbit.button_b.is_pressed())),
+            ('Key', 0x13A, int(self.microbit.pin0.is_touched())),
+            ('Key', 0x133, int(self.microbit.pin1.is_touched())),
+            ('Key', 0x134, int(self.microbit.pin2.is_touched())),
+        }
+        events = new_state - self.state
+        self.state = new_state
+        return events
+
+    def handle_input(self):
+        """Sends differences in the device state to the MicroBitPad
+        as events."""
+        difference = self.check_state()
+        if not difference:
+            return
+        self.events = []
+        self.handle_new_events(difference)
+        self.update_timeval()
+        self.events.append(self.sync_marker(self.timeval))
+        self.write_to_pipe(self.events)
+
+
+devices = DeviceManager()  # pylint: disable=invalid-name
+
+
+def get_key():
+    """Get a single keypress from a keyboard."""
+    try:
+        keyboard = devices.keyboards[0]
+    except IndexError:
+        raise UnpluggedError("No keyboard found.")
+    return keyboard.read()
+
+
+def get_mouse():
+    """Get a single movement or click from a mouse."""
+    try:
+        mouse = devices.mice[0]
+    except IndexError:
+        raise UnpluggedError("No mice found.")
+    return mouse.read()
+
+
+def get_gamepad():
+    """Get a single action from a gamepad."""
+    try:
+        gamepad = devices.gamepads[0]
+    except IndexError:
+        raise UnpluggedError("No gamepad found.")
+    return gamepad.read()
diff --git a/platypush/plugins/joystick/_manager.py b/platypush/plugins/joystick/_manager.py
new file mode 100644
index 000000000..9c97cd709
--- /dev/null
+++ b/platypush/plugins/joystick/_manager.py
@@ -0,0 +1,175 @@
+from dataclasses import asdict
+import multiprocessing
+from threading import Timer
+from typing import Optional, Type
+from time import time
+
+from platypush.context import get_bus
+from platypush.message.event.joystick import (
+    JoystickConnectedEvent,
+    JoystickDisconnectedEvent,
+    JoystickEvent,
+    JoystickStateEvent,
+)
+from platypush.schemas.joystick import JoystickDeviceSchema
+
+from ._inputs import GamePad, InputEvent, UnpluggedError
+from ._state import ConnectedState, JoystickDeviceState, JoystickState
+
+
+class JoystickManager(multiprocessing.Process):
+    """
+    A process that monitors and publishes joystick events.
+    """
+
+    MAX_TRIG_VAL = 1 << 8
+    MAX_JOY_VAL = 1 << 15
+    THROTTLE_INTERVAL = 0.2
+
+    def __init__(
+        self,
+        device: GamePad,
+        poll_interval: Optional[float],
+        state_queue: multiprocessing.Queue,
+    ):
+        super().__init__()
+        self.device = device
+        self.poll_interval = poll_interval
+        self.state = JoystickState()
+        self._state_queue = state_queue
+        self._connected_state = ConnectedState.UNKNOWN
+        self._stop_evt = multiprocessing.Event()
+        self._state_throttler: Optional[Timer] = None
+        self._state_timestamp: float = 0
+
+    @property
+    def should_stop(self):
+        return self._stop_evt.is_set()
+
+    def wait_stop(self, timeout: Optional[float] = None):
+        self._stop_evt.wait(timeout=timeout or self.poll_interval)
+
+    def _enqueue_state(self):
+        now = time()
+        self._state_queue.put_nowait(
+            JoystickDeviceState(
+                device=self.device.get_char_device_path(), state=self.state
+            )
+        )
+
+        if now - self._state_timestamp >= self.THROTTLE_INTERVAL:
+            self._post_state()
+            self._state_timestamp = now
+            return
+
+        self._state_timestamp = now
+        self._state_throttler = Timer(self.THROTTLE_INTERVAL, self._post_state)
+        self._state_throttler.start()
+
+    def _stop_state_throrttler(self):
+        if self._state_throttler:
+            self._state_throttler.cancel()
+            self._state_throttler = None
+
+    def _post_state(self):
+        self._post_event(JoystickStateEvent, state=asdict(self.state))
+        self._stop_state_throrttler()
+
+    def _parse_event(self, event: InputEvent):  # pylint: disable=too-many-branches
+        """
+        Solution adapted from https://stackoverflow.com/questions/46506850.
+        """
+        if event.code == "ABS_Y":
+            # normalize between -1 and 1
+            self.state.left_joystick_y = event.state / self.MAX_JOY_VAL
+        elif event.code == "ABS_X":
+            # normalize between -1 and 1
+            self.state.left_joystick_x = event.state / self.MAX_JOY_VAL
+        elif event.code == "ABS_RY":
+            # normalize between -1 and 1
+            self.state.right_joystick_y = event.state / self.MAX_JOY_VAL
+        elif event.code == "ABS_RX":
+            # normalize between -1 and 1
+            self.state.right_joystick_x = event.state / self.MAX_JOY_VAL
+        elif event.code == "ABS_Z":
+            # normalize between 0 and 1
+            self.state.left_trigger = event.state / self.MAX_TRIG_VAL
+        elif event.code == "ABS_RZ":
+            # normalize between 0 and 1
+            self.state.right_trigger = event.state / self.MAX_TRIG_VAL
+        elif event.code == "BTN_TL":
+            self.state.left_bumper = event.state
+        elif event.code == "BTN_TR":
+            self.state.right_bumper = event.state
+        elif event.code == "BTN_SOUTH":
+            self.state.a = event.state
+        elif event.code == "BTN_NORTH":
+            # previously switched with X
+            self.state.y = event.state
+        elif event.code == "BTN_WEST":
+            # previously switched with Y
+            self.state.x = event.state
+        elif event.code == "BTN_EAST":
+            self.state.b = event.state
+        elif event.code == "BTN_THUMBL":
+            self.state.left_thumb = event.state
+        elif event.code == "BTN_THUMBR":
+            self.state.right_thumb = event.state
+        elif event.code == "BTN_SELECT":
+            self.state.back = event.state
+        elif event.code == "BTN_START":
+            self.state.start = event.state
+        elif event.code == "BTN_TRIGGER_HAPPY1":
+            self.state.left_dir_pad = event.state
+        elif event.code == "BTN_TRIGGER_HAPPY2":
+            self.state.right_dir_pad = event.state
+        elif event.code == "BTN_TRIGGER_HAPPY3":
+            self.state.up_dir_pad = event.state
+        elif event.code == "BTN_TRIGGER_HAPPY4":
+            self.state.down_dir_pad = event.state
+
+    def _post_event(
+        self, type: Type[JoystickEvent], **kwargs  # pylint: disable=redefined-builtin
+    ):
+        get_bus().post(
+            type(device=dict(JoystickDeviceSchema().dump(self.device)), **kwargs)
+        )
+
+    def _on_connect(self):
+        if self._connected_state != ConnectedState.CONNECTED:
+            self._connected_state = ConnectedState.CONNECTED
+            self._post_event(JoystickConnectedEvent)
+
+    def _on_disconnect(self):
+        if self._connected_state != ConnectedState.DISCONNECTED:
+            self._connected_state = ConnectedState.DISCONNECTED
+            self._post_event(JoystickDisconnectedEvent)
+
+    def _loop(self):
+        try:
+            for event in self.device.read():
+                self._on_connect()
+                prev_state = asdict(self.state)
+                self._parse_event(event)
+                new_state = asdict(self.state)
+
+                if prev_state != new_state:
+                    self._enqueue_state()
+        except (UnpluggedError, OSError):
+            self._on_disconnect()
+        finally:
+            self.wait_stop(self.poll_interval)
+
+    def run(self):
+        try:
+            while not self.should_stop:
+                try:
+                    self._loop()
+                except KeyboardInterrupt:
+                    break
+        finally:
+            self._on_disconnect()
+
+    def stop(self):
+        self._stop_evt.set()
+        self._stop_state_throrttler()
diff --git a/platypush/plugins/joystick/_state.py b/platypush/plugins/joystick/_state.py
new file mode 100644
index 000000000..0681307e1
--- /dev/null
+++ b/platypush/plugins/joystick/_state.py
@@ -0,0 +1,50 @@
+from dataclasses import dataclass
+from enum import Enum
+
+
+class ConnectedState(Enum):
+    """
+    Enum to represent the connection state of a joystick.
+    """
+
+    UNKNOWN = 0
+    CONNECTED = 1
+    DISCONNECTED = 2
+
+
+@dataclass
+class JoystickState:
+    """
+    Dataclass that holds the joystick state properties.
+    """
+
+    left_joystick_y: float = 0
+    left_joystick_x: float = 0
+    right_joystick_y: float = 0
+    right_joystick_x: float = 0
+    left_trigger: float = 0
+    right_trigger: float = 0
+    left_bumper: int = 0
+    right_bumper: int = 0
+    a: int = 0
+    x: int = 0
+    y: int = 0
+    b: int = 0
+    left_thumb: int = 0
+    right_thumb: int = 0
+    back: int = 0
+    start: int = 0
+    left_dir_pad: int = 0
+    right_dir_pad: int = 0
+    up_dir_pad: int = 0
+    down_dir_pad: int = 0
+
+
+@dataclass
+class JoystickDeviceState:
+    """
+    Dataclass that holds the joystick device state properties.
+    """
+
+    device: str
+    state: JoystickState
diff --git a/platypush/backend/joystick/manifest.yaml b/platypush/plugins/joystick/manifest.yaml
similarity index 83%
rename from platypush/backend/joystick/manifest.yaml
rename to platypush/plugins/joystick/manifest.yaml
index c3fb77fe5..a5ce406c3 100644
--- a/platypush/backend/joystick/manifest.yaml
+++ b/platypush/plugins/joystick/manifest.yaml
@@ -1,8 +1,6 @@
 manifest:
   events:
     platypush.message.event.joystick.JoystickEvent: when a new joystick event is received
-  install:
-    pip:
-    - inputs
+  install: {}
   package: platypush.backend.joystick
   type: backend
diff --git a/platypush/plugins/media/chromecast/__init__.py b/platypush/plugins/media/chromecast/__init__.py
index 5c900760c..63eeeff02 100644
--- a/platypush/plugins/media/chromecast/__init__.py
+++ b/platypush/plugins/media/chromecast/__init__.py
@@ -287,6 +287,9 @@ class MediaChromecastPlugin(MediaPlugin, RunnablePlugin):
     @action
     def stop(self, *_, chromecast: Optional[str] = None, **__):
         chromecast = chromecast or self.chromecast
+        if not chromecast:
+            return
+
         cast = self.get_chromecast(chromecast)
         cast.media_controller.stop()
         cast.wait()
diff --git a/platypush/schemas/joystick.py b/platypush/schemas/joystick.py
new file mode 100644
index 000000000..4b0e4c81b
--- /dev/null
+++ b/platypush/schemas/joystick.py
@@ -0,0 +1,199 @@
+from marshmallow import EXCLUDE, fields
+from marshmallow.schema import Schema
+
+
+class JoystickStateSchema(Schema):
+    """
+    Joystick state schema.
+    """
+
+    left_joystick_y = fields.Float(
+        metadata={
+            'description': 'Left joystick Y axis value',
+            'example': 0.5,
+        }
+    )
+
+    left_joystick_x = fields.Float(
+        metadata={
+            'description': 'Left joystick X axis value',
+            'example': 0.5,
+        }
+    )
+
+    right_joystick_y = fields.Float(
+        metadata={
+            'description': 'Right joystick Y axis value',
+            'example': 0.5,
+        }
+    )
+
+    right_joystick_x = fields.Float(
+        metadata={
+            'description': 'Right joystick X axis value',
+            'example': 0.5,
+        }
+    )
+
+    left_trigger = fields.Float(
+        metadata={
+            'description': 'Left trigger value',
+            'example': 0.5,
+        }
+    )
+
+    right_trigger = fields.Float(
+        metadata={
+            'description': 'Right trigger value',
+            'example': 0.5,
+        }
+    )
+
+    left_bumper = fields.Integer(
+        metadata={
+            'description': 'Left bumper state',
+            'example': 1,
+        }
+    )
+
+    right_bumper = fields.Integer(
+        metadata={
+            'description': 'Right bumper state',
+            'example': 1,
+        }
+    )
+
+    a = fields.Integer(
+        metadata={
+            'description': 'A button state',
+            'example': 1,
+        }
+    )
+
+    x = fields.Integer(
+        metadata={
+            'description': 'X button state',
+            'example': 1,
+        }
+    )
+
+    y = fields.Integer(
+        metadata={
+            'description': 'Y button state',
+            'example': 1,
+        }
+    )
+
+    b = fields.Integer(
+        metadata={
+            'description': 'B button state',
+            'example': 1,
+        }
+    )
+
+    left_thumb = fields.Integer(
+        metadata={
+            'description': 'Left thumb button state',
+            'example': 1,
+        }
+    )
+
+    right_thumb = fields.Integer(
+        metadata={
+            'description': 'Right thumb button state',
+            'example': 1,
+        }
+    )
+
+    back = fields.Integer(
+        metadata={
+            'description': 'Back button state',
+            'example': 1,
+        }
+    )
+
+    start = fields.Integer(
+        metadata={
+            'description': 'Start button state',
+            'example': 1,
+        }
+    )
+
+    left_dir_pad = fields.Integer(
+        metadata={
+            'description': 'Left direction pad button state',
+            'example': 1,
+        }
+    )
+
+    right_dir_pad = fields.Integer(
+        metadata={
+            'description': 'Right direction pad button state',
+            'example': 1,
+        }
+    )
+
+    up_dir_pad = fields.Integer(
+        metadata={
+            'description': 'Up direction pad button state',
+            'example': 1,
+        }
+    )
+
+    down_dir_pad = fields.Integer(
+        metadata={
+            'description': 'Down direction pad button state',
+            'example': 1,
+        }
+    )
+
+
+class JoystickDeviceSchema(Schema):
+    """
+    Joystick device schema.
+    """
+
+    class Meta:
+        """
+        Meta class.
+        """
+
+        unknown = EXCLUDE
+
+    name = fields.String(
+        metadata={
+            'description': 'Joystick name',
+            'example': 'Xbox 360 Controller',
+        }
+    )
+
+    path = fields.Function(
+        lambda obj: obj.get_char_device_path(),
+        metadata={
+            'description': 'Joystick character device path',
+            'example': '/dev/input/event0',
+        },
+    )
+
+    number = fields.Integer(
+        metadata={
+            'description': 'Joystick number',
+            'example': 0,
+        }
+    )
+
+    protocol = fields.String(
+        metadata={
+            'description': 'Joystick protocol',
+            'example': 'usb',
+        }
+    )
+
+
+class JoystickStatusSchema(Schema):
+    """
+    Joystick status schema.
+    """
+
+    device = fields.Nested(JoystickDeviceSchema, required=True)
+    state = fields.Nested(JoystickStateSchema, required=True)