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.
Files changed (189) hide show
  1. opentrons/_version.py +2 -2
  2. opentrons/cli/analyze.py +4 -1
  3. opentrons/config/__init__.py +7 -0
  4. opentrons/drivers/asyncio/communication/serial_connection.py +126 -49
  5. opentrons/drivers/heater_shaker/abstract.py +5 -0
  6. opentrons/drivers/heater_shaker/driver.py +10 -0
  7. opentrons/drivers/heater_shaker/simulator.py +4 -0
  8. opentrons/drivers/thermocycler/abstract.py +6 -0
  9. opentrons/drivers/thermocycler/driver.py +61 -10
  10. opentrons/drivers/thermocycler/simulator.py +6 -0
  11. opentrons/drivers/vacuum_module/__init__.py +5 -0
  12. opentrons/drivers/vacuum_module/abstract.py +93 -0
  13. opentrons/drivers/vacuum_module/driver.py +208 -0
  14. opentrons/drivers/vacuum_module/errors.py +39 -0
  15. opentrons/drivers/vacuum_module/simulator.py +85 -0
  16. opentrons/drivers/vacuum_module/types.py +79 -0
  17. opentrons/execute.py +3 -0
  18. opentrons/hardware_control/api.py +24 -5
  19. opentrons/hardware_control/backends/controller.py +8 -2
  20. opentrons/hardware_control/backends/flex_protocol.py +1 -0
  21. opentrons/hardware_control/backends/ot3controller.py +35 -2
  22. opentrons/hardware_control/backends/ot3simulator.py +3 -1
  23. opentrons/hardware_control/backends/ot3utils.py +37 -0
  24. opentrons/hardware_control/backends/simulator.py +2 -1
  25. opentrons/hardware_control/backends/subsystem_manager.py +5 -2
  26. opentrons/hardware_control/emulation/abstract_emulator.py +6 -4
  27. opentrons/hardware_control/emulation/connection_handler.py +8 -5
  28. opentrons/hardware_control/emulation/heater_shaker.py +12 -3
  29. opentrons/hardware_control/emulation/settings.py +1 -1
  30. opentrons/hardware_control/emulation/thermocycler.py +67 -15
  31. opentrons/hardware_control/module_control.py +105 -10
  32. opentrons/hardware_control/modules/__init__.py +3 -0
  33. opentrons/hardware_control/modules/absorbance_reader.py +11 -4
  34. opentrons/hardware_control/modules/flex_stacker.py +38 -9
  35. opentrons/hardware_control/modules/heater_shaker.py +42 -5
  36. opentrons/hardware_control/modules/magdeck.py +8 -4
  37. opentrons/hardware_control/modules/mod_abc.py +14 -6
  38. opentrons/hardware_control/modules/tempdeck.py +25 -5
  39. opentrons/hardware_control/modules/thermocycler.py +68 -11
  40. opentrons/hardware_control/modules/types.py +20 -1
  41. opentrons/hardware_control/modules/utils.py +11 -4
  42. opentrons/hardware_control/motion_utilities.py +6 -6
  43. opentrons/hardware_control/nozzle_manager.py +3 -0
  44. opentrons/hardware_control/ot3api.py +85 -17
  45. opentrons/hardware_control/poller.py +22 -8
  46. opentrons/hardware_control/protocols/liquid_handler.py +6 -2
  47. opentrons/hardware_control/scripts/update_module_fw.py +5 -0
  48. opentrons/hardware_control/types.py +43 -2
  49. opentrons/legacy_commands/commands.py +58 -5
  50. opentrons/legacy_commands/module_commands.py +52 -0
  51. opentrons/legacy_commands/protocol_commands.py +53 -1
  52. opentrons/legacy_commands/types.py +155 -1
  53. opentrons/motion_planning/deck_conflict.py +17 -12
  54. opentrons/motion_planning/waypoints.py +15 -29
  55. opentrons/protocol_api/__init__.py +5 -1
  56. opentrons/protocol_api/_transfer_liquid_validation.py +17 -2
  57. opentrons/protocol_api/_types.py +8 -1
  58. opentrons/protocol_api/core/common.py +3 -1
  59. opentrons/protocol_api/core/engine/_default_labware_versions.py +33 -11
  60. opentrons/protocol_api/core/engine/deck_conflict.py +3 -1
  61. opentrons/protocol_api/core/engine/instrument.py +109 -26
  62. opentrons/protocol_api/core/engine/labware.py +8 -1
  63. opentrons/protocol_api/core/engine/module_core.py +95 -4
  64. opentrons/protocol_api/core/engine/protocol.py +51 -2
  65. opentrons/protocol_api/core/engine/stringify.py +2 -0
  66. opentrons/protocol_api/core/engine/tasks.py +48 -0
  67. opentrons/protocol_api/core/engine/well.py +8 -0
  68. opentrons/protocol_api/core/instrument.py +19 -2
  69. opentrons/protocol_api/core/legacy/legacy_instrument_core.py +19 -2
  70. opentrons/protocol_api/core/legacy/legacy_module_core.py +33 -2
  71. opentrons/protocol_api/core/legacy/legacy_protocol_core.py +23 -1
  72. opentrons/protocol_api/core/legacy/legacy_well_core.py +4 -0
  73. opentrons/protocol_api/core/legacy/tasks.py +19 -0
  74. opentrons/protocol_api/core/legacy_simulator/legacy_instrument_core.py +19 -2
  75. opentrons/protocol_api/core/legacy_simulator/legacy_protocol_core.py +14 -2
  76. opentrons/protocol_api/core/legacy_simulator/tasks.py +19 -0
  77. opentrons/protocol_api/core/module.py +58 -2
  78. opentrons/protocol_api/core/protocol.py +23 -2
  79. opentrons/protocol_api/core/tasks.py +31 -0
  80. opentrons/protocol_api/core/well.py +4 -0
  81. opentrons/protocol_api/instrument_context.py +388 -2
  82. opentrons/protocol_api/labware.py +10 -2
  83. opentrons/protocol_api/module_contexts.py +170 -6
  84. opentrons/protocol_api/protocol_context.py +87 -21
  85. opentrons/protocol_api/robot_context.py +41 -25
  86. opentrons/protocol_api/tasks.py +48 -0
  87. opentrons/protocol_api/validation.py +49 -3
  88. opentrons/protocol_engine/__init__.py +4 -0
  89. opentrons/protocol_engine/actions/__init__.py +6 -2
  90. opentrons/protocol_engine/actions/actions.py +31 -9
  91. opentrons/protocol_engine/clients/sync_client.py +42 -7
  92. opentrons/protocol_engine/commands/__init__.py +56 -0
  93. opentrons/protocol_engine/commands/absorbance_reader/close_lid.py +2 -15
  94. opentrons/protocol_engine/commands/absorbance_reader/open_lid.py +2 -15
  95. opentrons/protocol_engine/commands/absorbance_reader/read.py +22 -23
  96. opentrons/protocol_engine/commands/aspirate.py +1 -0
  97. opentrons/protocol_engine/commands/aspirate_while_tracking.py +52 -19
  98. opentrons/protocol_engine/commands/capture_image.py +302 -0
  99. opentrons/protocol_engine/commands/command.py +2 -0
  100. opentrons/protocol_engine/commands/command_unions.py +62 -0
  101. opentrons/protocol_engine/commands/create_timer.py +83 -0
  102. opentrons/protocol_engine/commands/dispense.py +1 -0
  103. opentrons/protocol_engine/commands/dispense_while_tracking.py +56 -19
  104. opentrons/protocol_engine/commands/drop_tip.py +32 -8
  105. opentrons/protocol_engine/commands/flex_stacker/common.py +35 -0
  106. opentrons/protocol_engine/commands/flex_stacker/set_stored_labware.py +7 -0
  107. opentrons/protocol_engine/commands/heater_shaker/__init__.py +14 -0
  108. opentrons/protocol_engine/commands/heater_shaker/common.py +20 -0
  109. opentrons/protocol_engine/commands/heater_shaker/set_and_wait_for_shake_speed.py +5 -4
  110. opentrons/protocol_engine/commands/heater_shaker/set_shake_speed.py +136 -0
  111. opentrons/protocol_engine/commands/heater_shaker/set_target_temperature.py +31 -5
  112. opentrons/protocol_engine/commands/move_labware.py +3 -4
  113. opentrons/protocol_engine/commands/move_to_addressable_area_for_drop_tip.py +1 -1
  114. opentrons/protocol_engine/commands/movement_common.py +31 -2
  115. opentrons/protocol_engine/commands/pick_up_tip.py +21 -11
  116. opentrons/protocol_engine/commands/pipetting_common.py +48 -3
  117. opentrons/protocol_engine/commands/set_tip_state.py +97 -0
  118. opentrons/protocol_engine/commands/temperature_module/set_target_temperature.py +38 -7
  119. opentrons/protocol_engine/commands/thermocycler/__init__.py +16 -0
  120. opentrons/protocol_engine/commands/thermocycler/run_extended_profile.py +6 -0
  121. opentrons/protocol_engine/commands/thermocycler/run_profile.py +8 -0
  122. opentrons/protocol_engine/commands/thermocycler/set_target_block_temperature.py +44 -7
  123. opentrons/protocol_engine/commands/thermocycler/set_target_lid_temperature.py +43 -14
  124. opentrons/protocol_engine/commands/thermocycler/start_run_extended_profile.py +191 -0
  125. opentrons/protocol_engine/commands/touch_tip.py +1 -1
  126. opentrons/protocol_engine/commands/unsafe/unsafe_place_labware.py +6 -22
  127. opentrons/protocol_engine/commands/wait_for_tasks.py +98 -0
  128. opentrons/protocol_engine/create_protocol_engine.py +12 -0
  129. opentrons/protocol_engine/engine_support.py +3 -0
  130. opentrons/protocol_engine/errors/__init__.py +12 -0
  131. opentrons/protocol_engine/errors/exceptions.py +119 -0
  132. opentrons/protocol_engine/execution/__init__.py +4 -0
  133. opentrons/protocol_engine/execution/command_executor.py +62 -1
  134. opentrons/protocol_engine/execution/create_queue_worker.py +9 -2
  135. opentrons/protocol_engine/execution/labware_movement.py +13 -15
  136. opentrons/protocol_engine/execution/movement.py +2 -0
  137. opentrons/protocol_engine/execution/pipetting.py +19 -25
  138. opentrons/protocol_engine/execution/queue_worker.py +4 -0
  139. opentrons/protocol_engine/execution/run_control.py +8 -0
  140. opentrons/protocol_engine/execution/task_handler.py +157 -0
  141. opentrons/protocol_engine/protocol_engine.py +137 -36
  142. opentrons/protocol_engine/resources/__init__.py +4 -0
  143. opentrons/protocol_engine/resources/camera_provider.py +110 -0
  144. opentrons/protocol_engine/resources/concurrency_provider.py +27 -0
  145. opentrons/protocol_engine/resources/deck_configuration_provider.py +7 -0
  146. opentrons/protocol_engine/resources/file_provider.py +133 -58
  147. opentrons/protocol_engine/resources/labware_validation.py +10 -6
  148. opentrons/protocol_engine/slot_standardization.py +2 -0
  149. opentrons/protocol_engine/state/_well_math.py +60 -18
  150. opentrons/protocol_engine/state/addressable_areas.py +2 -0
  151. opentrons/protocol_engine/state/camera.py +54 -0
  152. opentrons/protocol_engine/state/commands.py +37 -14
  153. opentrons/protocol_engine/state/geometry.py +276 -379
  154. opentrons/protocol_engine/state/labware.py +62 -108
  155. opentrons/protocol_engine/state/labware_origin_math/errors.py +94 -0
  156. opentrons/protocol_engine/state/labware_origin_math/stackup_origin_to_labware_origin.py +1336 -0
  157. opentrons/protocol_engine/state/module_substates/thermocycler_module_substate.py +37 -0
  158. opentrons/protocol_engine/state/modules.py +30 -8
  159. opentrons/protocol_engine/state/motion.py +44 -0
  160. opentrons/protocol_engine/state/preconditions.py +59 -0
  161. opentrons/protocol_engine/state/state.py +44 -0
  162. opentrons/protocol_engine/state/state_summary.py +4 -0
  163. opentrons/protocol_engine/state/tasks.py +139 -0
  164. opentrons/protocol_engine/state/tips.py +177 -258
  165. opentrons/protocol_engine/state/update_types.py +26 -9
  166. opentrons/protocol_engine/types/__init__.py +23 -4
  167. opentrons/protocol_engine/types/command_preconditions.py +18 -0
  168. opentrons/protocol_engine/types/deck_configuration.py +5 -1
  169. opentrons/protocol_engine/types/instrument.py +8 -1
  170. opentrons/protocol_engine/types/labware.py +1 -13
  171. opentrons/protocol_engine/types/location.py +26 -2
  172. opentrons/protocol_engine/types/module.py +11 -1
  173. opentrons/protocol_engine/types/tasks.py +38 -0
  174. opentrons/protocol_engine/types/tip.py +9 -0
  175. opentrons/protocol_runner/create_simulating_orchestrator.py +29 -2
  176. opentrons/protocol_runner/protocol_runner.py +14 -1
  177. opentrons/protocol_runner/run_orchestrator.py +49 -2
  178. opentrons/protocols/advanced_control/transfers/transfer_liquid_utils.py +2 -2
  179. opentrons/protocols/api_support/definitions.py +1 -1
  180. opentrons/protocols/api_support/types.py +2 -1
  181. opentrons/simulate.py +51 -15
  182. opentrons/system/camera.py +334 -4
  183. opentrons/system/ffmpeg.py +110 -0
  184. {opentrons-8.7.0a9.dist-info → opentrons-8.8.0a7.dist-info}/METADATA +4 -4
  185. {opentrons-8.7.0a9.dist-info → opentrons-8.8.0a7.dist-info}/RECORD +188 -160
  186. opentrons/protocol_engine/state/_labware_origin_math.py +0 -636
  187. {opentrons-8.7.0a9.dist-info → opentrons-8.8.0a7.dist-info}/WHEEL +0 -0
  188. {opentrons-8.7.0a9.dist-info → opentrons-8.8.0a7.dist-info}/entry_points.txt +0 -0
  189. {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: OnDeckLabwareLocation,
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: OnDeckLabwareLocation,
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: OnDeckLabwareLocation,
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, location=current_location
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, location=new_location
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
- aspirate_z_distance = self._state_view.geometry.get_liquid_handling_z_change(
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
- z_distance=aspirate_z_distance,
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
- dispense_z_distance = self._state_view.geometry.get_liquid_handling_z_change(
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
- z_distance=dispense_z_distance,
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
+ )