Exemple #1
0
    def __init__(self):
        """Construct the helper and initialize its sockets."""
        self._results = Pipe('/tmp/key-mapper/results')
        self._commands = Pipe('/tmp/key-mapper/commands')

        self._send_groups()

        self.group = None
        self._pipe = multiprocessing.Pipe()
Exemple #2
0
    def test_pipe_duo(self):
        p1 = Pipe('/tmp/key-mapper-test/pipe')
        p2 = Pipe('/tmp/key-mapper-test/pipe')
        self.assertEqual(p2.recv(), None)

        p1.send(1)
        self.assertEqual(p2.recv(), 1)
        self.assertEqual(p2.recv(), None)

        p1.send(2)
        p1.send(3)
        self.assertEqual(p2.recv(), 2)
        self.assertEqual(p2.recv(), 3)
        self.assertEqual(p2.recv(), None)
Exemple #3
0
    def test_pipe_single(self):
        p1 = Pipe('/tmp/key-mapper-test/pipe')
        self.assertEqual(p1.recv(), None)

        p1.send(1)
        self.assertTrue(p1.poll())
        self.assertEqual(p1.recv(), 1)
        self.assertFalse(p1.poll())
        self.assertEqual(p1.recv(), None)

        p1.send(2)
        self.assertTrue(p1.poll())
        p1.send(3)
        self.assertTrue(p1.poll())
        self.assertEqual(p1.recv(), 2)
        self.assertTrue(p1.poll())
        self.assertEqual(p1.recv(), 3)
        self.assertFalse(p1.poll())
        self.assertEqual(p1.recv(), None)
Exemple #4
0
class RootHelper:
    """Client that runs as root and works for the GUI.

    Sends device information and keycodes to the GUIs socket.

    Commands are either numbers for generic commands,
    or strings to start listening on a specific device.
    """
    def __init__(self):
        """Construct the helper and initialize its sockets."""
        self._results = Pipe('/tmp/key-mapper/results')
        self._commands = Pipe('/tmp/key-mapper/commands')

        self._send_groups()

        self.group = None
        self._pipe = multiprocessing.Pipe()

    def run(self):
        """Start doing stuff. Blocks."""
        while True:
            self._handle_commands()
            self._start_reading()

    def _send_groups(self):
        """Send the groups to the gui."""
        self._results.send({
            'type': 'groups',
            'message': groups.dumps()
        })

    def _handle_commands(self):
        """Handle all unread commands."""
        # wait for something to do
        select.select([self._commands], [], [])

        while self._commands.poll():
            cmd = self._commands.recv()
            logger.debug('Received command "%s"', cmd)

            if cmd == TERMINATE:
                logger.debug('Helper terminates')
                sys.exit(0)

            if cmd == REFRESH_GROUPS:
                groups.refresh()
                self._send_groups()
                continue

            group = groups.find(key=cmd)
            if group is None:
                groups.refresh()
                group = groups.find(key=cmd)

            if group is not None:
                self.group = group
                continue

            logger.error('Received unknown command "%s"', cmd)

    def _start_reading(self):
        """Tell the evdev lib to start looking for keycodes.

        If read is called without prior start_reading, no keycodes
        will be available.

        This blocks forever until it discovers a new command on the socket.
        """
        rlist = {}

        if self.group is None:
            logger.error('group is None')
            return

        virtual_devices = []
        # Watch over each one of the potentially multiple devices per
        # hardware
        for path in self.group.paths:
            try:
                device = evdev.InputDevice(path)
            except FileNotFoundError:
                continue

            if evdev.ecodes.EV_KEY in device.capabilities():
                virtual_devices.append(device)

        if len(virtual_devices) == 0:
            logger.debug('No interesting device for "%s"', self.group.key)
            return

        for device in virtual_devices:
            rlist[device.fd] = device

        logger.debug(
            'Starting reading keycodes from "%s"',
            '", "'.join([device.name for device in virtual_devices])
        )

        rlist[self._commands] = self._commands

        while True:
            ready_fds = select.select(rlist, [], [])
            if len(ready_fds[0]) == 0:
                # whatever, happens for sockets sometimes. Maybe the socket
                # is closed and select has nothing to select from?
                continue

            for fd in ready_fds[0]:
                if rlist[fd] == self._commands:
                    # all commands will cause the reader to start over
                    # (possibly for a different device).
                    # _handle_commands will check what is going on
                    return

                device = rlist[fd]

                try:
                    event = device.read_one()
                    if event:
                        self._send_event(event, device)
                except OSError:
                    logger.debug('Device "%s" disappeared', device.path)
                    return

    def _send_event(self, event, device):
        """Write the event into the pipe to the main process.

        Parameters
        ----------
        event : evdev.InputEvent
        device : evdev.InputDevice
        """
        # value: 1 for down, 0 for up, 2 for hold.
        if event.type == EV_KEY and event.value == 2:
            # ignore hold-down events
            return

        blacklisted_keys = [
            evdev.ecodes.BTN_TOOL_DOUBLETAP
        ]

        if event.type == EV_KEY and event.code in blacklisted_keys:
            return

        if event.type == EV_ABS:
            abs_range = utils.get_abs_range(device, event.code)
            event.value = utils.normalize_value(event, abs_range)
        else:
            event.value = utils.normalize_value(event)

        self._results.send({
            'type': 'event',
            'message': (
                event.sec, event.usec,
                event.type, event.code, event.value
            )
        })
Exemple #5
0
 def connect(self):
     """Connect to the helper."""
     self._results = Pipe('/tmp/key-mapper/results')
     self._commands = Pipe('/tmp/key-mapper/commands')
