Skip to content

Commit

Permalink
selftests/hid: tablets: add variants of states with buttons
Browse files Browse the repository at this point in the history
Turns out that there are transitions that are unlikely to happen:
for example, having both the tip switch and a button being changed
at the same time (in the same report) would require either a very talented
and precise user or a very bad hardware with a very low sampling rate.

So instead of manually building the button test by hand and forgetting
about some cases, let's reuse the state machine and transitions we have.

This patch only adds the states and the valid transitions. The actual
tests will be replaced later.

Reviewed-by: Peter Hutterer <peter.hutterer@who-t.net>
Acked-by: Jiri Kosina <jkosina@suse.com>
Link: https://lore.kernel.org/r/20231206-wip-selftests-v2-10-c0350c2f5986@kernel.org
Signed-off-by: Benjamin Tissoires <bentiss@kernel.org>
  • Loading branch information
Benjamin Tissoires committed Dec 7, 2023
1 parent 83912f8 commit 74452d6
Showing 1 changed file with 160 additions and 13 deletions.
173 changes: 160 additions & 13 deletions tools/testing/selftests/hid/tests/test_tablet.py
Original file line number Diff line number Diff line change
Expand Up @@ -30,25 +30,72 @@ class ToolType(Enum):
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


class PenState(Enum):
"""Pen states according to Microsoft reference:
https://docs.microsoft.com/en-us/windows-hardware/design/component-guidelines/windows-pen-states
"""
PEN_IS_OUT_OF_RANGE = BtnTouch.UP, None
PEN_IS_IN_RANGE = BtnTouch.UP, ToolType.PEN
PEN_IS_IN_CONTACT = BtnTouch.DOWN, ToolType.PEN
PEN_IS_IN_RANGE_WITH_ERASING_INTENT = BtnTouch.UP, ToolType.RUBBER
PEN_IS_ERASING = BtnTouch.DOWN, ToolType.RUBBER
We extend it with the various buttons when we need to check them.
"""

def __init__(self, touch: BtnTouch, tool: Optional[ToolType]):
PEN_IS_OUT_OF_RANGE = BtnTouch.UP, None, None
PEN_IS_IN_RANGE = BtnTouch.UP, ToolType.PEN, None
PEN_IS_IN_RANGE_WITH_BUTTON = BtnTouch.UP, ToolType.PEN, BtnPressed.PRIMARY_PRESSED
PEN_IS_IN_RANGE_WITH_SECOND_BUTTON = (
BtnTouch.UP,
ToolType.PEN,
BtnPressed.SECONDARY_PRESSED,
)
PEN_IS_IN_CONTACT = BtnTouch.DOWN, ToolType.PEN, None
PEN_IS_IN_CONTACT_WITH_BUTTON = (
BtnTouch.DOWN,
ToolType.PEN,
BtnPressed.PRIMARY_PRESSED,
)
PEN_IS_IN_CONTACT_WITH_SECOND_BUTTON = (
BtnTouch.DOWN,
ToolType.PEN,
BtnPressed.SECONDARY_PRESSED,
)
PEN_IS_IN_RANGE_WITH_ERASING_INTENT = BtnTouch.UP, ToolType.RUBBER, None
PEN_IS_IN_RANGE_WITH_ERASING_INTENT_WITH_BUTTON = (
BtnTouch.UP,
ToolType.RUBBER,
BtnPressed.PRIMARY_PRESSED,
)
PEN_IS_IN_RANGE_WITH_ERASING_INTENT_WITH_SECOND_BUTTON = (
BtnTouch.UP,
ToolType.RUBBER,
BtnPressed.SECONDARY_PRESSED,
)
PEN_IS_ERASING = BtnTouch.DOWN, ToolType.RUBBER, None
PEN_IS_ERASING_WITH_BUTTON = (
BtnTouch.DOWN,
ToolType.RUBBER,
BtnPressed.PRIMARY_PRESSED,
)
PEN_IS_ERASING_WITH_SECOND_BUTTON = (
BtnTouch.DOWN,
ToolType.RUBBER,
BtnPressed.SECONDARY_PRESSED,
)

def __init__(self, touch: BtnTouch, tool: Optional[ToolType], button: Optional[BtnPressed]):
self.touch = touch
self.tool = tool
self.button = button

@classmethod
def from_evdev(cls, evdev) -> "PenState":
touch = BtnTouch(evdev.value[libevdev.EV_KEY.BTN_TOUCH])
tool = None
button = None
if (
evdev.value[libevdev.EV_KEY.BTN_TOOL_RUBBER]
and not evdev.value[libevdev.EV_KEY.BTN_TOOL_PEN]
Expand All @@ -65,7 +112,17 @@ def from_evdev(cls, evdev) -> "PenState":
):
raise ValueError("2 tools are not allowed")

return cls((touch, tool))
# we take only the highest button in account
for b in [libevdev.EV_KEY.BTN_STYLUS, libevdev.EV_KEY.BTN_STYLUS2]:
if bool(evdev.value[b]):
button = b

# the kernel tends to insert an EV_SYN once removing the tool, so
# the button will be released after
if tool is None:
button = None

return cls((touch, tool, button))

def apply(self, events) -> "PenState":
if libevdev.EV_SYN.SYN_REPORT in events:
Expand All @@ -74,6 +131,8 @@ def apply(self, events) -> "PenState":
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):
Expand All @@ -88,12 +147,22 @@ def apply(self, events) -> "PenState":
if tool_found:
raise ValueError(f"duplicated BTN_TOOL_* in {events}")
tool_found = True
if ev.value:
tool = ToolType(ev.code)
else:
tool = None
tool = ToolType(ev.code) if ev.value else None
elif ev in (
libevdev.InputEvent(libevdev.EV_KEY.BTN_STYLUS),
libevdev.InputEvent(libevdev.EV_KEY.BTN_STYLUS2),
):
if button_found:
raise ValueError(f"duplicated BTN_STYLUS* in {events}")
button_found = True
button = ev.code if ev.value else None

new_state = PenState((touch, tool))
# the kernel tends to insert an EV_SYN once removing the tool, so
# the button will be released after
if tool is None:
button = None

new_state = PenState((touch, tool, button))
assert (
new_state in self.valid_transitions()
), f"moving from {self} to {new_state} is forbidden"
Expand All @@ -109,21 +178,29 @@ def valid_transitions(self) -> Tuple["PenState", ...]:
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_SECOND_BUTTON,
PenState.PEN_IS_IN_RANGE_WITH_ERASING_INTENT,
PenState.PEN_IS_IN_CONTACT,
PenState.PEN_IS_IN_CONTACT_WITH_BUTTON,
PenState.PEN_IS_IN_CONTACT_WITH_SECOND_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_IN_RANGE_WITH_SECOND_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_CONTACT_WITH_SECOND_BUTTON,
PenState.PEN_IS_IN_RANGE,
PenState.PEN_IS_OUT_OF_RANGE,
)
Expand All @@ -142,6 +219,38 @@ def valid_transitions(self) -> Tuple["PenState", ...]:
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,
)

if self == PenState.PEN_IS_IN_RANGE_WITH_SECOND_BUTTON:
return (
PenState.PEN_IS_IN_RANGE_WITH_SECOND_BUTTON,
PenState.PEN_IS_IN_RANGE,
PenState.PEN_IS_OUT_OF_RANGE,
PenState.PEN_IS_IN_CONTACT_WITH_SECOND_BUTTON,
)

if self == PenState.PEN_IS_IN_CONTACT_WITH_SECOND_BUTTON:
return (
PenState.PEN_IS_IN_CONTACT_WITH_SECOND_BUTTON,
PenState.PEN_IS_IN_CONTACT,
PenState.PEN_IS_IN_RANGE_WITH_SECOND_BUTTON,
PenState.PEN_IS_OUT_OF_RANGE,
)

return tuple()

@staticmethod
Expand Down Expand Up @@ -376,26 +485,64 @@ def move_to(self, pen, state):
pen.xtilt = 0
pen.ytilt = 0
pen.twist = 0
pen.barrelswitch = False
pen.secondarybarrelswitch = False
elif state == PenState.PEN_IS_IN_RANGE:
pen.tipswitch = False
pen.inrange = True
pen.invert = False
pen.eraser = False
pen.barrelswitch = False
pen.secondarybarrelswitch = False
elif state == PenState.PEN_IS_IN_CONTACT:
pen.tipswitch = True
pen.inrange = True
pen.invert = False
pen.eraser = False
pen.barrelswitch = False
pen.secondarybarrelswitch = False
elif state == PenState.PEN_IS_IN_RANGE_WITH_BUTTON:
pen.tipswitch = False
pen.inrange = True
pen.invert = False
pen.eraser = False
pen.barrelswitch = True
pen.secondarybarrelswitch = False
elif state == PenState.PEN_IS_IN_CONTACT_WITH_BUTTON:
pen.tipswitch = True
pen.inrange = True
pen.invert = False
pen.eraser = False
pen.barrelswitch = True
pen.secondarybarrelswitch = False
elif state == PenState.PEN_IS_IN_RANGE_WITH_SECOND_BUTTON:
pen.tipswitch = False
pen.inrange = True
pen.invert = False
pen.eraser = False
pen.barrelswitch = False
pen.secondarybarrelswitch = True
elif state == PenState.PEN_IS_IN_CONTACT_WITH_SECOND_BUTTON:
pen.tipswitch = True
pen.inrange = True
pen.invert = False
pen.eraser = False
pen.barrelswitch = False
pen.secondarybarrelswitch = True
elif state == PenState.PEN_IS_IN_RANGE_WITH_ERASING_INTENT:
pen.tipswitch = False
pen.inrange = True
pen.invert = True
pen.eraser = False
pen.barrelswitch = False
pen.secondarybarrelswitch = False
elif state == PenState.PEN_IS_ERASING:
pen.tipswitch = False
pen.inrange = True
pen.invert = False
pen.eraser = True
pen.barrelswitch = False
pen.secondarybarrelswitch = False

pen.current_state = state

Expand Down

0 comments on commit 74452d6

Please sign in to comment.