Skip to content

Commit 3d78c1f

Browse files
authored
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 85c4e96 commit 3d78c1f

File tree

33 files changed

+2260
-29
lines changed

33 files changed

+2260
-29
lines changed

api/src/opentrons/hardware_control/api.py

+5-1
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.
@@ -1248,7 +1249,10 @@ async def pick_up_tip(
12481249
await self.prepare_for_aspirate(mount)
12491250

12501251
async def tip_drop_moves(
1251-
self, mount: top_types.Mount, home_after: bool = True
1252+
self,
1253+
mount: top_types.Mount,
1254+
home_after: bool = True,
1255+
ignore_plunger: bool = False,
12521256
) -> None:
12531257
spec, _ = self.plan_check_drop_tip(mount, home_after)
12541258

api/src/opentrons/hardware_control/ot3api.py

+18-8
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:
@@ -2320,11 +2325,16 @@ def set_working_volume(
23202325
instrument.working_volume = tip_volume
23212326

23222327
async def tip_drop_moves(
2323-
self, mount: Union[top_types.Mount, OT3Mount], home_after: bool = False
2328+
self,
2329+
mount: Union[top_types.Mount, OT3Mount],
2330+
home_after: bool = False,
2331+
ignore_plunger: bool = False,
23242332
) -> None:
23252333
realmount = OT3Mount.from_mount(mount)
2326-
2327-
await self._move_to_plunger_bottom(realmount, rate=1.0, check_current_vol=False)
2334+
if ignore_plunger is False:
2335+
await self._move_to_plunger_bottom(
2336+
realmount, rate=1.0, check_current_vol=False
2337+
)
23282338

23292339
if self.gantry_load == GantryLoad.HIGH_THROUGHPUT:
23302340
spec = self._pipette_handler.plan_ht_drop_tip()

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

+4-1
Original file line numberDiff line numberDiff line change
@@ -183,7 +183,10 @@ async def pick_up_tip(
183183
...
184184

185185
async def tip_drop_moves(
186-
self, mount: MountArgType, home_after: bool = True
186+
self,
187+
mount: MountArgType,
188+
home_after: bool = True,
189+
ignore_plunger: bool = False,
187190
) -> None:
188191
...
189192

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

+1
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

+37
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

+39
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

+109
Original file line numberDiff line numberDiff line change
@@ -48,6 +48,8 @@
4848
from opentrons.protocol_api._liquid import LiquidClass
4949

5050
_DISPENSE_VOLUME_VALIDATION_ADDED_IN = APIVersion(2, 17)
51+
_RESIN_TIP_DEFAULT_VOLUME = 400
52+
_RESIN_TIP_DEFAULT_FLOW_RATE = 10.0
5153

5254

5355
class InstrumentCore(AbstractInstrument[WellCore]):
@@ -678,6 +680,113 @@ def move_to(
678680
location=location, mount=self.get_mount()
679681
)
680682

683+
def resin_tip_seal(
684+
self, location: Location, well_core: WellCore, in_place: Optional[bool] = False
685+
) -> None:
686+
labware_id = well_core.labware_id
687+
well_name = well_core.get_name()
688+
well_location = (
689+
self._engine_client.state.geometry.get_relative_pick_up_tip_well_location(
690+
labware_id=labware_id,
691+
well_name=well_name,
692+
absolute_point=location.point,
693+
)
694+
)
695+
696+
self._engine_client.execute_command(
697+
cmd.EvotipSealPipetteParams(
698+
pipetteId=self._pipette_id,
699+
labwareId=labware_id,
700+
wellName=well_name,
701+
wellLocation=well_location,
702+
)
703+
)
704+
705+
def resin_tip_unseal(self, location: Location, well_core: WellCore) -> None:
706+
well_name = well_core.get_name()
707+
labware_id = well_core.labware_id
708+
709+
if location is not None:
710+
relative_well_location = (
711+
self._engine_client.state.geometry.get_relative_well_location(
712+
labware_id=labware_id,
713+
well_name=well_name,
714+
absolute_point=location.point,
715+
)
716+
)
717+
718+
well_location = DropTipWellLocation(
719+
origin=DropTipWellOrigin(relative_well_location.origin.value),
720+
offset=relative_well_location.offset,
721+
)
722+
else:
723+
well_location = DropTipWellLocation()
724+
725+
pipette_movement_conflict.check_safe_for_pipette_movement(
726+
engine_state=self._engine_client.state,
727+
pipette_id=self._pipette_id,
728+
labware_id=labware_id,
729+
well_name=well_name,
730+
well_location=well_location,
731+
)
732+
self._engine_client.execute_command(
733+
cmd.EvotipUnsealPipetteParams(
734+
pipetteId=self._pipette_id,
735+
labwareId=labware_id,
736+
wellName=well_name,
737+
wellLocation=well_location,
738+
)
739+
)
740+
741+
self._protocol_core.set_last_location(location=location, mount=self.get_mount())
742+
743+
def resin_tip_dispense(
744+
self,
745+
location: Location,
746+
well_core: WellCore,
747+
volume: Optional[float] = None,
748+
flow_rate: Optional[float] = None,
749+
) -> None:
750+
"""
751+
Args:
752+
volume: The volume of liquid to dispense, in microliters. Defaults to 400uL.
753+
location: The exact location to dispense to.
754+
well_core: The well to dispense to, if applicable.
755+
flow_rate: The flow rate in µL/s to dispense at. Defaults to 10.0uL/S.
756+
"""
757+
if isinstance(location, (TrashBin, WasteChute)):
758+
raise ValueError("Trash Bin and Waste Chute have no Wells.")
759+
well_name = well_core.get_name()
760+
labware_id = well_core.labware_id
761+
if volume is None:
762+
volume = _RESIN_TIP_DEFAULT_VOLUME
763+
if flow_rate is None:
764+
flow_rate = _RESIN_TIP_DEFAULT_FLOW_RATE
765+
766+
well_location = self._engine_client.state.geometry.get_relative_liquid_handling_well_location(
767+
labware_id=labware_id,
768+
well_name=well_name,
769+
absolute_point=location.point,
770+
is_meniscus=None,
771+
)
772+
pipette_movement_conflict.check_safe_for_pipette_movement(
773+
engine_state=self._engine_client.state,
774+
pipette_id=self._pipette_id,
775+
labware_id=labware_id,
776+
well_name=well_name,
777+
well_location=well_location,
778+
)
779+
self._engine_client.execute_command(
780+
cmd.EvotipDispenseParams(
781+
pipetteId=self._pipette_id,
782+
labwareId=labware_id,
783+
wellName=well_name,
784+
wellLocation=well_location,
785+
volume=volume,
786+
flowRate=flow_rate,
787+
)
788+
)
789+
681790
def get_mount(self) -> Mount:
682791
"""Get the mount the pipette is attached to."""
683792
return self._engine_client.state.pipettes.get(

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

+27
Original file line numberDiff line numberDiff line change
@@ -180,6 +180,33 @@ def move_to(
180180
) -> None:
181181
...
182182

183+
@abstractmethod
184+
def resin_tip_seal(
185+
self,
186+
location: types.Location,
187+
well_core: WellCoreType,
188+
in_place: Optional[bool] = False,
189+
) -> None:
190+
...
191+
192+
@abstractmethod
193+
def resin_tip_unseal(
194+
self,
195+
location: types.Location,
196+
well_core: WellCoreType,
197+
) -> None:
198+
...
199+
200+
@abstractmethod
201+
def resin_tip_dispense(
202+
self,
203+
location: types.Location,
204+
well_core: WellCoreType,
205+
volume: Optional[float] = None,
206+
flow_rate: Optional[float] = None,
207+
) -> None:
208+
...
209+
183210
@abstractmethod
184211
def get_mount(self) -> types.Mount:
185212
...

0 commit comments

Comments
 (0)