opentrons 8.7.0a7__py3-none-any.whl → 8.8.0a7__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (109) 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 +8 -5
  5. opentrons/drivers/flex_stacker/driver.py +6 -1
  6. opentrons/drivers/vacuum_module/__init__.py +5 -0
  7. opentrons/drivers/vacuum_module/abstract.py +93 -0
  8. opentrons/drivers/vacuum_module/driver.py +208 -0
  9. opentrons/drivers/vacuum_module/errors.py +39 -0
  10. opentrons/drivers/vacuum_module/simulator.py +85 -0
  11. opentrons/drivers/vacuum_module/types.py +79 -0
  12. opentrons/execute.py +3 -0
  13. opentrons/hardware_control/backends/flex_protocol.py +2 -0
  14. opentrons/hardware_control/backends/ot3controller.py +35 -2
  15. opentrons/hardware_control/backends/ot3simulator.py +2 -0
  16. opentrons/hardware_control/backends/ot3utils.py +37 -0
  17. opentrons/hardware_control/module_control.py +23 -2
  18. opentrons/hardware_control/modules/mod_abc.py +1 -1
  19. opentrons/hardware_control/modules/types.py +1 -1
  20. opentrons/hardware_control/motion_utilities.py +6 -6
  21. opentrons/hardware_control/ot3api.py +62 -13
  22. opentrons/hardware_control/protocols/gripper_controller.py +1 -0
  23. opentrons/hardware_control/protocols/liquid_handler.py +6 -2
  24. opentrons/hardware_control/types.py +12 -0
  25. opentrons/legacy_commands/commands.py +58 -5
  26. opentrons/legacy_commands/module_commands.py +29 -0
  27. opentrons/legacy_commands/protocol_commands.py +33 -1
  28. opentrons/legacy_commands/types.py +75 -1
  29. opentrons/protocol_api/_transfer_liquid_validation.py +17 -2
  30. opentrons/protocol_api/_types.py +2 -0
  31. opentrons/protocol_api/core/engine/_default_labware_versions.py +1 -0
  32. opentrons/protocol_api/core/engine/deck_conflict.py +3 -1
  33. opentrons/protocol_api/core/engine/instrument.py +109 -26
  34. opentrons/protocol_api/core/engine/module_core.py +27 -3
  35. opentrons/protocol_api/core/engine/protocol.py +33 -1
  36. opentrons/protocol_api/core/engine/stringify.py +2 -0
  37. opentrons/protocol_api/core/instrument.py +19 -2
  38. opentrons/protocol_api/core/legacy/legacy_instrument_core.py +19 -2
  39. opentrons/protocol_api/core/legacy/legacy_module_core.py +15 -4
  40. opentrons/protocol_api/core/legacy/legacy_protocol_core.py +12 -0
  41. opentrons/protocol_api/core/legacy_simulator/legacy_instrument_core.py +19 -2
  42. opentrons/protocol_api/core/module.py +25 -2
  43. opentrons/protocol_api/core/protocol.py +12 -0
  44. opentrons/protocol_api/instrument_context.py +388 -2
  45. opentrons/protocol_api/labware.py +5 -2
  46. opentrons/protocol_api/module_contexts.py +133 -30
  47. opentrons/protocol_api/protocol_context.py +61 -17
  48. opentrons/protocol_api/robot_context.py +3 -4
  49. opentrons/protocol_api/validation.py +43 -2
  50. opentrons/protocol_engine/__init__.py +4 -0
  51. opentrons/protocol_engine/actions/__init__.py +2 -0
  52. opentrons/protocol_engine/actions/actions.py +9 -0
  53. opentrons/protocol_engine/commands/__init__.py +14 -0
  54. opentrons/protocol_engine/commands/absorbance_reader/read.py +22 -23
  55. opentrons/protocol_engine/commands/aspirate_while_tracking.py +52 -19
  56. opentrons/protocol_engine/commands/capture_image.py +302 -0
  57. opentrons/protocol_engine/commands/command.py +1 -0
  58. opentrons/protocol_engine/commands/command_unions.py +13 -0
  59. opentrons/protocol_engine/commands/dispense_while_tracking.py +56 -19
  60. opentrons/protocol_engine/commands/flex_stacker/common.py +35 -0
  61. opentrons/protocol_engine/commands/flex_stacker/set_stored_labware.py +7 -0
  62. opentrons/protocol_engine/commands/heater_shaker/set_shake_speed.py +1 -1
  63. opentrons/protocol_engine/commands/heater_shaker/set_target_temperature.py +1 -1
  64. opentrons/protocol_engine/commands/move_labware.py +3 -4
  65. opentrons/protocol_engine/commands/move_to_addressable_area_for_drop_tip.py +1 -1
  66. opentrons/protocol_engine/commands/movement_common.py +29 -2
  67. opentrons/protocol_engine/commands/pipetting_common.py +48 -3
  68. opentrons/protocol_engine/commands/thermocycler/set_target_block_temperature.py +12 -9
  69. opentrons/protocol_engine/commands/thermocycler/set_target_lid_temperature.py +17 -12
  70. opentrons/protocol_engine/commands/thermocycler/start_run_extended_profile.py +1 -1
  71. opentrons/protocol_engine/create_protocol_engine.py +12 -0
  72. opentrons/protocol_engine/engine_support.py +3 -0
  73. opentrons/protocol_engine/errors/__init__.py +8 -0
  74. opentrons/protocol_engine/errors/exceptions.py +64 -0
  75. opentrons/protocol_engine/execution/__init__.py +2 -0
  76. opentrons/protocol_engine/execution/command_executor.py +54 -1
  77. opentrons/protocol_engine/execution/create_queue_worker.py +4 -1
  78. opentrons/protocol_engine/execution/labware_movement.py +13 -4
  79. opentrons/protocol_engine/execution/pipetting.py +19 -25
  80. opentrons/protocol_engine/protocol_engine.py +62 -2
  81. opentrons/protocol_engine/resources/__init__.py +2 -0
  82. opentrons/protocol_engine/resources/camera_provider.py +110 -0
  83. opentrons/protocol_engine/resources/file_provider.py +133 -58
  84. opentrons/protocol_engine/slot_standardization.py +2 -0
  85. opentrons/protocol_engine/state/camera.py +54 -0
  86. opentrons/protocol_engine/state/commands.py +24 -4
  87. opentrons/protocol_engine/state/geometry.py +68 -10
  88. opentrons/protocol_engine/state/labware.py +10 -6
  89. opentrons/protocol_engine/state/labware_origin_math/stackup_origin_to_labware_origin.py +6 -1
  90. opentrons/protocol_engine/state/modules.py +9 -0
  91. opentrons/protocol_engine/state/preconditions.py +59 -0
  92. opentrons/protocol_engine/state/state.py +30 -0
  93. opentrons/protocol_engine/state/state_summary.py +2 -0
  94. opentrons/protocol_engine/state/update_types.py +10 -0
  95. opentrons/protocol_engine/types/__init__.py +14 -1
  96. opentrons/protocol_engine/types/command_preconditions.py +18 -0
  97. opentrons/protocol_engine/types/location.py +26 -2
  98. opentrons/protocol_engine/types/module.py +1 -1
  99. opentrons/protocol_runner/protocol_runner.py +14 -1
  100. opentrons/protocol_runner/run_orchestrator.py +31 -0
  101. opentrons/protocols/advanced_control/transfers/transfer_liquid_utils.py +2 -2
  102. opentrons/simulate.py +3 -0
  103. opentrons/system/camera.py +333 -3
  104. opentrons/system/ffmpeg.py +110 -0
  105. {opentrons-8.7.0a7.dist-info → opentrons-8.8.0a7.dist-info}/METADATA +4 -4
  106. {opentrons-8.7.0a7.dist-info → opentrons-8.8.0a7.dist-info}/RECORD +109 -97
  107. {opentrons-8.7.0a7.dist-info → opentrons-8.8.0a7.dist-info}/WHEEL +0 -0
  108. {opentrons-8.7.0a7.dist-info → opentrons-8.8.0a7.dist-info}/entry_points.txt +0 -0
  109. {opentrons-8.7.0a7.dist-info → opentrons-8.8.0a7.dist-info}/licenses/LICENSE +0 -0
