| # -*- coding: utf-8 -*- |
| # |
| # Copyright 2015 The Chromium OS Authors. All rights reserved. |
| # Use of this source code is governed by a BSD-style license that can be |
| # found in the LICENSE file. |
| |
| """Tests screen rotation through ChromeOS and accelerometer data. |
| |
| Tests that ChromeOS properly rotates the screen when the device is physically |
| rotated in tablet mode, and also checks that the orientation matches up with |
| accelerometer data if configured. |
| |
| Usage example:: |
| |
| # Only test ChromeOS rotation value. |
| OperatorTest( |
| id='TabletRotation', |
| label_zh=u'平板电脑旋转测试', |
| pytest_name='tablet_rotation', |
| dargs={ |
| 'timeout_secs': HOURS_24, |
| 'prompt_flip_tablet': False, |
| 'prompt_flip_notebook': True, |
| # Include to also check accelerometer data. |
| # 'check_accelerometer': True, |
| # 'degrees_to_orientations': { |
| # 0: {'in_accel_x_base': -1, |
| # 'in_accel_y_base': 0, |
| # 'in_accel_z_base': 0, |
| # 'in_accel_x_lid': -1, |
| # 'in_accel_y_lid': 0, |
| # 'in_accel_z_lid': 0}, |
| # 90: {'in_accel_x_base': 0, |
| # 'in_accel_y_base': 1, |
| # 'in_accel_z_base': 0, |
| # 'in_accel_x_lid': 0, |
| # 'in_accel_y_lid': 1, |
| # 'in_accel_z_lid': 0}, |
| # 180: {'in_accel_x_base': 1, |
| # 'in_accel_y_base': 0, |
| # 'in_accel_z_base': 0, |
| # 'in_accel_x_lid': 1, |
| # 'in_accel_y_lid': 0, |
| # 'in_accel_z_lid': 0}, |
| # 270: {'in_accel_x_base': 0, |
| # 'in_accel_y_base': -1, |
| # 'in_accel_z_base': 0, |
| # 'in_accel_x_lid': 0, |
| # 'in_accel_y_lid': -1, |
| # 'in_accel_z_lid': 0}}, |
| # 'spec_offset': (92, 91 + 61), |
| # 'spec_ideal_values': (0, 1024)}) |
| }) |
| """ |
| |
| import random |
| import time |
| import unittest |
| |
| import factory_common # pylint: disable=unused-import |
| from cros.factory.device import device_utils |
| from cros.factory.test import countdown_timer |
| from cros.factory.test import factory |
| from cros.factory.test.i18n import test_ui as i18n_test_ui |
| from cros.factory.test.pytests import tablet_mode_ui |
| from cros.factory.test import test_ui |
| from cros.factory.test import ui_templates |
| from cros.factory.utils.arg_utils import Arg |
| from cros.factory.utils import process_utils |
| |
| |
| _DEFAULT_TIMEOUT = 30 |
| _UNICODE_PICTURES = u'☃☺☎' |
| _TEST_DEGREES = [90, 180, 270, 0] |
| _POLL_ROTATION_INTERVAL = 0.1 |
| |
| _MSG_PROMPT_ROTATE_TABLET = i18n_test_ui.MakeI18nLabel( |
| 'Rotate the tablet to correctly align the picture, holding it at an ' |
| 'upright 90-degree angle.') |
| |
| _ID_COUNTDOWN_TIMER = 'countdown-timer' |
| _ID_PROMPT = 'prompt' |
| _ID_PICTURE = 'picture' |
| |
| _HTML_COUNTDOWN_TIMER = '<div id="%s" class="countdown-timer"></div>' % ( |
| _ID_COUNTDOWN_TIMER) |
| _HTML_BUILD_PICTURE = lambda degrees, picture: ( |
| '<span style="-webkit-transform: rotate(%sdeg);">%s</span>' |
| % (degrees, picture)) |
| _HTML = """ |
| <div class="prompt" id="%s"></div> |
| <div class="picture" id="%s"></div> |
| """ % (_ID_PROMPT, _ID_PICTURE) |
| |
| _CSS_COUNTDOWN_TIMER = """ |
| .countdown-timer { |
| position: absolute; |
| bottom: .3em; |
| right: .5em; |
| font-size: 2em; |
| } |
| """ |
| _CSS = """ |
| .prompt { |
| font-size: 2em; |
| margin-bottom: 1em; |
| } |
| .picture span { |
| font-size: 15em; |
| display: block; |
| } |
| """ |
| |
| |
| class TabletRotationTest(unittest.TestCase): |
| """Tablet rotation factory test.""" |
| ARGS = [ |
| Arg('timeout_secs', int, 'Timeout value for the test.', |
| default=_DEFAULT_TIMEOUT, optional=True), |
| Arg('prompt_flip_tablet', bool, |
| 'Assume the notebook is not yet in tablet mode, and operator should ' |
| 'first be instructed to flip it as such. (This is useful to unset if ' |
| 'the previous test finished in tablet mode.)', |
| default=True, optional=True), |
| Arg('prompt_flip_notebook', bool, |
| 'After the test, prompt the operator to flip back into notebook ' |
| 'mode. (This is useful to unset if the next test requires tablet ' |
| 'mode.)', |
| default=True, optional=True), |
| Arg('check_accelerometer', bool, |
| 'In addition to checking the ChromeOS screen orientation, also check ' |
| 'accelerometer data to ensure it reports the same orientation.', |
| default=False, optional=True), |
| Arg('degrees_to_orientations', dict, |
| 'Keys: degree of the orientation, limited to [0, 90, 180, 270]. ' |
| 'Values: a dictionary containing orientation configuration. Keys ' |
| 'should be the name of the accelerometer signal. For example, ' |
| '"in_accel_x_base" or "in_accel_x_lid". The possible keys are ' |
| '"in_accel_(x|y|z)_(base|lid)". Values should be one of [0, 1, -1], ' |
| 'representing the ideal value for gravity under such orientation.', |
| default={}, optional=True), |
| Arg('spec_offset', tuple, |
| 'A tuple of two integers, ex: (128, 230) ' |
| 'indicating the tolerance for the digital output of sensors under ' |
| 'zero gravity and one gravity. Those values are vendor-specific ' |
| 'and should be provided by the vendor.', optional=True), |
| Arg('spec_ideal_values', tuple, |
| 'A tuple of two integers, ex: (0, 1024) indicating the ideal value ' |
| 'of digital output corresponding to 0G and 1G, respectively. For ' |
| 'example, if a sensor has a 12-bit digital output and -/+ 2G ' |
| 'detection range so the sensitivity is 1024 count/G. The value ' |
| 'should be provided by the vendor.', optional=True), |
| Arg('sample_rate_hz', int, |
| 'The sample rate in Hz to get raw data from ' |
| 'accelerometers.', default=20, optional=True), |
| ] |
| |
| def setUp(self): |
| # args.check_accelerometer implies the following required arguments: |
| # degrees_to_orientations |
| # spec_offset |
| # spec_ideal_values |
| self.dut = device_utils.CreateDUTInterface() |
| self.accel_controller = None |
| if self.args.check_accelerometer: |
| if not all([self.args.degrees_to_orientations, |
| self.args.spec_offset, |
| self.args.spec_ideal_values]): |
| self.fail('If running in check_accelerometer mode, please provide ' |
| 'arguments degrees_to_orientations, spec_offset ' |
| 'and spec_ideal_values.') |
| return |
| |
| self.accel_controller = self.dut.accelerometer.GetController() |
| |
| self.ui = test_ui.UI() |
| self.state = factory.get_state_instance() |
| self.tablet_mode_ui = tablet_mode_ui.TabletModeUI(self.ui, |
| _HTML_COUNTDOWN_TIMER, |
| _CSS_COUNTDOWN_TIMER) |
| |
| # Create a thread to run countdown timer. |
| countdown_timer.StartCountdownTimer( |
| self.args.timeout_secs, |
| lambda: self.ui.Fail('Tablet rotation test failed due to timeout.'), |
| self.ui, |
| _ID_COUNTDOWN_TIMER) |
| |
| # Create a thread to control UI flow. |
| def _UIFlow(): |
| if self.args.prompt_flip_tablet: |
| self.tablet_mode_ui.AskForTabletMode( |
| lambda _: self.TestRotationUIFlow()) |
| else: |
| self.TestRotationUIFlow() |
| process_utils.StartDaemonThread(target=_UIFlow) |
| |
| def TestRotationUIFlow(self, degrees_targets=None): |
| if degrees_targets is None: |
| degrees_targets = _TEST_DEGREES |
| |
| for degrees_target in degrees_targets: |
| # Initialize UI template, HTML and CSS. |
| template = ui_templates.OneSection(self.ui) |
| template.SetState(_HTML + _HTML_COUNTDOWN_TIMER) |
| self.ui.AppendCSS(_CSS + _CSS_COUNTDOWN_TIMER) |
| |
| try: |
| self._PromptAndWaitForRotation(degrees_target) |
| except Exception as e: |
| self.ui.Fail(e.msg) |
| return |
| |
| self.tablet_mode_ui.FlashSuccess() |
| |
| if self.args.prompt_flip_notebook: |
| self.tablet_mode_ui.AskForNotebookMode(lambda _: self.ui.Pass()) |
| else: |
| self.ui.Pass() |
| |
| |
| def _PromptAndWaitForRotation(self, degrees_target): |
| # Choose a new picture and set the prompt message. |
| rand_int = random.randint(0, len(_UNICODE_PICTURES) - 1) |
| picture = _UNICODE_PICTURES[rand_int] |
| self.ui.SetHTML(_MSG_PROMPT_ROTATE_TABLET, id=_ID_PROMPT) |
| |
| degrees_previous = None |
| while True: |
| # Get current rotation. |
| degrees_current = self._GetCurrentDegrees() |
| |
| # TODO(kitching): Research disabling ChromeOS screen rotation when in |
| # factory mode. |
| # |
| # When the device is physically rotated, ChromeOS rotates the screen |
| # accordingly. If this wasn't the case, we could simply paint the |
| # picture in the desired orientation, and have the operator rotate the |
| # device appropriately: |
| # |
| # | > | | ^ | | < | |
| # |
| # But, because ChromeOS automatically rotates the screen, if we keep the |
| # picture's orientation the same, it would *always* face the same |
| # direction, regardless of how the tablet is rotated. (This makes |
| # perfect sense. We always want the UI to face the user in the correct |
| # orientation.) The picture would look like this: |
| # |
| # | > | | > | | > | |
| # |
| # Thus, instructing the operator to align the picture to an upright |
| # position becomes a fruitless effort. So, we need to offset this |
| # change by also rotating our picture every time the screen is rotated. |
| # We set the rotation via CSS using degrees_delta as the angle. |
| degrees_delta = degrees_target - degrees_current |
| success = (degrees_delta == 0) |
| |
| # Check accelerometer if necessary. |
| if (success and |
| self.accel_controller and |
| degrees_target in self.args.degrees_to_orientations): |
| orientations = self.args.degrees_to_orientations[degrees_target] |
| cal_data = self.accel_controller.GetData( |
| sample_rate=self.args.sample_rate_hz) |
| if not self.accel_controller.IsWithinOffsetRange( |
| cal_data, orientations, self.args.spec_ideal_values, |
| self.args.spec_offset): |
| success = False |
| |
| # Are we currently at our target? |
| if success: |
| return True |
| # If the device has been rotated, we also need to update our picture's |
| # orientation accordingly (see comment above describing degrees_delta). |
| elif degrees_previous != degrees_current: |
| self.ui.SetHTML(_HTML_BUILD_PICTURE(degrees_delta, picture), |
| id=_ID_PICTURE) |
| |
| # Target has still not been reached. Sleep and continue. |
| degrees_previous = degrees_current |
| time.sleep(_POLL_ROTATION_INTERVAL) |
| |
| def _GetCurrentDegrees(self): |
| display_info = None |
| try: |
| display_info = self.state.DeviceGetDisplayInfo() |
| except Exception: |
| pass |
| if not display_info: |
| raise Exception('Failed to get display_info') |
| |
| display_info = [info for info in display_info if info['isPrimary']] |
| if len(display_info) != 1: |
| raise Exception('Failed to get internal display') |
| |
| return display_info[0]['rotation'] |
| |
| def runTest(self): |
| self.ui.Run() |