opentrons 8.7.0a7__py3-none-any.whl → 8.8.0a7__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- opentrons/_version.py +2 -2
- opentrons/cli/analyze.py +4 -1
- opentrons/config/__init__.py +7 -0
- opentrons/drivers/asyncio/communication/serial_connection.py +8 -5
- opentrons/drivers/flex_stacker/driver.py +6 -1
- opentrons/drivers/vacuum_module/__init__.py +5 -0
- opentrons/drivers/vacuum_module/abstract.py +93 -0
- opentrons/drivers/vacuum_module/driver.py +208 -0
- opentrons/drivers/vacuum_module/errors.py +39 -0
- opentrons/drivers/vacuum_module/simulator.py +85 -0
- opentrons/drivers/vacuum_module/types.py +79 -0
- opentrons/execute.py +3 -0
- opentrons/hardware_control/backends/flex_protocol.py +2 -0
- opentrons/hardware_control/backends/ot3controller.py +35 -2
- opentrons/hardware_control/backends/ot3simulator.py +2 -0
- opentrons/hardware_control/backends/ot3utils.py +37 -0
- opentrons/hardware_control/module_control.py +23 -2
- opentrons/hardware_control/modules/mod_abc.py +1 -1
- opentrons/hardware_control/modules/types.py +1 -1
- opentrons/hardware_control/motion_utilities.py +6 -6
- opentrons/hardware_control/ot3api.py +62 -13
- opentrons/hardware_control/protocols/gripper_controller.py +1 -0
- opentrons/hardware_control/protocols/liquid_handler.py +6 -2
- opentrons/hardware_control/types.py +12 -0
- opentrons/legacy_commands/commands.py +58 -5
- opentrons/legacy_commands/module_commands.py +29 -0
- opentrons/legacy_commands/protocol_commands.py +33 -1
- opentrons/legacy_commands/types.py +75 -1
- opentrons/protocol_api/_transfer_liquid_validation.py +17 -2
- opentrons/protocol_api/_types.py +2 -0
- opentrons/protocol_api/core/engine/_default_labware_versions.py +1 -0
- opentrons/protocol_api/core/engine/deck_conflict.py +3 -1
- opentrons/protocol_api/core/engine/instrument.py +109 -26
- opentrons/protocol_api/core/engine/module_core.py +27 -3
- opentrons/protocol_api/core/engine/protocol.py +33 -1
- opentrons/protocol_api/core/engine/stringify.py +2 -0
- opentrons/protocol_api/core/instrument.py +19 -2
- opentrons/protocol_api/core/legacy/legacy_instrument_core.py +19 -2
- opentrons/protocol_api/core/legacy/legacy_module_core.py +15 -4
- opentrons/protocol_api/core/legacy/legacy_protocol_core.py +12 -0
- opentrons/protocol_api/core/legacy_simulator/legacy_instrument_core.py +19 -2
- opentrons/protocol_api/core/module.py +25 -2
- opentrons/protocol_api/core/protocol.py +12 -0
- opentrons/protocol_api/instrument_context.py +388 -2
- opentrons/protocol_api/labware.py +5 -2
- opentrons/protocol_api/module_contexts.py +133 -30
- opentrons/protocol_api/protocol_context.py +61 -17
- opentrons/protocol_api/robot_context.py +3 -4
- opentrons/protocol_api/validation.py +43 -2
- opentrons/protocol_engine/__init__.py +4 -0
- opentrons/protocol_engine/actions/__init__.py +2 -0
- opentrons/protocol_engine/actions/actions.py +9 -0
- opentrons/protocol_engine/commands/__init__.py +14 -0
- opentrons/protocol_engine/commands/absorbance_reader/read.py +22 -23
- opentrons/protocol_engine/commands/aspirate_while_tracking.py +52 -19
- opentrons/protocol_engine/commands/capture_image.py +302 -0
- opentrons/protocol_engine/commands/command.py +1 -0
- opentrons/protocol_engine/commands/command_unions.py +13 -0
- opentrons/protocol_engine/commands/dispense_while_tracking.py +56 -19
- opentrons/protocol_engine/commands/flex_stacker/common.py +35 -0
- opentrons/protocol_engine/commands/flex_stacker/set_stored_labware.py +7 -0
- opentrons/protocol_engine/commands/heater_shaker/set_shake_speed.py +1 -1
- opentrons/protocol_engine/commands/heater_shaker/set_target_temperature.py +1 -1
- opentrons/protocol_engine/commands/move_labware.py +3 -4
- opentrons/protocol_engine/commands/move_to_addressable_area_for_drop_tip.py +1 -1
- opentrons/protocol_engine/commands/movement_common.py +29 -2
- opentrons/protocol_engine/commands/pipetting_common.py +48 -3
- opentrons/protocol_engine/commands/thermocycler/set_target_block_temperature.py +12 -9
- opentrons/protocol_engine/commands/thermocycler/set_target_lid_temperature.py +17 -12
- opentrons/protocol_engine/commands/thermocycler/start_run_extended_profile.py +1 -1
- opentrons/protocol_engine/create_protocol_engine.py +12 -0
- opentrons/protocol_engine/engine_support.py +3 -0
- opentrons/protocol_engine/errors/__init__.py +8 -0
- opentrons/protocol_engine/errors/exceptions.py +64 -0
- opentrons/protocol_engine/execution/__init__.py +2 -0
- opentrons/protocol_engine/execution/command_executor.py +54 -1
- opentrons/protocol_engine/execution/create_queue_worker.py +4 -1
- opentrons/protocol_engine/execution/labware_movement.py +13 -4
- opentrons/protocol_engine/execution/pipetting.py +19 -25
- opentrons/protocol_engine/protocol_engine.py +62 -2
- opentrons/protocol_engine/resources/__init__.py +2 -0
- opentrons/protocol_engine/resources/camera_provider.py +110 -0
- opentrons/protocol_engine/resources/file_provider.py +133 -58
- opentrons/protocol_engine/slot_standardization.py +2 -0
- opentrons/protocol_engine/state/camera.py +54 -0
- opentrons/protocol_engine/state/commands.py +24 -4
- opentrons/protocol_engine/state/geometry.py +68 -10
- opentrons/protocol_engine/state/labware.py +10 -6
- opentrons/protocol_engine/state/labware_origin_math/stackup_origin_to_labware_origin.py +6 -1
- opentrons/protocol_engine/state/modules.py +9 -0
- opentrons/protocol_engine/state/preconditions.py +59 -0
- opentrons/protocol_engine/state/state.py +30 -0
- opentrons/protocol_engine/state/state_summary.py +2 -0
- opentrons/protocol_engine/state/update_types.py +10 -0
- opentrons/protocol_engine/types/__init__.py +14 -1
- opentrons/protocol_engine/types/command_preconditions.py +18 -0
- opentrons/protocol_engine/types/location.py +26 -2
- opentrons/protocol_engine/types/module.py +1 -1
- opentrons/protocol_runner/protocol_runner.py +14 -1
- opentrons/protocol_runner/run_orchestrator.py +31 -0
- opentrons/protocols/advanced_control/transfers/transfer_liquid_utils.py +2 -2
- opentrons/simulate.py +3 -0
- opentrons/system/camera.py +333 -3
- opentrons/system/ffmpeg.py +110 -0
- {opentrons-8.7.0a7.dist-info → opentrons-8.8.0a7.dist-info}/METADATA +4 -4
- {opentrons-8.7.0a7.dist-info → opentrons-8.8.0a7.dist-info}/RECORD +109 -97
- {opentrons-8.7.0a7.dist-info → opentrons-8.8.0a7.dist-info}/WHEEL +0 -0
- {opentrons-8.7.0a7.dist-info → opentrons-8.8.0a7.dist-info}/entry_points.txt +0 -0
- {opentrons-8.7.0a7.dist-info → opentrons-8.8.0a7.dist-info}/licenses/LICENSE +0 -0
|
@@ -1,36 +1,91 @@
|
|
|
1
1
|
"""File interaction resource provider."""
|
|
2
2
|
from datetime import datetime
|
|
3
|
+
from io import StringIO
|
|
4
|
+
import csv
|
|
3
5
|
from typing import List, Optional, Callable, Awaitable, Dict
|
|
6
|
+
from dataclasses import dataclass
|
|
4
7
|
from pydantic import BaseModel
|
|
5
|
-
from
|
|
8
|
+
from opentrons_shared_data.data_files import (
|
|
9
|
+
DataFileInfo,
|
|
10
|
+
MimeType,
|
|
11
|
+
RunFileNameMetadata,
|
|
12
|
+
)
|
|
13
|
+
|
|
14
|
+
SPECIAL_CHARACTERS = {
|
|
15
|
+
"#",
|
|
16
|
+
"%",
|
|
17
|
+
"&",
|
|
18
|
+
"{",
|
|
19
|
+
"}",
|
|
20
|
+
"\\",
|
|
21
|
+
"/",
|
|
22
|
+
"<",
|
|
23
|
+
">",
|
|
24
|
+
"*",
|
|
25
|
+
"$",
|
|
26
|
+
"!",
|
|
27
|
+
"?",
|
|
28
|
+
".",
|
|
29
|
+
"'",
|
|
30
|
+
'"',
|
|
31
|
+
":",
|
|
32
|
+
";",
|
|
33
|
+
"@",
|
|
34
|
+
"`",
|
|
35
|
+
"|",
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
|
|
39
|
+
@dataclass(frozen=True)
|
|
40
|
+
class FileNameCmdMetadata:
|
|
41
|
+
"""Command metadata associated with a specific data file."""
|
|
42
|
+
|
|
43
|
+
command_id: str
|
|
44
|
+
prev_command_id: str
|
|
45
|
+
|
|
46
|
+
|
|
47
|
+
@dataclass(frozen=True)
|
|
48
|
+
class ReadCmdFileNameMetadata(FileNameCmdMetadata):
|
|
49
|
+
"""Data from a plate reader `read` command used to build the finalized file name."""
|
|
50
|
+
|
|
51
|
+
base_filename: str
|
|
52
|
+
wavelength: int
|
|
53
|
+
|
|
54
|
+
|
|
55
|
+
@dataclass(frozen=True)
|
|
56
|
+
class ImageCaptureCmdFileNameMetadata(FileNameCmdMetadata):
|
|
57
|
+
"""Data from a camera capture command used to build the finalized file name."""
|
|
6
58
|
|
|
59
|
+
step_number: int
|
|
60
|
+
command_timestamp: datetime
|
|
61
|
+
base_filename: Optional[str]
|
|
7
62
|
|
|
8
|
-
MAXIMUM_CSV_FILE_LIMIT = 400
|
|
9
63
|
|
|
64
|
+
CommandFileNameMetadata = ReadCmdFileNameMetadata | ImageCaptureCmdFileNameMetadata
|
|
10
65
|
|
|
11
|
-
class GenericCsvTransform:
|
|
12
|
-
"""Generic CSV File Type data for rows of data to be seperated by a delimeter."""
|
|
13
66
|
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
67
|
+
class FileData:
|
|
68
|
+
"""File data container for writing to a file."""
|
|
69
|
+
|
|
70
|
+
data: bytes
|
|
71
|
+
mime_type: MimeType
|
|
72
|
+
run_metadata: RunFileNameMetadata
|
|
73
|
+
command_metadata: CommandFileNameMetadata
|
|
17
74
|
|
|
18
75
|
@staticmethod
|
|
19
76
|
def build(
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
csv.delimiter = delimiter
|
|
33
|
-
return csv
|
|
77
|
+
data: bytes,
|
|
78
|
+
mime_type: MimeType,
|
|
79
|
+
run_metadata: RunFileNameMetadata,
|
|
80
|
+
command_metadata: CommandFileNameMetadata,
|
|
81
|
+
) -> "FileData":
|
|
82
|
+
"""Build a generic file data class."""
|
|
83
|
+
file_data = FileData()
|
|
84
|
+
file_data.data = data
|
|
85
|
+
file_data.mime_type = mime_type
|
|
86
|
+
file_data.run_metadata = run_metadata
|
|
87
|
+
file_data.command_metadata = command_metadata
|
|
88
|
+
return file_data
|
|
34
89
|
|
|
35
90
|
|
|
36
91
|
class ReadData(BaseModel):
|
|
@@ -41,7 +96,7 @@ class ReadData(BaseModel):
|
|
|
41
96
|
|
|
42
97
|
|
|
43
98
|
class PlateReaderData(BaseModel):
|
|
44
|
-
"""Data from
|
|
99
|
+
"""Data from an Opentrons Plate Reader Read. Can be converted to CSV format."""
|
|
45
100
|
|
|
46
101
|
read_results: List[ReadData]
|
|
47
102
|
reference_wavelength: Optional[int] = None
|
|
@@ -49,13 +104,8 @@ class PlateReaderData(BaseModel):
|
|
|
49
104
|
finish_time: datetime
|
|
50
105
|
serial_number: str
|
|
51
106
|
|
|
52
|
-
def
|
|
53
|
-
|
|
54
|
-
) -> GenericCsvTransform:
|
|
55
|
-
"""Builds a CSV compatible object containing Plate Reader Measurements.
|
|
56
|
-
|
|
57
|
-
This will also automatically reformat the provided filename to include the wavelength of those measurements.
|
|
58
|
-
"""
|
|
107
|
+
def build_csv_bytes(self, measurement: ReadData) -> bytes: # noqa: C901
|
|
108
|
+
"""Builds CSV data as bytes containing Plate Reader Measurements."""
|
|
59
109
|
plate_alpharows = ["A", "B", "C", "D", "E", "F", "G", "H"]
|
|
60
110
|
rows = []
|
|
61
111
|
|
|
@@ -63,7 +113,7 @@ class PlateReaderData(BaseModel):
|
|
|
63
113
|
for i in range(8):
|
|
64
114
|
row = [plate_alpharows[i]]
|
|
65
115
|
for j in range(12):
|
|
66
|
-
row.append(str(measurement.data[f"{plate_alpharows[i]}{j+1}"]))
|
|
116
|
+
row.append(str(measurement.data[f"{plate_alpharows[i]}{j + 1}"]))
|
|
67
117
|
rows.append(row)
|
|
68
118
|
for i in range(3):
|
|
69
119
|
rows.append([])
|
|
@@ -116,16 +166,12 @@ class PlateReaderData(BaseModel):
|
|
|
116
166
|
["Measurement finished at", self.finish_time.strftime("%m %d %H:%M:%S %Y")]
|
|
117
167
|
)
|
|
118
168
|
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
169
|
+
output = StringIO()
|
|
170
|
+
writer = csv.writer(output, delimiter=",")
|
|
171
|
+
writer.writerows(rows)
|
|
172
|
+
csv_bytes = output.getvalue().encode("utf-8")
|
|
123
173
|
|
|
124
|
-
return
|
|
125
|
-
filename=filename,
|
|
126
|
-
rows=rows,
|
|
127
|
-
delimiter=",",
|
|
128
|
-
)
|
|
174
|
+
return csv_bytes
|
|
129
175
|
|
|
130
176
|
|
|
131
177
|
class FileProvider:
|
|
@@ -133,29 +179,58 @@ class FileProvider:
|
|
|
133
179
|
|
|
134
180
|
def __init__(
|
|
135
181
|
self,
|
|
136
|
-
|
|
137
|
-
Callable[[
|
|
182
|
+
data_files_write_file_cb: Optional[
|
|
183
|
+
Callable[[FileData], Awaitable[DataFileInfo]]
|
|
138
184
|
] = None,
|
|
139
|
-
data_files_filecount: Optional[Callable[[], Awaitable[int]]] = None,
|
|
140
185
|
) -> None:
|
|
141
186
|
"""Initialize the interface callbacks of the File Provider for data file handling within the Protocol Engine.
|
|
142
187
|
|
|
143
188
|
Params:
|
|
144
|
-
|
|
189
|
+
data_files_write_file_callback: Callback to write a file to the data files directory and add it to the database.
|
|
145
190
|
data_files_filecount: Callback to check the amount of data files already present in the data files directory.
|
|
146
191
|
"""
|
|
147
|
-
self.
|
|
148
|
-
self.
|
|
149
|
-
|
|
150
|
-
async def
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
192
|
+
self._data_files_write_file_cb = data_files_write_file_cb
|
|
193
|
+
self._run_metadata: RunFileNameMetadata | None = None
|
|
194
|
+
|
|
195
|
+
async def write_file(
|
|
196
|
+
self,
|
|
197
|
+
data: bytes,
|
|
198
|
+
mime_type: MimeType,
|
|
199
|
+
command_metadata: CommandFileNameMetadata,
|
|
200
|
+
) -> DataFileInfo:
|
|
201
|
+
"""Writes arbitrary data to a file in the Data Files directory.
|
|
202
|
+
|
|
203
|
+
Returns the `DataFileInfo` of the file created.
|
|
204
|
+
|
|
205
|
+
Raises:
|
|
206
|
+
Note that the callback may raise a StorageLimitReachedError.
|
|
207
|
+
"""
|
|
208
|
+
if self._data_files_write_file_cb is not None:
|
|
209
|
+
assert self._run_metadata is not None
|
|
210
|
+
file_data = FileData.build(
|
|
211
|
+
data=data,
|
|
212
|
+
mime_type=mime_type,
|
|
213
|
+
command_metadata=command_metadata,
|
|
214
|
+
run_metadata=self._run_metadata,
|
|
215
|
+
)
|
|
216
|
+
|
|
217
|
+
return await self._data_files_write_file_cb(file_data)
|
|
218
|
+
# If we are in an analysis or simulation state, return an empty `DataFileInfo`
|
|
219
|
+
return DataFileInfo(
|
|
220
|
+
id="",
|
|
221
|
+
name="",
|
|
222
|
+
file_hash="",
|
|
223
|
+
created_at=datetime.now(),
|
|
224
|
+
generated=True,
|
|
225
|
+
stored=False,
|
|
226
|
+
path="",
|
|
227
|
+
mime_type=mime_type,
|
|
228
|
+
)
|
|
229
|
+
|
|
230
|
+
def set_run_metadata(self, metadata: RunFileNameMetadata) -> None:
|
|
231
|
+
"""Sets metadata specific to the run."""
|
|
232
|
+
self._run_metadata = metadata
|
|
233
|
+
|
|
234
|
+
def clear_run_metadata(self) -> None:
|
|
235
|
+
"""Clears metadata specific to the run."""
|
|
236
|
+
self._run_metadata = None
|
|
@@ -27,6 +27,7 @@ from .types import (
|
|
|
27
27
|
AddressableAreaLocation,
|
|
28
28
|
ModuleLocation,
|
|
29
29
|
OnLabwareLocation,
|
|
30
|
+
WASTE_CHUTE_LOCATION,
|
|
30
31
|
)
|
|
31
32
|
|
|
32
33
|
|
|
@@ -116,6 +117,7 @@ def _standardize_labware_location(
|
|
|
116
117
|
)
|
|
117
118
|
or original == OFF_DECK_LOCATION
|
|
118
119
|
or original == SYSTEM_LOCATION
|
|
120
|
+
or original == WASTE_CHUTE_LOCATION
|
|
119
121
|
):
|
|
120
122
|
return original
|
|
121
123
|
|
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
"""Camera related state and store resource.
|
|
2
|
+
|
|
3
|
+
Camera settings, particularly for enablement, can be quieried from via the Camera Provider callback.
|
|
4
|
+
However, here Camera settings may also be provided to override or supercede those provided by the callbacks.
|
|
5
|
+
"""
|
|
6
|
+
from dataclasses import dataclass
|
|
7
|
+
from typing import Optional
|
|
8
|
+
from opentrons.protocol_engine.actions import AddCameraSettingsAction
|
|
9
|
+
from opentrons.protocol_engine.resources.camera_provider import CameraSettings
|
|
10
|
+
|
|
11
|
+
from ._abstract_store import HasState, HandlesActions
|
|
12
|
+
from ..actions import Action
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
@dataclass
|
|
16
|
+
class CameraState:
|
|
17
|
+
"""State of Engine Camera override settings."""
|
|
18
|
+
|
|
19
|
+
enablement_settings: Optional[CameraSettings]
|
|
20
|
+
# todo(chb, 2025-10-28): Eventually we will want to extend this to include the camera configurations overrides (contrast, zoom, etc)
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
class CameraStore(HasState[CameraState], HandlesActions):
|
|
24
|
+
"""Camera container."""
|
|
25
|
+
|
|
26
|
+
_state: CameraState
|
|
27
|
+
|
|
28
|
+
def __init__(self) -> None:
|
|
29
|
+
"""Initialize a Camera store and its state."""
|
|
30
|
+
self._state = CameraState(enablement_settings=None)
|
|
31
|
+
|
|
32
|
+
def handle_action(self, action: Action) -> None:
|
|
33
|
+
"""Modify state in reaction to an action."""
|
|
34
|
+
if isinstance(action, AddCameraSettingsAction):
|
|
35
|
+
# Update the Camera Enablement settings to the newest override settings
|
|
36
|
+
self._state.enablement_settings = action.enablement_settings
|
|
37
|
+
|
|
38
|
+
|
|
39
|
+
class CameraView:
|
|
40
|
+
"""Read-only engine created Camera state view."""
|
|
41
|
+
|
|
42
|
+
_state: CameraState
|
|
43
|
+
|
|
44
|
+
def __init__(self, state: CameraState) -> None:
|
|
45
|
+
"""Initialize the view of Camera state.
|
|
46
|
+
|
|
47
|
+
Arguments:
|
|
48
|
+
state: Camera dataclass used for tracking override settings for the camera.
|
|
49
|
+
"""
|
|
50
|
+
self._state = state
|
|
51
|
+
|
|
52
|
+
def get_enablement_settings(self) -> CameraSettings | None:
|
|
53
|
+
"""Get the enablement settings override currently in use. This will take priority over Camera Provider callback provided settings."""
|
|
54
|
+
return self._state.enablement_settings
|
|
@@ -239,7 +239,10 @@ class CommandState:
|
|
|
239
239
|
"""Whether the run has entered error recovery."""
|
|
240
240
|
|
|
241
241
|
stopped_by_async_error: bool
|
|
242
|
-
"""If this is set to True, the engine was stopped by an
|
|
242
|
+
"""If this is set to True, the engine was stopped by an async event."""
|
|
243
|
+
|
|
244
|
+
is_stopping_because_of_async_error: bool
|
|
245
|
+
"""If this is set to True, the engine was stopped by an asynch event and hasn't finished stopping."""
|
|
243
246
|
|
|
244
247
|
error_recovery_policy: ErrorRecoveryPolicy
|
|
245
248
|
"""See `CommandView.get_error_recovery_policy()`."""
|
|
@@ -273,6 +276,7 @@ class CommandStore(HasState[CommandState], HandlesActions):
|
|
|
273
276
|
run_started_at=None,
|
|
274
277
|
latest_protocol_command_hash=None,
|
|
275
278
|
stopped_by_async_error=False,
|
|
279
|
+
is_stopping_because_of_async_error=False,
|
|
276
280
|
error_recovery_policy=error_recovery_policy,
|
|
277
281
|
has_entered_error_recovery=False,
|
|
278
282
|
)
|
|
@@ -474,6 +478,7 @@ class CommandStore(HasState[CommandState], HandlesActions):
|
|
|
474
478
|
|
|
475
479
|
if action.from_asynchronous_error:
|
|
476
480
|
self._state.stopped_by_async_error = True
|
|
481
|
+
self._state.is_stopping_because_of_async_error = True
|
|
477
482
|
self._state.run_result = RunResult.FAILED
|
|
478
483
|
else:
|
|
479
484
|
self._state.run_result = RunResult.STOPPED
|
|
@@ -499,14 +504,29 @@ class CommandStore(HasState[CommandState], HandlesActions):
|
|
|
499
504
|
action.error_details.error,
|
|
500
505
|
)
|
|
501
506
|
else:
|
|
502
|
-
# HACK(sf): There needs to be a better way to
|
|
503
|
-
#
|
|
504
|
-
|
|
507
|
+
# HACK(sf): There needs to be a better way to handle async errors than this logic
|
|
508
|
+
# which is way too nonlocal. The idea here is that
|
|
509
|
+
# (1) there's an async error that calls one of the engine async error handlers,
|
|
510
|
+
# which emits a stop action and then tells the orchestrator to call finish
|
|
511
|
+
# (2) calling stop normally would lock out the run error field (since the idea is that
|
|
512
|
+
# stop happens in the handler of the error that stops the run, and thus the failed
|
|
513
|
+
# command has already set the run error), but here either the command didn't fail or
|
|
514
|
+
# the command failed with an error that isn't relevant or is duplicative, so we want
|
|
515
|
+
# to override that
|
|
516
|
+
# (3) but we don't want to override it twice, because some other error handler might
|
|
517
|
+
# tell us to finish with the cancelled error that happens because a command was cancelled
|
|
518
|
+
# in reaction to (2)
|
|
519
|
+
# So we set and clear this stopped_by_async_error and it's awful. Let's figure out a better
|
|
520
|
+
# way.
|
|
521
|
+
|
|
522
|
+
if self._state.is_stopping_because_of_async_error and action.error_details:
|
|
505
523
|
self._state.run_error = self._map_run_exception_to_error_occurrence(
|
|
506
524
|
action.error_details.error_id,
|
|
507
525
|
action.error_details.created_at,
|
|
508
526
|
action.error_details.error,
|
|
509
527
|
)
|
|
528
|
+
self._state.is_stopping_because_of_async_error = False
|
|
529
|
+
self._state.stopped_by_async_error = True
|
|
510
530
|
|
|
511
531
|
def _handle_hardware_stopped_action(self, action: HardwareStoppedAction) -> None:
|
|
512
532
|
self._state.queue_status = QueueStatus.PAUSED
|
|
@@ -68,6 +68,8 @@ from ..types import (
|
|
|
68
68
|
CurrentPipetteLocation,
|
|
69
69
|
TipGeometry,
|
|
70
70
|
InStackerHopperLocation,
|
|
71
|
+
WASTE_CHUTE_LOCATION,
|
|
72
|
+
AccessibleByGripperLocation,
|
|
71
73
|
OnDeckLabwareLocation,
|
|
72
74
|
AddressableAreaLocation,
|
|
73
75
|
AddressableOffsetVector,
|
|
@@ -198,6 +200,7 @@ class GeometryView:
|
|
|
198
200
|
if isinstance(loc, InStackerHopperLocation) or isinstance(
|
|
199
201
|
loc, NotOnDeckLocationSequenceComponent
|
|
200
202
|
):
|
|
203
|
+
|
|
201
204
|
return False
|
|
202
205
|
return True
|
|
203
206
|
|
|
@@ -396,6 +399,11 @@ class GeometryView:
|
|
|
396
399
|
"Labware does not have a slot or module associated with it"
|
|
397
400
|
" since it is no longer on the deck."
|
|
398
401
|
)
|
|
402
|
+
elif location == WASTE_CHUTE_LOCATION:
|
|
403
|
+
raise errors.LabwareNotOnDeckError(
|
|
404
|
+
"Labware does not have a slot or module associated with it"
|
|
405
|
+
" since it is in the waste chute."
|
|
406
|
+
)
|
|
399
407
|
|
|
400
408
|
def get_labware_origin_position(self, labware_id: str) -> Point:
|
|
401
409
|
"""Get the deck coordinates of a labware's origin.
|
|
@@ -459,7 +467,6 @@ class GeometryView:
|
|
|
459
467
|
current_definition = self._labware.get_definition(current_labware_id)
|
|
460
468
|
else:
|
|
461
469
|
break
|
|
462
|
-
|
|
463
470
|
return definitions_locations_top_to_bottom
|
|
464
471
|
|
|
465
472
|
def _get_stackup_module_parent_to_child_offset(
|
|
@@ -491,7 +498,6 @@ class GeometryView:
|
|
|
491
498
|
) -> LabwareStackupAncestorDefinition:
|
|
492
499
|
"""Traverse the stackup to find the first non-labware definition."""
|
|
493
500
|
current_location = top_most_lw_location
|
|
494
|
-
|
|
495
501
|
while True:
|
|
496
502
|
if isinstance(current_location, OnLabwareLocation):
|
|
497
503
|
current_labware_id = current_location.labwareId
|
|
@@ -500,6 +506,7 @@ class GeometryView:
|
|
|
500
506
|
else:
|
|
501
507
|
if isinstance(current_location, ModuleLocation):
|
|
502
508
|
return self._modules.get_definition(current_location.moduleId)
|
|
509
|
+
|
|
503
510
|
elif isinstance(current_location, AddressableAreaLocation):
|
|
504
511
|
return self._addressable_areas.get_addressable_area(
|
|
505
512
|
current_location.addressableAreaName
|
|
@@ -508,6 +515,10 @@ class GeometryView:
|
|
|
508
515
|
return self._addressable_areas.get_slot_definition(
|
|
509
516
|
current_location.slotName.id
|
|
510
517
|
)
|
|
518
|
+
elif current_location == WASTE_CHUTE_LOCATION:
|
|
519
|
+
return self._addressable_areas.get_addressable_area(
|
|
520
|
+
"gripperWasteChute"
|
|
521
|
+
)
|
|
511
522
|
else:
|
|
512
523
|
raise errors.InvalidLabwarePositionError(
|
|
513
524
|
f"Cannot get ancestor slot of location {current_location}"
|
|
@@ -522,6 +533,8 @@ class GeometryView:
|
|
|
522
533
|
return self._modules.get_provided_addressable_area(location.moduleId)
|
|
523
534
|
elif isinstance(location, OnLabwareLocation):
|
|
524
535
|
return self.get_ancestor_addressable_area_name(location.labwareId)
|
|
536
|
+
elif location == WASTE_CHUTE_LOCATION:
|
|
537
|
+
return "gripperWasteChute"
|
|
525
538
|
else:
|
|
526
539
|
raise errors.InvalidLabwarePositionError(
|
|
527
540
|
f"Cannot get ancestor slot of location {location}"
|
|
@@ -624,7 +637,9 @@ class GeometryView:
|
|
|
624
637
|
if meniscus_tracking:
|
|
625
638
|
location = LiquidHandlingWellLocation(
|
|
626
639
|
origin=WellOrigin.MENISCUS,
|
|
627
|
-
offset=WellOffset(
|
|
640
|
+
offset=WellOffset(
|
|
641
|
+
x=absolute_point.x, y=absolute_point.y, z=absolute_point.z
|
|
642
|
+
),
|
|
628
643
|
)
|
|
629
644
|
# TODO(cm): handle operationVolume being a float other than 0
|
|
630
645
|
if meniscus_tracking == MeniscusTrackingTarget.END:
|
|
@@ -680,6 +695,9 @@ class GeometryView:
|
|
|
680
695
|
return well_def.depth
|
|
681
696
|
|
|
682
697
|
def _get_highest_z_from_labware_data(self, lw_data: LoadedLabware) -> float:
|
|
698
|
+
if lw_data.location == WASTE_CHUTE_LOCATION:
|
|
699
|
+
# Returns 0 so that the waste chute height is not added to the height of the lbw
|
|
700
|
+
return 0
|
|
683
701
|
labware_pos = self.get_labware_position(lw_data.id)
|
|
684
702
|
z_dim = self._labware.get_dimensions(labware_id=lw_data.id).z
|
|
685
703
|
height_over_labware: float = 0
|
|
@@ -833,6 +851,11 @@ class GeometryView:
|
|
|
833
851
|
f"Labware {self._labware.get_display_name(labware_id)} does not have a slot associated with it"
|
|
834
852
|
f" since it is no longer on the deck."
|
|
835
853
|
)
|
|
854
|
+
elif labware.location == WASTE_CHUTE_LOCATION:
|
|
855
|
+
raise errors.LabwareNotOnDeckError(
|
|
856
|
+
f"Labware {self._labware.get_display_name(labware_id)} does not have a slot associated with it"
|
|
857
|
+
f" since it is in the waste chute."
|
|
858
|
+
)
|
|
836
859
|
else:
|
|
837
860
|
_LOG.error(
|
|
838
861
|
f"Unhandled location type in get_ancestor_slot_name: {labware.location}"
|
|
@@ -1018,9 +1041,7 @@ class GeometryView:
|
|
|
1018
1041
|
def get_labware_grip_point(
|
|
1019
1042
|
self,
|
|
1020
1043
|
labware_definition: LabwareDefinition,
|
|
1021
|
-
location:
|
|
1022
|
-
DeckSlotLocation, ModuleLocation, OnLabwareLocation, AddressableAreaLocation
|
|
1023
|
-
],
|
|
1044
|
+
location: AccessibleByGripperLocation,
|
|
1024
1045
|
move_type: GripperMoveType,
|
|
1025
1046
|
user_additional_offset: Point | None,
|
|
1026
1047
|
) -> Point:
|
|
@@ -1047,9 +1068,7 @@ class GeometryView:
|
|
|
1047
1068
|
def _get_aa_origin_to_nominal_grip_point(
|
|
1048
1069
|
self,
|
|
1049
1070
|
labware_definition: LabwareDefinition,
|
|
1050
|
-
location:
|
|
1051
|
-
DeckSlotLocation, ModuleLocation, OnLabwareLocation, AddressableAreaLocation
|
|
1052
|
-
],
|
|
1071
|
+
location: AccessibleByGripperLocation,
|
|
1053
1072
|
move_type: GripperMoveType,
|
|
1054
1073
|
) -> Point:
|
|
1055
1074
|
"""Get the nominal grip point of a labware.
|
|
@@ -1440,9 +1459,16 @@ class GeometryView:
|
|
|
1440
1459
|
def ensure_valid_gripper_location(
|
|
1441
1460
|
location: LabwareLocation,
|
|
1442
1461
|
) -> Union[
|
|
1443
|
-
DeckSlotLocation,
|
|
1462
|
+
DeckSlotLocation,
|
|
1463
|
+
ModuleLocation,
|
|
1464
|
+
OnLabwareLocation,
|
|
1465
|
+
AddressableAreaLocation,
|
|
1444
1466
|
]:
|
|
1445
1467
|
"""Ensure valid on-deck location for gripper, otherwise raise error."""
|
|
1468
|
+
if location == WASTE_CHUTE_LOCATION:
|
|
1469
|
+
raise errors.LabwareMovementNotAllowedError(
|
|
1470
|
+
"Labware movements out of the waste chute are not supported using the gripper."
|
|
1471
|
+
)
|
|
1446
1472
|
if not isinstance(
|
|
1447
1473
|
location,
|
|
1448
1474
|
(
|
|
@@ -1457,6 +1483,28 @@ class GeometryView:
|
|
|
1457
1483
|
)
|
|
1458
1484
|
return location
|
|
1459
1485
|
|
|
1486
|
+
@staticmethod
|
|
1487
|
+
def ensure_valid_new_gripper_location(
|
|
1488
|
+
location: LabwareLocation,
|
|
1489
|
+
) -> AccessibleByGripperLocation:
|
|
1490
|
+
"""Ensure valid on-deck location for gripper, otherwise raise error."""
|
|
1491
|
+
if (
|
|
1492
|
+
not isinstance(
|
|
1493
|
+
location,
|
|
1494
|
+
(
|
|
1495
|
+
DeckSlotLocation,
|
|
1496
|
+
ModuleLocation,
|
|
1497
|
+
OnLabwareLocation,
|
|
1498
|
+
AddressableAreaLocation,
|
|
1499
|
+
),
|
|
1500
|
+
)
|
|
1501
|
+
and location != WASTE_CHUTE_LOCATION
|
|
1502
|
+
):
|
|
1503
|
+
raise errors.LabwareMovementNotAllowedError(
|
|
1504
|
+
"Off-deck labware movements are not supported using the gripper."
|
|
1505
|
+
)
|
|
1506
|
+
return location
|
|
1507
|
+
|
|
1460
1508
|
# todo(mm, 2024-11-05): This may be incorrect because it does not take the following
|
|
1461
1509
|
# offsets into account, which *are* taken into account for the actual gripper movement:
|
|
1462
1510
|
#
|
|
@@ -1677,6 +1725,12 @@ class GeometryView:
|
|
|
1677
1725
|
return self._recurse_labware_location_from_stacker_hopper(
|
|
1678
1726
|
labware_location, building
|
|
1679
1727
|
)
|
|
1728
|
+
elif labware_location == WASTE_CHUTE_LOCATION:
|
|
1729
|
+
return [
|
|
1730
|
+
NotOnDeckLocationSequenceComponent(
|
|
1731
|
+
logicalLocationName=WASTE_CHUTE_LOCATION
|
|
1732
|
+
)
|
|
1733
|
+
]
|
|
1680
1734
|
else:
|
|
1681
1735
|
_LOG.warn(f"Unhandled labware location kind: {labware_location}")
|
|
1682
1736
|
return building
|
|
@@ -2262,6 +2316,10 @@ class GeometryView:
|
|
|
2262
2316
|
raise errors.LocationNotAccessibleByPipetteError(
|
|
2263
2317
|
f"Cannot move pipette to {labware.loadName}, labware is off-deck."
|
|
2264
2318
|
)
|
|
2319
|
+
elif labware_location == WASTE_CHUTE_LOCATION:
|
|
2320
|
+
raise errors.LocationNotAccessibleByPipetteError(
|
|
2321
|
+
f"Cannot move pipette to {labware.loadName}, labware is in waste chute."
|
|
2322
|
+
)
|
|
2265
2323
|
elif isinstance(labware_location, ModuleLocation):
|
|
2266
2324
|
module = self._modules.get(labware_location.moduleId)
|
|
2267
2325
|
if ModuleModel.is_flex_stacker(module.model):
|
|
@@ -52,6 +52,7 @@ from ..types import (
|
|
|
52
52
|
LabwareOffsetLocationSequence,
|
|
53
53
|
LegacyLabwareOffsetLocation,
|
|
54
54
|
InStackerHopperLocation,
|
|
55
|
+
WASTE_CHUTE_LOCATION,
|
|
55
56
|
LabwareLocation,
|
|
56
57
|
LoadedLabware,
|
|
57
58
|
ModuleLocation,
|
|
@@ -343,14 +344,18 @@ class LabwareStore(HasState[LabwareState], HandlesActions):
|
|
|
343
344
|
self, labware_id: str, new_location: LabwareLocation, new_offset_id: str | None
|
|
344
345
|
) -> None:
|
|
345
346
|
self._state.labware_by_id[labware_id].offsetId = new_offset_id
|
|
346
|
-
|
|
347
347
|
if isinstance(new_location, AddressableAreaLocation) and (
|
|
348
|
-
fixture_validation.
|
|
349
|
-
or fixture_validation.is_trash(new_location.addressableAreaName)
|
|
348
|
+
fixture_validation.is_trash(new_location.addressableAreaName)
|
|
350
349
|
):
|
|
351
|
-
#
|
|
350
|
+
# TODO (RC, 2025-10-07: create a specific trash off deck location)
|
|
351
|
+
# If a labware has been moved into trash and is now technically off deck
|
|
352
352
|
new_location = OFF_DECK_LOCATION
|
|
353
|
-
|
|
353
|
+
elif isinstance(
|
|
354
|
+
new_location, AddressableAreaLocation
|
|
355
|
+
) and fixture_validation.is_gripper_waste_chute(
|
|
356
|
+
new_location.addressableAreaName
|
|
357
|
+
):
|
|
358
|
+
new_location = WASTE_CHUTE_LOCATION
|
|
354
359
|
self._state.labware_by_id[labware_id].location = new_location
|
|
355
360
|
|
|
356
361
|
def _set_labware_location(self, state_update: update_types.StateUpdate) -> None:
|
|
@@ -483,7 +488,6 @@ class LabwareView:
|
|
|
483
488
|
) -> Optional[LoadedLabware]:
|
|
484
489
|
"""Get the labware located in a given slot, if any."""
|
|
485
490
|
loaded_labware = list(self._state.labware_by_id.values())
|
|
486
|
-
|
|
487
491
|
for labware in loaded_labware:
|
|
488
492
|
if (
|
|
489
493
|
isinstance(labware.location, DeckSlotLocation)
|
|
@@ -49,6 +49,7 @@ from opentrons.protocol_engine.types import (
|
|
|
49
49
|
OnLabwareLocation,
|
|
50
50
|
LabwareMovementOffsetData,
|
|
51
51
|
LabwareOffsetVector,
|
|
52
|
+
WASTE_CHUTE_LOCATION,
|
|
52
53
|
)
|
|
53
54
|
|
|
54
55
|
_OFFSET_ON_TC_OT2 = Point(x=0, y=0, z=10.7)
|
|
@@ -174,9 +175,13 @@ def _get_stackup_origin_to_lw_origin(
|
|
|
174
175
|
slot_name=slot_name,
|
|
175
176
|
is_topmost_labware=False,
|
|
176
177
|
)
|
|
178
|
+
elif location == WASTE_CHUTE_LOCATION:
|
|
179
|
+
raise LabwareNotOnDeckError(
|
|
180
|
+
f"Cannot access {definition.metadata.displayName} because it is in the waste chute."
|
|
181
|
+
)
|
|
177
182
|
else:
|
|
178
183
|
raise LabwareNotOnDeckError(
|
|
179
|
-
"Cannot access
|
|
184
|
+
f"Cannot access {definition.metadata.displayName} since it is not on the deck. "
|
|
180
185
|
"Either it has been loaded off-deck or its been moved off-deck."
|
|
181
186
|
)
|
|
182
187
|
|
|
@@ -267,6 +267,7 @@ class ModuleStore(HasState[ModuleState], HandlesActions):
|
|
|
267
267
|
heater_shaker.SetTargetTemperatureResult,
|
|
268
268
|
heater_shaker.DeactivateHeaterResult,
|
|
269
269
|
heater_shaker.SetAndWaitForShakeSpeedResult,
|
|
270
|
+
heater_shaker.SetShakeSpeedResult,
|
|
270
271
|
heater_shaker.DeactivateShakerResult,
|
|
271
272
|
heater_shaker.OpenLabwareLatchResult,
|
|
272
273
|
heater_shaker.CloseLabwareLatchResult,
|
|
@@ -430,6 +431,7 @@ class ModuleStore(HasState[ModuleState], HandlesActions):
|
|
|
430
431
|
heater_shaker.SetTargetTemperature,
|
|
431
432
|
heater_shaker.DeactivateHeater,
|
|
432
433
|
heater_shaker.SetAndWaitForShakeSpeed,
|
|
434
|
+
heater_shaker.SetShakeSpeed,
|
|
433
435
|
heater_shaker.DeactivateShaker,
|
|
434
436
|
heater_shaker.OpenLabwareLatch,
|
|
435
437
|
heater_shaker.CloseLabwareLatch,
|
|
@@ -465,6 +467,13 @@ class ModuleStore(HasState[ModuleState], HandlesActions):
|
|
|
465
467
|
is_plate_shaking=True,
|
|
466
468
|
plate_target_temperature=prev_state.plate_target_temperature,
|
|
467
469
|
)
|
|
470
|
+
elif isinstance(command.result, heater_shaker.SetShakeSpeedResult):
|
|
471
|
+
self._state.substate_by_module_id[module_id] = HeaterShakerModuleSubState(
|
|
472
|
+
module_id=HeaterShakerModuleId(module_id),
|
|
473
|
+
labware_latch_status=prev_state.labware_latch_status,
|
|
474
|
+
is_plate_shaking=True,
|
|
475
|
+
plate_target_temperature=prev_state.plate_target_temperature,
|
|
476
|
+
)
|
|
468
477
|
elif isinstance(command.result, heater_shaker.DeactivateShakerResult):
|
|
469
478
|
self._state.substate_by_module_id[module_id] = HeaterShakerModuleSubState(
|
|
470
479
|
module_id=HeaterShakerModuleId(module_id),
|