Skip to content

Commit c7bd4bb

Browse files
authored
feat(api): add a reload-labware command (#14963)
Adds a new command ReloadLabware, which allows dispatchers to change all the details of a loaded labware except for the location. This is primarily intended to allow getting a new labware offset that was not added to the engine by the time this labware was loaded (though it can technically do more, for symmetry). This doesn't really change a whole lot of behavior and is well-supported with testing. It's a prerequisite for #14940 Closes RSQ-29
1 parent 77bc720 commit c7bd4bb

File tree

12 files changed

+405
-1
lines changed

12 files changed

+405
-1
lines changed

api/src/opentrons/protocol_engine/clients/sync_client.py

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -127,6 +127,19 @@ def load_labware(
127127

128128
return cast(commands.LoadLabwareResult, result)
129129

130+
def reload_labware(
131+
self,
132+
labware_id: str,
133+
) -> commands.ReloadLabwareResult:
134+
"""Execute a ReloadLabware command and return the result."""
135+
request = commands.ReloadLabwareCreate(
136+
params=commands.ReloadLabwareParams(
137+
labwareId=labware_id,
138+
)
139+
)
140+
result = self._transport.execute_command(request=request)
141+
return cast(commands.ReloadLabwareResult, result)
142+
130143
# TODO (spp, 2022-12-14): https://opentrons.atlassian.net/browse/RLAB-237
131144
def move_labware(
132145
self,

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

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -120,6 +120,14 @@
120120
LoadLabwareCommandType,
121121
)
122122

123+
from .reload_labware import (
124+
ReloadLabware,
125+
ReloadLabwareParams,
126+
ReloadLabwareCreate,
127+
ReloadLabwareResult,
128+
ReloadLabwareCommandType,
129+
)
130+
123131
from .load_liquid import (
124132
LoadLiquid,
125133
LoadLiquidParams,
@@ -402,6 +410,12 @@
402410
"LoadLabwareParams",
403411
"LoadLabwareResult",
404412
"LoadLabwareCommandType",
413+
# reload labware command models
414+
"ReloadLabware",
415+
"ReloadLabwareCreate",
416+
"ReloadLabwareParams",
417+
"ReloadLabwareResult",
418+
"ReloadLabwareCommandType",
405419
# load module command models
406420
"LoadModule",
407421
"LoadModuleCreate",

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

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -100,6 +100,14 @@
100100
LoadLabwareCommandType,
101101
)
102102

103+
from .reload_labware import (
104+
ReloadLabware,
105+
ReloadLabwareParams,
106+
ReloadLabwareCreate,
107+
ReloadLabwareResult,
108+
ReloadLabwareCommandType,
109+
)
110+
103111
from .load_liquid import (
104112
LoadLiquid,
105113
LoadLiquidParams,
@@ -304,6 +312,7 @@
304312
Home,
305313
RetractAxis,
306314
LoadLabware,
315+
ReloadLabware,
307316
LoadLiquid,
308317
LoadModule,
309318
LoadPipette,
@@ -368,6 +377,7 @@
368377
HomeParams,
369378
RetractAxisParams,
370379
LoadLabwareParams,
380+
ReloadLabwareParams,
371381
LoadLiquidParams,
372382
LoadModuleParams,
373383
LoadPipetteParams,
@@ -431,6 +441,7 @@
431441
HomeCommandType,
432442
RetractAxisCommandType,
433443
LoadLabwareCommandType,
444+
ReloadLabwareCommandType,
434445
LoadLiquidCommandType,
435446
LoadModuleCommandType,
436447
LoadPipetteCommandType,
@@ -494,6 +505,7 @@
494505
HomeCreate,
495506
RetractAxisCreate,
496507
LoadLabwareCreate,
508+
ReloadLabwareCreate,
497509
LoadLiquidCreate,
498510
LoadModuleCreate,
499511
LoadPipetteCreate,
@@ -558,6 +570,7 @@
558570
HomeResult,
559571
RetractAxisResult,
560572
LoadLabwareResult,
573+
ReloadLabwareResult,
561574
LoadLiquidResult,
562575
LoadModuleResult,
563576
LoadPipetteResult,
Lines changed: 86 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,86 @@
1+
"""Reload labware command request, result, and implementation models."""
2+
from __future__ import annotations
3+
from pydantic import BaseModel, Field
4+
from typing import TYPE_CHECKING, Optional, Type
5+
from typing_extensions import Literal
6+
7+
from .command import AbstractCommandImpl, BaseCommand, BaseCommandCreate
8+
9+
if TYPE_CHECKING:
10+
from ..state import StateView
11+
from ..execution import EquipmentHandler
12+
13+
14+
ReloadLabwareCommandType = Literal["reloadLabware"]
15+
16+
17+
class ReloadLabwareParams(BaseModel):
18+
"""Payload required to load a labware into a slot."""
19+
20+
labwareId: str = Field(
21+
..., description="The already-loaded labware instance to update."
22+
)
23+
24+
25+
class ReloadLabwareResult(BaseModel):
26+
"""Result data from the execution of a LoadLabware command."""
27+
28+
labwareId: str = Field(
29+
...,
30+
description="An ID to reference this labware in subsequent commands. Same as the one in the parameters.",
31+
)
32+
offsetId: Optional[str] = Field(
33+
# Default `None` instead of `...` so this field shows up as non-required in
34+
# OpenAPI. The server is allowed to omit it or make it null.
35+
None,
36+
description=(
37+
"An ID referencing the labware offset that will apply"
38+
" to the reloaded labware."
39+
" This offset will be in effect until the labware is moved"
40+
" with a `moveLabware` command."
41+
" Null or undefined means no offset applies,"
42+
" so the default of (0, 0, 0) will be used."
43+
),
44+
)
45+
46+
47+
class ReloadLabwareImplementation(
48+
AbstractCommandImpl[ReloadLabwareParams, ReloadLabwareResult]
49+
):
50+
"""Reload labware command implementation."""
51+
52+
def __init__(
53+
self, equipment: EquipmentHandler, state_view: StateView, **kwargs: object
54+
) -> None:
55+
self._equipment = equipment
56+
self._state_view = state_view
57+
58+
async def execute(self, params: ReloadLabwareParams) -> ReloadLabwareResult:
59+
"""Reload the definition and calibration data for a specific labware."""
60+
reloaded_labware = await self._equipment.reload_labware(
61+
labware_id=params.labwareId,
62+
)
63+
64+
return ReloadLabwareResult(
65+
labwareId=params.labwareId,
66+
offsetId=reloaded_labware.offsetId,
67+
)
68+
69+
70+
class ReloadLabware(BaseCommand[ReloadLabwareParams, ReloadLabwareResult]):
71+
"""Reload labware command resource model."""
72+
73+
commandType: ReloadLabwareCommandType = "reloadLabware"
74+
params: ReloadLabwareParams
75+
result: Optional[ReloadLabwareResult]
76+
77+
_ImplementationCls: Type[ReloadLabwareImplementation] = ReloadLabwareImplementation
78+
79+
80+
class ReloadLabwareCreate(BaseCommandCreate[ReloadLabwareParams]):
81+
"""Reload labware command creation request."""
82+
83+
commandType: ReloadLabwareCommandType = "reloadLabware"
84+
params: ReloadLabwareParams
85+
86+
_CommandCls: Type[ReloadLabware] = ReloadLabware

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

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@
88
LoadedPipetteData,
99
LoadedModuleData,
1010
LoadedConfigureForVolumeData,
11+
ReloadedLabwareData,
1112
)
1213
from .movement import MovementHandler
1314
from .gantry_mover import GantryMover
@@ -29,6 +30,7 @@
2930
"create_queue_worker",
3031
"EquipmentHandler",
3132
"LoadedLabwareData",
33+
"ReloadedLabwareData",
3234
"LoadedPipetteData",
3335
"LoadedModuleData",
3436
"LoadedConfigureForVolumeData",

api/src/opentrons/protocol_engine/execution/equipment.py

Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -56,6 +56,14 @@ class LoadedLabwareData:
5656
offsetId: Optional[str]
5757

5858

59+
@dataclass(frozen=True)
60+
class ReloadedLabwareData:
61+
"""The result of a reload labware procedure."""
62+
63+
location: LabwareLocation
64+
offsetId: Optional[str]
65+
66+
5967
@dataclass(frozen=True)
6068
class LoadedPipetteData:
6169
"""The result of a load pipette procedure."""
@@ -171,6 +179,25 @@ async def load_labware(
171179
labware_id=labware_id, definition=definition, offsetId=offset_id
172180
)
173181

182+
async def reload_labware(self, labware_id: str) -> ReloadedLabwareData:
183+
"""Reload an already-loaded labware. This cannot change the labware location.
184+
185+
Args:
186+
labware_id: The ID of the already-loaded labware.
187+
188+
Raises:
189+
LabwareNotLoadedError: If `labware_id` does not reference a loaded labware.
190+
191+
"""
192+
location = self._state_store.labware.get_location(labware_id)
193+
definition_uri = self._state_store.labware.get_definition_uri(labware_id)
194+
offset_id = self.find_applicable_labware_offset_id(
195+
labware_definition_uri=definition_uri,
196+
labware_location=location,
197+
)
198+
199+
return ReloadedLabwareData(location=location, offsetId=offset_id)
200+
174201
async def load_pipette(
175202
self,
176203
pipette_name: PipetteNameType,

api/src/opentrons/protocol_engine/state/labware.py

Lines changed: 11 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -31,6 +31,7 @@
3131
Command,
3232
LoadLabwareResult,
3333
MoveLabwareResult,
34+
ReloadLabwareResult,
3435
)
3536
from ..types import (
3637
DeckSlotLocation,
@@ -187,18 +188,27 @@ def _handle_command(self, command: Command) -> None:
187188
)
188189

189190
self._state.definitions_by_uri[definition_uri] = command.result.definition
191+
if isinstance(command.result, LoadLabwareResult):
192+
location = command.params.location
193+
else:
194+
location = self._state.labware_by_id[command.result.labwareId].location
190195

191196
self._state.labware_by_id[
192197
command.result.labwareId
193198
] = LoadedLabware.construct(
194199
id=command.result.labwareId,
195-
location=command.params.location,
200+
location=location,
196201
loadName=command.result.definition.parameters.loadName,
197202
definitionUri=definition_uri,
198203
offsetId=command.result.offsetId,
199204
displayName=command.params.displayName,
200205
)
201206

207+
elif isinstance(command.result, ReloadLabwareResult):
208+
labware_id = command.params.labwareId
209+
new_offset_id = command.result.offsetId
210+
self._state.labware_by_id[labware_id].offsetId = new_offset_id
211+
202212
elif isinstance(command.result, MoveLabwareResult):
203213
labware_id = command.params.labwareId
204214
new_location = command.params.newLocation

api/tests/opentrons/protocol_engine/clients/test_sync_client.py

Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -161,6 +161,30 @@ def test_load_labware(
161161
assert result == expected_result
162162

163163

164+
def test_reload_labware(
165+
decoy: Decoy,
166+
transport: ChildThreadTransport,
167+
subject: SyncClient,
168+
) -> None:
169+
"""It should execute a reload labware command."""
170+
expected_request = commands.ReloadLabwareCreate(
171+
params=commands.ReloadLabwareParams(
172+
labwareId="some-labware-id",
173+
)
174+
)
175+
176+
expected_result = commands.ReloadLabwareResult(
177+
labwareId="some-labware-id", offsetId=None
178+
)
179+
decoy.when(transport.execute_command(request=expected_request)).then_return(
180+
expected_result
181+
)
182+
result = subject.reload_labware(
183+
labware_id="some-labware-id",
184+
)
185+
assert result == expected_result
186+
187+
164188
def test_load_module(
165189
decoy: Decoy,
166190
transport: ChildThreadTransport,

0 commit comments

Comments
 (0)