Skip to content

fix(api): Do not hang if there is an error in pickUpTip planning #18207

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

Merged
merged 11 commits into from
Apr 30, 2025
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
10 changes: 5 additions & 5 deletions api/src/opentrons/protocol_api/core/engine/instrument.py
Original file line number Diff line number Diff line change
Expand Up @@ -1010,15 +1010,15 @@ def get_hardware_state(self) -> PipetteDict:
return self._sync_hardware_api.get_attached_instrument(self.get_mount()) # type: ignore[no-any-return]

def get_channels(self) -> int:
return self._engine_client.state.tips.get_pipette_channels(self._pipette_id)
return self._engine_client.state.pipettes.get_channels(self._pipette_id)

def get_active_channels(self) -> int:
return self._engine_client.state.tips.get_pipette_active_channels(
self._pipette_id
)
return self._engine_client.state.pipettes.get_active_channels(self._pipette_id)

def get_nozzle_map(self) -> NozzleMapInterface:
return self._engine_client.state.tips.get_pipette_nozzle_map(self._pipette_id)
return self._engine_client.state.pipettes.get_nozzle_configuration(
self._pipette_id
)

def has_tip(self) -> bool:
return (
Expand Down
4 changes: 2 additions & 2 deletions api/src/opentrons/protocol_engine/commands/get_next_tip.py
Original file line number Diff line number Diff line change
Expand Up @@ -73,8 +73,8 @@ async def execute(self, params: GetNextTipParams) -> SuccessData[GetNextTipResul
pipette_id = params.pipetteId
starting_tip_name = params.startingTipWell

num_tips = self._state_view.tips.get_pipette_active_channels(pipette_id)
nozzle_map = self._state_view.tips.get_pipette_nozzle_map(pipette_id)
num_tips = self._state_view.pipettes.get_active_channels(pipette_id)
nozzle_map = self._state_view.pipettes.get_nozzle_configuration(pipette_id)

if (
starting_tip_name is not None
Expand Down
12 changes: 9 additions & 3 deletions api/src/opentrons/protocol_engine/commands/pick_up_tip.py
Original file line number Diff line number Diff line change
Expand Up @@ -121,6 +121,12 @@ async def execute(
labware_id = params.labwareId
well_name = params.wellName

tips_to_mark_as_used = self._state_view.tips.compute_tips_to_mark_as_used(
labware_id=labware_id,
well_name=well_name,
nozzle_map=self._state_view.pipettes.get_nozzle_configuration(pipette_id),
)

well_location = self._state_view.geometry.convert_pick_up_tip_well_location(
well_location=params.wellLocation
)
Expand Down Expand Up @@ -152,15 +158,15 @@ async def execute(
)
.set_fluid_empty(pipette_id=pipette_id, clean_tip=True)
.mark_tips_as_used(
pipette_id=pipette_id, labware_id=labware_id, well_name=well_name
labware_id=labware_id, well_names=tips_to_mark_as_used
)
)
state_update = (
update_types.StateUpdate.reduce(
update_types.StateUpdate(), move_result.state_update
)
.mark_tips_as_used(
pipette_id=pipette_id, labware_id=labware_id, well_name=well_name
labware_id=labware_id, well_names=tips_to_mark_as_used
)
.set_fluid_unknown(pipette_id=pipette_id)
)
Expand All @@ -186,7 +192,7 @@ async def execute(
tip_geometry=tip_geometry,
)
.mark_tips_as_used(
pipette_id=pipette_id, labware_id=labware_id, well_name=well_name
labware_id=labware_id, well_names=tips_to_mark_as_used
)
.set_fluid_empty(pipette_id=pipette_id, clean_tip=True)
.set_pipette_ready_to_aspirate(
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -249,7 +249,7 @@ async def execute(

# Begin relative pickup steps for the resin tips

channels = self._state_view.tips.get_pipette_active_channels(pipette_id)
channels = self._state_view.pipettes.get_active_channels(pipette_id)
mount = self._state_view.pipettes.get_mount(pipette_id)
tip_pick_up_params = params.tipPickUpParams

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -81,8 +81,7 @@ async def execute(
pipette_id=params.pipetteId,
home_after=params.homeAfter,
ignore_plunger=(
self._state_view.tips.get_pipette_active_channels(params.pipetteId)
== 96
self._state_view.pipettes.get_active_channels(params.pipetteId) == 96
),
)

Expand Down
8 changes: 2 additions & 6 deletions api/src/opentrons/protocol_engine/execution/movement.py
Original file line number Diff line number Diff line change
Expand Up @@ -105,9 +105,7 @@ async def move_to_well(
self._hs_movement_flagger.raise_if_movement_restricted(
hs_movement_restrictors=hs_movement_restrictors,
destination_slot=dest_slot_int,
is_multi_channel=(
self._state_store.tips.get_pipette_channels(pipette_id) > 1
),
is_multi_channel=(self._state_store.pipettes.get_channels(pipette_id) > 1),
destination_is_tip_rack=self._state_store.labware.is_tiprack(labware_id),
)

Expand Down Expand Up @@ -204,9 +202,7 @@ async def move_to_addressable_area(
self._hs_movement_flagger.raise_if_movement_restricted(
hs_movement_restrictors=hs_movement_restrictors,
destination_slot=dest_slot_int,
is_multi_channel=(
self._state_store.tips.get_pipette_channels(pipette_id) > 1
),
is_multi_channel=(self._state_store.pipettes.get_channels(pipette_id) > 1),
destination_is_tip_rack=False,
)

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -72,7 +72,14 @@ async def _run_commands(self) -> None:
try:
await self._command_executor.execute(command_id=command_id)
except BaseException:
log.exception("Unhandled failure in command executor")
log.exception(
# The state can tear if e.g. we've finished updating PipetteStore,
# but the exception came before we could update LabwareStore. Or
# the exception could have interrupted updating a single store.
"Unhandled failure in command executor."
" This is a bug in opentrons.protocol_engine"
" and has probably left the ProtocolEngine in a torn state."
)
raise
# Yield to the event loop in case we're executing a long sequence of commands
# that never yields internally. For example, a long sequence of comment commands.
Expand Down
4 changes: 2 additions & 2 deletions api/src/opentrons/protocol_engine/state/_well_math.py
Original file line number Diff line number Diff line change
Expand Up @@ -63,7 +63,7 @@ def wells_covered_dense( # noqa: C901
row_downsample = len(target_wells_by_column[0]) // 8
if column_downsample < 1 or row_downsample < 1:
raise InvalidStoredData(
"This labware cannot be used wells_covered_dense because it is less dense than an SBS 96 standard"
"This labware cannot be used with wells_covered_dense() because it is less dense than an SBS 96 standard"
)

for nozzle_column in range(len(nozzle_map.columns)):
Expand Down Expand Up @@ -126,7 +126,7 @@ def wells_covered_sparse( # noqa: C901
row_upsample = 8 // len(target_wells_by_column[0])
if column_upsample < 1 or row_upsample < 1:
raise InvalidStoredData(
"This labware cannot be used with wells_covered_sparse because it is more dense than an SBS 96 standard."
"This labware cannot be used with wells_covered_sparse() because it is more dense than an SBS 96 standard."
)
for nozzle_column in range(max(1, len(nozzle_map.columns) // column_upsample)):
for nozzle_row in range(max(1, len(nozzle_map.rows) // row_upsample)):
Expand Down
8 changes: 8 additions & 0 deletions api/src/opentrons/protocol_engine/state/pipettes.py
Original file line number Diff line number Diff line change
Expand Up @@ -650,6 +650,10 @@ def get_channels(self, pipette_id: str) -> int:
"""Return the max channels of the pipette."""
return self.get_config(pipette_id).channels

def get_active_channels(self, pipette_id: str) -> int:
"""Get the number of channels being used in the given pipette's configuration."""
return self.get_nozzle_configuration(pipette_id).tip_count

def get_minimum_volume(self, pipette_id: str) -> float:
"""Return the given pipette's minimum volume."""
return self.get_config(pipette_id).min_volume
Expand Down Expand Up @@ -727,6 +731,10 @@ def get_primary_nozzle(self, pipette_id: str) -> str:
nozzle_map = self._state.nozzle_configuration_by_id[pipette_id]
return nozzle_map.starting_nozzle

def get_nozzle_configurations(self) -> Dict[str, NozzleMap]:
"""Get the nozzle maps of all pipettes, keyed by pipette ID."""
return self._state.nozzle_configuration_by_id.copy()

def get_nozzle_configuration(self, pipette_id: str) -> NozzleMap:
"""Get the nozzle map of the pipette."""
return self._state.nozzle_configuration_by_id[pipette_id]
Expand Down
Loading