Repository: opentrons/opentrons
Issue: P10M Crashing During Tip Probe

P10M is currently crashing into trash during tip probe. 

## Expected Behavior
- p10m complete tip probe without crashing

## Observed Behavior
- p10m crashing into trash box during tip probe

## Steps to Recreate
- @Laura-Danielle / @btmorr will leave this to you.

Changed files:
api/opentrons/server/main.py, api/opentrons/util/calibration_functions.py, api/tests/opentrons/api/test_calibration.py

diff --git a/api/opentrons/server/main.py b/api/opentrons/server/main.py
index 0dcc2932a6..2a988b8712 100755
--- a/api/opentrons/server/main.py
+++ b/api/opentrons/server/main.py
@@ -74,6 +74,7 @@ def init(loop=None):
     routes for methods defined in opentrons.server.endpoints
     """
     server = Server(MainRouter(), loop=loop)
+
     server.app.router.add_get(
         '/health', endp.health)
     server.app.router.add_get(
@@ -108,6 +109,7 @@ def init(loop=None):
         '/robot/positions', control.position_info)
     server.app.router.add_post(
         '/robot/move', control.move)
+
     return server.app
 
 
diff --git a/api/opentrons/util/calibration_functions.py b/api/opentrons/util/calibration_functions.py
index f1e6504a79..6ec07dbf35 100644
--- a/api/opentrons/util/calibration_functions.py
+++ b/api/opentrons/util/calibration_functions.py
@@ -7,6 +7,9 @@ from opentrons.robot import robot_configs
 from opentrons.data_storage import database
 from opentrons.trackers.pose_tracker import absolute
 
+import logging
+
+log = logging.getLogger(__name__)
 
 # The X and Y switch offsets are to position relative to the *opposite* axes
 # during calibration, to make the tip hit the raised end of the switch plate,
@@ -18,7 +21,7 @@ X_SWITCH_OFFSET_MM = 2.0
 Y_SWITCH_OFFSET_MM = 5.0
 Z_SWITCH_OFFSET_MM = 5.0
 
-Z_DECK_CLEARANCE = 15.0
+Z_DECK_CLEARANCE = 5.0
 Z_PROBE_CLEARANCE = 5.0
 Z_PROBE_START_CLEARANCE = 20
 
@@ -58,36 +61,36 @@ def probe_instrument(instrument, robot, tip_length=None) -> Point:
         tip_length = robot.config.tip_length[instrument.name]
     instrument._add_tip(tip_length)
 
-    # probe_dimensions is the external bounding box of the probe unit
-    size_x, size_y, size_z = robot.config.probe_dimensions
     # probe_center is the point at the center of the switch pcb
     center = Point(*robot.config.probe_center)
 
-    rel_x_start = (size_x / 2) + SWITCH_CLEARANCE
-    rel_y_start = (size_y / 2) + SWITCH_CLEARANCE
-
-    # Ensure that the nozzle will clear the probe unit and tip will clear deck
-    nozzle_safe_z = (size_z - tip_length) + Z_PROBE_CLEARANCE
-    rel_z_start = center.z - max(Z_DECK_CLEARANCE, nozzle_safe_z)
-
-    # Each list item defines axis we are probing for, starting position vector
-    # relative to probe top center and travel distance
-    hot_spots = [
-        ('x',       -rel_x_start, X_SWITCH_OFFSET_MM,            -rel_z_start,  size_x),  # NOQA
-        ('x',        rel_x_start, X_SWITCH_OFFSET_MM,            -rel_z_start, -size_x),  # NOQA
-        ('y', Y_SWITCH_OFFSET_MM,       -rel_y_start,            -rel_z_start,  size_y),  # NOQA
-        ('y', Y_SWITCH_OFFSET_MM,        rel_y_start,            -rel_z_start, -size_y),  # NOQA
-        ('z',                0.0, Z_SWITCH_OFFSET_MM, Z_PROBE_START_CLEARANCE, -size_z)   # NOQA
-    ]
+    hot_spots = _calculate_hotspots(
+        robot,
+        tip_length,
+        SWITCH_CLEARANCE,
+        X_SWITCH_OFFSET_MM,
+        Y_SWITCH_OFFSET_MM,
+        Z_SWITCH_OFFSET_MM,
+        Z_DECK_CLEARANCE,
+        Z_PROBE_CLEARANCE,
+        Z_PROBE_START_CLEARANCE)
 
-    acc = []
+    # The saved axis positions from limit switch response
+    axis_pos = []
 
-    safe_height = center.z + Z_CROSSOVER_CLEARANCE
+    safe_height = _calculate_safeheight(robot, Z_CROSSOVER_CLEARANCE)
 
     robot.poses = instrument._move(robot.poses, z=safe_height)
 
-    for axis, *probing_vector, distance in hot_spots:
-        x, y, z = array(probing_vector) + center
+    for axis, x, y, z, distance in hot_spots:
+        if axis == 'z':
+
+            x = x + center.x
+            y = y + center.y
+            z = z + center.z
+        else:
+            x = x + center.x
+            y = y + center.y
 
         robot.poses = instrument._move(robot.poses, x=x, y=y)
         robot.poses = instrument._move(robot.poses, z=z)
@@ -98,17 +101,21 @@ def probe_instrument(instrument, robot, tip_length=None) -> Point:
         # Tip position is stored in accumulator and averaged for each axis
         # to be used for more accurate positioning for the next axis
         value = absolute(robot.poses, instrument)[axis_index]
-        acc.append(value)
+        axis_pos.append(value)
 
         # after probing two points along the same axis
         # average them out, update center and clear accumulator
         # except Z, we're only probing that once
         if axis == 'z':
-            center = center._replace(**{axis: acc[0]})
-            acc.clear()
-        elif len(acc) == 2:
-            center = center._replace(**{axis: (acc[0] + acc[1]) / 2.0})
-            acc.clear()
+            center = center._replace(**{axis: axis_pos[0]})
+            axis_pos.clear()
+        elif len(axis_pos) == 2:
+            center = center._replace(**{axis:
+                                        (axis_pos[0] + axis_pos[1]) / 2.0})
+
+            axis_pos.clear()
+
+        log.debug("Current axis positions for {}: {}".format(axis, axis_pos))
 
         # Bounce back to release end stop
         bounce = value + (BOUNCE_DISTANCE_MM * (-distance / abs(distance)))
@@ -116,11 +123,78 @@ def probe_instrument(instrument, robot, tip_length=None) -> Point:
         robot.poses = instrument._move(robot.poses, **{axis: bounce})
         robot.poses = instrument._move(robot.poses, z=safe_height)
 
+        log.debug("Updated center point tip probe {}".format(center))
+
     instrument._remove_tip(tip_length)
 
     return center
 
 
+def _calculate_hotspots(
+        robot,
+        tip_length,
+        switch_clearance,
+        x_switch_offset,
+        y_switch_offset,
+        z_switch_offset,
+        deck_clearance,
+        z_probe_clearance,
+        z_start_clearance):
+
+    # probe_dimensions is the external bounding box of the probe unit
+    size_x, size_y, size_z = robot.config.probe_dimensions
+
+    rel_x_start = (size_x / 2) + switch_clearance
+    rel_y_start = (size_y / 2) + switch_clearance
+
+    # Ensure that the nozzle will clear the probe unit and tip will clear deck
+    nozzle_safe_z = round((size_z - tip_length) + z_probe_clearance, 3)
+
+    z_start = max(deck_clearance, nozzle_safe_z)
+
+    # Each list item defines axis we are probing for, starting position vector
+    # relative to probe top center and travel distance
+    neg_x = ('x',
+             -rel_x_start,
+             x_switch_offset,
+             z_start,
+             size_x)
+    pos_x = ('x',
+             rel_x_start,
+             x_switch_offset,
+             z_start,
+             -size_x)
+    neg_y = ('y',
+             y_switch_offset,
+             -rel_y_start,
+             z_start,
+             size_y)
+    pos_y = ('y',
+             y_switch_offset,
+             rel_y_start,
+             z_start,
+             -size_y)
+    z = ('z',
+         0.0,
+         z_switch_offset,
+         z_start_clearance,
+         -size_z)
+
+    return [
+        neg_x,
+        pos_x,
+        neg_y,
+        pos_y,
+        z
+    ]
+
+
+def _calculate_safeheight(robot, z_crossover_clearance):
+    center = Point(*robot.config.probe_center)
+
+    return center.z + z_crossover_clearance
+
+
 def update_instrument_config(instrument, measured_center) -> (Point, float):
     """
     Update config and pose tree with instrument's x and y offsets
