From b0cb55109b001146f8ebd8864c020bb31578225b Mon Sep 17 00:00:00 2001 From: Max Marrone Date: Thu, 17 Oct 2024 12:37:55 -0400 Subject: [PATCH 01/10] Bring `API`'s logical tip removal code out of plan_check_drop_tip(). This is a trivial refactor and should have no behavioral effect. This is to bring make `API.drop_tip()` look structurally the same as `OT3API.drop_tip()`. --- api/src/opentrons/hardware_control/api.py | 8 ++++++-- .../hardware_control/instruments/ot2/pipette_handler.py | 2 ++ 2 files changed, 8 insertions(+), 2 deletions(-) diff --git a/api/src/opentrons/hardware_control/api.py b/api/src/opentrons/hardware_control/api.py index ea4c44265c3..bfa40431ec4 100644 --- a/api/src/opentrons/hardware_control/api.py +++ b/api/src/opentrons/hardware_control/api.py @@ -1244,7 +1244,7 @@ async def pick_up_tip( async def drop_tip(self, mount: top_types.Mount, home_after: bool = True) -> None: """Drop tip at the current location.""" - spec, _remove = self.plan_check_drop_tip(mount, home_after) + spec, _ = self.plan_check_drop_tip(mount, home_after) for move in spec.drop_moves: self._backend.set_active_current(move.current) @@ -1272,7 +1272,11 @@ async def drop_tip(self, mount: top_types.Mount, home_after: bool = True) -> Non await self.move_rel(mount, shake[0], speed=shake[1]) self._backend.set_active_current(spec.ending_current) - _remove() + + instrument = self.get_pipette(mount) + instrument.set_current_volume(0) + instrument.current_tiprack_diameter = 0.0 + instrument.remove_tip() async def create_simulating_module( self, diff --git a/api/src/opentrons/hardware_control/instruments/ot2/pipette_handler.py b/api/src/opentrons/hardware_control/instruments/ot2/pipette_handler.py index 99a7a49d41a..152c06f66ac 100644 --- a/api/src/opentrons/hardware_control/instruments/ot2/pipette_handler.py +++ b/api/src/opentrons/hardware_control/instruments/ot2/pipette_handler.py @@ -929,6 +929,8 @@ def plan_check_drop_tip( ) -> Tuple[DropTipSpec, Callable[[], None]]: ... + # todo(mm, 2024-10-17): The returned _remove_tips() callable is not used by anything + # anymore. Delete it. def plan_check_drop_tip( # type: ignore[no-untyped-def] self, mount, From c2c3c01b1f8655c6fa375dd764b94d3edd8c7e50 Mon Sep 17 00:00:00 2001 From: Max Marrone Date: Thu, 17 Oct 2024 11:44:27 -0400 Subject: [PATCH 02/10] Add drop_tip_moves(). The new drop_tip_moves() function is just the motion part of a drop_tip(), excluding any updates that the hardware API would otherwise make to its own logical state. It is like the existing tip_pickup_moves() function. Reimplement drop_tip() in terms of drop_tip_moves(). This is just a refactor so far, with no intended behavioral change. --- api/src/opentrons/hardware_control/api.py | 10 +++++--- api/src/opentrons/hardware_control/ot3api.py | 24 +++++++++++-------- .../protocols/liquid_handler.py | 5 ++++ 3 files changed, 26 insertions(+), 13 deletions(-) diff --git a/api/src/opentrons/hardware_control/api.py b/api/src/opentrons/hardware_control/api.py index bfa40431ec4..b1c0411b900 100644 --- a/api/src/opentrons/hardware_control/api.py +++ b/api/src/opentrons/hardware_control/api.py @@ -1241,9 +1241,9 @@ async def pick_up_tip( if prep_after: await self.prepare_for_aspirate(mount) - async def drop_tip(self, mount: top_types.Mount, home_after: bool = True) -> None: - """Drop tip at the current location.""" - + async def tip_drop_moves( + self, mount: top_types.Mount, home_after: bool = True + ) -> None: spec, _ = self.plan_check_drop_tip(mount, home_after) for move in spec.drop_moves: @@ -1273,6 +1273,10 @@ async def drop_tip(self, mount: top_types.Mount, home_after: bool = True) -> Non self._backend.set_active_current(spec.ending_current) + async def drop_tip(self, mount: top_types.Mount, home_after: bool = True) -> None: + """Drop tip at the current location.""" + await self.tip_drop_moves(mount, home_after) + instrument = self.get_pipette(mount) instrument.set_current_volume(0) instrument.current_tiprack_diameter = 0.0 diff --git a/api/src/opentrons/hardware_control/ot3api.py b/api/src/opentrons/hardware_control/ot3api.py index cdd69fc2f90..8d1d8c4b6a1 100644 --- a/api/src/opentrons/hardware_control/ot3api.py +++ b/api/src/opentrons/hardware_control/ot3api.py @@ -2289,17 +2289,10 @@ def set_working_volume( ) instrument.working_volume = tip_volume - async def drop_tip( + async def tip_drop_moves( self, mount: Union[top_types.Mount, OT3Mount], home_after: bool = False ) -> None: - """Drop tip at the current location.""" realmount = OT3Mount.from_mount(mount) - instrument = self._pipette_handler.get_pipette(realmount) - - def _remove_tips() -> None: - instrument.set_current_volume(0) - instrument.current_tiprack_diameter = 0.0 - instrument.remove_tip() await self._move_to_plunger_bottom(realmount, rate=1.0, check_current_vol=False) @@ -2326,8 +2319,19 @@ def _remove_tips() -> None: if home_after: await self._home([Axis.by_mount(mount)]) - _remove_tips() - # call this in case we're simulating + async def drop_tip( + self, mount: Union[top_types.Mount, OT3Mount], home_after: bool = False + ) -> None: + """Drop tip at the current location.""" + realmount = OT3Mount.from_mount(mount) + instrument = self._pipette_handler.get_pipette(realmount) + + await self.tip_drop_moves(mount=mount, home_after=home_after) + + instrument.set_current_volume(0) + instrument.current_tiprack_diameter = 0.0 + instrument.remove_tip() + # call this in case we're simulating: if isinstance(self._backend, OT3Simulator): self._backend._update_tip_state(realmount, False) diff --git a/api/src/opentrons/hardware_control/protocols/liquid_handler.py b/api/src/opentrons/hardware_control/protocols/liquid_handler.py index 8baa786dc9f..8707fc33024 100644 --- a/api/src/opentrons/hardware_control/protocols/liquid_handler.py +++ b/api/src/opentrons/hardware_control/protocols/liquid_handler.py @@ -164,6 +164,11 @@ async def pick_up_tip( """ ... + async def tip_drop_moves( + self, mount: MountArgType, home_after: bool = True + ) -> None: + ... + async def drop_tip( self, mount: MountArgType, From 98f92d753dd07b04a94a9eb9c6fb11a63dec6f76 Mon Sep 17 00:00:00 2001 From: Max Marrone Date: Thu, 17 Oct 2024 12:19:28 -0400 Subject: [PATCH 03/10] Leave todo comments for cache_tip(), add_tip(), and remove_tip(). --- api/src/opentrons/hardware_control/protocols/__init__.py | 8 ++++++++ .../hardware_control/protocols/instrument_configurer.py | 4 ++++ 2 files changed, 12 insertions(+) diff --git a/api/src/opentrons/hardware_control/protocols/__init__.py b/api/src/opentrons/hardware_control/protocols/__init__.py index cff17ff1d9a..a54217f37a3 100644 --- a/api/src/opentrons/hardware_control/protocols/__init__.py +++ b/api/src/opentrons/hardware_control/protocols/__init__.py @@ -58,6 +58,10 @@ class HardwareControlInterface( def get_robot_type(self) -> Type[OT2RobotType]: return OT2RobotType + # todo(mm, 2024-10-17): This probably belongs in InstrumentConfigurer, alongside + # add_tip() and remove_tip(). + # todo(mm, 2024-10-17): What is the difference between this and add_tip()? + # Can one of them be removed? def cache_tip(self, mount: MountArgType, tip_length: float) -> None: ... @@ -87,6 +91,10 @@ class FlexHardwareControlInterface( def get_robot_type(self) -> Type[FlexRobotType]: return FlexRobotType + # todo(mm, 2024-10-17): This probably belongs in InstrumentConfigurer, alongside + # add_tip() and remove_tip(). + # todo(mm, 2024-10-17): What is the difference between this and add_tip()? + # Can one of them be removed? def cache_tip(self, mount: MountArgType, tip_length: float) -> None: ... diff --git a/api/src/opentrons/hardware_control/protocols/instrument_configurer.py b/api/src/opentrons/hardware_control/protocols/instrument_configurer.py index 11e718a9aff..60381fcc90a 100644 --- a/api/src/opentrons/hardware_control/protocols/instrument_configurer.py +++ b/api/src/opentrons/hardware_control/protocols/instrument_configurer.py @@ -142,6 +142,9 @@ def get_instrument_max_height( """ ... + # todo(mm, 2024-10-17): Can this be made non-async? + # todo(mm, 2024-10-17): What is the difference between this and cache_tip()? + # Can one of them be removed? async def add_tip(self, mount: MountArgType, tip_length: float) -> None: """Inform the hardware that a tip is now attached to a pipette. @@ -150,6 +153,7 @@ async def add_tip(self, mount: MountArgType, tip_length: float) -> None: """ ... + # todo(mm, 2024-10-17): Can this be made non-async? async def remove_tip(self, mount: MountArgType) -> None: """Inform the hardware that a tip is no longer attached to a pipette. From c65dc75ffd4f77202e25d6c0a46c614ce051c091 Mon Sep 17 00:00:00 2001 From: Max Marrone Date: Thu, 17 Oct 2024 12:44:00 -0400 Subject: [PATCH 04/10] Implement drop_tip()'s state updates in terms of other public methods, where possible. This is not intended to have any behavioral change. The goal of this is to make it easier for callers (i.e. Protocol Engine) to see what they have to do in order to replicate the total effect of drop_tip(). And to make it easier for human code reviewers to confirm that that's done correctly. --- api/src/opentrons/hardware_control/api.py | 9 +++++++-- api/src/opentrons/hardware_control/ot3api.py | 13 +++++++++---- 2 files changed, 16 insertions(+), 6 deletions(-) diff --git a/api/src/opentrons/hardware_control/api.py b/api/src/opentrons/hardware_control/api.py index b1c0411b900..636ad165983 100644 --- a/api/src/opentrons/hardware_control/api.py +++ b/api/src/opentrons/hardware_control/api.py @@ -1277,10 +1277,15 @@ async def drop_tip(self, mount: top_types.Mount, home_after: bool = True) -> Non """Drop tip at the current location.""" await self.tip_drop_moves(mount, home_after) + # todo(mm, 2024-10-17): Ideally, callers would be able to replicate the behavior + # of this method via self.drop_tip_moves() plus other public methods. This + # currently prevents that: there is no public equivalent for + # instrument.set_current_volume(). instrument = self.get_pipette(mount) instrument.set_current_volume(0) - instrument.current_tiprack_diameter = 0.0 - instrument.remove_tip() + + self.set_current_tiprack_diameter(mount, 0.0) + await self.remove_tip(mount) async def create_simulating_module( self, diff --git a/api/src/opentrons/hardware_control/ot3api.py b/api/src/opentrons/hardware_control/ot3api.py index 8d1d8c4b6a1..52be73119a3 100644 --- a/api/src/opentrons/hardware_control/ot3api.py +++ b/api/src/opentrons/hardware_control/ot3api.py @@ -2323,14 +2323,19 @@ async def drop_tip( self, mount: Union[top_types.Mount, OT3Mount], home_after: bool = False ) -> None: """Drop tip at the current location.""" + await self.tip_drop_moves(mount=mount, home_after=home_after) + + # todo(mm, 2024-10-17): Ideally, callers would be able to replicate the behavior + # of this method via self.drop_tip_moves() plus other public methods. This + # currently prevents that: there is no public equivalent for + # instrument.set_current_volume(). realmount = OT3Mount.from_mount(mount) instrument = self._pipette_handler.get_pipette(realmount) + instrument.set_current_volume(0) - await self.tip_drop_moves(mount=mount, home_after=home_after) + self.set_current_tiprack_diameter(mount, 0.0) + await self.remove_tip(mount) - instrument.set_current_volume(0) - instrument.current_tiprack_diameter = 0.0 - instrument.remove_tip() # call this in case we're simulating: if isinstance(self._backend, OT3Simulator): self._backend._update_tip_state(realmount, False) From c45fd89d3bb8c2b387d619f983a2ea42f43341b4 Mon Sep 17 00:00:00 2001 From: Max Marrone Date: Thu, 17 Oct 2024 12:50:56 -0400 Subject: [PATCH 05/10] Update Protocol Engine to only change hardware state only if the tip drop succeeds. This is, finally, the part that makes the actual behavioral change and fixes the actual bug. Having split up drop_tip(), we can now do the hardware API's state updates only after we've verified that the tip drop has physically succeeded. This mirrors what Protocol Engine does for its own state after tip drop errors--see TipPhysicallyAttachedError. --- .../protocol_engine/execution/tip_handler.py | 11 ++++++++++- 1 file changed, 10 insertions(+), 1 deletion(-) diff --git a/api/src/opentrons/protocol_engine/execution/tip_handler.py b/api/src/opentrons/protocol_engine/execution/tip_handler.py index 3fed8510a43..fd1306785c0 100644 --- a/api/src/opentrons/protocol_engine/execution/tip_handler.py +++ b/api/src/opentrons/protocol_engine/execution/tip_handler.py @@ -238,6 +238,7 @@ async def pick_up_tip( await self._hardware_api.tip_pickup_moves( mount=hw_mount, presses=None, increment=None ) + # Allow TipNotAttachedError to propagate. await self.verify_tip_presence(pipette_id, TipPresenceStatus.PRESENT) self._hardware_api.cache_tip(hw_mount, actual_tip_length) @@ -248,6 +249,9 @@ async def pick_up_tip( tiprack_diameter=nominal_tip_geometry.diameter, ) + # todo(mm, 2024-10-15): The hardware API's original pick_up_tip() implementation + # seems to set the *current* volume to 0, not the working volume. + # Investigate that discrepancy. self._hardware_api.set_working_volume( mount=hw_mount, tip_volume=nominal_tip_geometry.volume, @@ -270,9 +274,14 @@ async def drop_tip(self, pipette_id: str, home_after: Optional[bool]) -> None: else: kwargs = {} - await self._hardware_api.drop_tip(mount=hw_mount, **kwargs) + await self._hardware_api.tip_drop_moves(mount=hw_mount, **kwargs) + + # Allow TipNotAttachedError to propagate. await self.verify_tip_presence(pipette_id, TipPresenceStatus.ABSENT) + await self._hardware_api.remove_tip(hw_mount) + self._hardware_api.set_current_tiprack_diameter(hw_mount, 0) + async def add_tip(self, pipette_id: str, tip: TipGeometry) -> None: """See documentation on abstract base class.""" hw_mount = self._state_view.pipettes.get_mount(pipette_id).to_hw_mount() From 9591f896d0683e4641335150caec77479eb84630 Mon Sep 17 00:00:00 2001 From: Max Marrone Date: Thu, 17 Oct 2024 13:29:11 -0400 Subject: [PATCH 06/10] Move simulation-specific thing to tip_drop_moves(). I don't understand what this does, and given that it's a state update, it seems like it should not live in tip_drop_moves(). But tip_pickup_moves() has the same thing, so I presume we should treat it the same way. --- api/src/opentrons/hardware_control/ot3api.py | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/api/src/opentrons/hardware_control/ot3api.py b/api/src/opentrons/hardware_control/ot3api.py index 52be73119a3..54c776d39a3 100644 --- a/api/src/opentrons/hardware_control/ot3api.py +++ b/api/src/opentrons/hardware_control/ot3api.py @@ -2319,6 +2319,10 @@ async def tip_drop_moves( if home_after: await self._home([Axis.by_mount(mount)]) + # call this in case we're simulating: + if isinstance(self._backend, OT3Simulator): + self._backend._update_tip_state(realmount, False) + async def drop_tip( self, mount: Union[top_types.Mount, OT3Mount], home_after: bool = False ) -> None: @@ -2336,10 +2340,6 @@ async def drop_tip( self.set_current_tiprack_diameter(mount, 0.0) await self.remove_tip(mount) - # call this in case we're simulating: - if isinstance(self._backend, OT3Simulator): - self._backend._update_tip_state(realmount, False) - async def clean_up(self) -> None: """Get the API ready to stop cleanly.""" await self._backend.clean_up() From ad146b4d22607eca8edd782d14d2260efb8852dc Mon Sep 17 00:00:00 2001 From: Max Marrone Date: Thu, 17 Oct 2024 17:32:40 -0400 Subject: [PATCH 07/10] Update test. --- .../protocol_engine/execution/test_tip_handler.py | 11 +++++++++-- 1 file changed, 9 insertions(+), 2 deletions(-) diff --git a/api/tests/opentrons/protocol_engine/execution/test_tip_handler.py b/api/tests/opentrons/protocol_engine/execution/test_tip_handler.py index 5abeb7308b6..3a453cc6816 100644 --- a/api/tests/opentrons/protocol_engine/execution/test_tip_handler.py +++ b/api/tests/opentrons/protocol_engine/execution/test_tip_handler.py @@ -228,6 +228,8 @@ async def test_pick_up_tip( ) +# todo(mm, 2024-10-17): Test that when verify_tip_presence raises, +# the hardware API state is NOT updated. async def test_drop_tip( decoy: Decoy, mock_state_view: StateView, @@ -251,8 +253,13 @@ async def test_drop_tip( await subject.drop_tip(pipette_id="pipette-id", home_after=True) decoy.verify( - await mock_hardware_api.drop_tip(mount=Mount.RIGHT, home_after=True), - times=1, + await mock_hardware_api.tip_drop_moves(mount=Mount.RIGHT, home_after=True) + ) + decoy.verify(await mock_hardware_api.remove_tip(mount=Mount.RIGHT)) + decoy.verify( + mock_hardware_api.set_current_tiprack_diameter( + mount=Mount.RIGHT, tiprack_diameter=0 + ) ) From b265cb0152406f4ee37d390f602e495c993543ab Mon Sep 17 00:00:00 2001 From: Max Marrone Date: Thu, 17 Oct 2024 16:38:37 -0400 Subject: [PATCH 08/10] Clean up pick_up_tip() test. - Consistently use Decoy. - Remove unused rehearsals. - Add missing rehearsals. --- .../execution/test_tip_handler.py | 61 ++++++++++--------- 1 file changed, 32 insertions(+), 29 deletions(-) diff --git a/api/tests/opentrons/protocol_engine/execution/test_tip_handler.py b/api/tests/opentrons/protocol_engine/execution/test_tip_handler.py index 3a453cc6816..b80e11241e6 100644 --- a/api/tests/opentrons/protocol_engine/execution/test_tip_handler.py +++ b/api/tests/opentrons/protocol_engine/execution/test_tip_handler.py @@ -1,7 +1,6 @@ """Pipetting execution handler.""" import pytest -from decoy import Decoy -from mock import AsyncMock, patch +from decoy import Decoy, matchers from typing import Dict, ContextManager, Optional, OrderedDict from contextlib import nullcontext as does_not_raise @@ -91,7 +90,6 @@ async def test_create_tip_handler( ) -@pytest.mark.ot3_only @pytest.mark.parametrize("tip_state", [TipStateType.PRESENT, TipStateType.ABSENT]) async def test_flex_pick_up_tip_state( decoy: Decoy, @@ -99,22 +97,23 @@ async def test_flex_pick_up_tip_state( mock_labware_data_provider: LabwareDataProvider, tip_rack_definition: LabwareDefinition, tip_state: TipStateType, + mock_hardware_api: HardwareAPI, ) -> None: """Test the protocol engine's pick_up_tip logic.""" - from opentrons.hardware_control.ot3api import OT3API - - ot3_hardware_api = decoy.mock(cls=OT3API) - decoy.when(ot3_hardware_api.get_robot_type()).then_return(FlexRobotType) - subject = HardwareTipHandler( state_view=mock_state_view, - hardware_api=ot3_hardware_api, + hardware_api=mock_hardware_api, labware_data_provider=mock_labware_data_provider, ) - decoy.when(subject._state_view.config.robot_type).then_return("OT-3 Standard") decoy.when(mock_state_view.pipettes.get_mount("pipette-id")).then_return( MountType.LEFT ) + decoy.when(mock_state_view.pipettes.get_serial_number("pipette-id")).then_return( + "pipette-serial" + ) + decoy.when(mock_state_view.labware.get_definition("labware-id")).then_return( + tip_rack_definition + ) decoy.when(mock_state_view.pipettes.state.nozzle_configuration_by_id).then_return( {"pipette-id": MOCK_MAP} ) @@ -134,30 +133,34 @@ async def test_flex_pick_up_tip_state( ) ).then_return(42) - with patch.object( - ot3_hardware_api, "cache_tip", AsyncMock(spec=ot3_hardware_api.cache_tip) - ) as mock_add_tip: - if tip_state == TipStateType.PRESENT: + if tip_state == TipStateType.PRESENT: + await subject.pick_up_tip( + pipette_id="pipette-id", + labware_id="labware-id", + well_name="B2", + ) + decoy.verify(mock_hardware_api.cache_tip(Mount.LEFT, 42), times=1) + else: + decoy.when( + await subject.verify_tip_presence( + pipette_id="pipette-id", expected=TipPresenceStatus.PRESENT + ) + ).then_raise(TipNotAttachedError()) + # if a TipNotAttchedError is caught, we should not add any tip information + with pytest.raises(TipNotAttachedError): await subject.pick_up_tip( pipette_id="pipette-id", labware_id="labware-id", well_name="B2", ) - mock_add_tip.assert_called_once() - else: - decoy.when( - await subject.verify_tip_presence( - pipette_id="pipette-id", expected=TipPresenceStatus.PRESENT - ) - ).then_raise(TipNotAttachedError()) - # if a TipNotAttchedError is caught, we should not add any tip information - with pytest.raises(TipNotAttachedError): - await subject.pick_up_tip( - pipette_id="pipette-id", - labware_id="labware-id", - well_name="B2", - ) - mock_add_tip.assert_not_called() + decoy.verify( + mock_hardware_api.cache_tip( + mount=matchers.Anything(), + tip_length=matchers.Anything(), + ), + ignore_extra_args=True, + times=0, + ) async def test_pick_up_tip( From 58a0c75af97b3f107268f125da8ee9bd2d24ce6b Mon Sep 17 00:00:00 2001 From: Max Marrone Date: Thu, 17 Oct 2024 18:19:56 -0400 Subject: [PATCH 09/10] Delete todo about working volume. --- api/src/opentrons/protocol_engine/execution/tip_handler.py | 3 --- 1 file changed, 3 deletions(-) diff --git a/api/src/opentrons/protocol_engine/execution/tip_handler.py b/api/src/opentrons/protocol_engine/execution/tip_handler.py index fd1306785c0..6e496246b8f 100644 --- a/api/src/opentrons/protocol_engine/execution/tip_handler.py +++ b/api/src/opentrons/protocol_engine/execution/tip_handler.py @@ -249,9 +249,6 @@ async def pick_up_tip( tiprack_diameter=nominal_tip_geometry.diameter, ) - # todo(mm, 2024-10-15): The hardware API's original pick_up_tip() implementation - # seems to set the *current* volume to 0, not the working volume. - # Investigate that discrepancy. self._hardware_api.set_working_volume( mount=hw_mount, tip_volume=nominal_tip_geometry.volume, From ce9d55ecb916c7e6744633b43945ef24cc2e1012 Mon Sep 17 00:00:00 2001 From: Max Marrone Date: Fri, 18 Oct 2024 09:22:55 -0400 Subject: [PATCH 10/10] Update todo comments from answers on PR. --- api/src/opentrons/hardware_control/protocols/__init__.py | 4 ---- .../hardware_control/protocols/instrument_configurer.py | 4 ++-- 2 files changed, 2 insertions(+), 6 deletions(-) diff --git a/api/src/opentrons/hardware_control/protocols/__init__.py b/api/src/opentrons/hardware_control/protocols/__init__.py index a54217f37a3..1f3442ded3a 100644 --- a/api/src/opentrons/hardware_control/protocols/__init__.py +++ b/api/src/opentrons/hardware_control/protocols/__init__.py @@ -60,8 +60,6 @@ def get_robot_type(self) -> Type[OT2RobotType]: # todo(mm, 2024-10-17): This probably belongs in InstrumentConfigurer, alongside # add_tip() and remove_tip(). - # todo(mm, 2024-10-17): What is the difference between this and add_tip()? - # Can one of them be removed? def cache_tip(self, mount: MountArgType, tip_length: float) -> None: ... @@ -93,8 +91,6 @@ def get_robot_type(self) -> Type[FlexRobotType]: # todo(mm, 2024-10-17): This probably belongs in InstrumentConfigurer, alongside # add_tip() and remove_tip(). - # todo(mm, 2024-10-17): What is the difference between this and add_tip()? - # Can one of them be removed? def cache_tip(self, mount: MountArgType, tip_length: float) -> None: ... diff --git a/api/src/opentrons/hardware_control/protocols/instrument_configurer.py b/api/src/opentrons/hardware_control/protocols/instrument_configurer.py index 60381fcc90a..a4ba63c8e56 100644 --- a/api/src/opentrons/hardware_control/protocols/instrument_configurer.py +++ b/api/src/opentrons/hardware_control/protocols/instrument_configurer.py @@ -143,8 +143,8 @@ def get_instrument_max_height( ... # todo(mm, 2024-10-17): Can this be made non-async? - # todo(mm, 2024-10-17): What is the difference between this and cache_tip()? - # Can one of them be removed? + # todo(mm, 2024-10-17): Consider deleting this in favor of cache_tip(), which is + # the same except for `assert`s, if we can do so without breaking anything. async def add_tip(self, mount: MountArgType, tip_length: float) -> None: """Inform the hardware that a tip is now attached to a pipette.