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.
Potentially problematic release.
This version of opentrons might be problematic. Click here for more details.
- 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
|
@@ -10,12 +10,15 @@ from opentrons_shared_data.errors.exceptions import (
|
|
|
10
10
|
EnumeratedError,
|
|
11
11
|
PythonException,
|
|
12
12
|
)
|
|
13
|
+
from opentrons_shared_data.data_files import MimeType
|
|
13
14
|
|
|
14
15
|
from opentrons.protocol_engine.commands.command import SuccessData
|
|
15
16
|
from opentrons.protocol_engine.notes import make_error_recovery_debug_note
|
|
16
17
|
|
|
17
18
|
from ..state.state import StateStore
|
|
18
|
-
from ..resources import ModelUtils, FileProvider
|
|
19
|
+
from ..resources import ModelUtils, FileProvider, CameraProvider
|
|
20
|
+
from ..resources.file_provider import ImageCaptureCmdFileNameMetadata
|
|
21
|
+
from ..resources.camera_provider import ImageParameters
|
|
19
22
|
from ..commands import CommandStatus
|
|
20
23
|
from ..actions import (
|
|
21
24
|
ActionDispatcher,
|
|
@@ -36,6 +39,7 @@ from .run_control import RunControlHandler
|
|
|
36
39
|
from .rail_lights import RailLightsHandler
|
|
37
40
|
from .status_bar import StatusBarHandler
|
|
38
41
|
from .task_handler import TaskHandler
|
|
42
|
+
from ..commands import Command
|
|
39
43
|
|
|
40
44
|
|
|
41
45
|
log = getLogger(__name__)
|
|
@@ -75,6 +79,7 @@ class CommandExecutor:
|
|
|
75
79
|
self,
|
|
76
80
|
hardware_api: HardwareControlAPI,
|
|
77
81
|
file_provider: FileProvider,
|
|
82
|
+
camera_provider: CameraProvider,
|
|
78
83
|
state_store: StateStore,
|
|
79
84
|
action_dispatcher: ActionDispatcher,
|
|
80
85
|
equipment: EquipmentHandler,
|
|
@@ -93,6 +98,7 @@ class CommandExecutor:
|
|
|
93
98
|
"""Initialize the CommandExecutor with access to its dependencies."""
|
|
94
99
|
self._hardware_api = hardware_api
|
|
95
100
|
self._file_provider = file_provider
|
|
101
|
+
self._camera_provider = camera_provider
|
|
96
102
|
self._state_store = state_store
|
|
97
103
|
self._action_dispatcher = action_dispatcher
|
|
98
104
|
self._equipment = equipment
|
|
@@ -123,6 +129,7 @@ class CommandExecutor:
|
|
|
123
129
|
state_view=self._state_store,
|
|
124
130
|
hardware_api=self._hardware_api,
|
|
125
131
|
file_provider=self._file_provider,
|
|
132
|
+
camera_provider=self._camera_provider,
|
|
126
133
|
equipment=self._equipment,
|
|
127
134
|
movement=self._movement,
|
|
128
135
|
gantry_mover=self._gantry_mover,
|
|
@@ -148,6 +155,7 @@ class CommandExecutor:
|
|
|
148
155
|
log.debug(
|
|
149
156
|
f"Executing {running_command.id}, {running_command.commandType}, {running_command.params}"
|
|
150
157
|
)
|
|
158
|
+
error_occurred = False
|
|
151
159
|
try:
|
|
152
160
|
result = await command_impl.execute(
|
|
153
161
|
running_command.params # type: ignore[arg-type]
|
|
@@ -171,6 +179,7 @@ class CommandExecutor:
|
|
|
171
179
|
running_command,
|
|
172
180
|
None,
|
|
173
181
|
)
|
|
182
|
+
|
|
174
183
|
note_tracker(make_error_recovery_debug_note(error_recovery_type))
|
|
175
184
|
self._action_dispatcher.dispatch(
|
|
176
185
|
FailCommandAction(
|
|
@@ -183,6 +192,7 @@ class CommandExecutor:
|
|
|
183
192
|
type=error_recovery_type,
|
|
184
193
|
)
|
|
185
194
|
)
|
|
195
|
+
error_occurred = True
|
|
186
196
|
|
|
187
197
|
else:
|
|
188
198
|
if isinstance(result, SuccessData):
|
|
@@ -218,7 +228,50 @@ class CommandExecutor:
|
|
|
218
228
|
type=error_recovery_type,
|
|
219
229
|
)
|
|
220
230
|
)
|
|
231
|
+
error_occurred = True
|
|
232
|
+
finally:
|
|
233
|
+
# Handle error image capture if appropriate
|
|
234
|
+
if error_occurred:
|
|
235
|
+
await self.capture_error_image(running_command)
|
|
221
236
|
|
|
222
237
|
def cancel_tasks(self, message: str | None = None) -> None:
|
|
223
238
|
"""Cancel all concurrent tasks."""
|
|
224
239
|
self._task_handler.cancel_all(message=message)
|
|
240
|
+
|
|
241
|
+
async def capture_error_image(self, running_command: Command) -> None:
|
|
242
|
+
"""Capture an image of an error event."""
|
|
243
|
+
try:
|
|
244
|
+
camera_enablement = self._state_store.camera.get_enablement_settings()
|
|
245
|
+
if camera_enablement is None:
|
|
246
|
+
# Utilize the global camera settings
|
|
247
|
+
camera_enablement = await self._camera_provider.get_camera_settings()
|
|
248
|
+
# Only capture photos of errors if the setting to do so is enabled
|
|
249
|
+
if (
|
|
250
|
+
camera_enablement.cameraEnabled
|
|
251
|
+
and camera_enablement.errorRecoveryEnabled
|
|
252
|
+
):
|
|
253
|
+
# todo(chb, 2025-10-25): Eventually we will need to pass in client provided global settings here
|
|
254
|
+
image_data = await self._camera_provider.capture_image(
|
|
255
|
+
self._state_store.config.robot_type, ImageParameters()
|
|
256
|
+
)
|
|
257
|
+
commands = self._state_store.commands.get_all()
|
|
258
|
+
prev_command_id = commands[-2].id if len(commands) > 1 else ""
|
|
259
|
+
if image_data:
|
|
260
|
+
write_result = await self._file_provider.write_file(
|
|
261
|
+
data=image_data,
|
|
262
|
+
mime_type=MimeType.IMAGE_JPEG,
|
|
263
|
+
command_metadata=ImageCaptureCmdFileNameMetadata(
|
|
264
|
+
command_id=running_command.id,
|
|
265
|
+
prev_command_id=prev_command_id,
|
|
266
|
+
step_number=len(commands),
|
|
267
|
+
base_filename=None,
|
|
268
|
+
command_timestamp=running_command.createdAt,
|
|
269
|
+
),
|
|
270
|
+
)
|
|
271
|
+
log.info(
|
|
272
|
+
f"Image captured of error event with file name: {write_result.name}"
|
|
273
|
+
)
|
|
274
|
+
except Exception as e:
|
|
275
|
+
log.info(
|
|
276
|
+
f"Failed to capture image of error with the following exception: {e}"
|
|
277
|
+
)
|
|
@@ -6,7 +6,7 @@ from opentrons.protocol_engine.execution.rail_lights import RailLightsHandler
|
|
|
6
6
|
|
|
7
7
|
from ..state.state import StateStore
|
|
8
8
|
from ..actions import ActionDispatcher
|
|
9
|
-
from ..resources import FileProvider
|
|
9
|
+
from ..resources import FileProvider, CameraProvider
|
|
10
10
|
from .equipment import EquipmentHandler
|
|
11
11
|
from .movement import MovementHandler
|
|
12
12
|
from .gantry_mover import create_gantry_mover
|
|
@@ -23,6 +23,7 @@ from .task_handler import TaskHandler
|
|
|
23
23
|
def create_queue_worker(
|
|
24
24
|
hardware_api: HardwareControlAPI,
|
|
25
25
|
file_provider: FileProvider,
|
|
26
|
+
camera_provider: CameraProvider,
|
|
26
27
|
state_store: StateStore,
|
|
27
28
|
action_dispatcher: ActionDispatcher,
|
|
28
29
|
command_generator: Callable[[], AsyncGenerator[str, None]],
|
|
@@ -32,6 +33,7 @@ def create_queue_worker(
|
|
|
32
33
|
Arguments:
|
|
33
34
|
hardware_api: Hardware control API to pass down to dependencies.
|
|
34
35
|
file_provider: Provides access to robot server file writing procedures for protocol output.
|
|
36
|
+
camera_provider: Provides access to camera interface with image capture and callbacks.
|
|
35
37
|
state_store: StateStore to pass down to dependencies.
|
|
36
38
|
action_dispatcher: ActionDispatcher to pass down to dependencies.
|
|
37
39
|
error_recovery_policy: ErrorRecoveryPolicy to pass down to dependencies.
|
|
@@ -85,6 +87,7 @@ def create_queue_worker(
|
|
|
85
87
|
command_executor = CommandExecutor(
|
|
86
88
|
hardware_api=hardware_api,
|
|
87
89
|
file_provider=file_provider,
|
|
90
|
+
camera_provider=camera_provider,
|
|
88
91
|
state_store=state_store,
|
|
89
92
|
action_dispatcher=action_dispatcher,
|
|
90
93
|
equipment=equipment_handler,
|
|
@@ -4,7 +4,7 @@ from __future__ import annotations
|
|
|
4
4
|
|
|
5
5
|
from typing import Optional, TYPE_CHECKING, overload
|
|
6
6
|
|
|
7
|
-
from opentrons_shared_data.labware.labware_definition import LabwareDefinition
|
|
7
|
+
from opentrons_shared_data.labware.labware_definition import LabwareDefinition, Quirks
|
|
8
8
|
|
|
9
9
|
from opentrons.types import Point
|
|
10
10
|
|
|
@@ -32,6 +32,7 @@ from ..types import (
|
|
|
32
32
|
LabwareLocation,
|
|
33
33
|
OnDeckLabwareLocation,
|
|
34
34
|
GripperMoveType,
|
|
35
|
+
AccessibleByGripperLocation,
|
|
35
36
|
)
|
|
36
37
|
|
|
37
38
|
if TYPE_CHECKING:
|
|
@@ -95,7 +96,7 @@ class LabwareMovementHandler:
|
|
|
95
96
|
*,
|
|
96
97
|
labware_id: str,
|
|
97
98
|
current_location: OnDeckLabwareLocation,
|
|
98
|
-
new_location:
|
|
99
|
+
new_location: AccessibleByGripperLocation,
|
|
99
100
|
user_pick_up_offset: Point,
|
|
100
101
|
user_drop_offset: Point,
|
|
101
102
|
post_drop_slide_offset: Optional[Point],
|
|
@@ -108,7 +109,7 @@ class LabwareMovementHandler:
|
|
|
108
109
|
*,
|
|
109
110
|
labware_definition: LabwareDefinition,
|
|
110
111
|
current_location: OnDeckLabwareLocation,
|
|
111
|
-
new_location:
|
|
112
|
+
new_location: AccessibleByGripperLocation,
|
|
112
113
|
user_pick_up_offset: Point,
|
|
113
114
|
user_drop_offset: Point,
|
|
114
115
|
post_drop_slide_offset: Optional[Point],
|
|
@@ -122,7 +123,7 @@ class LabwareMovementHandler:
|
|
|
122
123
|
labware_id: str | None = None,
|
|
123
124
|
labware_definition: LabwareDefinition | None = None,
|
|
124
125
|
current_location: OnDeckLabwareLocation,
|
|
125
|
-
new_location:
|
|
126
|
+
new_location: AccessibleByGripperLocation,
|
|
126
127
|
user_pick_up_offset: Point,
|
|
127
128
|
user_drop_offset: Point,
|
|
128
129
|
post_drop_slide_offset: Optional[Point],
|
|
@@ -235,6 +236,13 @@ class LabwareMovementHandler:
|
|
|
235
236
|
labware_definition=labware_definition
|
|
236
237
|
)
|
|
237
238
|
|
|
239
|
+
disable_geometry_grip_check = False
|
|
240
|
+
if labware_definition.parameters.quirks is not None:
|
|
241
|
+
disable_geometry_grip_check = (
|
|
242
|
+
Quirks.disableGeometryBasedGripCheck.value
|
|
243
|
+
in labware_definition.parameters.quirks
|
|
244
|
+
)
|
|
245
|
+
|
|
238
246
|
# todo(mm, 2024-09-26): This currently raises a lower-level 2015 FailedGripperPickupError.
|
|
239
247
|
# Convert this to a higher-level 3001 LabwareDroppedError or 3002 LabwareNotPickedUpError,
|
|
240
248
|
# depending on what waypoint we're at, to propagate a more specific error code to users.
|
|
@@ -242,6 +250,7 @@ class LabwareMovementHandler:
|
|
|
242
250
|
expected_grip_width=grip_specs.targetY,
|
|
243
251
|
grip_width_uncertainty_wider=grip_specs.uncertaintyWider,
|
|
244
252
|
grip_width_uncertainty_narrower=grip_specs.uncertaintyNarrower,
|
|
253
|
+
disable_geometry_grip_check=disable_geometry_grip_check,
|
|
245
254
|
)
|
|
246
255
|
await ot3api.move_to(
|
|
247
256
|
mount=gripper_mount, abs_position=waypoint_data.position
|
|
@@ -13,13 +13,13 @@ from ..errors.exceptions import (
|
|
|
13
13
|
InvalidAspirateVolumeError,
|
|
14
14
|
InvalidPushOutVolumeError,
|
|
15
15
|
InvalidDispenseVolumeError,
|
|
16
|
-
InvalidLiquidHeightFound,
|
|
17
16
|
)
|
|
18
17
|
from opentrons.protocol_engine.types import WellLocation
|
|
19
18
|
from opentrons.protocol_engine.types.liquid_level_detection import (
|
|
20
19
|
SimulatedProbeResult,
|
|
21
20
|
LiquidTrackingType,
|
|
22
21
|
)
|
|
22
|
+
from opentrons.types import Point
|
|
23
23
|
|
|
24
24
|
# 1e-9 µL (1 femtoliter!) is a good value because:
|
|
25
25
|
# * It's large relative to rounding errors that occur in practice in protocols. For
|
|
@@ -61,7 +61,9 @@ class PipettingHandler(TypingProtocol):
|
|
|
61
61
|
well_name: str,
|
|
62
62
|
volume: float,
|
|
63
63
|
flow_rate: float,
|
|
64
|
+
end_point: Point,
|
|
64
65
|
command_note_adder: CommandNoteAdder,
|
|
66
|
+
movement_delay: Optional[float] = None,
|
|
65
67
|
) -> float:
|
|
66
68
|
"""Set flow-rate and aspirate while tracking."""
|
|
67
69
|
|
|
@@ -72,8 +74,10 @@ class PipettingHandler(TypingProtocol):
|
|
|
72
74
|
well_name: str,
|
|
73
75
|
volume: float,
|
|
74
76
|
flow_rate: float,
|
|
77
|
+
end_point: Point,
|
|
75
78
|
push_out: Optional[float],
|
|
76
79
|
is_full_dispense: bool = False,
|
|
80
|
+
movement_delay: Optional[float] = None,
|
|
77
81
|
) -> float:
|
|
78
82
|
"""Set flow-rate and dispense while tracking."""
|
|
79
83
|
|
|
@@ -184,7 +188,9 @@ class HardwarePipettingHandler(PipettingHandler):
|
|
|
184
188
|
well_name: str,
|
|
185
189
|
volume: float,
|
|
186
190
|
flow_rate: float,
|
|
191
|
+
end_point: Point,
|
|
187
192
|
command_note_adder: CommandNoteAdder,
|
|
193
|
+
movement_delay: Optional[float] = None,
|
|
188
194
|
) -> float:
|
|
189
195
|
"""Set flow-rate and aspirate.
|
|
190
196
|
|
|
@@ -195,22 +201,13 @@ class HardwarePipettingHandler(PipettingHandler):
|
|
|
195
201
|
hw_pipette, adjusted_volume = self.get_hw_aspirate_params(
|
|
196
202
|
pipette_id, volume, command_note_adder
|
|
197
203
|
)
|
|
198
|
-
|
|
199
|
-
labware_id=labware_id,
|
|
200
|
-
well_name=well_name,
|
|
201
|
-
operation_volume=volume * -1,
|
|
202
|
-
pipette_id=pipette_id,
|
|
203
|
-
)
|
|
204
|
-
if isinstance(aspirate_z_distance, SimulatedProbeResult):
|
|
205
|
-
raise InvalidLiquidHeightFound(
|
|
206
|
-
"Aspirate distance must be a float in Hardware pipetting handler."
|
|
207
|
-
)
|
|
204
|
+
|
|
208
205
|
with self._set_flow_rate(pipette=hw_pipette, aspirate_flow_rate=flow_rate):
|
|
209
206
|
await self._hardware_api.aspirate_while_tracking(
|
|
210
207
|
mount=hw_pipette.mount,
|
|
211
|
-
|
|
212
|
-
flow_rate=flow_rate,
|
|
208
|
+
end_point=end_point,
|
|
213
209
|
volume=adjusted_volume,
|
|
210
|
+
movement_delay=movement_delay,
|
|
214
211
|
)
|
|
215
212
|
return adjusted_volume
|
|
216
213
|
|
|
@@ -221,8 +218,10 @@ class HardwarePipettingHandler(PipettingHandler):
|
|
|
221
218
|
well_name: str,
|
|
222
219
|
volume: float,
|
|
223
220
|
flow_rate: float,
|
|
221
|
+
end_point: Point,
|
|
224
222
|
push_out: Optional[float],
|
|
225
223
|
is_full_dispense: bool = False,
|
|
224
|
+
movement_delay: Optional[float] = None,
|
|
226
225
|
) -> float:
|
|
227
226
|
"""Set flow-rate and dispense.
|
|
228
227
|
|
|
@@ -231,24 +230,15 @@ class HardwarePipettingHandler(PipettingHandler):
|
|
|
231
230
|
"""
|
|
232
231
|
# get mount and config data from state and hardware controller
|
|
233
232
|
hw_pipette, adjusted_volume = self.get_hw_dispense_params(pipette_id, volume)
|
|
234
|
-
|
|
235
|
-
labware_id=labware_id,
|
|
236
|
-
well_name=well_name,
|
|
237
|
-
operation_volume=volume,
|
|
238
|
-
pipette_id=pipette_id,
|
|
239
|
-
)
|
|
240
|
-
if isinstance(dispense_z_distance, SimulatedProbeResult):
|
|
241
|
-
raise InvalidLiquidHeightFound(
|
|
242
|
-
"Dispense distance must be a float in Hardware pipetting handler."
|
|
243
|
-
)
|
|
233
|
+
|
|
244
234
|
with self._set_flow_rate(pipette=hw_pipette, dispense_flow_rate=flow_rate):
|
|
245
235
|
await self._hardware_api.dispense_while_tracking(
|
|
246
236
|
mount=hw_pipette.mount,
|
|
247
|
-
|
|
248
|
-
flow_rate=flow_rate,
|
|
237
|
+
end_point=end_point,
|
|
249
238
|
volume=adjusted_volume,
|
|
250
239
|
push_out=push_out,
|
|
251
240
|
is_full_dispense=is_full_dispense,
|
|
241
|
+
movement_delay=movement_delay,
|
|
252
242
|
)
|
|
253
243
|
return adjusted_volume
|
|
254
244
|
|
|
@@ -476,7 +466,9 @@ class VirtualPipettingHandler(PipettingHandler):
|
|
|
476
466
|
well_name: str,
|
|
477
467
|
volume: float,
|
|
478
468
|
flow_rate: float,
|
|
469
|
+
end_point: Point,
|
|
479
470
|
command_note_adder: CommandNoteAdder,
|
|
471
|
+
movement_delay: Optional[float] = None,
|
|
480
472
|
) -> float:
|
|
481
473
|
"""Virtually aspirate (no-op)."""
|
|
482
474
|
self._validate_tip_attached(pipette_id=pipette_id, command_name="aspirate")
|
|
@@ -495,8 +487,10 @@ class VirtualPipettingHandler(PipettingHandler):
|
|
|
495
487
|
well_name: str,
|
|
496
488
|
volume: float,
|
|
497
489
|
flow_rate: float,
|
|
490
|
+
end_point: Point,
|
|
498
491
|
push_out: Optional[float],
|
|
499
492
|
is_full_dispense: bool = False,
|
|
493
|
+
movement_delay: Optional[float] = None,
|
|
500
494
|
) -> float:
|
|
501
495
|
"""Virtually dispense (no-op)."""
|
|
502
496
|
# TODO (tz, 8-23-23): add a check for push_out not larger that the max volume allowed when working on this https://opentrons.atlassian.net/browse/RSS-329
|
|
@@ -13,6 +13,7 @@ from opentrons_shared_data.labware.labware_definition import LabwareDefinition
|
|
|
13
13
|
from opentrons.hardware_control import HardwareControlAPI
|
|
14
14
|
from opentrons.hardware_control.modules import AbstractModule as HardwareModuleAPI
|
|
15
15
|
from opentrons.hardware_control.types import PauseType as HardwarePauseType
|
|
16
|
+
from opentrons.system import camera
|
|
16
17
|
|
|
17
18
|
from .actions.actions import (
|
|
18
19
|
ResumeFromRecoveryAction,
|
|
@@ -22,7 +23,8 @@ from .errors import ProtocolCommandFailedError, ErrorOccurrence, CommandNotAllow
|
|
|
22
23
|
from .errors.exceptions import EStopActivatedError
|
|
23
24
|
from .error_recovery_policy import ErrorRecoveryPolicy
|
|
24
25
|
from . import commands, slot_standardization, labware_offset_standardization
|
|
25
|
-
from .resources import ModelUtils, ModuleDataProvider, FileProvider
|
|
26
|
+
from .resources import ModelUtils, ModuleDataProvider, FileProvider, CameraProvider
|
|
27
|
+
from .resources.camera_provider import CameraSettings
|
|
26
28
|
from .types import (
|
|
27
29
|
LabwareOffset,
|
|
28
30
|
LabwareOffsetCreate,
|
|
@@ -55,6 +57,7 @@ from .actions import (
|
|
|
55
57
|
AddLabwareOffsetAction,
|
|
56
58
|
AddLabwareDefinitionAction,
|
|
57
59
|
AddLiquidAction,
|
|
60
|
+
AddCameraSettingsAction,
|
|
58
61
|
SetDeckConfigurationAction,
|
|
59
62
|
AddAddressableAreaAction,
|
|
60
63
|
AddModuleAction,
|
|
@@ -96,6 +99,7 @@ class ProtocolEngine:
|
|
|
96
99
|
door_watcher: DoorWatcher,
|
|
97
100
|
module_data_provider: ModuleDataProvider,
|
|
98
101
|
file_provider: FileProvider,
|
|
102
|
+
camera_provider: CameraProvider,
|
|
99
103
|
queue_worker: Optional[QueueWorker] = None,
|
|
100
104
|
) -> None:
|
|
101
105
|
"""Initialize a ProtocolEngine instance.
|
|
@@ -107,6 +111,7 @@ class ProtocolEngine:
|
|
|
107
111
|
"""
|
|
108
112
|
self._hardware_api = hardware_api
|
|
109
113
|
self._file_provider = file_provider
|
|
114
|
+
self._camera_provider = camera_provider
|
|
110
115
|
self._state_store = state_store
|
|
111
116
|
self._model_utils = model_utils
|
|
112
117
|
self._action_dispatcher = action_dispatcher
|
|
@@ -405,6 +410,39 @@ class ProtocolEngine:
|
|
|
405
410
|
await self._do_hardware_stop()
|
|
406
411
|
return True
|
|
407
412
|
|
|
413
|
+
async def module_disconnected(
|
|
414
|
+
self, module_model: ModuleModel, serial: str | None
|
|
415
|
+
) -> bool:
|
|
416
|
+
"""Signal to the engine that a module has disconnected.
|
|
417
|
+
|
|
418
|
+
The return value of this function signals whether the module was relevant to this
|
|
419
|
+
protocol or not. If the function returns True, the module was relevant. The engine
|
|
420
|
+
will stop, and the caller should call `finish()` with an appropriate error to indicate
|
|
421
|
+
the missing module. If the function returns False, the error is not relevant. The engine
|
|
422
|
+
will not stop, and the caller should not call `finish()`.
|
|
423
|
+
|
|
424
|
+
Module disconnects are signaled when a hardware module's status poller indicates that the
|
|
425
|
+
module is no logner connected - for instance, someone unplugs the module, or it crashes.
|
|
426
|
+
These errors are not related to a particular command, even a currently-happening module
|
|
427
|
+
control command for the module in the error state.
|
|
428
|
+
|
|
429
|
+
Similar to an estop error, the error can occur at any time relative to the lifecycle
|
|
430
|
+
of the engine run or of any particular command.
|
|
431
|
+
|
|
432
|
+
Unlike an estop, the motion control hardware will not be raising an error and will not
|
|
433
|
+
stop on its own; the stop action derived from this call will do that.
|
|
434
|
+
"""
|
|
435
|
+
if not self._state_store.modules.get_has_module_probably_matching_hardware_details(
|
|
436
|
+
module_model, serial
|
|
437
|
+
):
|
|
438
|
+
return False
|
|
439
|
+
self._stop_from_asynchronous_error()
|
|
440
|
+
# like self.request_stop, and unlike self.estop(), we must explicitly request that the
|
|
441
|
+
# hardware stops execution, since not all asynchronous errors will cause the hardware
|
|
442
|
+
# to know that it should stop.
|
|
443
|
+
await self._do_hardware_stop()
|
|
444
|
+
return True
|
|
445
|
+
|
|
408
446
|
async def _do_hardware_stop(self) -> None:
|
|
409
447
|
"""Make the hardware stop now."""
|
|
410
448
|
if self._hardware_api.is_movement_execution_taskified():
|
|
@@ -445,7 +483,7 @@ class ProtocolEngine:
|
|
|
445
483
|
)
|
|
446
484
|
self._state_store.commands.raise_fatal_command_error()
|
|
447
485
|
|
|
448
|
-
async def finish(
|
|
486
|
+
async def finish( # noqa: C901
|
|
449
487
|
self,
|
|
450
488
|
error: Optional[Exception] = None,
|
|
451
489
|
drop_tips_after_run: bool = True,
|
|
@@ -559,6 +597,16 @@ class ProtocolEngine:
|
|
|
559
597
|
else:
|
|
560
598
|
finish_error_details = None
|
|
561
599
|
|
|
600
|
+
try:
|
|
601
|
+
await camera.update_live_stream_status(
|
|
602
|
+
self.state_view.config.robot_type,
|
|
603
|
+
False,
|
|
604
|
+
self._camera_provider,
|
|
605
|
+
self.state_view.camera.get_enablement_settings(),
|
|
606
|
+
)
|
|
607
|
+
except Exception as e:
|
|
608
|
+
_log.exception(f"Exception during live stream post-run cleanup: {e}")
|
|
609
|
+
|
|
562
610
|
self._action_dispatcher.dispatch(
|
|
563
611
|
HardwareStoppedAction(
|
|
564
612
|
completed_at=self._model_utils.get_timestamp(),
|
|
@@ -596,6 +644,17 @@ class ProtocolEngine:
|
|
|
596
644
|
labware_offset_id=labware_offset_id
|
|
597
645
|
)
|
|
598
646
|
|
|
647
|
+
def add_camera_enablement_settings(
|
|
648
|
+
self, enablement_settings: CameraSettings
|
|
649
|
+
) -> CameraSettings:
|
|
650
|
+
"""Add new camera enablement settings."""
|
|
651
|
+
self._action_dispatcher.dispatch(
|
|
652
|
+
AddCameraSettingsAction(enablement_settings=enablement_settings)
|
|
653
|
+
)
|
|
654
|
+
camera_settings = self.state_view.camera.get_enablement_settings()
|
|
655
|
+
assert camera_settings is not None
|
|
656
|
+
return camera_settings
|
|
657
|
+
|
|
599
658
|
def add_labware_definition(self, definition: LabwareDefinition) -> LabwareUri:
|
|
600
659
|
"""Add a labware definition to the state for subsequent labware loads."""
|
|
601
660
|
self._action_dispatcher.dispatch(
|
|
@@ -676,6 +735,7 @@ class ProtocolEngine:
|
|
|
676
735
|
self._queue_worker = create_queue_worker(
|
|
677
736
|
hardware_api=self._hardware_api,
|
|
678
737
|
file_provider=self._file_provider,
|
|
738
|
+
camera_provider=self._camera_provider,
|
|
679
739
|
state_store=self._state_store,
|
|
680
740
|
action_dispatcher=self._action_dispatcher,
|
|
681
741
|
command_generator=command_generator,
|
|
@@ -10,6 +10,7 @@ from .deck_data_provider import DeckDataProvider, DeckFixedLabware
|
|
|
10
10
|
from .labware_data_provider import LabwareDataProvider
|
|
11
11
|
from .module_data_provider import ModuleDataProvider
|
|
12
12
|
from .file_provider import FileProvider
|
|
13
|
+
from .camera_provider import CameraProvider
|
|
13
14
|
from .ot3_validation import ensure_ot3_hardware
|
|
14
15
|
from .concurrency_provider import ConcurrencyProvider
|
|
15
16
|
|
|
@@ -22,6 +23,7 @@ __all__ = [
|
|
|
22
23
|
"ConcurrencyProvider",
|
|
23
24
|
"ModuleDataProvider",
|
|
24
25
|
"FileProvider",
|
|
26
|
+
"CameraProvider",
|
|
25
27
|
"ensure_ot3_hardware",
|
|
26
28
|
"pipette_data_provider",
|
|
27
29
|
"labware_validation",
|
|
@@ -0,0 +1,110 @@
|
|
|
1
|
+
"""Camera interaction resource provider."""
|
|
2
|
+
from typing import Optional, Callable, Tuple, Awaitable
|
|
3
|
+
from pydantic import BaseModel, Field
|
|
4
|
+
from ..errors import CameraCaptureError, CameraSettingsInvalidError
|
|
5
|
+
import logging
|
|
6
|
+
|
|
7
|
+
from opentrons_shared_data.robot.types import RobotType
|
|
8
|
+
|
|
9
|
+
log = logging.getLogger(__name__)
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
class CameraError(BaseModel):
|
|
13
|
+
"""Generic base class for Camera errors that occur on entities handled through the Camera Provider."""
|
|
14
|
+
|
|
15
|
+
message: str = Field(..., description="Description of error content.")
|
|
16
|
+
code: str | None = Field(
|
|
17
|
+
..., description="Return code, if any, that was paired with the error."
|
|
18
|
+
)
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
class CameraSettings(BaseModel):
|
|
22
|
+
"""Camera API settings for general enablement and use."""
|
|
23
|
+
|
|
24
|
+
cameraEnabled: bool = Field(
|
|
25
|
+
..., description="Enablement status for general camera use."
|
|
26
|
+
)
|
|
27
|
+
liveStreamEnabled: bool = Field(
|
|
28
|
+
..., description="Enablement status for the Opentrons Live Stream service."
|
|
29
|
+
)
|
|
30
|
+
errorRecoveryEnabled: bool = Field(
|
|
31
|
+
..., description="Enablement status for camera usage with Error Recovery."
|
|
32
|
+
)
|
|
33
|
+
|
|
34
|
+
|
|
35
|
+
class ImageParameters(BaseModel):
|
|
36
|
+
"""Parameters for an Image Capture to determine filters. These are the inputs as expected by FFMPEG."""
|
|
37
|
+
|
|
38
|
+
resolution: Optional[Tuple[int, int]] = Field(
|
|
39
|
+
None,
|
|
40
|
+
description="Width by height resolution in pixels for the image to be captured with.",
|
|
41
|
+
)
|
|
42
|
+
zoom: Optional[float] = Field(
|
|
43
|
+
None,
|
|
44
|
+
description="Multiplier to use when cropping and scaling a captured image.",
|
|
45
|
+
)
|
|
46
|
+
pan: Optional[Tuple[int, int]] = Field(
|
|
47
|
+
None,
|
|
48
|
+
description="Position to pan to for a given zoom. Format is X and Y coordinates (in pixels) to the bottom left of a frame.",
|
|
49
|
+
)
|
|
50
|
+
contrast: Optional[float] = Field(
|
|
51
|
+
None, description="The contrast to use when processing an image."
|
|
52
|
+
)
|
|
53
|
+
brightness: Optional[int] = Field(
|
|
54
|
+
None, description="The brightness to use when processing an image."
|
|
55
|
+
)
|
|
56
|
+
saturation: Optional[float] = Field(
|
|
57
|
+
None, description="The saturation to use when processing an image."
|
|
58
|
+
)
|
|
59
|
+
|
|
60
|
+
|
|
61
|
+
class CameraProvider:
|
|
62
|
+
"""Provider class to wrap camera interactions between the server and the engine."""
|
|
63
|
+
|
|
64
|
+
def __init__(
|
|
65
|
+
self,
|
|
66
|
+
camera_settings_callback: Optional[Callable[[], CameraSettings]] = None,
|
|
67
|
+
image_capture_callback: Optional[
|
|
68
|
+
Callable[[RobotType, ImageParameters], Awaitable[bytes | CameraError]]
|
|
69
|
+
] = None,
|
|
70
|
+
) -> None:
|
|
71
|
+
"""Initialize the interface callbacks of the Camera Provider within the Protocol Engine.
|
|
72
|
+
|
|
73
|
+
Params:
|
|
74
|
+
camera_settings_callback: Callback to query the Camera Enablement settings from the Boolean Settings table.
|
|
75
|
+
image_capture_callback: Callback to process an image capture request and return a bytestream of image data in response.
|
|
76
|
+
"""
|
|
77
|
+
self._camera_settings_callback = camera_settings_callback
|
|
78
|
+
self._image_capture_callback = image_capture_callback
|
|
79
|
+
|
|
80
|
+
async def get_camera_settings(self) -> CameraSettings:
|
|
81
|
+
"""Query the Robot Server for the current Camera Enablement settings."""
|
|
82
|
+
if self._camera_settings_callback is not None:
|
|
83
|
+
return self._camera_settings_callback()
|
|
84
|
+
# If we are in analysis or simulation, return as if the camera is enabled
|
|
85
|
+
return CameraSettings(
|
|
86
|
+
cameraEnabled=True, liveStreamEnabled=True, errorRecoveryEnabled=True
|
|
87
|
+
)
|
|
88
|
+
|
|
89
|
+
async def capture_image(
|
|
90
|
+
self, robot_type: RobotType, parameters: ImageParameters
|
|
91
|
+
) -> bytes | None:
|
|
92
|
+
"""Process through the Camera Executor on robot server an image capture request with a given set of filters.
|
|
93
|
+
|
|
94
|
+
Returns a bytesteam of image data upon success. Raises an error if an error occurred during capture.
|
|
95
|
+
Conditionally returns None if an image capture callback does not exist (simulation).
|
|
96
|
+
"""
|
|
97
|
+
if self._image_capture_callback is not None:
|
|
98
|
+
capture_result = await self._image_capture_callback(robot_type, parameters)
|
|
99
|
+
if not isinstance(capture_result, CameraError):
|
|
100
|
+
return capture_result
|
|
101
|
+
else:
|
|
102
|
+
if capture_result.code == "IMAGE_SETTINGS":
|
|
103
|
+
raise CameraSettingsInvalidError(message=capture_result.message)
|
|
104
|
+
elif capture_result.code is not None:
|
|
105
|
+
error_str = f"Camera capture has failed to return an image with return code {capture_result.code}: {capture_result.message}"
|
|
106
|
+
else:
|
|
107
|
+
error_str = f"Camera capture has failed with exception: {capture_result.message}"
|
|
108
|
+
raise CameraCaptureError(message=error_str)
|
|
109
|
+
# Return None if the image capture callback is unavailable (simulation)
|
|
110
|
+
return None
|