blob: adca042d3268ac3d1937b4823396fb5358997c06 [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.
"""A factory test for gyroscopes.
Description
-----------
There are steps required to run a complete gyroscope test::
- Motion sensor setup via `ectool motionsense odr ${gyro_id} ${freq}`
- (optional) Calibration for tri-axis (x, y, and z) gyroscopes.
- The main gyroscope test.
This pytest executes the motion sensor setup and main gyro test in sequence.
Test Procedure
--------------
This test supports and enables auto start by default. In this case:
1. Put the device (base/lid) on a static plane then press space.
2. Wait for completion.
Otherwise operators will be asked to place DUT on a horizontal plane and
press space.
Dependency
----------
- Device API (``cros.factory.device.gyroscope``).
Examples
--------
To run a test on base gyroscope:
.. test_list::
generic_ec_component_accel_examples:Gyroscope
"""
import collections
import enum
import logging
import re
import statistics
import time
from cros.factory.device import device_utils
from cros.factory.device import gyroscope
from cros.factory.test.i18n import _
from cros.factory.test import test_case
from cros.factory.test import test_ui
from cros.factory.utils.arg_utils import Arg
from cros.factory.utils import sync_utils
_RADIAN_TO_DEGREE = gyroscope.RADIAN_TO_DEGREE
_DEFAULT_POLL_INTERVAL = 0
class Gyroscope(test_case.TestCase):
related_components = (test_case.TestCategory.ACCELEROMETER, )
ARGS = [
Arg('rotation_threshold', float,
'The expected value (rad/s) to read when dut start rotating.'),
Arg('stop_threshold', float,
'The expected value to read when dut stop moving.'),
Arg('gyro_id', int,
'Gyroscope ID. Will read a default ID via ectool if not set.',
default=None),
Arg(
'freq', int,
'Gyroscope sampling frequency in mHz. Will apply the minimal '
'frequency from ectool info if not set.', default=None),
Arg('timeout_secs', int,
'Timeout in seconds for gyro to return expected value.', default=30),
Arg('setup_time_secs', int, 'Seconds to wait before starting the test.',
default=2),
Arg('autostart', bool, 'Auto start this test.', default=False),
Arg('setup_sensor', bool, 'Setup gyro sensor via ectool', default=True),
Arg('location', enum.Enum('location', ['base', 'lid']),
'Gyro is located in "base" or "lid".', default='base'),
Arg('capture_count', int,
'How many records to read for each time getting data', default=50),
Arg('sample_rate', int, 'Sample rate in Hz to read data from sensors',
default=200)
]
def setUp(self):
self.dut = device_utils.CreateDUTInterface()
self.gyroscope = self.dut.gyroscope.GetController(
location=self.args.location,
gyro_id=self.args.gyro_id,
freq=self.args.freq)
self.ui.ToggleTemplateClass('font-large', True)
def runTest(self):
if self.args.setup_sensor:
self.gyroscope.SetupMotionSensor()
logging.info('%r', self.gyroscope)
if not self.args.autostart:
self.ui.SetInstruction(
_('Please put device on a horizontal plane then press space to '
'start testing.'))
self.ui.WaitKeysOnce(test_ui.SPACE_KEY)
for i in range(self.args.setup_time_secs):
self.ui.SetInstruction(
_(
'Test will be started within {secs} seconds. '
'Please do not move the device.',
secs=self.args.setup_time_secs - i))
self.Sleep(1)
logging.info('Wait for device stop.')
self.ui.SetInstruction(_('Please do not move the device.'))
self._WaitForDeviceStop()
logging.info('Wait for device rotate.')
self.ui.SetInstruction(_('Please rotate the device.'))
self.SetImage('chromebook.png')
self._WaitForDeviceRotate()
def _UpdateState(self, data, is_passed, rule_text, rotation_degree):
html = [f'<div>{rule_text}</div>']
degree_x, degree_y, degree_z = 0, 0, 0
for k, v in data.items():
state = ('test-status-passed' if is_passed[k] else 'test-status-failed')
axis = re.match(r'in_anglvel_(?P<axis>x|y|z)', k).group('axis')
if axis == 'x':
degree_x = int(rotation_degree[k])
elif axis == 'y':
degree_y = int(rotation_degree[k])
elif axis == 'z':
degree_z = int(rotation_degree[k])
html.append(f'<div class="{state}">{test_ui.Escape(k)}={v:.4f} rad/s,'
f'{rotation_degree[k]:.2f} deg</div>')
self.RotateImage(degree_x, degree_y, degree_z)
self.ui.SetHTML(''.join(html), id='state')
def _UpdateRotationDegree(self, data, rotation_degree, time_period):
for k, v in data.items():
degree = v * _RADIAN_TO_DEGREE
rotation_degree[k] += degree * time_period
def _WaitForDeviceStop(self):
"""Wait until absolute value of all sensors less than stop_threshold."""
rotation_degree = collections.defaultdict(int)
def CheckSensorState():
data = self.gyroscope.GetData()
logging.info('sensor value: %r', data)
is_passed = {
k: abs(v) < self.args.stop_threshold
for k, v in data.items()
}
self._UpdateState(data, is_passed, f'< {self.args.stop_threshold:.10f}',
rotation_degree)
return all(is_passed.values())
sync_utils.WaitFor(CheckSensorState, self.args.timeout_secs)
def _WaitForDeviceRotate(self):
"""Wait until all sensors has absolute value > rotation_threshold."""
max_values = collections.defaultdict(float)
rotation_degree = collections.defaultdict(int)
def CheckSensorMaxValues():
before_get_data = time.time()
data = self.gyroscope.GetData(capture_count=self.args.capture_count,
sample_rate=self.args.sample_rate,
average=False)
cleaned_data = collections.defaultdict(float)
for sensor_name, values in data.items():
# Remove the first five data items since the sensor bug caused the
# first few data to be incorrect. After experimenting with different
# values, 5 is the smallest value to avoid wrong data items
# and minimize data lost
cleaned_data[sensor_name] = statistics.mean(values[5:])
logging.info('sensor value: %r', data)
for sensor_name, value in cleaned_data.items():
max_values[sensor_name] = max(max_values[sensor_name], abs(value))
is_passed = {
k: v > self.args.rotation_threshold
for k, v in max_values.items()
}
after_get_data = time.time()
self._UpdateRotationDegree(cleaned_data, rotation_degree,
after_get_data - before_get_data)
self._UpdateState(max_values, is_passed,
f'> {self.args.rotation_threshold:.10f}',
rotation_degree)
return all(is_passed.values())
sync_utils.WaitFor(condition=CheckSensorMaxValues,
timeout_secs=self.args.timeout_secs,
poll_interval=_DEFAULT_POLL_INTERVAL)
def SetImage(self, url):
"""Sets the image src."""
self.ui.RunJS('document.getElementById("chromebook_img").src = args.url;',
url=url)
def RotateImage(self, degree_x, degree_y, degree_z):
"""Rotates the image according to the degree captured."""
if self.args.location == 'base':
# Switch y and z axis as the image follows the axes of the screen
degree_y, degree_z = degree_z, degree_y
if self.args.location == 'lid':
# Turn y and z values into negative as the axes have opposite direction
degree_y, degree_z = -degree_y, -degree_z
self.ui.RunJS(f'document.getElementById("chromebook_img").style.transform '
f'="rotateX({degree_x}deg) rotateY({degree_y}deg) '
f'rotateZ({degree_z}deg)";')