Added generic joystick.jstest
backend
This commit is contained in:
parent
9eab526e47
commit
f296f4b161
6 changed files with 349 additions and 3 deletions
|
@ -16,6 +16,10 @@ Given the high speed of development in the first phase, changes are being report
|
|||
|
||||
- Support for custom MQTT timeout on all the `zwavejs2mqtt` calls.
|
||||
|
||||
- Added generic joystick backend `backend.joystick.jstest` which uses `jstest` from the
|
||||
standard `joystick` system package to read the state of joysticks not compatible with
|
||||
`python-inputs`.
|
||||
|
||||
### Changed
|
||||
|
||||
- `switch.switchbot` plugin renamed to `switchbot.bluetooth` plugin, while the new plugin
|
||||
|
|
|
@ -32,6 +32,7 @@ Backends
|
|||
platypush/backend/http.poll.rst
|
||||
platypush/backend/inotify.rst
|
||||
platypush/backend/joystick.rst
|
||||
platypush/backend/joystick.jstest.rst
|
||||
platypush/backend/kafka.rst
|
||||
platypush/backend/light.hue.rst
|
||||
platypush/backend/linode.rst
|
||||
|
|
5
docs/source/platypush/backend/joystick.jstest.rst
Normal file
5
docs/source/platypush/backend/joystick.jstest.rst
Normal file
|
@ -0,0 +1,5 @@
|
|||
``platypush.backend.joystick.jstest``
|
||||
=====================================
|
||||
|
||||
.. automodule:: platypush.backend.joystick.jstest
|
||||
:members:
|
262
platypush/backend/joystick/jstest.py
Normal file
262
platypush/backend/joystick/jstest.py
Normal file
|
@ -0,0 +1,262 @@
|
|||
import json
|
||||
import os
|
||||
import re
|
||||
import subprocess
|
||||
import time
|
||||
from typing import Optional, List
|
||||
|
||||
from platypush.backend.joystick import JoystickBackend
|
||||
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:
|
||||
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(JoystickBackend):
|
||||
"""
|
||||
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.
|
||||
|
||||
Instructions on Debian-based distros::
|
||||
|
||||
# apt-get install joystick
|
||||
|
||||
Instructions on Arch-based distros::
|
||||
|
||||
# pacman -S joyutils
|
||||
|
||||
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]
|
||||
|
||||
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.JoystickStateEvent` when the state of the joystick (i.e. some of its
|
||||
axes or buttons values) changes.
|
||||
* :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.
|
||||
|
||||
"""
|
||||
|
||||
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__(**kwargs)
|
||||
|
||||
self.device_path = 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_path}')
|
||||
|
||||
while not self.should_stop() or not os.path.exists(self.device_path):
|
||||
time.sleep(1)
|
||||
|
||||
if self.should_stop():
|
||||
return
|
||||
|
||||
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 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 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 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 not self.should_stop():
|
||||
ch = self._process.stdout.read(1).decode()
|
||||
if not ch:
|
||||
continue
|
||||
|
||||
line += ch
|
||||
if line.endswith('Buttons: '):
|
||||
break
|
||||
|
||||
while 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 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 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('buttons', {}).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_path],
|
||||
stdout=subprocess.PIPE) as self._process:
|
||||
self.logger.info('Device opened')
|
||||
self._initialize()
|
||||
|
||||
for state in self._read_states():
|
||||
self._process_state(state)
|
||||
|
||||
if not os.path.exists(self.device_path):
|
||||
self.logger.warning(f'Connection to {self.device_path} lost')
|
||||
self.bus.post(JoystickDisconnectedEvent(self.device_path))
|
||||
break
|
||||
finally:
|
||||
self._process = None
|
||||
|
||||
|
||||
# vim:sw=4:ts=4:et:
|
|
@ -1,9 +1,12 @@
|
|||
from abc import ABC
|
||||
from typing import List
|
||||
|
||||
from platypush.message.event import Event
|
||||
|
||||
|
||||
class JoystickEvent(Event):
|
||||
"""
|
||||
Event triggered upon joystick event
|
||||
Generic joystick event.
|
||||
"""
|
||||
|
||||
def __init__(self, code, state, *args, **kwargs):
|
||||
|
@ -11,12 +14,83 @@ class JoystickEvent(Event):
|
|||
: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.
|
||||
: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)
|
||||
|
||||
|
||||
# vim:sw=4:ts=4:et:
|
||||
class _JoystickEvent(Event, ABC):
|
||||
"""
|
||||
Base joystick event class.
|
||||
"""
|
||||
|
||||
def __init__(self, device: str, **kwargs):
|
||||
super().__init__(device=device, **kwargs)
|
||||
|
||||
|
||||
class JoystickConnectedEvent(_JoystickEvent):
|
||||
"""
|
||||
Event triggered upon joystick connection.
|
||||
"""
|
||||
|
||||
|
||||
class JoystickDisconnectedEvent(_JoystickEvent):
|
||||
"""
|
||||
Event triggered upon joystick disconnection.
|
||||
"""
|
||||
|
||||
|
||||
class JoystickStateEvent(_JoystickEvent):
|
||||
"""
|
||||
Event triggered when the state of the joystick changes.
|
||||
"""
|
||||
|
||||
def __init__(self, *args, axes: List[int], buttons: List[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).
|
||||
"""
|
||||
super().__init__(*args, axes=axes, buttons=buttons, **kwargs)
|
||||
|
||||
|
||||
class JoystickButtonPressedEvent(_JoystickEvent):
|
||||
"""
|
||||
Event triggered when a joystick button is pressed.
|
||||
"""
|
||||
|
||||
def __init__(self, *args, button: int, **kwargs):
|
||||
"""
|
||||
:param button: Button index.
|
||||
"""
|
||||
super().__init__(*args, button=button, **kwargs)
|
||||
|
||||
|
||||
class JoystickButtonReleasedEvent(_JoystickEvent):
|
||||
"""
|
||||
Event triggered when a joystick button is released.
|
||||
"""
|
||||
|
||||
def __init__(self, *args, button: int, **kwargs):
|
||||
"""
|
||||
:param button: Button index.
|
||||
"""
|
||||
super().__init__(*args, button=button, **kwargs)
|
||||
|
||||
|
||||
class JoystickAxisEvent(_JoystickEvent):
|
||||
"""
|
||||
Event triggered when an axis value of the joystick changes.
|
||||
"""
|
||||
|
||||
def __init__(self, *args, axis: int, value: int, **kwargs):
|
||||
"""
|
||||
:param axis: Axis index.
|
||||
:param value: Axis value.
|
||||
"""
|
||||
super().__init__(*args, axis=axis, value=value, **kwargs)
|
||||
|
||||
|
||||
# vim:sw=4:ts=4:et:
|
||||
|
|
Loading…
Reference in a new issue