@@ -82,7 +82,7 @@ class SetTargetTemperatureImpl(
82
82
  async def start_set_temperature(task_handler: TaskHandler) -> None:
83
83
  if hs_hardware_module is not None:
84
84
  async with task_handler.synchronize_cancel_previous(
85
- hs_module_substate.module_id
85
+ hs_module_substate.module_id + "-temp"
86
86
  ):
87
87
  await hs_hardware_module.start_set_temperature(validated_temp)
88
88
  await hs_hardware_module.await_temperature(validated_temp)
@@ -38,6 +38,7 @@ from ..types import (
38
38
  LabwareLocationSequence,
39
39
  NotOnDeckLocationSequenceComponent,
40
40
  OFF_DECK_LOCATION,
41
+ WASTE_CHUTE_LOCATION,
41
42
  )
42
43
  from ..errors import (
43
44
  LabwareMovementNotAllowedError,
@@ -228,9 +229,10 @@ class MoveLabwareImplementation(AbstractCommandImpl[MoveLabwareParams, _ExecuteR
228
229
  )
229
230
  eventual_destination_location_sequence = [
230
231
  NotOnDeckLocationSequenceComponent(
231
- logicalLocationName=OFF_DECK_LOCATION
232
+ logicalLocationName=WASTE_CHUTE_LOCATION
232
233
  )
233
234
  ]
235
+
234
236
  elif fixture_validation.is_trash(area_name):
235
237
  # When dropping labware in the trash bins we want to ensure they are lids
236
238
  # and enforce a y-axis drop offset to ensure they fall within the trash bin
@@ -351,7 +353,6 @@ class MoveLabwareImplementation(AbstractCommandImpl[MoveLabwareParams, _ExecuteR
351
353
  validated_new_loc = self._state_view.geometry.ensure_valid_gripper_location(
352
354
  available_new_location,
353
355
  )
354
-
355
356
  user_pick_up_offset = (
356
357
  Point.from_xyz_attrs(params.pickUpOffset)
357
358
  if params.pickUpOffset is not None
@@ -464,7 +465,6 @@ class MoveLabwareImplementation(AbstractCommandImpl[MoveLabwareParams, _ExecuteR
464
465
  new_location=available_new_location,
465
466
  new_offset_id=new_offset_id,
466
467
  )
467
-
468
468
  if labware_validation.validate_definition_is_lid(
469
469
  definition=self._state_view.labware.get_definition(params.labwareId)
470
470
  ):
@@ -498,7 +498,6 @@ class MoveLabwareImplementation(AbstractCommandImpl[MoveLabwareParams, _ExecuteR
498
498
  parent_labware_ids=parent_updates,
499
499
  lid_ids=lid_updates,
500
500
  )
501
-
502
501
  return SuccessData(
503
502
  public=MoveLabwareResult(
504
503
  offsetId=new_offset_id,
@@ -85,7 +85,7 @@ class MoveToAddressableAreaForDropTipParams(PipetteIdMixin, MovementMixin):
85
85
  ignoreTipConfiguration: bool | SkipJsonSchema[None] = Field(
86
86
  True,
87
87
  description=(
88
- "Whether to utilize the critical point of the tip configuraiton when moving to an addressable area."
88
+ "Whether to utilize the critical point of the tip configuration when moving to an addressable area."
89
89
  " If True, this command will ignore the tip configuration and use the center of the entire instrument"
90
90
  " as the critical point for movement."
91
91
  " If False, this command will use the critical point provided by the current tip configuration."
@@ -48,8 +48,8 @@ class WellLocationMixin(BaseModel):
48
48
  )
49
49
 
50
50
 
51
- class LiquidHandlingWellLocationMixin(BaseModel):
52
- """Mixin for command requests that take a location that's somewhere in a well."""
51
+ class LiquidHandlingWellMixin(BaseModel):
52
+ """Base Mixin for command requests that take a well."""
53
53
 
54
54
  labwareId: str = Field(
55
55
  ...,
@@ -59,12 +59,39 @@ class LiquidHandlingWellLocationMixin(BaseModel):
59
59
  ...,
60
60
  description="Name of well to use in labware.",
61
61
  )
62
+
63
+
64
+ class LiquidHandlingWellLocationMixin(LiquidHandlingWellMixin):
65
+ """Mixin for command requests that take a location that's somewhere in a well."""
66
+
62
67
  wellLocation: LiquidHandlingWellLocation = Field(
63
68
  default_factory=LiquidHandlingWellLocation,
64
69
  description="Relative well location at which to perform the operation",
65
70
  )
66
71
 
67
72
 
73
+ class DynamicLiquidHandlingWellLocationMixin(LiquidHandlingWellMixin):
74
+ """Mixin for command requests that move between two locations in a well."""
75
+
76
+ trackFromLocation: LiquidHandlingWellLocation = Field(
77
+ default_factory=LiquidHandlingWellLocation,
78
+ description="Relative well location at which to start the operation",
79
+ )
80
+ trackToLocation: LiquidHandlingWellLocation = Field(
81
+ default_factory=LiquidHandlingWellLocation,
82
+ description="Relative well location at which to end the operation",
83
+ )
84
+ movement_delay: float | SkipJsonSchema[None] = Field(
85
+ None,
86
+ description=(
87
+ "Optional movement delay in seconds."
88
+ " When this argument is used, the x/y/z movement will wait movement_delay seconds"
89
+ " after the pipetting action starts before beginning the x/y/z move."
90
+ ),
91
+ json_schema_extra=_remove_default,
92
+ )
93
+
94
+
68
95
  class MovementMixin(BaseModel):
69
96
  """Mixin for command requests that move a pipette."""
70
97
 
@@ -9,10 +9,15 @@ from pydantic import BaseModel, Field
9
9
  from opentrons_shared_data.errors import ErrorCodes
10
10
  from opentrons.protocol_engine.errors.error_occurrence import ErrorOccurrence
11
11
  from opentrons.protocol_engine.types import AspiratedFluid, FluidKind
12
- from opentrons_shared_data.errors.exceptions import PipetteOverpressureError
12
+ from opentrons_shared_data.errors.exceptions import (
13
+ PipetteOverpressureError,
14
+ StallOrCollisionDetectedError,
15
+ )
13
16
  from .command import DefinedErrorData, SuccessData
14
17
  from opentrons.protocol_engine.state.update_types import StateUpdate
18
+ from opentrons.types import Point
15
19
 
20
+ from .movement_common import StallOrCollisionError
16
21
 
17
22
  if TYPE_CHECKING:
18
23
  from ..execution.pipetting import PipettingHandler
@@ -241,11 +246,15 @@ async def aspirate_while_tracking(
241
246
  well_name: str,
242
247
  volume: float,
243
248
  flow_rate: float,
249
+ end_point: Point,
244
250
  location_if_error: ErrorLocationInfo,
245
251
  command_note_adder: CommandNoteAdder,
246
252
  pipetting: PipettingHandler,
247
253
  model_utils: ModelUtils,
248
- ) -> SuccessData[BaseLiquidHandlingResult] | DefinedErrorData[OverpressureError]:
254
+ movement_delay: Optional[float] = None,
255
+ ) -> SuccessData[BaseLiquidHandlingResult] | DefinedErrorData[
256
+ OverpressureError
257
+ ] | DefinedErrorData[StallOrCollisionError]:
249
258
  """Execute an aspirate while tracking microoperation."""
250
259
  try:
251
260
  volume_aspirated = await pipetting.aspirate_while_tracking(
@@ -254,6 +263,7 @@ async def aspirate_while_tracking(
254
263
  well_name=well_name,
255
264
  volume=volume,
256
265
  flow_rate=flow_rate,
266
+ end_point=end_point,
257
267
  command_note_adder=command_note_adder,
258
268
  )
259
269
  except PipetteOverpressureError as e:
@@ -272,6 +282,21 @@ async def aspirate_while_tracking(
272
282
  ),
273
283
  state_update=StateUpdate().set_fluid_unknown(pipette_id=pipette_id),
274
284
  )
285
+ except StallOrCollisionDetectedError as e:
286
+ return DefinedErrorData(
287
+ public=StallOrCollisionError(
288
+ id=model_utils.generate_id(),
289
+ createdAt=model_utils.get_timestamp(),
290
+ wrappedErrors=[
291
+ ErrorOccurrence.from_failed(
292
+ id=model_utils.generate_id(),
293
+ createdAt=model_utils.get_timestamp(),
294
+ error=e,
295
+ )
296
+ ],
297
+ ),
298
+ state_update=StateUpdate().clear_all_pipette_locations(),
299
+ )
275
300
  else:
276
301
  return SuccessData(
277
302
  public=BaseLiquidHandlingResult(
@@ -290,11 +315,15 @@ async def dispense_while_tracking(
290
315
  well_name: str,
291
316
  volume: float,
292
317
  flow_rate: float,
318
+ end_point: Point,
293
319
  push_out: float | None,
294
320
  location_if_error: ErrorLocationInfo,
295
321
  pipetting: PipettingHandler,
296
322
  model_utils: ModelUtils,
297
- ) -> SuccessData[BaseLiquidHandlingResult] | DefinedErrorData[OverpressureError]:
323
+ movement_delay: Optional[float] = None,
324
+ ) -> SuccessData[BaseLiquidHandlingResult] | DefinedErrorData[
325
+ OverpressureError
326
+ ] | DefinedErrorData[StallOrCollisionError]:
298
327
  """Execute an dispense while tracking microoperation."""
299
328
  # The current volume won't be none since it passed validation
300
329
  current_volume = (
@@ -309,6 +338,7 @@ async def dispense_while_tracking(
309
338
  well_name=well_name,
310
339
  volume=volume,
311
340
  flow_rate=flow_rate,
341
+ end_point=end_point,
312
342
  push_out=push_out,
313
343
  is_full_dispense=is_full_dispense,
314
344
  )
@@ -332,6 +362,21 @@ async def dispense_while_tracking(
332
362
  pipette_id=pipette_id, ready_to_aspirate=False
333
363
  ),
334
364
  )
365
+ except StallOrCollisionDetectedError as e:
366
+ return DefinedErrorData(
367
+ public=StallOrCollisionError(
368
+ id=model_utils.generate_id(),
369
+ createdAt=model_utils.get_timestamp(),
370
+ wrappedErrors=[
371
+ ErrorOccurrence.from_failed(
372
+ id=model_utils.generate_id(),
373
+ createdAt=model_utils.get_timestamp(),
374
+ error=e,
375
+ )
376
+ ],
377
+ ),
378
+ state_update=StateUpdate().clear_all_pipette_locations(),
379
+ )
335
380
  else:
336
381
  return SuccessData(
337
382
  public=BaseLiquidHandlingResult(
@@ -1,4 +1,4 @@
1
- """Command models to start heating a Thermocycler's block."""
1
+ """Command models for heating a Thermocycler's block."""
2
2
  from __future__ import annotations
3
3
  from typing import Optional, TYPE_CHECKING, Any
4
4
  from typing_extensions import Literal, Type
@@ -61,7 +61,7 @@ class SetTargetBlockTemperatureResult(BaseModel):
61
61
  )
62
62
  taskId: str | SkipJsonSchema[None] = Field(
63
63
  None,
64
- description="The task id for the setTargetBlockTemperature",
64
+ description="Id for the background task that manages the temperature.",
65
65
  json_schema_extra=_remove_default,
66
66
  )
67
67
 
@@ -122,13 +122,16 @@ class SetTargetBlockTemperatureImpl(
122
122
 
123
123
  async def set_target_block_temperature(task_handler: TaskHandler) -> None:
124
124
  if thermocycler_hardware is not None:
125
- await thermocycler_hardware.set_target_block_temperature(
126
- target_temperature,
127
- volume=target_volume,
128
- hold_time_seconds=hold_time,
129
- ramp_rate=target_ramp_rate,
130
- )
131
- await thermocycler_hardware.wait_for_block_target()
125
+ async with task_handler.synchronize_cancel_latest(
126
+ thermocycler_state.module_id + "-block"
127
+ ):
128
+ await thermocycler_hardware.set_target_block_temperature(
129
+ celsius=target_temperature,
130
+ volume=target_volume,
131
+ ramp_rate=target_ramp_rate,
132
+ hold_time_seconds=hold_time,
133
+ )
134
+ await thermocycler_hardware.wait_for_block_target()
132
135
 
133
136
  task = await self._task_handler.create_task(
134
137
  task_function=set_target_block_temperature, id=params.taskId
@@ -1,4 +1,4 @@
1
- """Command models to start heating a Thermocycler's lid."""
1
+ """Command models for heating a Thermocycler's lid."""
2
2
  from __future__ import annotations
3
3
  from typing import Optional, TYPE_CHECKING, Any
4
4
  from typing_extensions import Literal, Type
@@ -13,7 +13,6 @@ if TYPE_CHECKING:
13
13
  from opentrons.protocol_engine.state.state import StateView
14
14
  from opentrons.protocol_engine.execution import EquipmentHandler, TaskHandler
15
15
 
16
-
17
16
  SetTargetLidTemperatureCommandType = Literal["thermocycler/setTargetLidTemperature"]
18
17
 
19
18
 
@@ -22,7 +21,7 @@ def _remove_default(s: dict[str, Any]) -> None:
22
21
 
23
22
 
24
23
  class SetTargetLidTemperatureParams(BaseModel):
25
- """Input parameters to set a Thermocycler's target lid temperature."""
24
+ """Input parameters to to set a Thermocycler's target lid temperature."""
26
25
 
27
26
  moduleId: str = Field(..., description="Unique ID of the Thermocycler Module.")
28
27
  celsius: float = Field(..., description="Target temperature in °C.")
@@ -49,10 +48,11 @@ class SetTargetLidTemperatureResult(BaseModel):
49
48
 
50
49
  class SetTargetLidTemperatureImpl(
51
50
  AbstractCommandImpl[
52
- SetTargetLidTemperatureParams, SuccessData[SetTargetLidTemperatureResult]
51
+ SetTargetLidTemperatureParams,
52
+ SuccessData[SetTargetLidTemperatureResult],
53
53
  ]
54
54
  ):
55
- """Execution implementation of a Thermocycler's set lid temperature command."""
55
+ """Execution implementation of a Thermocycler's to set lid temperature command."""
56
56
 
57
57
  def __init__(
58
58
  self,
@@ -69,7 +69,7 @@ class SetTargetLidTemperatureImpl(
69
69
  self,
70
70
  params: SetTargetLidTemperatureParams,
71
71
  ) -> SuccessData[SetTargetLidTemperatureResult]:
72
- """Set a Thermocycler's target lid temperature."""
72
+ """To set a Thermocycler's target lid temperature."""
73
73
  thermocycler_state = self._state_view.modules.get_thermocycler_module_substate(
74
74
  params.moduleId
75
75
  )
@@ -82,10 +82,13 @@ class SetTargetLidTemperatureImpl(
82
82
 
83
83
  async def set_target_lid_temperature(task_handler: TaskHandler) -> None:
84
84
  if thermocycler_hardware is not None:
85
- await thermocycler_hardware.set_target_lid_temperature(
86
- target_temperature
87
- )
88
- await thermocycler_hardware.wait_for_lid_target()
85
+ async with task_handler.synchronize_cancel_latest(
86
+ thermocycler_state.module_id + "-lid"
87
+ ):
88
+ await thermocycler_hardware.set_target_lid_temperature(
89
+ target_temperature
90
+ )
91
+ await thermocycler_hardware.wait_for_lid_target()
89
92
 
90
93
  task = await self._task_handler.create_task(
91
94
  task_function=set_target_lid_temperature, id=params.taskId
@@ -99,10 +102,12 @@ class SetTargetLidTemperatureImpl(
99
102
 
100
103
  class SetTargetLidTemperature(
101
104
  BaseCommand[
102
- SetTargetLidTemperatureParams, SetTargetLidTemperatureResult, ErrorOccurrence
105
+ SetTargetLidTemperatureParams,
106
+ SetTargetLidTemperatureResult,
107
+ ErrorOccurrence,
103
108
  ]
104
109
  ):
105
- """A command to set a Thermocycler's target lid temperature."""
110
+ """A command to to set a Thermocycler's target lid temperature."""
106
111
 
107
112
  commandType: SetTargetLidTemperatureCommandType = (
108
113
  "thermocycler/setTargetLidTemperature"
@@ -150,7 +150,7 @@ class StartRunExtendedProfileImpl(
150
150
  async def start_run_profile(task_handler: TaskHandler) -> None:
151
151
  if thermocycler_hardware is not None:
152
152
  async with task_handler.synchronize_cancel_latest(
153
- thermocycler_state.module_id
153
+ thermocycler_state.module_id + "-block"
154
154
  ):
155
155
  await thermocycler_hardware.execute_profile(
156
156
  profile=profile, volume=target_volume
@@ -11,6 +11,7 @@ from opentrons.protocol_engine.execution.error_recovery_hardware_state_synchroni
11
11
  from opentrons.protocol_engine.resources.labware_data_provider import (
12
12
  LabwareDataProvider,
13
13
  )
14
+ from opentrons.protocol_engine.resources.camera_provider import CameraProvider
14
15
  from opentrons.util.async_helpers import async_context_manager_in_thread
15
16
 
16
17
  from opentrons_shared_data.robot import load as load_robot
@@ -38,6 +39,7 @@ async def create_protocol_engine(
38
39
  load_fixed_trash: bool = False,
39
40
  deck_configuration: typing.Optional[DeckConfigurationType] = None,
40
41
  file_provider: typing.Optional[FileProvider] = None,
42
+ camera_provider: typing.Optional[CameraProvider] = None,
41
43
  notify_publishers: typing.Optional[typing.Callable[[], None]] = None,
42
44
  ) -> ProtocolEngine:
43
45
  """Create a ProtocolEngine instance.
@@ -50,6 +52,7 @@ async def create_protocol_engine(
50
52
  load_fixed_trash: Automatically load fixed trash labware in engine.
51
53
  deck_configuration: The initial deck configuration the engine will be instantiated with.
52
54
  file_provider: Provides access to robot server file writing procedures for protocol output.
55
+ camera_provider: Provides access to camera interface with image capture and callbacks.
53
56
  notify_publishers: Notifies robot server publishers of internal state change.
54
57
  """
55
58
  deck_data = DeckDataProvider(config.deck_type)
@@ -83,6 +86,7 @@ async def create_protocol_engine(
83
86
  door_watcher = DoorWatcher(state_store, hardware_api, action_dispatcher)
84
87
  module_data_provider = ModuleDataProvider()
85
88
  file_provider = file_provider or FileProvider()
89
+ camera_provider = camera_provider or CameraProvider()
86
90
 
87
91
  pe = ProtocolEngine(
88
92
  hardware_api=hardware_api,
@@ -94,6 +98,7 @@ async def create_protocol_engine(
94
98
  door_watcher=door_watcher,
95
99
  module_data_provider=module_data_provider,
96
100
  file_provider=file_provider,
101
+ camera_provider=camera_provider,
97
102
  )
98
103
 
99
104
  # todo(mm, 2024-11-08): This is a quick hack to support the absorbance reader, which
@@ -121,6 +126,7 @@ def create_protocol_engine_in_thread(
121
126
  drop_tips_after_run: bool,
122
127
  post_run_hardware_state: PostRunHardwareState,
123
128
  load_fixed_trash: bool = False,
129
+ camera_provider: typing.Optional[CameraProvider] = None,
124
130
  ) -> typing.Generator[
125
131
  typing.Tuple[ProtocolEngine, asyncio.AbstractEventLoop], None, None
126
132
  ]:
@@ -151,6 +157,7 @@ def create_protocol_engine_in_thread(
151
157
  drop_tips_after_run,
152
158
  post_run_hardware_state,
153
159
  load_fixed_trash,
160
+ camera_provider,
154
161
  )
155
162
  ) as (
156
163
  protocol_engine,
@@ -169,6 +176,7 @@ async def _protocol_engine(
169
176
  drop_tips_after_run: bool,
170
177
  post_run_hardware_state: PostRunHardwareState,
171
178
  load_fixed_trash: bool = False,
179
+ camera_provider: typing.Optional[CameraProvider] = None,
172
180
  ) -> typing.AsyncGenerator[ProtocolEngine, None]:
173
181
  protocol_engine = await create_protocol_engine(
174
182
  hardware_api=hardware_api,
@@ -179,9 +187,13 @@ async def _protocol_engine(
179
187
  )
180
188
 
181
189
  # TODO(tz, 6-20-2024): This feels like a hack, we should probably return the orchestrator instead of pe.
190
+
182
191
  orchestrator = create_run_orchestrator(
183
192
  hardware_api=hardware_api,
184
193
  protocol_engine=protocol_engine,
194
+ camera_provider=camera_provider
195
+ if camera_provider is not None
196
+ else CameraProvider(),
185
197
  )
186
198
  try:
187
199
  orchestrator.play(deck_configuration)
@@ -3,16 +3,19 @@ from . import ProtocolEngine
3
3
  from ..hardware_control import HardwareControlAPI
4
4
 
5
5
  from opentrons.protocol_runner import protocol_runner, RunOrchestrator
6
+ from opentrons.protocol_engine.resources.camera_provider import CameraProvider
6
7
 
7
8
 
8
9
  def create_run_orchestrator(
9
10
  hardware_api: HardwareControlAPI,
10
11
  protocol_engine: ProtocolEngine,
12
+ camera_provider: CameraProvider,
11
13
  ) -> RunOrchestrator:
12
14
  """Create a RunOrchestrator instance."""
13
15
  return RunOrchestrator(
14
16
  protocol_engine=protocol_engine,
15
17
  hardware_api=hardware_api,
18
+ camera_provider=camera_provider,
16
19
  setup_runner=protocol_runner.LiveRunner(
17
20
  protocol_engine=protocol_engine,
18
21
  hardware_api=hardware_api,
@@ -84,6 +84,7 @@ from .exceptions import (
84
84
  OperationLocationNotInWellError,
85
85
  InvalidDispenseVolumeError,
86
86
  StorageLimitReachedError,
87
+ FileNameInvalidError,
87
88
  InvalidLiquidError,
88
89
  LiquidClassDoesNotExistError,
89
90
  LiquidClassRedefinitionError,
@@ -92,6 +93,9 @@ from .exceptions import (
92
93
  FlexStackerNotLogicallyEmptyError,
93
94
  InvalidLabwarePositionError,
94
95
  InvalidModuleOrientation,
96
+ CameraCaptureError,
97
+ CameraDisabledError,
98
+ CameraSettingsInvalidError,
95
99
  )
96
100
 
97
101
  from .error_occurrence import ErrorOccurrence, ProtocolCommandFailedError
@@ -190,6 +194,10 @@ __all__ = [
190
194
  "OperationLocationNotInWellError",
191
195
  "InvalidDispenseVolumeError",
192
196
  "StorageLimitReachedError",
197
+ "FileNameInvalidError",
193
198
  "LiquidClassDoesNotExistError",
194
199
  "LiquidClassRedefinitionError",
200
+ "CameraCaptureError",
201
+ "CameraDisabledError",
202
+ "CameraSettingsInvalidError",
195
203
  ]
@@ -1291,6 +1291,19 @@ class StorageLimitReachedError(ProtocolEngineError):
1291
1291
  super().__init__(ErrorCodes.GENERAL_ERROR, message, detail, wrapping)
1292
1292
 
1293
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
+
1294
1307
  class LiquidClassDoesNotExistError(ProtocolEngineError):
1295
1308
  """Raised when referencing a liquid class that has not been loaded."""
1296
1309
 
@@ -1339,6 +1352,18 @@ class FlexStackerLabwarePoolNotYetDefinedError(ProtocolEngineError):
1339
1352
  super().__init__(ErrorCodes.GENERAL_ERROR, message, details, wrapping)
1340
1353
 
1341
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
+
1342
1367
  class InvalidLabwarePositionError(ProtocolEngineError):
1343
1368
  """Raised when a labware position is internally invalid."""
1344
1369
 
@@ -1361,3 +1386,42 @@ class InvalidModuleOrientation(ProtocolEngineError):
1361
1386
  wrapping: Optional[Sequence[EnumeratedError]] = None,
1362
1387
  ) -> None:
1363
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)
@@ -23,6 +23,7 @@ from .door_watcher import DoorWatcher
23
23
  from .status_bar import StatusBarHandler
24
24
  from .task_handler import TaskHandler
25
25
  from ..resources.file_provider import FileProvider
26
+ from ..resources.camera_provider import CameraProvider
26
27
 
27
28
  # .thermocycler_movement_flagger omitted from package's public interface.
28
29
 
@@ -49,4 +50,5 @@ __all__ = [
49
50
  "StatusBarHandler",
50
51
  "TaskHandler",
51
52
  "FileProvider",
53
+ "CameraProvider",
52
54
  ]