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 5import logging 6import os 7import subprocess 8import tempfile 9import time 10 11from autotest_lib.client.bin import utils 12from autotest_lib.client.common_lib import error 13 14 15class 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 37class 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