Skip to content

Commit 79b84d4

Browse files
CaseyBattencaila-marashaj
authored andcommitted
feat(api): Addition of Evotip specific commands (#17351)
Covers EXEC-907 Introduces Evotip (or resin-tip) specific commands to the instrument context. These commands achieve the desired sealing, pressurization and unsealing steps for a resin tip protocol.
1 parent 18da739 commit 79b84d4

File tree

33 files changed

+3304
-29
lines changed

33 files changed

+3304
-29
lines changed

api/src/opentrons/hardware_control/api.py

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -778,6 +778,7 @@ async def move_axes(
778778
position: Mapping[Axis, float],
779779
speed: Optional[float] = None,
780780
max_speeds: Optional[Dict[Axis, float]] = None,
781+
expect_stalls: bool = False,
781782
) -> None:
782783
"""Moves the effectors of the specified axis to the specified position.
783784
The effector of the x,y axis is the center of the carriage.
@@ -1250,7 +1251,10 @@ async def pick_up_tip(
12501251
await self.prepare_for_aspirate(mount)
12511252

12521253
async def tip_drop_moves(
1253-
self, mount: top_types.Mount, home_after: bool = True
1254+
self,
1255+
mount: top_types.Mount,
1256+
home_after: bool = True,
1257+
ignore_plunger: bool = False,
12541258
) -> None:
12551259
spec, _ = self.plan_check_drop_tip(mount, home_after)
12561260

api/src/opentrons/hardware_control/ot3api.py

Lines changed: 18 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -1189,7 +1189,7 @@ async def move_to(
11891189
speed: Optional[float] = None,
11901190
critical_point: Optional[CriticalPoint] = None,
11911191
max_speeds: Union[None, Dict[Axis, float], OT3AxisMap[float]] = None,
1192-
_expect_stalls: bool = False,
1192+
expect_stalls: bool = False,
11931193
) -> None:
11941194
"""Move the critical point of the specified mount to a location
11951195
relative to the deck, at the specified speed."""
@@ -1233,14 +1233,15 @@ async def move_to(
12331233
target_position,
12341234
speed=speed,
12351235
max_speeds=checked_max,
1236-
expect_stalls=_expect_stalls,
1236+
expect_stalls=expect_stalls,
12371237
)
12381238

12391239
async def move_axes( # noqa: C901
12401240
self,
12411241
position: Mapping[Axis, float],
12421242
speed: Optional[float] = None,
12431243
max_speeds: Optional[Dict[Axis, float]] = None,
1244+
expect_stalls: bool = False,
12441245
) -> None:
12451246
"""Moves the effectors of the specified axis to the specified position.
12461247
The effector of the x,y axis is the center of the carriage.
@@ -1296,7 +1297,11 @@ async def move_axes( # noqa: C901
12961297
if axis not in absolute_positions:
12971298
absolute_positions[axis] = position_value
12981299

1299-
await self._move(target_position=absolute_positions, speed=speed)
1300+
await self._move(
1301+
target_position=absolute_positions,
1302+
speed=speed,
1303+
expect_stalls=expect_stalls,
1304+
)
13001305

13011306
async def move_rel(
13021307
self,
@@ -1306,7 +1311,7 @@ async def move_rel(
13061311
max_speeds: Union[None, Dict[Axis, float], OT3AxisMap[float]] = None,
13071312
check_bounds: MotionChecks = MotionChecks.NONE,
13081313
fail_on_not_homed: bool = False,
1309-
_expect_stalls: bool = False,
1314+
expect_stalls: bool = False,
13101315
) -> None:
13111316
"""Move the critical point of the specified mount by a specified
13121317
displacement in a specified direction, at the specified speed."""
@@ -1348,7 +1353,7 @@ async def move_rel(
13481353
speed=speed,
13491354
max_speeds=checked_max,
13501355
check_bounds=check_bounds,
1351-
expect_stalls=_expect_stalls,
1356+
expect_stalls=expect_stalls,
13521357
)
13531358

13541359
async def _cache_and_maybe_retract_mount(self, mount: OT3Mount) -> None:
@@ -2329,11 +2334,16 @@ def set_working_volume(
23292334
instrument.working_volume = tip_volume
23302335

23312336
async def tip_drop_moves(
2332-
self, mount: Union[top_types.Mount, OT3Mount], home_after: bool = False
2337+
self,
2338+
mount: Union[top_types.Mount, OT3Mount],
2339+
home_after: bool = False,
2340+
ignore_plunger: bool = False,
23332341
) -> None:
23342342
realmount = OT3Mount.from_mount(mount)
2335-
2336-
await self._move_to_plunger_bottom(realmount, rate=1.0, check_current_vol=False)
2343+
if ignore_plunger is False:
2344+
await self._move_to_plunger_bottom(
2345+
realmount, rate=1.0, check_current_vol=False
2346+
)
23372347

23382348
if self.gantry_load == GantryLoad.HIGH_THROUGHPUT:
23392349
spec = self._pipette_handler.plan_ht_drop_tip()

api/src/opentrons/hardware_control/protocols/liquid_handler.py

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -187,7 +187,10 @@ async def pick_up_tip(
187187
...
188188

189189
async def tip_drop_moves(
190-
self, mount: MountArgType, home_after: bool = True
190+
self,
191+
mount: MountArgType,
192+
home_after: bool = True,
193+
ignore_plunger: bool = False,
191194
) -> None:
192195
...
193196

api/src/opentrons/hardware_control/protocols/motion_controller.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -171,6 +171,7 @@ async def move_axes(
171171
position: Mapping[Axis, float],
172172
speed: Optional[float] = None,
173173
max_speeds: Optional[Dict[Axis, float]] = None,
174+
expect_stalls: bool = False,
174175
) -> None:
175176
"""Moves the effectors of the specified axis to the specified position.
176177
The effector of the x,y axis is the center of the carriage.

api/src/opentrons/legacy_commands/commands.py

Lines changed: 37 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -299,3 +299,40 @@ def move_to_disposal_location(
299299
"name": command_types.MOVE_TO_DISPOSAL_LOCATION,
300300
"payload": {"instrument": instrument, "location": location, "text": text},
301301
}
302+
303+
304+
def seal(
305+
instrument: InstrumentContext,
306+
location: Well,
307+
) -> command_types.SealCommand:
308+
location_text = stringify_location(location)
309+
text = f"Sealing to {location_text}"
310+
return {
311+
"name": command_types.SEAL,
312+
"payload": {"instrument": instrument, "location": location, "text": text},
313+
}
314+
315+
316+
def unseal(
317+
instrument: InstrumentContext,
318+
location: Well,
319+
) -> command_types.UnsealCommand:
320+
location_text = stringify_location(location)
321+
text = f"Unsealing from {location_text}"
322+
return {
323+
"name": command_types.UNSEAL,
324+
"payload": {"instrument": instrument, "location": location, "text": text},
325+
}
326+
327+
328+
def resin_tip_dispense(
329+
instrument: InstrumentContext,
330+
flow_rate: float | None,
331+
) -> command_types.PressurizeCommand:
332+
if flow_rate is None:
333+
flow_rate = 10 # The Protocol Engine default for Resin Tip Dispense
334+
text = f"Pressurize pipette to dispense from resin tip at {flow_rate}uL/s."
335+
return {
336+
"name": command_types.PRESSURIZE,
337+
"payload": {"instrument": instrument, "text": text},
338+
}

api/src/opentrons/legacy_commands/types.py

Lines changed: 39 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -43,6 +43,10 @@
4343
RETURN_TIP: Final = "command.RETURN_TIP"
4444
MOVE_TO: Final = "command.MOVE_TO"
4545
MOVE_TO_DISPOSAL_LOCATION: Final = "command.MOVE_TO_DISPOSAL_LOCATION"
46+
SEAL: Final = "command.SEAL"
47+
UNSEAL: Final = "command.UNSEAL"
48+
PRESSURIZE: Final = "command.PRESSURIZE"
49+
4650

4751
# Modules #
4852

@@ -535,11 +539,40 @@ class MoveLabwareCommandPayload(TextOnlyPayload):
535539
pass
536540

537541

542+
class SealCommandPayload(TextOnlyPayload):
543+
instrument: InstrumentContext
544+
location: Union[None, Location, Well]
545+
546+
547+
class UnsealCommandPayload(TextOnlyPayload):
548+
instrument: InstrumentContext
549+
location: Union[None, Location, Well]
550+
551+
552+
class PressurizeCommandPayload(TextOnlyPayload):
553+
instrument: InstrumentContext
554+
555+
538556
class MoveLabwareCommand(TypedDict):
539557
name: Literal["command.MOVE_LABWARE"]
540558
payload: MoveLabwareCommandPayload
541559

542560

561+
class SealCommand(TypedDict):
562+
name: Literal["command.SEAL"]
563+
payload: SealCommandPayload
564+
565+
566+
class UnsealCommand(TypedDict):
567+
name: Literal["command.UNSEAL"]
568+
payload: UnsealCommandPayload
569+
570+
571+
class PressurizeCommand(TypedDict):
572+
name: Literal["command.PRESSURIZE"]
573+
payload: PressurizeCommandPayload
574+
575+
543576
Command = Union[
544577
DropTipCommand,
545578
DropTipInDisposalLocationCommand,
@@ -588,6 +621,9 @@ class MoveLabwareCommand(TypedDict):
588621
MoveToCommand,
589622
MoveToDisposalLocationCommand,
590623
MoveLabwareCommand,
624+
SealCommand,
625+
UnsealCommand,
626+
PressurizeCommand,
591627
]
592628

593629

@@ -637,6 +673,9 @@ class MoveLabwareCommand(TypedDict):
637673
MoveToCommandPayload,
638674
MoveToDisposalLocationCommandPayload,
639675
MoveLabwareCommandPayload,
676+
SealCommandPayload,
677+
UnsealCommandPayload,
678+
PressurizeCommandPayload,
640679
]
641680

642681

api/src/opentrons/protocol_api/core/engine/instrument.py

Lines changed: 109 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -61,6 +61,8 @@
6161
from opentrons.protocol_api._liquid_properties import TransferProperties
6262

6363
_DISPENSE_VOLUME_VALIDATION_ADDED_IN = APIVersion(2, 17)
64+
_RESIN_TIP_DEFAULT_VOLUME = 400
65+
_RESIN_TIP_DEFAULT_FLOW_RATE = 10.0
6466

6567

6668
class InstrumentCore(AbstractInstrument[WellCore, LabwareCore]):
@@ -710,6 +712,113 @@ def move_to(
710712
location=location, mount=self.get_mount()
711713
)
712714

715+
def resin_tip_seal(
716+
self, location: Location, well_core: WellCore, in_place: Optional[bool] = False
717+
) -> None:
718+
labware_id = well_core.labware_id
719+
well_name = well_core.get_name()
720+
well_location = (
721+
self._engine_client.state.geometry.get_relative_pick_up_tip_well_location(
722+
labware_id=labware_id,
723+
well_name=well_name,
724+
absolute_point=location.point,
725+
)
726+
)
727+
728+
self._engine_client.execute_command(
729+
cmd.EvotipSealPipetteParams(
730+
pipetteId=self._pipette_id,
731+
labwareId=labware_id,
732+
wellName=well_name,
733+
wellLocation=well_location,
734+
)
735+
)
736+
737+
def resin_tip_unseal(self, location: Location, well_core: WellCore) -> None:
738+
well_name = well_core.get_name()
739+
labware_id = well_core.labware_id
740+
741+
if location is not None:
742+
relative_well_location = (
743+
self._engine_client.state.geometry.get_relative_well_location(
744+
labware_id=labware_id,
745+
well_name=well_name,
746+
absolute_point=location.point,
747+
)
748+
)
749+
750+
well_location = DropTipWellLocation(
751+
origin=DropTipWellOrigin(relative_well_location.origin.value),
752+
offset=relative_well_location.offset,
753+
)
754+
else:
755+
well_location = DropTipWellLocation()
756+
757+
pipette_movement_conflict.check_safe_for_pipette_movement(
758+
engine_state=self._engine_client.state,
759+
pipette_id=self._pipette_id,
760+
labware_id=labware_id,
761+
well_name=well_name,
762+
well_location=well_location,
763+
)
764+
self._engine_client.execute_command(
765+
cmd.EvotipUnsealPipetteParams(
766+
pipetteId=self._pipette_id,
767+
labwareId=labware_id,
768+
wellName=well_name,
769+
wellLocation=well_location,
770+
)
771+
)
772+
773+
self._protocol_core.set_last_location(location=location, mount=self.get_mount())
774+
775+
def resin_tip_dispense(
776+
self,
777+
location: Location,
778+
well_core: WellCore,
779+
volume: Optional[float] = None,
780+
flow_rate: Optional[float] = None,
781+
) -> None:
782+
"""
783+
Args:
784+
volume: The volume of liquid to dispense, in microliters. Defaults to 400uL.
785+
location: The exact location to dispense to.
786+
well_core: The well to dispense to, if applicable.
787+
flow_rate: The flow rate in µL/s to dispense at. Defaults to 10.0uL/S.
788+
"""
789+
if isinstance(location, (TrashBin, WasteChute)):
790+
raise ValueError("Trash Bin and Waste Chute have no Wells.")
791+
well_name = well_core.get_name()
792+
labware_id = well_core.labware_id
793+
if volume is None:
794+
volume = _RESIN_TIP_DEFAULT_VOLUME
795+
if flow_rate is None:
796+
flow_rate = _RESIN_TIP_DEFAULT_FLOW_RATE
797+
798+
well_location = self._engine_client.state.geometry.get_relative_liquid_handling_well_location(
799+
labware_id=labware_id,
800+
well_name=well_name,
801+
absolute_point=location.point,
802+
is_meniscus=None,
803+
)
804+
pipette_movement_conflict.check_safe_for_pipette_movement(
805+
engine_state=self._engine_client.state,
806+
pipette_id=self._pipette_id,
807+
labware_id=labware_id,
808+
well_name=well_name,
809+
well_location=well_location,
810+
)
811+
self._engine_client.execute_command(
812+
cmd.EvotipDispenseParams(
813+
pipetteId=self._pipette_id,
814+
labwareId=labware_id,
815+
wellName=well_name,
816+
wellLocation=well_location,
817+
volume=volume,
818+
flowRate=flow_rate,
819+
)
820+
)
821+
713822
def get_mount(self) -> Mount:
714823
"""Get the mount the pipette is attached to."""
715824
return self._engine_client.state.pipettes.get(

api/src/opentrons/protocol_api/core/instrument.py

Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -189,6 +189,33 @@ def move_to(
189189
) -> None:
190190
...
191191

192+
@abstractmethod
193+
def resin_tip_seal(
194+
self,
195+
location: types.Location,
196+
well_core: WellCoreType,
197+
in_place: Optional[bool] = False,
198+
) -> None:
199+
...
200+
201+
@abstractmethod
202+
def resin_tip_unseal(
203+
self,
204+
location: types.Location,
205+
well_core: WellCoreType,
206+
) -> None:
207+
...
208+
209+
@abstractmethod
210+
def resin_tip_dispense(
211+
self,
212+
location: types.Location,
213+
well_core: WellCoreType,
214+
volume: Optional[float] = None,
215+
flow_rate: Optional[float] = None,
216+
) -> None:
217+
...
218+
192219
@abstractmethod
193220
def get_mount(self) -> types.Mount:
194221
...

0 commit comments

Comments
 (0)