blob: 30330631ae8fdcce8994edef5a1df3c33d6ba6de [file] [log] [blame]
# Copyright 2016 The ChromiumOS Authors
# Use of this source code is governed by a BSD-style license that can be
# found in the LICENSE file.
"""Test stylus functionality.
Description
-----------
Verifies if stylus is functional by asking operator to draw specified lines or
shapes using stylus.
For EMR stylus, drawing a diagonal line from left-bottom to right-top corner
should be sufficient to validate all scan lines. But for clamshells with hall
sensor, the magnet may cause EMR stylus to be non-functional in particular area.
To test that, set argument `endpoints_ratio` to build the lines for operator to
draw.
Test Procedure
--------------
1. When started, a diagonal line is displayed on screen.
2. Operator must use stylus to draw and follow the displayed line.
3. If the stylus moved too far (specified in argument `error_margin`) from the
requested path, test will fail.
Dependency
----------
- Based on Linux evdev.
Examples
--------
To check stylus functionality by drawing a diagonal line, add this in test
list:
.. test_list::
generic_touchscreen_examples:TouchscreenTests.Stylus
To check if the magnet in left side will cause problems, add this in test list
to draw a line from left-top to left-bottom:
.. test_list::
generic_touchscreen_examples:TouchscreenTests.StylusTopLeftToBottomLeft
"""
import threading
from cros.factory.test.i18n import _
from cros.factory.test import state
from cros.factory.test import test_case
from cros.factory.test import test_ui
from cros.factory.test.utils import evdev_utils
from cros.factory.test.utils import touch_monitor
from cros.factory.utils.arg_utils import Arg
from cros.factory.utils import sync_utils
from cros.factory.external.py_lib import evdev
class StylusMonitor(touch_monitor.SingleTouchMonitor):
def __init__(self, device, ui):
super().__init__(device)
self._ui = ui
self._lock = threading.RLock()
self._buffer = []
@sync_utils.Synchronized
def OnMove(self):
"""See SingleTouchMonitor.OnMove."""
cur_state = self.GetState()
# yapf: disable
if cur_state.keys[evdev.ecodes.BTN_TOUCH]: # type: ignore #TODO(b/338318729) Fixit! # pylint: disable=line-too-long
# yapf: enable
# Instead of directly call JavaScript function 'handler' here, we buffer
# the events to reduce the latency from CallJSFunction.
self._buffer.append([cur_state.x, cur_state.y])
@sync_utils.Synchronized
def Flush(self):
if self._buffer:
self._ui.CallJSFunction('handler', self._buffer)
self._buffer = []
class StylusTest(test_case.TestCase):
"""Stylus factory test."""
related_components = (
test_case.TestCategory.EMR_IC,
test_case.TestCategory.USI_CONTROLLER,
)
ARGS = [
Arg('device_filter', (int, str, list), 'Stylus input event id, evdev '
'name, or evdev events.', default=None),
Arg('error_margin', int,
'Maximum tolerable distance to the diagonal line (in pixel).',
default=25),
Arg(
'begin_ratio', float,
'The beginning position of the diagonal line segment to check. '
'Should be in (0, 1).', default=0.01),
Arg(
'end_ratio', float,
'The ending position of the diagonal line segment to check. '
'Should be in (0, 1).', default=0.99),
Arg(
'step_ratio', float,
'If the distance between an input event to the latest accepted '
'input event is larger than this size, it would be ignored. '
'Should be in (0, 1).', default=0.01),
Arg(
'endpoints_ratio', list,
'A list of two pairs, each pair contains the X and Y coordinates '
'ratio of an endpoint of the line segment for operator to draw. '
'Both endpoints must be on the border '
'(e.g., X=0 or X=1 or Y=0 or Y=1).', default=[[0, 1], [1, 0]]),
Arg(
'autostart', bool,
'Starts the test automatically without prompting. Operators can '
'still press ESC to fail the test.', default=False),
Arg('flush_interval', float,
'The time interval of flushing event buffers.', default=0.1),
Arg(
'angle_compensation', touch_monitor.AngleCompensation,
'Specify a degree to compensate the orientation difference between '
'panel and system in counter-clockwise direction. It is used when '
'panel scanout is different from default system orientation, i.e., '
'a angle difference between the instruction line displayed on '
'screen and the position read from evdev.', default=0)
]
def setUp(self):
filters = [evdev_utils.IsStylusDevice]
# yapf: disable
if isinstance(self.args.device_filter, list): # type: ignore #TODO(b/338318729) Fixit! # pylint: disable=line-too-long
# yapf: enable
# yapf: disable
filters += self.args.device_filter # type: ignore #TODO(b/338318729) Fixit! # pylint: disable=line-too-long
# yapf: enable
else:
# yapf: disable
filters += [self.args.device_filter] # type: ignore #TODO(b/338318729) Fixit! # pylint: disable=line-too-long
# yapf: enable
self._device = evdev_utils.FindDevice(*filters)
self._monitor = None
self._dispatcher = None
self._daemon = None
self._state = state.GetInstance()
# yapf: disable
assert self.args.error_margin >= 0 # type: ignore #TODO(b/338318729) Fixit! # pylint: disable=line-too-long
# yapf: enable
# yapf: disable
assert 0 < self.args.begin_ratio < self.args.end_ratio < 1 # type: ignore #TODO(b/338318729) Fixit! # pylint: disable=line-too-long
# yapf: enable
# yapf: disable
assert 0 < self.args.step_ratio < 1 # type: ignore #TODO(b/338318729) Fixit! # pylint: disable=line-too-long
# yapf: enable
# yapf: disable
assert len(self.args.endpoints_ratio) == 2 # type: ignore #TODO(b/338318729) Fixit! # pylint: disable=line-too-long
# yapf: enable
# yapf: disable
assert self.args.endpoints_ratio[0] != self.args.endpoints_ratio[1] # type: ignore #TODO(b/338318729) Fixit! # pylint: disable=line-too-long
# yapf: enable
# yapf: disable
for point in self.args.endpoints_ratio: # type: ignore #TODO(b/338318729) Fixit! # pylint: disable=line-too-long
# yapf: enable
assert isinstance(point, list) and len(point) == 2
assert all(0 <= x_or_y <= 1 for x_or_y in point)
assert any(x_or_y in [0, 1] for x_or_y in point)
# yapf: disable
assert self.args.flush_interval > 0 # type: ignore #TODO(b/338318729) Fixit! # pylint: disable=line-too-long
# yapf: enable
def tearDown(self):
if self._dispatcher is not None:
self._dispatcher.Close()
self._device.ungrab()
self._SetInternalDisplayRotation(-1)
def runTest(self):
# yapf: disable
self.ui.BindStandardFailKeys() # type: ignore #TODO(b/338318729) Fixit! # pylint: disable=line-too-long
# yapf: enable
# yapf: disable
if not self.args.autostart: # type: ignore #TODO(b/338318729) Fixit! # pylint: disable=line-too-long
# yapf: enable
# yapf: disable
self.ui.SetHTML( # type: ignore #TODO(b/338318729) Fixit! # pylint: disable=line-too-long
# yapf: enable
_('Please extend the green line with stylus to the other end.<br>'
'Stay between the two red lines.<br>'
'Press SPACE to start; Esc to fail.'),
id='msg')
# yapf: disable
self.ui.WaitKeysOnce(test_ui.SPACE_KEY) # type: ignore #TODO(b/338318729) Fixit! # pylint: disable=line-too-long
# yapf: enable
# yapf: disable
self._SetInternalDisplayRotation(self.args.angle_compensation) # type: ignore #TODO(b/338318729) Fixit! # pylint: disable=line-too-long
# yapf: enable
# Waits the screen rotates, then starts the test.
self.Sleep(1)
# yapf: disable
self.ui.CallJSFunction('setupStylusTest', self.args.error_margin, # type: ignore #TODO(b/338318729) Fixit! # pylint: disable=line-too-long
# yapf: enable
# yapf: disable
self.args.begin_ratio, self.args.end_ratio, # type: ignore #TODO(b/338318729) Fixit! # pylint: disable=line-too-long
# yapf: enable
# yapf: disable
self.args.step_ratio, self.args.endpoints_ratio) # type: ignore #TODO(b/338318729) Fixit! # pylint: disable=line-too-long
# yapf: enable
self._device = evdev_utils.DeviceReopen(self._device)
self._device.grab()
self._monitor = StylusMonitor(self._device, self.ui)
self._dispatcher = evdev_utils.InputDeviceDispatcher(self._device,
self._monitor.Handler)
self._dispatcher.StartDaemon()
while True:
self._monitor.Flush()
# yapf: disable
self.Sleep(self.args.flush_interval) # type: ignore #TODO(b/338318729) Fixit! # pylint: disable=line-too-long
# yapf: enable
def _SetInternalDisplayRotation(self, degree):
# degree should be one of [0, 90, 180, 270, -1], where -1 means auto-rotate
display_id = self._GetInternalDisplayInfo()['id']
self._state.DeviceSetDisplayProperties(display_id, {"rotation": degree})
def _GetInternalDisplayInfo(self):
display_info = self._state.DeviceGetDisplayInfo()
display_info = [info for info in display_info if info['isInternal']]
if len(display_info) != 1:
self.fail('Failed to get internal display.')
return display_info[0]