From 57722fce2a946c31549661b9a2ab2dcdc2a770c0 Mon Sep 17 00:00:00 2001 From: Fabio Manganiello Date: Wed, 23 May 2018 17:07:06 +0200 Subject: [PATCH] Added support for Leap Motion device events --- platypush/backend/__init__.py | 3 +- platypush/backend/sensor/leap.py | 167 +++++++++++++++++++++++++ platypush/message/event/sensor/leap.py | 30 +++++ setup.py | 1 + 4 files changed, 199 insertions(+), 2 deletions(-) create mode 100644 platypush/backend/sensor/leap.py create mode 100644 platypush/message/event/sensor/leap.py diff --git a/platypush/backend/__init__.py b/platypush/backend/__init__.py index 2fb0a66a..43122e04 100644 --- a/platypush/backend/__init__.py +++ b/platypush/backend/__init__.py @@ -190,8 +190,7 @@ class Backend(Thread): Param: msg -- The message """ - - raise NotImplementedError("send_message should be implemented in a derived class") + pass def run(self): diff --git a/platypush/backend/sensor/leap.py b/platypush/backend/sensor/leap.py new file mode 100644 index 00000000..2f8773d7 --- /dev/null +++ b/platypush/backend/sensor/leap.py @@ -0,0 +1,167 @@ +import logging +import time + +import Leap + +from platypush.backend import Backend +from platypush.context import get_backend +from platypush.message.event.sensor.leap import LeapFrameEvent, \ + LeapFrameStartEvent, LeapFrameStopEvent, LeapConnectEvent, LeapDisconnectEvent + + +class SensorLeapBackend(Backend): + """ + Backend for events generated using a Leap Motion device to track hands and + gestures, https://www.leapmotion.com/ + + Note that the default SDK is not compatible with Python 3. Follow the + instructions on https://github.com/BlackLight/leap-sdk-python3 to build the + Python 3 module. + + Also, you'll need the Leap driver and utils installed on your OS (follow + instructions at https://www.leapmotion.com/setup/) and the `leapd` daemon + running to recognize your controller. + """ + + def __init__(self, + position_ranges=[ + [-300.0, 300.0], # x axis + [25.0, 600.0], # y axis + [-300.0, 300.0], # z axis + ], + position_tolerance=0.2, # Position variation tolerance in % + *args, **kwargs): + super().__init__(*args, **kwargs) + + self.position_ranges = position_ranges + self.position_tolerance = position_tolerance + + + def run(self): + super().run() + + listener = LeapListener(position_ranges=self.position_ranges, + position_tolerance=self.position_tolerance) + + controller = Leap.Controller() + + if not controller: + raise RuntimeError('No Leap Motion controller found - is your ' + + 'device connected and is leapd running?') + + controller.add_listener(listener) + logging.info('Leap Motion backend initialized') + + try: + while not self.should_stop(): + time.sleep(0.1) + finally: + controller.remove_listener(listener) + + + +class LeapListener(Leap.Listener): + def __init__(self, position_ranges, position_tolerance, *args, **kwargs): + super().__init__(*args, **kwargs) + self.prev_frame = None + self.position_ranges = position_ranges + self.position_tolerance = position_tolerance + + + def send_event(self, event): + backend = get_backend('redis') + if not backend: + logging.warning('Redis backend not configured, I cannot propagate the following event: {}'.format(event)) + return + + backend.send_message(event) + + + def on_init(self, controller): + self.prev_frame = None + logging.info('Leap controller listener initialized') + + def on_connect(self, controller): + logging.info('Leap controller connected') + self.prev_frame = None + self.send_event(LeapConnectEvent()) + + def on_disconnect(self, controller): + logging.info('Leap controller disconnected') + self.prev_frame = None + self.send_event(LeapDisconnectEvent()) + + def on_exit(self, controller): + logging.info('Leap listener terminated') + + def on_frame(self, controller): + frame = controller.frame() + + if len(frame.hands) > 0: + hands = self._flatten_hands(frame) + if hands: + if not self.prev_frame: + self.send_event(LeapFrameStartEvent()) + self.send_event(LeapFrameEvent(hands=hands)) + self.prev_frame = frame + else: + if self.prev_frame: + self.send_event(LeapFrameStopEvent()) + self.prev_frame = None + + def _flatten_hands(self, frame): + return [ + { + 'confidence': hand.confidence, + 'direction': [hand.direction[0], hand.direction[1], hand.direction[2]], + 'id': hand.id, + 'is_left': hand.is_left, + 'is_right': hand.is_right, + 'palm_normal': [hand.palm_normal[0], hand.palm_normal[1], hand.palm_normal[2]], + 'palm_position': self._normalize_position(hand.palm_position), + 'palm_velocity': [hand.palm_velocity[0], hand.palm_velocity[1], hand.palm_velocity[2]], + 'palm_width': hand.palm_width, + 'sphere_center': [hand.sphere_center[0], hand.sphere_center[1], hand.sphere_center[2]], + 'sphere_radius': hand.sphere_radius, + 'stabilized_palm_position': self._normalize_position(hand.stabilized_palm_position), + 'time_visible': hand.time_visible, + 'wrist_position': self._normalize_position(hand.wrist_position), + } + for i, hand in enumerate(frame.hands) + if hand.is_valid and ( + len(frame.hands) != len(self.prev_frame.hands) or + self._position_changed( + old_position=self.prev_frame.hands[i].stabilized_palm_position, + new_position=hand.stabilized_palm_position) + + if self.prev_frame + else True + ) + ] + + def _normalize_position(self, position): + # Normalize absolute position onto a semisphere centered in (0,0) + # having x_range = z_range = [-100, 100], y_range = [0, 100] + + return [ + self._scale_scalar(value=position[0], range=self.position_ranges[0], new_range=[-100.0, 100.0]), + self._scale_scalar(value=position[1], range=self.position_ranges[1], new_range=[0.0, 100.0]), + self._scale_scalar(value=position[2], range=self.position_ranges[2], new_range=[-100.0, 100.0]), + ] + + + def _scale_scalar(self, value, range, new_range): + if value < range[0]: value=range[0] + if value > range[1]: value=range[1] + return ((new_range[1]-new_range[0])/(range[1]-range[0]))*(value-range[0]) + new_range[0] + + + def _position_changed(self, old_position, new_position): + return ( + abs(old_position[0]-new_position[0]) > self.position_tolerance or + abs(old_position[1]-new_position[1]) > self.position_tolerance or + abs(old_position[2]-new_position[2]) > self.position_tolerance) + + +# vim:sw=4:ts=4:et: + diff --git a/platypush/message/event/sensor/leap.py b/platypush/message/event/sensor/leap.py new file mode 100644 index 00000000..c3ddf56e --- /dev/null +++ b/platypush/message/event/sensor/leap.py @@ -0,0 +1,30 @@ +from platypush.message.event import Event + + +class LeapFrameEvent(Event): + def __init__(self, hands, *args, **kwargs): + super().__init__(hands=hands, *args, **kwargs) + + +class LeapFrameStartEvent(Event): + def __init__(self, *args, **kwargs): + super().__init__(*args, **kwargs) + + +class LeapFrameStopEvent(Event): + def __init__(self, *args, **kwargs): + super().__init__(*args, **kwargs) + + +class LeapConnectEvent(Event): + def __init__(self, *args, **kwargs): + super().__init__(*args, **kwargs) + + +class LeapDisconnectEvent(Event): + def __init__(self, *args, **kwargs): + super().__init__(*args, **kwargs) + + +# vim:sw=4:ts=4:et: + diff --git a/setup.py b/setup.py index 396ec771..264cd3d4 100755 --- a/setup.py +++ b/setup.py @@ -83,6 +83,7 @@ setup( 'Support for custom hotword detection': ['snowboy'], 'Support for GPIO pins access': ['RPi.GPIO'], 'Support for MCP3008 analog-to-digital converter plugin': ['adafruit-mcp3008'], + # 'Support for Leap Motion backend': ['git+ssh://git@github.com:BlackLight/leap-sdk-python3.git', 'redis'], # 'Support for Flic buttons': ['git+ssh://git@github.com/50ButtonsEach/fliclib-linux-hci'] }, )