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
|
@@ -13,7 +13,7 @@ from .pipetting_common import (
|
|
|
13
13
|
aspirate_while_tracking,
|
|
14
14
|
)
|
|
15
15
|
from .movement_common import (
|
|
16
|
-
|
|
16
|
+
DynamicLiquidHandlingWellLocationMixin,
|
|
17
17
|
DestinationPositionResult,
|
|
18
18
|
StallOrCollisionError,
|
|
19
19
|
move_to_well,
|
|
@@ -45,7 +45,7 @@ class AspirateWhileTrackingParams(
|
|
|
45
45
|
PipetteIdMixin,
|
|
46
46
|
AspirateVolumeMixin,
|
|
47
47
|
FlowRateMixin,
|
|
48
|
-
|
|
48
|
+
DynamicLiquidHandlingWellLocationMixin,
|
|
49
49
|
):
|
|
50
50
|
"""Parameters required to aspirate from a specific well."""
|
|
51
51
|
|
|
@@ -107,14 +107,20 @@ class AspirateWhileTrackingImplementation(
|
|
|
107
107
|
)
|
|
108
108
|
state_update = StateUpdate()
|
|
109
109
|
|
|
110
|
+
end_point = self._state_view.geometry.get_well_position(
|
|
111
|
+
labware_id=params.labwareId,
|
|
112
|
+
well_name=params.wellName,
|
|
113
|
+
well_location=params.trackToLocation,
|
|
114
|
+
operation_volume=-params.volume,
|
|
115
|
+
pipette_id=params.pipetteId,
|
|
116
|
+
)
|
|
110
117
|
move_result = await move_to_well(
|
|
111
118
|
movement=self._movement,
|
|
112
119
|
model_utils=self._model_utils,
|
|
113
120
|
pipette_id=params.pipetteId,
|
|
114
121
|
labware_id=params.labwareId,
|
|
115
122
|
well_name=params.wellName,
|
|
116
|
-
well_location=params.
|
|
117
|
-
operation_volume=-params.volume,
|
|
123
|
+
well_location=params.trackFromLocation,
|
|
118
124
|
)
|
|
119
125
|
state_update.append(move_result.state_update)
|
|
120
126
|
if isinstance(move_result, DefinedErrorData):
|
|
@@ -128,6 +134,7 @@ class AspirateWhileTrackingImplementation(
|
|
|
128
134
|
well_name=params.wellName,
|
|
129
135
|
volume=params.volume,
|
|
130
136
|
flow_rate=params.flowRate,
|
|
137
|
+
end_point=end_point,
|
|
131
138
|
location_if_error={
|
|
132
139
|
"retryLocation": (
|
|
133
140
|
move_result.public.position.x,
|
|
@@ -138,7 +145,48 @@ class AspirateWhileTrackingImplementation(
|
|
|
138
145
|
command_note_adder=self._command_note_adder,
|
|
139
146
|
pipetting=self._pipetting,
|
|
140
147
|
model_utils=self._model_utils,
|
|
148
|
+
movement_delay=params.movement_delay,
|
|
141
149
|
)
|
|
150
|
+
if isinstance(aspirate_result, DefinedErrorData):
|
|
151
|
+
if isinstance(aspirate_result.public, OverpressureError):
|
|
152
|
+
return DefinedErrorData(
|
|
153
|
+
public=OverpressureError(
|
|
154
|
+
id=aspirate_result.public.id,
|
|
155
|
+
createdAt=aspirate_result.public.createdAt,
|
|
156
|
+
wrappedErrors=aspirate_result.public.wrappedErrors,
|
|
157
|
+
errorInfo=aspirate_result.public.errorInfo,
|
|
158
|
+
),
|
|
159
|
+
state_update=aspirate_result.state_update.set_liquid_operated(
|
|
160
|
+
labware_id=params.labwareId,
|
|
161
|
+
well_names=self._state_view.geometry.get_wells_covered_by_pipette_with_active_well(
|
|
162
|
+
params.labwareId,
|
|
163
|
+
params.wellName,
|
|
164
|
+
params.pipetteId,
|
|
165
|
+
),
|
|
166
|
+
volume_added=CLEAR,
|
|
167
|
+
),
|
|
168
|
+
state_update_if_false_positive=aspirate_result.state_update_if_false_positive,
|
|
169
|
+
)
|
|
170
|
+
elif isinstance(aspirate_result.public, StallOrCollisionError):
|
|
171
|
+
return DefinedErrorData(
|
|
172
|
+
public=StallOrCollisionError(
|
|
173
|
+
id=aspirate_result.public.id,
|
|
174
|
+
createdAt=aspirate_result.public.createdAt,
|
|
175
|
+
wrappedErrors=aspirate_result.public.wrappedErrors,
|
|
176
|
+
errorInfo=aspirate_result.public.errorInfo,
|
|
177
|
+
),
|
|
178
|
+
state_update=aspirate_result.state_update.set_liquid_operated(
|
|
179
|
+
labware_id=params.labwareId,
|
|
180
|
+
well_names=self._state_view.geometry.get_wells_covered_by_pipette_with_active_well(
|
|
181
|
+
params.labwareId,
|
|
182
|
+
params.wellName,
|
|
183
|
+
params.pipetteId,
|
|
184
|
+
),
|
|
185
|
+
volume_added=CLEAR,
|
|
186
|
+
),
|
|
187
|
+
state_update_if_false_positive=aspirate_result.state_update_if_false_positive,
|
|
188
|
+
)
|
|
189
|
+
|
|
142
190
|
position_after_aspirate = await self._gantry_mover.get_position(
|
|
143
191
|
params.pipetteId
|
|
144
192
|
)
|
|
@@ -147,21 +195,6 @@ class AspirateWhileTrackingImplementation(
|
|
|
147
195
|
y=position_after_aspirate.y,
|
|
148
196
|
z=position_after_aspirate.z,
|
|
149
197
|
)
|
|
150
|
-
if isinstance(aspirate_result, DefinedErrorData):
|
|
151
|
-
return DefinedErrorData(
|
|
152
|
-
public=aspirate_result.public,
|
|
153
|
-
state_update=aspirate_result.state_update.set_liquid_operated(
|
|
154
|
-
labware_id=params.labwareId,
|
|
155
|
-
well_names=self._state_view.geometry.get_wells_covered_by_pipette_with_active_well(
|
|
156
|
-
params.labwareId,
|
|
157
|
-
params.wellName,
|
|
158
|
-
params.pipetteId,
|
|
159
|
-
),
|
|
160
|
-
volume_added=CLEAR,
|
|
161
|
-
),
|
|
162
|
-
state_update_if_false_positive=aspirate_result.state_update_if_false_positive,
|
|
163
|
-
)
|
|
164
|
-
|
|
165
198
|
return SuccessData(
|
|
166
199
|
public=AspirateWhileTrackingResult(
|
|
167
200
|
volume=aspirate_result.public.volume,
|
|
@@ -0,0 +1,302 @@
|
|
|
1
|
+
"""Command models to capture an image with a camera."""
|
|
2
|
+
from __future__ import annotations
|
|
3
|
+
from typing import Optional, TYPE_CHECKING, Tuple, Any
|
|
4
|
+
from datetime import datetime
|
|
5
|
+
|
|
6
|
+
from typing_extensions import Literal, Type
|
|
7
|
+
from pydantic import BaseModel, Field
|
|
8
|
+
from pydantic.json_schema import SkipJsonSchema
|
|
9
|
+
|
|
10
|
+
from opentrons_shared_data.data_files import MimeType
|
|
11
|
+
from opentrons.system.camera import (
|
|
12
|
+
CONTRAST_DEFAULT,
|
|
13
|
+
BRIGHTNESS_DEFAULT,
|
|
14
|
+
SATURATION_DEFAULT,
|
|
15
|
+
ZOOM_MIN,
|
|
16
|
+
ZOOM_MAX,
|
|
17
|
+
ZOOM_DEFAULT,
|
|
18
|
+
RESOLUTION_MIN,
|
|
19
|
+
RESOLUTION_MAX,
|
|
20
|
+
RESOLUTION_DEFAULT,
|
|
21
|
+
)
|
|
22
|
+
from ..types import PreconditionTypes
|
|
23
|
+
from .command import AbstractCommandImpl, BaseCommand, BaseCommandCreate, SuccessData
|
|
24
|
+
from ..errors import (
|
|
25
|
+
CameraDisabledError,
|
|
26
|
+
CameraSettingsInvalidError,
|
|
27
|
+
FileNameInvalidError,
|
|
28
|
+
)
|
|
29
|
+
from ..errors.error_occurrence import ErrorOccurrence
|
|
30
|
+
|
|
31
|
+
from ..resources.file_provider import (
|
|
32
|
+
ImageCaptureCmdFileNameMetadata,
|
|
33
|
+
)
|
|
34
|
+
from ..resources import FileProvider
|
|
35
|
+
from ..resources.file_provider import SPECIAL_CHARACTERS
|
|
36
|
+
from ..resources import CameraProvider
|
|
37
|
+
from ..resources.camera_provider import ImageParameters
|
|
38
|
+
from ..state import update_types
|
|
39
|
+
|
|
40
|
+
if TYPE_CHECKING:
|
|
41
|
+
from opentrons.protocol_engine.state.state import StateView
|
|
42
|
+
|
|
43
|
+
|
|
44
|
+
def _remove_default(s: dict[str, Any]) -> None:
|
|
45
|
+
s.pop("default", None)
|
|
46
|
+
|
|
47
|
+
|
|
48
|
+
CaptureImageCommandType = Literal["captureImage"]
|
|
49
|
+
|
|
50
|
+
|
|
51
|
+
class CaptureImageParams(BaseModel):
|
|
52
|
+
"""Input parameters for an image capture."""
|
|
53
|
+
|
|
54
|
+
fileName: str | SkipJsonSchema[None] = Field(
|
|
55
|
+
None,
|
|
56
|
+
description="Optional file name to use when storing the results of an Image Capture.",
|
|
57
|
+
json_schema_extra=_remove_default,
|
|
58
|
+
)
|
|
59
|
+
resolution: Optional[Tuple[int, int]] = Field(
|
|
60
|
+
None,
|
|
61
|
+
description="Width by height resolution in pixels for the image to be captured with.",
|
|
62
|
+
)
|
|
63
|
+
zoom: Optional[float] = Field(
|
|
64
|
+
None,
|
|
65
|
+
description="Multiplier to use when cropping and scaling a captured image. Scale is 1.0 to 2.0.",
|
|
66
|
+
)
|
|
67
|
+
pan: Optional[Tuple[int, int]] = Field(
|
|
68
|
+
None,
|
|
69
|
+
description="X/Y (pixels) position to pan to for a given zoom. Default is the center of the image.",
|
|
70
|
+
)
|
|
71
|
+
contrast: Optional[float] = Field(
|
|
72
|
+
None,
|
|
73
|
+
description="The contrast to use when processing an image. Scale is 0% to 100%.",
|
|
74
|
+
)
|
|
75
|
+
brightness: Optional[float] = Field(
|
|
76
|
+
None,
|
|
77
|
+
description="The brightness to use when processing an image. Scale is 0% to 100%.",
|
|
78
|
+
)
|
|
79
|
+
saturation: Optional[float] = Field(
|
|
80
|
+
None,
|
|
81
|
+
description="The saturation to use when processing an image. Scale is 0% to 100%.",
|
|
82
|
+
)
|
|
83
|
+
|
|
84
|
+
|
|
85
|
+
class CaptureImageResult(BaseModel):
|
|
86
|
+
"""Result data from running an image capture."""
|
|
87
|
+
|
|
88
|
+
fileId: Optional[str] = Field(
|
|
89
|
+
None,
|
|
90
|
+
description="File ID for image files output as a result of an image capture action.",
|
|
91
|
+
)
|
|
92
|
+
resolution: Tuple[int, int] = Field(
|
|
93
|
+
...,
|
|
94
|
+
description="Width by height resolution in pixels the image was captured with.",
|
|
95
|
+
)
|
|
96
|
+
zoom: float = Field(
|
|
97
|
+
...,
|
|
98
|
+
description="Multiplier used when cropping and scaling the captured image. Scale is 1.0 to 2.0.",
|
|
99
|
+
)
|
|
100
|
+
pan: Optional[Tuple[int, int]] = Field(
|
|
101
|
+
None,
|
|
102
|
+
description="X/Y (pixels) position panned to.",
|
|
103
|
+
)
|
|
104
|
+
contrast: float = Field(
|
|
105
|
+
...,
|
|
106
|
+
description="The contrast used when processing the image. Scale is 0% to 100%.",
|
|
107
|
+
)
|
|
108
|
+
brightness: float = Field(
|
|
109
|
+
...,
|
|
110
|
+
description="The brightness used when processing the image. Scale is 0% to 100%.",
|
|
111
|
+
)
|
|
112
|
+
saturation: float = Field(
|
|
113
|
+
...,
|
|
114
|
+
description="The saturation used when processing the image. Scale is 0% to 100%.",
|
|
115
|
+
)
|
|
116
|
+
|
|
117
|
+
|
|
118
|
+
def _converted_image_params(params: CaptureImageParams) -> ImageParameters:
|
|
119
|
+
return ImageParameters(
|
|
120
|
+
resolution=params.resolution,
|
|
121
|
+
zoom=params.zoom,
|
|
122
|
+
pan=params.pan,
|
|
123
|
+
contrast=(
|
|
124
|
+
(params.contrast / 100) * 2.0 if params.contrast is not None else None
|
|
125
|
+
),
|
|
126
|
+
brightness=(
|
|
127
|
+
int(((params.brightness * 256) // 100) - 128) * -1
|
|
128
|
+
if params.brightness is not None
|
|
129
|
+
else None
|
|
130
|
+
),
|
|
131
|
+
saturation=(
|
|
132
|
+
(params.saturation / 100) * 2.0 if params.saturation is not None else None
|
|
133
|
+
),
|
|
134
|
+
)
|
|
135
|
+
|
|
136
|
+
|
|
137
|
+
def _revert_image_parameters(
|
|
138
|
+
file_id: Optional[str], image_params: ImageParameters
|
|
139
|
+
) -> CaptureImageResult:
|
|
140
|
+
contrast = (
|
|
141
|
+
image_params.contrast if image_params.contrast is not None else CONTRAST_DEFAULT
|
|
142
|
+
)
|
|
143
|
+
brightness = (
|
|
144
|
+
image_params.brightness
|
|
145
|
+
if image_params.brightness is not None
|
|
146
|
+
else BRIGHTNESS_DEFAULT
|
|
147
|
+
)
|
|
148
|
+
saturation = (
|
|
149
|
+
image_params.saturation
|
|
150
|
+
if image_params.saturation is not None
|
|
151
|
+
else SATURATION_DEFAULT
|
|
152
|
+
)
|
|
153
|
+
# todo (chb, 2025-10-29): Eventually we will have override defaults that can be passed into the Camera state, load those if they exist
|
|
154
|
+
|
|
155
|
+
return CaptureImageResult(
|
|
156
|
+
fileId=file_id,
|
|
157
|
+
resolution=image_params.resolution
|
|
158
|
+
if image_params.resolution is not None
|
|
159
|
+
else RESOLUTION_DEFAULT,
|
|
160
|
+
zoom=image_params.zoom if image_params.zoom is not None else ZOOM_DEFAULT,
|
|
161
|
+
pan=image_params.pan,
|
|
162
|
+
contrast=(contrast / 2) * 100.0,
|
|
163
|
+
brightness=round((((brightness * -1) + 128) * 100) / 256),
|
|
164
|
+
saturation=(saturation / 2) * 100.0,
|
|
165
|
+
)
|
|
166
|
+
|
|
167
|
+
|
|
168
|
+
def _validate_image_params(params: CaptureImageParams) -> None:
|
|
169
|
+
# Validate the filename param provided to fail analysis
|
|
170
|
+
if params.fileName is not None and set(SPECIAL_CHARACTERS).intersection(
|
|
171
|
+
set(params.fileName)
|
|
172
|
+
):
|
|
173
|
+
raise FileNameInvalidError(
|
|
174
|
+
message=f"Capture image filename cannot contain character(s): {SPECIAL_CHARACTERS.intersection(set(params.fileName))}"
|
|
175
|
+
)
|
|
176
|
+
|
|
177
|
+
# Validate the image filter parameters
|
|
178
|
+
if params.zoom is not None and (params.zoom < ZOOM_MIN or params.zoom > ZOOM_MAX):
|
|
179
|
+
raise CameraSettingsInvalidError(
|
|
180
|
+
message="Capture image zoom must be a valid value from 1.0X to 2.0X zoom."
|
|
181
|
+
)
|
|
182
|
+
if params.resolution is not None and (
|
|
183
|
+
params.resolution[0] < RESOLUTION_MIN[0]
|
|
184
|
+
or params.resolution[1] < RESOLUTION_MIN[1]
|
|
185
|
+
or params.resolution[0] > RESOLUTION_MAX[0]
|
|
186
|
+
or params.resolution[1] > RESOLUTION_MAX[1]
|
|
187
|
+
):
|
|
188
|
+
raise CameraSettingsInvalidError(
|
|
189
|
+
message="Capture image resolution must be a valid resolution from 240p through 8K resolutuon."
|
|
190
|
+
)
|
|
191
|
+
if params.brightness is not None and (
|
|
192
|
+
params.brightness < 0 or params.brightness > 100
|
|
193
|
+
):
|
|
194
|
+
raise CameraSettingsInvalidError(
|
|
195
|
+
message="Capture image brightness must be a percentage from 0% to 100%."
|
|
196
|
+
)
|
|
197
|
+
if params.contrast is not None and (params.contrast < 0 or params.contrast > 100):
|
|
198
|
+
raise CameraSettingsInvalidError(
|
|
199
|
+
message="Capture image contrast must be a percentage from 0% to 100%."
|
|
200
|
+
)
|
|
201
|
+
if params.saturation is not None and (
|
|
202
|
+
params.saturation < 0 or params.saturation > 100
|
|
203
|
+
):
|
|
204
|
+
raise CameraSettingsInvalidError(
|
|
205
|
+
message="Capture image saturation must be a percentage from 0% to 100%."
|
|
206
|
+
)
|
|
207
|
+
|
|
208
|
+
|
|
209
|
+
class CaptureImageImpl(
|
|
210
|
+
AbstractCommandImpl[CaptureImageParams, SuccessData[CaptureImageResult]]
|
|
211
|
+
):
|
|
212
|
+
"""Execution implementation of an image capture."""
|
|
213
|
+
|
|
214
|
+
def __init__(
|
|
215
|
+
self,
|
|
216
|
+
state_view: StateView,
|
|
217
|
+
file_provider: FileProvider,
|
|
218
|
+
camera_provider: CameraProvider,
|
|
219
|
+
**unused_dependencies: object,
|
|
220
|
+
) -> None:
|
|
221
|
+
self._state_view = state_view
|
|
222
|
+
self._file_provider = file_provider
|
|
223
|
+
self._camera_provider = camera_provider
|
|
224
|
+
|
|
225
|
+
async def execute(
|
|
226
|
+
self, params: CaptureImageParams
|
|
227
|
+
) -> SuccessData[CaptureImageResult]:
|
|
228
|
+
"""Initiate an image capture with a camera."""
|
|
229
|
+
state_update = update_types.StateUpdate()
|
|
230
|
+
state_update.precondition_update = update_types.PreconditionUpdate(
|
|
231
|
+
{PreconditionTypes.IS_CAMERA_USED: True}
|
|
232
|
+
)
|
|
233
|
+
|
|
234
|
+
# Validate that the provided parameters are all acceptable. We do this here and in system/camera.py to ensure analysis fails properly.
|
|
235
|
+
_validate_image_params(params)
|
|
236
|
+
|
|
237
|
+
# Handle capturing an image with the CameraProvider - Engine camera settings take priority
|
|
238
|
+
camera_settings = await self._camera_provider.get_camera_settings()
|
|
239
|
+
engine_camera_settings = self._state_view.camera.get_enablement_settings()
|
|
240
|
+
if (
|
|
241
|
+
engine_camera_settings is None and camera_settings.cameraEnabled is False
|
|
242
|
+
) or (
|
|
243
|
+
engine_camera_settings is not None
|
|
244
|
+
and engine_camera_settings.cameraEnabled is False
|
|
245
|
+
):
|
|
246
|
+
raise CameraDisabledError(
|
|
247
|
+
"Cannot capture image because Camera is disabled."
|
|
248
|
+
)
|
|
249
|
+
|
|
250
|
+
parameters = _converted_image_params(params=params)
|
|
251
|
+
camera_data = await self._camera_provider.capture_image(
|
|
252
|
+
self._state_view.config.robot_type, parameters
|
|
253
|
+
)
|
|
254
|
+
|
|
255
|
+
# Conditionally save file if camera data was returned - in simulation we don't return anything.
|
|
256
|
+
file_id: str | None = None
|
|
257
|
+
if camera_data:
|
|
258
|
+
this_cmd_id = self._state_view.commands.get_running_command_id()
|
|
259
|
+
prev_cmd = self._state_view.commands.get_most_recently_finalized_command()
|
|
260
|
+
prev_cmd_id = prev_cmd.command.id if prev_cmd is not None else None
|
|
261
|
+
|
|
262
|
+
file_info = await self._file_provider.write_file(
|
|
263
|
+
data=camera_data,
|
|
264
|
+
mime_type=MimeType.IMAGE_JPEG,
|
|
265
|
+
command_metadata=ImageCaptureCmdFileNameMetadata(
|
|
266
|
+
step_number=len(self._state_view.commands.get_all()),
|
|
267
|
+
command_timestamp=datetime.now(),
|
|
268
|
+
base_filename=params.fileName,
|
|
269
|
+
command_id=this_cmd_id or "",
|
|
270
|
+
prev_command_id=prev_cmd_id or "",
|
|
271
|
+
),
|
|
272
|
+
)
|
|
273
|
+
file_id = file_info.id
|
|
274
|
+
state_update.files_added = update_types.FilesAddedUpdate(file_ids=[file_id])
|
|
275
|
+
|
|
276
|
+
result = _revert_image_parameters(file_id=file_id, image_params=parameters)
|
|
277
|
+
|
|
278
|
+
return SuccessData(
|
|
279
|
+
public=result,
|
|
280
|
+
state_update=state_update,
|
|
281
|
+
)
|
|
282
|
+
|
|
283
|
+
|
|
284
|
+
class CaptureImage(
|
|
285
|
+
BaseCommand[CaptureImageParams, CaptureImageResult, ErrorOccurrence]
|
|
286
|
+
):
|
|
287
|
+
"""A command to execute an Absorbance Reader measurement."""
|
|
288
|
+
|
|
289
|
+
commandType: CaptureImageCommandType = "captureImage"
|
|
290
|
+
params: CaptureImageParams
|
|
291
|
+
result: Optional[CaptureImageResult] = None
|
|
292
|
+
|
|
293
|
+
_ImplementationCls: Type[CaptureImageImpl] = CaptureImageImpl
|
|
294
|
+
|
|
295
|
+
|
|
296
|
+
class CaptureImageCreate(BaseCommandCreate[CaptureImageParams]):
|
|
297
|
+
"""A request to execute an Absorbance Reader measurement."""
|
|
298
|
+
|
|
299
|
+
commandType: CaptureImageCommandType = "captureImage"
|
|
300
|
+
params: CaptureImageParams
|
|
301
|
+
|
|
302
|
+
_CommandCls: Type[CaptureImage] = CaptureImage
|
|
@@ -178,6 +178,7 @@ class AbstractCommandImpl(
|
|
|
178
178
|
hardware_api: HardwareControlAPI,
|
|
179
179
|
equipment: execution.EquipmentHandler,
|
|
180
180
|
file_provider: execution.FileProvider,
|
|
181
|
+
camera_provider: execution.CameraProvider,
|
|
181
182
|
movement: execution.MovementHandler,
|
|
182
183
|
gantry_mover: execution.GantryMover,
|
|
183
184
|
labware_movement: execution.LabwareMovementHandler,
|
|
@@ -441,6 +441,14 @@ from .identify_module import (
|
|
|
441
441
|
IdentifyModuleCommandType,
|
|
442
442
|
)
|
|
443
443
|
|
|
444
|
+
from .capture_image import (
|
|
445
|
+
CaptureImage,
|
|
446
|
+
CaptureImageParams,
|
|
447
|
+
CaptureImageCreate,
|
|
448
|
+
CaptureImageResult,
|
|
449
|
+
CaptureImageCommandType,
|
|
450
|
+
)
|
|
451
|
+
|
|
444
452
|
Command = Annotated[
|
|
445
453
|
Union[
|
|
446
454
|
AirGapInPlace,
|
|
@@ -494,6 +502,7 @@ Command = Annotated[
|
|
|
494
502
|
SealPipetteToTip,
|
|
495
503
|
PressureDispense,
|
|
496
504
|
UnsealPipetteFromTip,
|
|
505
|
+
CaptureImage,
|
|
497
506
|
heater_shaker.WaitForTemperature,
|
|
498
507
|
heater_shaker.SetTargetTemperature,
|
|
499
508
|
heater_shaker.DeactivateHeater,
|
|
@@ -602,6 +611,7 @@ CommandParams = Union[
|
|
|
602
611
|
SealPipetteToTipParams,
|
|
603
612
|
PressureDispenseParams,
|
|
604
613
|
UnsealPipetteFromTipParams,
|
|
614
|
+
CaptureImageParams,
|
|
605
615
|
heater_shaker.WaitForTemperatureParams,
|
|
606
616
|
heater_shaker.SetTargetTemperatureParams,
|
|
607
617
|
heater_shaker.DeactivateHeaterParams,
|
|
@@ -708,6 +718,7 @@ CommandType = Union[
|
|
|
708
718
|
SealPipetteToTipCommandType,
|
|
709
719
|
PressureDispenseCommandType,
|
|
710
720
|
UnsealPipetteFromTipCommandType,
|
|
721
|
+
CaptureImageCommandType,
|
|
711
722
|
heater_shaker.WaitForTemperatureCommandType,
|
|
712
723
|
heater_shaker.SetTargetTemperatureCommandType,
|
|
713
724
|
heater_shaker.DeactivateHeaterCommandType,
|
|
@@ -815,6 +826,7 @@ CommandCreate = Annotated[
|
|
|
815
826
|
SealPipetteToTipCreate,
|
|
816
827
|
PressureDispenseCreate,
|
|
817
828
|
UnsealPipetteFromTipCreate,
|
|
829
|
+
CaptureImageCreate,
|
|
818
830
|
heater_shaker.WaitForTemperatureCreate,
|
|
819
831
|
heater_shaker.SetTargetTemperatureCreate,
|
|
820
832
|
heater_shaker.DeactivateHeaterCreate,
|
|
@@ -930,6 +942,7 @@ CommandResult = Union[
|
|
|
930
942
|
SealPipetteToTipResult,
|
|
931
943
|
PressureDispenseResult,
|
|
932
944
|
UnsealPipetteFromTipResult,
|
|
945
|
+
CaptureImageResult,
|
|
933
946
|
heater_shaker.WaitForTemperatureResult,
|
|
934
947
|
heater_shaker.SetTargetTemperatureResult,
|
|
935
948
|
heater_shaker.DeactivateHeaterResult,
|
|
@@ -19,7 +19,7 @@ from .pipetting_common import (
|
|
|
19
19
|
dispense_while_tracking,
|
|
20
20
|
)
|
|
21
21
|
from .movement_common import (
|
|
22
|
-
|
|
22
|
+
DynamicLiquidHandlingWellLocationMixin,
|
|
23
23
|
DestinationPositionResult,
|
|
24
24
|
StallOrCollisionError,
|
|
25
25
|
move_to_well,
|
|
@@ -49,7 +49,7 @@ class DispenseWhileTrackingParams(
|
|
|
49
49
|
PipetteIdMixin,
|
|
50
50
|
DispenseVolumeMixin,
|
|
51
51
|
FlowRateMixin,
|
|
52
|
-
|
|
52
|
+
DynamicLiquidHandlingWellLocationMixin,
|
|
53
53
|
):
|
|
54
54
|
"""Payload required to dispense to a specific well."""
|
|
55
55
|
|
|
@@ -100,14 +100,24 @@ class DispenseWhileTrackingImplementation(
|
|
|
100
100
|
# TODO(pbm, 10-15-24): call self._state_view.geometry.validate_dispense_volume_into_well()
|
|
101
101
|
|
|
102
102
|
state_update = StateUpdate()
|
|
103
|
+
|
|
104
|
+
end_point = self._state_view.geometry.get_well_position(
|
|
105
|
+
labware_id=params.labwareId,
|
|
106
|
+
well_name=params.wellName,
|
|
107
|
+
well_location=params.trackToLocation,
|
|
108
|
+
operation_volume=params.volume,
|
|
109
|
+
pipette_id=params.pipetteId,
|
|
110
|
+
)
|
|
111
|
+
|
|
103
112
|
move_result = await move_to_well(
|
|
104
113
|
movement=self._movement,
|
|
105
114
|
model_utils=self._model_utils,
|
|
106
115
|
pipette_id=params.pipetteId,
|
|
107
116
|
labware_id=params.labwareId,
|
|
108
117
|
well_name=params.wellName,
|
|
109
|
-
well_location=params.
|
|
118
|
+
well_location=params.trackFromLocation,
|
|
110
119
|
)
|
|
120
|
+
|
|
111
121
|
state_update.append(move_result.state_update)
|
|
112
122
|
if isinstance(move_result, DefinedErrorData):
|
|
113
123
|
return DefinedErrorData(
|
|
@@ -120,6 +130,7 @@ class DispenseWhileTrackingImplementation(
|
|
|
120
130
|
well_name=well_name,
|
|
121
131
|
volume=params.volume,
|
|
122
132
|
flow_rate=params.flowRate,
|
|
133
|
+
end_point=end_point,
|
|
123
134
|
push_out=params.pushOut,
|
|
124
135
|
location_if_error={
|
|
125
136
|
"retryLocation": (
|
|
@@ -130,7 +141,49 @@ class DispenseWhileTrackingImplementation(
|
|
|
130
141
|
},
|
|
131
142
|
pipetting=self._pipetting,
|
|
132
143
|
model_utils=self._model_utils,
|
|
144
|
+
movement_delay=params.movement_delay,
|
|
133
145
|
)
|
|
146
|
+
|
|
147
|
+
if isinstance(dispense_result, DefinedErrorData):
|
|
148
|
+
if isinstance(dispense_result.public, OverpressureError):
|
|
149
|
+
return DefinedErrorData(
|
|
150
|
+
public=OverpressureError(
|
|
151
|
+
id=dispense_result.public.id,
|
|
152
|
+
createdAt=dispense_result.public.createdAt,
|
|
153
|
+
wrappedErrors=dispense_result.public.wrappedErrors,
|
|
154
|
+
errorInfo=dispense_result.public.errorInfo,
|
|
155
|
+
),
|
|
156
|
+
state_update=dispense_result.state_update.set_liquid_operated(
|
|
157
|
+
labware_id=params.labwareId,
|
|
158
|
+
well_names=self._state_view.geometry.get_wells_covered_by_pipette_with_active_well(
|
|
159
|
+
params.labwareId,
|
|
160
|
+
params.wellName,
|
|
161
|
+
params.pipetteId,
|
|
162
|
+
),
|
|
163
|
+
volume_added=CLEAR,
|
|
164
|
+
),
|
|
165
|
+
state_update_if_false_positive=dispense_result.state_update_if_false_positive,
|
|
166
|
+
)
|
|
167
|
+
elif isinstance(dispense_result.public, StallOrCollisionError):
|
|
168
|
+
return DefinedErrorData(
|
|
169
|
+
public=StallOrCollisionError(
|
|
170
|
+
id=dispense_result.public.id,
|
|
171
|
+
createdAt=dispense_result.public.createdAt,
|
|
172
|
+
wrappedErrors=dispense_result.public.wrappedErrors,
|
|
173
|
+
errorInfo=dispense_result.public.errorInfo,
|
|
174
|
+
),
|
|
175
|
+
state_update=dispense_result.state_update.set_liquid_operated(
|
|
176
|
+
labware_id=params.labwareId,
|
|
177
|
+
well_names=self._state_view.geometry.get_wells_covered_by_pipette_with_active_well(
|
|
178
|
+
params.labwareId,
|
|
179
|
+
params.wellName,
|
|
180
|
+
params.pipetteId,
|
|
181
|
+
),
|
|
182
|
+
volume_added=CLEAR,
|
|
183
|
+
),
|
|
184
|
+
state_update_if_false_positive=dispense_result.state_update_if_false_positive,
|
|
185
|
+
)
|
|
186
|
+
|
|
134
187
|
position_after_dispense = await self._gantry_mover.get_position(
|
|
135
188
|
params.pipetteId
|
|
136
189
|
)
|
|
@@ -139,22 +192,6 @@ class DispenseWhileTrackingImplementation(
|
|
|
139
192
|
y=position_after_dispense.y,
|
|
140
193
|
z=position_after_dispense.z,
|
|
141
194
|
)
|
|
142
|
-
|
|
143
|
-
if isinstance(dispense_result, DefinedErrorData):
|
|
144
|
-
return DefinedErrorData(
|
|
145
|
-
public=dispense_result.public,
|
|
146
|
-
state_update=dispense_result.state_update.set_liquid_operated(
|
|
147
|
-
labware_id=params.labwareId,
|
|
148
|
-
well_names=self._state_view.geometry.get_wells_covered_by_pipette_with_active_well(
|
|
149
|
-
params.labwareId,
|
|
150
|
-
params.wellName,
|
|
151
|
-
params.pipetteId,
|
|
152
|
-
),
|
|
153
|
-
volume_added=CLEAR,
|
|
154
|
-
),
|
|
155
|
-
state_update_if_false_positive=dispense_result.state_update_if_false_positive,
|
|
156
|
-
)
|
|
157
|
-
|
|
158
195
|
return SuccessData(
|
|
159
196
|
public=DispenseWhileTrackingResult(
|
|
160
197
|
volume=dispense_result.public.volume,
|
|
@@ -10,6 +10,10 @@ from opentrons_shared_data.errors import ErrorCodes
|
|
|
10
10
|
from opentrons_shared_data.errors.exceptions import CommandPreconditionViolated
|
|
11
11
|
from opentrons_shared_data.labware.labware_definition import LabwareDefinition
|
|
12
12
|
|
|
13
|
+
from opentrons.protocol_engine.errors.exceptions import (
|
|
14
|
+
LabwarePoolNotCompatibleWithModuleError,
|
|
15
|
+
)
|
|
16
|
+
|
|
13
17
|
|
|
14
18
|
from ...errors import ErrorOccurrence
|
|
15
19
|
from ...types import (
|
|
@@ -35,6 +39,15 @@ if TYPE_CHECKING:
|
|
|
35
39
|
from opentrons.protocol_engine.execution.equipment import LoadedLabwarePoolData
|
|
36
40
|
from opentrons.protocol_engine.state.module_substates import FlexStackerSubState
|
|
37
41
|
|
|
42
|
+
|
|
43
|
+
# The stacker cannot dispense labware where there is no gap between the top surface
|
|
44
|
+
# of the bottom labware being dispensed, and bottom surface of the top labware.
|
|
45
|
+
# This is because the stacker latch, which holds the labware stack, needs enough
|
|
46
|
+
# empty space to free the bottom labware, but still hold the top labware once it
|
|
47
|
+
# closes.
|
|
48
|
+
STACKER_INCOMPATIBLE_LABWARE = set(["opentrons_tough_universal_lid"])
|
|
49
|
+
|
|
50
|
+
|
|
38
51
|
INITIAL_COUNT_DESCRIPTION = dedent(
|
|
39
52
|
"""\
|
|
40
53
|
The number of labware that should be initially stored in the stacker. This number will be silently clamped to
|
|
@@ -911,3 +924,25 @@ def build_retrieve_labware_move_updates(
|
|
|
911
924
|
lid_offset_location,
|
|
912
925
|
)
|
|
913
926
|
return locations_for_ids, offset_ids_by_id
|
|
927
|
+
|
|
928
|
+
|
|
929
|
+
def validate_labware_pool_compatible_with_stacker(
|
|
930
|
+
pool_primary_definition: LabwareDefinition,
|
|
931
|
+
pool_adapter_definition: LabwareDefinition | None,
|
|
932
|
+
pool_lid_definition: LabwareDefinition | None,
|
|
933
|
+
) -> None:
|
|
934
|
+
"""Verifies that the given labware pool is compatible with the stacker."""
|
|
935
|
+
labware_pool = set(
|
|
936
|
+
lw.parameters.loadName
|
|
937
|
+
for lw in [
|
|
938
|
+
pool_primary_definition,
|
|
939
|
+
pool_adapter_definition,
|
|
940
|
+
pool_lid_definition,
|
|
941
|
+
]
|
|
942
|
+
if lw is not None
|
|
943
|
+
)
|
|
944
|
+
incompatible_labware = list(labware_pool & STACKER_INCOMPATIBLE_LABWARE)
|
|
945
|
+
if incompatible_labware:
|
|
946
|
+
raise LabwarePoolNotCompatibleWithModuleError(
|
|
947
|
+
f"The stacker cannot store {incompatible_labware}"
|
|
948
|
+
)
|
|
@@ -31,6 +31,7 @@ from .common import (
|
|
|
31
31
|
primary_location_sequences,
|
|
32
32
|
adapter_location_sequences_with_default,
|
|
33
33
|
lid_location_sequences_with_default,
|
|
34
|
+
validate_labware_pool_compatible_with_stacker,
|
|
34
35
|
)
|
|
35
36
|
|
|
36
37
|
if TYPE_CHECKING:
|
|
@@ -210,6 +211,12 @@ class SetStoredLabwareImpl(
|
|
|
210
211
|
)
|
|
211
212
|
)
|
|
212
213
|
|
|
214
|
+
validate_labware_pool_compatible_with_stacker(
|
|
215
|
+
pool_primary_definition=labware_def,
|
|
216
|
+
pool_adapter_definition=adapter_def,
|
|
217
|
+
pool_lid_definition=lid_def,
|
|
218
|
+
)
|
|
219
|
+
|
|
213
220
|
pool_height = self._state_view.geometry.get_height_of_labware_stack(
|
|
214
221
|
pool_definitions
|
|
215
222
|
)
|
|
@@ -100,7 +100,7 @@ class SetShakeSpeedImpl(
|
|
|
100
100
|
async def start_shake(task_handler: TaskHandler) -> None:
|
|
101
101
|
if hs_hardware_module is not None:
|
|
102
102
|
async with task_handler.synchronize_cancel_previous(
|
|
103
|
-
hs_module_substate.module_id
|
|
103
|
+
hs_module_substate.module_id + "-shake"
|
|
104
104
|
):
|
|
105
105
|
await hs_hardware_module.set_speed(rpm=validated_speed)
|
|
106
106
|
|