Repository: opentrons/opentrons
Issue: Plunger Hard Limit Blocks Z-Movement

As a user, I would like to be able to continue robot use even if it is accidentally stopped while the plunger is at its limit switch.

## Expected Behavior
-- If a robot process is terminated while the pipette plunger is in contact with the hard stop (axis limit), user has a way to resume movement.

## Observed Behavior
-- If robot process is terminated with pipette plunger hitting hard stop, user cannot resume robot use (user has to disassemble the pipette and manually back off the plunger)

## Steps to recreate

- In a robot terminal:
```
from opentrons import robot
robot.connect()
robot.home()
robot._driver.move({'C': 100})  # This will cause a limit-switch error and not back off
robot._driver.move({'A': 100})  # This should work, but causes a limit-switch error instead
```

## Implementation Notes
- We believe this happens b/c the robot gets stopped unexpectedly with the plunger at it limit switch
- Robot will not attempt to move plunger in either direction while the limit switch is depressed (even though we only have one switch, at one end of the limit -- indicating that we have plenty of room in the other direction)
- Knowing that we're at one end of the limit, we should be able to home that axis before raising an error, which will put the robot in a recovered state so the user can inspect what caused the limit switch error and restart



Changed files:
api/opentrons/drivers/smoothie_drivers/driver_3_0.py, api/opentrons/robot/robot.py, api/tests/opentrons/drivers/test_driver.py

diff --git a/api/opentrons/drivers/smoothie_drivers/driver_3_0.py b/api/opentrons/drivers/smoothie_drivers/driver_3_0.py
index 9a350b44b2..f425e92b8a 100755
--- a/api/opentrons/drivers/smoothie_drivers/driver_3_0.py
+++ b/api/opentrons/drivers/smoothie_drivers/driver_3_0.py
@@ -429,6 +429,15 @@ class SmoothieDriver_3_0_0:
         (the axis for plunger control) do current ramp-up and ramp-down, so
         that plunger motors rest at a low current to prevent burn-out.
 
+        In the case of a limit-switch alarm during any command other than home,
+        the robot should home the axis from the alarm and then raise a
+        SmoothieError. The robot should *not* recover and continue to run the
+        protocol, as this could result in unpredicable handling of liquids.
+        When a SmoothieError is raised, the user should inspect the physical
+        configuration of the robot and the protocol and determine why the limit
+        switch was hit unexpectedly. This is usually due to an undetected
+        collision in a previous move command.
+
         :param command: the GCODE to submit to the robot
         :param timeout: the time to wait before returning (indefinite wait if
             this is set to none
@@ -445,6 +454,9 @@ class SmoothieDriver_3_0_0:
             if (ERROR_KEYWORD in ret_code.lower()) or \\
                     (ALARM_KEYWORD in ret_code.lower()):
                 self._reset_from_error()
+                error_axis = ret_code.strip()[-1]
+                if GCODES['HOME'] not in command and error_axis in 'XYZABC':
+                    self.home(error_axis)
                 raise SmoothieError(ret_code)
 
             return ret_code
diff --git a/api/opentrons/robot/robot.py b/api/opentrons/robot/robot.py
index e77676bda3..950b02ffff 100755
--- a/api/opentrons/robot/robot.py
+++ b/api/opentrons/robot/robot.py
@@ -578,10 +578,6 @@ class Robot(object):
     def move_head(self, *args, **kwargs):
         self.poses = self.gantry.move(self.poses, **kwargs)
 
-    # DEPRECATED
-    def move_plunger(self, *args, **kwargs):
-        self._driver.move_plunger(*args, **kwargs)
-
     def head_speed(
             self, combined_speed=None,
             x=None, y=None, z=None, a=None, b=None, c=None):
diff --git a/api/tests/opentrons/drivers/test_driver.py b/api/tests/opentrons/drivers/test_driver.py
index 506a3e988a..689bcdcc78 100755
--- a/api/tests/opentrons/drivers/test_driver.py
+++ b/api/tests/opentrons/drivers/test_driver.py
@@ -314,6 +314,50 @@ def test_switch_state(model):
     assert driver.switch_state == expected
 
 
+def test_clear_limit_switch(virtual_smoothie_env, model, monkeypatch):
+    """
+    This functions as a contract test around recovery from a limit-switch hit.
+    Note that this *does not* itself guarantee correct physical behavior--this
+    interaction has been designed and tested on the robot manually and then
+    encoded in this test. If requirements change around physical behavior, then
+    this test will need to be revised.
+    """
+    from opentrons.drivers.smoothie_drivers.driver_3_0 import (
+        serial_communication, GCODES, SmoothieError)
+
+    driver = model.robot._driver
+    model.robot.home()
+    cmd_list = []
+
+    def write_mock(command, serial_connection, timeout):
+        nonlocal cmd_list
+        cmd_list.append(command)
+        if GCODES['MOVE'] in command:
+            return "ALARM: Hard limit +C"
+        elif GCODES['CURRENT_POSITION'] in command:
+            return 'ok M114.2 X:10 Y:20: Z:30 A:40 B:50 C:60'
+        else:
+            return "ok"
+
+    monkeypatch.setattr(serial_communication, 'write_and_return', write_mock)
+
+    driver.simulating = False
+    # This will cause a limit-switch error and not back off
+    with pytest.raises(SmoothieError):
+        driver.move({'C': 100})
+
+    assert [c.split(' ')[0] for c in cmd_list] == [
+        'M907',        # set current up
+        'G4P0.05',     # dwell
+        'G0C100.3',    # move C (with backlash compensation)
+        'M999',        # reset from error
+        'G28.2C',      # home
+        'M907',        # set current
+        'G4P0.05',     # dwell
+        'M114.2'       # get current position
+    ]
+
+
 def test_pause_resume(model):
     """
     This test has to use an ugly work-around with the `simulating` member of