blob: bf2dd21bf1c58342f109577ff84114b8f8e3293e [file] [log] [blame]
# -*- coding: utf-8 -*-
#
# Copyright (c) 2010 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.
# DESCRIPTION :
#
# Intended for use during manufacturing to validate that the touchpad
# is functioning properly.
import cairo
import gobject
import gtk
import os
import pty
import re
import subprocess
import sys
import time
from cmath import pi
from glob import glob
from gtk import gdk
from autotest_lib.client.bin import test
from autotest_lib.client.bin.input.input_device import *
from autotest_lib.client.common_lib.error import CmdError
from autotest_lib.client.common_lib import error
from autotest_lib.client.common_lib import utils
from autotest_lib.client.cros import factory_setup_modules
from cros.factory.test import factory
from cros.factory.test import ui as ful
_X_SEGMENTS = 5
_Y_SEGMENTS = 4
_X_TP_OFFSET = 12
_Y_TP_OFFSET = 12
_TP_WIDTH = 396
_TP_HEIGHT = 212
_TP_SECTOR_WIDTH = (_TP_WIDTH / _X_SEGMENTS) - 1
_TP_SECTOR_HEIGHT = (_TP_HEIGHT / _Y_SEGMENTS) - 1
_X_SP_OFFSET = 428
_SP_WIDTH = 15
_F_RADIUS = 21
_X_OF_OFFSET = 486 + _F_RADIUS + 2
_Y_OF_OFFSET = 54 + _F_RADIUS + 2
_X_TFL_OFFSET = 459 + _F_RADIUS + 2
_X_TFR_OFFSET = 513 + _F_RADIUS + 2
_Y_TF_OFFSET = 117 + _F_RADIUS + 2
class TouchpadTest:
def __init__(self, tp_image, drawing_area):
self._tp_image = tp_image
self._drawing_area = drawing_area
self._motion_grid = {}
for x in range(_X_SEGMENTS):
for y in range(_Y_SEGMENTS):
self._motion_grid['%d,%d' % (x, y)] = False
self._scroll_array = {}
for y in range(_Y_SEGMENTS):
self._scroll_array[y] = False
self._l_click = False
self._r_click = False
self._of_z_rad = 0
self._tf_z_rad = 0
def calc_missing_string(self):
missing = []
missing_motion_sectors = sorted(
i for i, v in self._motion_grid.items() if v is False)
if missing_motion_sectors:
missing.append('Missing following motion sectors\n'
'未侦测到下列位置的触控移动讯号 [%s]' %
', '.join(missing_motion_sectors))
missing_scroll_segments = sorted(
str(i) for i, v in self._scroll_array.items() if v is False)
if missing_scroll_segments:
missing.append('Missing following scroll segments\n'
'未侦测到下列位置的触控卷动讯号 [%s]' %
', '.join(missing_scroll_segments))
if not self._l_click:
missing.append('Missing left click\n'
'没有侦测到左键被按下,请检修')
if not self._r_click:
missing.append('Missing right click\n'
'没有侦测到右键被按下,请检修')
return '\n'.join(missing)
def device_event(self, x, y, z, fingers, left, right):
x_seg = int(round(x / (1.0 / float(_X_SEGMENTS - 1))))
y_seg = int(round(y / (1.0 / float(_Y_SEGMENTS - 1))))
z_rad = int(round(z / (1.0 / float(_F_RADIUS - 1))))
index = '%d,%d' % (x_seg, y_seg)
assert(index in self._motion_grid)
assert(y_seg in self._scroll_array)
new_stuff = False
if left and not self._l_click:
self._l_click = True
self._of_z_rad = _F_RADIUS
factory.log('ok left click')
new_stuff = True
elif right and not self._r_click:
self._r_click = True
self._tf_z_rad = _F_RADIUS
factory.log('ok right click')
new_stuff = True
if fingers == 1 and not self._motion_grid[index]:
self._motion_grid[index] = True
new_stuff = True
elif fingers == 2 and not self._scroll_array[y_seg]:
self._scroll_array[y_seg] = True
new_stuff = True
if fingers == 1 and not self._l_click and z_rad != self._of_z_rad:
self._of_z_rad = z_rad
new_stuff = True
elif fingers == 2 and not self._r_click and z_rad != self._tf_z_rad:
self._tf_z_rad = z_rad
new_stuff = True
if new_stuff:
self._drawing_area.queue_draw()
if not self.calc_missing_string():
factory.log('completed successfully')
gtk.main_quit()
def expose_event(self, widget, event):
context = widget.window.cairo_create()
# Fill context with factory UI default background color.
context.set_source_rgb(0, 0, 0)
context.paint()
# Show touchpad image as the background.
context.set_source_surface(self._tp_image, 0, 0)
context.paint()
context.set_source_rgba(*ful.RGBA_GREEN_OVERLAY)
for index in self._motion_grid:
if not self._motion_grid[index]:
continue
ind_x, ind_y = map(int, index.split(','))
x = _X_TP_OFFSET + (ind_x * (_TP_SECTOR_WIDTH + 1))
y = _Y_TP_OFFSET + (ind_y * (_TP_SECTOR_HEIGHT + 1))
coords = (x, y, _TP_SECTOR_WIDTH, _TP_SECTOR_HEIGHT)
context.rectangle(*coords)
context.fill()
for y_seg in self._scroll_array:
if not self._scroll_array[y_seg]:
continue
y = _Y_TP_OFFSET + (y_seg * (_TP_SECTOR_HEIGHT + 1))
coords = (_X_SP_OFFSET, y, _SP_WIDTH, _TP_SECTOR_HEIGHT)
context.rectangle(*coords)
context.fill()
if not self._l_click:
context.set_source_rgba(*ful.RGBA_YELLOW_OVERLAY)
context.arc(_X_OF_OFFSET, _Y_OF_OFFSET, self._of_z_rad, 0.0, 2.0 * pi)
context.fill()
if self._l_click and not self._r_click:
context.set_source_rgba(*ful.RGBA_YELLOW_OVERLAY)
context.arc(_X_TFL_OFFSET, _Y_TF_OFFSET, self._tf_z_rad, 0.0, 2.0 * pi)
context.fill()
context.arc(_X_TFR_OFFSET, _Y_TF_OFFSET, self._tf_z_rad, 0.0, 2.0 * pi)
context.fill()
return True
def button_press_event(self, widget, event):
factory.log('button_press_event %d,%d' % (event.x, event.y))
return True
def button_release_event(self, widget, event):
factory.log('button_release_event %d,%d' % (event.x, event.y))
return True
def motion_event(self, widget, event):
factory.log('motion_event %d,%d' % (event.x, event.y))
return True
class SynClient:
_SETTINGS_CMDLINE = '/usr/bin/synclient -l'
_CMDLINE = '/usr/bin/synclient -m 50'
def __init__(self, test):
self._test = test
try:
settings_data = utils.system_output(self._SETTINGS_CMDLINE)
except CmdError as e:
raise error.TestError('Failure on "%s" [%d]' %
(self._SETTINGS_CMDLINE,
e.args[1].exit_status))
settings = {}
for line in settings_data.split('\n'):
cols = [x for x in line.rstrip().split(' ') if x]
if len(cols) != 3 or cols[1] != '=':
continue
settings[cols[0]] = cols[2]
try:
for key, attr in (('LeftEdge', '_xmin'),
('RightEdge', '_xmax'),
('TopEdge', '_ymin'),
('BottomEdge', '_ymax'),
('FingerLow', '_zmin'),
('FingerHigh', '_zmax')):
v = float(settings[key])
setattr(self, attr, v)
except KeyError as e:
factory.log('Field %s does not exist' % e.args)
raise error.TestNAError("Can't detect all hardware information")
except ValueError as e:
factory.log('Invalid literal format of %s: %s' % (key, e.args[0]))
raise error.TestNAError("Can't understand all hardware information")
try:
self._proc = subprocess.Popen(self._CMDLINE.split(),
stdout=subprocess.PIPE,
stderr=subprocess.PIPE)
except OSError as e:
raise error.TestError('Failure on launching "%s"' % self._CMDLINE)
# delay before we poll
time.sleep(0.1)
if self._proc.poll() is not None:
if self._proc.returncode != 0:
raise error.TestError('Failure on "%s" [%d]' %
(self._CMDLINE, self._proc.returncode))
else:
raise error.TestError('Termination unexpected on "%s"' %
self._CMDLINE)
gobject.io_add_watch(self._proc.stdout, gobject.IO_IN, self.recv)
def recv(self, src, cond):
''' header and data look as:
time x y z f w l r u d m multi gl gm gr gdx gdy
0.000 3532 3807 0 0 0 0 0 0 0 0 00000000
'''
data = self._proc.stdout.readline().split()
if data[0] == 'time':
return True
if len(data) != 12:
factory.log('unknown data : %d, %s' % (len(data), data))
return True
data_x, data_y, data_z, f, w, l, r = data[1:8]
x = sorted([self._xmin, float(data_x), self._xmax])[1]
x = (x - self._xmin) / (self._xmax - self._xmin)
y = sorted([self._ymin, float(data_y), self._ymax])[1]
y = (y - self._ymin) / (self._ymax - self._ymin)
z = sorted([self._zmin, float(data_z), self._zmax])[1]
z = (z - self._zmin) / (self._zmax - self._zmin)
# Detect right click button or alt right click
alt_r = int(r) or (int(l) and int(f) == 2)
self._test.device_event(x, y, z, int(f), int(l), alt_r)
return True
def quit(self):
factory.log('killing SynClient ...')
self._proc.kill()
factory.log('dead')
class EvdevClient:
def __init__(self, test, device):
self._test = test
self.ev = InputEvent()
self.device = device
gobject.io_add_watch(device.f, gobject.IO_IN, self.recv)
self._xmin = device.get_x_min()
self._xmax = device.get_x_max()
self._ymin = device.get_y_min()
self._ymax = device.get_y_max()
self._zmin = device.get_pressure_min()
self._zmax = device.get_pressure_max()
factory.log('x:(%d : %d), y:(%d : %d), z:(%d, %d)' %
(self._xmin, self._xmax, self._ymin, self._ymax,
self._zmin, self._zmax))
def _to_percent(self, val, _min, _max):
bound = sorted([_min, float(val), _max])[1]
return (bound - _min) / (_max - _min)
def recv(self, src, cond):
try:
self.ev.read(src)
except:
raise error.TestError('Error reading events from %s' %
self.device.path)
if not self.device.process_event(self.ev):
return True
f = self.device.get_num_fingers()
if f == 0:
return True
x = self.device.get_x()
y = self.device.get_y()
z = self.device.get_pressure()
l = self.device.get_left()
# Detect right click button or alt right click
r = self.device.get_right() or (l and f == 2)
# Convert raw coordinate to % of range.
x_pct = self._to_percent(x, self._xmin, self._xmax)
y_pct = self._to_percent(y, self._ymin, self._ymax)
z_pct = self._to_percent(z, self._zmin, self._zmax)
factory.log('x=%f y=%f z=%f f=%d l=%d r=%d' %
(x_pct, y_pct, z_pct, f, l, r))
self._test.device_event(x_pct, y_pct, z_pct, f, l, r)
return True
def quit(self):
if self.device and self.device.f and not self.device.f.closed:
factory.log('Closing %s...' % self.device.path)
self.device.f.close()
class SynControl:
''' Use syncontrol to read packets and pass them to TouchpadTest '''
# A typical packet looks like
# x: 4357, y: 2973, z: 48, w: 4, dx: 0, dy: 0,
# finger_index: 0, left_button: 0, right_button: 0
pattern = (u'x: \d+, y: \d+, z: \d+, w: \d+, dx: -?\d+, dy: -?\d+, '
u'finger_index: \d+, left_button: \d+, right_button: \d+')
_SYNCONTROL = '/opt/Synaptics/bin/syncontrol'
_CMDLINE = '%s packets' % _SYNCONTROL
# Set the default min and max values for typical bezel limits and Z.
# These are approximate values, and may be different in different models.
# Use these values only if they cannot be derived from the diag file.
_X_RANGE = (1400, 5400)
_Y_RANGE = (1300, 4300)
_Z_RANGE = (0, 255)
def __init__(self, test):
self._test = test
# Read settings from diag file
self._get_settings()
# Open pty to avoid buffered output in subprocess.Popen
master, slave = pty.openpty()
self.pty_stdout = os.fdopen(master)
try:
self._proc = subprocess.Popen(self._CMDLINE, shell=True,
stdout=slave, stderr=slave)
except OSError as e:
raise error.TestError('Failure on launching "%s"' % self._CMDLINE)
if self._proc.poll() is not None:
if self._proc.returncode != 0:
raise error.TestError('Failure on "%s" [%d]' %
(self._CMDLINE, self._proc.returncode))
else:
raise error.TestError('Termination unexpected on "%s"' %
self._CMDLINE)
gobject.io_add_watch(self.pty_stdout, gobject.IO_IN, self.recv)
def _get_settings(self):
''' Get min x, min y, max x, max y, and max z '''
def _delete_diag_files(tmp_dir, diag_file):
for f in glob(os.path.join(tmp_dir, diag_file)):
if os.path.isfile(f):
os.remove(f)
tmp_dir = '/tmp'
diag_file = 'SynDiag*'
diag_cmd = 'HOME=%s %s diag' % (tmp_dir, self._SYNCONTROL)
# delete any old diag file
_delete_diag_files(tmp_dir, diag_file)
# Execute syncontrol diag to dump touchpad settings
utils.system(diag_cmd)
# Initialize the settings
# Note: there is no min z in the diag file. Set it to default value 0.
self._xmin, self._xmax = self._X_RANGE
self._ymin, self._ymax = self._Y_RANGE
self._zmin, self._zmax = self._Z_RANGE
found_z = False
found_rect = False
# A bezel rectangle in diag file looks as:
# 'Bezel Rectangle (1374, 1324) (5538, 4464)'
# The max z in diag file looks as:
# 'Maximum Z 255'
rect_str = u'Bezel Rectangle\s+\((\d+),\s*(\d+)\)\s*\((\d+),\s*(\d+)\)'
diag = glob(os.path.join(tmp_dir, diag_file))
if diag != []:
factory.log('diag_file: %s' % diag)
with open(diag[0]) as f:
for line in f:
# Read min x, min y, max x, max y from the bezel rectangle
if not found_rect and line.startswith('Bezel Rectangle'):
s = re.search(rect_str, line)
if s is not None:
self._xmin = int(s.group(1))
self._ymin = int(s.group(2))
self._xmax = int(s.group(3))
self._ymax = int(s.group(4))
found_rect = True
# Read max z
elif not found_z and line.startswith('Maximum Z'):
self._zmax = int(line.split()[-1])
found_z = True
if found_rect and found_z:
break
# delete the diag file
_delete_diag_files(tmp_dir, diag_file)
def recv(self, src, cond):
line = self.pty_stdout.readline()
if line == '':
return True
# check packet validity
if re.search(self.pattern, line) is None:
factory.log(' Invalid packet skipped: %s' % line)
return True
data = line.split(',')
(data_x, data_y, data_z, data_w, data_dx, data_dy, data_finger_index,
data_left_button, data_right_button) = \
(d.split(':')[-1].strip().rstrip('\n') for d in data)
x = sorted([self._xmin, float(data_x), self._xmax])[1]
x = (x - self._xmin) / (self._xmax - self._xmin)
y = sorted([self._ymin, float(data_y), self._ymax])[1]
y = (y - self._ymin) / (self._ymax - self._ymin)
y = 1 - y
z = sorted([self._zmin, float(data_z), self._zmax])[1]
z = (z - self._zmin) / (self._zmax - self._zmin)
fingers = int(data_finger_index) + 1
left_button = int(data_left_button)
right_button = int(data_right_button)
self._test.device_event(x, y, z, fingers, left_button, right_button)
return True
def quit(self):
factory.log('killing SynControl ...')
self._proc.kill()
factory.log('dead')
class factory_Touchpad(test.test):
version = 1
preserve_srcdir = True
def run_once(self):
factory.log('%s run_once' % self.__class__)
os.chdir(self.srcdir)
tp_image = cairo.ImageSurface.create_from_png('touchpad.png')
image_size = (tp_image.get_width(), tp_image.get_height())
drawing_area = gtk.DrawingArea()
test = TouchpadTest(tp_image, drawing_area)
drawing_area.set_size_request(*image_size)
drawing_area.connect('expose_event', test.expose_event)
drawing_area.connect('button-press-event', test.button_press_event)
drawing_area.connect('button-release-event', test.button_release_event)
drawing_area.connect('motion-notify-event', test.motion_event)
drawing_area.add_events(gdk.EXPOSURE_MASK |
gdk.BUTTON_PRESS_MASK |
gdk.BUTTON_RELEASE_MASK |
gdk.POINTER_MOTION_MASK)
test_widget = gtk.VBox()
test_widget.set_spacing(20)
test_widget.pack_start(drawing_area, False, False)
usage_label = ful.make_label(
'1. Move one finger across entire touchpad surface\n'
'2. Scroll from top to bottom of pad with two fingers\n'
'3. Click touchpad with one finger\n'
'4. Click touchpad with two fingers\n')
test_widget.pack_start(usage_label, False, False)
raw_dev = glob('/dev/serio_raw*')
# Check if synaptics closed source kernel driver is used
if len(raw_dev) > 0:
factory.log('Syncontrol: found device: %s' % raw_dev[0])
touchpad = SynControl(test)
else:
# Detect an evdev compatible touchpad device.
# TODO(djkurtz): Use gudev to detect touchpad
for evdev in glob('/dev/input/event*'):
device = InputDevice(evdev)
if device.is_touchpad():
break
else:
device = None
# Using EvdevCient if an evdev compatible touchpad device is found
if device:
factory.log('EvdevClient: using %s, device %s' %
(device.name, device.path))
touchpad = EvdevClient(test, device)
else:
factory.log('Using SynClient.')
touchpad = SynClient(test)
ful.run_test_widget(self.job, test_widget,
cleanup_callback=touchpad.quit)
missing = test.calc_missing_string()
if missing:
raise error.TestFail(missing)
factory.log('%s run_once finished' % self.__class__)