Exemple #6
0
class Reader:
    """Processes events from the helper for the GUI to use.

    Does not serve any purpose for the injection service.

    When a button was pressed, the newest keycode can be obtained from this
    object. GTK has get_key for keyboard keys, but Reader also
    has knowledge of buttons like the middle-mouse button.
    """
    def __init__(self):
        self.previous_event = None
        self.previous_result = None
        self._unreleased = {}
        self._debounce_remove = {}
        self._devices_updated = False
        self._cleared_at = 0
        self.group = None

        self._results = None
        self._commands = None
        self.connect()

    def connect(self):
        """Connect to the helper."""
        self._results = Pipe('/tmp/key-mapper/results')
        self._commands = Pipe('/tmp/key-mapper/commands')

    def are_new_devices_available(self):
        """Check if groups contains new devices.

        The ui should then update its list.
        """
        outdated = self._devices_updated
        self._devices_updated = False  # assume the ui will react accordingly
        return outdated

    def _get_event(self, message):
        """Return an InputEvent if the message contains one. None otherwise."""
        message_type = message['type']
        message_body = message['message']

        if message_type == 'groups':
            if message_body != groups.dumps():
                groups.loads(message_body)
                logger.debug('Received %d devices', len(groups))
                self._devices_updated = True
            return None

        if message_type == 'event':
            return evdev.InputEvent(*message_body)

        logger.error('Received unknown message "%s"', message)
        return None

    def read(self):
        """Get the newest key/combination as Key object.

        Only reports keys from down-events.

        On key-down events the pipe returns changed combinations. Release
        events won't cause that and the reader will return None as in
        "nothing new to report". So In order to change a combination, one
        of its keys has to be released and then a different one pressed.

        Otherwise making combinations wouldn't be possible. Because at
        some point the keys have to be released, and that shouldn't cause
        the combination to get trimmed.
        """
        # this is in some ways similar to the keycode_mapper and
        # event_producer, but its much simpler because it doesn't
        # have to trigger anything, manage any macros and only
        # reports key-down events. This function is called periodically
        # by the window.

        # remember the previous down-event from the pipe in order to
        # be able to tell if the reader should return the updated combination
        previous_event = self.previous_event
        key_down_received = False

        self._debounce_tick()

        while self._results.poll():
            message = self._results.recv()
            event = self._get_event(message)
            if event is None:
                continue

            gamepad = GAMEPAD in self.group.types
            if not utils.should_map_as_btn(event, custom_mapping, gamepad):
                continue

            event_tuple = (event.type, event.code, event.value)

            type_code = (event.type, event.code)

            if event.value == 0:
                logger.key_spam(event_tuple, 'release')
                self._release(type_code)
                continue

            if self._unreleased.get(type_code) == event_tuple:
                logger.key_spam(event_tuple, 'duplicate key down')
                self._debounce_start(event_tuple)
                continue

            # to keep track of combinations.
            # "I have got this release event, what was this for?" A release
            # event for a D-Pad axis might be any direction, hence this maps
            # from release to input in order to remember it. Since all release
            # events have value 0, the value is not used in the key.
            key_down_received = True
            logger.key_spam(event_tuple, 'down')
            self._unreleased[type_code] = event_tuple
            self._debounce_start(event_tuple)
            previous_event = event

        if not key_down_received:
            # This prevents writing a subset of the combination into
            # result after keys were released. In order to control the gui,
            # they have to be released.
            return None

        self.previous_event = previous_event

        if len(self._unreleased) > 0:
            result = Key(*self._unreleased.values())
            if result == self.previous_result:
                # don't return the same stuff twice
                return None

            self.previous_result = result
            logger.key_spam(result.keys, 'read result')

            return result

        return None

    def start_reading(self, group):
        """Start reading keycodes for a device."""
        logger.debug('Sending start msg to helper for "%s"', group.key)
        self._commands.send(group.key)
        self.group = group
        self.clear()

    def terminate(self):
        """Stop reading keycodes for good."""
        logger.debug('Sending close msg to helper')
        self._commands.send(TERMINATE)

    def refresh_groups(self):
        """Ask the helper for new device groups."""
        self._commands.send(REFRESH_GROUPS)

    def clear(self):
        """Next time when reading don't return the previous keycode."""
        logger.debug('Clearing reader')
        while self._results.poll():
            # clear the results pipe and handle any non-event messages,
            # otherwise a 'groups' message might get lost
            message = self._results.recv()
            self._get_event(message)

        self._unreleased = {}
        self.previous_event = None
        self.previous_result = None

    def get_unreleased_keys(self):
        """Get a Key object of the current keyboard state."""
        unreleased = list(self._unreleased.values())

        if len(unreleased) == 0:
            return None

        return Key(*unreleased)

    def _release(self, type_code):
        """Modify the state to recognize the releasing of the key."""
        if type_code in self._unreleased:
            del self._unreleased[type_code]
        if type_code in self._debounce_remove:
            del self._debounce_remove[type_code]

    def _debounce_start(self, event_tuple):
        """Act like the key was released if no new event arrives in time."""
        if not will_report_up(event_tuple[0]):
            self._debounce_remove[event_tuple[:2]] = DEBOUNCE_TICKS

    def _debounce_tick(self):
        """If the counter reaches 0, the key is not considered held down."""
        for type_code in list(self._debounce_remove.keys()):
            if type_code not in self._unreleased:
                continue

            # clear wheel events from unreleased after some time
            if self._debounce_remove[type_code] == 0:
                logger.key_spam(self._unreleased[type_code],
                                'Considered as released')
                self._release(type_code)
            else:
                self._debounce_remove[type_code] -= 1

    def __del__(self):
        self.terminate()