Merge branch 'for-6.8/selftests' into for-linus
- greatly improved coverage of Tablets in hid-selftests (Benjamin Tissoires)
This commit is contained in:
commit
1cb09b552b
@ -14,7 +14,7 @@ import logging
|
||||
|
||||
from hidtools.device.base_device import BaseDevice, EvdevMatch, SysfsFile
|
||||
from pathlib import Path
|
||||
from typing import Final
|
||||
from typing import Final, List, Tuple
|
||||
|
||||
logger = logging.getLogger("hidtools.test.base")
|
||||
|
||||
@ -155,7 +155,7 @@ class BaseTestCase:
|
||||
# if any module is not available (not compiled), the test will skip.
|
||||
# Each element is a tuple '(kernel driver name, kernel module)',
|
||||
# for example ("playstation", "hid-playstation")
|
||||
kernel_modules = []
|
||||
kernel_modules: List[Tuple[str, str]] = []
|
||||
|
||||
def assertInputEventsIn(self, expected_events, effective_events):
|
||||
effective_events = effective_events.copy()
|
||||
@ -238,8 +238,7 @@ class BaseTestCase:
|
||||
try:
|
||||
with HIDTestUdevRule.instance():
|
||||
with new_uhdev as self.uhdev:
|
||||
skip_cond = request.node.get_closest_marker("skip_if_uhdev")
|
||||
if skip_cond:
|
||||
for skip_cond in request.node.iter_markers("skip_if_uhdev"):
|
||||
test, message, *rest = skip_cond.args
|
||||
|
||||
if test(self.uhdev):
|
||||
|
@ -52,13 +52,13 @@ class BaseMouse(base.UHIDTestDevice):
|
||||
:param reportID: the numeric report ID for this report, if needed
|
||||
"""
|
||||
if buttons is not None:
|
||||
l, r, m = buttons
|
||||
if l is not None:
|
||||
self.left = l
|
||||
if r is not None:
|
||||
self.right = r
|
||||
if m is not None:
|
||||
self.middle = m
|
||||
left, right, middle = buttons
|
||||
if left is not None:
|
||||
self.left = left
|
||||
if right is not None:
|
||||
self.right = right
|
||||
if middle is not None:
|
||||
self.middle = middle
|
||||
left = self.left
|
||||
right = self.right
|
||||
middle = self.middle
|
||||
|
@ -13,62 +13,133 @@ from hidtools.util import BusType
|
||||
import libevdev
|
||||
import logging
|
||||
import pytest
|
||||
from typing import Dict, Tuple
|
||||
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
|
||||
|
||||
|
||||
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 = (False, None)
|
||||
PEN_IS_IN_RANGE = (False, libevdev.EV_KEY.BTN_TOOL_PEN)
|
||||
PEN_IS_IN_CONTACT = (True, libevdev.EV_KEY.BTN_TOOL_PEN)
|
||||
PEN_IS_IN_RANGE_WITH_ERASING_INTENT = (False, libevdev.EV_KEY.BTN_TOOL_RUBBER)
|
||||
PEN_IS_ERASING = (True, libevdev.EV_KEY.BTN_TOOL_RUBBER)
|
||||
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, tool):
|
||||
self.touch = touch
|
||||
self.tool = tool
|
||||
def __init__(self, touch: BtnTouch, tool: Optional[ToolType], button: Optional[BtnPressed]):
|
||||
self.touch = touch # type: ignore
|
||||
self.tool = tool # type: ignore
|
||||
self.button = button # type: ignore
|
||||
|
||||
@classmethod
|
||||
def from_evdev(cls, evdev) -> "PenState":
|
||||
touch = bool(evdev.value[libevdev.EV_KEY.BTN_TOUCH])
|
||||
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]
|
||||
):
|
||||
tool = libevdev.EV_KEY.BTN_TOOL_RUBBER
|
||||
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 = libevdev.EV_KEY.BTN_TOOL_PEN
|
||||
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")
|
||||
|
||||
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 = BtnPressed(b)
|
||||
|
||||
def apply(self, events) -> "PenState":
|
||||
# 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)) # type: ignore
|
||||
|
||||
def apply(self, events: List[libevdev.InputEvent], strict: bool) -> "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 = bool(ev.value)
|
||||
touch = BtnTouch(ev.value)
|
||||
elif ev in (
|
||||
libevdev.InputEvent(libevdev.EV_KEY.BTN_TOOL_PEN),
|
||||
libevdev.InputEvent(libevdev.EV_KEY.BTN_TOOL_RUBBER),
|
||||
@ -76,19 +147,113 @@ class PenState(Enum):
|
||||
if tool_found:
|
||||
raise ValueError(f"duplicated BTN_TOOL_* in {events}")
|
||||
tool_found = True
|
||||
if ev.value:
|
||||
tool = 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 = BtnPressed(ev.code) if ev.value else None
|
||||
|
||||
new_state = PenState((touch, tool))
|
||||
assert (
|
||||
new_state in self.valid_transitions()
|
||||
), f"moving from {self} to {new_state} is forbidden"
|
||||
# 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)) # 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_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,
|
||||
)
|
||||
|
||||
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,
|
||||
)
|
||||
|
||||
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,
|
||||
)
|
||||
|
||||
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.
|
||||
|
||||
@ -97,14 +262,20 @@ class PenState(Enum):
|
||||
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,
|
||||
)
|
||||
@ -112,6 +283,8 @@ class PenState(Enum):
|
||||
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,
|
||||
)
|
||||
@ -130,110 +303,42 @@ class PenState(Enum):
|
||||
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()
|
||||
|
||||
|
||||
class Data(object):
|
||||
pass
|
||||
|
||||
|
||||
class Pen(object):
|
||||
def __init__(self, x, y):
|
||||
self.x = x
|
||||
self.y = y
|
||||
self.tipswitch = False
|
||||
self.tippressure = 15
|
||||
self.azimuth = 0
|
||||
self.inrange = False
|
||||
self.width = 10
|
||||
self.height = 10
|
||||
self.barrelswitch = False
|
||||
self.invert = False
|
||||
self.eraser = False
|
||||
self.x_tilt = 0
|
||||
self.y_tilt = 0
|
||||
self.twist = 0
|
||||
self._old_values = None
|
||||
self.current_state = None
|
||||
|
||||
def _restore(self):
|
||||
if self._old_values is not None:
|
||||
for i in [
|
||||
"x",
|
||||
"y",
|
||||
"tippressure",
|
||||
"azimuth",
|
||||
"width",
|
||||
"height",
|
||||
"twist",
|
||||
"x_tilt",
|
||||
"y_tilt",
|
||||
]:
|
||||
setattr(self, i, getattr(self._old_values, i))
|
||||
|
||||
def move_to(self, state):
|
||||
# fill in the previous values
|
||||
if self.current_state == PenState.PEN_IS_OUT_OF_RANGE:
|
||||
self._restore()
|
||||
|
||||
print(f"\n *** pen is moving to {state} ***")
|
||||
|
||||
if state == PenState.PEN_IS_OUT_OF_RANGE:
|
||||
self._old_values = copy.copy(self)
|
||||
self.x = 0
|
||||
self.y = 0
|
||||
self.tipswitch = False
|
||||
self.tippressure = 0
|
||||
self.azimuth = 0
|
||||
self.inrange = False
|
||||
self.width = 0
|
||||
self.height = 0
|
||||
self.invert = False
|
||||
self.eraser = False
|
||||
self.x_tilt = 0
|
||||
self.y_tilt = 0
|
||||
self.twist = 0
|
||||
elif state == PenState.PEN_IS_IN_RANGE:
|
||||
self.tipswitch = False
|
||||
self.inrange = True
|
||||
self.invert = False
|
||||
self.eraser = False
|
||||
elif state == PenState.PEN_IS_IN_CONTACT:
|
||||
self.tipswitch = True
|
||||
self.inrange = True
|
||||
self.invert = False
|
||||
self.eraser = False
|
||||
elif state == PenState.PEN_IS_IN_RANGE_WITH_ERASING_INTENT:
|
||||
self.tipswitch = False
|
||||
self.inrange = True
|
||||
self.invert = True
|
||||
self.eraser = False
|
||||
elif state == PenState.PEN_IS_ERASING:
|
||||
self.tipswitch = False
|
||||
self.inrange = True
|
||||
self.invert = True
|
||||
self.eraser = True
|
||||
|
||||
self.current_state = state
|
||||
|
||||
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}"
|
||||
|
||||
def assert_expected_input_events(self, evdev):
|
||||
assert evdev.value[libevdev.EV_ABS.ABS_X] == self.x
|
||||
assert evdev.value[libevdev.EV_ABS.ABS_Y] == self.y
|
||||
assert self.current_state == PenState.from_evdev(evdev)
|
||||
|
||||
@staticmethod
|
||||
def legal_transitions() -> Dict[str, Tuple[PenState, ...]]:
|
||||
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
|
||||
@ -259,7 +364,7 @@ class Pen(object):
|
||||
}
|
||||
|
||||
@staticmethod
|
||||
def legal_transitions_with_invert() -> Dict[str, Tuple[PenState, ...]]:
|
||||
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.
|
||||
@ -297,7 +402,106 @@ class Pen(object):
|
||||
}
|
||||
|
||||
@staticmethod
|
||||
def tolerated_transitions() -> Dict[str, Tuple[PenState, ...]]:
|
||||
def legal_transitions_with_primary_button() -> Dict[str, Tuple["PenState", ...]]:
|
||||
"""We revisit the Windows Pen Implementation state machine:
|
||||
we now have a primary 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 legal_transitions_with_secondary_button() -> Dict[str, Tuple["PenState", ...]]:
|
||||
"""We revisit the Windows Pen Implementation state machine:
|
||||
we now have a secondary button.
|
||||
Note: we don't looks for 2 buttons interactions.
|
||||
"""
|
||||
return {
|
||||
"hover-button": (PenState.PEN_IS_IN_RANGE_WITH_SECOND_BUTTON,),
|
||||
"hover-button -> out-of-range": (
|
||||
PenState.PEN_IS_IN_RANGE_WITH_SECOND_BUTTON,
|
||||
PenState.PEN_IS_OUT_OF_RANGE,
|
||||
),
|
||||
"in-range -> button-press": (
|
||||
PenState.PEN_IS_IN_RANGE,
|
||||
PenState.PEN_IS_IN_RANGE_WITH_SECOND_BUTTON,
|
||||
),
|
||||
"in-range -> button-press -> button-release": (
|
||||
PenState.PEN_IS_IN_RANGE,
|
||||
PenState.PEN_IS_IN_RANGE_WITH_SECOND_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_SECOND_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_SECOND_BUTTON,
|
||||
PenState.PEN_IS_IN_RANGE_WITH_SECOND_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_SECOND_BUTTON,
|
||||
PenState.PEN_IS_IN_CONTACT_WITH_SECOND_BUTTON,
|
||||
PenState.PEN_IS_IN_RANGE_WITH_SECOND_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_SECOND_BUTTON,
|
||||
PenState.PEN_IS_IN_CONTACT_WITH_SECOND_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."""
|
||||
@ -310,7 +514,7 @@ class Pen(object):
|
||||
}
|
||||
|
||||
@staticmethod
|
||||
def tolerated_transitions_with_invert() -> Dict[str, Tuple[PenState, ...]]:
|
||||
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.
|
||||
@ -325,7 +529,7 @@ class Pen(object):
|
||||
}
|
||||
|
||||
@staticmethod
|
||||
def broken_transitions() -> Dict[str, Tuple[PenState, ...]]:
|
||||
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
|
||||
@ -363,6 +567,61 @@ class Pen(object):
|
||||
}
|
||||
|
||||
|
||||
class Pen(object):
|
||||
def __init__(self, x, y):
|
||||
self.x = x
|
||||
self.y = y
|
||||
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",
|
||||
"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}"
|
||||
|
||||
def assert_expected_input_events(self, evdev):
|
||||
assert evdev.value[libevdev.EV_ABS.ABS_X] == self.x
|
||||
assert evdev.value[libevdev.EV_ABS.ABS_Y] == self.y
|
||||
assert self.current_state == PenState.from_evdev(evdev)
|
||||
|
||||
|
||||
class PenDigitizer(base.UHIDTestDevice):
|
||||
def __init__(
|
||||
self,
|
||||
@ -388,6 +647,89 @@ class PenDigitizer(base.UHIDTestDevice):
|
||||
continue
|
||||
self.fields = [f.usage_name for f in r]
|
||||
|
||||
def move_to(self, pen, state):
|
||||
# fill in the previous values
|
||||
if pen.current_state == PenState.PEN_IS_OUT_OF_RANGE:
|
||||
pen.restore()
|
||||
|
||||
print(f"\n *** pen is moving to {state} ***")
|
||||
|
||||
if state == PenState.PEN_IS_OUT_OF_RANGE:
|
||||
pen.backup()
|
||||
pen.x = 0
|
||||
pen.y = 0
|
||||
pen.tipswitch = False
|
||||
pen.tippressure = 0
|
||||
pen.azimuth = 0
|
||||
pen.inrange = False
|
||||
pen.width = 0
|
||||
pen.height = 0
|
||||
pen.invert = False
|
||||
pen.eraser = False
|
||||
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
|
||||
|
||||
def event(self, pen):
|
||||
rs = []
|
||||
r = self.create_report(application=self.cur_application, data=pen)
|
||||
@ -435,10 +777,14 @@ class BaseTest:
|
||||
self.debug_reports(r, uhdev, events)
|
||||
return events
|
||||
|
||||
def validate_transitions(self, from_state, pen, evdev, events):
|
||||
def validate_transitions(
|
||||
self, from_state, pen, evdev, events, allow_intermediate_states
|
||||
):
|
||||
# check that the final state is correct
|
||||
pen.assert_expected_input_events(evdev)
|
||||
|
||||
state = from_state
|
||||
|
||||
# check that the transitions are valid
|
||||
sync_events = []
|
||||
while libevdev.InputEvent(libevdev.EV_SYN.SYN_REPORT) in events:
|
||||
@ -448,12 +794,12 @@ class BaseTest:
|
||||
events = events[idx + 1 :]
|
||||
|
||||
# now check for a valid transition
|
||||
from_state = from_state.apply(sync_events)
|
||||
state = state.apply(sync_events, not allow_intermediate_states)
|
||||
|
||||
if events:
|
||||
from_state = from_state.apply(sync_events)
|
||||
state = state.apply(sync_events, not allow_intermediate_states)
|
||||
|
||||
def _test_states(self, state_list, scribble):
|
||||
def _test_states(self, state_list, scribble, allow_intermediate_states):
|
||||
"""Internal method to test against a list of
|
||||
transition between states.
|
||||
state_list is a list of PenState objects
|
||||
@ -466,9 +812,11 @@ class BaseTest:
|
||||
cur_state = PenState.PEN_IS_OUT_OF_RANGE
|
||||
|
||||
p = Pen(50, 60)
|
||||
p.move_to(PenState.PEN_IS_OUT_OF_RANGE)
|
||||
uhdev.move_to(p, PenState.PEN_IS_OUT_OF_RANGE)
|
||||
events = self.post(uhdev, p)
|
||||
self.validate_transitions(cur_state, p, evdev, events)
|
||||
self.validate_transitions(
|
||||
cur_state, p, evdev, events, allow_intermediate_states
|
||||
)
|
||||
|
||||
cur_state = p.current_state
|
||||
|
||||
@ -477,38 +825,77 @@ class BaseTest:
|
||||
p.x += 1
|
||||
p.y -= 1
|
||||
events = self.post(uhdev, p)
|
||||
self.validate_transitions(cur_state, p, evdev, events)
|
||||
self.validate_transitions(
|
||||
cur_state, p, evdev, events, allow_intermediate_states
|
||||
)
|
||||
assert len(events) >= 3 # X, Y, SYN
|
||||
p.move_to(state)
|
||||
uhdev.move_to(p, state)
|
||||
if scribble and state != PenState.PEN_IS_OUT_OF_RANGE:
|
||||
p.x += 1
|
||||
p.y -= 1
|
||||
events = self.post(uhdev, p)
|
||||
self.validate_transitions(cur_state, p, evdev, events)
|
||||
self.validate_transitions(
|
||||
cur_state, p, evdev, events, allow_intermediate_states
|
||||
)
|
||||
cur_state = p.current_state
|
||||
|
||||
@pytest.mark.parametrize("scribble", [True, False], ids=["scribble", "static"])
|
||||
@pytest.mark.parametrize(
|
||||
"state_list",
|
||||
[pytest.param(v, id=k) for k, v in Pen.legal_transitions().items()],
|
||||
[pytest.param(v, id=k) for k, v in PenState.legal_transitions().items()],
|
||||
)
|
||||
def test_valid_pen_states(self, state_list, scribble):
|
||||
"""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
|
||||
"""
|
||||
self._test_states(state_list, scribble)
|
||||
self._test_states(state_list, scribble, allow_intermediate_states=False)
|
||||
|
||||
@pytest.mark.parametrize("scribble", [True, False], ids=["scribble", "static"])
|
||||
@pytest.mark.parametrize(
|
||||
"state_list",
|
||||
[pytest.param(v, id=k) for k, v in Pen.tolerated_transitions().items()],
|
||||
[
|
||||
pytest.param(v, id=k)
|
||||
for k, v in PenState.tolerated_transitions().items()
|
||||
],
|
||||
)
|
||||
def test_tolerated_pen_states(self, state_list, scribble):
|
||||
"""This is not adhering to the Windows Pen Implementation state machine
|
||||
but we should expect the kernel to behave properly, mostly for historical
|
||||
reasons."""
|
||||
self._test_states(state_list, scribble)
|
||||
self._test_states(state_list, scribble, allow_intermediate_states=True)
|
||||
|
||||
@pytest.mark.skip_if_uhdev(
|
||||
lambda uhdev: "Barrel Switch" not in uhdev.fields,
|
||||
"Device not compatible, missing Barrel Switch usage",
|
||||
)
|
||||
@pytest.mark.parametrize("scribble", [True, False], ids=["scribble", "static"])
|
||||
@pytest.mark.parametrize(
|
||||
"state_list",
|
||||
[
|
||||
pytest.param(v, id=k)
|
||||
for k, v in PenState.legal_transitions_with_primary_button().items()
|
||||
],
|
||||
)
|
||||
def test_valid_primary_button_pen_states(self, state_list, scribble):
|
||||
"""Rework the transition state machine by adding the primary button."""
|
||||
self._test_states(state_list, scribble, allow_intermediate_states=False)
|
||||
|
||||
@pytest.mark.skip_if_uhdev(
|
||||
lambda uhdev: "Secondary Barrel Switch" not in uhdev.fields,
|
||||
"Device not compatible, missing Secondary Barrel Switch usage",
|
||||
)
|
||||
@pytest.mark.parametrize("scribble", [True, False], ids=["scribble", "static"])
|
||||
@pytest.mark.parametrize(
|
||||
"state_list",
|
||||
[
|
||||
pytest.param(v, id=k)
|
||||
for k, v in PenState.legal_transitions_with_secondary_button().items()
|
||||
],
|
||||
)
|
||||
def test_valid_secondary_button_pen_states(self, state_list, scribble):
|
||||
"""Rework the transition state machine by adding the secondary button."""
|
||||
self._test_states(state_list, scribble, allow_intermediate_states=False)
|
||||
|
||||
@pytest.mark.skip_if_uhdev(
|
||||
lambda uhdev: "Invert" not in uhdev.fields,
|
||||
@ -519,7 +906,7 @@ class BaseTest:
|
||||
"state_list",
|
||||
[
|
||||
pytest.param(v, id=k)
|
||||
for k, v in Pen.legal_transitions_with_invert().items()
|
||||
for k, v in PenState.legal_transitions_with_invert().items()
|
||||
],
|
||||
)
|
||||
def test_valid_invert_pen_states(self, state_list, scribble):
|
||||
@ -528,7 +915,7 @@ class BaseTest:
|
||||
to erase.
|
||||
https://docs.microsoft.com/en-us/windows-hardware/design/component-guidelines/windows-pen-states
|
||||
"""
|
||||
self._test_states(state_list, scribble)
|
||||
self._test_states(state_list, scribble, allow_intermediate_states=False)
|
||||
|
||||
@pytest.mark.skip_if_uhdev(
|
||||
lambda uhdev: "Invert" not in uhdev.fields,
|
||||
@ -539,7 +926,7 @@ class BaseTest:
|
||||
"state_list",
|
||||
[
|
||||
pytest.param(v, id=k)
|
||||
for k, v in Pen.tolerated_transitions_with_invert().items()
|
||||
for k, v in PenState.tolerated_transitions_with_invert().items()
|
||||
],
|
||||
)
|
||||
def test_tolerated_invert_pen_states(self, state_list, scribble):
|
||||
@ -548,7 +935,7 @@ class BaseTest:
|
||||
to erase.
|
||||
https://docs.microsoft.com/en-us/windows-hardware/design/component-guidelines/windows-pen-states
|
||||
"""
|
||||
self._test_states(state_list, scribble)
|
||||
self._test_states(state_list, scribble, allow_intermediate_states=True)
|
||||
|
||||
@pytest.mark.skip_if_uhdev(
|
||||
lambda uhdev: "Invert" not in uhdev.fields,
|
||||
@ -557,7 +944,7 @@ class BaseTest:
|
||||
@pytest.mark.parametrize("scribble", [True, False], ids=["scribble", "static"])
|
||||
@pytest.mark.parametrize(
|
||||
"state_list",
|
||||
[pytest.param(v, id=k) for k, v in Pen.broken_transitions().items()],
|
||||
[pytest.param(v, id=k) for k, v in PenState.broken_transitions().items()],
|
||||
)
|
||||
def test_tolerated_broken_pen_states(self, state_list, scribble):
|
||||
"""Those tests are definitely not part of the Windows specification.
|
||||
@ -565,102 +952,7 @@ class BaseTest:
|
||||
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."""
|
||||
self._test_states(state_list, scribble)
|
||||
|
||||
@pytest.mark.skip_if_uhdev(
|
||||
lambda uhdev: "Barrel Switch" not in uhdev.fields,
|
||||
"Device not compatible, missing Barrel Switch usage",
|
||||
)
|
||||
def test_primary_button(self):
|
||||
"""Primary button (stylus) pressed, reports as pressed even while hovering.
|
||||
Actual reporting from the device: hid=TIPSWITCH,BARRELSWITCH,INRANGE (code=TOUCH,STYLUS,PEN):
|
||||
{ 0, 0, 1 } <- hover
|
||||
{ 0, 1, 1 } <- primary button pressed
|
||||
{ 0, 1, 1 } <- liftoff
|
||||
{ 0, 0, 0 } <- leaves
|
||||
"""
|
||||
|
||||
uhdev = self.uhdev
|
||||
evdev = uhdev.get_evdev()
|
||||
|
||||
p = Pen(50, 60)
|
||||
p.inrange = True
|
||||
events = self.post(uhdev, p)
|
||||
assert libevdev.InputEvent(libevdev.EV_KEY.BTN_TOOL_PEN, 1) in events
|
||||
assert evdev.value[libevdev.EV_ABS.ABS_X] == 50
|
||||
assert evdev.value[libevdev.EV_ABS.ABS_Y] == 60
|
||||
assert not evdev.value[libevdev.EV_KEY.BTN_STYLUS]
|
||||
|
||||
p.barrelswitch = True
|
||||
events = self.post(uhdev, p)
|
||||
assert libevdev.InputEvent(libevdev.EV_KEY.BTN_STYLUS, 1) in events
|
||||
|
||||
p.x += 1
|
||||
p.y -= 1
|
||||
events = self.post(uhdev, p)
|
||||
assert len(events) == 3 # X, Y, SYN
|
||||
assert libevdev.InputEvent(libevdev.EV_ABS.ABS_X, 51) in events
|
||||
assert libevdev.InputEvent(libevdev.EV_ABS.ABS_Y, 59) in events
|
||||
|
||||
p.barrelswitch = False
|
||||
events = self.post(uhdev, p)
|
||||
assert libevdev.InputEvent(libevdev.EV_KEY.BTN_STYLUS, 0) in events
|
||||
|
||||
p.inrange = False
|
||||
events = self.post(uhdev, p)
|
||||
assert libevdev.InputEvent(libevdev.EV_KEY.BTN_TOOL_PEN, 0) in events
|
||||
|
||||
@pytest.mark.skip_if_uhdev(
|
||||
lambda uhdev: "Barrel Switch" not in uhdev.fields,
|
||||
"Device not compatible, missing Barrel Switch usage",
|
||||
)
|
||||
def test_contact_primary_button(self):
|
||||
"""Primary button (stylus) pressed, reports as pressed even while hovering.
|
||||
Actual reporting from the device: hid=TIPSWITCH,BARRELSWITCH,INRANGE (code=TOUCH,STYLUS,PEN):
|
||||
{ 0, 0, 1 } <- hover
|
||||
{ 0, 1, 1 } <- primary button pressed
|
||||
{ 1, 1, 1 } <- touch-down
|
||||
{ 1, 1, 1 } <- still touch, scribble on the screen
|
||||
{ 0, 1, 1 } <- liftoff
|
||||
{ 0, 0, 0 } <- leaves
|
||||
"""
|
||||
|
||||
uhdev = self.uhdev
|
||||
evdev = uhdev.get_evdev()
|
||||
|
||||
p = Pen(50, 60)
|
||||
p.inrange = True
|
||||
events = self.post(uhdev, p)
|
||||
assert libevdev.InputEvent(libevdev.EV_KEY.BTN_TOOL_PEN, 1) in events
|
||||
assert evdev.value[libevdev.EV_ABS.ABS_X] == 50
|
||||
assert evdev.value[libevdev.EV_ABS.ABS_Y] == 60
|
||||
assert not evdev.value[libevdev.EV_KEY.BTN_STYLUS]
|
||||
|
||||
p.barrelswitch = True
|
||||
events = self.post(uhdev, p)
|
||||
assert libevdev.InputEvent(libevdev.EV_KEY.BTN_STYLUS, 1) in events
|
||||
|
||||
p.tipswitch = True
|
||||
events = self.post(uhdev, p)
|
||||
assert libevdev.InputEvent(libevdev.EV_KEY.BTN_TOUCH, 1) in events
|
||||
assert evdev.value[libevdev.EV_KEY.BTN_STYLUS]
|
||||
|
||||
p.x += 1
|
||||
p.y -= 1
|
||||
events = self.post(uhdev, p)
|
||||
assert len(events) == 3 # X, Y, SYN
|
||||
assert libevdev.InputEvent(libevdev.EV_ABS.ABS_X, 51) in events
|
||||
assert libevdev.InputEvent(libevdev.EV_ABS.ABS_Y, 59) in events
|
||||
|
||||
p.tipswitch = False
|
||||
events = self.post(uhdev, p)
|
||||
assert libevdev.InputEvent(libevdev.EV_KEY.BTN_TOUCH, 0) in events
|
||||
|
||||
p.barrelswitch = False
|
||||
p.inrange = False
|
||||
events = self.post(uhdev, p)
|
||||
assert libevdev.InputEvent(libevdev.EV_KEY.BTN_TOOL_PEN, 0) in events
|
||||
assert libevdev.InputEvent(libevdev.EV_KEY.BTN_STYLUS, 0) in events
|
||||
self._test_states(state_list, scribble, allow_intermediate_states=True)
|
||||
|
||||
|
||||
class GXTP_pen(PenDigitizer):
|
||||
|
@ -909,7 +909,7 @@ class TestDTH2452Tablet(test_multitouch.BaseTest.TestMultitouch, TouchTabletTest
|
||||
Ensure that the confidence bit being set to false should not result in a touch event.
|
||||
"""
|
||||
uhdev = self.uhdev
|
||||
evdev = uhdev.get_evdev()
|
||||
_evdev = uhdev.get_evdev()
|
||||
|
||||
t0 = test_multitouch.Touch(1, 50, 100)
|
||||
t0.confidence = False
|
||||
@ -917,6 +917,6 @@ class TestDTH2452Tablet(test_multitouch.BaseTest.TestMultitouch, TouchTabletTest
|
||||
events = uhdev.next_sync_events()
|
||||
self.debug_reports(r, uhdev, events)
|
||||
|
||||
slot = self.get_slot(uhdev, t0, 0)
|
||||
_slot = self.get_slot(uhdev, t0, 0)
|
||||
|
||||
assert not events
|
||||
assert not events
|
||||
|
@ -19,12 +19,12 @@ esac
|
||||
SCRIPT_DIR="$(dirname $(realpath $0))"
|
||||
OUTPUT_DIR="$SCRIPT_DIR/results"
|
||||
KCONFIG_REL_PATHS=("${SCRIPT_DIR}/config" "${SCRIPT_DIR}/config.common" "${SCRIPT_DIR}/config.${ARCH}")
|
||||
B2C_URL="https://gitlab.freedesktop.org/mupuf/boot2container/-/raw/master/vm2c.py"
|
||||
B2C_URL="https://gitlab.freedesktop.org/gfx-ci/boot2container/-/raw/main/vm2c.py"
|
||||
NUM_COMPILE_JOBS="$(nproc)"
|
||||
LOG_FILE_BASE="$(date +"hid_selftests.%Y-%m-%d_%H-%M-%S")"
|
||||
LOG_FILE="${LOG_FILE_BASE}.log"
|
||||
EXIT_STATUS_FILE="${LOG_FILE_BASE}.exit_status"
|
||||
CONTAINER_IMAGE="registry.freedesktop.org/libevdev/hid-tools/fedora/37:2023-02-17.1"
|
||||
CONTAINER_IMAGE="registry.freedesktop.org/bentiss/hid/fedora/39:2023-11-22.1"
|
||||
|
||||
TARGETS="${TARGETS:=$(basename ${SCRIPT_DIR})}"
|
||||
DEFAULT_COMMAND="pip3 install hid-tools; make -C tools/testing/selftests TARGETS=${TARGETS} run_tests"
|
||||
@ -32,7 +32,7 @@ DEFAULT_COMMAND="pip3 install hid-tools; make -C tools/testing/selftests TARGETS
|
||||
usage()
|
||||
{
|
||||
cat <<EOF
|
||||
Usage: $0 [-i] [-s] [-d <output_dir>] -- [<command>]
|
||||
Usage: $0 [-j N] [-s] [-b] [-d <output_dir>] -- [<command>]
|
||||
|
||||
<command> is the command you would normally run when you are in
|
||||
the source kernel direcory. e.g:
|
||||
@ -55,6 +55,7 @@ Options:
|
||||
|
||||
-u) Update the boot2container script to a newer version.
|
||||
-d) Update the output directory (default: ${OUTPUT_DIR})
|
||||
-b) Run only the build steps for the kernel and the selftests
|
||||
-j) Number of jobs for compilation, similar to -j in make
|
||||
(default: ${NUM_COMPILE_JOBS})
|
||||
-s) Instead of powering off the VM, start an interactive
|
||||
@ -191,8 +192,9 @@ main()
|
||||
local command="${DEFAULT_COMMAND}"
|
||||
local update_b2c="no"
|
||||
local debug_shell="no"
|
||||
local build_only="no"
|
||||
|
||||
while getopts ':hsud:j:' opt; do
|
||||
while getopts ':hsud:j:b' opt; do
|
||||
case ${opt} in
|
||||
u)
|
||||
update_b2c="yes"
|
||||
@ -207,6 +209,9 @@ main()
|
||||
command="/bin/sh"
|
||||
debug_shell="yes"
|
||||
;;
|
||||
b)
|
||||
build_only="yes"
|
||||
;;
|
||||
h)
|
||||
usage
|
||||
exit 0
|
||||
@ -226,8 +231,7 @@ main()
|
||||
shift $((OPTIND -1))
|
||||
|
||||
# trap 'catch "$?"' EXIT
|
||||
|
||||
if [[ "${debug_shell}" == "no" ]]; then
|
||||
if [[ "${build_only}" == "no" && "${debug_shell}" == "no" ]]; then
|
||||
if [[ $# -eq 0 ]]; then
|
||||
echo "No command specified, will run ${DEFAULT_COMMAND} in the vm"
|
||||
else
|
||||
@ -267,24 +271,26 @@ main()
|
||||
update_kconfig "${kernel_checkout}" "${kconfig_file}"
|
||||
|
||||
recompile_kernel "${kernel_checkout}" "${make_command}"
|
||||
|
||||
if [[ "${update_b2c}" == "no" && ! -f "${b2c}" ]]; then
|
||||
echo "vm2c script not found in ${b2c}"
|
||||
update_b2c="yes"
|
||||
fi
|
||||
|
||||
if [[ "${update_b2c}" == "yes" ]]; then
|
||||
download $B2C_URL $b2c
|
||||
chmod +x $b2c
|
||||
fi
|
||||
|
||||
update_selftests "${kernel_checkout}" "${make_command}"
|
||||
run_vm "${kernel_checkout}" $b2c "${kernel_bzimage}" "${command}"
|
||||
if [[ "${debug_shell}" != "yes" ]]; then
|
||||
echo "Logs saved in ${OUTPUT_DIR}/${LOG_FILE}"
|
||||
fi
|
||||
|
||||
exit $(cat ${OUTPUT_DIR}/${EXIT_STATUS_FILE})
|
||||
if [[ "${build_only}" == "no" ]]; then
|
||||
if [[ "${update_b2c}" == "no" && ! -f "${b2c}" ]]; then
|
||||
echo "vm2c script not found in ${b2c}"
|
||||
update_b2c="yes"
|
||||
fi
|
||||
|
||||
if [[ "${update_b2c}" == "yes" ]]; then
|
||||
download $B2C_URL $b2c
|
||||
chmod +x $b2c
|
||||
fi
|
||||
|
||||
run_vm "${kernel_checkout}" $b2c "${kernel_bzimage}" "${command}"
|
||||
if [[ "${debug_shell}" != "yes" ]]; then
|
||||
echo "Logs saved in ${OUTPUT_DIR}/${LOG_FILE}"
|
||||
fi
|
||||
|
||||
exit $(cat ${OUTPUT_DIR}/${EXIT_STATUS_FILE})
|
||||
fi
|
||||
}
|
||||
|
||||
main "$@"
|
||||
|
Loading…
x
Reference in New Issue
Block a user