1577 lines · 94.6 KB · Python
RawDownload
#!/bin/env python3# SPDX-License-Identifier: GPL-2.0# -*- coding: utf-8 -*-## Copyright (c) 2021 Benjamin Tissoires <benjamin.tissoires@gmail.com># Copyright (c) 2021 Red Hat, Inc.# from . import baseimport copyfrom enum import Enumfrom hidtools.util import BusTypefrom .base import HidBpfimport libevdevimport loggingimport pytestfrom typing import Dict, List, Optional, Tuple logger = logging.getLogger("hidtools.test.tablet")  class BtnTouch(Enum):    """Represents whether the BTN_TOUCH event is set to True or False"""     DOWN = True    UP = False  class ToolType(Enum):    PEN = libevdev.EV_KEY.BTN_TOOL_PEN    RUBBER = libevdev.EV_KEY.BTN_TOOL_RUBBER  class BtnPressed(Enum):    """Represents whether a button is pressed on the stylus"""     PRIMARY_PRESSED = libevdev.EV_KEY.BTN_STYLUS    SECONDARY_PRESSED = libevdev.EV_KEY.BTN_STYLUS2    THIRD_PRESSED = libevdev.EV_KEY.BTN_STYLUS3  class PenState(Enum):    """Pen states according to Microsoft reference:    https://docs.microsoft.com/en-us/windows-hardware/design/component-guidelines/windows-pen-states     We extend it with the various buttons when we need to check them.    """     PEN_IS_OUT_OF_RANGE = BtnTouch.UP, None, False    PEN_IS_IN_RANGE = BtnTouch.UP, ToolType.PEN, False    PEN_IS_IN_RANGE_WITH_BUTTON = BtnTouch.UP, ToolType.PEN, True    PEN_IS_IN_CONTACT = BtnTouch.DOWN, ToolType.PEN, False    PEN_IS_IN_CONTACT_WITH_BUTTON = BtnTouch.DOWN, ToolType.PEN, True    PEN_IS_IN_RANGE_WITH_ERASING_INTENT = BtnTouch.UP, ToolType.RUBBER, False    PEN_IS_IN_RANGE_WITH_ERASING_INTENT_WITH_BUTTON = BtnTouch.UP, ToolType.RUBBER, True    PEN_IS_ERASING = BtnTouch.DOWN, ToolType.RUBBER, False    PEN_IS_ERASING_WITH_BUTTON = BtnTouch.DOWN, ToolType.RUBBER, True     def __init__(        self, touch: BtnTouch, tool: Optional[ToolType], button: Optional[bool]    ):        self.touch = touch  # type: ignore        self.tool = tool  # type: ignore        self.button = button  # type: ignore     @classmethod    def from_evdev(cls, evdev, test_button) -> "PenState":        touch = BtnTouch(evdev.value[libevdev.EV_KEY.BTN_TOUCH])        tool = None        button = False        if (            evdev.value[libevdev.EV_KEY.BTN_TOOL_RUBBER]            and not evdev.value[libevdev.EV_KEY.BTN_TOOL_PEN]        ):            tool = ToolType(libevdev.EV_KEY.BTN_TOOL_RUBBER)        elif (            evdev.value[libevdev.EV_KEY.BTN_TOOL_PEN]            and not evdev.value[libevdev.EV_KEY.BTN_TOOL_RUBBER]        ):            tool = ToolType(libevdev.EV_KEY.BTN_TOOL_PEN)        elif (            evdev.value[libevdev.EV_KEY.BTN_TOOL_PEN]            or evdev.value[libevdev.EV_KEY.BTN_TOOL_RUBBER]        ):            raise ValueError("2 tools are not allowed")         # we take only the provided button into account        if test_button is not None:            button = bool(evdev.value[test_button.value])         # the kernel tends to insert an EV_SYN once removing the tool, so        # the button will be released after        if tool is None:            button = False         return cls((touch, tool, button))  # type: ignore     def apply(        self, events: List[libevdev.InputEvent], strict: bool, test_button: BtnPressed    ) -> "PenState":        if libevdev.EV_SYN.SYN_REPORT in events:            raise ValueError("EV_SYN is in the event sequence")        touch = self.touch        touch_found = False        tool = self.tool        tool_found = False        button = self.button        button_found = False         for ev in events:            if ev == libevdev.InputEvent(libevdev.EV_KEY.BTN_TOUCH):                if touch_found:                    raise ValueError(f"duplicated BTN_TOUCH in {events}")                touch_found = True                touch = BtnTouch(ev.value)            elif ev in (                libevdev.InputEvent(libevdev.EV_KEY.BTN_TOOL_PEN),                libevdev.InputEvent(libevdev.EV_KEY.BTN_TOOL_RUBBER),            ):                if tool_found:                    raise ValueError(f"duplicated BTN_TOOL_* in {events}")                tool_found = True                tool = ToolType(ev.code) if ev.value else None            elif test_button is not None and ev in (test_button.value,):                if button_found:                    raise ValueError(f"duplicated BTN_STYLUS* in {events}")                button_found = True                button = bool(ev.value)         # the kernel tends to insert an EV_SYN once removing the tool, so        # the button will be released after        if tool is None:            button = False         new_state = PenState((touch, tool, button))  # type: ignore        if strict:            assert (                new_state in self.valid_transitions()            ), f"moving from {self} to {new_state} is forbidden"        else:            assert (                new_state in self.historically_tolerated_transitions()            ), f"moving from {self} to {new_state} is forbidden"         return new_state     def valid_transitions(self) -> Tuple["PenState", ...]:        """Following the state machine in the URL above.         Note that those transitions are from the evdev point of view, not HID"""        if self == PenState.PEN_IS_OUT_OF_RANGE:            return (                PenState.PEN_IS_OUT_OF_RANGE,                PenState.PEN_IS_IN_RANGE,                PenState.PEN_IS_IN_RANGE_WITH_BUTTON,                PenState.PEN_IS_IN_RANGE_WITH_ERASING_INTENT,                PenState.PEN_IS_IN_CONTACT,                PenState.PEN_IS_IN_CONTACT_WITH_BUTTON,                PenState.PEN_IS_ERASING,            )         if self == PenState.PEN_IS_IN_RANGE:            return (                PenState.PEN_IS_IN_RANGE,                PenState.PEN_IS_IN_RANGE_WITH_BUTTON,                PenState.PEN_IS_OUT_OF_RANGE,                PenState.PEN_IS_IN_CONTACT,            )         if self == PenState.PEN_IS_IN_CONTACT:            return (                PenState.PEN_IS_IN_CONTACT,                PenState.PEN_IS_IN_CONTACT_WITH_BUTTON,                PenState.PEN_IS_IN_RANGE,            )         if self == PenState.PEN_IS_IN_RANGE_WITH_ERASING_INTENT:            return (                PenState.PEN_IS_IN_RANGE_WITH_ERASING_INTENT,                PenState.PEN_IS_OUT_OF_RANGE,                PenState.PEN_IS_ERASING,            )         if self == PenState.PEN_IS_ERASING:            return (                PenState.PEN_IS_ERASING,                PenState.PEN_IS_IN_RANGE_WITH_ERASING_INTENT,            )         if self == PenState.PEN_IS_IN_RANGE_WITH_BUTTON:            return (                PenState.PEN_IS_IN_RANGE_WITH_BUTTON,                PenState.PEN_IS_IN_RANGE,                PenState.PEN_IS_OUT_OF_RANGE,                PenState.PEN_IS_IN_CONTACT_WITH_BUTTON,            )         if self == PenState.PEN_IS_IN_CONTACT_WITH_BUTTON:            return (                PenState.PEN_IS_IN_CONTACT_WITH_BUTTON,                PenState.PEN_IS_IN_CONTACT,                PenState.PEN_IS_IN_RANGE_WITH_BUTTON,            )         return tuple()     def historically_tolerated_transitions(self) -> Tuple["PenState", ...]:        """Following the state machine in the URL above, with a couple of addition        for skipping the in-range state, due to historical reasons.         Note that those transitions are from the evdev point of view, not HID"""        if self == PenState.PEN_IS_OUT_OF_RANGE:            return (                PenState.PEN_IS_OUT_OF_RANGE,                PenState.PEN_IS_IN_RANGE,                PenState.PEN_IS_IN_RANGE_WITH_BUTTON,                PenState.PEN_IS_IN_RANGE_WITH_ERASING_INTENT,                PenState.PEN_IS_IN_CONTACT,                PenState.PEN_IS_IN_CONTACT_WITH_BUTTON,                PenState.PEN_IS_ERASING,            )         if self == PenState.PEN_IS_IN_RANGE:            return (                PenState.PEN_IS_IN_RANGE,                PenState.PEN_IS_IN_RANGE_WITH_BUTTON,                PenState.PEN_IS_OUT_OF_RANGE,                PenState.PEN_IS_IN_CONTACT,            )         if self == PenState.PEN_IS_IN_CONTACT:            return (                PenState.PEN_IS_IN_CONTACT,                PenState.PEN_IS_IN_CONTACT_WITH_BUTTON,                PenState.PEN_IS_IN_RANGE,                PenState.PEN_IS_OUT_OF_RANGE,            )         if self == PenState.PEN_IS_IN_RANGE_WITH_ERASING_INTENT:            return (                PenState.PEN_IS_IN_RANGE_WITH_ERASING_INTENT,                PenState.PEN_IS_OUT_OF_RANGE,                PenState.PEN_IS_ERASING,            )         if self == PenState.PEN_IS_ERASING:            return (                PenState.PEN_IS_ERASING,                PenState.PEN_IS_IN_RANGE_WITH_ERASING_INTENT,                PenState.PEN_IS_OUT_OF_RANGE,            )         if self == PenState.PEN_IS_IN_RANGE_WITH_BUTTON:            return (                PenState.PEN_IS_IN_RANGE_WITH_BUTTON,                PenState.PEN_IS_IN_RANGE,                PenState.PEN_IS_OUT_OF_RANGE,                PenState.PEN_IS_IN_CONTACT_WITH_BUTTON,            )         if self == PenState.PEN_IS_IN_CONTACT_WITH_BUTTON:            return (                PenState.PEN_IS_IN_CONTACT_WITH_BUTTON,                PenState.PEN_IS_IN_CONTACT,                PenState.PEN_IS_IN_RANGE_WITH_BUTTON,                PenState.PEN_IS_OUT_OF_RANGE,            )         return tuple()     @staticmethod    def legal_transitions() -> Dict[str, Tuple["PenState", ...]]:        """This is the first half of the Windows Pen Implementation state machine:        we don't have Invert nor Erase bits, so just move in/out-of-range or proximity.        https://docs.microsoft.com/en-us/windows-hardware/design/component-guidelines/windows-pen-states        """        return {            "in-range": (PenState.PEN_IS_IN_RANGE,),            "in-range -> out-of-range": (                PenState.PEN_IS_IN_RANGE,                PenState.PEN_IS_OUT_OF_RANGE,            ),            "in-range -> touch": (PenState.PEN_IS_IN_RANGE, PenState.PEN_IS_IN_CONTACT),            "in-range -> touch -> release": (                PenState.PEN_IS_IN_RANGE,                PenState.PEN_IS_IN_CONTACT,                PenState.PEN_IS_IN_RANGE,            ),            "in-range -> touch -> release -> out-of-range": (                PenState.PEN_IS_IN_RANGE,                PenState.PEN_IS_IN_CONTACT,                PenState.PEN_IS_IN_RANGE,                PenState.PEN_IS_OUT_OF_RANGE,            ),        }     @staticmethod    def legal_transitions_with_invert() -> Dict[str, Tuple["PenState", ...]]:        """This is the second half of the Windows Pen Implementation state machine:        we now have Invert and Erase bits, so move in/out or proximity with the intend        to erase.        https://docs.microsoft.com/en-us/windows-hardware/design/component-guidelines/windows-pen-states        """        return {            "hover-erasing": (PenState.PEN_IS_IN_RANGE_WITH_ERASING_INTENT,),            "hover-erasing -> out-of-range": (                PenState.PEN_IS_IN_RANGE_WITH_ERASING_INTENT,                PenState.PEN_IS_OUT_OF_RANGE,            ),            "hover-erasing -> erase": (                PenState.PEN_IS_IN_RANGE_WITH_ERASING_INTENT,                PenState.PEN_IS_ERASING,            ),            "hover-erasing -> erase -> release": (                PenState.PEN_IS_IN_RANGE_WITH_ERASING_INTENT,                PenState.PEN_IS_ERASING,                PenState.PEN_IS_IN_RANGE_WITH_ERASING_INTENT,            ),            "hover-erasing -> erase -> release -> out-of-range": (                PenState.PEN_IS_IN_RANGE_WITH_ERASING_INTENT,                PenState.PEN_IS_ERASING,                PenState.PEN_IS_IN_RANGE_WITH_ERASING_INTENT,                PenState.PEN_IS_OUT_OF_RANGE,            ),            "hover-erasing -> in-range": (                PenState.PEN_IS_IN_RANGE_WITH_ERASING_INTENT,                PenState.PEN_IS_IN_RANGE,            ),            "in-range -> hover-erasing": (                PenState.PEN_IS_IN_RANGE,                PenState.PEN_IS_IN_RANGE_WITH_ERASING_INTENT,            ),        }     @staticmethod    def legal_transitions_with_button() -> Dict[str, Tuple["PenState", ...]]:        """We revisit the Windows Pen Implementation state machine:        we now have a button.        """        return {            "hover-button": (PenState.PEN_IS_IN_RANGE_WITH_BUTTON,),            "hover-button -> out-of-range": (                PenState.PEN_IS_IN_RANGE_WITH_BUTTON,                PenState.PEN_IS_OUT_OF_RANGE,            ),            "in-range -> button-press": (                PenState.PEN_IS_IN_RANGE,                PenState.PEN_IS_IN_RANGE_WITH_BUTTON,            ),            "in-range -> button-press -> button-release": (                PenState.PEN_IS_IN_RANGE,                PenState.PEN_IS_IN_RANGE_WITH_BUTTON,                PenState.PEN_IS_IN_RANGE,            ),            "in-range -> touch -> button-press -> button-release": (                PenState.PEN_IS_IN_RANGE,                PenState.PEN_IS_IN_CONTACT,                PenState.PEN_IS_IN_CONTACT_WITH_BUTTON,                PenState.PEN_IS_IN_CONTACT,            ),            "in-range -> touch -> button-press -> release -> button-release": (                PenState.PEN_IS_IN_RANGE,                PenState.PEN_IS_IN_CONTACT,                PenState.PEN_IS_IN_CONTACT_WITH_BUTTON,                PenState.PEN_IS_IN_RANGE_WITH_BUTTON,                PenState.PEN_IS_IN_RANGE,            ),            "in-range -> button-press -> touch -> release -> button-release": (                PenState.PEN_IS_IN_RANGE,                PenState.PEN_IS_IN_RANGE_WITH_BUTTON,                PenState.PEN_IS_IN_CONTACT_WITH_BUTTON,                PenState.PEN_IS_IN_RANGE_WITH_BUTTON,                PenState.PEN_IS_IN_RANGE,            ),            "in-range -> button-press -> touch -> button-release -> release": (                PenState.PEN_IS_IN_RANGE,                PenState.PEN_IS_IN_RANGE_WITH_BUTTON,                PenState.PEN_IS_IN_CONTACT_WITH_BUTTON,                PenState.PEN_IS_IN_CONTACT,                PenState.PEN_IS_IN_RANGE,            ),        }     @staticmethod    def tolerated_transitions() -> Dict[str, Tuple["PenState", ...]]:        """This is not adhering to the Windows Pen Implementation state machine        but we should expect the kernel to behave properly, mostly for historical        reasons."""        return {            "direct-in-contact": (PenState.PEN_IS_IN_CONTACT,),            "direct-in-contact -> out-of-range": (                PenState.PEN_IS_IN_CONTACT,                PenState.PEN_IS_OUT_OF_RANGE,            ),        }     @staticmethod    def tolerated_transitions_with_invert() -> Dict[str, Tuple["PenState", ...]]:        """This is the second half of the Windows Pen Implementation state machine:        we now have Invert and Erase bits, so move in/out or proximity with the intend        to erase.        https://docs.microsoft.com/en-us/windows-hardware/design/component-guidelines/windows-pen-states        """        return {            "direct-erase": (PenState.PEN_IS_ERASING,),            "direct-erase -> out-of-range": (                PenState.PEN_IS_ERASING,                PenState.PEN_IS_OUT_OF_RANGE,            ),        }     @staticmethod    def broken_transitions() -> Dict[str, Tuple["PenState", ...]]:        """Those tests are definitely not part of the Windows specification.        However, a half broken device might export those transitions.        For example, a pen that has the eraser button might wobble between        touching and erasing if the tablet doesn't enforce the Windows        state machine."""        return {            "in-range -> touch -> erase -> hover-erase": (                PenState.PEN_IS_IN_RANGE,                PenState.PEN_IS_IN_CONTACT,                PenState.PEN_IS_ERASING,                PenState.PEN_IS_IN_RANGE_WITH_ERASING_INTENT,            ),            "in-range -> erase -> hover-erase": (                PenState.PEN_IS_IN_RANGE,                PenState.PEN_IS_ERASING,                PenState.PEN_IS_IN_RANGE_WITH_ERASING_INTENT,            ),            "hover-erase -> erase -> touch -> in-range": (                PenState.PEN_IS_IN_RANGE_WITH_ERASING_INTENT,                PenState.PEN_IS_ERASING,                PenState.PEN_IS_IN_CONTACT,                PenState.PEN_IS_IN_RANGE,            ),            "hover-erase -> touch -> in-range": (                PenState.PEN_IS_IN_RANGE_WITH_ERASING_INTENT,                PenState.PEN_IS_IN_CONTACT,                PenState.PEN_IS_IN_RANGE,            ),            "touch -> erase -> touch -> erase": (                PenState.PEN_IS_IN_CONTACT,                PenState.PEN_IS_ERASING,                PenState.PEN_IS_IN_CONTACT,                PenState.PEN_IS_ERASING,            ),        }  class Pen(object):    def __init__(self, x, y):        self.x = x        self.y = y        self.distance = -10        self.tipswitch = False        self.tippressure = 15        self.azimuth = 0        self.inrange = False        self.width = 10        self.height = 10        self.barrelswitch = False        self.secondarybarrelswitch = False        self.invert = False        self.eraser = False        self.xtilt = 1        self.ytilt = 1        self.twist = 1        self._old_values = None        self.current_state = None     def restore(self):        if self._old_values is not None:            for i in [                "x",                "y",                "distance",                "tippressure",                "azimuth",                "width",                "height",                "twist",                "xtilt",                "ytilt",            ]:                setattr(self, i, getattr(self._old_values, i))     def backup(self):        self._old_values = copy.copy(self)     def __assert_axis(self, evdev, axis, value):        if (            axis == libevdev.EV_KEY.BTN_TOOL_RUBBER            and evdev.value[libevdev.EV_KEY.BTN_TOOL_RUBBER] is None        ):            return         assert (            evdev.value[axis] == value        ), f"assert evdev.value[{axis}] ({evdev.value[axis]}) != {value}"

Lines 1–500 of 1577.

Keyboard shortcuts

General

Show keyboard shortcuts
?
Focus the current search
/

Issues and pull requests

Open filters
f

Issues, pull requests and Inbox

Select next item
j↓
Select previous item
k↑
Open selected item
Enter
Dismiss filters or clear selection
Esc

Repository code

Go to file
t