blob: 68036e938292fc4c705facc4096e38ee3f76a3f1 [file] [log] [blame]
# Copyright (c) 2013 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.
import math
import numpy as np
import re
import sys
import time
from mtlib.util import RequiredRegex, SafeExecute, Execute, Path
from remote import ChromeOSTouchDevice
# locate required folders in chroot enviornment
src_dir = Path("/mnt/host/source/src/")
touch_firmware_test_dir = src_dir / "platform/touch_firmware_test"
scripts_dir = src_dir / "scripts"
autotest_dir = src_dir / "third_party/autotest/files"
pressure_calib_dir = autotest_dir / "client/site_tests/firmware_TouchMTB"
xorg_conf_template = """\
Section "InputClass"
Identifier "touchpad {board} {vendor_lower}"
MatchIsTouchpad "on"
MatchDevicePath "/dev/input/event*"
MatchProduct "{vendor}"
Option "Integrated Touchpad" "1"
Option "Touchpad Stack Version" "2"
{comment}
Option "Pressure Calibration Offset" "{intercept}"
Option "Pressure Calibration Slope" "{slope}"
EndSection\
"""
class XorgConfBuilder(object):
def __init__(self, info, conf_file, touchpad):
self.board_variant = info.board_variant
self.xorg_conf = conf_file
self.touchpad = touchpad
self.sheet_name = self.board_variant + " " + self.touchpad.fw_version
def GetCalibrationResults(self):
cmd = "python spreadsheet.py --print-info -n \"{}\""
cmd = cmd.format(self.sheet_name)
res = Execute(cmd, cwd=pressure_calib_dir, verbose=True)
if not res:
return False
slope_regex = RequiredRegex("slope=([0-9.\\-]+)")
slope = slope_regex.Search(res).group(1)
intercept_regex = RequiredRegex("intercept=([0-9.\\-]+)")
intercept = intercept_regex.Search(res).group(1)
return (float(slope), float(intercept))
def RunCalibration(self, remote, use_existing=True):
if use_existing:
existing = remote.Execute("readlink /var/tmp/touch_firmware_test/latest")
else:
existing = False
if not existing:
print "Installing firmware_TouchMTB on device"
cmd = ("sh test_that --autotest_dir {} " +
"{} firmware_TouchMTBSetup")
cmd = cmd.format(autotest_dir, remote.ip)
SafeExecute(cmd, cwd=scripts_dir, verbose=True)
print "Executing Pressure Calibration."
print "Follow instructions on device"
remote.SafeExecute("XAUTHORITY=\"/home/chronos/.Xauthority\" DISPLAY=:0" +
" python main.py -m calibration",
cwd="/usr/local/autotest/tests/firmware_TouchMTB",
verbose=True)
print "Uploading data to spreadsheet. This can take a minute."
cmd = ["python", "spreadsheet.py", "-v", "-d", remote.ip,
"--result-dir=latest", "-n", self.sheet_name]
SafeExecute(cmd, cwd=pressure_calib_dir, verbose=True, interactive=True)
def UpdateXorgConf(self, calib):
conf = xorg_conf_template.format(
board=self.board_variant,
vendor=self.touchpad.vendor,
vendor_lower=self.touchpad.vendor.lower(),
slope=calib[0],
intercept=calib[1])
if self.xorg_conf.exists:
current = self.xorg_conf.Read()
match_regex = RequiredRegex("MatchProduct\s+\"{}\"".format(
self.touchpad.vendor))
match = match_regex.Search(current, safe=False)
if match:
begin = current.rindex("Section", 0, match.start())
end = current.index("EndSection", match.end()) + len("EndSection")
new = current[:begin] + conf + current[end:]
else:
new = current + "\n" + conf
else:
new = conf
self.xorg_conf.Write(new)
class OzoneConfBuilder(object):
"""Xorg/Ozone configuration file builder.
Generates the xorg/ozone configuration file. It updates an existing or creates
a new xorg config for the correct platform and fills it with a config
template.
Part of the config is a pressure calibration which is executed by this class
before the config is written.
"""
num_samples = 300
probe_diameters = np.asarray([3.9, 5.7, 7.7, 9.9, 11.7, 13.9, 17.8])
def __init__(self, info, conf_file, touchpad):
self.board_variant = info.board_variant
self.xorg_conf = conf_file
self.touchpad = touchpad
self.average_list = []
def ProbeName(self, probe):
minmax = ""
if probe == 0:
minmax = ", smallest"
if probe == len(self.probe_diameters) - 1:
minmax = ", largest"
return "#%d (diameter=%.2fmm%s)" % (probe + 1,
self.probe_diameters[probe],
minmax)
def RunCalibration(self, remote, use_existing=True):
remote_touch = ChromeOSTouchDevice(remote.ip, is_touchscreen=False)
average_list = []
for i, diameter in enumerate(self.probe_diameters):
pressures = self.ReadProbePressure(remote_touch, i)
if i + 1 < len(self.probe_diameters):
time.sleep(2)
average = float(sum(pressures)) / float(len(pressures))
average_list.append(average)
self.average_list = average_list
def CalculateSlopeIntercept(self, average_list, expected_list):
# only do the calculation on the first 5 probes, as we find that
# the larger ones can introduce errors and are less representative
# of typical real fingers.
average_list = average_list[0:5]
expected_list = expected_list[0:5]
slope = np.cov(average_list, expected_list)[0][1] / np.var(average_list)
inter = np.mean(expected_list) - slope * np.mean(average_list)
return (slope, inter)
def ReadProbePressure(self, remote_touch, probe):
probe_name = self.ProbeName(probe)
print "Move probe %s in circles over the touchpad:" % probe_name
pressures = []
remote_touch.FlushSnapshotBuffer()
while len(pressures) < self.num_samples:
state = remote_touch.NextSnapshot()
if len(state.fingers) == 1:
pressures.append(state.fingers[0].pressure)
sys.stdout.write("\r%d/%d" % (len(pressures), self.num_samples))
sys.stdout.flush()
print
return pressures
def GenerateComment(self, average, expected):
slope, intercept = self.CalculateSlopeIntercept(average, expected)
calibrated = intercept + slope * average
lines = []
header_format = " # {:<5} {:<10} {:<10} {:<10} {:}"
line_format = " # {:<5} {:<10.2f} {:<10.2f} {:<10.2f} {:.2f}"
lines.append(" # Pressure calibration results:")
lines.append(header_format.format("Probe", "Diameter", "Measured",
"Expected", "Calibrated"))
for i in range(len(self.probe_diameters)):
lines.append(line_format.format(i, self.probe_diameters[i],
average[i], expected[i], calibrated[i]))
return "\n".join(lines)
def UpdateXorgConf(self):
expected = math.pi * ((self.probe_diameters / 2.0) ** 2)
average = np.asarray(self.average_list)
slope, intercept = self.CalculateSlopeIntercept(average, expected)
comment = self.GenerateComment(average, expected)
conf = xorg_conf_template.format(
board=self.board_variant,
vendor=self.touchpad.vendor,
vendor_lower=self.touchpad.vendor.lower(),
slope=slope,
intercept=intercept,
comment=comment)
if self.xorg_conf.exists:
current = self.xorg_conf.Read()
match_regex = RequiredRegex("MatchProduct\s+\"{}\"".format(
self.touchpad.vendor))
match = match_regex.Search(current, must_succeed=False)
if match:
begin = current.rindex("Section", 0, match.start())
end = current.index("EndSection", match.end()) + len("EndSection")
new = current[:begin] + conf + current[end:]
else:
new = current + "\n" + conf
else:
new = conf
self.xorg_conf.Write(new)
class XorgInputClassParser(object):
""" Parser for xorg input config files.
This class is used to parse xorg config files for input class options.
Only input class sections with a valid Identifier line are parsed.
"""
def Parse(self, file=None, string=None):
""" Parse xorg file and return dictionary of input classes.
Provide either a filename or file-like object to the file parameters
or a string containing the configuration to the string parameter.
The return value is a dictionary of all InputClasses, with the
Identifier as a key. The value is another dictionary containing
all options set for this input class.
"""
if file:
if isinstance(file, basestring):
return self._ParseString(open(file).read())
elif hasattr(file, 'read'):
return self._ParseString(file.read())
else:
raise ValueError
elif string:
return self._ParseString(string)
else:
raise ValueError
def _ParseString(self, string):
lines = string.splitlines()
lines = map(lambda l: l.strip(), lines)
section_regex = re.compile('Section\\s+\"([^\"]+)\"')
id_regex = re.compile('Identifier\\s+\"([^\"]+)\"')
option_regex = re.compile('Option\\s+\"([^\"]+)\"\\s+\"([^\"]+)\"')
classes = {}
current_options = None
current_id = None
for line in lines:
if current_options is None:
# Outside of sections
section_match = section_regex.match(line)
if section_match and section_match.group(1) == 'InputClass':
current_options = {}
continue
else:
# Inside of a section
if line == 'EndSection':
if current_id:
# if class had an id, save in results
classes[current_id] = current_options
current_options = None
continue
# look for identifier line
id_match = id_regex.match(line)
if id_match:
current_id = id_match.group(1)
continue
# store options in current_options
option_match = option_regex.match(line)
if option_match:
key = option_match.group(1)
value = option_match.group(2)
current_options[key] = value
return classes