@@ -136,6 +210,7 @@ def update_instrument_config(instrument, measured_center) -> (Point, float):
 
     dx, dy, dz = array(measured_center) - config.probe_center
 
+    log.debug("This is measured probe center dx {}".format(Point(dx, dy, dz)))
     # any Z offset will adjust the tip length, so instruments have Z=0 offset
     old_x, old_y, _ = instrument_offset[instrument.mount][instrument.type]
     instrument_offset[instrument.mount][instrument.type] = \\
diff --git a/api/tests/opentrons/api/test_calibration.py b/api/tests/opentrons/api/test_calibration.py
index 1e4ac5332d..a9d06437cc 100755
--- a/api/tests/opentrons/api/test_calibration.py
+++ b/api/tests/opentrons/api/test_calibration.py
@@ -1,8 +1,11 @@
 from unittest import mock
 from functools import partial
 from tests.opentrons.conftest import state
+
 import pytest
 
+from opentrons.util import calibration_functions
+
 state = partial(state, 'calibration')
 
 
@@ -37,6 +40,67 @@ async def test_tip_probe(main_router, model):
     await main_router.wait_until(state('ready'))
 
 
+async def test_correct_hotspots(main_router, model):
+
+    robot = model.robot
+
+    tip_length = robot.config.tip_length[model.instrument._instrument.name]
+    switch_clearance = 7.5
+    x_switch_offset = 5.0
+    y_switch_offset = 2.0
+    z_switch_offset = 5.0
+    deck_clearance = 5.0
+    z_probe_clearance = 5.0
+    z_start_clearance = 20.0
+
+    size_x, size_y, size_z = robot.config.probe_dimensions
+
+    rel_x_start = (size_x / 2) + switch_clearance
+    rel_y_start = (size_y / 2) + switch_clearance
+
+    nozzle_safe_z = round((size_z - tip_length) + z_probe_clearance, 3)
+    z_start = max(deck_clearance, nozzle_safe_z)
+    expected = [('x',
+                 -rel_x_start,
+                 x_switch_offset,
+                 z_start,
+                 size_x),
+                ('x',
+                 rel_x_start,
+                 x_switch_offset,
+                 z_start,
+                 -size_x),
+                ('y',
+                 y_switch_offset,
+                 -rel_y_start,
+                 z_start,
+                 size_y),
+                ('y',
+                y_switch_offset,
+                rel_y_start,
+                z_start,
+                -size_y),
+                ('z',
+                 0.0,
+                 z_switch_offset,
+                 z_start_clearance,
+                 -size_z)
+                ]
+
+    actual = calibration_functions._calculate_hotspots(
+        robot,
+        tip_length,
+        switch_clearance,
+        x_switch_offset,
+        y_switch_offset,
+        z_switch_offset,
+        deck_clearance,
+        z_probe_clearance,
+        z_start_clearance)
+
+    assert expected == actual
+
+
 async def test_move_to_front(main_router, model):
     robot = model.robot