blob: 5bf08805bc7cafda01835d2f5784479c91be4dbf [file] [log] [blame]
# Copyright 2015 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.
import copy
import math
import os
import sys
import time
import numpy as np
from device_spec import DeviceSpec
from fingertip import Fingertip, LoadFingertips
from numpy import clip
from position import Position
from profile import Profile
from touchbotcomm import TouchbotComm
class Touchbot:
""" High level Touchbot control class
This class hides the internals of communicating with the robot and
offers high level robot control commands.
A touchbot object is initialized and then its member functions can
be called to execute various gestures
"""
# Network connection parameters for the robot
TOUCHBOT_IP_ADDR = '192.168.0.1'
TOUCHBOT_PORT = 10100
# How high above to approach from to be safe
SAFETY_CLEARANCE = 220
# Useful speeds for movement
SPEED_VERY_SLOW = 1.0
SPEED_SLOW = 20.0
SPEED_MEDIUM = 40.0
SPEED_FAST = 75.0
SPEED_VERY_FAST = 85.0
MAX_SPEED = 100.0
# How much area around the edge should be avoided for quickstep
QUICKSTEP_BUFFER = 0.1
# Limits on how far the robot hand can move the fingers in and out
MIN_FINGER_DISTANCE = 0.5
MAX_FINGER_DISTANCE = 140
MIN_CENTER_TO_CENTER_DISTANCE_MM = 10.0
# Settings for how to interpolate movement between two points
STRAIGHT_INTERPOLATION = -1
JOINT_INTERPOLATION = 0
INTERPOLATION_TYPES = [STRAIGHT_INTERPOLATION, JOINT_INTERPOLATION]
# Setting for movement blending on the robot. These set how the robot
# blends consecutive movement commands
BLEND_MOVEMENTS = -1
PRECISE_MOVEMENTS = 0
SMOOTHING_TYPES = [BLEND_MOVEMENTS, PRECISE_MOVEMENTS]
# Constants for the finger actuators
FINGER_NUMBERS = [1, 2]
FINGER_CONTINUOUS_CURRENT_LIMIT_MA = 500
FINGER_PEAK_CURRENT_LIMIT_MA = 700
FINGER_HOME_OFFSET = 2500
CALIBRATION_EXTENSION = 1500
MIN_FINGER_SPACING = 12
DEFAULT_FINGER_SPACING = 20
DRUMROLL_SPACING = 8
FINGER_ACCEL_MM_S_S = 500
FINGER_DECEL_MM_S_S = 2000
FINGER_PD_CONTROLLER_P = 20
FINGER_PD_CONTROLLER_D = 10
CALIBRATION_FINGERTIP1 = '1round_8mm'
CALIBRATION_FINGERTIP2 = '2round_8mm'
LARGE_FINGERTIP_VERTICAL_OFFSET = 2000
TAP_LIFT_DISTANCE = 500
CLICK_CURRENT_MA = 500
PHYSICAL_CLICK_SPEED_MM_S = 5
NO_RESPONSES = 0
REQUIRE_RESPONSES = 2
FINGER_RESPONSE_LEVELS = [NO_RESPONSES, REQUIRE_RESPONSES]
# The parameter number of the robot's name in the controller's memory
TOUCHBOT_NAME_PARAM_NUM = 2002
def __init__(self, ip_address=TOUCHBOT_IP_ADDR, port=TOUCHBOT_PORT):
self.speed_stack = []
self.finger_speed_stack = []
self.comm = TouchbotComm(ip_address, port)
# Load all the fingertip locations from disk
robot_name = self._GetRobotName()
print 'Robot name "%s" detected.' % robot_name
self.nest_locations_dir = 'nest_locations/%s' % robot_name
self.fingertips = LoadFingertips(self.nest_locations_dir)
if not self.fingertips:
print 'ERROR: Unable to load fingertip locations for that robot!'
self.attached_fingertips = set()
# Lift up and center the wrist to prevent it starting twisted
self._SetCartesian(self._AddSafetyClearance(self._GetCurrentPosition()))
self._CenterWrist()
# Initialize the fingers
self._InitializeFingers()
# Set everything to a sane speed
self.PushSpeed(Touchbot.SPEED_MEDIUM)
self._PushFingerSpeed_mm_per_s(1000)
# By default we want the robot to use linear movement interpolation
self._SetInterpolation(Touchbot.STRAIGHT_INTERPOLATION)
# By default the robot should be set to not blend consecutive move commands
# to produce more precise motions, at the cost of pauses between them.
self._SetSmoothing(Touchbot.PRECISE_MOVEMENTS)
def __del__(self):
""" Return all fingertips and disable the fingers before quitting. """
if self.comm:
self._RequireFingertips(None, [], should_end_over_dut=False)
self._SetCartesian(self._AddSafetyClearance(self._GetCurrentPosition()))
for finger_number in Touchbot.FINGER_NUMBERS:
self._DisableFinger(finger_number)
def _CenterWrist(self):
""" Rotate the wrist (axis 4) to it's middle point.
This should be done intermittently to make sure it doesn't drift too far.
The joint can only wrap around so many times before it's stuck, so this is
a safe place to start a gesture from.
"""
self.PushSpeed(Touchbot.SPEED_MEDIUM)
pos = self._GetCurrentPosition()
pos.ax_4 = 0
pos.ax_5 = self._SuggestSpacing(self.attached_fingertips)
err, _ = self._SetAngles(pos)
self.PopSpeed()
if err:
print 'ERROR: unable to center the wrist.'
def _InitializeFingers(self):
# First turn on the fingers, and and lift them up as high as they go
for finger_number in Touchbot.FINGER_NUMBERS:
self._TurnOnFinger(finger_number)
# Move the robot's hand over a known, flat surface to "zero" them on
self._MoveToNest()
zeroing_file = os.path.join(self.nest_locations_dir, 'zeroing_position.p')
zeroing_pos= Position.FromPickledFile(zeroing_file)
self._SetCartesian(zeroing_pos, finger_distance=5)
# Now lower the fingers slowly until they hit the surface. This will
# allow us to make sure they are perfectly zeroed
for finger_number in Touchbot.FINGER_NUMBERS:
self._ZeroFinger(finger_number)
def _ZeroFinger(self, finger_number):
self._ExtendFingerUntilContact(finger_number, speed=10)
self._SetFingerZeroPoint(finger_number)
def _TurnOnFinger(self, finger_number):
""" Do all the setup for a finger
This function sets the current limit for the finger controller and sets
the Zero point for the fingers
"""
self._SetFingerResponseLevel(Touchbot.REQUIRE_RESPONSES)
self._SendFingerCmd('%dDI' % finger_number)
time.sleep(0.5)
self._SetFingerContinuousCurrentLimit_mA(
finger_number, Touchbot.FINGER_CONTINUOUS_CURRENT_LIMIT_MA)
self._SetFingerPeakCurrentLimit_mA(
finger_number, Touchbot.FINGER_PEAK_CURRENT_LIMIT_MA)
self._SetFingerAcceleration(finger_number, Touchbot.FINGER_ACCEL_MM_S_S)
self._SetFingerDeceleration(finger_number, Touchbot.FINGER_DECEL_MM_S_S)
self._SetFingerPDParameters(finger_number,
Touchbot.FINGER_PD_CONTROLLER_P,
Touchbot.FINGER_PD_CONTROLLER_D)
# Track to roughly the middle of the range and call that Zero
self._SendFingerCmd('%dAPL0' % finger_number) # Disable range limiting
self._EnableFinger(finger_number)
self._ExtendFingerUntilContact(finger_number, speed=-30)
self._SetFingerZeroPoint(finger_number)
self._MoveFingerAbsolute(finger_number, 500)
def _PushFingerSpeed_mm_per_s(self, speed):
self.finger_speed_stack.append(self._GetFingerSpeed())
self._SetFingerSpeed(speed)
def _PopFingerSpeed(self):
if len(self.finger_speed_stack) >= 1:
self._SetFingerSpeed(self.finger_speed_stack.pop())
def _GetFingerSpeed(self):
return int(self._SendFingerCmd('1GSP'))
def _SetFingerSpeed(self, speed):
self._SendFingerCmd('1SP%d' % speed)
self._SendFingerCmd('2SP%d' % speed)
def _SetFingerPDParameters(self, finger_number, p, d):
""" Configure the P and D parameters for the PD controller used to servo
the finger positions.
"""
self._SendFingerCmd('%dPP%d' % (finger_number, p))
self._SendFingerCmd('%dPD%d' % (finger_number, d))
def _SetFingerAcceleration(self, finger_number, speed):
self._SendFingerCmd('%dAC%d' % (finger_number, speed))
def _SetFingerDeceleration(self, finger_number, speed):
self._SendFingerCmd('%dDEC%d' % (finger_number, speed))
def _ExtendFingerUntilContact(self, finger_number, speed=30,
contact_current_ma=400):
""" Extend the specified finger(s) slowly until it touches something.
This is done by telling the finger to extend slowly and polling the
amount of current the actuator is drawing. As soon as it meets some
resistance the current will jump, indicating that it has made contact.
"""
if finger_number not in Touchbot.FINGER_NUMBERS + [None]:
print 'ERROR: illegal finger_number (%d)' % finger_number
return None
fingers_to_extend = (set(Touchbot.FINGER_NUMBERS) if finger_number is None
else set([finger_number]))
# First stop any fingertip motion and wait to let the current
# drop for a little while to make sure they aren't going to
# immediately trigger the current spike value. This can occur if
# the servo mechanism in the finger is still oscilating around its
# target position.
for finger in fingers_to_extend:
self._SetFingerVelocity_mm_s(finger, 0)
# Start the finger(s) moving at the specified velocity
for finger in fingers_to_extend:
self._SetFingerVelocity_mm_s(finger, speed)
# Now poll the fingers' positions and current to know when to stop
while len(fingers_to_extend) > 0:
for finger in list(fingers_to_extend):
current = self._GetFingerCurrent_mA(finger)
# If the current spikes, we know we've touched something
if (current >= contact_current_ma):
self._SetFingerVelocity_mm_s(finger, 0)
fingers_to_extend.remove(finger)
return self._GetFingerPos(finger)
def _SetFingerVelocity_mm_s(self, finger_number, speed):
""" Set the specified finger's velocity (as opposed to position) """
self._SendFingerCmd('%dV%d' % (finger_number, speed))
def _GetFingerCurrent_mA(self, finger_number):
""" Get the current for a specified finger in mA """
return int(self._SendFingerCmd('%dGRC' % finger_number))
def _SetFingerZeroPoint(self, finger_number):
""" Set Home (position 0) for the given finger to where it is right now """
self._SendFingerCmd('%dHO' % finger_number)
def _SetFingerContinuousCurrentLimit_mA(self, finger_number, current):
""" Set the maximum continuous current that this finger can draw in mA """
self._SendFingerCmd('%dLCC%d' % (finger_number, current))
def _SetFingerPeakCurrentLimit_mA(self, finger_number, current):
""" Set the maximum Peak current that this finger can draw in mA """
self._SendFingerCmd('%dLCC%d' % (finger_number, current))
def _EnableFinger(self, finger_number):
""" Enable the finger, it can now move when given movement commands """
self._SendFingerCmd('%dEN' % finger_number)
def _GetFingerPos(self, finger_number):
""" Get the current position of the finger """
return int(self._SendFingerCmd('%dPOS' % finger_number))
def _GetFingerTargetPos(self, finger_number):
""" Get the position that this finger is currently trying to go to """
return int(self._SendFingerCmd('%dTPOS' % finger_number))
def _SetFingerResponseLevel(self, response_level):
if response_level not in Touchbot.FINGER_RESPONSE_LEVELS:
print 'ERROR: Unknown finger response level "%s"!' % str(response_level)
return
self.finger_response_level = response_level
for finger in Touchbot.FINGER_NUMBERS:
self._SendFingerCmd('%dANSW%d' % (finger, response_level))
def _SendFingerCmd(self, cmd):
# Messages not beginning in a finger number (1 or 2) are broadcast to
# both fingers, and can cause collisions on the serial bus if they both
# respond at the same time.
if (self.finger_response_level != Touchbot.NO_RESPONSES and
cmd[0] not in ('1', '2')):
print ('WARNING: broadcast commands are dangerous to use when '
'the fingertips are configured to respond!')
return self.comm.SendFingerCmd(cmd)
def _ExecuteFingerMove(self, finger_number, must_sync, blocking):
""" Tell the finger to execute a pending move.
If blocking is set to True then this will poll the finger until the
true location of the finger and the target location are sufficiently
close together.
"""
CORRIDOR = 20
if finger_number in Touchbot.FINGER_NUMBERS:
required_fingers = [finger_number]
self._SendFingerCmd('%dM' % finger_number)
else:
required_fingers = Touchbot.FINGER_NUMBERS
if must_sync:
# Briefly disable responses so we can send a broacast "Move" instruction
# to both fingers simultaneously. If they don't move perfectly in sync
# with eachother, they can't stay aligned with the magnets in the two
# finger probes, which makes them liable to fall off.
# Note: This is slow, it takes the fingers several seconds to do this
self._SetFingerResponseLevel(Touchbot.NO_RESPONSES)
self._SendFingerCmd('M')
self._SetFingerResponseLevel(Touchbot.REQUIRE_RESPONSES)
else:
for finger in required_fingers:
self._SendFingerCmd('%dM' % finger)
if blocking:
targets = {}
for finger in required_fingers:
targets[finger] = self._GetFingerTargetPos(finger)
while True:
completed_fingers = set()
for finger in required_fingers:
if finger not in completed_fingers:
current = self._GetFingerPos(finger)
if abs(targets[finger] - current) <= CORRIDOR:
completed_fingers.add(finger)
if len(completed_fingers) == len(required_fingers):
return
def _MoveFingerAbsolute(self, finger_number, position, blocking=True):
""" Move the finger to an absolute position. """
self._SendFingerCmd('%dLA%d' % (finger_number, position))
self._ExecuteFingerMove(finger_number, False, blocking)
def _MoveFingerRelative(self, finger_number, delta, blocking=True):
""" Move the finger to an relative position. """
self._SendFingerCmd('%dLR%d' % (finger_number, delta))
self._ExecuteFingerMove(finger_number, False, blocking)
def _MoveBothFingersAbsolute(self, position1, position2,
must_sync=False, blocking=True):
""" Move both fingers to absolute positions.
The must_sync parameter allows you to indicate that the fingers must
move totally in sync (as is the case for two-finger fingertips). This
incurs a delay, so it's better not to specify it unless you need to.
For most situations, they do not need to be perfectly synced.
"""
self._SendFingerCmd('1LA%d' % position1)
self._SendFingerCmd('2LA%d' % position2)
self._ExecuteFingerMove(None, must_sync, blocking)
def _MoveBothFingersRelative(self, delta1, delta2,
must_sync=False, blocking=True):
""" Move both fingers to relative positions.
This uses the same arguments as _MoveBothFingersAbsolute, but
uses relative position values instead of absolute.
"""
self._SendFingerCmd('1LR%d' % delta1)
self._SendFingerCmd('2LR%d' % delta2)
self._ExecuteFingerMove(None, must_sync, blocking)
def _DisableFinger(self, finger_number):
self._MoveFingerAbsolute(finger_number, 0.9 * Touchbot.FINGER_HOME_OFFSET)
self._SendFingerCmd('%dDI' % finger_number)
def _CalibratePosition(self):
""" Make the robot go limp and record its position after the operator
moves it into the desired configuration.
Returns a Position object of the recorded posisition.
"""
successful = True
response = self._FreeAxes()
if not response or response[0] != 0:
print 'There was an error entering free mode.'
return None
print 'Move the robot CAREFULLY to where you want it.'
raw_input('Press "enter" to record that position.')
response = self._UnFreeAxes()
if not response or response[0] != 0:
print 'There was an error leaving free mode.'
return None
return self._GetCurrentPosition()
def _SetSpeed(self, speed):
prof = self._GetCurrentProfile()
prof.speed = speed
self._SetProfileData(prof)
def _GetSpeed(self):
prof = self._GetCurrentProfile()
return prof.speed
def _SetInterpolation(self, interpolation_type):
if interpolation_type not in Touchbot.INTERPOLATION_TYPES:
print 'ERROR: Invalid interpolation type (%d).' % interpolation_type
return
prof = self._GetCurrentProfile()
prof.straight = interpolation_type
self._SetProfileData(prof)
def _SetSmoothing(self, smoothing_type):
if smoothing_type not in Touchbot.SMOOTHING_TYPES:
print 'ERROR: Invalid smoothing type (%d).' % smoothing_type
return
prof = self._GetCurrentProfile()
prof.inRange = smoothing_type
self._SetProfileData(prof)
def _FreeAxes(self):
""" Set all axis into free mode, ie: make them go limp. """
return self.comm.SendCmd('freemode', 0)
def _UnFreeAxes(self):
""" Return all axes to computer control. """
return self.comm.SendCmd('freemode', -1)
def _GetCurrentPosition(self):
""" Record the current position as a Position object. """
response = self.comm.SendCmd('where')
if not response:
return None
error_code, data = response
if error_code != 0:
return None
return Position.FromTouchbotResponse(data)
def _GetCurrentProfile(self):
""" Get a copy of the current movement profile. """
error_code, data = self.comm.SendCmd('profile')
return Profile.FromTouchbotResponse(data)
def _SetProfileData(self, profile):
""" Replace the robot's current profile with this one. """
return self.comm.SendCmd('profile', str(profile))
def _SetAngles(self, pos, blocking=True):
""" Move the robot to pos by the raw joint angles.
Generally speaking _SetCartesian() is preferable to this function, so
only use this function if you need to control the axes directly.
"""
response = self.comm.SendCmd('movej', pos.ax_1, pos.ax_2, pos.ax_3,
pos.ax_4, pos.ax_5)
if blocking:
self._Wait()
return response
def _GetRobotName(self):
""" Query the robot for its name. This value is used to determine which
set of trained nest positions to use. """
error, name = self.comm.SendCmd('pd', Touchbot.TOUCHBOT_NAME_PARAM_NUM)
if error:
return ''
# Remove the carrage return at the end, to get a cleaner string
return name.rstrip()
def _SetCartesian(self, pos, finger_distance=None, blocking=True):
""" Move the robot to pos by Cartesian coordinates. """
# If a finger spacing has been specified add that to the movement
# command. The finger spacing motors were added as an additional axis
# so are not controlled by the 'movec' command by default. They must
# be specified before with the 'moveExtraAxis' command, and then are
# moved when the robot receives the next 'movec.'
if finger_distance is not None:
finger_distance = clip(finger_distance,
Touchbot.MIN_FINGER_DISTANCE,
Touchbot.MAX_FINGER_DISTANCE)
self.comm.SendCmd('moveExtraAxis', finger_distance)
response = self.comm.SendCmd('movec', pos.x, pos.y, pos.z,
pos.yaw, pos.pitch, pos.roll)
if blocking:
self._Wait()
return response
def _Wait(self):
return self.comm.SendCmd('waitForEom')
def _AddSafetyClearance(self, pos):
new_pos = copy.deepcopy(pos)
new_pos.ax_1 = Touchbot.SAFETY_CLEARANCE
new_pos.z = Touchbot.SAFETY_CLEARANCE
return new_pos
def PushSpeed(self, new_speed):
self.speed_stack.append(self._GetSpeed())
self._SetSpeed(new_speed)
def PopSpeed(self):
if len(self.speed_stack) >= 1:
self._SetSpeed(self.speed_stack.pop())
def _GetFingertip(self, fingertip):
""" Retrieve the specified fingertip from the nest. """
self._MoveBothFingersAbsolute(0, 0)
self._SetCartesian(fingertip.above_pos,
finger_distance=fingertip.above_pos.ax_5)
self.PushSpeed(Touchbot.SPEED_MEDIUM)
self._MoveBothFingersAbsolute(fingertip.extension1, fingertip.extension2)
self._SetCartesian(fingertip.slide_pos,
finger_distance=fingertip.slide_pos.ax_5)
self.PopSpeed()
self._PushFingerSpeed_mm_per_s(10 if fingertip.IsLarge() else 20)
is_dual_fingertip = (fingertip.finger_number is None)
self._MoveBothFingersAbsolute(0, 0, must_sync=is_dual_fingertip)
self._PopFingerSpeed()
self.attached_fingertips.add(fingertip)
def _DropFingertip(self, fingertip):
""" Drop off the specified fingertip at its place in the nest. """
is_dual_fingertip = (fingertip.finger_number is None)
self._PushFingerSpeed_mm_per_s(10 if fingertip.IsLarge() else 50)
self._MoveBothFingersAbsolute(0, 0, must_sync=is_dual_fingertip)
self._SetCartesian(self._AddSafetyClearance(fingertip.slide_pos),
finger_distance=fingertip.slide_pos.ax_5)
self.PushSpeed(Touchbot.SPEED_MEDIUM)
self._SetCartesian(fingertip.slide_pos,
finger_distance=fingertip.slide_pos.ax_5)
self._MoveBothFingersAbsolute(fingertip.extension1, fingertip.extension2,
must_sync=is_dual_fingertip)
self._SetCartesian(fingertip.above_pos,
finger_distance=fingertip.above_pos.ax_5)
self.PopSpeed()
self._PopFingerSpeed()
self._MoveBothFingersAbsolute(0, 0)
self.attached_fingertips.remove(fingertip)
def _MoveToNest(self):
""" Safely move the robot from over the DUT to over the nest """
# Find a position just over the current spot, to avoid hitting anything
curr_pos = self._GetCurrentPosition()
above_curr_pos = self._AddSafetyClearance(curr_pos)
# Find a spot over the nest
nest_pos = None
for fingertip in self.fingertips.values():
if nest_pos is None:
nest_pos = copy.deepcopy(fingertip.above_pos)
else:
nest_pos.x += fingertip.above_pos.x
nest_pos.y += fingertip.above_pos.y
nest_pos.x /= len(self.fingertips)
nest_pos.y /= len(self.fingertips)
above_nest = self._AddSafetyClearance(nest_pos)
# Compute an intermediate step to prevent the hand colliding with the
# body of the robot.
intermediate_pos = copy.deepcopy(above_nest)
intermediate_pos.x = above_curr_pos.x
# Actually execute the moves
self._SetCartesian(above_curr_pos, blocking=True)
self._SetCartesian(intermediate_pos, blocking=True)
self._SetCartesian(above_nest, blocking=True)
def _CenterOverDevice(self, device_spec):
""" Safely move the robot from over the nest to over the DUT """
# Find a position just over the current spot, to avoid hitting anything
curr_pos = self._GetCurrentPosition()
above_curr_pos = self._AddSafetyClearance(curr_pos)
# Move over the center of the DUT
center = device_spec.RelativePosToAbsolutePos((0.5, 0.5))
above_dut = self._AddSafetyClearance(center)
# Compute an intermediate step to prevent collisions
intermediate_pos = copy.deepcopy(above_curr_pos)
intermediate_pos.x = above_dut.x
# Finally, execute the moves
self.PushSpeed(Touchbot.SPEED_FAST)
self._SetCartesian(above_curr_pos, blocking=True)
self._SetCartesian(intermediate_pos, blocking=True)
self._SetCartesian(above_dut, blocking=True)
self.PopSpeed()
def _RequireFingertips(self, device_spec, required_fingertips,
should_end_over_dut=True):
""" Swap out fingertips as needed to have the specified ones equipped.
If they are already attached, this will do nothing, but otherwise will
put away any incorrect fingertips that are currenlty attached and will
attach the requested tips from the nest.
"""
self.PushSpeed(Touchbot.SPEED_MEDIUM)
# This function starts assuming the robot is over the dut and ready to
# perform gestures
is_over_dut = True
# Remove any unneeded fingertips
if not self.attached_fingertips.issubset(set(required_fingertips)):
self._MoveToNest()
is_over_dut = False
for fingertip in list(self.attached_fingertips):
if fingertip not in required_fingertips:
self._DropFingertip(fingertip)
# Attach the required ones that are not already attached
if not set(required_fingertips).issubset(self.attached_fingertips):
if is_over_dut:
self._MoveToNest()
is_over_dut = False
for fingertip in required_fingertips:
if fingertip not in self.attached_fingertips:
self._GetFingertip(fingertip)
# Finally move the robot back over the DUT if we had to move it to
# the nest to do some fingertip swaps
if should_end_over_dut and not is_over_dut:
self._CenterOverDevice(device_spec)
self._CenterWrist()
self.PopSpeed()
def _CenterForFingertip(self, fingertip, pos):
""" Offset the coordinates so that is centered at the point given,
not the point directly between the two fingers.
"""
# Two-finger tips are already centered
if fingertip.finger_number not in Touchbot.FINGER_NUMBERS:
return pos
# Shift the x and y accordingly for both points in the line
angle_deg = pos.yaw + (-90 if fingertip.finger_number == 1 else 90)
angle_rad = math.radians(angle_deg)
pos.x += math.cos(angle_rad) * (pos.ax_5 + Touchbot.MIN_FINGER_SPACING) / 2.0
pos.y += math.sin(angle_rad) * (pos.ax_5 + Touchbot.MIN_FINGER_SPACING) / 2.0
return pos
def _IntermediatePoints(self, start, end, steps=15):
""" Compute a number of intermediate points between the given
coordinates.
This function returns a list of coordinate tuples, linearly interpolating
between start and end.
"""
x1, y1 = start
x2, y2 = end
return [(x1 * (1.0 - alpha) + x2 * alpha, y1 * (1.0 - alpha) + y2 * alpha)
for alpha in np.linspace(0.0, 1.0, num=steps)]
def _SuggestSpacing(self, fingertips):
# Guess how far apart to space the fingers for a gesture using these tips
for fingertip in fingertips:
if fingertip.finger_number is None:
return fingertip.above_pos.ax_5
return Touchbot.DEFAULT_FINGER_SPACING
################################################################################
# Actual gestures after this point #
################################################################################
def CalibrateDevice(self, filename=None):
fingertips = [self.fingertips[Touchbot.CALIBRATION_FINGERTIP1],
self.fingertips[Touchbot.CALIBRATION_FINGERTIP2]]
self._RequireFingertips(None, fingertips, should_end_over_dut=False)
self.PushSpeed(Touchbot.SPEED_FAST)
# First, lift the arm up and clear of any obstructions with the fingers as
# close together as they can to allow for easy centering
pos = self._GetCurrentPosition()
pos = self._AddSafetyClearance(pos)
self._SetCartesian(pos, finger_distance=4, blocking=True)
self._MoveBothFingersAbsolute(Touchbot.CALIBRATION_EXTENSION,
Touchbot.CALIBRATION_EXTENSION)
# Prompt the user to calibrate each corner of the device
CORNER_NAMES = ['top right', 'top left', 'bottom left', 'bottom right']
corners = []
for corner in CORNER_NAMES:
print 'Position the fingers in the %s corner of the device' % corner
print 'and keep the hand as aligned with the touchpad as possible'
corners.append(self._CalibratePosition())
self._MoveBothFingersAbsolute(0, 0)
pos = self._GetCurrentPosition()
pos = self._AddSafetyClearance(pos)
self._SetCartesian(pos)
self.PopSpeed()
# Build a DeviceSpec and save to disk if a filename was specified
device_spec = DeviceSpec(*corners)
if filename:
device_spec.SaveToDisk(filename)
return device_spec
def Tap(self, device_spec, fingertips, tap_location,
touch_time_s=None, angle=0, vertical_offset=0, num_taps=1):
""" This tells the robot to perform a tap gesture at the specified
location on the device.
If a touch_time_s is specified (in seconds) the robot will leave
its finger in contact with the touch sensor for that long, otherwise it
will simply perform a quick tap.
"""
self._RequireFingertips(device_spec, fingertips)
is_dual_fingertip = False
abs_pos = device_spec.RelativePosToAbsolutePos(tap_location, angle=angle)
abs_pos.ax_5 = self._SuggestSpacing(fingertips)
if len(fingertips) == 1:
abs_pos = self._CenterForFingertip(fingertips[0], abs_pos)
is_dual_fingertip = (fingertips[0].finger_number is None)
# Get into position
self._MoveBothFingersAbsolute(-vertical_offset, -vertical_offset,
must_sync=is_dual_fingertip)
self._SetCartesian(abs_pos, finger_distance=abs_pos.ax_5)
# Precomputing tap parameters based on the fingertip
finger_number = None
if len(fingertips) == 1:
finger_number = fingertips[0].finger_number
speed = 5000 if not fingertips[0].IsLarge() else 10
self._PushFingerSpeed_mm_per_s(speed)
for tap in range(num_taps):
# Touch the pad
self._ExtendFingerUntilContact(finger_number, speed=speed)
if touch_time_s:
time.sleep(touch_time_s)
# Then lift up
if finger_number is not None:
self._MoveFingerRelative(finger_number, -Touchbot.TAP_LIFT_DISTANCE)
else:
self._MoveBothFingersRelative(-Touchbot.TAP_LIFT_DISTANCE,
-Touchbot.TAP_LIFT_DISTANCE,
must_sync=is_dual_fingertip)
self._MoveBothFingersAbsolute(-vertical_offset, -vertical_offset,
must_sync=is_dual_fingertip)
self._PopFingerSpeed()
def Line(self, device_spec, fingertips, start_location, end_location,
fingertip_angle=0, fingertip_spacing=None, pause_s=0, swipe=False):
""" Draw a line on the pad
This gesture will:
1. Center the specified fingertip(s) over the start location
2. Touch down
3. Optionally pause for the specified number of seconds
4. Move to the end location
5. Lift up
"""
self._RequireFingertips(device_spec, fingertips)
# Compute how far apart the fingers should be spread
if fingertip_spacing is None:
fingertip_spacing = self._SuggestSpacing(fingertips)
# Note which fingers are in play
fingers_to_extend = fingertips[0].finger_number
if len(fingertips) > 1:
fingers_to_extend = None
# Raise up both fingers
self._MoveBothFingersAbsolute(0, 0)
# Move into position
self.PushSpeed(Touchbot.SPEED_MEDIUM)
start_abs = device_spec.RelativePosToAbsolutePos(start_location)
start_abs.yaw += fingertip_angle
start_abs.ax_5 = fingertip_spacing
if len(fingertips) == 1:
start_abs = self._CenterForFingertip(fingertips[0], start_abs)
self._SetCartesian(start_abs, finger_distance=start_abs.ax_5)
self.PopSpeed()
# If this is NOT a swipe, extend the fingers now (before anything moves)
if not swipe:
self._ExtendFingerUntilContact(fingers_to_extend, speed=10)
time.sleep(pause_s)
# Move to the end position
end_abs = device_spec.RelativePosToAbsolutePos(end_location)
end_abs.yaw += fingertip_angle
end_abs.ax_5 = fingertip_spacing
if len(fingertips) == 1:
end_abs = self._CenterForFingertip(fingertips[0], end_abs)
self._SetCartesian(end_abs, finger_distance=end_abs.ax_5,
blocking=(not swipe))
# If this IS a swipe, extend the fingers now (after they're already moving)
if swipe:
self._ExtendFingerUntilContact(fingers_to_extend, speed=200)
# Then lift up
self._MoveBothFingersAbsolute(0, 0)
# Make sure nothing exits until all the moves have completed, just in case
self._Wait()
def LineWithStationaryFinger(self, device_spec, stationary_fingertip,
moving_fingertip, start_location, end_location,
stationary_location):
self._RequireFingertips(device_spec,
[stationary_fingertip, moving_fingertip])
stationary_abs = device_spec.RelativePosToAbsolutePos(stationary_location,
max_diagonal_distance=Touchbot.MAX_FINGER_DISTANCE)
sx, sy = stationary_abs.x, stationary_abs.y
positions = []
for moving_relative in self._IntermediatePoints(start_location,
end_location):
# Convert into the absolute space, using the device spec
moving_abs = device_spec.RelativePosToAbsolutePos(moving_relative,
max_diagonal_distance=Touchbot.MAX_FINGER_DISTANCE)
mx, my = moving_abs.x, moving_abs.y
# Find the center point between the two fingers
cx = (mx + sx) / 2.0
cy = (my + sy) / 2.0
# Compute the angle between the two fingers
dx = mx - sx
dy = my - sy
angle = math.degrees(math.atan2(dy, dx)) + 90
# Compute the distance between the fingers
d = (dx ** 2 + dy ** 2) ** 0.5
d -= Touchbot.MIN_CENTER_TO_CENTER_DISTANCE_MM
# Issue the movement command. Start with a known good location on the
# sensor and then modify the coordinates to fit the gesture.
pos = moving_abs
pos.x = cx
pos.y = cy
pos.yaw = angle
pos.ax_5 = d
positions.append(pos)
# Move the robot into position
self._SetCartesian(positions[0], finger_distance=positions[0].ax_5,
blocking=True)
self._ExtendFingerUntilContact(stationary_fingertip.finger_number)
self._ExtendFingerUntilContact(moving_fingertip.finger_number)
# Perform the gesture
self._SetSmoothing(Touchbot.BLEND_MOVEMENTS)
for pos in positions:
self._SetCartesian(pos, finger_distance=pos.ax_5, blocking=False)
self._SetSmoothing(Touchbot.PRECISE_MOVEMENTS)
self._Wait()
# Lift off the pad
self._MoveBothFingersAbsolute(0, 0)
def Pinch(self, device_spec, fingertips, center, angle, start_distance,
end_distance):
self._RequireFingertips(device_spec, fingertips)
pos = device_spec.RelativePosToAbsolutePos(center, angle=angle)
self._SetCartesian(pos, finger_distance=start_distance, blocking=True)
self._ExtendFingerUntilContact(None)
self._SetCartesian(pos, finger_distance=end_distance, blocking=True)
self._MoveBothFingersAbsolute(0, 0)
def Click(self, device_spec, fingertips, tap_location):
""" This tells the robot to perform a PHYSICAL click gesture at the
specified location on the device. This means it will press down
hard, to make sure that when the touchpad is physically clicked down
the dome switch reports a click.
"""
self._RequireFingertips(device_spec, fingertips)
finger_number = None
abs_pos = device_spec.RelativePosToAbsolutePos(tap_location)
abs_pos.ax_5 = self._SuggestSpacing(fingertips)
if len(fingertips) == 1:
finger_number = fingertips[0].finger_number
abs_pos = self._CenterForFingertip(fingertips[0], abs_pos)
# Get into position
self._MoveBothFingersAbsolute(0, 0)
self._SetCartesian(abs_pos, finger_distance=abs_pos.ax_5)
# Exert some force on the pad to cause a click
self._ExtendFingerUntilContact(finger_number,
speed=Touchbot.PHYSICAL_CLICK_SPEED_MM_S,
contact_current_ma=Touchbot.CLICK_CURRENT_MA)
# Lift up
self._MoveBothFingersAbsolute(0, 0)
def Drumroll(self, device_spec, fingertips, location, num_taps=20):
""" This gesture performs a 'drumroll' at the specified location by tapping
with two fingertips alternating quickly.
"""
self._RequireFingertips(device_spec, fingertips)
if len(fingertips) != 2:
print 'WARNING, a drumroll should always use TWO fingertips'
# Get into position
abs_pos = device_spec.RelativePosToAbsolutePos(location)
abs_pos.ax_5 = Touchbot.DRUMROLL_SPACING
self._MoveBothFingersAbsolute(0, 0)
self._SetCartesian(abs_pos, finger_distance=abs_pos.ax_5)
# Increasing the accelleration and speed temporarily on the fingertips
for finger in Touchbot.FINGER_NUMBERS:
self._SetFingerAcceleration(finger, 3 * Touchbot.FINGER_ACCEL_MM_S_S)
self._SetFingerDeceleration(finger, 3 * Touchbot.FINGER_DECEL_MM_S_S)
self._PushFingerSpeed_mm_per_s(2000)
# Actually perform the taps
# Start with finger 1 down, and then alternate
distance_to_pad = self._ExtendFingerUntilContact(1)
for tap in range(num_taps):
self._MoveFingerAbsolute(2, distance_to_pad, blocking=False)
self._MoveFingerRelative(1, -Touchbot.TAP_LIFT_DISTANCE, blocking=True)
self._MoveFingerAbsolute(1, distance_to_pad, blocking=False)
self._MoveFingerRelative(2, -Touchbot.TAP_LIFT_DISTANCE, blocking=True)
# Slowing everything back down to normal before continuing
for finger in Touchbot.FINGER_NUMBERS:
self._SetFingerAcceleration(finger, Touchbot.FINGER_ACCEL_MM_S_S)
self._SetFingerDeceleration(finger, Touchbot.FINGER_DECEL_MM_S_S)
self._PopFingerSpeed()
# Lift up both fingers before finishing, to end in a known state
self._MoveBothFingersAbsolute(0, 0)
def Quickstep(self, device_spec, fingertips, num_passes, speed):
""" Move a finger back and forth vertically on the touch device as it
slowly works it's way down, covering the whole area in vertical lines at
a constant speed. This is used by the Quickstep latency measurement
device, which needs a finger to cross a laser beam repeatedly at a fixed
speed to measure the drag latency on a touch sensor.
"""
# Prepare for the gesture, by getting the required fingertips
self._RequireFingertips(device_spec, fingertips)
# First, move the robot into position over the top left corner
init_abs_pos = device_spec.RelativePosToAbsolutePos(
(self.QUICKSTEP_BUFFER, self.QUICKSTEP_BUFFER))
init_abs_pos.ax_5 = self._SuggestSpacing(fingertips)
init_abs_pos = self._CenterForFingertip(fingertips[0], init_abs_pos)
self.PushSpeed(self.SPEED_SLOW)
self._MoveBothFingersAbsolute(0, 0)
self._SetCartesian(init_abs_pos, finger_distance=init_abs_pos.ax_5)
self.PopSpeed()
self.PushSpeed(speed)
self._ExtendFingerUntilContact(1)
# Then draw a series of vertical lines evenly spaced over the sensor
for i in xrange(num_passes):
# Draw a down stroke
downstroke_x = ((float(i) / float(num_passes)) *
(1.0 - self.QUICKSTEP_BUFFER * 2) + self.QUICKSTEP_BUFFER)
downstroke_start = (downstroke_x, self.QUICKSTEP_BUFFER)
downstroke_start_abs_pos = device_spec.RelativePosToAbsolutePos(
downstroke_start)
downstroke_start_abs_pos.ax_5 = self._SuggestSpacing(fingertips)
downstroke_start_abs_pos = self._CenterForFingertip(
fingertips[0],
downstroke_start_abs_pos)
self._SetCartesian(downstroke_start_abs_pos,
finger_distance=downstroke_start_abs_pos.ax_5)
downstroke_end = (downstroke_x, 1.0 - self.QUICKSTEP_BUFFER)
downstroke_end_abs_pos = device_spec.RelativePosToAbsolutePos(
downstroke_end)
downstroke_end_abs_pos.ax_5 = self._SuggestSpacing(fingertips)
downstroke_end_abs_pos = self._CenterForFingertip(
fingertips[0],
downstroke_end_abs_pos)
self._SetCartesian(downstroke_end_abs_pos,
finger_distance=downstroke_end_abs_pos.ax_5)
# Draw an upstroke
upstroke_x = ((float(i + 0.5) / float(num_passes)) *
(1.0 - self.QUICKSTEP_BUFFER * 2) + self.QUICKSTEP_BUFFER)
upstroke_start = (upstroke_x, 1.0 - self.QUICKSTEP_BUFFER)
upstroke_start_abs_pos = device_spec.RelativePosToAbsolutePos(
upstroke_start)
upstroke_start_abs_pos.ax_5 = self._SuggestSpacing(fingertips)
upstroke_start_abs_pos = self._CenterForFingertip(
fingertips[0],
upstroke_start_abs_pos)
self._SetCartesian(upstroke_start_abs_pos,
finger_distance=upstroke_start_abs_pos.ax_5)
upstroke_end = (upstroke_x, self.QUICKSTEP_BUFFER)
upstroke_end_abs_pos = device_spec.RelativePosToAbsolutePos(upstroke_end)
upstroke_end_abs_pos.ax_5 = self._SuggestSpacing(fingertips)
upstroke_end_abs_pos = self._CenterForFingertip(fingertips[0],
upstroke_end_abs_pos)
self._SetCartesian(upstroke_end_abs_pos,
finger_distance=upstroke_end_abs_pos.ax_5)
# Lift the fingers both up and return the robot to its original speed
self._MoveBothFingersAbsolute(0, 0)
self.PopSpeed()
def Draw45DegreeLineSegment(self, device_spec, fingertips, location):
""" Draw a short line segment 45 degrees off from the x axis of
the touch device. This gesture is slightly different than the others,
in that it uses real measurements instead of reletive to the dimensions
of the sensor. This is to allow us to test and see if the resolution
of the touch device is configured squarely. ie: if the robot draws
what we know is a 45 degree angle, and the touchpad reports it as
45, then we know the resolution is configured correctly, otherwise
motion is being compressed on one of the axes.
"""
LINE_LENGTH_MM = 10
# Prepare for the gesture, by getting the required fingertips
self._RequireFingertips(device_spec, fingertips)
# Compute the positions of the corners of the square
center = device_spec.RelativePosToAbsolutePos(location)
center.ax_5 = self._SuggestSpacing(fingertips)
center = self._CenterForFingertip(fingertips[0], center)
start = copy.copy(center)
start.x -= LINE_LENGTH_MM // 2
start.y -= LINE_LENGTH_MM // 2
end = copy.copy(center)
end.x += LINE_LENGTH_MM // 2
end.y += LINE_LENGTH_MM // 2
# Get the robot into position, touching the starting position
self.PushSpeed(self.SPEED_MEDIUM)
self._SetCartesian(start, finger_distance=start.ax_5)
self._ExtendFingerUntilContact(1)
self.PopSpeed()
# Have the robot trace slowly between the computer points
self.PushSpeed(self.SPEED_SLOW)
self._SetCartesian(end, finger_distance=end.ax_5)
self.PopSpeed()
self._MoveBothFingersAbsolute(0, 0)