| # Copyright (c) 2013 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. |
| |
| """A wrapper for robot manipulation with Touchbot II.""" |
| |
| import math |
| import os |
| import re |
| import sys |
| import time |
| |
| import common_util |
| import test_conf as conf |
| |
| from firmware_constants import GV, MODE |
| |
| |
| # Define the robot control script names. |
| SCRIPT_LINE = 'line.py' |
| SCRIPT_TAP = 'tap.py' |
| SCRIPT_CLICK = 'click.py' |
| SCRIPT_ONE_STATIONARY_FINGER = 'one_stationary_finger.py' |
| SCRIPT_STATIONARY_WITH_TAPS = 'stationary_finger_with_taps_around_it.py' |
| SCRIPT_QUICKSTEP = 'quickstep.py' |
| |
| # A script to "reset" the robot by having it move safely to a spot just above |
| # the center of the touchpad. |
| SCRIPT_RESET = 'reset.py' |
| |
| # Define constants for coordinates. |
| # Normally, a gesture is performed within [START, END]. |
| # For tests involved with RangeValidator which intends to verify |
| # the min/max reported coordinates, use [OFF_START, OFF_END] instead |
| # so that the gestures are performed off the edge. |
| START = 0.1 |
| CENTER = 0.5 |
| END = 0.9 |
| OFF_START = -0.05 |
| OFF_END = 1.05 |
| ABOVE_CENTER = 0.25 |
| BELOW_CENTER = 0.75 |
| LEFT_TO_CENTER = 0.25 |
| RIGHT_TO_CENTER = 0.75 |
| |
| OUTER_PINCH_SPACING = 70 |
| INNER_PINCH_SPACING = 25 |
| |
| PHYSICAL_CLICK_SPACING = 20 |
| PHYSICAL_CLICK_FINGER_SIZE = 2 |
| |
| TWO_CLOSE_FINGER_SPACING = 17 |
| TWO_FINGER_SPACING = 25 |
| |
| class RobotWrapperError(Exception): |
| """An exception class for the robot_wrapper module.""" |
| pass |
| |
| |
| class RobotWrapper: |
| """A class to wrap and manipulate the robot library.""" |
| |
| def __init__(self, board, mode, is_touchscreen, should_calibrate=True): |
| self._board = board |
| self._mode = mode |
| self.is_touchscreen = is_touchscreen |
| self._robot_script_dir = self._get_robot_script_dir() |
| |
| # Each get_contorol_command method maps to a script name. |
| self._robot_script_name_dict = { |
| self._get_control_command_line: SCRIPT_LINE, |
| self._get_control_command_rapid_taps: SCRIPT_TAP, |
| self._get_control_command_single_tap: SCRIPT_TAP, |
| self._get_control_command_drumroll: SCRIPT_TAP, |
| self._get_control_command_click: SCRIPT_CLICK, |
| self._get_control_command_one_stationary_finger: |
| SCRIPT_ONE_STATIONARY_FINGER, |
| self._get_control_command_pinch: SCRIPT_LINE, |
| self._get_control_command_stationary_with_taps: |
| SCRIPT_STATIONARY_WITH_TAPS, |
| self._get_control_command_quickstep: SCRIPT_QUICKSTEP, |
| } |
| |
| # Each gesture maps to a get_contorol_command method |
| self._method_of_control_command_dict = { |
| conf.ONE_FINGER_TRACKING: self._get_control_command_line, |
| conf.ONE_FINGER_TO_EDGE: self._get_control_command_line, |
| conf.ONE_FINGER_SWIPE: self._get_control_command_line, |
| conf.ONE_FINGER_TAP: self._get_control_command_single_tap, |
| conf.ONE_FINGER_TRACKING_FROM_CENTER: |
| self._get_control_command_line, |
| conf.RAPID_TAPS: self._get_control_command_rapid_taps, |
| conf.TWO_FINGER_TRACKING: self._get_control_command_line, |
| conf.TWO_FINGER_SWIPE: self._get_control_command_line, |
| conf.TWO_FINGER_TAP: self._get_control_command_single_tap, |
| conf.TWO_CLOSE_FINGERS_TRACKING: self._get_control_command_line, |
| conf.RESTING_FINGER_PLUS_2ND_FINGER_MOVE: |
| self._get_control_command_one_stationary_finger, |
| conf.FIRST_FINGER_TRACKING_AND_SECOND_FINGER_TAPS: |
| self._get_control_command_one_stationary_finger, |
| conf.FINGER_CROSSING: |
| self._get_control_command_one_stationary_finger, |
| conf.PINCH_TO_ZOOM: self._get_control_command_pinch, |
| conf.DRUMROLL: self._get_control_command_drumroll, |
| conf.TWO_FAT_FINGERS_TRACKING: self._get_control_command_line, |
| conf.ONE_FINGER_PHYSICAL_CLICK: self._get_control_command_click, |
| conf.TWO_FINGER_PHYSICAL_CLICK: self._get_control_command_click, |
| conf.THREE_FINGER_PHYSICAL_CLICK: self._get_control_command_click, |
| conf.FOUR_FINGER_PHYSICAL_CLICK: self._get_control_command_click, |
| conf.STATIONARY_FINGER_NOT_AFFECTED_BY_2ND_FINGER_TAPS: |
| self._get_control_command_stationary_with_taps, |
| conf.FAT_FINGER_MOVE_WITH_RESTING_FINGER: |
| self._get_control_command_one_stationary_finger, |
| conf.DRAG_LATENCY: |
| self._get_control_command_quickstep, |
| } |
| |
| self._line_dict = { |
| GV.LR: (START, CENTER, END, CENTER), |
| GV.RL: (END, CENTER, START, CENTER), |
| GV.TB: (CENTER, START, CENTER, END), |
| GV.BT: (CENTER, END, CENTER, START), |
| GV.BLTR: (START, END, END, START), |
| GV.TRBL: (END, START, START, END), |
| GV.BRTL: (END, END, START, START), |
| GV.TLBR: (START, START, END, END), |
| GV.CUR: (CENTER, CENTER, END, START), |
| GV.CUL: (CENTER, CENTER, START, START), |
| GV.CLR: (CENTER, CENTER, END, END), |
| GV.CLL: (CENTER, CENTER, START, END), |
| |
| # Overshoot for this one-finger gesture only: ONE_FINGER_TO_EDGE |
| GV.CL: (CENTER, CENTER, OFF_START, CENTER), |
| GV.CR: (CENTER, CENTER, OFF_END, CENTER), |
| GV.CT: (CENTER, CENTER, CENTER, OFF_START), |
| GV.CB: (CENTER, CENTER, CENTER, OFF_END), |
| } |
| |
| # The angle wrt the pad that the fingers should take when doing a 2f |
| # gesture along these lines, or doing 2f taps at different angles. |
| self._angle_dict = { |
| GV.LR: -45, |
| GV.RL: -45, |
| GV.TB: 45, |
| GV.BT: 45, |
| GV.TLBR: 90, |
| GV.BLTR: 0, |
| GV.TRBL: 0, |
| GV.HORIZONTAL: -45, |
| GV.VERTICAL: 45, |
| GV.DIAGONAL: 0, |
| } |
| |
| # The stationary finger locations corresponding the line specifications |
| # for the finger crossing tests |
| self._stationary_finger_dict = { |
| GV.LR: (CENTER, ABOVE_CENTER), |
| GV.RL: (CENTER, BELOW_CENTER), |
| GV.TB: (RIGHT_TO_CENTER, CENTER), |
| GV.BT: (LEFT_TO_CENTER, CENTER), |
| GV.BLTR: (RIGHT_TO_CENTER, BELOW_CENTER), |
| GV.TRBL: (LEFT_TO_CENTER, ABOVE_CENTER), |
| } |
| |
| self._speed_dict = { |
| GV.SLOW: 10, |
| GV.NORMAL: 20, |
| GV.FAST: 30, |
| GV.FULL_SPEED: 100, |
| } |
| |
| self._location_dict = { |
| # location parameters for one-finger taps |
| GV.TL: (START, START), |
| GV.TR: (END, START), |
| GV.BL: (START, END), |
| GV.BR: (END, END), |
| GV.TS: (CENTER, START), |
| GV.BS: (CENTER, END), |
| GV.LS: (START, CENTER), |
| GV.RS: (END, CENTER), |
| GV.CENTER: (CENTER, CENTER), |
| } |
| |
| self.fingertips = [None, None, None, None] |
| |
| self._build_robot_script_paths() |
| |
| if should_calibrate: |
| self._calibrate_device(board) |
| |
| def _get_fingertips(self, tips_to_get): |
| if self.fingertips != [None, None, None, None]: |
| print 'Error, there are still fingertips on' |
| sys.exit(1) |
| |
| for size in conf.ALL_FINGERTIP_SIZES: |
| fingers = [1 if f == size else 0 for f in tips_to_get] |
| print 'size: %d\tfingers: %s' % (size, str(fingers)) |
| if fingers == [0, 0, 0, 0]: |
| continue |
| |
| script = os.path.join(self._robot_script_dir, |
| 'manipulate_fingertips.py') |
| para = (script, fingers[0], fingers[1], fingers[2], fingers[3], |
| size, str(True)) |
| cmd = 'python %s %d %d %d %d %d %s' % para |
| |
| if self._execute_control_command(cmd): |
| raise RobotWrapperError('Error getting the fingertips!') |
| self.fingertips = tips_to_get |
| |
| def _return_fingertips(self): |
| """ Return all the fingertips to the nest, one size at a time. |
| This function uses the self.fingertips member variable to know which |
| finger has which tip size, then returns them all to the nest |
| """ |
| for size in conf.ALL_FINGERTIP_SIZES: |
| # See which (if any) of the fingers currently have this size tip |
| fingers = [1 if f == size else 0 for f in self.fingertips] |
| if fingers == [0, 0, 0, 0]: |
| continue |
| |
| script = os.path.join(self._robot_script_dir, |
| 'manipulate_fingertips.py') |
| para = (script, fingers[0], fingers[1], fingers[2], fingers[3], |
| size, str(False)) |
| cmd = 'python %s %d %d %d %d %d %s' % para |
| |
| if self._execute_control_command(cmd): |
| raise RobotWrapperError('Error returning the fingertips!') |
| |
| self.fingertips = [None, None, None, None] |
| |
| def _calibrate_device(self, board): |
| """ Have the operator show the robot where the device is.""" |
| calib_script = os.path.join(self._robot_script_dir, |
| 'calibrate_for_new_device.py') |
| calib_cmd = 'python %s %s' % (calib_script, board) |
| self._execute_control_command(calib_cmd) |
| |
| def _is_robot_action_mode(self): |
| """Is it in robot action mode? |
| |
| In the robot action mode, it actually invokes the robot control script. |
| """ |
| return self._mode in [MODE.ROBOT] |
| |
| def _raise_error(self, msg): |
| """Only raise an error if it is in the robot action mode.""" |
| if self._is_robot_action_mode(): |
| raise RobotWrapperError(msg) |
| |
| def _get_robot_script_dir(self): |
| """Get the directory of the robot control scripts.""" |
| for lib_path in [conf.robot_lib_path_local, conf.robot_lib_path]: |
| cmd = ('find %s -maxdepth 1 -type d -name %s' % |
| (lib_path, conf.python_package)) |
| path = common_util.simple_system_output(cmd) |
| if path: |
| robot_script_dir = os.path.join(path, conf.gestures_sub_path) |
| if os.path.isdir(robot_script_dir): |
| return robot_script_dir |
| return '' |
| |
| def _get_num_taps(self, gesture): |
| """Determine the number of times to tap.""" |
| matches = re.match('[^0-9]*([0-9]*)[^0-9]*', gesture) |
| return int(matches.group(1)) if matches else None |
| |
| def _reverse_coord_if_is_touchscreen(self, coordinates): |
| """Reverse the coordinates if the device is a touchscreen. |
| |
| E.g., the original coordinates = (0.1, 0.9) |
| After reverse, the coordinates = (1 - 0.1, 1 - 0.9) = (0.9, 0.1) |
| |
| @param coordinates: a tuple of coordinates |
| """ |
| return (tuple(1.0 - c for c in coordinates) if self.is_touchscreen else |
| coordinates) |
| |
| def _get_control_command_pinch(self, robot_script, gesture, variation): |
| # Depending on which direction you're zooming, change the order of the |
| # finger spacings. |
| if GV.ZOOM_IN in variation: |
| starting_spacing = INNER_PINCH_SPACING |
| ending_spacing = OUTER_PINCH_SPACING |
| else: |
| starting_spacing = OUTER_PINCH_SPACING |
| ending_spacing = INNER_PINCH_SPACING |
| |
| # Keep the hand centered on the pad, and make the fingers move |
| # in or out with only two opposing fingers on the pad. |
| para = (robot_script, self._board, |
| CENTER, CENTER, 45, starting_spacing, |
| CENTER, CENTER, 45, ending_spacing, |
| 0, 1, 0, 1, self._speed_dict[GV.SLOW], 'basic') |
| return 'python %s %s_min.p %f %f %d %d %f %f %d %d %d %d %d %d %f %s' % para |
| |
| def _get_control_command_quickstep(self, robot_script, gesture, variation): |
| """have the robot do the zig-zag gesture for latency testing.""" |
| para = (robot_script, self._board, self._speed_dict[GV.FULL_SPEED]) |
| return 'python %s %s_min.p %d' % para |
| |
| def _get_control_command_one_stationary_finger(self, robot_script, gesture, |
| variation): |
| line = speed = None |
| num_taps = 0 |
| # The stationary finger should be in the bottom left corner for resting |
| # finger tests, and various locations for finger crossing tests |
| stationary_finger = (START, END) |
| |
| for element in variation: |
| if element in GV.GESTURE_DIRECTIONS: |
| line = self._line_dict[element] |
| if 'finger_crossing' in gesture: |
| stationary_finger = self._stationary_finger_dict[element] |
| elif 'second_finger_taps' in gesture: |
| stationary_finger = self._location_dict[GV.BL] |
| speed = self._speed_dict[GV.SLOW] |
| num_taps = 3 |
| elif 'fat_finger' in gesture: |
| stationary_finger = self._stationary_finger_dict[element] |
| speed = self._speed_dict[GV.SLOW] |
| elif element in GV.GESTURE_SPEED: |
| speed = self._speed_dict[element] |
| |
| if line is None or speed is None: |
| msg = 'Cannot derive the line/speed parameters from %s %s.' |
| self._raise_error(msg % (gesture, variation)) |
| |
| line = self._reverse_coord_if_is_touchscreen(line) |
| start_x, start_y, end_x, end_y = line |
| stationary_x, stationary_y = stationary_finger |
| |
| para = (robot_script, self._board, stationary_x, stationary_y, |
| start_x, start_y, end_x, end_y, speed, num_taps) |
| cmd = 'python %s %s_min.p %f %f %f %f %f %f %s --taps=%d' % para |
| return cmd |
| |
| def _get_control_command_line(self, robot_script, gesture, variation): |
| """Get robot control command for gestures using robot line script.""" |
| line_type = 'swipe' if bool('swipe' in gesture) else 'basic' |
| line = speed = finger_angle = None |
| for element in variation: |
| if element in GV.GESTURE_DIRECTIONS: |
| line = self._line_dict[element] |
| finger_angle = self._angle_dict.get(element, None) |
| elif element in GV.GESTURE_SPEED: |
| speed = self._speed_dict[element] |
| |
| if not speed: |
| if line_type is 'swipe': |
| speed = self._speed_dict[GV.FAST] |
| if 'two_close_fingers' in gesture or 'fat' in gesture: |
| speed = self._speed_dict[GV.NORMAL] |
| |
| if line is None or speed is None: |
| msg = 'Cannot derive the line/speed parameters from %s %s.' |
| self._raise_error(msg % (gesture, variation)) |
| |
| line = self._reverse_coord_if_is_touchscreen(line) |
| start_x, start_y, end_x, end_y = line |
| |
| if 'two' in gesture: |
| if 'close_fingers' in gesture: |
| finger_spacing = TWO_CLOSE_FINGER_SPACING |
| finger_angle += 45 |
| fingers = (1, 1, 0, 0) |
| else: |
| finger_spacing = TWO_FINGER_SPACING |
| fingers = (0, 1, 0, 1) |
| |
| if finger_angle is None: |
| msg = 'Unable to determine finger angle for %s %s.' |
| self._raise_error(msg % (gesture, variation)) |
| else: |
| finger_spacing = 17 |
| fingers = (0, 1, 0, 0) |
| finger_angle = 0 |
| |
| para = (robot_script, self._board, |
| start_x, start_y, finger_angle, finger_spacing, |
| end_x, end_y, finger_angle, finger_spacing, |
| fingers[0], fingers[1], fingers[2], fingers[3], |
| speed, line_type) |
| cmd = 'python %s %s.p %f %f %d %d %f %f %d %d %d %d %d %d %f %s' % para |
| return cmd |
| |
| def _get_control_command_stationary_with_taps(self, robot_script, gesture, |
| variation): |
| """ There is only one variant of this gesture, so there is only one |
| command to generate. This is the command for tapping around a |
| stationary finger on the pad. |
| """ |
| return 'python %s %s.p 0.5 0.5' % (robot_script, self._board) |
| |
| def _get_control_command_rapid_taps(self, robot_script, gesture, variation): |
| num_taps = self._get_num_taps(gesture) |
| return self._get_control_command_taps(robot_script, gesture, |
| variation, num_taps) |
| |
| def _get_control_command_single_tap(self, robot_script, gesture, variation): |
| return self._get_control_command_taps(robot_script, gesture, |
| variation, 1) |
| |
| def _get_control_command_drumroll(self, robot_script, gesture, variation): |
| """Get robot control command for the drumroll gesture. There is only |
| one so there is no need for complicated parsing here. |
| """ |
| return ('python %s %s.p 0.5 0.5 45 20 0 1 0 1 50 drumroll' % |
| (robot_script, self._board)) |
| |
| def _get_control_command_taps(self, robot_script, gesture, |
| variation, num_taps): |
| """Get robot control command for tap gestures. This includes rapid tap |
| tests as well as 1 and 2 finger taps at various locations on the pad. |
| """ |
| if num_taps is None: |
| msg = 'Cannot determine the number of taps to do from %s.' |
| self._raise_error(msg % gesture) |
| |
| # The tap commands have identical arguments as the click except with |
| # two additional arguments at the end. As such we generate the 'click' |
| # command and add these on to make it work as a tap. |
| cmd = self._get_control_command_click_tap(robot_script, gesture, |
| variation) |
| control_cmd = '%s %d tap' % (cmd, num_taps) |
| return control_cmd |
| |
| def _get_control_command_click(self, robot_script, gesture, variation): |
| """Get the robot control command for a physical click gesture""" |
| cmd = self._get_control_command_click_tap(robot_script, gesture, |
| variation) |
| control_cmd = '%s %d' % (cmd, PHYSICAL_CLICK_FINGER_SIZE) |
| return control_cmd |
| |
| def _get_control_command_click_tap(self, robot_script, gesture, variation): |
| """Get robot control arguments that are common between pysical click |
| and tapping gestures |
| """ |
| location = None |
| angle = 45 |
| for element in variation: |
| if element in self._location_dict: |
| location = self._location_dict[element] |
| if 'two' in gesture and element in self._angle_dict: |
| angle = self._angle_dict[element] |
| |
| # All non-one finger taps are simply perfomed in the middle of the pad |
| if 'one' not in gesture and location is None: |
| location = self._location_dict[GV.CENTER] |
| |
| if location is None: |
| msg = 'Cannot determine the location parameters from %s %s.' |
| self._raise_error(msg % (gesture, variation)) |
| |
| fingers = [1, 0, 0, 0] |
| if 'two' in gesture: |
| fingers = [0, 1, 0, 1] |
| elif 'three' in gesture: |
| fingers = [0, 1, 1, 1] |
| elif 'four' in gesture: |
| fingers = [1, 1, 1, 1] |
| |
| location_str = ' '.join( |
| map(str, self._reverse_coord_if_is_touchscreen(location))) |
| |
| para = (robot_script, self._board, location_str, angle, |
| PHYSICAL_CLICK_SPACING, |
| fingers[0], fingers[1], fingers[2], fingers[3]) |
| control_cmd = 'python %s %s.p %s %d %d %d %d %d %d' % para |
| return control_cmd |
| |
| def _build_robot_script_paths(self): |
| """Build the robot script paths.""" |
| # Check if the robot script dir could be found. |
| if not self._robot_script_dir: |
| script_path = os.path.join(conf.robot_lib_path, conf.python_package, |
| conf.gestures_sub_path) |
| msg = 'Cannot find robot script directory in "%s".' |
| self._raise_error(msg % script_path) |
| |
| # Build the robot script path dictionary |
| self._robot_script_dict = {} |
| for method in self._robot_script_name_dict: |
| script_name = self._robot_script_name_dict.get(method) |
| |
| # Check if the control script actually exists. |
| robot_script = os.path.join(self._robot_script_dir, script_name) |
| if not os.path.isfile(robot_script): |
| msg = 'Cannot find the robot control script: %s' |
| self._raise_error(msg % robot_script) |
| |
| self._robot_script_dict[method] = robot_script |
| |
| def _get_control_command(self, gesture, variation): |
| """Get robot control command based on the gesture and variation.""" |
| script_method = self._method_of_control_command_dict.get(gesture) |
| if not script_method: |
| self._raise_error('Cannot find "%s" gesture in ' |
| '_method_of_control_command_dict.' % gesture) |
| |
| robot_script = self._robot_script_dict.get(script_method) |
| if not robot_script: |
| msg = 'Cannot find "%s" method in _robot_script_dict.' |
| self._raise_error(msg % script_method) |
| |
| return script_method(robot_script, gesture, variation) |
| |
| def _execute_control_command(self, control_cmd): |
| """Execute a control command.""" |
| print 'Executing: "%s"' % control_cmd |
| if self._is_robot_action_mode(): |
| # Pausing to give everything time to settle |
| time.sleep(0.5) |
| return common_util.simple_system(control_cmd) |
| return 0 |
| |
| def control(self, gesture, variation): |
| """Have the robot perform the gesture variation.""" |
| tips_needed = conf.finger_tips_required[gesture.name] |
| if self.fingertips != tips_needed: |
| self._return_fingertips() |
| self._get_fingertips(tips_needed) |
| |
| if not isinstance(variation, tuple): |
| variation = (variation,) |
| try: |
| print gesture.name, variation |
| control_cmd = self._get_control_command(gesture.name, variation) |
| self._execute_control_command(control_cmd) |
| except RobotWrapperError as e: |
| print gesture.name, variation |
| print 'RobotWrapperError: %s' % str(e) |
| sys.exit(1) |