Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

fix(api): home before retrieving gantry position for disengaged Z mounts in FLEX #14437

7 changes: 5 additions & 2 deletions api/src/opentrons/hardware_control/api.py
Original file line number Diff line number Diff line change
Expand Up @@ -762,7 +762,7 @@ async def move_to(
top_types.Point(0, 0, 0),
)

await self._cache_and_maybe_retract_mount(mount)
await self.prepare_for_mount_movement(mount)
await self._move(target_position, speed=speed, max_speeds=max_speeds)

async def move_axes(
Expand Down Expand Up @@ -822,7 +822,7 @@ async def move_rel(
detail={"mount": str(mount), "unhomed_axes": str(unhomed)},
)

await self._cache_and_maybe_retract_mount(mount)
await self.prepare_for_mount_movement(mount)
await self._move(
target_position,
speed=speed,
Expand All @@ -842,6 +842,9 @@ async def _cache_and_maybe_retract_mount(self, mount: top_types.Mount) -> None:
await self.retract(self._last_moved_mount, 10)
self._last_moved_mount = mount

async def prepare_for_mount_movement(self, mount: top_types.Mount) -> None:
await self._cache_and_maybe_retract_mount(mount)

@ExecutionManagerProvider.wait_for_running
async def _move(
self,
Expand Down
43 changes: 35 additions & 8 deletions api/src/opentrons/hardware_control/ot3api.py
Original file line number Diff line number Diff line change
Expand Up @@ -247,6 +247,22 @@ def estop_cb(event: HardwareEvent) -> None:
OT3RobotCalibrationProvider.__init__(self, self._config)
ExecutionManagerProvider.__init__(self, isinstance(backend, OT3Simulator))

def is_idle_mount(self, mount: Union[top_types.Mount, OT3Mount]) -> bool:
"""Only the gripper mount or the 96-channel pipette mount would be idle
(disengaged).

If gripper mount is NOT the last moved mount, it's idle.
If a 96-channel pipette is attached, the mount is idle if it's not
the last moved mount.
"""
realmount = OT3Mount.from_mount(mount)
if not self._last_moved_mount or realmount == self._last_moved_mount:
return False

return (self._gantry_load == GantryLoad.HIGH_THROUGHPUT) or (
ahiuchingau marked this conversation as resolved.
Show resolved Hide resolved
realmount == OT3Mount.GRIPPER
)

@property
def door_state(self) -> DoorState:
return self._door_state
Expand Down Expand Up @@ -1150,7 +1166,7 @@ async def move_to(
else:
checked_max = None

await self._cache_and_maybe_retract_mount(realmount)
await self.prepare_for_mount_movement(realmount)
await self._move(
target_position,
speed=speed,
Expand Down Expand Up @@ -1261,7 +1277,8 @@ async def move_rel(
checked_max: Optional[OT3AxisMap[float]] = max_speeds
else:
checked_max = None
await self._cache_and_maybe_retract_mount(realmount)

await self.prepare_for_mount_movement(realmount)
await self._move(
target_position,
speed=speed,
Expand All @@ -1271,24 +1288,26 @@ async def move_rel(
)

async def _cache_and_maybe_retract_mount(self, mount: OT3Mount) -> None:
"""Retract the 'other' mount if necessary
"""Retract the 'other' mount if necessary.

If `mount` does not match the value in :py:attr:`_last_moved_mount`
(and :py:attr:`_last_moved_mount` exists) then retract the mount
in :py:attr:`_last_moved_mount`. Also unconditionally update
:py:attr:`_last_moved_mount` to contain `mount`.

Disengage the 96-channel and gripper mount if retracted.
Disengage the 96-channel and gripper mount if retracted. Re-engage
the 96-channel or gripper mount if it is about to move.
"""
if self.is_idle_mount(mount):
# home the left/gripper mount if it is current disengaged
await self.home_z(mount)

if mount != self._last_moved_mount and self._last_moved_mount:
await self.retract(self._last_moved_mount, 10)

# disengage Axis.Z_L motor and engage the brake to lower power
# consumption and reduce the chance of the 96-channel pipette dropping
if (
self.gantry_load == GantryLoad.HIGH_THROUGHPUT
and self._last_moved_mount == OT3Mount.LEFT
):
if self.is_idle_mount(OT3Mount.LEFT):
await self.disengage_axes([Axis.Z_L])

# disegnage Axis.Z_G when we can to reduce the chance of
Expand All @@ -1298,8 +1317,16 @@ async def _cache_and_maybe_retract_mount(self, mount: OT3Mount) -> None:

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

the gripper mount above should get this treatment right?

if mount != OT3Mount.GRIPPER:
await self.idle_gripper()

self._last_moved_mount = mount

async def prepare_for_mount_movement(
self, mount: Union[top_types.Mount, OT3Mount]
) -> None:
"""Retract the idle mount if necessary."""
realmount = OT3Mount.from_mount(mount)
await self._cache_and_maybe_retract_mount(realmount)

async def idle_gripper(self) -> None:
"""Move gripper to its idle, gripped position."""
try:
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -226,3 +226,7 @@ def should_taskify_movement_execution(self, taskify: bool) -> None:
async def cancel_execution_and_running_tasks(self) -> None:
"""Cancel all tasks and set execution manager state to Cancelled."""
...

async def prepare_for_mount_movement(self, mount: MountArgType) -> None:
"""Retract the other mount if necessary."""
...
Original file line number Diff line number Diff line change
Expand Up @@ -82,10 +82,7 @@ async def execute(
)
# the 96-channel mount is disengaged during gripper calibration and
# must be homed before the gantry position can be called
if ot3_api.encoder_status_ok(Axis.Z_L) and not ot3_api.motor_status_ok(
Axis.Z_L
):
await ot3_api.home([Axis.Z_L])
await ot3_api.prepare_for_mount_movement(Mount.LEFT)
current_position_mount = await ot3_api.gantry_position(
Mount.LEFT, critical_point=CriticalPoint.MOUNT
)
Expand All @@ -105,6 +102,7 @@ async def execute(
Point(x=_ATTACH_POINT.x, y=_ATTACH_POINT.y, z=max_height_z_mount),
]

await ot3_api.prepare_for_mount_movement(Mount.LEFT)
for movement in movement_points:
await ot3_api.move_to(
mount=Mount.LEFT,
Expand Down
13 changes: 13 additions & 0 deletions api/src/opentrons/protocol_engine/execution/gantry_mover.py
Original file line number Diff line number Diff line change
Expand Up @@ -75,6 +75,11 @@ async def home(self, axes: Optional[List[MotorAxis]]) -> None:

async def retract_axis(self, axis: MotorAxis) -> None:
"""Retract the specified axis to its home position."""
...

async def prepare_for_mount_movement(self, mount: Mount) -> None:
"""Retract the 'idle' mount if necessary."""
...


class HardwareGantryMover(GantryMover):
Expand Down Expand Up @@ -211,6 +216,10 @@ async def retract_axis(self, axis: MotorAxis) -> None:
)
await self._hardware_api.retract_axis(axis=hardware_axis)

async def prepare_for_mount_movement(self, mount: Mount) -> None:
"""Retract the 'idle' mount if necessary."""
await self._hardware_api.prepare_for_mount_movement(mount)


class VirtualGantryMover(GantryMover):
"""State store based gantry movement handler for simulation/analysis."""
Expand Down Expand Up @@ -286,6 +295,10 @@ async def retract_axis(self, axis: MotorAxis) -> None:
"""Retract the specified axis. No-op in virtual implementation."""
pass

async def prepare_for_mount_movement(self, mount: Mount) -> None:
"""Retract the 'idle' mount if necessary."""
pass


def create_gantry_mover(
state_view: StateView, hardware_api: HardwareControlAPI
Expand Down
13 changes: 13 additions & 0 deletions api/src/opentrons/protocol_engine/execution/movement.py
Original file line number Diff line number Diff line change
Expand Up @@ -111,6 +111,9 @@ async def move_to_well(
)
origin_cp = pipette_location.critical_point

await self._gantry_mover.prepare_for_mount_movement(
pipette_location.mount.to_hw_mount()
)
origin = await self._gantry_mover.get_position(pipette_id=pipette_id)
max_travel_z = self._gantry_mover.get_max_travel_z(pipette_id=pipette_id)

Expand Down Expand Up @@ -180,6 +183,9 @@ async def move_to_addressable_area(
)
origin_cp = pipette_location.critical_point

await self._gantry_mover.prepare_for_mount_movement(
pipette_location.mount.to_hw_mount()
)
origin = await self._gantry_mover.get_position(pipette_id=pipette_id)
max_travel_z = self._gantry_mover.get_max_travel_z(pipette_id=pipette_id)

Expand Down Expand Up @@ -237,6 +243,13 @@ async def move_to_coordinates(
speed: Optional[float] = None,
) -> Point:
"""Move pipette to a given deck coordinate."""
# get the pipette's mount, if applicable
pipette_location = self._state_store.motion.get_pipette_location(
pipette_id=pipette_id
)
await self._gantry_mover.prepare_for_mount_movement(
pipette_location.mount.to_hw_mount()
)
origin = await self._gantry_mover.get_position(pipette_id=pipette_id)
max_travel_z = self._gantry_mover.get_max_travel_z(pipette_id=pipette_id)

Expand Down
Loading