| # 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] |