Skip to content

Commit 2527891

Browse files
committed
add protocol engine stacker home command
1 parent 85a8fc9 commit 2527891

File tree

6 files changed

+334
-50
lines changed

6 files changed

+334
-50
lines changed

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

+15
Original file line numberDiff line numberDiff line change
@@ -416,6 +416,21 @@ async def _prepare_for_action(self) -> bool:
416416
await self.close_latch()
417417
return True
418418

419+
async def home_all(self) -> None:
420+
"""Home all axes based on current state, assuming normal operation."""
421+
await self._reader.read()
422+
# we should always be able to home the X axis first
423+
await self.home_axis(StackerAxis.X, Direction.RETRACT)
424+
# If latch is open, we must first close it
425+
if self.limit_switch_status[StackerAxis.L] == StackerAxisState.EXTENDED:
426+
if self.limit_switch_status[StackerAxis.Z] != StackerAxisState.RETRACTED:
427+
# it was likely in the middle of a dispense/store command
428+
# z should be moved up before we can safely close the latch
429+
await self.home_axis(StackerAxis.Z, Direction.EXTEND)
430+
await self.close_latch()
431+
await self.home_axis(StackerAxis.Z, Direction.RETRACT)
432+
await self.home_axis(StackerAxis.X, Direction.EXTEND)
433+
419434

420435
class FlexStackerReader(Reader):
421436
error: Optional[str]

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

+5
Original file line numberDiff line numberDiff line change
@@ -482,6 +482,7 @@
482482
flex_stacker.SetStoredLabware,
483483
flex_stacker.Fill,
484484
flex_stacker.Empty,
485+
flex_stacker.Home,
485486
calibration.CalibrateGripper,
486487
calibration.CalibratePipette,
487488
calibration.CalibrateModule,
@@ -580,6 +581,7 @@
580581
flex_stacker.SetStoredLabwareParams,
581582
flex_stacker.FillParams,
582583
flex_stacker.EmptyParams,
584+
flex_stacker.HomeParams,
583585
calibration.CalibrateGripperParams,
584586
calibration.CalibratePipetteParams,
585587
calibration.CalibrateModuleParams,
@@ -676,6 +678,7 @@
676678
flex_stacker.SetStoredLabwareCommandType,
677679
flex_stacker.FillCommandType,
678680
flex_stacker.EmptyCommandType,
681+
flex_stacker.HomeCommandType,
679682
calibration.CalibrateGripperCommandType,
680683
calibration.CalibratePipetteCommandType,
681684
calibration.CalibrateModuleCommandType,
@@ -773,6 +776,7 @@
773776
flex_stacker.SetStoredLabwareCreate,
774777
flex_stacker.FillCreate,
775778
flex_stacker.EmptyCreate,
779+
flex_stacker.HomeCreate,
776780
calibration.CalibrateGripperCreate,
777781
calibration.CalibratePipetteCreate,
778782
calibration.CalibrateModuleCreate,
@@ -878,6 +882,7 @@
878882
flex_stacker.SetStoredLabwareResult,
879883
flex_stacker.FillResult,
880884
flex_stacker.EmptyResult,
885+
flex_stacker.HomeResult,
881886
calibration.CalibrateGripperResult,
882887
calibration.CalibratePipetteResult,
883888
calibration.CalibrateModuleResult,

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

+8
Original file line numberDiff line numberDiff line change
@@ -29,6 +29,8 @@
2929

3030
from .empty import EmptyCommandType, EmptyParams, EmptyResult, Empty, EmptyCreate
3131

32+
from .home import HomeCommandType, HomeParams, HomeResult, Home, HomeCreate
33+
3234

