blob: e329f43d5f2d58b4928ecebdb2ed6dfca179b063 [file] [log] [blame]
# -*- 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.
Description
-----------
Tests that ChromeOS properly rotates the screen when the device is physically
rotated in tablet mode.
If ``check_accelerometer`` is set, also checks that the orientation matches up
with accelerometer data.
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.
2. If ``check_accelerometer`` is set, the test would also check if the value of
accelerometer is within acceptable range for each orientation.
Dependency
----------
``chrome.display.system.getInfo`` in Chrome extension to get screen
orientation. Refer to https://developer.chrome.com/apps/system_display for
more information.
If ``check_accelerometer`` is set, also depends on device API
``cros.factory.device.accelerometer``.
Examples
--------
To test screen rotation for Chrome, add this in test list::
{
"pytest_name": "tablet_rotation"
}
To test screen rotation, and have a timeout of 10 minutes::
{
"pytest_name": "tablet_rotation",
"args": {
"timeout_secs": 600
}
}
To also check accelerometer when testing screen rotation::
{
"pytest_name": "tablet_rotation",
"args": {
"check_accelerometer": true,
"degrees_to_orientations": [
[
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
}
]
],
"spec_offset": [0.5, 0.5]
}
}
To test screen rotation for Chrome and prompt operator to flip before and after
the test::
{
"subtests": [
{
"pytest_name": "tablet_mode",
"args": {
"prompt_flip_tablet": true
}
},
{
"pytest_name": "tablet_rotation"
},
{
"pytest_name": "tablet_mode",
"args": {
"prompt_flip_notebook": true
}
}
]
}
"""
import random
import factory_common # pylint: disable=unused-import
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
_UNICODE_PICTURES = u'☃☺☎'
_TEST_DEGREES = [90, 180, 270, 0]
_POLL_ROTATION_INTERVAL = 0.1
class TabletRotationTest(test_case.TestCase):
"""Tablet rotation factory test."""
ARGS = [
Arg('timeout_secs', int, 'Timeout value for the test.', default=30),
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),
Arg('accelerometer_location', str,
'If check_accelerometer is true, the location of accelerometer that '
'should be checked. Should be either "lid" or "base"',
default='lid'),
Arg('degrees_to_orientations', list,
'A list of [key, value] pairs. '
'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. 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=[]),
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=None),
Arg('sample_rate_hz', int,
'The sample rate in Hz to get raw data from '
'accelerometers.', default=20),
]
def setUp(self):
# args.check_accelerometer implies the following required arguments:
# degrees_to_orientations
# spec_offset
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.fail('If running in check_accelerometer mode, please provide '
'arguments degrees_to_orientations and spec_offset.')
self.accel_controller = self.dut.accelerometer.GetController(
location=self.args.accelerometer_location)
self.degrees_to_orientations = dict(self.args.degrees_to_orientations)
self.state = state.GetInstance()
def _PromptAndWaitForRotation(self, degrees_target):
self.ui.SetInstruction(
_('Rotate the tablet to correctly align the picture, holding it at '
'an upright 90-degree angle.'))
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)
# If the device has been rotated, we also need to update our picture's
# orientation accordingly (see comment above describing degrees_delta).
if degrees_previous != degrees_current:
self.ui.RunJS('document.getElementById("picture").style.transform = '
'"rotate(%ddeg)"' % degrees_delta)
if degrees_previous is None:
# This is the first iteration, show a random picture.
self.ui.SetHTML(random.choice(_UNICODE_PICTURES), id='picture')
# Check accelerometer if necessary.
if (success and
self.accel_controller and
degrees_target in self.degrees_to_orientations):
orientations = self.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_offset):
success = False
# Are we currently at our target?
if success:
return
# Target has still not been reached. Sleep and continue.
degrees_previous = degrees_current
self.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.StartFailingCountdownTimer(self.args.timeout_secs)
for degrees_target in _TEST_DEGREES:
self.ui.SetView('main')
self._PromptAndWaitForRotation(degrees_target)
self.ui.SetView('success')
self.Sleep(1)