Skip to content

Commit cec46ef

Browse files
authored
feat(api): add stacker module context and protocol command skeletons (#17206)
This PR adds flex stacker support skeleton in the protocol engine. With the changes in this PR, you can load a flex stacker into your protocol.
1 parent 4d6dfc8 commit cec46ef

File tree

27 files changed

+626
-192
lines changed

27 files changed

+626
-192
lines changed

api/src/opentrons/hardware_control/modules/__init__.py

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -62,7 +62,6 @@
6262
"AbsorbanceReader",
6363
"AbsorbanceReaderStatus",
6464
"AbsorbanceReaderDisconnectedError",
65-
"ModuleDisconnectedCallback",
6665
"FlexStacker",
6766
"FlexStackerStatus",
6867
"PlatformState",

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

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@
1111
AbstractHeaterShakerCore,
1212
AbstractMagneticBlockCore,
1313
AbstractAbsorbanceReaderCore,
14+
AbstractFlexStackerCore,
1415
)
1516
from .protocol import AbstractProtocol
1617
from .well import AbstractWellCore
@@ -27,5 +28,6 @@
2728
HeaterShakerCore = AbstractHeaterShakerCore
2829
MagneticBlockCore = AbstractMagneticBlockCore
2930
AbsorbanceReaderCore = AbstractAbsorbanceReaderCore
31+
FlexStackerCore = AbstractFlexStackerCore
3032
RobotCore = AbstractRobot
3133
ProtocolCore = AbstractProtocol[InstrumentCore, LabwareCore, ModuleCore]

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

Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -37,6 +37,7 @@
3737
AbstractHeaterShakerCore,
3838
AbstractMagneticBlockCore,
3939
AbstractAbsorbanceReaderCore,
40+
AbstractFlexStackerCore,
4041
)
4142
from .exceptions import InvalidMagnetEngageHeightError
4243

@@ -692,3 +693,25 @@ def is_lid_on(self) -> bool:
692693
self.module_id
693694
)
694695
return abs_state.is_lid_on
696+
697+
698+
class FlexStackerCore(ModuleCore, AbstractFlexStackerCore):
699+
"""Flex Stacker core logic implementation for Python protocols."""
700+
701+
_sync_module_hardware: SynchronousAdapter[hw_modules.FlexStacker]
702+
703+
def retrieve(self) -> None:
704+
"""Retrieve a labware from the bottom of the Flex Stacker's stack."""
705+
self._engine_client.execute_command(
706+
cmd.flex_stacker.RetrieveParams(
707+
moduleId=self.module_id,
708+
)
709+
)
710+
711+
def store(self) -> None:
712+
"""Store a labware at the bottom of the Flex Stacker's stack."""
713+
self._engine_client.execute_command(
714+
cmd.flex_stacker.StoreParams(
715+
moduleId=self.module_id,
716+
)
717+
)

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

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -379,3 +379,22 @@ def open_lid(self) -> None:
379379
@abstractmethod
380380
def is_lid_on(self) -> bool:
381381
"""Return True if the Absorbance Reader's lid is currently closed."""
382+
383+
384+
class AbstractFlexStackerCore(AbstractModuleCore):
385+
"""Core control interface for an attached Flex Stacker."""
386+
387+
MODULE_TYPE: ClassVar = ModuleType.FLEX_STACKER
388+
389+
@abstractmethod
390+
def get_serial_number(self) -> str:
391+
"""Get the module's unique hardware serial number."""
392+
393+
@abstractmethod
394+
def retrieve(self) -> None:
395+
"""Release and return a labware at the bottom of the labware stack."""
396+
397+
@abstractmethod
398+
def store(self) -> None:
399+
"""Store a labware at the bottom of the labware stack."""
400+
pass

