Home | History | Annotate | Download | only in input_playback
      1 # Copyright 2015 The Chromium OS Authors. All rights reserved.
      2 # Use of this source code is governed by a BSD-style license that can be
      3 # found in the LICENSE file.
      4 
      5 import logging
      6 import os
      7 import subprocess
      8 import tempfile
      9 import time
     10 
     11 from autotest_lib.client.bin import utils
     12 from autotest_lib.client.common_lib import error
     13 
     14 
     15 class Device(object):
     16     """Information about a specific input device."""
     17     def __init__(self, input_type):
     18         self.input_type = input_type  # e.g. 'touchpad'
     19         self.emulated = False  # Whether device is real or not
     20         self.emulation_process = None  # Process of running emulation
     21         self.name = 'unknown'  # e.g. 'Atmel maXTouch Touchpad'
     22         self.fw_id = None  # e.g. '6.0'
     23         self.hw_id = None  # e.g. '90.0'
     24         self.node = None  # e.g. '/dev/input/event4'
     25         self.device_dir = None  # e.g. '/sys/class/input/event4/device/device'
     26 
     27     def __str__(self):
     28         s = '%s:' % self.input_type
     29         s += '\n  Name: %s' % self.name
     30         s += '\n  Node: %s' % self.node
     31         s += '\n  hw_id: %s' % self.hw_id
     32         s += '\n  fw_id: %s' % self.fw_id
     33         s += '\n  Emulated: %s' % self.emulated
     34         return s
     35 
     36 
     37 class InputPlayback(object):
     38     """
     39     Provides an interface for playback and emulating peripherals via evemu-*.
     40 
     41     Example use: player = InputPlayback()
     42                  player.emulate(property_file=path_to_file)
     43                  player.find_connected_inputs()
     44                  player.playback(path_to_file)
     45                  player.blocking_playback(path_to_file)
     46                  player.close()
     47 
     48     """
     49 
     50     _DEFAULT_PROPERTY_FILES = {'mouse': 'mouse.prop',
     51                                'keyboard': 'keyboard.prop'}
     52     _PLAYBACK_COMMAND = 'evemu-play --insert-slot0 %s < %s'
     53 
     54     # Define a keyboard as anything with any keys #2 to #248 inclusive,
     55     # as defined in the linux input header.  This definition includes things
     56     # like the power button, so reserve the "keyboard" label for things with
     57     # letters/numbers and define the rest as "other_keyboard".
     58     _MINIMAL_KEYBOARD_KEYS = ['1', 'Q', 'SPACE']
     59     _KEYBOARD_KEYS = [
     60             '1', '2', '3', '4', '5', '6', '7', '8', '9', '0', 'MINUS', 'EQUAL',
     61             'BACKSPACE', 'TAB', 'Q', 'W', 'E', 'R', 'T', 'Y', 'U', 'I', 'O',
     62             'P', 'LEFTBRACE', 'RIGHTBRACE', 'ENTER', 'LEFTCTRL', 'A', 'S', 'D',
     63             'F', 'G', 'H', 'J', 'K', 'L', 'SEMICOLON', 'APOSTROPHE', 'GRAVE',
     64             'LEFTSHIFT', 'BACKSLASH', 'Z', 'X', 'C', 'V', 'B', 'N', 'M',
     65             'COMMA', 'DOT', 'SLASH', 'RIGHTSHIFT', 'KPASTERISK', 'LEFTALT',
     66             'SPACE', 'CAPSLOCK', 'F1', 'F2', 'F3', 'F4', 'F5', 'F6', 'F7', 'F8',
     67             'F9', 'F10', 'NUMLOCK', 'SCROLLLOCK', 'KP7', 'KP8', 'KP9',
     68             'KPMINUS', 'KP4', 'KP5', 'KP6', 'KPPLUS', 'KP1', 'KP2', 'KP3',
     69             'KP0', 'KPDOT', 'ZENKAKUHANKAKU', '102ND', 'F11', 'F12', 'RO',
     70             'KATAKANA', 'HIRAGANA', 'HENKAN', 'KATAKANAHIRAGANA', 'MUHENKAN',
     71             'KPJPCOMMA', 'KPENTER', 'RIGHTCTRL', 'KPSLASH', 'SYSRQ', 'RIGHTALT',
     72             'LINEFEED', 'HOME', 'UP', 'PAGEUP', 'LEFT', 'RIGHT', 'END', 'DOWN',
     73             'PAGEDOWN', 'INSERT', 'DELETE', 'MACRO', 'MUTE', 'VOLUMEDOWN',
     74             'VOLUMEUP', 'POWER', 'KPEQUAL', 'KPPLUSMINUS', 'PAUSE', 'SCALE',
     75             'KPCOMMA', 'HANGEUL', 'HANGUEL', 'HANJA', 'YEN', 'LEFTMETA',
     76             'RIGHTMETA', 'COMPOSE', 'STOP', 'AGAIN', 'PROPS', 'UNDO', 'FRONT',
     77             'COPY', 'OPEN', 'PASTE', 'FIND', 'CUT', 'HELP', 'MENU', 'CALC',
     78             'SETUP', 'WAKEUP', 'FILE', 'SENDFILE', 'DELETEFILE', 'XFER',
     79             'PROG1', 'PROG2', 'WWW', 'MSDOS', 'COFFEE', 'SCREENLOCK',
     80             'DIRECTION', 'CYCLEWINDOWS', 'MAIL', 'BOOKMARKS', 'COMPUTER',
     81             'BACK', 'FORWARD', 'CLOSECD', 'EJECTCD', 'EJECTCLOSECD', 'NEXTSONG',
     82             'PLAYPAUSE', 'PREVIOUSSONG', 'STOPCD', 'RECORD', 'REWIND', 'PHONE',
     83             'ISO', 'CONFIG', 'HOMEPAGE', 'REFRESH', 'EXIT', 'MOVE', 'EDIT',
     84             'SCROLLUP', 'SCROLLDOWN', 'KPLEFTPAREN', 'KPRIGHTPAREN', 'NEW',
     85             'REDO', 'F13', 'F14', 'F15', 'F16', 'F17', 'F18', 'F19', 'F20',
     86             'F21', 'F22', 'F23', 'F24', 'PLAYCD', 'PAUSECD', 'PROG3', 'PROG4',
     87             'DASHBOARD', 'SUSPEND', 'CLOSE', 'PLAY', 'FASTFORWARD', 'BASSBOOST',
     88             'PRINT', 'HP', 'CAMERA', 'SOUND', 'QUESTION', 'EMAIL', 'CHAT',
     89             'SEARCH', 'CONNECT', 'FINANCE', 'SPORT', 'SHOP', 'ALTERASE',
     90             'CANCEL', 'BRIGHTNESSDOWN', 'BRIGHTNESSUP', 'MEDIA',
     91             'SWITCHVIDEOMODE', 'KBDILLUMTOGGLE', 'KBDILLUMDOWN', 'KBDILLUMUP',
     92             'SEND', 'REPLY', 'FORWARDMAIL', 'SAVE', 'DOCUMENTS', 'BATTERY',
     93             'BLUETOOTH', 'WLAN', 'UWB', 'UNKNOWN', 'VIDEO_NEXT', 'VIDEO_PREV',
     94             'BRIGHTNESS_CYCLE', 'BRIGHTNESS_AUTO', 'BRIGHTNESS_ZERO',
     95             'DISPLAY_OFF', 'WWAN', 'WIMAX', 'RFKILL', 'MICMUTE']
     96 
     97 
     98     def __init__(self):
     99         self.devices = {}
    100         self._emulated_device = None
    101 
    102 
    103     def has(self, input_type):
    104         """Return True/False if device has a input of given type.
    105 
    106         @param input_type: string of type, e.g. 'touchpad'
    107 
    108         """
    109         return input_type in self.devices
    110 
    111 
    112     def _get_input_events(self):
    113         """Return a list of all input event nodes."""
    114         return utils.run('ls /dev/input/event*').stdout.strip().split()
    115 
    116 
    117     def emulate(self, input_type='mouse', property_file=None):
    118         """
    119         Emulate the given input (or default for type) with evemu-device.
    120 
    121         Emulating more than one of the same device type will only allow playback
    122         on the last one emulated.  The name of the last-emulated device is
    123         noted to be sure this is the case.
    124 
    125         Property files are made with the evemu-describe command,
    126         e.g. 'evemu-describe /dev/input/event12 > property_file'.
    127 
    128         @param input_type: 'mouse' or 'keyboard' to use default property files.
    129                            Need not be specified if supplying own file.
    130         @param property_file: Property file of device to be emulated.  Generate
    131                               with 'evemu-describe' command on test image.
    132 
    133         """
    134         new_device = Device(input_type)
    135         new_device.emulated = True
    136 
    137         # Checks for any previous emulated device and kills the process
    138         self.close()
    139 
    140         if not property_file:
    141             if input_type not in self._DEFAULT_PROPERTY_FILES:
    142                 raise error.TestError('Please supply a property file for input '
    143                                       'type %s' % input_type)
    144             current_dir = os.path.dirname(os.path.realpath(__file__))
    145             property_file = os.path.join(
    146                     current_dir, self._DEFAULT_PROPERTY_FILES[input_type])
    147         if not os.path.isfile(property_file):
    148             raise error.TestError('Property file %s not found!' % property_file)
    149 
    150         logging.info('Emulating %s %s', input_type, property_file)
    151         num_events_before = len(self._get_input_events())
    152         new_device.emulation_process = subprocess.Popen(
    153                 ['evemu-device', property_file], stdout=subprocess.PIPE)
    154         utils.poll_for_condition(
    155                 lambda: len(self._get_input_events()) > num_events_before,
    156                 exception=error.TestError('Error emulating %s!' % input_type))
    157 
    158         with open(property_file) as fh:
    159             name_line = fh.readline()  # Format "N: NAMEOFDEVICE"
    160             new_device.name = name_line[3:-1]
    161 
    162         self._emulated_device = new_device
    163 
    164 
    165     def _find_device_properties(self, device):
    166         """Return string of properties for given node.
    167 
    168         @return: string of properties.
    169 
    170         """
    171         with tempfile.NamedTemporaryFile() as temp_file:
    172             filename = temp_file.name
    173             evtest_process = subprocess.Popen(['evtest', device],
    174                                               stdout=temp_file)
    175 
    176             def find_exit():
    177                 """Polling function for end of output."""
    178                 interrupt_cmd = 'grep "interrupt to exit" %s | wc -l' % filename
    179                 line_count = utils.run(interrupt_cmd).stdout.strip()
    180                 return line_count != '0'
    181 
    182             utils.poll_for_condition(find_exit)
    183             evtest_process.kill()
    184             temp_file.seek(0)
    185             props = temp_file.read()
    186         return props
    187 
    188 
    189     def _determine_input_type(self, props):
    190         """Find input type (if any) from a string of properties.
    191 
    192         @return: string of type, or None
    193 
    194         """
    195         if props.find('REL_X') >= 0 and props.find('REL_Y') >= 0:
    196             if (props.find('ABS_MT_POSITION_X') >= 0 and
    197                 props.find('ABS_MT_POSITION_Y') >= 0):
    198                 return 'multitouch_mouse'
    199             else:
    200                 return 'mouse'
    201         if props.find('ABS_X') >= 0 and props.find('ABS_Y') >= 0:
    202             if (props.find('BTN_STYLUS') >= 0 or
    203                 props.find('BTN_STYLUS2') >= 0 or
    204                 props.find('BTN_TOOL_PEN') >= 0):
    205                 return 'tablet'
    206             if (props.find('ABS_PRESSURE') >= 0 or
    207                 props.find('BTN_TOUCH') >= 0):
    208                 if (props.find('BTN_LEFT') >= 0 or
    209                     props.find('BTN_MIDDLE') >= 0 or
    210                     props.find('BTN_RIGHT') >= 0 or
    211                     props.find('BTN_TOOL_FINGER') >= 0):
    212                     return 'touchpad'
    213                 else:
    214                     return 'touchscreen'
    215             if props.find('BTN_LEFT') >= 0:
    216                 return 'touchscreen'
    217         if props.find('KEY_') >= 0:
    218             for key in self._MINIMAL_KEYBOARD_KEYS:
    219                 if props.find('KEY_%s' % key) >= 0:
    220                     return 'keyboard'
    221             for key in self._KEYBOARD_KEYS:
    222                 if props.find('KEY_%s' % key) >= 0:
    223                     return 'other_keyboard'
    224         return
    225 
    226 
    227     def _get_contents_of_file(self, filepath):
    228         """Return the contents of the given file.
    229 
    230         @param filepath: string of path to file
    231 
    232         @returns: contents of file.  Assumes file exists.
    233 
    234         """
    235         return utils.run('cat %s' % filepath).stdout.strip()
    236 
    237 
    238     def _find_device_ids(self, device_dir, input_type):
    239         """Find the fw_id and hw_id for the given device directory.
    240 
    241         Finding fw_id and hw_id applicable only for touchpads and touchscreens.
    242 
    243         @param device_dir: the device directory.
    244         @param input_type: string of input type.
    245 
    246         @returns: firmware id, hardware id
    247 
    248         """
    249         fw_id, hw_id = None, None
    250 
    251         if not device_dir or input_type not in ['touchpad', 'touchscreen']:
    252             return fw_id, hw_id
    253 
    254         # Touch devices with custom drivers save this info as a file.
    255         fw_filenames = ['fw_version', 'firmware_version', 'firmware_id']
    256         for fw_filename in fw_filenames:
    257             fw_path = os.path.join(device_dir, fw_filename)
    258             if os.path.exists(fw_path):
    259                 fw_id = self._get_contents_of_file(fw_path)
    260                 break
    261 
    262         hw_filenames = ['hw_version', 'product_id', 'board_id']
    263         for hw_filename in hw_filenames:
    264             hw_path = os.path.join(device_dir, hw_filename)
    265             if os.path.exists(hw_path):
    266                 hw_id = self._get_contents_of_file(hw_path)
    267                 break
    268 
    269         # Hw_ids for Weida and 2nd gen Synaptics are different.
    270         if not hw_id:
    271             id_folder = os.path.abspath(os.path.join(device_dir, '..', 'id'))
    272             product_path = os.path.join(id_folder, 'product')
    273             vendor_path = os.path.join(id_folder, 'vendor')
    274 
    275             if os.path.isfile(product_path):
    276                 product = self._get_contents_of_file(product_path)
    277                 if input_type == 'touchscreen':
    278                     if os.path.isfile(vendor_path):
    279                         vendor = self._get_contents_of_file(vendor_path)
    280                         hw_id = vendor + product
    281                 else:
    282                     hw_id = product
    283 
    284         # Fw_ids for 2nd gen Synaptics can only be found via rmi4update.
    285         # See if any /dev/hidraw* link to this device's input event.
    286         if not fw_id:
    287             input_name_path = os.path.join(device_dir, 'input')
    288             input_name = utils.run('ls %s' % input_name_path,
    289                                    ignore_status=True).stdout.strip()
    290             hidraws = utils.run('ls /dev/hidraw*').stdout.strip().split()
    291             for hidraw in hidraws:
    292                 class_folder = hidraw.replace('dev', 'sys/class/hidraw')
    293                 input_folder_path = os.path.join(class_folder, 'device',
    294                                                  'input', input_name)
    295                 if os.path.exists(input_folder_path):
    296                     fw_id = utils.run('rmi4update -p -d %s' % hidraw,
    297                                       ignore_status=True).stdout.strip()
    298                     if fw_id == '':
    299                         fw_id = None
    300 
    301         return fw_id, hw_id
    302 
    303 
    304     def find_connected_inputs(self):
    305         """Determine the nodes of all present input devices, if any.
    306 
    307         Cycle through all possible /dev/input/event* and find which ones
    308         are touchpads, touchscreens, mice, keyboards, etc.
    309         These nodes can be used for playback later.
    310         If the type of input is already emulated, prefer that device. Otherwise,
    311         prefer the last node found of that type (e.g. for multiple touchpads).
    312         Record the found devices in self.devices.
    313 
    314         """
    315         self.devices = {}  # Discard any previously seen nodes.
    316 
    317         input_events = self._get_input_events()
    318         for event in input_events:
    319             properties = self._find_device_properties(event)
    320             input_type = self._determine_input_type(properties)
    321             if input_type:
    322                 new_device = Device(input_type)
    323                 new_device.node = event
    324 
    325                 class_folder = event.replace('dev', 'sys/class')
    326                 name_file = os.path.join(class_folder, 'device', 'name')
    327                 if os.path.isfile(name_file):
    328                     name = self._get_contents_of_file(name_file)
    329                 logging.info('Found %s: %s at %s.', input_type, name, event)
    330 
    331                 # If a particular device is expected, make sure name matches.
    332                 if (self._emulated_device and
    333                     self._emulated_device.input_type == input_type):
    334                     if self._emulated_device.name != name:
    335                         continue
    336                     else:
    337                         new_device.emulated = True
    338                         process = self._emulated_device.emulation_process
    339                         new_device.emulation_process = process
    340                 new_device.name = name
    341 
    342                 # Find the devices folder containing power info
    343                 # e.g. /sys/class/event4/device/device
    344                 # Search that folder for hwid and fwid
    345                 device_dir = os.path.join(class_folder, 'device', 'device')
    346                 if os.path.exists(device_dir):
    347                     new_device.device_dir = device_dir
    348                     fw_id, hw_id = self._find_device_ids(device_dir, input_type)
    349                     new_device.fw_id, new_device.hw_id = fw_id, hw_id
    350 
    351                 if new_device.emulated:
    352                     self._emulated_device = new_device
    353 
    354                 self.devices[input_type] = new_device
    355                 logging.debug(self.devices[input_type])
    356 
    357 
    358     def playback(self, filepath, input_type='touchpad'):
    359         """Playback a given input file.
    360 
    361         Create input file using evemu-record.
    362         E.g. 'evemu-record $NODE -1 > $FILENAME'
    363 
    364         @param filepath: path to the input file on the DUT.
    365         @param input_type: name of device type; 'touchpad' by default.
    366                            Types are returned by the _determine_input_type()
    367                            function.
    368                            input_type must be known. Check using has().
    369 
    370         """
    371         assert(input_type in self.devices)
    372         node = self.devices[input_type].node
    373         logging.info('Playing back finger-movement on %s, file=%s.', node,
    374                      filepath)
    375         utils.run(self._PLAYBACK_COMMAND % (node, filepath))
    376 
    377 
    378     def blocking_playback(self, filepath, input_type='touchpad'):
    379         """Playback a given set of inputs and sleep for duration.
    380 
    381         The input file is of the format <name>\nE: <time> <input>\nE: ...
    382         Find the total time by the difference between the first and last input.
    383 
    384         @param filepath: path to the input file on the DUT.
    385         @param input_type: name of device type; 'touchpad' by default.
    386                            Types are returned by the _determine_input_type()
    387                            function.
    388                            input_type must be known. Check using has().
    389 
    390         """
    391         with open(filepath) as fh:
    392             lines = fh.readlines()
    393             start = float(lines[0].split(' ')[1])
    394             end = float(lines[-1].split(' ')[1])
    395             sleep_time = end - start
    396         self.playback(filepath, input_type)
    397         logging.info('Sleeping for %s seconds during playback.', sleep_time)
    398         time.sleep(sleep_time)
    399 
    400 
    401     def blocking_playback_of_default_file(self, filename, input_type='mouse'):
    402         """Playback a default file and sleep for duration.
    403 
    404         Use a default gesture file for the default keyboard/mouse, saved in
    405         this folder.
    406         Device should be emulated first.
    407 
    408         @param filename: the name of the file (path is to this folder).
    409         @param input_type: name of device type; 'mouse' by default.
    410                            Types are returned by the _determine_input_type()
    411                            function.
    412                            input_type must be known. Check using has().
    413 
    414         """
    415         current_dir = os.path.dirname(os.path.realpath(__file__))
    416         gesture_file = os.path.join(current_dir, filename)
    417         self.blocking_playback(gesture_file, input_type=input_type)
    418 
    419 
    420     def close(self):
    421         """Kill emulation if necessary."""
    422         if self._emulated_device:
    423             self._emulated_device.emulation_process.kill()
    424 
    425 
    426     def __exit__(self):
    427         self.close()
    428