Skip to content

Commit 3b574aa

Browse files
ahiuchingaucaila-marashaj
authored andcommitted
feat(api): Flex Stacker Module Support for EVT (#17300)
Covers EXEC-967, EXEC-965, EXEC-946, EXEC-1078 This PR introduces the .store(), .retrieve(), and .load_labware_to_hopper(...) commands. These commands allow the declaration of labware inside the hopper, retrieval of a handleable labware core from the stacker, and storage of labware into the stacker.
1 parent ac117dd commit 3b574aa

33 files changed

+1125
-72
lines changed

analyses-snapshot-testing/tests/__snapshots__/analyses_snapshot_test/test_analysis_snapshot[6126498df7][Flex_X_v2_16_NO_PIPETTES_TM_ModuleInStagingAreaCol4].json

+4-4
Original file line numberDiff line numberDiff line change
@@ -25,7 +25,7 @@
2525
"errors": [
2626
{
2727
"createdAt": "TIMESTAMP",
28-
"detail": "ValueError [line 15]: Cannot load a module onto a staging slot.",
28+
"detail": "ValueError [line 15]: Cannot load temperature module gen2 onto a staging slot.",
2929
"errorCode": "4000",
3030
"errorInfo": {},
3131
"errorType": "ExceptionInProtocolError",
@@ -34,12 +34,12 @@
3434
"wrappedErrors": [
3535
{
3636
"createdAt": "TIMESTAMP",
37-
"detail": "ValueError: Cannot load a module onto a staging slot.",
37+
"detail": "ValueError: Cannot load temperature module gen2 onto a staging slot.",
3838
"errorCode": "4000",
3939
"errorInfo": {
40-
"args": "('Cannot load a module onto a staging slot.',)",
40+
"args": "('Cannot load temperature module gen2 onto a staging slot.',)",
4141
"class": "ValueError",
42-
"traceback": " File \"/usr/local/lib/python3.10/site-packages/opentrons/protocols/execution/execute_python.py\", line N, in exec_run\n exec(\"run(__context)\", new_globs)\n\n File \"<string>\", line N, in <module>\n\n File \"Flex_X_v2_16_NO_PIPETTES_TM_ModuleInStagingAreaCol4.py\", line N, in run\n\n File \"/usr/local/lib/python3.10/site-packages/opentrons/protocols/api_support/util.py\", line N, in _check_version_wrapper\n return decorated_obj(*args, **kwargs)\n\n File \"/usr/local/lib/python3.10/site-packages/opentrons/protocol_api/protocol_context.py\", line N, in load_module\n raise ValueError(\"Cannot load a module onto a staging slot.\")\n"
42+
"traceback": " File \"/usr/local/lib/python3.10/site-packages/opentrons/protocols/execution/execute_python.py\", line N, in exec_run\n exec(\"run(__context)\", new_globs)\n\n File \"<string>\", line N, in <module>\n\n File \"Flex_X_v2_16_NO_PIPETTES_TM_ModuleInStagingAreaCol4.py\", line N, in run\n\n File \"/usr/local/lib/python3.10/site-packages/opentrons/protocols/api_support/util.py\", line N, in _check_version_wrapper\n return decorated_obj(*args, **kwargs)\n\n File \"/usr/local/lib/python3.10/site-packages/opentrons/protocol_api/protocol_context.py\", line N, in load_module\n raise ValueError(f\"Cannot load {module_name} onto a staging slot.\")\n"
4343
},
4444
"errorType": "PythonException",
4545
"id": "UUID",

api/src/opentrons/protocol_api/__init__.py

+2
Original file line numberDiff line numberDiff line change
@@ -27,6 +27,7 @@
2727
HeaterShakerContext,
2828
MagneticBlockContext,
2929
AbsorbanceReaderContext,
30+
FlexStackerContext,
3031
)
3132
from .disposal_locations import TrashBin, WasteChute
3233
from ._liquid import Liquid, LiquidClass
@@ -70,6 +71,7 @@
7071
"HeaterShakerContext",
7172
"MagneticBlockContext",
7273
"AbsorbanceReaderContext",
74+
"FlexStackerContext",
7375
"ParameterContext",
7476
"Labware",
7577
"TrashBin",

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

+3
Original file line numberDiff line numberDiff line change
@@ -300,6 +300,9 @@ def _map_module(
300300
is_semi_configuration=False,
301301
),
302302
)
303+
elif module_type == ModuleType.FLEX_STACKER:
304+
# TODO: This is a placeholder. We need to implement this.
305+
return None
303306
else:
304307
return (
305308
mapped_location,

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

+16-2
Original file line numberDiff line numberDiff line change
@@ -700,16 +700,30 @@ class FlexStackerCore(ModuleCore, AbstractFlexStackerCore):
700700

701701
_sync_module_hardware: SynchronousAdapter[hw_modules.FlexStacker]
702702

703+
def set_static_mode(self, static: bool) -> None:
704+
"""Set the Flex Stacker's static mode.
705+
706+
The Flex Stacker cannot retrieve and or store when in static mode.
707+
This allows the Flex Stacker carriage to be used as a staging slot,
708+
and allowed the labware to be loaded onto it.
709+
"""
710+
self._engine_client.execute_command(
711+
cmd.flex_stacker.ConfigureParams(
712+
moduleId=self.module_id,
713+
static=static,
714+
)
715+
)
716+
703717
def retrieve(self) -> None:
704-
"""Retrieve a labware from the bottom of the Flex Stacker's stack."""
718+
"""Retrieve a labware from the Flex Stacker's hopper."""
705719
self._engine_client.execute_command(
706720
cmd.flex_stacker.RetrieveParams(
707721
moduleId=self.module_id,
708722
)
709723
)
710724

711725
def store(self) -> None:
712-
"""Store a labware at the bottom of the Flex Stacker's stack."""
726+
"""Store a labware into Flex Stacker's hopper."""
713727
self._engine_client.execute_command(
714728
cmd.flex_stacker.StoreParams(
715729
moduleId=self.module_id,

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

+30
Original file line numberDiff line numberDiff line change
@@ -75,6 +75,7 @@
7575
NonConnectedModuleCore,
7676
MagneticBlockCore,
7777
AbsorbanceReaderCore,
78+
FlexStackerCore,
7879
)
7980
from .exceptions import InvalidModuleLocationError
8081
from . import load_labware_params, deck_conflict, overlap_versions
@@ -373,6 +374,34 @@ def load_lid(
373374
self._labware_cores_by_id[labware_core.labware_id] = labware_core
374375
return labware_core
375376

377+
def load_labware_to_flex_stacker_hopper(
378+
self,
379+
module_core: Union[ModuleCore, NonConnectedModuleCore],
380+
load_name: str,
381+
quantity: int,
382+
label: Optional[str],
383+
namespace: Optional[str],
384+
version: Optional[int],
385+
lid: Optional[str],
386+
) -> None:
387+
"""Load one or more labware with or without a lid to the flex stacker hopper."""
388+
assert isinstance(module_core, FlexStackerCore)
389+
for _ in range(quantity):
390+
labware_core = self.load_labware(
391+
load_name=load_name,
392+
location=module_core,
393+
label=label,
394+
namespace=namespace,
395+
version=version,
396+
)
397+
if lid is not None:
398+
self.load_lid(
399+
load_name=lid,
400+
location=labware_core,
401+
namespace=namespace,
402+
version=version,
403+
)
404+
376405
def move_labware(
377406
self,
378407
labware_core: LabwareCore,
@@ -726,6 +755,7 @@ def _create_module_core(
726755
ModuleType.THERMOCYCLER: ThermocyclerModuleCore,
727756
ModuleType.HEATER_SHAKER: HeaterShakerModuleCore,
728757
ModuleType.ABSORBANCE_READER: AbsorbanceReaderCore,
758+
ModuleType.FLEX_STACKER: FlexStackerCore,
729759
}
730760

731761
module_type = load_module_result.model.as_type()

api/src/opentrons/protocol_api/core/legacy/legacy_protocol_core.py

+13
Original file line numberDiff line numberDiff line change
@@ -524,6 +524,19 @@ def load_lid_stack(
524524
"""Load a Stack of Lids to a given location, creating a Lid Stack."""
525525
raise APIVersionError(api_element="Lid stack")
526526

527+
def load_labware_to_flex_stacker_hopper(
528+
self,
529+
module_core: legacy_module_core.LegacyModuleCore,
530+
load_name: str,
531+
quantity: int,
532+
label: Optional[str],
533+
namespace: Optional[str],
534+
version: Optional[int],
535+
lid: Optional[str],
536+
) -> None:
537+
"""Load labware to a Flex stacker hopper."""
538+
raise APIVersionError(api_element="Flex stacker")
539+
527540
def get_module_cores(self) -> List[legacy_module_core.LegacyModuleCore]:
528541
"""Get loaded module cores."""
529542
return self._module_cores

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

+6-3
Original file line numberDiff line numberDiff line change
@@ -390,11 +390,14 @@ class AbstractFlexStackerCore(AbstractModuleCore):
390390
def get_serial_number(self) -> str:
391391
"""Get the module's unique hardware serial number."""
392392

393+
@abstractmethod
394+
def set_static_mode(self, static: bool) -> None:
395+
"""Set the Flex Stacker's static mode."""
396+
393397
@abstractmethod
394398
def retrieve(self) -> None:
395-
"""Release and return a labware at the bottom of the labware stack."""
399+
"""Release a labware from the hopper to the staging slot."""
396400

397401
@abstractmethod
398402
def store(self) -> None:
399-
"""Store a labware at the bottom of the labware stack."""
400-
pass
403+
"""Store a labware in the stacker hopper."""

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

+14
Original file line numberDiff line numberDiff line change
@@ -111,6 +111,20 @@ def load_lid(
111111
"""Load an individual lid labware using its identifying parameters. Must be loaded on a labware."""
112112
...
113113

114+
@abstractmethod
115+
def load_labware_to_flex_stacker_hopper(
116+
self,
117+
module_core: ModuleCoreType,
118+
load_name: str,
119+
quantity: int,
120+
label: Optional[str],
121+
namespace: Optional[str],
122+
version: Optional[int],
123+
lid: Optional[str],
124+
) -> None:
125+
"""Load one or more labware with or without a lid to the flex stacker hopper."""
126+
...
127+
114128
@abstractmethod
115129
def move_labware(
116130
self,

api/src/opentrons/protocol_api/module_contexts.py

+59-1
Original file line numberDiff line numberDiff line change
@@ -1112,21 +1112,79 @@ class FlexStackerContext(ModuleContext):
11121112

11131113
_core: FlexStackerCore
11141114

1115+
@requires_version(2, 23)
1116+
def load_labware_to_hopper(
1117+
self,
1118+
load_name: str,
1119+
quantity: int,
1120+
label: Optional[str] = None,
1121+
namespace: Optional[str] = None,
1122+
version: Optional[int] = None,
1123+
lid: Optional[str] = None,
1124+
) -> None:
1125+
"""Load one or more labware onto the flex stacker."""
1126+
self._protocol_core.load_labware_to_flex_stacker_hopper(
1127+
module_core=self._core,
1128+
load_name=load_name,
1129+
quantity=quantity,
1130+
label=label,
1131+
namespace=namespace,
1132+
version=version,
1133+
lid=lid,
1134+
)
1135+
1136+
@requires_version(2, 23)
1137+
def enter_static_mode(self) -> None:
1138+
"""Enter static mode.
1139+
1140+
In static mode, the Flex Stacker will not move labware between the hopper and
1141+
the deck, and can be used as a staging slot area.
1142+
"""
1143+
self._core.set_static_mode(static=True)
1144+
1145+
@requires_version(2, 23)
1146+
def exit_static_mode(self) -> None:
1147+
"""End static mode.
1148+
1149+
In static mode, the Flex Stacker will not move labware between the hopper and
1150+
the deck, and can be used as a staging slot area.
1151+
"""
1152+
self._core.set_static_mode(static=False)
1153+
11151154
@property
11161155
@requires_version(2, 23)
11171156
def serial_number(self) -> str:
11181157
"""Get the module's unique hardware serial number."""
11191158
return self._core.get_serial_number()
11201159

11211160
@requires_version(2, 23)
1122-
def retrieve(self) -> None:
1161+
def retrieve(self) -> Labware:
11231162
"""Release and return a labware at the bottom of the labware stack."""
11241163
self._core.retrieve()
1164+
labware_core = self._protocol_core.get_labware_on_module(self._core)
1165+
# the core retrieve command should have already raised the error
1166+
# if labware_core is None, this is just to satisfy the type checker
1167+
assert labware_core is not None, "Retrieve failed to return labware"
1168+
# check core map first
1169+
try:
1170+
labware = self._core_map.get(labware_core)
1171+
except KeyError:
1172+
# If the labware is not already in the core map,
1173+
# create a new Labware object
1174+
labware = Labware(
1175+
core=labware_core,
1176+
api_version=self._api_version,
1177+
protocol_core=self._protocol_core,
1178+
core_map=self._core_map,
1179+
)
1180+
self._core_map.add(labware_core, labware)
1181+
return labware
11251182

11261183
@requires_version(2, 23)
11271184
def store(self, labware: Labware) -> None:
11281185
"""Store a labware at the bottom of the labware stack.
11291186
11301187
:param labware: The labware object to store.
11311188
"""
1189+
assert labware._core is not None
11321190
self._core.store()

api/src/opentrons/protocol_api/protocol_context.py

+26-2
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,7 @@
2222
from opentrons.hardware_control.modules.types import (
2323
MagneticBlockModel,
2424
AbsorbanceReaderModel,
25+
FlexStackerModuleModel,
2526
)
2627
from opentrons.legacy_commands import protocol_commands as cmds, types as cmd_types
2728
from opentrons.legacy_commands.helpers import (
@@ -61,6 +62,7 @@
6162
AbstractHeaterShakerCore,
6263
AbstractMagneticBlockCore,
6364
AbstractAbsorbanceReaderCore,
65+
AbstractFlexStackerCore,
6466
)
6567
from .robot_context import RobotContext, HardwareManager
6668
from .core.engine import ENGINE_CORE_API_VERSION
@@ -79,6 +81,7 @@
7981
HeaterShakerContext,
8082
MagneticBlockContext,
8183
AbsorbanceReaderContext,
84+
FlexStackerContext,
8285
ModuleContext,
8386
)
8487
from ._parameters import Parameters
@@ -94,6 +97,7 @@
9497
HeaterShakerContext,
9598
MagneticBlockContext,
9699
AbsorbanceReaderContext,
100+
FlexStackerContext,
97101
]
98102

99103

@@ -862,6 +866,9 @@ def load_module(
862866
863867
.. versionchanged:: 2.15
864868
Added ``MagneticBlockContext`` return value.
869+
870+
.. versionchanged:: 2.23
871+
Added ``FlexStackerModuleContext`` return value.
865872
"""
866873
if configuration:
867874
if self._api_version < APIVersion(2, 4):
@@ -890,7 +897,18 @@ def load_module(
890897
requested_model, AbsorbanceReaderModel
891898
) and self._api_version < APIVersion(2, 21):
892899
raise APIVersionError(
893-
f"Module of type {module_name} is only available in versions 2.21 and above."
900+
api_element=f"Module of type {module_name}",
901+
until_version="2.21",
902+
current_version=f"{self._api_version}",
903+
)
904+
if (
905+
isinstance(requested_model, FlexStackerModuleModel)
906+
and self._api_version < validation.FLEX_STACKER_VERSION_GATE
907+
):
908+
raise APIVersionError(
909+
api_element=f"Module of type {module_name}",
910+
until_version=str(validation.FLEX_STACKER_VERSION_GATE),
911+
current_version=f"{self._api_version}",
894912
)
895913

896914
deck_slot = (
@@ -901,7 +919,11 @@ def load_module(
901919
)
902920
)
903921
if isinstance(deck_slot, StagingSlotName):
904-
raise ValueError("Cannot load a module onto a staging slot.")
922+
# flex stacker modules can only be loaded into staging slot inside a protocol
923+
if isinstance(requested_model, FlexStackerModuleModel):
924+
deck_slot = validation.convert_flex_stacker_load_slot(deck_slot)
925+
else:
926+
raise ValueError(f"Cannot load {module_name} onto a staging slot.")
905927

906928
module_core = self._core.load_module(
907929
model=requested_model,
@@ -1572,6 +1594,8 @@ def _create_module_context(
15721594
module_cls = MagneticBlockContext
15731595
elif isinstance(module_core, AbstractAbsorbanceReaderCore):
15741596
module_cls = AbsorbanceReaderContext
1597+
elif isinstance(module_core, AbstractFlexStackerCore):
1598+
module_cls = FlexStackerContext
15751599
else:
15761600
assert False, "Unsupported module type"
15771601

0 commit comments

Comments
 (0)