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