3335
__all__ = [
3436
# flexStacker/store
@@ -62,4 +64,10 @@
6264
"EmptyResult",
6365
"Empty",
6466
"EmptyCreate",
67+
# flexStacker/home
68+
"HomeCommandType",
69+
"HomeParams",
70+
"HomeResult",
71+
"Home",
72+
"HomeCreate",
6573
]
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,105 @@
1+
"""Command models to home the stacker."""
2+
3+
from __future__ import annotations
4+
5+
from __future__ import annotations
6+
from typing import Literal, Union, TYPE_CHECKING
7+
from typing_extensions import Type
8+
9+
from pydantic import BaseModel, Field
10+
11+
from ..flex_stacker.common import FlexStackerStallOrCollisionError
12+
from opentrons_shared_data.errors.exceptions import FlexStackerStallError
13+
14+
from ..command import (
15+
AbstractCommandImpl,
16+
BaseCommand,
17+
BaseCommandCreate,
18+
SuccessData,
19+
DefinedErrorData,
20+
)
21+
from ...errors import ErrorOccurrence
22+
from ...resources import ModelUtils
23+
24+
if TYPE_CHECKING:
25+
from ...state.state import StateView
26+
from ...execution import EquipmentHandler
27+
28+
HomeCommandType = Literal["flexStacker/home"]
29+
30+
31+
class HomeParams(BaseModel):
32+
"""The parameters defining how a stacker should be emptied."""
33+
34+
moduleId: str = Field(..., description="Unique ID of the Flex Stacker")
35+
36+
37+
class HomeResult(BaseModel):
38+
"""Result data from a stacker empty command."""
39+
40+
41+
_ExecuteReturn = Union[
42+
SuccessData[HomeResult], DefinedErrorData[FlexStackerStallOrCollisionError]
43+
]
44+
45+
46+
class HomeImpl(AbstractCommandImpl[HomeParams, _ExecuteReturn]):
47+
"""Implementation of a stacker empty command."""
48+
49+
def __init__(
50+
self,
51+
state_view: StateView,
52+
equipment: EquipmentHandler,
53+
model_utils: ModelUtils,
54+
**kwargs: object,
55+
) -> None:
56+
self._state_view = state_view
57+
self._equipment = equipment
58+
self._model_utils = model_utils
59+
60+
async def execute(self, params: HomeParams) -> _ExecuteReturn:
61+
"""Execute the stacker empty command."""
62+
stacker_state = self._state_view.modules.get_flex_stacker_substate(
63+
params.moduleId
64+
)
65+
# Allow propagation of ModuleNotAttachedError.
66+
stacker_hw = self._equipment.get_module_hardware_api(stacker_state.module_id)
67+
68+
try:
69+
if stacker_hw is not None:
70+
await stacker_hw.home_all()
71+
except FlexStackerStallError as e:
72+
return DefinedErrorData(
73+
public=FlexStackerStallOrCollisionError(
74+
id=self._model_utils.generate_id(),
75+
createdAt=self._model_utils.get_timestamp(),
76+
wrappedErrors=[
77+
ErrorOccurrence.from_failed(
78+
id=self._model_utils.generate_id(),
79+
createdAt=self._model_utils.get_timestamp(),
80+
error=e,
81+
)
82+
],
83+
),
84+
)
85+
86+
return SuccessData(public=HomeResult())
87+
88+
89+
class Home(BaseCommand[HomeParams, HomeResult, ErrorOccurrence]):
90+
"""A command to home a Flex Stacker."""
91+
92+
commandType: HomeCommandType = "flexStacker/home"
93+
params: HomeParams
94+
result: HomeResult | None = None
95+
96+
_ImplementationCls: Type[HomeImpl] = HomeImpl
97+
98+
99+
class HomeCreate(BaseCommandCreate[HomeParams]):
100+
"""A request to execute a Flex Stacker home command."""
101+
102+
commandType: HomeCommandType = "flexStacker/home"
103+
params: HomeParams
104+
105+
_CommandCls: Type[Home] = Home
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,106 @@
1+
"""Test Flex Stacker home command implementation."""
2+
3+
from datetime import datetime
4+
5+
import pytest
6+
from decoy import Decoy, matchers
7+
8+
from opentrons.drivers.flex_stacker.types import StackerAxis
9+
from opentrons.hardware_control.modules import FlexStacker
10+
from opentrons.protocol_engine.commands.flex_stacker.common import (
11+
FlexStackerStallOrCollisionError,
12+
)
13+
from opentrons.protocol_engine.resources import ModelUtils
14+
from opentrons.protocol_engine.state.state import StateView
15+
from opentrons.protocol_engine.state.module_substates import (
16+
FlexStackerSubState,
17+
FlexStackerId,
18+
)
19+
from opentrons.protocol_engine.execution import EquipmentHandler
20+
from opentrons.protocol_engine.commands import flex_stacker
21+
from opentrons.protocol_engine.commands.command import SuccessData, DefinedErrorData
22+
from opentrons.protocol_engine.commands.flex_stacker.home import HomeImpl
23+
24+
from opentrons_shared_data.errors.exceptions import FlexStackerStallError
25+
26+
27+
@pytest.fixture
28+
def subject(
29+
state_view: StateView, equipment: EquipmentHandler, model_utils: ModelUtils
30+
) -> HomeImpl:
31+
"""Get a home command to test."""
32+
return HomeImpl(state_view=state_view, equipment=equipment, model_utils=model_utils)
33+
34+
35+
async def test_home_command(
36+
decoy: Decoy,
37+
state_view: StateView,
38+
equipment: EquipmentHandler,
39+
subject: HomeImpl,
40+
stacker_id: FlexStackerId,
41+
stacker_hardware: FlexStacker,
42+
) -> None:
43+
"""It should return a success data."""
44+
data = flex_stacker.HomeParams(moduleId=stacker_id)
45+
46+
fs_module_substate = FlexStackerSubState(
47+
module_id=stacker_id,
48+
pool_primary_definition=None,
49+
pool_adapter_definition=None,
50+
pool_lid_definition=None,
51+
pool_count=0,
52+
max_pool_count=0,
53+
)
54+
decoy.when(
55+
state_view.modules.get_flex_stacker_substate(module_id=stacker_id)
56+
).then_return(fs_module_substate)
57+
58+
result = await subject.execute(data)
59+
60+
decoy.verify(await stacker_hardware.home_all(), times=1)
61+
62+
assert result == SuccessData(public=flex_stacker.HomeResult())
63+
64+
65+
async def test_home_command_with_stall_detected(
66+
decoy: Decoy,
67+
state_view: StateView,
68+
equipment: EquipmentHandler,
69+
subject: HomeImpl,
70+
model_utils: ModelUtils,
71+
stacker_id: FlexStackerId,
72+
stacker_hardware: FlexStacker,
73+
) -> None:
74+
"""It should return a success data."""
75+
err_id = "error-id"
76+
err_timestamp = datetime(year=2025, month=3, day=19)
77+
78+
data = flex_stacker.HomeParams(moduleId=stacker_id)
79+
80+
fs_module_substate = FlexStackerSubState(
81+
module_id=stacker_id,
82+
pool_primary_definition=None,
83+
pool_adapter_definition=None,
84+
pool_lid_definition=None,
85+
pool_count=0,
86+
max_pool_count=0,
87+
)
88+
decoy.when(
89+
state_view.modules.get_flex_stacker_substate(module_id=stacker_id)
90+
).then_return(fs_module_substate)
91+
decoy.when(model_utils.generate_id()).then_return(err_id)
92+
decoy.when(model_utils.get_timestamp()).then_return(err_timestamp)
93+
94+
decoy.when(await stacker_hardware.home_all()).then_raise(
95+
FlexStackerStallError(serial="123", axis=StackerAxis.Z)
96+
)
97+
98+
result = await subject.execute(data)
99+
100+
assert result == DefinedErrorData(
101+
public=FlexStackerStallOrCollisionError.model_construct(
102+
id=err_id,
103+
createdAt=err_timestamp,
104+
wrappedErrors=[matchers.Anything()],
105+
)
106+
)

0 commit comments

Comments
 (0)