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