selftests/hid: tablets: add variants of states with buttons

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>
This commit is contained in:
Benjamin Tissoires
2023-12-06 11:46:01 +01:00
parent 83912f83fa
commit 74452d6329

View File

@@ -30,25 +30,72 @@ class ToolType(Enum):
RUBBER = libevdev.EV_KEY.BTN_TOOL_RUBBER 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): class PenState(Enum):
"""Pen states according to Microsoft reference: """Pen states according to Microsoft reference:
https://docs.microsoft.com/en-us/windows-hardware/design/component-guidelines/windows-pen-states 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 PEN_IS_OUT_OF_RANGE = BtnTouch.UP, None, None
PEN_IS_IN_RANGE = BtnTouch.UP, ToolType.PEN PEN_IS_IN_RANGE = BtnTouch.UP, ToolType.PEN, None
PEN_IS_IN_CONTACT = BtnTouch.DOWN, ToolType.PEN PEN_IS_IN_RANGE_WITH_BUTTON = BtnTouch.UP, ToolType.PEN, BtnPressed.PRIMARY_PRESSED
PEN_IS_IN_RANGE_WITH_ERASING_INTENT = BtnTouch.UP, ToolType.RUBBER PEN_IS_IN_RANGE_WITH_SECOND_BUTTON = (
PEN_IS_ERASING = BtnTouch.DOWN, ToolType.RUBBER 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]): def __init__(self, touch: BtnTouch, tool: Optional[ToolType], button: Optional[BtnPressed]):
self.touch = touch self.touch = touch
self.tool = tool self.tool = tool
self.button = button
@classmethod @classmethod
def from_evdev(cls, evdev) -> "PenState": def from_evdev(cls, evdev) -> "PenState":
touch = BtnTouch(evdev.value[libevdev.EV_KEY.BTN_TOUCH]) touch = BtnTouch(evdev.value[libevdev.EV_KEY.BTN_TOUCH])
tool = None tool = None
button = None
if ( if (
evdev.value[libevdev.EV_KEY.BTN_TOOL_RUBBER] evdev.value[libevdev.EV_KEY.BTN_TOOL_RUBBER]
and not evdev.value[libevdev.EV_KEY.BTN_TOOL_PEN] and not evdev.value[libevdev.EV_KEY.BTN_TOOL_PEN]
@@ -65,7 +112,17 @@ class PenState(Enum):
): ):
raise ValueError("2 tools are not allowed") 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": def apply(self, events) -> "PenState":
if libevdev.EV_SYN.SYN_REPORT in events: if libevdev.EV_SYN.SYN_REPORT in events:
@@ -74,6 +131,8 @@ class PenState(Enum):
touch_found = False touch_found = False
tool = self.tool tool = self.tool
tool_found = False tool_found = False
button = self.button
button_found = False
for ev in events: for ev in events:
if ev == libevdev.InputEvent(libevdev.EV_KEY.BTN_TOUCH): if ev == libevdev.InputEvent(libevdev.EV_KEY.BTN_TOUCH):
@@ -88,12 +147,22 @@ class PenState(Enum):
if tool_found: if tool_found:
raise ValueError(f"duplicated BTN_TOOL_* in {events}") raise ValueError(f"duplicated BTN_TOOL_* in {events}")
tool_found = True tool_found = True
if ev.value: tool = ToolType(ev.code) if ev.value else None
tool = ToolType(ev.code) elif ev in (
else: libevdev.InputEvent(libevdev.EV_KEY.BTN_STYLUS),
tool = None 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 ( assert (
new_state in self.valid_transitions() new_state in self.valid_transitions()
), f"moving from {self} to {new_state} is forbidden" ), f"moving from {self} to {new_state} is forbidden"
@@ -109,14 +178,20 @@ class PenState(Enum):
return ( return (
PenState.PEN_IS_OUT_OF_RANGE, PenState.PEN_IS_OUT_OF_RANGE,
PenState.PEN_IS_IN_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_RANGE_WITH_ERASING_INTENT,
PenState.PEN_IS_IN_CONTACT, PenState.PEN_IS_IN_CONTACT,
PenState.PEN_IS_IN_CONTACT_WITH_BUTTON,
PenState.PEN_IS_IN_CONTACT_WITH_SECOND_BUTTON,
PenState.PEN_IS_ERASING, PenState.PEN_IS_ERASING,
) )
if self == PenState.PEN_IS_IN_RANGE: if self == PenState.PEN_IS_IN_RANGE:
return ( return (
PenState.PEN_IS_IN_RANGE, 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_OUT_OF_RANGE,
PenState.PEN_IS_IN_CONTACT, PenState.PEN_IS_IN_CONTACT,
) )
@@ -124,6 +199,8 @@ class PenState(Enum):
if self == PenState.PEN_IS_IN_CONTACT: if self == PenState.PEN_IS_IN_CONTACT:
return ( return (
PenState.PEN_IS_IN_CONTACT, 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_IN_RANGE,
PenState.PEN_IS_OUT_OF_RANGE, PenState.PEN_IS_OUT_OF_RANGE,
) )
@@ -142,6 +219,38 @@ class PenState(Enum):
PenState.PEN_IS_OUT_OF_RANGE, 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() return tuple()
@staticmethod @staticmethod
@@ -376,26 +485,64 @@ class PenDigitizer(base.UHIDTestDevice):
pen.xtilt = 0 pen.xtilt = 0
pen.ytilt = 0 pen.ytilt = 0
pen.twist = 0 pen.twist = 0
pen.barrelswitch = False
pen.secondarybarrelswitch = False
elif state == PenState.PEN_IS_IN_RANGE: elif state == PenState.PEN_IS_IN_RANGE:
pen.tipswitch = False pen.tipswitch = False
pen.inrange = True pen.inrange = True
pen.invert = False pen.invert = False
pen.eraser = False pen.eraser = False
pen.barrelswitch = False
pen.secondarybarrelswitch = False
elif state == PenState.PEN_IS_IN_CONTACT: elif state == PenState.PEN_IS_IN_CONTACT:
pen.tipswitch = True pen.tipswitch = True
pen.inrange = True pen.inrange = True
pen.invert = False pen.invert = False
pen.eraser = 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: elif state == PenState.PEN_IS_IN_RANGE_WITH_ERASING_INTENT:
pen.tipswitch = False pen.tipswitch = False
pen.inrange = True pen.inrange = True
pen.invert = True pen.invert = True
pen.eraser = False pen.eraser = False
pen.barrelswitch = False
pen.secondarybarrelswitch = False
elif state == PenState.PEN_IS_ERASING: elif state == PenState.PEN_IS_ERASING:
pen.tipswitch = False pen.tipswitch = False
pen.inrange = True pen.inrange = True
pen.invert = False pen.invert = False
pen.eraser = True pen.eraser = True
pen.barrelswitch = False
pen.secondarybarrelswitch = False
pen.current_state = state pen.current_state = state