api/src/opentrons/protocol_api/module_contexts.py

Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -29,6 +29,7 @@
2929
HeaterShakerCore,
3030
MagneticBlockCore,
3131
AbsorbanceReaderCore,
32+
FlexStackerCore,
3233
)
3334
from .core.core_map import LoadedCoreMap
3435
from .core.engine import ENGINE_CORE_API_VERSION
@@ -1098,3 +1099,34 @@ def read(
10981099
:returns: A dictionary of wavelengths to dictionary of values ordered by well name.
10991100
"""
11001101
return self._core.read(filename=export_filename)
1102+
1103+
1104+
class FlexStackerContext(ModuleContext):
1105+
"""An object representing a connected Flex Stacker module.
1106+
1107+
It should not be instantiated directly; instead, it should be
1108+
created through :py:meth:`.ProtocolContext.load_module`.
1109+
1110+
.. versionadded:: 2.23
1111+
"""
1112+
1113+
_core: FlexStackerCore
1114+
1115+
@property
1116+
@requires_version(2, 23)
1117+
def serial_number(self) -> str:
1118+
"""Get the module's unique hardware serial number."""
1119+
return self._core.get_serial_number()
1120+
1121+
@requires_version(2, 23)
1122+
def retrieve(self) -> None:
1123+
"""Release and return a labware at the bottom of the labware stack."""
1124+
self._core.retrieve()
1125+
1126+
@requires_version(2, 23)
1127+
def store(self, labware: Labware) -> None:
1128+
"""Store a labware at the bottom of the labware stack.
1129+
1130+
:param labware: The labware object to store.
1131+
"""
1132+
self._core.store()

api/src/opentrons/protocol_engine/commands/__init__.py

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,7 @@
1414
"""
1515

1616
from . import absorbance_reader
17+
from . import flex_stacker
1718
from . import heater_shaker
1819
from . import magnetic_module
1920
from . import temperature_module
@@ -614,6 +615,7 @@
614615
# hardware control command models
615616
# hardware module command bundles
616617
"absorbance_reader",
618+
"flex_stacker",
617619
"heater_shaker",
618620
"magnetic_module",
619621
"temperature_module",

api/src/opentrons/protocol_engine/commands/command_unions.py

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,7 @@
1616
from .movement_common import StallOrCollisionError
1717

1818
from . import absorbance_reader
19+
from . import flex_stacker
1920
from . import heater_shaker
2021
from . import magnetic_module
2122
from . import temperature_module
@@ -430,6 +431,8 @@
430431
absorbance_reader.OpenLid,
431432
absorbance_reader.Initialize,
432433
absorbance_reader.ReadAbsorbance,
434+
flex_stacker.Retrieve,
435+
flex_stacker.Store,
433436
calibration.CalibrateGripper,
434437
calibration.CalibratePipette,
435438
calibration.CalibrateModule,
@@ -518,6 +521,8 @@
518521
absorbance_reader.OpenLidParams,
519522
absorbance_reader.InitializeParams,
520523
absorbance_reader.ReadAbsorbanceParams,
524+
flex_stacker.RetrieveParams,
525+
flex_stacker.StoreParams,
521526
calibration.CalibrateGripperParams,
522527
calibration.CalibratePipetteParams,
523528
calibration.CalibrateModuleParams,
@@ -604,6 +609,8 @@
604609
absorbance_reader.OpenLidCommandType,
605610
absorbance_reader.InitializeCommandType,
606611
absorbance_reader.ReadAbsorbanceCommandType,
612+
flex_stacker.RetrieveCommandType,
613+
flex_stacker.StoreCommandType,
607614
calibration.CalibrateGripperCommandType,
608615
calibration.CalibratePipetteCommandType,
609616
calibration.CalibrateModuleCommandType,
@@ -691,6 +698,8 @@
691698
absorbance_reader.OpenLidCreate,
692699
absorbance_reader.InitializeCreate,
693700
absorbance_reader.ReadAbsorbanceCreate,
701+
flex_stacker.RetrieveCreate,
702+
flex_stacker.StoreCreate,
694703
calibration.CalibrateGripperCreate,
695704
calibration.CalibratePipetteCreate,
696705
calibration.CalibrateModuleCreate,
@@ -786,6 +795,8 @@
786795
absorbance_reader.OpenLidResult,
787796
absorbance_reader.InitializeResult,
788797
absorbance_reader.ReadAbsorbanceResult,
798+
flex_stacker.RetrieveResult,
799+
flex_stacker.StoreResult,
789800
calibration.CalibrateGripperResult,
790801
calibration.CalibratePipetteResult,
791802
calibration.CalibrateModuleResult,
Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,33 @@
1+
"""Command models for Flex Stacker commands."""
2+
3+
from .store import (
4+
StoreCommandType,
5+
StoreParams,
6+
StoreResult,
7+
Store,
8+
StoreCreate,
9+
)
10+
11+
from .retrieve import (
12+
RetrieveCommandType,
13+
RetrieveParams,
14+
RetrieveResult,
15+
Retrieve,
16+
RetrieveCreate,
17+
)
18+
19+
20+
__all__ = [
21+
# flexStacker/store
22+
"StoreCommandType",
23+
"StoreParams",
24+
"StoreResult",
25+
"Store",
26+
"StoreCreate",
27+
# flexStacker/retrieve
28+
"RetrieveCommandType",
29+
"RetrieveParams",
30+
"RetrieveResult",
31+
"Retrieve",
32+
"RetrieveCreate",
33+
]
Lines changed: 77 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,77 @@
1+
"""Command models to retrieve a labware from a Flex Stacker."""
2+
from __future__ import annotations
3+
from typing import Optional, Literal, TYPE_CHECKING
4+
from typing_extensions import Type
5+
6+
from pydantic import BaseModel, Field
7+
8+
from ..command import AbstractCommandImpl, BaseCommand, BaseCommandCreate, SuccessData
9+
from ...errors.error_occurrence import ErrorOccurrence
10+
from ...state import update_types
11+
12+
if TYPE_CHECKING:
13+
from opentrons.protocol_engine.state.state import StateView
14+
from opentrons.protocol_engine.execution import EquipmentHandler
15+
16+
RetrieveCommandType = Literal["flexStacker/retrieve"]
17+
18+
19+
class RetrieveParams(BaseModel):
20+
"""Input parameters for a labware retrieval command."""
21+
22+
moduleId: str = Field(
23+
...,
24+
description="Unique ID of the Flex Stacker.",
25+
)
26+
27+
28+
class RetrieveResult(BaseModel):
29+
"""Result data from a labware retrieval command."""
30+
31+
32+
class RetrieveImpl(AbstractCommandImpl[RetrieveParams, SuccessData[RetrieveResult]]):
33+
"""Implementation of a labware retrieval command."""
34+
35+
def __init__(
36+
self,
37+
state_view: StateView,
38+
equipment: EquipmentHandler,
39+
**kwargs: object,
40+
) -> None:
41+
self._state_view = state_view
42+
self._equipment = equipment
43+
44+
async def execute(self, params: RetrieveParams) -> SuccessData[RetrieveResult]:
45+
"""Execute the labware retrieval command."""
46+
state_update = update_types.StateUpdate()
47+
stacker_substate = self._state_view.modules.get_flex_stacker_substate(
48+
module_id=params.moduleId
49+
)
50+
51+
# Allow propagation of ModuleNotAttachedError.
52+
stacker = self._equipment.get_module_hardware_api(stacker_substate.module_id)
53+
54+
if stacker is not None:
55+
# TODO: get labware height from labware state view
56+
await stacker.dispense_labware(labware_height=50.0)
57+
58+
return SuccessData(public=RetrieveResult(), state_update=state_update)
59+
60+
61+
class Retrieve(BaseCommand[RetrieveParams, RetrieveResult, ErrorOccurrence]):
62+
"""A command to retrieve a labware from a Flex Stacker."""
63+
64+
commandType: RetrieveCommandType = "flexStacker/retrieve"
65+
params: RetrieveParams
66+
result: Optional[RetrieveResult]
67+
68+
_ImplementationCls: Type[RetrieveImpl] = RetrieveImpl
69+
70+
71+
class RetrieveCreate(BaseCommandCreate[RetrieveParams]):
72+
"""A request to execute a Flex Stacker retrieve command."""
73+
74+
commandType: RetrieveCommandType = "flexStacker/retrieve"
75+
params: RetrieveParams
76+
77+
_CommandCls: Type[Retrieve] = Retrieve
Lines changed: 78 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,78 @@
1+
"""Command models to retrieve a labware from a Flex Stacker."""
2+
from __future__ import annotations
3+
from typing import Optional, Literal, TYPE_CHECKING
4+
from typing_extensions import Type
5+
6+
from pydantic import BaseModel, Field
7+
8+
from ..command import AbstractCommandImpl, BaseCommand, BaseCommandCreate, SuccessData
9+
from ...errors.error_occurrence import ErrorOccurrence
10+
from ...state import update_types
11+
12+
if TYPE_CHECKING:
13+
from opentrons.protocol_engine.state.state import StateView
14+
from opentrons.protocol_engine.execution import EquipmentHandler
15+
16+
17+
StoreCommandType = Literal["flexStacker/store"]
18+
19+
20+
class StoreParams(BaseModel):
21+
"""Input parameters for a labware storage command."""
22+
23+
moduleId: str = Field(
24+
...,
25+
description="Unique ID of the flex stacker.",
26+
)
27+
28+
29+
class StoreResult(BaseModel):
30+
"""Result data from a labware storage command."""
31+
32+
33+
class StoreImpl(AbstractCommandImpl[StoreParams, SuccessData[StoreResult]]):
34+
"""Implementation of a labware storage command."""
35+
36+
def __init__(
37+
self,
38+
state_view: StateView,
39+
equipment: EquipmentHandler,
40+
**kwargs: object,
41+
) -> None:
42+
self._state_view = state_view
43+
self._equipment = equipment
44+
45+
async def execute(self, params: StoreParams) -> SuccessData[StoreResult]:
46+
"""Execute the labware storage command."""
47+
state_update = update_types.StateUpdate()
48+
stacker_substate = self._state_view.modules.get_flex_stacker_substate(
49+
module_id=params.moduleId
50+
)
51+
52+
# Allow propagation of ModuleNotAttachedError.
53+
stacker = self._equipment.get_module_hardware_api(stacker_substate.module_id)
54+
55+
if stacker is not None:
56+
# TODO: get labware height from labware state view
57+
await stacker.store_labware(labware_height=50.0)
58+
59+
return SuccessData(public=StoreResult(), state_update=state_update)
60+
61+
62+
class Store(BaseCommand[StoreParams, StoreResult, ErrorOccurrence]):
63+
"""A command to store a labware in a Flex Stacker."""
64+
65+
commandType: StoreCommandType = "flexStacker/store"
66+
params: StoreParams
67+
result: Optional[StoreResult]
68+
69+
_ImplementationCls: Type[StoreImpl] = StoreImpl
70+
71+
72+
class StoreCreate(BaseCommandCreate[StoreParams]):
73+
"""A request to execute a Flex Stacker store command."""
74+
75+
commandType: StoreCommandType = "flexStacker/store"
76+
params: StoreParams
77+
78+
_CommandCls: Type[Store] = Store

0 commit comments

Comments
 (0)