#!/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. Load more
#!/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 base import copy from enum import Enum from hidtools.util import BusType from .base import HidBpf import libevdev import logging import pytest from 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}"