From fc1d9ad3e61b31549abbe5e5fb0c4c49f2f07c41 Mon Sep 17 00:00:00 2001 From: Fabio Manganiello Date: Mon, 17 May 2021 14:51:53 +0200 Subject: [PATCH] Added joystick.linux backend --- CHANGELOG.md | 3 + docs/source/backends.rst | 1 + .../platypush/backend/joystick.linux.rst | 5 + docs/source/platypush/plugins/pwm.pca9685.rst | 5 + docs/source/plugins.rst | 1 + platypush/backend/joystick/jstest.py | 4 + platypush/backend/joystick/linux.py | 186 ++++++++++++++++++ platypush/message/event/joystick.py | 28 ++- 8 files changed, 225 insertions(+), 8 deletions(-) create mode 100644 docs/source/platypush/backend/joystick.linux.rst create mode 100644 docs/source/platypush/plugins/pwm.pca9685.rst create mode 100644 platypush/backend/joystick/linux.py diff --git a/CHANGELOG.md b/CHANGELOG.md index f3f5ba48..8fb102dc 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -22,6 +22,9 @@ Given the high speed of development in the first phase, changes are being report - Added PWM PCA9685 plugin. +- Added Linux native joystick plugin, ``backend.joystick.linux``, for the cases where + ``python-inputs`` doesn't work and ``jstest`` is too slow. + ### Changed - `switch.switchbot` plugin renamed to `switchbot.bluetooth` plugin, while the new plugin diff --git a/docs/source/backends.rst b/docs/source/backends.rst index df729be9..f9f9edb9 100644 --- a/docs/source/backends.rst +++ b/docs/source/backends.rst @@ -33,6 +33,7 @@ Backends platypush/backend/inotify.rst platypush/backend/joystick.rst platypush/backend/joystick.jstest.rst + platypush/backend/joystick.linux.rst platypush/backend/kafka.rst platypush/backend/light.hue.rst platypush/backend/linode.rst diff --git a/docs/source/platypush/backend/joystick.linux.rst b/docs/source/platypush/backend/joystick.linux.rst new file mode 100644 index 00000000..b4f2a638 --- /dev/null +++ b/docs/source/platypush/backend/joystick.linux.rst @@ -0,0 +1,5 @@ +``platypush.backend.joystick.linux`` +==================================== + +.. automodule:: platypush.backend.joystick.linux + :members: diff --git a/docs/source/platypush/plugins/pwm.pca9685.rst b/docs/source/platypush/plugins/pwm.pca9685.rst new file mode 100644 index 00000000..7a054831 --- /dev/null +++ b/docs/source/platypush/plugins/pwm.pca9685.rst @@ -0,0 +1,5 @@ +``platypush.plugins.pwm.pca9685`` +================================= + +.. automodule:: platypush.plugins.pwm.pca9685 + :members: diff --git a/docs/source/plugins.rst b/docs/source/plugins.rst index 5af5c7d1..033c869e 100644 --- a/docs/source/plugins.rst +++ b/docs/source/plugins.rst @@ -104,6 +104,7 @@ Plugins platypush/plugins/ping.rst platypush/plugins/printer.cups.rst platypush/plugins/pushbullet.rst + platypush/plugins/pwm.pca9685.rst platypush/plugins/qrcode.rst platypush/plugins/redis.rst platypush/plugins/rtorrent.rst diff --git a/platypush/backend/joystick/jstest.py b/platypush/backend/joystick/jstest.py index 7ec87443..e7d3547e 100644 --- a/platypush/backend/joystick/jstest.py +++ b/platypush/backend/joystick/jstest.py @@ -51,6 +51,10 @@ class JoystickJstestBackend(Backend): 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`. + Instructions on Debian-based distros:: # apt-get install joystick diff --git a/platypush/backend/joystick/linux.py b/platypush/backend/joystick/linux.py new file mode 100644 index 00000000..f7155bb7 --- /dev/null +++ b/platypush/backend/joystick/linux.py @@ -0,0 +1,186 @@ +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 `_ to interact with + the devices. + + Triggers: + + * :class:`platypush.message.event.joystick.JoystickConnectedEvent` when the joystick is connected. + * :class:`platypush.message.event.joystick.JoystickDisconnectedEvent` when the joystick is disconnected. + * :class:`platypush.message.event.joystick.JoystickButtonPressedEvent` when a joystick button is pressed. + * :class:`platypush.message.event.joystick.JoystickButtonReleasedEvent` when a joystick button is released. + * :class:`platypush.message.event.joystick.JoystickAxisEvent` when an axis value of the joystick changes. + + """ + + # 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() + + while not self.should_stop(): + # Open the joystick device. + self.logger.info(f'Opening {self.device}...') + try: + jsdev = open(self.device, 'rb') + self._init_joystick(jsdev) + except Exception as e: + self.logger.debug(f'Joystick device on {self.device} not available: {e}') + time.sleep(5) + continue + + # 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 & 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(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(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 diff --git a/platypush/message/event/joystick.py b/platypush/message/event/joystick.py index 08e37a54..472049b3 100644 --- a/platypush/message/event/joystick.py +++ b/platypush/message/event/joystick.py @@ -1,5 +1,5 @@ from abc import ABC -from typing import List +from typing import Optional, Iterable, Union from platypush.message.event import Event @@ -35,6 +35,18 @@ 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): @@ -48,7 +60,7 @@ class JoystickStateEvent(_JoystickEvent): Event triggered when the state of the joystick changes. """ - def __init__(self, *args, axes: List[int], buttons: List[bool], **kwargs): + def __init__(self, *args, axes: Iterable[int], buttons: Iterable[bool], **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). @@ -61,9 +73,9 @@ class JoystickButtonPressedEvent(_JoystickEvent): Event triggered when a joystick button is pressed. """ - def __init__(self, *args, button: int, **kwargs): + def __init__(self, *args, button: Union[int, str], **kwargs): """ - :param button: Button index. + :param button: Button index or name. """ super().__init__(*args, button=button, **kwargs) @@ -73,9 +85,9 @@ class JoystickButtonReleasedEvent(_JoystickEvent): Event triggered when a joystick button is released. """ - def __init__(self, *args, button: int, **kwargs): + def __init__(self, *args, button: Union[int, str], **kwargs): """ - :param button: Button index. + :param button: Button index or name. """ super().__init__(*args, button=button, **kwargs) @@ -85,9 +97,9 @@ class JoystickAxisEvent(_JoystickEvent): Event triggered when an axis value of the joystick changes. """ - def __init__(self, *args, axis: int, value: int, **kwargs): + def __init__(self, *args, axis: Union[int, str], value: int, **kwargs): """ - :param axis: Axis index. + :param axis: Axis index or name. :param value: Axis value. """ super().__init__(*args, axis=axis, value=value, **kwargs)