opentrons 8.7.0a1__py3-none-any.whl → 8.7.0a2__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/drivers/thermocycler/abstract.py +1 -0
- opentrons/drivers/thermocycler/driver.py +33 -4
- opentrons/drivers/thermocycler/simulator.py +2 -0
- opentrons/hardware_control/api.py +24 -5
- opentrons/hardware_control/backends/controller.py +8 -2
- opentrons/hardware_control/backends/ot3controller.py +3 -0
- opentrons/hardware_control/backends/ot3simulator.py +2 -1
- opentrons/hardware_control/backends/simulator.py +2 -1
- opentrons/hardware_control/backends/subsystem_manager.py +5 -2
- opentrons/hardware_control/module_control.py +82 -8
- opentrons/hardware_control/modules/__init__.py +3 -0
- opentrons/hardware_control/modules/absorbance_reader.py +11 -4
- opentrons/hardware_control/modules/flex_stacker.py +38 -9
- opentrons/hardware_control/modules/heater_shaker.py +30 -5
- opentrons/hardware_control/modules/magdeck.py +8 -4
- opentrons/hardware_control/modules/mod_abc.py +13 -5
- opentrons/hardware_control/modules/tempdeck.py +25 -5
- opentrons/hardware_control/modules/thermocycler.py +56 -10
- opentrons/hardware_control/modules/types.py +20 -1
- opentrons/hardware_control/modules/utils.py +11 -4
- opentrons/hardware_control/nozzle_manager.py +3 -0
- opentrons/hardware_control/ot3api.py +26 -5
- opentrons/hardware_control/scripts/update_module_fw.py +5 -0
- opentrons/hardware_control/types.py +31 -2
- opentrons/legacy_commands/protocol_commands.py +20 -0
- opentrons/legacy_commands/types.py +42 -0
- opentrons/motion_planning/waypoints.py +15 -29
- opentrons/protocol_api/__init__.py +5 -0
- opentrons/protocol_api/_types.py +6 -1
- opentrons/protocol_api/core/common.py +3 -1
- opentrons/protocol_api/core/engine/_default_labware_versions.py +32 -11
- opentrons/protocol_api/core/engine/labware.py +8 -1
- opentrons/protocol_api/core/engine/module_core.py +4 -0
- opentrons/protocol_api/core/engine/protocol.py +18 -1
- opentrons/protocol_api/core/engine/tasks.py +35 -0
- opentrons/protocol_api/core/legacy/legacy_module_core.py +2 -0
- opentrons/protocol_api/core/legacy/legacy_protocol_core.py +11 -1
- opentrons/protocol_api/core/legacy/tasks.py +19 -0
- opentrons/protocol_api/core/legacy_simulator/legacy_protocol_core.py +14 -2
- opentrons/protocol_api/core/legacy_simulator/tasks.py +19 -0
- opentrons/protocol_api/core/module.py +1 -0
- opentrons/protocol_api/core/protocol.py +11 -2
- opentrons/protocol_api/core/tasks.py +31 -0
- opentrons/protocol_api/module_contexts.py +1 -0
- opentrons/protocol_api/protocol_context.py +26 -4
- opentrons/protocol_api/robot_context.py +38 -21
- opentrons/protocol_api/tasks.py +48 -0
- opentrons/protocol_api/validation.py +6 -1
- opentrons/protocol_engine/actions/__init__.py +4 -2
- opentrons/protocol_engine/actions/actions.py +22 -9
- opentrons/protocol_engine/clients/sync_client.py +6 -7
- opentrons/protocol_engine/commands/__init__.py +42 -0
- opentrons/protocol_engine/commands/absorbance_reader/close_lid.py +2 -15
- opentrons/protocol_engine/commands/absorbance_reader/open_lid.py +2 -15
- opentrons/protocol_engine/commands/aspirate.py +1 -0
- opentrons/protocol_engine/commands/command.py +1 -0
- opentrons/protocol_engine/commands/command_unions.py +39 -0
- opentrons/protocol_engine/commands/create_timer.py +83 -0
- opentrons/protocol_engine/commands/dispense.py +1 -0
- opentrons/protocol_engine/commands/drop_tip.py +32 -8
- opentrons/protocol_engine/commands/movement_common.py +2 -0
- opentrons/protocol_engine/commands/pick_up_tip.py +21 -11
- opentrons/protocol_engine/commands/set_tip_state.py +97 -0
- opentrons/protocol_engine/commands/thermocycler/run_extended_profile.py +6 -0
- opentrons/protocol_engine/commands/thermocycler/run_profile.py +8 -0
- opentrons/protocol_engine/commands/thermocycler/set_target_block_temperature.py +17 -1
- opentrons/protocol_engine/commands/touch_tip.py +1 -1
- opentrons/protocol_engine/commands/unsafe/unsafe_place_labware.py +6 -22
- opentrons/protocol_engine/commands/wait_for_tasks.py +98 -0
- opentrons/protocol_engine/errors/__init__.py +4 -0
- opentrons/protocol_engine/errors/exceptions.py +55 -0
- opentrons/protocol_engine/execution/__init__.py +2 -0
- opentrons/protocol_engine/execution/command_executor.py +8 -0
- opentrons/protocol_engine/execution/create_queue_worker.py +5 -1
- opentrons/protocol_engine/execution/labware_movement.py +9 -12
- opentrons/protocol_engine/execution/movement.py +2 -0
- opentrons/protocol_engine/execution/queue_worker.py +4 -0
- opentrons/protocol_engine/execution/run_control.py +8 -0
- opentrons/protocol_engine/execution/task_handler.py +157 -0
- opentrons/protocol_engine/protocol_engine.py +67 -33
- opentrons/protocol_engine/resources/__init__.py +2 -0
- opentrons/protocol_engine/resources/concurrency_provider.py +27 -0
- opentrons/protocol_engine/resources/deck_configuration_provider.py +7 -0
- opentrons/protocol_engine/resources/labware_validation.py +10 -6
- opentrons/protocol_engine/state/_well_math.py +60 -18
- opentrons/protocol_engine/state/addressable_areas.py +2 -0
- opentrons/protocol_engine/state/commands.py +7 -7
- opentrons/protocol_engine/state/geometry.py +204 -374
- opentrons/protocol_engine/state/labware.py +52 -102
- opentrons/protocol_engine/state/labware_origin_math/errors.py +94 -0
- opentrons/protocol_engine/state/labware_origin_math/stackup_origin_to_labware_origin.py +1331 -0
- opentrons/protocol_engine/state/module_substates/thermocycler_module_substate.py +37 -0
- opentrons/protocol_engine/state/modules.py +21 -8
- opentrons/protocol_engine/state/motion.py +44 -0
- opentrons/protocol_engine/state/state.py +14 -0
- opentrons/protocol_engine/state/state_summary.py +2 -0
- opentrons/protocol_engine/state/tasks.py +139 -0
- opentrons/protocol_engine/state/tips.py +177 -258
- opentrons/protocol_engine/state/update_types.py +16 -9
- opentrons/protocol_engine/types/__init__.py +9 -3
- opentrons/protocol_engine/types/deck_configuration.py +5 -1
- opentrons/protocol_engine/types/instrument.py +8 -1
- opentrons/protocol_engine/types/labware.py +1 -13
- opentrons/protocol_engine/types/module.py +10 -0
- opentrons/protocol_engine/types/tasks.py +38 -0
- opentrons/protocol_engine/types/tip.py +9 -0
- opentrons/protocol_runner/create_simulating_orchestrator.py +29 -2
- opentrons/protocol_runner/run_orchestrator.py +18 -2
- opentrons/protocols/api_support/definitions.py +1 -1
- opentrons/protocols/api_support/types.py +2 -1
- opentrons/simulate.py +48 -15
- opentrons/system/camera.py +1 -1
- {opentrons-8.7.0a1.dist-info → opentrons-8.7.0a2.dist-info}/METADATA +4 -4
- {opentrons-8.7.0a1.dist-info → opentrons-8.7.0a2.dist-info}/RECORD +118 -105
- opentrons/protocol_engine/state/_labware_origin_math.py +0 -636
- {opentrons-8.7.0a1.dist-info → opentrons-8.7.0a2.dist-info}/WHEEL +0 -0
- {opentrons-8.7.0a1.dist-info → opentrons-8.7.0a2.dist-info}/entry_points.txt +0 -0
- {opentrons-8.7.0a1.dist-info → opentrons-8.7.0a2.dist-info}/licenses/LICENSE +0 -0
|
@@ -0,0 +1,98 @@
|
|
|
1
|
+
"""WaitForTasks command request, result, and implementation models."""
|
|
2
|
+
from __future__ import annotations
|
|
3
|
+
from pydantic import BaseModel, Field
|
|
4
|
+
from typing import Optional, Type, TYPE_CHECKING
|
|
5
|
+
from typing_extensions import Literal
|
|
6
|
+
|
|
7
|
+
from .command import AbstractCommandImpl, BaseCommand, BaseCommandCreate, SuccessData
|
|
8
|
+
from ..errors.error_occurrence import ErrorOccurrence, ProtocolCommandFailedError
|
|
9
|
+
from ..errors.exceptions import TaskFailedError
|
|
10
|
+
|
|
11
|
+
if TYPE_CHECKING:
|
|
12
|
+
from ..execution import TaskHandler, RunControlHandler
|
|
13
|
+
from ..state.state import StateView
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
WaitForTasksCommandType = Literal["waitForTasks"]
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
class WaitForTasksParams(BaseModel):
|
|
20
|
+
"""Payload required to annotate execution with a WaitForTasks."""
|
|
21
|
+
|
|
22
|
+
task_ids: list[str] = Field(
|
|
23
|
+
...,
|
|
24
|
+
description="The list of task ids to wait for.",
|
|
25
|
+
)
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
class WaitForTasksResult(BaseModel):
|
|
29
|
+
"""Result data from the execution of a WaitForTasks command."""
|
|
30
|
+
|
|
31
|
+
task_ids: list[str] = Field(
|
|
32
|
+
...,
|
|
33
|
+
description="The list of completed task ids.",
|
|
34
|
+
)
|
|
35
|
+
|
|
36
|
+
|
|
37
|
+
class WaitForTasksImplementation(
|
|
38
|
+
AbstractCommandImpl[WaitForTasksParams, SuccessData[WaitForTasksResult]]
|
|
39
|
+
):
|
|
40
|
+
"""WaitForTasks command implementation."""
|
|
41
|
+
|
|
42
|
+
def __init__(
|
|
43
|
+
self,
|
|
44
|
+
task_handler: TaskHandler,
|
|
45
|
+
run_control: RunControlHandler,
|
|
46
|
+
state_view: StateView,
|
|
47
|
+
**kwargs: object,
|
|
48
|
+
) -> None:
|
|
49
|
+
self._task_handler = task_handler
|
|
50
|
+
self._run_control = run_control
|
|
51
|
+
self._state_view = state_view
|
|
52
|
+
|
|
53
|
+
async def execute(
|
|
54
|
+
self, params: WaitForTasksParams
|
|
55
|
+
) -> SuccessData[WaitForTasksResult]:
|
|
56
|
+
"""Checks for existance of task id and then asynchronously waits for the valid, specified tasks to finish."""
|
|
57
|
+
# Raises the exception if we don't have a valid task id.
|
|
58
|
+
for task_id in params.task_ids:
|
|
59
|
+
_ = self._state_view.tasks.get(task_id)
|
|
60
|
+
|
|
61
|
+
await self._run_control.wait_for_tasks(params.task_ids)
|
|
62
|
+
|
|
63
|
+
failed_tasks = self._state_view.tasks.get_failed_tasks(params.task_ids)
|
|
64
|
+
if failed_tasks:
|
|
65
|
+
raise TaskFailedError(
|
|
66
|
+
message=f"{len(failed_tasks)} tasks failed.",
|
|
67
|
+
details={"failed_task_ids": failed_tasks},
|
|
68
|
+
wrapping=[
|
|
69
|
+
ProtocolCommandFailedError(
|
|
70
|
+
original_error=self._state_view.tasks.get_finished(
|
|
71
|
+
task_id
|
|
72
|
+
).error
|
|
73
|
+
)
|
|
74
|
+
for task_id in failed_tasks
|
|
75
|
+
],
|
|
76
|
+
)
|
|
77
|
+
return SuccessData(public=WaitForTasksResult(task_ids=params.task_ids))
|
|
78
|
+
|
|
79
|
+
|
|
80
|
+
class WaitForTasks(
|
|
81
|
+
BaseCommand[WaitForTasksParams, WaitForTasksResult, ErrorOccurrence]
|
|
82
|
+
):
|
|
83
|
+
"""WaitForTasks command model."""
|
|
84
|
+
|
|
85
|
+
commandType: WaitForTasksCommandType = "waitForTasks"
|
|
86
|
+
params: WaitForTasksParams
|
|
87
|
+
result: Optional[WaitForTasksResult] = None
|
|
88
|
+
|
|
89
|
+
_ImplementationCls: Type[WaitForTasksImplementation] = WaitForTasksImplementation
|
|
90
|
+
|
|
91
|
+
|
|
92
|
+
class WaitForTasksCreate(BaseCommandCreate[WaitForTasksParams]):
|
|
93
|
+
"""WaitForTasks command request model."""
|
|
94
|
+
|
|
95
|
+
commandType: WaitForTasksCommandType = "waitForTasks"
|
|
96
|
+
params: WaitForTasksParams
|
|
97
|
+
|
|
98
|
+
_CommandCls: Type[WaitForTasks] = WaitForTasks
|
|
@@ -57,6 +57,7 @@ from .exceptions import (
|
|
|
57
57
|
InvalidTargetSpeedError,
|
|
58
58
|
InvalidTargetTemperatureError,
|
|
59
59
|
InvalidBlockVolumeError,
|
|
60
|
+
InvalidRampRateError,
|
|
60
61
|
InvalidHoldTimeError,
|
|
61
62
|
InvalidWavelengthError,
|
|
62
63
|
CannotPerformModuleAction,
|
|
@@ -90,6 +91,7 @@ from .exceptions import (
|
|
|
90
91
|
FlexStackerLabwarePoolNotYetDefinedError,
|
|
91
92
|
FlexStackerNotLogicallyEmptyError,
|
|
92
93
|
InvalidLabwarePositionError,
|
|
94
|
+
InvalidModuleOrientation,
|
|
93
95
|
)
|
|
94
96
|
|
|
95
97
|
from .error_occurrence import ErrorOccurrence, ProtocolCommandFailedError
|
|
@@ -151,6 +153,7 @@ __all__ = [
|
|
|
151
153
|
"NoTargetTemperatureSetError",
|
|
152
154
|
"InvalidTargetTemperatureError",
|
|
153
155
|
"InvalidTargetSpeedError",
|
|
156
|
+
"InvalidRampRateError",
|
|
154
157
|
"InvalidBlockVolumeError",
|
|
155
158
|
"InvalidHoldTimeError",
|
|
156
159
|
"InvalidLiquidError",
|
|
@@ -174,6 +177,7 @@ __all__ = [
|
|
|
174
177
|
"FlexStackerLabwarePoolNotYetDefinedError",
|
|
175
178
|
"FlexStackerNotLogicallyEmptyError",
|
|
176
179
|
"InvalidLabwarePositionError",
|
|
180
|
+
"InvalidModuleOrientation",
|
|
177
181
|
# error occurrence models
|
|
178
182
|
"ErrorOccurrence",
|
|
179
183
|
"CommandNotAllowedError",
|
|
@@ -413,6 +413,36 @@ class WellDoesNotExistError(ProtocolEngineError):
|
|
|
413
413
|
super().__init__(ErrorCodes.GENERAL_ERROR, message, details, wrapping)
|
|
414
414
|
|
|
415
415
|
|
|
416
|
+
class NoTaskFoundError(ProtocolEngineError):
|
|
417
|
+
"""Raised when referencing a task that does not exist.
|
|
418
|
+
|
|
419
|
+
This error could be raised if a protocol references a task before it
|
|
420
|
+
has been created.
|
|
421
|
+
"""
|
|
422
|
+
|
|
423
|
+
def __init__(
|
|
424
|
+
self,
|
|
425
|
+
message: Optional[str] = None,
|
|
426
|
+
details: Optional[Dict[str, Any]] = None,
|
|
427
|
+
wrapping: Optional[Sequence[EnumeratedError]] = None,
|
|
428
|
+
) -> None:
|
|
429
|
+
"""Build a NoTaskFoundError."""
|
|
430
|
+
super().__init__(ErrorCodes.GENERAL_ERROR, message, details, wrapping)
|
|
431
|
+
|
|
432
|
+
|
|
433
|
+
class TaskFailedError(ProtocolEngineError):
|
|
434
|
+
"""Raised when waiting on a task that failed."""
|
|
435
|
+
|
|
436
|
+
def __init__(
|
|
437
|
+
self,
|
|
438
|
+
message: Optional[str] = None,
|
|
439
|
+
details: Optional[Dict[str, Any]] = None,
|
|
440
|
+
wrapping: Optional[Sequence[EnumeratedError]] = None,
|
|
441
|
+
) -> None:
|
|
442
|
+
"""Build a TaskFailedError."""
|
|
443
|
+
super().__init__(ErrorCodes.GENERAL_ERROR, message, details, wrapping)
|
|
444
|
+
|
|
445
|
+
|
|
416
446
|
class PipetteNotLoadedError(ProtocolEngineError):
|
|
417
447
|
"""Raised when referencing a pipette that has not been loaded."""
|
|
418
448
|
|
|
@@ -825,6 +855,19 @@ class InvalidTargetTemperatureError(ProtocolEngineError):
|
|
|
825
855
|
super().__init__(ErrorCodes.GENERAL_ERROR, message, details, wrapping)
|
|
826
856
|
|
|
827
857
|
|
|
858
|
+
class InvalidRampRateError(ProtocolEngineError):
|
|
859
|
+
"""Raised when attempting to set an invalid ramp rate."""
|
|
860
|
+
|
|
861
|
+
def __init__(
|
|
862
|
+
self,
|
|
863
|
+
message: Optional[str] = None,
|
|
864
|
+
details: Optional[Dict[str, Any]] = None,
|
|
865
|
+
wrapping: Optional[Sequence[EnumeratedError]] = None,
|
|
866
|
+
) -> None:
|
|
867
|
+
"""Build a InvalidRampRateError."""
|
|
868
|
+
super().__init__(ErrorCodes.GENERAL_ERROR, message, details, wrapping)
|
|
869
|
+
|
|
870
|
+
|
|
828
871
|
class InvalidBlockVolumeError(ProtocolEngineError):
|
|
829
872
|
"""Raised when attempting to set an invalid block max volume."""
|
|
830
873
|
|
|
@@ -1306,3 +1349,15 @@ class InvalidLabwarePositionError(ProtocolEngineError):
|
|
|
1306
1349
|
wrapping: Optional[Sequence[EnumeratedError]] = None,
|
|
1307
1350
|
) -> None:
|
|
1308
1351
|
super().__init__(ErrorCodes.GENERAL_ERROR, message, details, wrapping)
|
|
1352
|
+
|
|
1353
|
+
|
|
1354
|
+
class InvalidModuleOrientation(ProtocolEngineError):
|
|
1355
|
+
"""Raised when a module orientation is invalid for a slot id."""
|
|
1356
|
+
|
|
1357
|
+
def __init__(
|
|
1358
|
+
self,
|
|
1359
|
+
message: Optional[str] = None,
|
|
1360
|
+
details: Optional[dict[str, Any]] = None,
|
|
1361
|
+
wrapping: Optional[Sequence[EnumeratedError]] = None,
|
|
1362
|
+
) -> None:
|
|
1363
|
+
super().__init__(ErrorCodes.GENERAL_ERROR, message, details, wrapping)
|
|
@@ -21,6 +21,7 @@ from .run_control import RunControlHandler
|
|
|
21
21
|
from .hardware_stopper import HardwareStopper
|
|
22
22
|
from .door_watcher import DoorWatcher
|
|
23
23
|
from .status_bar import StatusBarHandler
|
|
24
|
+
from .task_handler import TaskHandler
|
|
24
25
|
from ..resources.file_provider import FileProvider
|
|
25
26
|
|
|
26
27
|
# .thermocycler_movement_flagger omitted from package's public interface.
|
|
@@ -46,5 +47,6 @@ __all__ = [
|
|
|
46
47
|
"DoorWatcher",
|
|
47
48
|
"RailLightsHandler",
|
|
48
49
|
"StatusBarHandler",
|
|
50
|
+
"TaskHandler",
|
|
49
51
|
"FileProvider",
|
|
50
52
|
]
|
|
@@ -35,6 +35,7 @@ from .tip_handler import TipHandler
|
|
|
35
35
|
from .run_control import RunControlHandler
|
|
36
36
|
from .rail_lights import RailLightsHandler
|
|
37
37
|
from .status_bar import StatusBarHandler
|
|
38
|
+
from .task_handler import TaskHandler
|
|
38
39
|
|
|
39
40
|
|
|
40
41
|
log = getLogger(__name__)
|
|
@@ -85,6 +86,7 @@ class CommandExecutor:
|
|
|
85
86
|
run_control: RunControlHandler,
|
|
86
87
|
rail_lights: RailLightsHandler,
|
|
87
88
|
status_bar: StatusBarHandler,
|
|
89
|
+
task_handler: TaskHandler,
|
|
88
90
|
model_utils: Optional[ModelUtils] = None,
|
|
89
91
|
command_note_tracker_provider: Optional[CommandNoteTrackerProvider] = None,
|
|
90
92
|
) -> None:
|
|
@@ -106,6 +108,7 @@ class CommandExecutor:
|
|
|
106
108
|
self._command_note_tracker_provider = (
|
|
107
109
|
command_note_tracker_provider or _NoteTracker
|
|
108
110
|
)
|
|
111
|
+
self._task_handler = task_handler
|
|
109
112
|
|
|
110
113
|
async def execute(self, command_id: str) -> None:
|
|
111
114
|
"""Run a given command's execution procedure.
|
|
@@ -131,6 +134,7 @@ class CommandExecutor:
|
|
|
131
134
|
model_utils=self._model_utils,
|
|
132
135
|
status_bar=self._status_bar,
|
|
133
136
|
command_note_adder=note_tracker,
|
|
137
|
+
task_handler=self._task_handler,
|
|
134
138
|
)
|
|
135
139
|
|
|
136
140
|
started_at = self._model_utils.get_timestamp()
|
|
@@ -214,3 +218,7 @@ class CommandExecutor:
|
|
|
214
218
|
type=error_recovery_type,
|
|
215
219
|
)
|
|
216
220
|
)
|
|
221
|
+
|
|
222
|
+
def cancel_tasks(self, message: str | None = None) -> None:
|
|
223
|
+
"""Cancel all concurrent tasks."""
|
|
224
|
+
self._task_handler.cancel_all(message=message)
|
|
@@ -17,6 +17,7 @@ from .run_control import RunControlHandler
|
|
|
17
17
|
from .command_executor import CommandExecutor
|
|
18
18
|
from .queue_worker import QueueWorker
|
|
19
19
|
from .status_bar import StatusBarHandler
|
|
20
|
+
from .task_handler import TaskHandler
|
|
20
21
|
|
|
21
22
|
|
|
22
23
|
def create_queue_worker(
|
|
@@ -76,7 +77,9 @@ def create_queue_worker(
|
|
|
76
77
|
rail_lights_handler = RailLightsHandler(
|
|
77
78
|
hardware_api=hardware_api,
|
|
78
79
|
)
|
|
79
|
-
|
|
80
|
+
task_handler = TaskHandler(
|
|
81
|
+
state_store=state_store, action_dispatcher=action_dispatcher
|
|
82
|
+
)
|
|
80
83
|
status_bar_handler = StatusBarHandler(hardware_api=hardware_api)
|
|
81
84
|
|
|
82
85
|
command_executor = CommandExecutor(
|
|
@@ -93,6 +96,7 @@ def create_queue_worker(
|
|
|
93
96
|
run_control=run_control_handler,
|
|
94
97
|
rail_lights=rail_lights_handler,
|
|
95
98
|
status_bar=status_bar_handler,
|
|
99
|
+
task_handler=task_handler,
|
|
96
100
|
)
|
|
97
101
|
|
|
98
102
|
return QueueWorker(
|
|
@@ -31,6 +31,7 @@ from ..types import (
|
|
|
31
31
|
OnLabwareLocation,
|
|
32
32
|
LabwareLocation,
|
|
33
33
|
OnDeckLabwareLocation,
|
|
34
|
+
GripperMoveType,
|
|
34
35
|
)
|
|
35
36
|
|
|
36
37
|
if TYPE_CHECKING:
|
|
@@ -141,10 +142,16 @@ class LabwareMovementHandler:
|
|
|
141
142
|
labware_definition = self._state_store.labware.get_definition(labware_id)
|
|
142
143
|
|
|
143
144
|
from_labware_center = self._state_store.geometry.get_labware_grip_point(
|
|
144
|
-
labware_definition=labware_definition,
|
|
145
|
+
labware_definition=labware_definition,
|
|
146
|
+
location=current_location,
|
|
147
|
+
move_type=GripperMoveType.PICK_UP_LABWARE,
|
|
148
|
+
user_additional_offset=user_pick_up_offset,
|
|
145
149
|
)
|
|
146
150
|
to_labware_center = self._state_store.geometry.get_labware_grip_point(
|
|
147
|
-
labware_definition=labware_definition,
|
|
151
|
+
labware_definition=labware_definition,
|
|
152
|
+
location=new_location,
|
|
153
|
+
move_type=GripperMoveType.DROP_LABWARE,
|
|
154
|
+
user_additional_offset=user_drop_offset,
|
|
148
155
|
)
|
|
149
156
|
|
|
150
157
|
if use_virtual_gripper:
|
|
@@ -193,20 +200,10 @@ class LabwareMovementHandler:
|
|
|
193
200
|
async with self._thermocycler_plate_lifter.lift_plate_for_labware_movement(
|
|
194
201
|
labware_location=current_location
|
|
195
202
|
):
|
|
196
|
-
final_offsets = (
|
|
197
|
-
self._state_store.geometry.get_final_labware_movement_offset_vectors(
|
|
198
|
-
from_location=current_location,
|
|
199
|
-
to_location=new_location,
|
|
200
|
-
additional_pick_up_offset=user_pick_up_offset,
|
|
201
|
-
additional_drop_offset=user_drop_offset,
|
|
202
|
-
current_labware=labware_definition,
|
|
203
|
-
)
|
|
204
|
-
)
|
|
205
203
|
movement_waypoints = get_gripper_labware_movement_waypoints(
|
|
206
204
|
from_labware_center=from_labware_center,
|
|
207
205
|
to_labware_center=to_labware_center,
|
|
208
206
|
gripper_home_z=gripper_homed_position.z,
|
|
209
|
-
offset_data=final_offsets,
|
|
210
207
|
post_drop_slide_offset=post_drop_slide_offset,
|
|
211
208
|
gripper_home_z_offset=gripper_z_offset,
|
|
212
209
|
)
|
|
@@ -83,6 +83,7 @@ class MovementHandler:
|
|
|
83
83
|
minimum_z_height: Optional[float] = None,
|
|
84
84
|
speed: Optional[float] = None,
|
|
85
85
|
operation_volume: Optional[float] = None,
|
|
86
|
+
offset_pipette_for_reservoir_subwells: bool = False,
|
|
86
87
|
) -> Point:
|
|
87
88
|
"""Move to a specific well."""
|
|
88
89
|
self._state_store.geometry.raise_if_labware_inaccessible_by_pipette(
|
|
@@ -143,6 +144,7 @@ class MovementHandler:
|
|
|
143
144
|
force_direct=force_direct,
|
|
144
145
|
minimum_z_height=minimum_z_height,
|
|
145
146
|
operation_volume=operation_volume,
|
|
147
|
+
offset_pipette_for_reservoir_subwells=offset_pipette_for_reservoir_subwells,
|
|
146
148
|
)
|
|
147
149
|
|
|
148
150
|
speed = self._state_store.pipettes.get_movement_speed(
|
|
@@ -51,6 +51,7 @@ class QueueWorker:
|
|
|
51
51
|
"""
|
|
52
52
|
if self._worker_task:
|
|
53
53
|
self._worker_task.cancel()
|
|
54
|
+
self._command_executor.cancel_tasks("Engine cancelled")
|
|
54
55
|
|
|
55
56
|
async def join(self) -> None:
|
|
56
57
|
"""Wait for the worker to finish, propagating any errors."""
|
|
@@ -65,7 +66,10 @@ class QueueWorker:
|
|
|
65
66
|
pass
|
|
66
67
|
except Exception as e:
|
|
67
68
|
log.error("Unhandled exception in QueueWorker job", exc_info=e)
|
|
69
|
+
self._command_executor.cancel_tasks("Engine failed")
|
|
68
70
|
raise e
|
|
71
|
+
else:
|
|
72
|
+
self._command_executor.cancel_tasks("Engine commands complete")
|
|
69
73
|
|
|
70
74
|
async def _run_commands(self) -> None:
|
|
71
75
|
async for command_id in self._command_generator():
|
|
@@ -31,3 +31,11 @@ class RunControlHandler:
|
|
|
31
31
|
"""Delay protocol execution for a duration."""
|
|
32
32
|
if not self._state_store.config.ignore_pause:
|
|
33
33
|
await asyncio.sleep(seconds)
|
|
34
|
+
|
|
35
|
+
async def wait_for_tasks(self, tasks: list[str]) -> None:
|
|
36
|
+
"""Wait for concurrent tasks to complete."""
|
|
37
|
+
await self._state_store.wait_for(
|
|
38
|
+
condition=lambda: self._state_store.tasks.all_tasks_finished_or_any_task_failed(
|
|
39
|
+
task_ids=tasks
|
|
40
|
+
)
|
|
41
|
+
)
|
|
@@ -0,0 +1,157 @@
|
|
|
1
|
+
"""Task handling."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
import logging
|
|
5
|
+
from typing import Protocol, AsyncIterator
|
|
6
|
+
from ..state.state import StateStore
|
|
7
|
+
from ..resources import ModelUtils, ConcurrencyProvider
|
|
8
|
+
from ..types import Task
|
|
9
|
+
import asyncio
|
|
10
|
+
import contextlib
|
|
11
|
+
from ..actions import ActionDispatcher, FinishTaskAction, StartTaskAction
|
|
12
|
+
from ..errors import ErrorOccurrence
|
|
13
|
+
from opentrons_shared_data.errors.exceptions import EnumeratedError, PythonException
|
|
14
|
+
|
|
15
|
+
log = logging.getLogger(__name__)
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
class TaskFunction(Protocol):
|
|
19
|
+
"""The function run inside a task protocol."""
|
|
20
|
+
|
|
21
|
+
async def __call__(self, task_handler: TaskHandler) -> None:
|
|
22
|
+
"""The function called inside a task."""
|
|
23
|
+
...
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
class TaskHandler:
|
|
27
|
+
"""Implementation logic for fast concurrency."""
|
|
28
|
+
|
|
29
|
+
_state_store: StateStore
|
|
30
|
+
_model_utils: ModelUtils
|
|
31
|
+
_concurrency_provider: ConcurrencyProvider
|
|
32
|
+
|
|
33
|
+
def __init__(
|
|
34
|
+
self,
|
|
35
|
+
state_store: StateStore,
|
|
36
|
+
action_dispatcher: ActionDispatcher,
|
|
37
|
+
model_utils: ModelUtils | None = None,
|
|
38
|
+
concurrency_provider: ConcurrencyProvider | None = None,
|
|
39
|
+
) -> None:
|
|
40
|
+
"""Initialize a TaskHandler instance."""
|
|
41
|
+
self._state_store = state_store
|
|
42
|
+
self._model_utils = model_utils or ModelUtils()
|
|
43
|
+
self._concurrency_provider = concurrency_provider or ConcurrencyProvider()
|
|
44
|
+
self._action_dispatcher = action_dispatcher
|
|
45
|
+
|
|
46
|
+
async def create_task(
|
|
47
|
+
self, task_function: TaskFunction, id: str | None = None
|
|
48
|
+
) -> Task:
|
|
49
|
+
"""Create a task and immediately schedules it."""
|
|
50
|
+
task_id = self._model_utils.ensure_id(id)
|
|
51
|
+
asyncio_task = asyncio.create_task(
|
|
52
|
+
task_function(task_handler=self), name=f"engine-task-{task_id}"
|
|
53
|
+
)
|
|
54
|
+
|
|
55
|
+
def _done_callback(task: asyncio.Task[None]) -> None:
|
|
56
|
+
try:
|
|
57
|
+
maybe_exception = task.exception()
|
|
58
|
+
except asyncio.CancelledError as e:
|
|
59
|
+
maybe_exception = e
|
|
60
|
+
if isinstance(maybe_exception, EnumeratedError):
|
|
61
|
+
occurence: ErrorOccurrence | None = ErrorOccurrence.from_failed(
|
|
62
|
+
id=self._model_utils.generate_id(),
|
|
63
|
+
createdAt=self._model_utils.get_timestamp(),
|
|
64
|
+
error=maybe_exception,
|
|
65
|
+
)
|
|
66
|
+
elif isinstance(maybe_exception, BaseException):
|
|
67
|
+
occurence = ErrorOccurrence.from_failed(
|
|
68
|
+
id=self._model_utils.generate_id(),
|
|
69
|
+
createdAt=self._model_utils.get_timestamp(),
|
|
70
|
+
error=PythonException(maybe_exception),
|
|
71
|
+
)
|
|
72
|
+
else:
|
|
73
|
+
occurence = None
|
|
74
|
+
try:
|
|
75
|
+
self._action_dispatcher.dispatch(
|
|
76
|
+
FinishTaskAction(
|
|
77
|
+
task_id=task_id,
|
|
78
|
+
finished_at=self._model_utils.get_timestamp(),
|
|
79
|
+
error=occurence,
|
|
80
|
+
),
|
|
81
|
+
)
|
|
82
|
+
except BaseException:
|
|
83
|
+
log.exception("Exception in task finish dispatch.")
|
|
84
|
+
|
|
85
|
+
asyncio_task.add_done_callback(_done_callback)
|
|
86
|
+
task = Task(
|
|
87
|
+
id=task_id,
|
|
88
|
+
createdAt=self._model_utils.get_timestamp(),
|
|
89
|
+
asyncioTask=asyncio_task,
|
|
90
|
+
)
|
|
91
|
+
self._action_dispatcher.dispatch(StartTaskAction(task))
|
|
92
|
+
return task
|
|
93
|
+
|
|
94
|
+
@staticmethod
|
|
95
|
+
def _empty_queue(
|
|
96
|
+
queue: "asyncio.Queue[asyncio.Task[None]]", this_task: asyncio.Task[None]
|
|
97
|
+
) -> None:
|
|
98
|
+
"""Empties the queue."""
|
|
99
|
+
try:
|
|
100
|
+
while True:
|
|
101
|
+
task = queue.get_nowait()
|
|
102
|
+
if task is this_task:
|
|
103
|
+
break
|
|
104
|
+
except asyncio.QueueEmpty:
|
|
105
|
+
pass
|
|
106
|
+
|
|
107
|
+
@contextlib.asynccontextmanager
|
|
108
|
+
async def synchronize_cancel_latest(self, group_id: str) -> AsyncIterator[None]:
|
|
109
|
+
"""Cancel current task."""
|
|
110
|
+
lock = self._concurrency_provider.lock_for_group(group_id)
|
|
111
|
+
if lock.locked():
|
|
112
|
+
raise asyncio.CancelledError()
|
|
113
|
+
async with lock:
|
|
114
|
+
yield
|
|
115
|
+
|
|
116
|
+
@contextlib.asynccontextmanager
|
|
117
|
+
async def synchronize_cancel_previous(self, group_id: str) -> AsyncIterator[None]:
|
|
118
|
+
"""Cancel previous run."""
|
|
119
|
+
queue = self._concurrency_provider.queue_for_group(group_id)
|
|
120
|
+
while not queue.empty():
|
|
121
|
+
task = queue.get_nowait()
|
|
122
|
+
task.cancel()
|
|
123
|
+
this_task = asyncio.current_task()
|
|
124
|
+
assert this_task is not None
|
|
125
|
+
queue.put_nowait(this_task)
|
|
126
|
+
try:
|
|
127
|
+
yield
|
|
128
|
+
except asyncio.CancelledError:
|
|
129
|
+
raise
|
|
130
|
+
except BaseException:
|
|
131
|
+
self._empty_queue(queue, this_task)
|
|
132
|
+
raise
|
|
133
|
+
else:
|
|
134
|
+
self._empty_queue(queue, this_task)
|
|
135
|
+
|
|
136
|
+
@contextlib.asynccontextmanager
|
|
137
|
+
async def synchronize_sequential(self, group_id: str) -> AsyncIterator[None]:
|
|
138
|
+
"""Run tasks one after the other."""
|
|
139
|
+
lock = self._concurrency_provider.lock_for_group(group_id)
|
|
140
|
+
async with lock:
|
|
141
|
+
yield
|
|
142
|
+
|
|
143
|
+
@contextlib.asynccontextmanager
|
|
144
|
+
async def synchronize_concurrent(self, group_id: str) -> AsyncIterator[None]:
|
|
145
|
+
"""Run a list of tasks at the same time."""
|
|
146
|
+
yield
|
|
147
|
+
|
|
148
|
+
def cancel_all(self, message: str | None = None) -> None:
|
|
149
|
+
"""Cancel all asyncio tasks immediately.
|
|
150
|
+
|
|
151
|
+
Do not call this more than once synchronously because
|
|
152
|
+
that could lead to tasks cancelling more than once.
|
|
153
|
+
It can be called if there are no current tasks. In that case
|
|
154
|
+
nothing will happen.
|
|
155
|
+
"""
|
|
156
|
+
for task in self._state_store.tasks.get_all_current():
|
|
157
|
+
task.asyncioTask.cancel(msg=message)
|
|
@@ -59,7 +59,6 @@ from .actions import (
|
|
|
59
59
|
AddAddressableAreaAction,
|
|
60
60
|
AddModuleAction,
|
|
61
61
|
HardwareStoppedAction,
|
|
62
|
-
ResetTipsAction,
|
|
63
62
|
SetPipetteMovementSpeedAction,
|
|
64
63
|
)
|
|
65
64
|
|
|
@@ -322,24 +321,10 @@ class ProtocolEngine:
|
|
|
322
321
|
)
|
|
323
322
|
return completed_command
|
|
324
323
|
|
|
325
|
-
def
|
|
326
|
-
"""Signal to the engine that an E-stop event occurred.
|
|
327
|
-
|
|
328
|
-
If an estop happens while the robot is moving, lower layers physically stop
|
|
329
|
-
motion and raise the event as an exception, which fails the Protocol Engine
|
|
330
|
-
command. No action from the `ProtocolEngine` caller is needed to handle that.
|
|
331
|
-
|
|
332
|
-
However, if an estop happens in between commands, or in the middle of
|
|
333
|
-
a command like `comment` or `waitForDuration` that doesn't access the hardware,
|
|
334
|
-
`ProtocolEngine` needs to be told about it so it can interrupt the command
|
|
335
|
-
and stop executing any more. This method is how to do that.
|
|
336
|
-
|
|
337
|
-
This acts roughly like `request_stop()`. After calling this, you should call
|
|
338
|
-
`finish()` with an EStopActivatedError.
|
|
339
|
-
"""
|
|
324
|
+
def _stop_from_asynchronous_error(self) -> None:
|
|
340
325
|
try:
|
|
341
326
|
action = self._state_store.commands.validate_action_allowed(
|
|
342
|
-
StopAction(
|
|
327
|
+
StopAction(from_asynchronous_error=True)
|
|
343
328
|
)
|
|
344
329
|
except Exception: # todo(mm, 2024-04-16): Catch a more specific type.
|
|
345
330
|
# This is likely called from some hardware API callback that doesn't care
|
|
@@ -358,9 +343,72 @@ class ProtocolEngine:
|
|
|
358
343
|
# do this because we want to make sure non-hardware commands, like
|
|
359
344
|
# `waitForDuration`, are also interrupted.
|
|
360
345
|
self._get_queue_worker.cancel()
|
|
346
|
+
|
|
347
|
+
def estop(self) -> None:
|
|
348
|
+
"""Signal to the engine that an E-stop event occurred.
|
|
349
|
+
|
|
350
|
+
If an estop happens while the robot is moving, lower layers physically stop
|
|
351
|
+
motion and raise the event as an exception, which fails the Protocol Engine
|
|
352
|
+
command. No action from the `ProtocolEngine` caller is needed to handle that.
|
|
353
|
+
|
|
354
|
+
However, if an estop happens in between commands, or in the middle of
|
|
355
|
+
a command like `comment` or `waitForDuration` that doesn't access the hardware,
|
|
356
|
+
`ProtocolEngine` needs to be told about it so it can interrupt the command
|
|
357
|
+
and stop executing any more. This method is how to do that.
|
|
358
|
+
|
|
359
|
+
This acts roughly like `request_stop()`. After calling this, you should call
|
|
360
|
+
`finish()` with an EStopActivatedError.
|
|
361
|
+
"""
|
|
361
362
|
# Unlike self.request_stop(), we don't need to do
|
|
362
363
|
# self._hardware_api.cancel_execution_and_running_tasks(). Since this was an
|
|
363
364
|
# E-stop event, the hardware API already knows.
|
|
365
|
+
self._stop_from_asynchronous_error()
|
|
366
|
+
|
|
367
|
+
async def async_module_error(
|
|
368
|
+
self, module_model: ModuleModel, serial: str | None
|
|
369
|
+
) -> bool:
|
|
370
|
+
"""Signal to the engine that an asynchronous module error occured.
|
|
371
|
+
|
|
372
|
+
The return value of this function signals whether the error is relevant to the protocol
|
|
373
|
+
or not. If the function returns True, the error is relevant. The engine will stop, and
|
|
374
|
+
the caller should call `finish()` with the error object that signaled the error. If
|
|
375
|
+
the function returns False, the error is not relevant. The engine will not stop, and the
|
|
376
|
+
caller should not call `finish()`.
|
|
377
|
+
|
|
378
|
+
Asynchronous module errors are signaled when a module enters a hardware error state
|
|
379
|
+
- for instance, a thermocycler's thermistors fail because of condensation, or a
|
|
380
|
+
heater-shaker's wires fray and snap, or a module is accidentally disconnected. These
|
|
381
|
+
errors are not related to a particular command, even a currently-happening module
|
|
382
|
+
control command for the module in the error state.
|
|
383
|
+
|
|
384
|
+
Similar to an estop error, the error can occur at any time relative to the lifecycle
|
|
385
|
+
of the engine run or of any particular command.
|
|
386
|
+
|
|
387
|
+
Unlike an estop, the motion control hardware will not be raising an error and will not
|
|
388
|
+
stop on its own; the stop action derived from this call will do that.
|
|
389
|
+
"""
|
|
390
|
+
if not self._state_store.modules.get_has_module_probably_matching_hardware_details(
|
|
391
|
+
module_model, serial
|
|
392
|
+
):
|
|
393
|
+
return False
|
|
394
|
+
self._stop_from_asynchronous_error()
|
|
395
|
+
# like self.request_stop, and unlike self.estop(), we must explicitly request that the
|
|
396
|
+
# hardware stops execution, since not all asynchronous errors will cause the hardware
|
|
397
|
+
# to know that it should stop.
|
|
398
|
+
await self._do_hardware_stop()
|
|
399
|
+
return True
|
|
400
|
+
|
|
401
|
+
async def _do_hardware_stop(self) -> None:
|
|
402
|
+
"""Make the hardware stop now."""
|
|
403
|
+
if self._hardware_api.is_movement_execution_taskified():
|
|
404
|
+
# We 'taskify' hardware controller movement functions when running protocols
|
|
405
|
+
# that are not backed by the engine. Such runs cannot be stopped by cancelling
|
|
406
|
+
# the queue worker and hence need to be stopped via the execution manager.
|
|
407
|
+
# `cancel_execution_and_running_tasks()` sets the execution manager in a CANCELLED state
|
|
408
|
+
# and cancels the running tasks, which raises an error and gets us out of the
|
|
409
|
+
# run function execution, just like `_queue_worker.cancel()` does for
|
|
410
|
+
# engine-backed runs.
|
|
411
|
+
await self._hardware_api.cancel_execution_and_running_tasks()
|
|
364
412
|
|
|
365
413
|
async def request_stop(self) -> None:
|
|
366
414
|
"""Make command execution stop soon.
|
|
@@ -378,15 +426,7 @@ class ProtocolEngine:
|
|
|
378
426
|
action = self._state_store.commands.validate_action_allowed(StopAction())
|
|
379
427
|
self._action_dispatcher.dispatch(action)
|
|
380
428
|
self._get_queue_worker.cancel()
|
|
381
|
-
|
|
382
|
-
# We 'taskify' hardware controller movement functions when running protocols
|
|
383
|
-
# that are not backed by the engine. Such runs cannot be stopped by cancelling
|
|
384
|
-
# the queue worker and hence need to be stopped via the execution manager.
|
|
385
|
-
# `cancel_execution_and_running_tasks()` sets the execution manager in a CANCELLED state
|
|
386
|
-
# and cancels the running tasks, which raises an error and gets us out of the
|
|
387
|
-
# run function execution, just like `_queue_worker.cancel()` does for
|
|
388
|
-
# engine-backed runs.
|
|
389
|
-
await self._hardware_api.cancel_execution_and_running_tasks()
|
|
429
|
+
await self._do_hardware_stop()
|
|
390
430
|
|
|
391
431
|
async def wait_until_complete(self) -> None:
|
|
392
432
|
"""Wait until there are no more commands to execute.
|
|
@@ -429,7 +469,7 @@ class ProtocolEngine:
|
|
|
429
469
|
post_run_hardware_state: The state in which to leave the gantry and motors in
|
|
430
470
|
after the run is over.
|
|
431
471
|
"""
|
|
432
|
-
if self._state_store.commands.
|
|
472
|
+
if self._state_store.commands.get_is_stopped_by_async_error():
|
|
433
473
|
# This handles the case where the E-stop was pressed while we were *not* in the middle
|
|
434
474
|
# of some hardware interaction that would raise it as an exception. For example, imagine
|
|
435
475
|
# we were paused between two commands, or imagine we were executing a waitForDuration.
|
|
@@ -586,12 +626,6 @@ class ProtocolEngine:
|
|
|
586
626
|
AddAddressableAreaAction(addressable_area_name)
|
|
587
627
|
)
|
|
588
628
|
|
|
589
|
-
def reset_tips(self, labware_id: str) -> None:
|
|
590
|
-
"""Reset the tip state of a given labware."""
|
|
591
|
-
# TODO(mm, 2023-03-10): Safely raise an error if the given labware isn't a
|
|
592
|
-
# tip rack?
|
|
593
|
-
self._action_dispatcher.dispatch(ResetTipsAction(labware_id=labware_id))
|
|
594
|
-
|
|
595
629
|
# TODO(mm, 2022-11-10): This is a method on ProtocolEngine instead of a command
|
|
596
630
|
# as a quick hack to support Python protocols. We should consider making this a
|
|
597
631
|
# command, or adding speed parameters to existing commands.
|