blob: cef92a410bdf04f4cc3e87fa201ac51811a6f82e [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 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 InputPlayback(object):
Provides an interface for playback and emulating peripherals via evemu-*.
Example use: player = InputPlayback()
_DEFAULT_PROPERTY_FILES = {'mouse' : 'mouse.prop',
'keyboard' : 'keyboard.prop'}
_PLAYBACK_COMMAND = 'evemu-play --insert-slot0 %s < %s'
# 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".
'1', '2', '3', '4', '5', '6', '7', '8', '9', '0', 'MINUS', 'EQUAL',
'BACKSPACE', 'TAB', 'Q', 'W', 'E', 'R', 'T', 'Y', 'U', 'I', 'O',
'F', 'G', 'H', 'J', 'K', 'L', 'SEMICOLON', 'APOSTROPHE', 'GRAVE',
'LEFTSHIFT', 'BACKSLASH', 'Z', 'X', 'C', 'V', 'B', 'N', 'M',
'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',
'REDO', 'F13', 'F14', 'F15', 'F16', 'F17', 'F18', 'F19', 'F20',
'F21', 'F22', 'F23', 'F24', 'PLAYCD', 'PAUSECD', 'PROG3', 'PROG4',
def __init__(self):
self.nodes = {} # e.g. /dev/input/event4
self.device_dirs = {} # e.g. /sys/class/event4/device/device
self.names = {} # e.g. Atmel maXTouch Touchpad
self._device_emulation_process = 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.nodes
def _get_input_events(self):
"""Return a list of all input event nodes."""
return'ls /dev/input/event*').stdout.strip().split()
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.
# Checks for any previous emulated device and kills the process
if self._device_emulation_process:
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)'Emulating %s %s', input_type, property_file)
num_events_before = len(self._get_input_events())
self._device_emulation_process = subprocess.Popen(
['evemu-device', property_file], stdout=subprocess.PIPE)
lambda: len(self._get_input_events()) > num_events_before,
exception=error.TestError('Error emulating %s!' % input_type))
with open(property_file) as fh:
name_line = fh.readline() #Format "N: NAMEOFDEVICE"
self.names[input_type] = name_line[3:-1]
def _find_device_properties(self, device):
"""Return string of properties for given node.
@return: string of properties.
with tempfile.NamedTemporaryFile() as temp_file:
filename =
evtest_process = subprocess.Popen(['evtest', device],
def find_exit():
"""Polling function for end of output."""
interrupt_cmd = 'grep "interrupt to exit" %s | wc -l' % filename
line_count =
return line_count != '0'
props =
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'
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 'tablet'
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'
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'
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 a name already exists in self.names, prefer that device.
Record the found nodes in self.nodes and their names in self.names.
self.nodes = {} #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:
class_folder = event.replace('dev', 'sys/class')
name_file = os.path.join(class_folder, 'device', 'name')
name = 'unknown'
if os.path.isfile(name_file):
name ='cat %s' % name_file).stdout.strip()'Found %s: %s at %s.', input_type, name, event)
# If a particular device is expected, make sure name matches.
if input_type in self.names:
if self.names[input_type] != name:
# Find the devices folder containing power info
# e.g. /sys/class/event4/device/device
device_dir = os.path.join(class_folder, 'device', 'device')
if not os.path.exists(device_dir):
device_dir = None
# Save this device information for later use.
self.nodes[input_type] = event
self.names[input_type] = name
self.device_dirs[input_type] = device_dir
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()
input_type must be in self.nodes. Check using has().
assert(input_type in self.nodes)
node = self.nodes[input_type]'Playing back finger-movement on %s, file=%s.', node,
filepath) % (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()
input_type must be in self.nodes. 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(filepath, input_type)'Sleeping for %s seconds during playback.', 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()
input_type must be in self.nodes. 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._device_emulation_process:
def __exit__(self):