opentrons 8.7.0a9__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 +126 -49
- opentrons/drivers/heater_shaker/abstract.py +5 -0
- opentrons/drivers/heater_shaker/driver.py +10 -0
- opentrons/drivers/heater_shaker/simulator.py +4 -0
- opentrons/drivers/thermocycler/abstract.py +6 -0
- opentrons/drivers/thermocycler/driver.py +61 -10
- opentrons/drivers/thermocycler/simulator.py +6 -0
- 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/api.py +24 -5
- opentrons/hardware_control/backends/controller.py +8 -2
- opentrons/hardware_control/backends/flex_protocol.py +1 -0
- opentrons/hardware_control/backends/ot3controller.py +35 -2
- opentrons/hardware_control/backends/ot3simulator.py +3 -1
- opentrons/hardware_control/backends/ot3utils.py +37 -0
- opentrons/hardware_control/backends/simulator.py +2 -1
- opentrons/hardware_control/backends/subsystem_manager.py +5 -2
- opentrons/hardware_control/emulation/abstract_emulator.py +6 -4
- opentrons/hardware_control/emulation/connection_handler.py +8 -5
- opentrons/hardware_control/emulation/heater_shaker.py +12 -3
- opentrons/hardware_control/emulation/settings.py +1 -1
- opentrons/hardware_control/emulation/thermocycler.py +67 -15
- opentrons/hardware_control/module_control.py +105 -10
- 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 +42 -5
- opentrons/hardware_control/modules/magdeck.py +8 -4
- opentrons/hardware_control/modules/mod_abc.py +14 -6
- opentrons/hardware_control/modules/tempdeck.py +25 -5
- opentrons/hardware_control/modules/thermocycler.py +68 -11
- opentrons/hardware_control/modules/types.py +20 -1
- opentrons/hardware_control/modules/utils.py +11 -4
- opentrons/hardware_control/motion_utilities.py +6 -6
- opentrons/hardware_control/nozzle_manager.py +3 -0
- opentrons/hardware_control/ot3api.py +85 -17
- opentrons/hardware_control/poller.py +22 -8
- opentrons/hardware_control/protocols/liquid_handler.py +6 -2
- opentrons/hardware_control/scripts/update_module_fw.py +5 -0
- opentrons/hardware_control/types.py +43 -2
- opentrons/legacy_commands/commands.py +58 -5
- opentrons/legacy_commands/module_commands.py +52 -0
- opentrons/legacy_commands/protocol_commands.py +53 -1
- opentrons/legacy_commands/types.py +155 -1
- opentrons/motion_planning/deck_conflict.py +17 -12
- opentrons/motion_planning/waypoints.py +15 -29
- opentrons/protocol_api/__init__.py +5 -1
- opentrons/protocol_api/_transfer_liquid_validation.py +17 -2
- opentrons/protocol_api/_types.py +8 -1
- opentrons/protocol_api/core/common.py +3 -1
- opentrons/protocol_api/core/engine/_default_labware_versions.py +33 -11
- opentrons/protocol_api/core/engine/deck_conflict.py +3 -1
- opentrons/protocol_api/core/engine/instrument.py +109 -26
- opentrons/protocol_api/core/engine/labware.py +8 -1
- opentrons/protocol_api/core/engine/module_core.py +95 -4
- opentrons/protocol_api/core/engine/protocol.py +51 -2
- opentrons/protocol_api/core/engine/stringify.py +2 -0
- opentrons/protocol_api/core/engine/tasks.py +48 -0
- opentrons/protocol_api/core/engine/well.py +8 -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 +33 -2
- opentrons/protocol_api/core/legacy/legacy_protocol_core.py +23 -1
- opentrons/protocol_api/core/legacy/legacy_well_core.py +4 -0
- opentrons/protocol_api/core/legacy/tasks.py +19 -0
- opentrons/protocol_api/core/legacy_simulator/legacy_instrument_core.py +19 -2
- 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 +58 -2
- opentrons/protocol_api/core/protocol.py +23 -2
- opentrons/protocol_api/core/tasks.py +31 -0
- opentrons/protocol_api/core/well.py +4 -0
- opentrons/protocol_api/instrument_context.py +388 -2
- opentrons/protocol_api/labware.py +10 -2
- opentrons/protocol_api/module_contexts.py +170 -6
- opentrons/protocol_api/protocol_context.py +87 -21
- opentrons/protocol_api/robot_context.py +41 -25
- opentrons/protocol_api/tasks.py +48 -0
- opentrons/protocol_api/validation.py +49 -3
- opentrons/protocol_engine/__init__.py +4 -0
- opentrons/protocol_engine/actions/__init__.py +6 -2
- opentrons/protocol_engine/actions/actions.py +31 -9
- opentrons/protocol_engine/clients/sync_client.py +42 -7
- opentrons/protocol_engine/commands/__init__.py +56 -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/absorbance_reader/read.py +22 -23
- opentrons/protocol_engine/commands/aspirate.py +1 -0
- 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 +2 -0
- opentrons/protocol_engine/commands/command_unions.py +62 -0
- opentrons/protocol_engine/commands/create_timer.py +83 -0
- opentrons/protocol_engine/commands/dispense.py +1 -0
- opentrons/protocol_engine/commands/dispense_while_tracking.py +56 -19
- opentrons/protocol_engine/commands/drop_tip.py +32 -8
- 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/__init__.py +14 -0
- opentrons/protocol_engine/commands/heater_shaker/common.py +20 -0
- opentrons/protocol_engine/commands/heater_shaker/set_and_wait_for_shake_speed.py +5 -4
- opentrons/protocol_engine/commands/heater_shaker/set_shake_speed.py +136 -0
- opentrons/protocol_engine/commands/heater_shaker/set_target_temperature.py +31 -5
- 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 +31 -2
- opentrons/protocol_engine/commands/pick_up_tip.py +21 -11
- opentrons/protocol_engine/commands/pipetting_common.py +48 -3
- opentrons/protocol_engine/commands/set_tip_state.py +97 -0
- opentrons/protocol_engine/commands/temperature_module/set_target_temperature.py +38 -7
- opentrons/protocol_engine/commands/thermocycler/__init__.py +16 -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 +44 -7
- opentrons/protocol_engine/commands/thermocycler/set_target_lid_temperature.py +43 -14
- opentrons/protocol_engine/commands/thermocycler/start_run_extended_profile.py +191 -0
- 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/create_protocol_engine.py +12 -0
- opentrons/protocol_engine/engine_support.py +3 -0
- opentrons/protocol_engine/errors/__init__.py +12 -0
- opentrons/protocol_engine/errors/exceptions.py +119 -0
- opentrons/protocol_engine/execution/__init__.py +4 -0
- opentrons/protocol_engine/execution/command_executor.py +62 -1
- opentrons/protocol_engine/execution/create_queue_worker.py +9 -2
- opentrons/protocol_engine/execution/labware_movement.py +13 -15
- opentrons/protocol_engine/execution/movement.py +2 -0
- opentrons/protocol_engine/execution/pipetting.py +19 -25
- 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 +137 -36
- opentrons/protocol_engine/resources/__init__.py +4 -0
- opentrons/protocol_engine/resources/camera_provider.py +110 -0
- opentrons/protocol_engine/resources/concurrency_provider.py +27 -0
- opentrons/protocol_engine/resources/deck_configuration_provider.py +7 -0
- opentrons/protocol_engine/resources/file_provider.py +133 -58
- opentrons/protocol_engine/resources/labware_validation.py +10 -6
- opentrons/protocol_engine/slot_standardization.py +2 -0
- opentrons/protocol_engine/state/_well_math.py +60 -18
- opentrons/protocol_engine/state/addressable_areas.py +2 -0
- opentrons/protocol_engine/state/camera.py +54 -0
- opentrons/protocol_engine/state/commands.py +37 -14
- opentrons/protocol_engine/state/geometry.py +276 -379
- opentrons/protocol_engine/state/labware.py +62 -108
- opentrons/protocol_engine/state/labware_origin_math/errors.py +94 -0
- opentrons/protocol_engine/state/labware_origin_math/stackup_origin_to_labware_origin.py +1336 -0
- opentrons/protocol_engine/state/module_substates/thermocycler_module_substate.py +37 -0
- opentrons/protocol_engine/state/modules.py +30 -8
- opentrons/protocol_engine/state/motion.py +44 -0
- opentrons/protocol_engine/state/preconditions.py +59 -0
- opentrons/protocol_engine/state/state.py +44 -0
- opentrons/protocol_engine/state/state_summary.py +4 -0
- opentrons/protocol_engine/state/tasks.py +139 -0
- opentrons/protocol_engine/state/tips.py +177 -258
- opentrons/protocol_engine/state/update_types.py +26 -9
- opentrons/protocol_engine/types/__init__.py +23 -4
- opentrons/protocol_engine/types/command_preconditions.py +18 -0
- 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/location.py +26 -2
- opentrons/protocol_engine/types/module.py +11 -1
- 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/protocol_runner.py +14 -1
- opentrons/protocol_runner/run_orchestrator.py +49 -2
- opentrons/protocols/advanced_control/transfers/transfer_liquid_utils.py +2 -2
- opentrons/protocols/api_support/definitions.py +1 -1
- opentrons/protocols/api_support/types.py +2 -1
- opentrons/simulate.py +51 -15
- opentrons/system/camera.py +334 -4
- opentrons/system/ffmpeg.py +110 -0
- {opentrons-8.7.0a9.dist-info → opentrons-8.8.0a7.dist-info}/METADATA +4 -4
- {opentrons-8.7.0a9.dist-info → opentrons-8.8.0a7.dist-info}/RECORD +188 -160
- opentrons/protocol_engine/state/_labware_origin_math.py +0 -636
- {opentrons-8.7.0a9.dist-info → opentrons-8.8.0a7.dist-info}/WHEEL +0 -0
- {opentrons-8.7.0a9.dist-info → opentrons-8.8.0a7.dist-info}/entry_points.txt +0 -0
- {opentrons-8.7.0a9.dist-info → opentrons-8.8.0a7.dist-info}/licenses/LICENSE +0 -0
|
@@ -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
|
|
|
@@ -1248,6 +1291,19 @@ class StorageLimitReachedError(ProtocolEngineError):
|
|
|
1248
1291
|
super().__init__(ErrorCodes.GENERAL_ERROR, message, detail, wrapping)
|
|
1249
1292
|
|
|
1250
1293
|
|
|
1294
|
+
class FileNameInvalidError(ProtocolEngineError):
|
|
1295
|
+
"""Raised to indicate that a file cannot be saved with a given name."""
|
|
1296
|
+
|
|
1297
|
+
def __init__(
|
|
1298
|
+
self,
|
|
1299
|
+
message: Optional[str] = None,
|
|
1300
|
+
detail: Optional[Dict[str, str]] = None,
|
|
1301
|
+
wrapping: Optional[Sequence[EnumeratedError]] = None,
|
|
1302
|
+
) -> None:
|
|
1303
|
+
"""Build an FileNameInvalidError."""
|
|
1304
|
+
super().__init__(ErrorCodes.GENERAL_ERROR, message, detail, wrapping)
|
|
1305
|
+
|
|
1306
|
+
|
|
1251
1307
|
class LiquidClassDoesNotExistError(ProtocolEngineError):
|
|
1252
1308
|
"""Raised when referencing a liquid class that has not been loaded."""
|
|
1253
1309
|
|
|
@@ -1296,6 +1352,18 @@ class FlexStackerLabwarePoolNotYetDefinedError(ProtocolEngineError):
|
|
|
1296
1352
|
super().__init__(ErrorCodes.GENERAL_ERROR, message, details, wrapping)
|
|
1297
1353
|
|
|
1298
1354
|
|
|
1355
|
+
class LabwarePoolNotCompatibleWithModuleError(ProtocolEngineError):
|
|
1356
|
+
"""Raised when attempting to use a labware pool that is incompatible with a module."""
|
|
1357
|
+
|
|
1358
|
+
def __init__(
|
|
1359
|
+
self,
|
|
1360
|
+
message: Optional[str] = None,
|
|
1361
|
+
details: Optional[dict[str, Any]] = None,
|
|
1362
|
+
wrapping: Optional[Sequence[EnumeratedError]] = None,
|
|
1363
|
+
) -> None:
|
|
1364
|
+
super().__init__(ErrorCodes.GENERAL_ERROR, message, details, wrapping)
|
|
1365
|
+
|
|
1366
|
+
|
|
1299
1367
|
class InvalidLabwarePositionError(ProtocolEngineError):
|
|
1300
1368
|
"""Raised when a labware position is internally invalid."""
|
|
1301
1369
|
|
|
@@ -1306,3 +1374,54 @@ class InvalidLabwarePositionError(ProtocolEngineError):
|
|
|
1306
1374
|
wrapping: Optional[Sequence[EnumeratedError]] = None,
|
|
1307
1375
|
) -> None:
|
|
1308
1376
|
super().__init__(ErrorCodes.GENERAL_ERROR, message, details, wrapping)
|
|
1377
|
+
|
|
1378
|
+
|
|
1379
|
+
class InvalidModuleOrientation(ProtocolEngineError):
|
|
1380
|
+
"""Raised when a module orientation is invalid for a slot id."""
|
|
1381
|
+
|
|
1382
|
+
def __init__(
|
|
1383
|
+
self,
|
|
1384
|
+
message: Optional[str] = None,
|
|
1385
|
+
details: Optional[dict[str, Any]] = None,
|
|
1386
|
+
wrapping: Optional[Sequence[EnumeratedError]] = None,
|
|
1387
|
+
) -> None:
|
|
1388
|
+
super().__init__(ErrorCodes.GENERAL_ERROR, message, details, wrapping)
|
|
1389
|
+
|
|
1390
|
+
|
|
1391
|
+
class CameraCaptureError(ProtocolEngineError):
|
|
1392
|
+
"""Raised when an Camera Capture attempt fails."""
|
|
1393
|
+
|
|
1394
|
+
def __init__(
|
|
1395
|
+
self,
|
|
1396
|
+
message: Optional[str] = None,
|
|
1397
|
+
details: Optional[Dict[str, Any]] = None,
|
|
1398
|
+
wrapping: Optional[Sequence[EnumeratedError]] = None,
|
|
1399
|
+
) -> None:
|
|
1400
|
+
"""Build a CameraCaptureError."""
|
|
1401
|
+
super().__init__(ErrorCodes.CAMERA_ERROR, message, details, wrapping)
|
|
1402
|
+
|
|
1403
|
+
|
|
1404
|
+
class CameraDisabledError(ProtocolEngineError):
|
|
1405
|
+
"""Raised when a Camera was referenced while cameras are disabled."""
|
|
1406
|
+
|
|
1407
|
+
def __init__(
|
|
1408
|
+
self,
|
|
1409
|
+
message: Optional[str] = None,
|
|
1410
|
+
details: Optional[Dict[str, Any]] = None,
|
|
1411
|
+
wrapping: Optional[Sequence[EnumeratedError]] = None,
|
|
1412
|
+
) -> None:
|
|
1413
|
+
"""Build a CameraDisabledError."""
|
|
1414
|
+
super().__init__(ErrorCodes.CAMERA_ERROR, message, details, wrapping)
|
|
1415
|
+
|
|
1416
|
+
|
|
1417
|
+
class CameraSettingsInvalidError(ProtocolEngineError):
|
|
1418
|
+
"""Raised when a Camera was given invalid settings."""
|
|
1419
|
+
|
|
1420
|
+
def __init__(
|
|
1421
|
+
self,
|
|
1422
|
+
message: Optional[str] = None,
|
|
1423
|
+
details: Optional[Dict[str, Any]] = None,
|
|
1424
|
+
wrapping: Optional[Sequence[EnumeratedError]] = None,
|
|
1425
|
+
) -> None:
|
|
1426
|
+
"""Build a CameraSettingsInvalidError."""
|
|
1427
|
+
super().__init__(ErrorCodes.CAMERA_ERROR, message, details, wrapping)
|
|
@@ -21,7 +21,9 @@ 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
|
|
26
|
+
from ..resources.camera_provider import CameraProvider
|
|
25
27
|
|
|
26
28
|
# .thermocycler_movement_flagger omitted from package's public interface.
|
|
27
29
|
|
|
@@ -46,5 +48,7 @@ __all__ = [
|
|
|
46
48
|
"DoorWatcher",
|
|
47
49
|
"RailLightsHandler",
|
|
48
50
|
"StatusBarHandler",
|
|
51
|
+
"TaskHandler",
|
|
49
52
|
"FileProvider",
|
|
53
|
+
"CameraProvider",
|
|
50
54
|
]
|
|
@@ -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,
|
|
@@ -35,6 +38,8 @@ from .tip_handler import TipHandler
|
|
|
35
38
|
from .run_control import RunControlHandler
|
|
36
39
|
from .rail_lights import RailLightsHandler
|
|
37
40
|
from .status_bar import StatusBarHandler
|
|
41
|
+
from .task_handler import TaskHandler
|
|
42
|
+
from ..commands import Command
|
|
38
43
|
|
|
39
44
|
|
|
40
45
|
log = getLogger(__name__)
|
|
@@ -74,6 +79,7 @@ class CommandExecutor:
|
|
|
74
79
|
self,
|
|
75
80
|
hardware_api: HardwareControlAPI,
|
|
76
81
|
file_provider: FileProvider,
|
|
82
|
+
camera_provider: CameraProvider,
|
|
77
83
|
state_store: StateStore,
|
|
78
84
|
action_dispatcher: ActionDispatcher,
|
|
79
85
|
equipment: EquipmentHandler,
|
|
@@ -85,12 +91,14 @@ class CommandExecutor:
|
|
|
85
91
|
run_control: RunControlHandler,
|
|
86
92
|
rail_lights: RailLightsHandler,
|
|
87
93
|
status_bar: StatusBarHandler,
|
|
94
|
+
task_handler: TaskHandler,
|
|
88
95
|
model_utils: Optional[ModelUtils] = None,
|
|
89
96
|
command_note_tracker_provider: Optional[CommandNoteTrackerProvider] = None,
|
|
90
97
|
) -> None:
|
|
91
98
|
"""Initialize the CommandExecutor with access to its dependencies."""
|
|
92
99
|
self._hardware_api = hardware_api
|
|
93
100
|
self._file_provider = file_provider
|
|
101
|
+
self._camera_provider = camera_provider
|
|
94
102
|
self._state_store = state_store
|
|
95
103
|
self._action_dispatcher = action_dispatcher
|
|
96
104
|
self._equipment = equipment
|
|
@@ -106,6 +114,7 @@ class CommandExecutor:
|
|
|
106
114
|
self._command_note_tracker_provider = (
|
|
107
115
|
command_note_tracker_provider or _NoteTracker
|
|
108
116
|
)
|
|
117
|
+
self._task_handler = task_handler
|
|
109
118
|
|
|
110
119
|
async def execute(self, command_id: str) -> None:
|
|
111
120
|
"""Run a given command's execution procedure.
|
|
@@ -120,6 +129,7 @@ class CommandExecutor:
|
|
|
120
129
|
state_view=self._state_store,
|
|
121
130
|
hardware_api=self._hardware_api,
|
|
122
131
|
file_provider=self._file_provider,
|
|
132
|
+
camera_provider=self._camera_provider,
|
|
123
133
|
equipment=self._equipment,
|
|
124
134
|
movement=self._movement,
|
|
125
135
|
gantry_mover=self._gantry_mover,
|
|
@@ -131,6 +141,7 @@ class CommandExecutor:
|
|
|
131
141
|
model_utils=self._model_utils,
|
|
132
142
|
status_bar=self._status_bar,
|
|
133
143
|
command_note_adder=note_tracker,
|
|
144
|
+
task_handler=self._task_handler,
|
|
134
145
|
)
|
|
135
146
|
|
|
136
147
|
started_at = self._model_utils.get_timestamp()
|
|
@@ -144,6 +155,7 @@ class CommandExecutor:
|
|
|
144
155
|
log.debug(
|
|
145
156
|
f"Executing {running_command.id}, {running_command.commandType}, {running_command.params}"
|
|
146
157
|
)
|
|
158
|
+
error_occurred = False
|
|
147
159
|
try:
|
|
148
160
|
result = await command_impl.execute(
|
|
149
161
|
running_command.params # type: ignore[arg-type]
|
|
@@ -167,6 +179,7 @@ class CommandExecutor:
|
|
|
167
179
|
running_command,
|
|
168
180
|
None,
|
|
169
181
|
)
|
|
182
|
+
|
|
170
183
|
note_tracker(make_error_recovery_debug_note(error_recovery_type))
|
|
171
184
|
self._action_dispatcher.dispatch(
|
|
172
185
|
FailCommandAction(
|
|
@@ -179,6 +192,7 @@ class CommandExecutor:
|
|
|
179
192
|
type=error_recovery_type,
|
|
180
193
|
)
|
|
181
194
|
)
|
|
195
|
+
error_occurred = True
|
|
182
196
|
|
|
183
197
|
else:
|
|
184
198
|
if isinstance(result, SuccessData):
|
|
@@ -214,3 +228,50 @@ class CommandExecutor:
|
|
|
214
228
|
type=error_recovery_type,
|
|
215
229
|
)
|
|
216
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)
|
|
236
|
+
|
|
237
|
+
def cancel_tasks(self, message: str | None = None) -> None:
|
|
238
|
+
"""Cancel all concurrent tasks."""
|
|
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
|
|
@@ -17,11 +17,13 @@ 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(
|
|
23
24
|
hardware_api: HardwareControlAPI,
|
|
24
25
|
file_provider: FileProvider,
|
|
26
|
+
camera_provider: CameraProvider,
|
|
25
27
|
state_store: StateStore,
|
|
26
28
|
action_dispatcher: ActionDispatcher,
|
|
27
29
|
command_generator: Callable[[], AsyncGenerator[str, None]],
|
|
@@ -31,6 +33,7 @@ def create_queue_worker(
|
|
|
31
33
|
Arguments:
|
|
32
34
|
hardware_api: Hardware control API to pass down to dependencies.
|
|
33
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.
|
|
34
37
|
state_store: StateStore to pass down to dependencies.
|
|
35
38
|
action_dispatcher: ActionDispatcher to pass down to dependencies.
|
|
36
39
|
error_recovery_policy: ErrorRecoveryPolicy to pass down to dependencies.
|
|
@@ -76,12 +79,15 @@ def create_queue_worker(
|
|
|
76
79
|
rail_lights_handler = RailLightsHandler(
|
|
77
80
|
hardware_api=hardware_api,
|
|
78
81
|
)
|
|
79
|
-
|
|
82
|
+
task_handler = TaskHandler(
|
|
83
|
+
state_store=state_store, action_dispatcher=action_dispatcher
|
|
84
|
+
)
|
|
80
85
|
status_bar_handler = StatusBarHandler(hardware_api=hardware_api)
|
|
81
86
|
|
|
82
87
|
command_executor = CommandExecutor(
|
|
83
88
|
hardware_api=hardware_api,
|
|
84
89
|
file_provider=file_provider,
|
|
90
|
+
camera_provider=camera_provider,
|
|
85
91
|
state_store=state_store,
|
|
86
92
|
action_dispatcher=action_dispatcher,
|
|
87
93
|
equipment=equipment_handler,
|
|
@@ -93,6 +99,7 @@ def create_queue_worker(
|
|
|
93
99
|
run_control=run_control_handler,
|
|
94
100
|
rail_lights=rail_lights_handler,
|
|
95
101
|
status_bar=status_bar_handler,
|
|
102
|
+
task_handler=task_handler,
|
|
96
103
|
)
|
|
97
104
|
|
|
98
105
|
return QueueWorker(
|
|
@@ -31,6 +31,8 @@ from ..types import (
|
|
|
31
31
|
OnLabwareLocation,
|
|
32
32
|
LabwareLocation,
|
|
33
33
|
OnDeckLabwareLocation,
|
|
34
|
+
GripperMoveType,
|
|
35
|
+
AccessibleByGripperLocation,
|
|
34
36
|
)
|
|
35
37
|
|
|
36
38
|
if TYPE_CHECKING:
|
|
@@ -94,7 +96,7 @@ class LabwareMovementHandler:
|
|
|
94
96
|
*,
|
|
95
97
|
labware_id: str,
|
|
96
98
|
current_location: OnDeckLabwareLocation,
|
|
97
|
-
new_location:
|
|
99
|
+
new_location: AccessibleByGripperLocation,
|
|
98
100
|
user_pick_up_offset: Point,
|
|
99
101
|
user_drop_offset: Point,
|
|
100
102
|
post_drop_slide_offset: Optional[Point],
|
|
@@ -107,7 +109,7 @@ class LabwareMovementHandler:
|
|
|
107
109
|
*,
|
|
108
110
|
labware_definition: LabwareDefinition,
|
|
109
111
|
current_location: OnDeckLabwareLocation,
|
|
110
|
-
new_location:
|
|
112
|
+
new_location: AccessibleByGripperLocation,
|
|
111
113
|
user_pick_up_offset: Point,
|
|
112
114
|
user_drop_offset: Point,
|
|
113
115
|
post_drop_slide_offset: Optional[Point],
|
|
@@ -121,7 +123,7 @@ class LabwareMovementHandler:
|
|
|
121
123
|
labware_id: str | None = None,
|
|
122
124
|
labware_definition: LabwareDefinition | None = None,
|
|
123
125
|
current_location: OnDeckLabwareLocation,
|
|
124
|
-
new_location:
|
|
126
|
+
new_location: AccessibleByGripperLocation,
|
|
125
127
|
user_pick_up_offset: Point,
|
|
126
128
|
user_drop_offset: Point,
|
|
127
129
|
post_drop_slide_offset: Optional[Point],
|
|
@@ -141,10 +143,16 @@ class LabwareMovementHandler:
|
|
|
141
143
|
labware_definition = self._state_store.labware.get_definition(labware_id)
|
|
142
144
|
|
|
143
145
|
from_labware_center = self._state_store.geometry.get_labware_grip_point(
|
|
144
|
-
labware_definition=labware_definition,
|
|
146
|
+
labware_definition=labware_definition,
|
|
147
|
+
location=current_location,
|
|
148
|
+
move_type=GripperMoveType.PICK_UP_LABWARE,
|
|
149
|
+
user_additional_offset=user_pick_up_offset,
|
|
145
150
|
)
|
|
146
151
|
to_labware_center = self._state_store.geometry.get_labware_grip_point(
|
|
147
|
-
labware_definition=labware_definition,
|
|
152
|
+
labware_definition=labware_definition,
|
|
153
|
+
location=new_location,
|
|
154
|
+
move_type=GripperMoveType.DROP_LABWARE,
|
|
155
|
+
user_additional_offset=user_drop_offset,
|
|
148
156
|
)
|
|
149
157
|
|
|
150
158
|
if use_virtual_gripper:
|
|
@@ -193,20 +201,10 @@ class LabwareMovementHandler:
|
|
|
193
201
|
async with self._thermocycler_plate_lifter.lift_plate_for_labware_movement(
|
|
194
202
|
labware_location=current_location
|
|
195
203
|
):
|
|
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
204
|
movement_waypoints = get_gripper_labware_movement_waypoints(
|
|
206
205
|
from_labware_center=from_labware_center,
|
|
207
206
|
to_labware_center=to_labware_center,
|
|
208
207
|
gripper_home_z=gripper_homed_position.z,
|
|
209
|
-
offset_data=final_offsets,
|
|
210
208
|
post_drop_slide_offset=post_drop_slide_offset,
|
|
211
209
|
gripper_home_z_offset=gripper_z_offset,
|
|
212
210
|
)
|
|
@@ -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(
|
|
@@ -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
|
|
@@ -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
|
+
)
|