Linux kernel mirror (for testing) git.kernel.org/pub/scm/linux/kernel/git/torvalds/linux.git
kernel os linux

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>

+159 -12
+159 -12
tools/testing/selftests/hid/tests/test_tablet.py
··· 30 30 RUBBER = libevdev.EV_KEY.BTN_TOOL_RUBBER 31 31 32 32 33 + class BtnPressed(Enum): 34 + """Represents whether a button is pressed on the stylus""" 35 + 36 + PRIMARY_PRESSED = libevdev.EV_KEY.BTN_STYLUS 37 + SECONDARY_PRESSED = libevdev.EV_KEY.BTN_STYLUS2 38 + 39 + 33 40 class PenState(Enum): 34 41 """Pen states according to Microsoft reference: 35 42 https://docs.microsoft.com/en-us/windows-hardware/design/component-guidelines/windows-pen-states 43 + 44 + We extend it with the various buttons when we need to check them. 36 45 """ 37 46 38 - PEN_IS_OUT_OF_RANGE = BtnTouch.UP, None 39 - PEN_IS_IN_RANGE = BtnTouch.UP, ToolType.PEN 40 - PEN_IS_IN_CONTACT = BtnTouch.DOWN, ToolType.PEN 41 - PEN_IS_IN_RANGE_WITH_ERASING_INTENT = BtnTouch.UP, ToolType.RUBBER 42 - PEN_IS_ERASING = BtnTouch.DOWN, ToolType.RUBBER 47 + PEN_IS_OUT_OF_RANGE = BtnTouch.UP, None, None 48 + PEN_IS_IN_RANGE = BtnTouch.UP, ToolType.PEN, None 49 + PEN_IS_IN_RANGE_WITH_BUTTON = BtnTouch.UP, ToolType.PEN, BtnPressed.PRIMARY_PRESSED 50 + PEN_IS_IN_RANGE_WITH_SECOND_BUTTON = ( 51 + BtnTouch.UP, 52 + ToolType.PEN, 53 + BtnPressed.SECONDARY_PRESSED, 54 + ) 55 + PEN_IS_IN_CONTACT = BtnTouch.DOWN, ToolType.PEN, None 56 + PEN_IS_IN_CONTACT_WITH_BUTTON = ( 57 + BtnTouch.DOWN, 58 + ToolType.PEN, 59 + BtnPressed.PRIMARY_PRESSED, 60 + ) 61 + PEN_IS_IN_CONTACT_WITH_SECOND_BUTTON = ( 62 + BtnTouch.DOWN, 63 + ToolType.PEN, 64 + BtnPressed.SECONDARY_PRESSED, 65 + ) 66 + PEN_IS_IN_RANGE_WITH_ERASING_INTENT = BtnTouch.UP, ToolType.RUBBER, None 67 + PEN_IS_IN_RANGE_WITH_ERASING_INTENT_WITH_BUTTON = ( 68 + BtnTouch.UP, 69 + ToolType.RUBBER, 70 + BtnPressed.PRIMARY_PRESSED, 71 + ) 72 + PEN_IS_IN_RANGE_WITH_ERASING_INTENT_WITH_SECOND_BUTTON = ( 73 + BtnTouch.UP, 74 + ToolType.RUBBER, 75 + BtnPressed.SECONDARY_PRESSED, 76 + ) 77 + PEN_IS_ERASING = BtnTouch.DOWN, ToolType.RUBBER, None 78 + PEN_IS_ERASING_WITH_BUTTON = ( 79 + BtnTouch.DOWN, 80 + ToolType.RUBBER, 81 + BtnPressed.PRIMARY_PRESSED, 82 + ) 83 + PEN_IS_ERASING_WITH_SECOND_BUTTON = ( 84 + BtnTouch.DOWN, 85 + ToolType.RUBBER, 86 + BtnPressed.SECONDARY_PRESSED, 87 + ) 43 88 44 - def __init__(self, touch: BtnTouch, tool: Optional[ToolType]): 89 + def __init__(self, touch: BtnTouch, tool: Optional[ToolType], button: Optional[BtnPressed]): 45 90 self.touch = touch 46 91 self.tool = tool 92 + self.button = button 47 93 48 94 @classmethod 49 95 def from_evdev(cls, evdev) -> "PenState": 50 96 touch = BtnTouch(evdev.value[libevdev.EV_KEY.BTN_TOUCH]) 51 97 tool = None 98 + button = None 52 99 if ( 53 100 evdev.value[libevdev.EV_KEY.BTN_TOOL_RUBBER] 54 101 and not evdev.value[libevdev.EV_KEY.BTN_TOOL_PEN] ··· 112 65 ): 113 66 raise ValueError("2 tools are not allowed") 114 67 115 - return cls((touch, tool)) 68 + # we take only the highest button in account 69 + for b in [libevdev.EV_KEY.BTN_STYLUS, libevdev.EV_KEY.BTN_STYLUS2]: 70 + if bool(evdev.value[b]): 71 + button = b 72 + 73 + # the kernel tends to insert an EV_SYN once removing the tool, so 74 + # the button will be released after 75 + if tool is None: 76 + button = None 77 + 78 + return cls((touch, tool, button)) 116 79 117 80 def apply(self, events) -> "PenState": 118 81 if libevdev.EV_SYN.SYN_REPORT in events: ··· 131 74 touch_found = False 132 75 tool = self.tool 133 76 tool_found = False 77 + button = self.button 78 + button_found = False 134 79 135 80 for ev in events: 136 81 if ev == libevdev.InputEvent(libevdev.EV_KEY.BTN_TOUCH): ··· 147 88 if tool_found: 148 89 raise ValueError(f"duplicated BTN_TOOL_* in {events}") 149 90 tool_found = True 150 - if ev.value: 151 - tool = ToolType(ev.code) 152 - else: 153 - tool = None 91 + tool = ToolType(ev.code) if ev.value else None 92 + elif ev in ( 93 + libevdev.InputEvent(libevdev.EV_KEY.BTN_STYLUS), 94 + libevdev.InputEvent(libevdev.EV_KEY.BTN_STYLUS2), 95 + ): 96 + if button_found: 97 + raise ValueError(f"duplicated BTN_STYLUS* in {events}") 98 + button_found = True 99 + button = ev.code if ev.value else None 154 100 155 - new_state = PenState((touch, tool)) 101 + # the kernel tends to insert an EV_SYN once removing the tool, so 102 + # the button will be released after 103 + if tool is None: 104 + button = None 105 + 106 + new_state = PenState((touch, tool, button)) 156 107 assert ( 157 108 new_state in self.valid_transitions() 158 109 ), f"moving from {self} to {new_state} is forbidden" ··· 178 109 return ( 179 110 PenState.PEN_IS_OUT_OF_RANGE, 180 111 PenState.PEN_IS_IN_RANGE, 112 + PenState.PEN_IS_IN_RANGE_WITH_BUTTON, 113 + PenState.PEN_IS_IN_RANGE_WITH_SECOND_BUTTON, 181 114 PenState.PEN_IS_IN_RANGE_WITH_ERASING_INTENT, 182 115 PenState.PEN_IS_IN_CONTACT, 116 + PenState.PEN_IS_IN_CONTACT_WITH_BUTTON, 117 + PenState.PEN_IS_IN_CONTACT_WITH_SECOND_BUTTON, 183 118 PenState.PEN_IS_ERASING, 184 119 ) 185 120 186 121 if self == PenState.PEN_IS_IN_RANGE: 187 122 return ( 188 123 PenState.PEN_IS_IN_RANGE, 124 + PenState.PEN_IS_IN_RANGE_WITH_BUTTON, 125 + PenState.PEN_IS_IN_RANGE_WITH_SECOND_BUTTON, 189 126 PenState.PEN_IS_OUT_OF_RANGE, 190 127 PenState.PEN_IS_IN_CONTACT, 191 128 ) ··· 199 124 if self == PenState.PEN_IS_IN_CONTACT: 200 125 return ( 201 126 PenState.PEN_IS_IN_CONTACT, 127 + PenState.PEN_IS_IN_CONTACT_WITH_BUTTON, 128 + PenState.PEN_IS_IN_CONTACT_WITH_SECOND_BUTTON, 202 129 PenState.PEN_IS_IN_RANGE, 203 130 PenState.PEN_IS_OUT_OF_RANGE, 204 131 ) ··· 216 139 return ( 217 140 PenState.PEN_IS_ERASING, 218 141 PenState.PEN_IS_IN_RANGE_WITH_ERASING_INTENT, 142 + PenState.PEN_IS_OUT_OF_RANGE, 143 + ) 144 + 145 + if self == PenState.PEN_IS_IN_RANGE_WITH_BUTTON: 146 + return ( 147 + PenState.PEN_IS_IN_RANGE_WITH_BUTTON, 148 + PenState.PEN_IS_IN_RANGE, 149 + PenState.PEN_IS_OUT_OF_RANGE, 150 + PenState.PEN_IS_IN_CONTACT_WITH_BUTTON, 151 + ) 152 + 153 + if self == PenState.PEN_IS_IN_CONTACT_WITH_BUTTON: 154 + return ( 155 + PenState.PEN_IS_IN_CONTACT_WITH_BUTTON, 156 + PenState.PEN_IS_IN_CONTACT, 157 + PenState.PEN_IS_IN_RANGE_WITH_BUTTON, 158 + PenState.PEN_IS_OUT_OF_RANGE, 159 + ) 160 + 161 + if self == PenState.PEN_IS_IN_RANGE_WITH_SECOND_BUTTON: 162 + return ( 163 + PenState.PEN_IS_IN_RANGE_WITH_SECOND_BUTTON, 164 + PenState.PEN_IS_IN_RANGE, 165 + PenState.PEN_IS_OUT_OF_RANGE, 166 + PenState.PEN_IS_IN_CONTACT_WITH_SECOND_BUTTON, 167 + ) 168 + 169 + if self == PenState.PEN_IS_IN_CONTACT_WITH_SECOND_BUTTON: 170 + return ( 171 + PenState.PEN_IS_IN_CONTACT_WITH_SECOND_BUTTON, 172 + PenState.PEN_IS_IN_CONTACT, 173 + PenState.PEN_IS_IN_RANGE_WITH_SECOND_BUTTON, 219 174 PenState.PEN_IS_OUT_OF_RANGE, 220 175 ) 221 176 ··· 485 376 pen.xtilt = 0 486 377 pen.ytilt = 0 487 378 pen.twist = 0 379 + pen.barrelswitch = False 380 + pen.secondarybarrelswitch = False 488 381 elif state == PenState.PEN_IS_IN_RANGE: 489 382 pen.tipswitch = False 490 383 pen.inrange = True 491 384 pen.invert = False 492 385 pen.eraser = False 386 + pen.barrelswitch = False 387 + pen.secondarybarrelswitch = False 493 388 elif state == PenState.PEN_IS_IN_CONTACT: 494 389 pen.tipswitch = True 495 390 pen.inrange = True 496 391 pen.invert = False 497 392 pen.eraser = False 393 + pen.barrelswitch = False 394 + pen.secondarybarrelswitch = False 395 + elif state == PenState.PEN_IS_IN_RANGE_WITH_BUTTON: 396 + pen.tipswitch = False 397 + pen.inrange = True 398 + pen.invert = False 399 + pen.eraser = False 400 + pen.barrelswitch = True 401 + pen.secondarybarrelswitch = False 402 + elif state == PenState.PEN_IS_IN_CONTACT_WITH_BUTTON: 403 + pen.tipswitch = True 404 + pen.inrange = True 405 + pen.invert = False 406 + pen.eraser = False 407 + pen.barrelswitch = True 408 + pen.secondarybarrelswitch = False 409 + elif state == PenState.PEN_IS_IN_RANGE_WITH_SECOND_BUTTON: 410 + pen.tipswitch = False 411 + pen.inrange = True 412 + pen.invert = False 413 + pen.eraser = False 414 + pen.barrelswitch = False 415 + pen.secondarybarrelswitch = True 416 + elif state == PenState.PEN_IS_IN_CONTACT_WITH_SECOND_BUTTON: 417 + pen.tipswitch = True 418 + pen.inrange = True 419 + pen.invert = False 420 + pen.eraser = False 421 + pen.barrelswitch = False 422 + pen.secondarybarrelswitch = True 498 423 elif state == PenState.PEN_IS_IN_RANGE_WITH_ERASING_INTENT: 499 424 pen.tipswitch = False 500 425 pen.inrange = True 501 426 pen.invert = True 502 427 pen.eraser = False 428 + pen.barrelswitch = False 429 + pen.secondarybarrelswitch = False 503 430 elif state == PenState.PEN_IS_ERASING: 504 431 pen.tipswitch = False 505 432 pen.inrange = True 506 433 pen.invert = False 507 434 pen.eraser = True 435 + pen.barrelswitch = False 436 + pen.secondarybarrelswitch = False 508 437 509 438 pen.current_state = state 510 439