blob: 9ee165ce36a9361b0e7ac25e4bde2b30305a9d6d [file] [log] [blame]
# -*- coding: utf-8 -*-
#
# Copyright 2015 The ChromiumOS Authors
# 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.
Description
-----------
Tests that the accelerometer data matches the orientation when the device is
physically rotated.
Test Procedure
--------------
1. A picture would be shown on the screen. Operator should rotate the tablet to
align with the image. This would repeat four times with each orientations,
and the test automatically pass when the desired orientation is reached.
Dependency
----------
``chrome.display.system.getInfo`` in Chrome extension to get screen
information. Refer to https://developer.chrome.com/apps/system_display for
more information.
``cros.factory.device.accelerometer`` is used to determine device orientation.
Examples
--------
To test screen rotation, and have a timeout of an hour:
.. test_list::
generic_ec_component_accel_examples:TabletRotation
To provide more parameters for accelerometer when testing:
.. test_list::
generic_ec_component_accel_examples:ScreenRotation.TabletRotationAll
To test screen rotation for Chrome and prompt operator to flip before and after
the test, we can combine the test with `tablet_mode.py <./tablet_mode.html>`_:
.. test_list::
generic_ec_component_accel_examples:ScreenRotation
"""
import enum
import math
from cros.factory.device import device_utils
from cros.factory.test.i18n import _
from cros.factory.test import state
from cros.factory.test import test_case
from cros.factory.utils.arg_utils import Arg
from cros.factory.utils.schema import JSONSchemaDict
_TEST_DEGREES = ['90', '180', '270', '0']
_POLL_ROTATION_INTERVAL = 0.01
_VALID_LOCATIONS = ['base', 'lid']
_RADIAN_TO_DEGREE = 180 / math.pi
_ARG_DEGREES_TO_ORIENTATION_SCHEMA = JSONSchemaDict(
'degrees_to_orientation schema object', {
'type': 'object',
'patternProperties': {
'(base|lid)': {
'type': 'object',
'patternProperties': {
'(0|90|180|270)': {
'type': 'object',
'patternProperties': {
'in_accel_(x|y|z)': {
'enum': [0, 1, -1]
}
},
'additionalProperties': False
}
},
'additionalProperties': False
}
},
'additionalProperties': False
})
class TabletRotationTest(test_case.TestCase):
"""Tablet rotation factory test."""
related_components = (test_case.TestCategory.ACCELEROMETER, )
ARGS = [
Arg('timeout_secs', int, 'Timeout value for the test.', default=30),
Arg(
'degrees_to_orientations', dict,
'The keys should be "base" or "lid", which are the locations of the '
'accelerometer. And each value is a dict of (key, value) pair as '
'follows: '
'Keys: degrees of the orientations, ["0", "90", "180", "270"]. '
'Values: a dictionary containing orientation configuration. Keys '
'should be the name of the accelerometer signal. The possible keys '
'are "in_accel_(x|y|z)". Values should be one of [0, 1, -1], '
'representing the ideal value for gravity under such orientation.',
default={
'base': {
'0': {
'in_accel_x': 0,
'in_accel_y': -1,
'in_accel_z': 0
},
'90': {
'in_accel_x': 1,
'in_accel_y': 0,
'in_accel_z': 0
},
'180': {
'in_accel_x': 0,
'in_accel_y': 1,
'in_accel_z': 0
},
'270': {
'in_accel_x': -1,
'in_accel_y': 0,
'in_accel_z': 0
}
},
'lid': {
'0': {
'in_accel_x': 0,
'in_accel_y': 1,
'in_accel_z': 0
},
'90': {
'in_accel_x': 1,
'in_accel_y': 0,
'in_accel_z': 0
},
'180': {
'in_accel_x': 0,
'in_accel_y': -1,
'in_accel_z': 0
},
'270': {
'in_accel_x': -1,
'in_accel_y': 0,
'in_accel_z': 0
}
}
}, schema=_ARG_DEGREES_TO_ORIENTATION_SCHEMA),
Arg(
'spec_offset', list, 'Two numbers, ex: [1.5, 1.5] '
'indicating the tolerance for the digital output of sensors under '
'zero gravity and one gravity.', default=[1, 1]),
Arg('sample_rate_hz', int, 'The sample rate in Hz to get raw data from '
'accelerometers.', default=200),
Arg('mode', enum.Enum('mode', ['classic', 'animation']),
'The mode for showing instruction of the test', default='classic'),
]
def setUp(self):
self.accel_controllers = {}
self.dut = device_utils.CreateDUTInterface()
for location, dic in self.args.degrees_to_orientations.items():
self.accel_controllers[location] = self.dut.accelerometer.GetController(
location=location)
if (not set(dic).issubset(set(_TEST_DEGREES)) or
location not in _VALID_LOCATIONS):
self.fail(
'Please provide proper arguments for degrees_to_orientations.')
self.state = state.GetInstance()
self._SetInternalDisplayRotation(0)
def tearDown(self):
self._SetInternalDisplayRotation(-1)
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]
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 _SetStyle(self, div_id, key, value):
self.ui.RunJS(f'document.getElementById("{div_id}")'
f'.style.{key} = "{value}"')
def _PromptAndWaitForRotation(self, degree, need_test_locations):
while need_test_locations:
self.Sleep(_POLL_ROTATION_INTERVAL)
if not self._GetInternalDisplayInfo()['isAutoRotationAllowed']:
# Auto rotation is allowed when the device is in a physical tablet
# state or kSupportsClamshellAutoRotation is set.
# So, if kSupportsClamshellAutoRotation is set, the value would be
# true even if the tablet mode switch is off (false).
# But this should be fine, because we will run the "tablet_mode" test
# before running this test, and the test will check the "tablet mode
# switch" in evtest.
# To set the `kSupportsClamshellAutoRotation`, one can edit the
# `init/goofy.d/chrome_dev.conf` and add one line to enable the option
# `--supports-clamshell-auto-rotation`.
self.fail('Auto rotation is not allowed.')
for loc, dic in self.args.degrees_to_orientations.items():
if loc not in need_test_locations:
continue
orientations = dic[degree]
cal_data = self.accel_controllers[loc].GetData(
sample_rate=self.args.sample_rate_hz)
if self.args.mode == "animation":
rotate_x, rotate_y, rotate_z = self.CalculateRotateDegree(
loc, cal_data)
self.RotateImage(loc, rotate_x, rotate_y, rotate_z)
if self.accel_controllers[loc].IsWithinOffsetRange(
cal_data, orientations, self.args.spec_offset):
need_test_locations.remove(loc)
self._SetStyle(loc, 'color', 'green')
def runTest(self):
self.ui.StartFailingCountdownTimer(self.args.timeout_secs)
self.ui.SetInstruction(
_('Rotate the tablet to correctly align the picture, holding it at '
'an upright 90-degree angle.'))
self.SetImageAndMode('chromebook_lid_rotate.png',
'chromebook_base_rotate.png', 'chromebook_lid.png',
'chromebook_base.png')
for degree in _TEST_DEGREES:
need_test_locations = set()
for loc, dic in self.args.degrees_to_orientations.items():
if degree not in dic:
self._SetStyle(loc, 'display', 'none')
continue
need_test_locations.add(loc)
self._SetStyle(loc, 'color', '')
self._SetStyle(loc, 'transform', f'rotate({degree}deg)')
self._SetStyle(loc, 'display', 'block')
self._SetStyle(f'{loc}_instruction', 'rotate', f'z {90-int(degree)}deg')
self._SetStyle(f'{loc}_instruction', 'display', 'inline-block')
self._SetStyle(f'chromebook_{loc}', 'display', 'inline-block')
if not need_test_locations:
continue
self.ui.SetView('main')
self._PromptAndWaitForRotation(degree, need_test_locations)
self.ui.SetView('success')
self.Sleep(1)
def SetImageAndMode(self, lid_instruction_url, base_instruction_url, lid_url,
base_url):
"""Sets the image src and the display mode."""
self.ui.RunJS('document.getElementById("lid_instruction").src = args.url;',
url=lid_instruction_url)
self.ui.RunJS('document.getElementById("base_instruction").src = args.url;',
url=base_instruction_url)
self.ui.RunJS('document.getElementById("chromebook_lid").src = args.url;',
url=lid_url)
self.ui.RunJS('document.getElementById("chromebook_base").src = args.url;',
url=base_url)
if self.args.mode == 'classic':
self._SetStyle('animation', 'display', 'none')
elif self.args.mode == 'animation':
self._SetStyle('classic', 'display', 'none')
def RotateImage(self, loc, degree_x, degree_y, degree_z):
"""Rotates the image according to the degree captured."""
if loc == 'lid':
self._SetStyle(
'chromebook_lid', 'transform', f'rotateX({degree_x}deg) '
f'rotateY({degree_y}deg) rotateZ({degree_z}deg)')
elif loc == 'base':
self._SetStyle(
'chromebook_base', 'transform', f'rotateX({degree_x}deg) '
f'rotateY({degree_y}deg) rotateZ({degree_z}deg)')
def CalculateRotateDegree(self, loc, cal_data):
accel_x = cal_data['in_accel_x']
accel_y = cal_data['in_accel_y']
accel_z = cal_data['in_accel_z']
degree_x = int(math.atan2(accel_y, accel_z) * _RADIAN_TO_DEGREE)
degree_y = int(math.atan2(accel_x, accel_z) * _RADIAN_TO_DEGREE)
degree_z = int(math.atan2(accel_y, accel_x) * _RADIAN_TO_DEGREE)
if loc == 'lid':
degree_x, degree_y, degree_z = 0, 0, degree_z - 90
elif loc == 'base':
degree_x, degree_y, degree_z = 0, 0, -90 - degree_z
return degree_x, degree_y, degree_z