blob: 98c9f980b9319e9731864ce30cf1767322aac22d [file] [log] [blame]
# 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.
import glob
import logging
import os
import subprocess
import tempfile
import time
from autotest_lib.client.bin import utils
from autotest_lib.client.common_lib import error
class Device(object):
"""Information about a specific input device."""
def __init__(self, input_type):
self.input_type = input_type # e.g. 'touchpad'
self.emulated = False # Whether device is real or not
self.emulation_process = None # Process of running emulation
self.name = 'unknown' # e.g. 'Atmel maXTouch Touchpad'
self.fw_id = None # e.g. '6.0'
self.hw_id = None # e.g. '90.0'
self.node = None # e.g. '/dev/input/event4'
self.device_dir = None # e.g. '/sys/class/input/event4/device/device'
def __str__(self):
s = '%s:' % self.input_type
s += '\n Name: %s' % self.name
s += '\n Node: %s' % self.node
s += '\n hw_id: %s' % self.hw_id
s += '\n fw_id: %s' % self.fw_id
s += '\n Emulated: %s' % self.emulated
return s
class InputPlayback(object):
"""
Provides an interface for playback and emulating peripherals via evemu-*.
Example use: player = InputPlayback()
player.emulate(property_file=path_to_file)
player.find_connected_inputs()
player.playback(path_to_file)
player.blocking_playback(path_to_file)
player.close()
"""
_DEFAULT_PROPERTY_FILES = {'mouse': 'mouse.prop',
'keyboard': 'keyboard.prop'}
_PLAYBACK_COMMAND = 'evemu-play --insert-slot0 %s < %s'
# Define the overhead (500 ms) elapsed for launching evemu-play and the
# latency from event injection to the first event read by Chrome Input
# thread.
_PLAYBACK_OVERHEAD_LATENCY = 0.5
# Define a keyboard as anything with any keys #2 to #248 inclusive,
# as defined in the linux input header. This definition includes things
# like the power button, so reserve the "keyboard" label for things with
# letters/numbers and define the rest as "other_keyboard".
_MINIMAL_KEYBOARD_KEYS = ['1', 'Q', 'SPACE']
_KEYBOARD_KEYS = [
'1', '2', '3', '4', '5', '6', '7', '8', '9', '0', 'MINUS', 'EQUAL',
'BACKSPACE', 'TAB', 'Q', 'W', 'E', 'R', 'T', 'Y', 'U', 'I', 'O',
'P', 'LEFTBRACE', 'RIGHTBRACE', 'ENTER', 'LEFTCTRL', 'A', 'S', 'D',
'F', 'G', 'H', 'J', 'K', 'L', 'SEMICOLON', 'APOSTROPHE', 'GRAVE',
'LEFTSHIFT', 'BACKSLASH', 'Z', 'X', 'C', 'V', 'B', 'N', 'M',
'COMMA', 'DOT', 'SLASH', 'RIGHTSHIFT', 'KPASTERISK', 'LEFTALT',
'SPACE', 'CAPSLOCK', 'F1', 'F2', 'F3', 'F4', 'F5', 'F6', 'F7', 'F8',
'F9', 'F10', 'NUMLOCK', 'SCROLLLOCK', 'KP7', 'KP8', 'KP9',
'KPMINUS', 'KP4', 'KP5', 'KP6', 'KPPLUS', 'KP1', 'KP2', 'KP3',
'KP0', 'KPDOT', 'ZENKAKUHANKAKU', '102ND', 'F11', 'F12', 'RO',
'KATAKANA', 'HIRAGANA', 'HENKAN', 'KATAKANAHIRAGANA', 'MUHENKAN',
'KPJPCOMMA', 'KPENTER', 'RIGHTCTRL', 'KPSLASH', 'SYSRQ', 'RIGHTALT',
'LINEFEED', 'HOME', 'UP', 'PAGEUP', 'LEFT', 'RIGHT', 'END', 'DOWN',
'PAGEDOWN', 'INSERT', 'DELETE', 'MACRO', 'MUTE', 'VOLUMEDOWN',
'VOLUMEUP', 'POWER', 'KPEQUAL', 'KPPLUSMINUS', 'PAUSE', 'SCALE',
'KPCOMMA', 'HANGEUL', 'HANGUEL', 'HANJA', 'YEN', 'LEFTMETA',
'RIGHTMETA', 'COMPOSE', 'STOP', 'AGAIN', 'PROPS', 'UNDO', 'FRONT',
'COPY', 'OPEN', 'PASTE', 'FIND', 'CUT', 'HELP', 'MENU', 'CALC',
'SETUP', 'WAKEUP', 'FILE', 'SENDFILE', 'DELETEFILE', 'XFER',
'PROG1', 'PROG2', 'WWW', 'MSDOS', 'COFFEE', 'SCREENLOCK',
'DIRECTION', 'CYCLEWINDOWS', 'MAIL', 'BOOKMARKS', 'COMPUTER',
'BACK', 'FORWARD', 'CLOSECD', 'EJECTCD', 'EJECTCLOSECD', 'NEXTSONG',
'PLAYPAUSE', 'PREVIOUSSONG', 'STOPCD', 'RECORD', 'REWIND', 'PHONE',
'ISO', 'CONFIG', 'HOMEPAGE', 'REFRESH', 'EXIT', 'MOVE', 'EDIT',
'SCROLLUP', 'SCROLLDOWN', 'KPLEFTPAREN', 'KPRIGHTPAREN', 'NEW',
'REDO', 'F13', 'F14', 'F15', 'F16', 'F17', 'F18', 'F19', 'F20',
'F21', 'F22', 'F23', 'F24', 'PLAYCD', 'PAUSECD', 'PROG3', 'PROG4',
'DASHBOARD', 'SUSPEND', 'CLOSE', 'PLAY', 'FASTFORWARD', 'BASSBOOST',
'PRINT', 'HP', 'CAMERA', 'SOUND', 'QUESTION', 'EMAIL', 'CHAT',
'SEARCH', 'CONNECT', 'FINANCE', 'SPORT', 'SHOP', 'ALTERASE',
'CANCEL', 'BRIGHTNESSDOWN', 'BRIGHTNESSUP', 'MEDIA',
'SWITCHVIDEOMODE', 'KBDILLUMTOGGLE', 'KBDILLUMDOWN', 'KBDILLUMUP',
'SEND', 'REPLY', 'FORWARDMAIL', 'SAVE', 'DOCUMENTS', 'BATTERY',
'BLUETOOTH', 'WLAN', 'UWB', 'UNKNOWN', 'VIDEO_NEXT', 'VIDEO_PREV',
'BRIGHTNESS_CYCLE', 'BRIGHTNESS_AUTO', 'BRIGHTNESS_ZERO',
'DISPLAY_OFF', 'WWAN', 'WIMAX', 'RFKILL', 'MICMUTE']
def __init__(self):
self.devices = {}
self._emulated_device = None
def has(self, input_type):
"""Return True/False if device has a input of given type.
@param input_type: string of type, e.g. 'touchpad'
"""
return input_type in self.devices
def _get_input_events(self):
"""Return a list of all input event nodes."""
return glob.glob('/dev/input/event*')
def emulate(self, input_type='mouse', property_file=None):
"""
Emulate the given input (or default for type) with evemu-device.
Emulating more than one of the same device type will only allow playback
on the last one emulated. The name of the last-emulated device is
noted to be sure this is the case.
Property files are made with the evemu-describe command,
e.g. 'evemu-describe /dev/input/event12 > property_file'.
@param input_type: 'mouse' or 'keyboard' to use default property files.
Need not be specified if supplying own file.
@param property_file: Property file of device to be emulated. Generate
with 'evemu-describe' command on test image.
"""
new_device = Device(input_type)
new_device.emulated = True
# Checks for any previous emulated device and kills the process
self.close()
if not property_file:
if input_type not in self._DEFAULT_PROPERTY_FILES:
raise error.TestError('Please supply a property file for input '
'type %s' % input_type)
current_dir = os.path.dirname(os.path.realpath(__file__))
property_file = os.path.join(
current_dir, self._DEFAULT_PROPERTY_FILES[input_type])
if not os.path.isfile(property_file):
raise error.TestError('Property file %s not found!' % property_file)
with open(property_file) as fh:
name_line = fh.readline() # Format "N: NAMEOFDEVICE"
new_device.name = name_line[3:-1]
logging.info('Emulating %s %s (%s).', input_type, new_device.name,
property_file)
num_events_before = len(self._get_input_events())
new_device.emulation_process = subprocess.Popen(
['evemu-device', property_file], stdout=subprocess.PIPE)
self._emulated_device = new_device
# Ensure there are more input events than there were before.
try:
expected = num_events_before + 1
exception = error.TestError('Error emulating %s!' % input_type)
utils.poll_for_condition(
lambda: len(self._get_input_events()) == expected,
exception=exception)
except error.TestError as e:
self.close()
raise e
def _find_device_properties(self, device):
"""Return string of properties for given node.
@return: string of properties.
"""
with tempfile.NamedTemporaryFile() as temp_file:
filename = temp_file.name
evtest_process = subprocess.Popen(['evtest', device],
stdout=temp_file)
def find_exit():
"""Polling function for end of output."""
interrupt_cmd = 'grep "interrupt to exit" %s | wc -l' % filename
line_count = utils.run(interrupt_cmd).stdout.strip()
return line_count != '0'
utils.poll_for_condition(find_exit)
evtest_process.kill()
temp_file.seek(0)
props = temp_file.read()
return props
def _determine_input_type(self, props):
"""Find input type (if any) from a string of properties.
@return: string of type, or None
"""
if props.find('REL_X') >= 0 and props.find('REL_Y') >= 0:
if (props.find('ABS_MT_POSITION_X') >= 0 and
props.find('ABS_MT_POSITION_Y') >= 0):
return 'multitouch_mouse'
else:
return 'mouse'
if props.find('ABS_X') >= 0 and props.find('ABS_Y') >= 0:
if (props.find('BTN_STYLUS') >= 0 or
props.find('BTN_STYLUS2') >= 0 or
props.find('BTN_TOOL_PEN') >= 0):
return 'stylus'
if (props.find('ABS_PRESSURE') >= 0 or
props.find('BTN_TOUCH') >= 0):
if (props.find('BTN_LEFT') >= 0 or
props.find('BTN_MIDDLE') >= 0 or
props.find('BTN_RIGHT') >= 0 or
props.find('BTN_TOOL_FINGER') >= 0):
return 'touchpad'
else:
return 'touchscreen'
if props.find('BTN_LEFT') >= 0:
return 'touchscreen'
if props.find('KEY_') >= 0:
for key in self._MINIMAL_KEYBOARD_KEYS:
if props.find('KEY_%s' % key) >= 0:
return 'keyboard'
for key in self._KEYBOARD_KEYS:
if props.find('KEY_%s' % key) >= 0:
return 'other_keyboard'
return
def _get_contents_of_file(self, filepath):
"""Return the contents of the given file.
@param filepath: string of path to file
@returns: contents of file. Assumes file exists.
"""
return utils.run('cat %s' % filepath).stdout.strip()
def _find_input_name(self, device_dir, name=None):
"""Find the associated input* name for the given device directory.
E.g. given '/dev/input/event4', return 'input3'.
@param device_dir: the device directory.
@param name: the device name.
@returns: string of the associated input name.
"""
input_names = glob.glob(os.path.join(device_dir, 'input', 'input*'))
for input_name in input_names:
name_path = os.path.join(input_name, 'name')
if not os.path.exists(name_path):
continue
if name == self._get_contents_of_file(name_path):
return os.path.basename(input_name)
# Raise if name could not be matched.
logging.error('Input names found(%s): %s', device_dir, input_names)
raise error.TestError('Could not match input* to this device!')
def _find_device_ids_for_styluses(self, device_dir, name=None):
"""Find the fw_id and hw_id for the stylus in the given directory.
@param device_dir: the device directory.
@param name: the device name.
@returns: firmware id, hardware id for this device.
"""
hw_id = 'wacom' # Wacom styluses don't actually have hwids.
fw_id = None
# Find fw_id for wacom styluses via wacom_flash command. Arguments
# to this command are wacom_flash (dummy placeholder arg) -a (i2c name)
# Find i2c name if any /dev/i2c-* link to this device's input event.
input_name = self._find_input_name(device_dir, name)
i2c_paths = glob.glob('/dev/i2c-*')
for i2c_path in i2c_paths:
class_folder = i2c_path.replace('dev', 'sys/class/i2c-adapter')
input_folder_path = os.path.join(class_folder, '*', '*',
'input', input_name)
contents_of_input_folder = glob.glob(input_folder_path)
if len(contents_of_input_folder) != 0:
i2c_name = i2c_path[len('/dev/'):]
cmd = 'wacom_flash dummy -a %s' % i2c_name
# Do not throw an exception if wacom_flash does not exist.
result = utils.run(cmd, ignore_status=True)
if result.exit_status == 0:
fw_id = result.stdout.split()[-1]
break
if fw_id == '':
fw_id = None
return fw_id, hw_id
def _find_device_ids(self, device_dir, input_type, name):
"""Find the fw_id and hw_id for the given device directory.
Finding fw_id and hw_id applicable only for touchpads, touchscreens,
and styluses.
@param device_dir: the device directory.
@param input_type: string of input type.
@param name: string of input name.
@returns: firmware id, hardware id
"""
fw_id, hw_id = None, None
if not device_dir or input_type not in ['touchpad', 'touchscreen',
'stylus']:
return fw_id, hw_id
if input_type == 'stylus':
return self._find_device_ids_for_styluses(device_dir, name)
# Touch devices with custom drivers usually save this info as a file.
fw_filenames = ['fw_version', 'firmware_version', 'firmware_id']
for fw_filename in fw_filenames:
fw_path = os.path.join(device_dir, fw_filename)
if os.path.exists(fw_path):
if fw_id:
logging.warning('Found new potential fw_id when previous '
'value was %s!', fw_id)
fw_id = self._get_contents_of_file(fw_path)
hw_filenames = ['hw_version', 'product_id', 'board_id']
for hw_filename in hw_filenames:
hw_path = os.path.join(device_dir, hw_filename)
if os.path.exists(hw_path):
if hw_id:
logging.warning('Found new potential hw_id when previous '
'value was %s!', hw_id)
hw_id = self._get_contents_of_file(hw_path)
# Hw_ids for Weida and 2nd gen Synaptics are different.
if not hw_id:
id_folder = os.path.abspath(os.path.join(device_dir, '..', 'id'))
product_path = os.path.join(id_folder, 'product')
vendor_path = os.path.join(id_folder, 'vendor')
if os.path.isfile(product_path):
product = self._get_contents_of_file(product_path)
if name.startswith('WD'): # Weida ts, e.g. sumo
if os.path.isfile(vendor_path):
vendor = self._get_contents_of_file(vendor_path)
hw_id = vendor + product
else: # Synaptics tp or ts, e.g. heli, lulu, setzer
hw_id = product
if not fw_id:
# Fw_ids for 2nd gen Synaptics can only be found via rmi4update.
# See if any /dev/hidraw* link to this device's input event.
input_name = self._find_input_name(device_dir, name)
hidraws = glob.glob('/dev/hidraw*')
for hidraw in hidraws:
class_folder = hidraw.replace('dev', 'sys/class/hidraw')
input_folder_path = os.path.join(class_folder, 'device',
'input', input_name)
if os.path.exists(input_folder_path):
fw_id = utils.run('rmi4update -p -d %s' % hidraw,
ignore_status=True).stdout.strip()
if fw_id == '':
fw_id = None
break
return fw_id, hw_id
def find_connected_inputs(self):
"""Determine the nodes of all present input devices, if any.
Cycle through all possible /dev/input/event* and find which ones
are touchpads, touchscreens, mice, keyboards, etc.
These nodes can be used for playback later.
If the type of input is already emulated, prefer that device. Otherwise,
prefer the last node found of that type (e.g. for multiple touchpads).
Record the found devices in self.devices.
"""
self.devices = {} # Discard any previously seen nodes.
input_events = self._get_input_events()
for event in input_events:
properties = self._find_device_properties(event)
input_type = self._determine_input_type(properties)
if input_type:
new_device = Device(input_type)
new_device.node = event
class_folder = event.replace('dev', 'sys/class')
name_file = os.path.join(class_folder, 'device', 'name')
if os.path.isfile(name_file):
name = self._get_contents_of_file(name_file)
logging.info('Found %s: %s at %s.', input_type, name, event)
# If a particular device is expected, make sure name matches.
if (self._emulated_device and
self._emulated_device.input_type == input_type):
if self._emulated_device.name != name:
continue
else:
new_device.emulated = True
process = self._emulated_device.emulation_process
new_device.emulation_process = process
new_device.name = name
# Find the devices folder containing power info
# e.g. /sys/class/event4/device/device
# Search that folder for hwid and fwid
device_dir = os.path.join(class_folder, 'device', 'device')
if os.path.exists(device_dir):
new_device.device_dir = device_dir
new_device.fw_id, new_device.hw_id = self._find_device_ids(
device_dir, input_type, new_device.name)
if new_device.emulated:
self._emulated_device = new_device
self.devices[input_type] = new_device
logging.debug(self.devices[input_type])
def playback(self, filepath, input_type='touchpad'):
"""Playback a given input file.
Create input file using evemu-record.
E.g. 'evemu-record $NODE -1 > $FILENAME'
@param filepath: path to the input file on the DUT.
@param input_type: name of device type; 'touchpad' by default.
Types are returned by the _determine_input_type()
function.
input_type must be known. Check using has().
"""
assert(input_type in self.devices)
node = self.devices[input_type].node
logging.info('Playing back finger-movement on %s, file=%s.', node,
filepath)
utils.run(self._PLAYBACK_COMMAND % (node, filepath))
def blocking_playback(self, filepath, input_type='touchpad'):
"""Playback a given set of inputs and sleep for duration.
The input file is of the format <name>\nE: <time> <input>\nE: ...
Find the total time by the difference between the first and last input.
@param filepath: path to the input file on the DUT.
@param input_type: name of device type; 'touchpad' by default.
Types are returned by the _determine_input_type()
function.
input_type must be known. Check using has().
"""
with open(filepath) as fh:
lines = fh.readlines()
start = float(lines[0].split(' ')[1])
end = float(lines[-1].split(' ')[1])
sleep_time = end - start + self._PLAYBACK_OVERHEAD_LATENCY
start_time = time.time()
self.playback(filepath, input_type)
end_time = time.time()
elapsed_time = end_time - start_time
if elapsed_time < sleep_time:
sleep_time -= elapsed_time
logging.info('Blocking for %s seconds after playback.', sleep_time)
time.sleep(sleep_time)
def blocking_playback_of_default_file(self, filename, input_type='mouse'):
"""Playback a default file and sleep for duration.
Use a default gesture file for the default keyboard/mouse, saved in
this folder.
Device should be emulated first.
@param filename: the name of the file (path is to this folder).
@param input_type: name of device type; 'mouse' by default.
Types are returned by the _determine_input_type()
function.
input_type must be known. Check using has().
"""
current_dir = os.path.dirname(os.path.realpath(__file__))
gesture_file = os.path.join(current_dir, filename)
self.blocking_playback(gesture_file, input_type=input_type)
def close(self):
"""Kill emulation if necessary."""
if self._emulated_device:
num_events_before = len(self._get_input_events())
device_name = self._emulated_device.name
self._emulated_device.emulation_process.kill()
# Ensure there is one fewer input event before returning.
try:
expected = num_events_before - 1
utils.poll_for_condition(
lambda: len(self._get_input_events()) == expected,
exception=error.TestError())
except error.TestError as e:
logging.warning('Could not kill emulated %s!', device_name)
self._emulated_device = None
def __enter__(self):
"""Allow usage in 'with' statements."""
return self
def __exit__(self, exc_type, exc_val, exc_tb):
"""Release resources on completion of a 'with' statement."""
self.close()