opentrons 8.7.0a9__py3-none-any.whl → 8.8.0a8__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 (190) 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 +92 -17
  45. opentrons/hardware_control/poller.py +22 -8
  46. opentrons/hardware_control/protocols/liquid_handler.py +12 -4
  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/pipette_movement_conflict.py +4 -18
  65. opentrons/protocol_api/core/engine/protocol.py +51 -2
  66. opentrons/protocol_api/core/engine/stringify.py +2 -0
  67. opentrons/protocol_api/core/engine/tasks.py +48 -0
  68. opentrons/protocol_api/core/engine/well.py +8 -0
  69. opentrons/protocol_api/core/instrument.py +19 -2
  70. opentrons/protocol_api/core/legacy/legacy_instrument_core.py +19 -2
  71. opentrons/protocol_api/core/legacy/legacy_module_core.py +33 -2
  72. opentrons/protocol_api/core/legacy/legacy_protocol_core.py +23 -1
  73. opentrons/protocol_api/core/legacy/legacy_well_core.py +4 -0
  74. opentrons/protocol_api/core/legacy/tasks.py +19 -0
  75. opentrons/protocol_api/core/legacy_simulator/legacy_instrument_core.py +19 -2
  76. opentrons/protocol_api/core/legacy_simulator/legacy_protocol_core.py +14 -2
  77. opentrons/protocol_api/core/legacy_simulator/tasks.py +19 -0
  78. opentrons/protocol_api/core/module.py +58 -2
  79. opentrons/protocol_api/core/protocol.py +23 -2
  80. opentrons/protocol_api/core/tasks.py +31 -0
  81. opentrons/protocol_api/core/well.py +4 -0
  82. opentrons/protocol_api/instrument_context.py +388 -2
  83. opentrons/protocol_api/labware.py +10 -2
  84. opentrons/protocol_api/module_contexts.py +170 -6
  85. opentrons/protocol_api/protocol_context.py +87 -21
  86. opentrons/protocol_api/robot_context.py +41 -25
  87. opentrons/protocol_api/tasks.py +48 -0
  88. opentrons/protocol_api/validation.py +49 -3
  89. opentrons/protocol_engine/__init__.py +4 -0
  90. opentrons/protocol_engine/actions/__init__.py +6 -2
  91. opentrons/protocol_engine/actions/actions.py +31 -9
  92. opentrons/protocol_engine/clients/sync_client.py +42 -7
  93. opentrons/protocol_engine/commands/__init__.py +56 -0
  94. opentrons/protocol_engine/commands/absorbance_reader/close_lid.py +2 -15
  95. opentrons/protocol_engine/commands/absorbance_reader/open_lid.py +2 -15
  96. opentrons/protocol_engine/commands/absorbance_reader/read.py +22 -23
  97. opentrons/protocol_engine/commands/aspirate.py +1 -0
  98. opentrons/protocol_engine/commands/aspirate_while_tracking.py +52 -19
  99. opentrons/protocol_engine/commands/capture_image.py +302 -0
  100. opentrons/protocol_engine/commands/command.py +2 -0
  101. opentrons/protocol_engine/commands/command_unions.py +62 -0
  102. opentrons/protocol_engine/commands/create_timer.py +83 -0
  103. opentrons/protocol_engine/commands/dispense.py +1 -0
  104. opentrons/protocol_engine/commands/dispense_while_tracking.py +56 -19
  105. opentrons/protocol_engine/commands/drop_tip.py +32 -8
  106. opentrons/protocol_engine/commands/flex_stacker/common.py +35 -0
  107. opentrons/protocol_engine/commands/flex_stacker/set_stored_labware.py +7 -0
  108. opentrons/protocol_engine/commands/heater_shaker/__init__.py +14 -0
  109. opentrons/protocol_engine/commands/heater_shaker/common.py +20 -0
  110. opentrons/protocol_engine/commands/heater_shaker/set_and_wait_for_shake_speed.py +5 -4
  111. opentrons/protocol_engine/commands/heater_shaker/set_shake_speed.py +136 -0
  112. opentrons/protocol_engine/commands/heater_shaker/set_target_temperature.py +31 -5
  113. opentrons/protocol_engine/commands/move_labware.py +3 -4
  114. opentrons/protocol_engine/commands/move_to_addressable_area_for_drop_tip.py +1 -1
  115. opentrons/protocol_engine/commands/movement_common.py +31 -2
  116. opentrons/protocol_engine/commands/pick_up_tip.py +21 -11
  117. opentrons/protocol_engine/commands/pipetting_common.py +48 -3
  118. opentrons/protocol_engine/commands/set_tip_state.py +97 -0
  119. opentrons/protocol_engine/commands/temperature_module/set_target_temperature.py +38 -7
  120. opentrons/protocol_engine/commands/thermocycler/__init__.py +16 -0
  121. opentrons/protocol_engine/commands/thermocycler/run_extended_profile.py +6 -0
  122. opentrons/protocol_engine/commands/thermocycler/run_profile.py +8 -0
  123. opentrons/protocol_engine/commands/thermocycler/set_target_block_temperature.py +44 -7
  124. opentrons/protocol_engine/commands/thermocycler/set_target_lid_temperature.py +43 -14
  125. opentrons/protocol_engine/commands/thermocycler/start_run_extended_profile.py +191 -0
  126. opentrons/protocol_engine/commands/touch_tip.py +1 -1
  127. opentrons/protocol_engine/commands/unsafe/unsafe_place_labware.py +6 -22
  128. opentrons/protocol_engine/commands/wait_for_tasks.py +98 -0
  129. opentrons/protocol_engine/create_protocol_engine.py +12 -0
  130. opentrons/protocol_engine/engine_support.py +3 -0
  131. opentrons/protocol_engine/errors/__init__.py +12 -0
  132. opentrons/protocol_engine/errors/exceptions.py +119 -0
  133. opentrons/protocol_engine/execution/__init__.py +4 -0
  134. opentrons/protocol_engine/execution/command_executor.py +62 -1
  135. opentrons/protocol_engine/execution/create_queue_worker.py +9 -2
  136. opentrons/protocol_engine/execution/labware_movement.py +13 -15
  137. opentrons/protocol_engine/execution/movement.py +2 -0
  138. opentrons/protocol_engine/execution/pipetting.py +26 -25
  139. opentrons/protocol_engine/execution/queue_worker.py +4 -0
  140. opentrons/protocol_engine/execution/run_control.py +8 -0
  141. opentrons/protocol_engine/execution/task_handler.py +157 -0
  142. opentrons/protocol_engine/protocol_engine.py +137 -36
  143. opentrons/protocol_engine/resources/__init__.py +4 -0
  144. opentrons/protocol_engine/resources/camera_provider.py +110 -0
  145. opentrons/protocol_engine/resources/concurrency_provider.py +27 -0
  146. opentrons/protocol_engine/resources/deck_configuration_provider.py +7 -0
  147. opentrons/protocol_engine/resources/file_provider.py +133 -58
  148. opentrons/protocol_engine/resources/labware_validation.py +10 -6
  149. opentrons/protocol_engine/slot_standardization.py +2 -0
  150. opentrons/protocol_engine/state/_well_math.py +60 -18
  151. opentrons/protocol_engine/state/addressable_areas.py +2 -0
  152. opentrons/protocol_engine/state/camera.py +54 -0
  153. opentrons/protocol_engine/state/commands.py +37 -14
  154. opentrons/protocol_engine/state/geometry.py +276 -379
  155. opentrons/protocol_engine/state/labware.py +62 -108
  156. opentrons/protocol_engine/state/labware_origin_math/errors.py +94 -0
  157. opentrons/protocol_engine/state/labware_origin_math/stackup_origin_to_labware_origin.py +1336 -0
  158. opentrons/protocol_engine/state/module_substates/thermocycler_module_substate.py +37 -0
  159. opentrons/protocol_engine/state/modules.py +30 -8
  160. opentrons/protocol_engine/state/motion.py +60 -18
  161. opentrons/protocol_engine/state/preconditions.py +59 -0
  162. opentrons/protocol_engine/state/state.py +44 -0
  163. opentrons/protocol_engine/state/state_summary.py +4 -0
  164. opentrons/protocol_engine/state/tasks.py +139 -0
  165. opentrons/protocol_engine/state/tips.py +177 -258
  166. opentrons/protocol_engine/state/update_types.py +26 -9
  167. opentrons/protocol_engine/types/__init__.py +23 -4
  168. opentrons/protocol_engine/types/command_preconditions.py +18 -0
  169. opentrons/protocol_engine/types/deck_configuration.py +5 -1
  170. opentrons/protocol_engine/types/instrument.py +8 -1
  171. opentrons/protocol_engine/types/labware.py +1 -13
  172. opentrons/protocol_engine/types/location.py +26 -2
  173. opentrons/protocol_engine/types/module.py +11 -1
  174. opentrons/protocol_engine/types/tasks.py +38 -0
  175. opentrons/protocol_engine/types/tip.py +9 -0
  176. opentrons/protocol_runner/create_simulating_orchestrator.py +29 -2
  177. opentrons/protocol_runner/protocol_runner.py +14 -1
  178. opentrons/protocol_runner/run_orchestrator.py +49 -2
  179. opentrons/protocols/advanced_control/transfers/transfer_liquid_utils.py +2 -2
  180. opentrons/protocols/api_support/definitions.py +1 -1
  181. opentrons/protocols/api_support/types.py +2 -1
  182. opentrons/simulate.py +51 -15
  183. opentrons/system/camera.py +334 -4
  184. opentrons/system/ffmpeg.py +110 -0
  185. {opentrons-8.7.0a9.dist-info → opentrons-8.8.0a8.dist-info}/METADATA +4 -4
  186. {opentrons-8.7.0a9.dist-info → opentrons-8.8.0a8.dist-info}/RECORD +189 -161
  187. opentrons/protocol_engine/state/_labware_origin_math.py +0 -636
  188. {opentrons-8.7.0a9.dist-info → opentrons-8.8.0a8.dist-info}/WHEEL +0 -0
  189. {opentrons-8.7.0a9.dist-info → opentrons-8.8.0a8.dist-info}/entry_points.txt +0 -0
  190. {opentrons-8.7.0a9.dist-info → opentrons-8.8.0a8.dist-info}/licenses/LICENSE +0 -0
@@ -52,6 +52,7 @@ from opentrons_shared_data.errors.exceptions import (
52
52
  InvalidActuator,
53
53
  FirmwareUpdateFailedError,
54
54
  PipetteLiquidNotFoundError,
55
+ PipetteOverpressureError,
55
56
  )
56
57
 
57
58
  from .util import use_or_initialize_loop, check_motion_bounds
@@ -74,6 +75,8 @@ from .types import (
74
75
  DoorStateNotification,
75
76
  ErrorMessageNotification,
76
77
  HardwareEvent,
78
+ AsynchronousModuleErrorNotification,
79
+ ModuleDisconnectedNotification,
77
80
  HardwareEventHandler,
78
81
  HardwareAction,
79
82
  HepaFanState,
@@ -367,6 +370,24 @@ class OT3API(
367
370
 
368
371
  return futures
369
372
 
373
+ def _send_module_notification(self, event: HardwareEvent) -> None:
374
+ if not isinstance(
375
+ event,
376
+ (
377
+ AsynchronousModuleErrorNotification,
378
+ ModuleDisconnectedNotification,
379
+ ),
380
+ ):
381
+ return
382
+ mod_log.info(
383
+ f"Forwarding module event {event.event} for {event.module_model} {event.module_serial} at {event.port}"
384
+ )
385
+ for cb in self._callbacks:
386
+ try:
387
+ cb(event)
388
+ except Exception:
389
+ mod_log.exception("Errored during module asynchronous callback")
390
+
370
391
  def _reset_last_mount(self) -> None:
371
392
  self._last_moved_mount = None
372
393
 
@@ -422,7 +443,9 @@ class OT3API(
422
443
 
423
444
  await api_instance.set_status_bar_enabled(status_bar_enabled)
424
445
  module_controls = await AttachedModulesControl.build(
425
- api_instance, board_revision=backend.board_revision
446
+ api_instance,
447
+ board_revision=backend.board_revision,
448
+ event_callback=api_instance._send_module_notification,
426
449
  )
427
450
  backend.module_controls = module_controls
428
451
  await backend.build_estop_detector()
@@ -484,7 +507,9 @@ class OT3API(
484
507
  )
485
508
  await api_instance.cache_instruments()
486
509
  module_controls = await AttachedModulesControl.build(
487
- api_instance, board_revision=backend.board_revision
510
+ api_instance,
511
+ board_revision=backend.board_revision,
512
+ event_callback=api_instance._send_module_notification,
488
513
  )
489
514
  backend.module_controls = module_controls
490
515
  await backend.watch(api_instance.loop)
@@ -627,9 +652,10 @@ class OT3API(
627
652
  self.is_simulator
628
653
  ), "Cannot build simulating module from non-simulating hardware control API"
629
654
 
630
- return await self._backend.module_controls.build_module(
631
- port="",
632
- usb_port=USBPort(name="", port_number=1, port_group=PortGroup.LEFT),
655
+ return await self._backend.module_controls.register_simulated_module(
656
+ simulated_usb_port=USBPort(
657
+ name="", port_number=1, port_group=PortGroup.LEFT
658
+ ),
633
659
  type=modules.ModuleType.from_model(model),
634
660
  sim_model=model.value,
635
661
  )
@@ -1499,6 +1525,7 @@ class OT3API(
1499
1525
  acquire_lock: bool = True,
1500
1526
  check_bounds: MotionChecks = MotionChecks.NONE,
1501
1527
  expect_stalls: bool = False,
1528
+ delay: Optional[Tuple[List[Axis], float]] = None,
1502
1529
  ) -> None:
1503
1530
  """Worker function to apply robot motion."""
1504
1531
  machine_pos = machine_from_deck(
@@ -1532,6 +1559,7 @@ class OT3API(
1532
1559
  machine_pos,
1533
1560
  speed or 400.0,
1534
1561
  HWStopCondition.stall if expect_stalls else HWStopCondition.none,
1562
+ delay=delay,
1535
1563
  )
1536
1564
  except Exception:
1537
1565
  self._log.exception("Move failed")
@@ -3057,9 +3085,11 @@ class OT3API(
3057
3085
  async def aspirate_while_tracking(
3058
3086
  self,
3059
3087
  mount: Union[top_types.Mount, OT3Mount],
3060
- z_distance: float,
3088
+ end_point: top_types.Point,
3061
3089
  volume: float,
3062
- flow_rate: float = 1.0,
3090
+ rate: float = 1.0,
3091
+ movement_delay: Optional[float] = None,
3092
+ end_critical_point: Optional[CriticalPoint] = None,
3063
3093
  ) -> None:
3064
3094
  """
3065
3095
  Aspirate a volume of liquid (in microliters/uL) while moving the z axis synchronously.
@@ -3067,19 +3097,35 @@ class OT3API(
3067
3097
  :param mount: A robot mount that the instrument is on.
3068
3098
  :param z_distance: The distance the z axis will move during apsiration.
3069
3099
  :param volume: The volume of liquid to be aspirated.
3070
- :param flow_rate: The flow rate to aspirate with.
3100
+ :param rate: The rate multiplier to aspirate with.
3101
+ :param movement_delay: Time to wait after the pipette starts aspirating before x/y/z movement.
3071
3102
  """
3072
3103
  realmount = OT3Mount.from_mount(mount)
3073
3104
  aspirate_spec = self._pipette_handler.plan_check_aspirate(
3074
- realmount, volume, flow_rate
3105
+ realmount, volume, rate
3075
3106
  )
3076
3107
  if not aspirate_spec:
3077
3108
  return
3109
+ end_position = target_position_from_absolute(
3110
+ realmount,
3111
+ end_point,
3112
+ partial(self.critical_point_for, cp_override=end_critical_point),
3113
+ top_types.Point(*self._config.left_mount_offset),
3114
+ top_types.Point(*self._config.right_mount_offset),
3115
+ top_types.Point(*self._config.gripper_mount_offset),
3116
+ )
3117
+
3078
3118
  target_pos = target_positions_from_plunger_tracking(
3079
3119
  realmount,
3080
3120
  aspirate_spec.plunger_distance,
3081
- z_distance,
3082
- self._current_position,
3121
+ end_position,
3122
+ )
3123
+
3124
+ delay: Optional[Tuple[List[Axis], float]] = None
3125
+ if movement_delay is not None:
3126
+ delay = ([Axis.X, Axis.Y, Axis.Z_L, Axis.Z_R], movement_delay)
3127
+ self._log.info(
3128
+ f"aspirate_while_tracking: end at {end_point} {end_critical_point}, machine pos {end_position}, with plunger {target_pos}, delay {delay}"
3083
3129
  )
3084
3130
  try:
3085
3131
  await self._backend.set_active_current(
@@ -3093,7 +3139,13 @@ class OT3API(
3093
3139
  target_pos,
3094
3140
  speed=aspirate_spec.speed,
3095
3141
  home_flagged_axes=False,
3142
+ delay=delay,
3096
3143
  )
3144
+ except PipetteOverpressureError:
3145
+ self._log.exception("Aspirate failed with overpressure")
3146
+ # refresh positions during an over pressure here so we know where the gantry stopped.
3147
+ await self.refresh_positions()
3148
+ raise
3097
3149
  except Exception:
3098
3150
  self._log.exception("Aspirate failed")
3099
3151
  aspirate_spec.instr.set_current_volume(0)
@@ -3104,11 +3156,13 @@ class OT3API(
3104
3156
  async def dispense_while_tracking(
3105
3157
  self,
3106
3158
  mount: Union[top_types.Mount, OT3Mount],
3107
- z_distance: float,
3159
+ end_point: top_types.Point,
3108
3160
  volume: float,
3109
3161
  push_out: Optional[float],
3110
- flow_rate: float = 1.0,
3162
+ rate: float = 1.0,
3111
3163
  is_full_dispense: bool = False,
3164
+ movement_delay: Optional[float] = None,
3165
+ end_critical_point: Optional[CriticalPoint] = None,
3112
3166
  ) -> None:
3113
3167
  """
3114
3168
  Dispense a volume of liquid (in microliters/uL) while moving the z axis synchronously.
@@ -3116,21 +3170,36 @@ class OT3API(
3116
3170
  :param mount: A robot mount that the instrument is on.
3117
3171
  :param z_distance: The distance the z axis will move during dispensing.
3118
3172
  :param volume: The volume of liquid to be dispensed.
3119
- :param flow_rate: The flow rate to dispense with.
3173
+ :param rate: The rate multiplier to dispense with.
3174
+ :param movement_delay: Time to wait after the pipette starts dispensing before x/y/z movement.
3120
3175
  """
3121
3176
  realmount = OT3Mount.from_mount(mount)
3122
3177
  dispense_spec = self._pipette_handler.plan_check_dispense(
3123
- realmount, volume, flow_rate, push_out, is_full_dispense
3178
+ realmount, volume, rate, push_out, is_full_dispense
3124
3179
  )
3125
3180
  if not dispense_spec:
3126
3181
  return
3182
+ end_position = target_position_from_absolute(
3183
+ realmount,
3184
+ end_point,
3185
+ partial(self.critical_point_for, cp_override=end_critical_point),
3186
+ top_types.Point(*self._config.left_mount_offset),
3187
+ top_types.Point(*self._config.right_mount_offset),
3188
+ top_types.Point(*self._config.gripper_mount_offset),
3189
+ )
3190
+
3127
3191
  target_pos = target_positions_from_plunger_tracking(
3128
3192
  realmount,
3129
3193
  dispense_spec.plunger_distance,
3130
- z_distance,
3131
- self._current_position,
3194
+ end_position,
3132
3195
  )
3133
3196
 
3197
+ delay: Optional[Tuple[List[Axis], float]] = None
3198
+ if movement_delay is not None:
3199
+ delay = ([Axis.X, Axis.Y, Axis.Z_L, Axis.Z_R], movement_delay)
3200
+ self._log.info(
3201
+ f"dispense_while_tracking: end at {end_point} {end_critical_point}, machine pos {end_position}, with plunger {target_pos}, delay {delay}"
3202
+ )
3134
3203
  try:
3135
3204
  await self._backend.set_active_current(
3136
3205
  {dispense_spec.axis: dispense_spec.current}
@@ -3143,7 +3212,13 @@ class OT3API(
3143
3212
  target_pos,
3144
3213
  speed=dispense_spec.speed,
3145
3214
  home_flagged_axes=False,
3215
+ delay=delay,
3146
3216
  )
3217
+ except PipetteOverpressureError:
3218
+ self._log.exception("Aspirate failed with overpressure")
3219
+ # refresh positions during an over pressure here so we know where the gantry stopped.
3220
+ await self.refresh_positions()
3221
+ raise
3147
3222
  except Exception:
3148
3223
  self._log.exception("dispense failed")
3149
3224
  dispense_spec.instr.set_current_volume(0)
@@ -5,6 +5,7 @@ from abc import ABC, abstractmethod
5
5
  from typing import AsyncGenerator, List, Optional
6
6
  from opentrons.hardware_control.modules.errors import AbsorbanceReaderDisconnectedError
7
7
  from opentrons_shared_data.errors.exceptions import ModuleCommunicationError
8
+ from opentrons.drivers.asyncio.communication.errors import SerialException
8
9
 
9
10
 
10
11
  log = logging.getLogger(__name__)
@@ -88,6 +89,18 @@ class Poller:
88
89
  except asyncio.InvalidStateError:
89
90
  log.warning("Poller waiter was already cancelled")
90
91
 
92
+ def _error_callback(self, exc: Exception) -> None:
93
+ try:
94
+ self._reader.on_error(exc)
95
+ except Exception:
96
+ log.exception("Exception in reader callback")
97
+
98
+ def _complete_all(
99
+ self, exc: Exception | None, previous: List["asyncio.Future[None]"]
100
+ ) -> None:
101
+ for waiter in previous:
102
+ Poller._set_waiter_complete(waiter, exc)
103
+
91
104
  async def _poll_once(self) -> None:
92
105
  """Trigger a single read, notifying listeners of success or error."""
93
106
  previous_waiters = self._poll_waiters
@@ -99,14 +112,15 @@ class Poller:
99
112
  except asyncio.CancelledError:
100
113
  raise
101
114
  except AbsorbanceReaderDisconnectedError as e:
102
- for waiter in previous_waiters:
103
- Poller._set_waiter_complete(waiter, None)
104
- self._reader.on_error(e)
115
+ self._error_callback(e)
116
+ self._complete_all(e, previous_waiters)
117
+ except SerialException as se:
118
+ log.error(f"Polling gcode error: {se}")
119
+ self._error_callback(se)
120
+ self._complete_all(se, previous_waiters)
105
121
  except Exception as e:
106
122
  log.exception("Polling exception")
107
- self._reader.on_error(e)
108
- for waiter in previous_waiters:
109
- Poller._set_waiter_complete(waiter, e)
123
+ self._error_callback(e)
124
+ self._complete_all(e, previous_waiters)
110
125
  else:
111
- for waiter in previous_waiters:
112
- Poller._set_waiter_complete(waiter)
126
+ self._complete_all(None, previous_waiters)
@@ -125,17 +125,21 @@ class LiquidHandler(
125
125
  async def aspirate_while_tracking(
126
126
  self,
127
127
  mount: MountArgType,
128
- z_distance: float,
128
+ end_point: Point,
129
129
  volume: float,
130
130
  flow_rate: float = 1.0,
131
+ movement_delay: Optional[float] = None,
132
+ end_critical_point: Optional[CriticalPoint] = None,
131
133
  ) -> None:
132
134
  """
133
135
  Aspirate a volume of liquid (in microliters/uL) while moving the z axis synchronously.
134
136
 
135
137
  :param mount: A robot mount that the instrument is on.
136
- :param z_distance: The distance the z axis will move during apsiration.
138
+ :param end_point: The deck coordinate point to move the tip to during the aspirate.
137
139
  :param volume: The volume of liquid to be aspirated.
138
140
  :param flow_rate: The flow rate to aspirate with.
141
+ :param movement_delay: Time to wait after the pipette starts aspirating before x/y/z movement.
142
+ :param end_critical_point: The critical point for the end_point position.
139
143
  """
140
144
  ...
141
145
 
@@ -164,19 +168,23 @@ class LiquidHandler(
164
168
  async def dispense_while_tracking(
165
169
  self,
166
170
  mount: MountArgType,
167
- z_distance: float,
171
+ end_point: Point,
168
172
  volume: float,
169
173
  push_out: Optional[float],
170
174
  flow_rate: float = 1.0,
171
175
  is_full_dispense: bool = False,
176
+ movement_delay: Optional[float] = None,
177
+ end_critical_point: Optional[CriticalPoint] = None,
172
178
  ) -> None:
173
179
  """
174
180
  Dispense a volume of liquid (in microliters/uL) while moving the z axis synchronously.
175
181
 
176
182
  :param mount: A robot mount that the instrument is on.
177
- :param z_distance: The distance the z axis will move during dispensing.
183
+ :param end_point: The deck coordinate point to move the tip to during the dispense.
178
184
  :param volume: The volume of liquid to be dispensed.
179
185
  :param flow_rate: The flow rate to dispense with.
186
+ :param movement_delay: Time to wait after the pipette starts dispensing before x/y/z movement.
187
+ :param end_critical_point: The critical point for the end_point position.
180
188
  """
181
189
  ...
182
190
 
@@ -1,4 +1,5 @@
1
1
  """Module Firmware update script."""
2
+
2
3
  import argparse
3
4
  import asyncio
4
5
  from glob import glob
@@ -14,6 +15,7 @@ from opentrons.hardware_control import modules
14
15
  from opentrons.hardware_control.modules.mod_abc import AbstractModule
15
16
  from opentrons.hardware_control.modules.update import update_firmware
16
17
  from opentrons.hardware_control.types import BoardRevision
18
+ from opentrons.hardware_control.execution_manager import ExecutionManager
17
19
 
18
20
 
19
21
  # Constants for checking if module is back online
@@ -84,6 +86,9 @@ async def build_module(
84
86
  port=port,
85
87
  usb_port=mod.usb_port,
86
88
  type=modules.MODULE_TYPE_BY_NAME[mod.name],
89
+ execution_manager=ExecutionManager(),
90
+ disconnected_callback=lambda *args: None,
91
+ error_callback=lambda *args: None,
87
92
  simulating=False,
88
93
  hw_control_loop=loop,
89
94
  )
@@ -2,12 +2,26 @@ from asyncio import Queue
2
2
  import enum
3
3
  import logging
4
4
  from dataclasses import dataclass
5
- from typing import cast, Tuple, Union, List, Callable, Dict, TypeVar, Type
5
+ from typing import (
6
+ cast,
7
+ Tuple,
8
+ Union,
9
+ List,
10
+ Callable,
11
+ Dict,
12
+ TypeVar,
13
+ Type,
14
+ TYPE_CHECKING,
15
+ )
6
16
  from typing_extensions import Literal
7
17
  from opentrons import types as top_types
8
18
  from opentrons_shared_data.pipette.types import PipetteChannelType
19
+ from opentrons_shared_data.errors.exceptions import EnumeratedError
9
20
  from opentrons.config import feature_flags
10
21
 
22
+ if TYPE_CHECKING:
23
+ from .modules.types import ModuleModel
24
+
11
25
  MODULE_LOG = logging.getLogger(__name__)
12
26
 
13
27
 
@@ -384,6 +398,8 @@ class HardwareEventType(enum.Enum):
384
398
  DOOR_SWITCH_CHANGE = enum.auto()
385
399
  ERROR_MESSAGE = enum.auto()
386
400
  ESTOP_CHANGE = enum.auto()
401
+ ASYNCHRONOUS_MODULE_ERROR = enum.auto()
402
+ MODULE_DISCONNECTED = enum.auto()
387
403
 
388
404
 
389
405
  @dataclass
@@ -428,10 +444,35 @@ class ErrorMessageNotification:
428
444
  event: Literal[HardwareEventType.ERROR_MESSAGE] = HardwareEventType.ERROR_MESSAGE
429
445
 
430
446
 
447
+ @dataclass(frozen=True)
448
+ class AsynchronousModuleErrorNotification:
449
+ exception: EnumeratedError
450
+ module_serial: str | None
451
+ module_model: "ModuleModel"
452
+ port: str
453
+ event: Literal[
454
+ HardwareEventType.ASYNCHRONOUS_MODULE_ERROR
455
+ ] = HardwareEventType.ASYNCHRONOUS_MODULE_ERROR
456
+
457
+
458
+ @dataclass(frozen=True)
459
+ class ModuleDisconnectedNotification:
460
+ module_serial: str | None
461
+ module_model: "ModuleModel"
462
+ port: str
463
+ event: Literal[
464
+ HardwareEventType.MODULE_DISCONNECTED
465
+ ] = HardwareEventType.MODULE_DISCONNECTED
466
+
467
+
431
468
  # new event types get new dataclasses
432
469
  # when we add more event types we add them here
433
470
  HardwareEvent = Union[
434
- DoorStateNotification, ErrorMessageNotification, EstopStateNotification
471
+ DoorStateNotification,
472
+ ErrorMessageNotification,
473
+ EstopStateNotification,
474
+ AsynchronousModuleErrorNotification,
475
+ ModuleDisconnectedNotification,
435
476
  ]
436
477
 
437
478
  HardwareEventHandler = Callable[[HardwareEvent], None]
@@ -1,5 +1,5 @@
1
1
  from __future__ import annotations
2
- from typing import TYPE_CHECKING, List, Sequence, Union, overload
2
+ from typing import TYPE_CHECKING, List, Sequence, Union, overload, Optional
3
3
 
4
4
 
5
5
  from .helpers import (
@@ -31,10 +31,21 @@ def aspirate(
31
31
  location: Location,
32
32
  flow_rate: float,
33
33
  rate: float,
34
+ end_location: Optional[Location],
34
35
  ) -> command_types.AspirateCommand:
35
36
  location_text = stringify_location(location)
36
- template = "Aspirating {volume} uL from {location} at {flow} uL/sec"
37
- text = template.format(volume=float(volume), location=location_text, flow=flow_rate)
37
+ end_location_text = (
38
+ f" while moving to {stringify_location(end_location)}"
39
+ if end_location is not None
40
+ else ""
41
+ )
42
+ template = "Aspirating {volume} uL from {location} at {flow} uL/sec{end}"
43
+ text = template.format(
44
+ volume=float(volume),
45
+ location=location_text,
46
+ flow=flow_rate,
47
+ end=end_location_text,
48
+ )
38
49
 
39
50
  return {
40
51
  "name": command_types.ASPIRATE,
@@ -44,6 +55,7 @@ def aspirate(
44
55
  "location": location,
45
56
  "rate": rate,
46
57
  "text": text,
58
+ "end_location": end_location,
47
59
  },
48
60
  }
49
61
 
@@ -54,10 +66,21 @@ def dispense(
54
66
  location: Location,
55
67
  flow_rate: float,
56
68
  rate: float,
69
+ end_location: Optional[Location],
57
70
  ) -> command_types.DispenseCommand:
58
71
  location_text = stringify_location(location)
59
- template = "Dispensing {volume} uL into {location} at {flow} uL/sec"
60
- text = template.format(volume=float(volume), location=location_text, flow=flow_rate)
72
+ end_location_text = (
73
+ f" while moving to {stringify_location(end_location)}"
74
+ if end_location is not None
75
+ else ""
76
+ )
77
+ template = "Dispensing {volume} uL into {location} at {flow} uL/sec{end}"
78
+ text = template.format(
79
+ volume=float(volume),
80
+ location=location_text,
81
+ flow=flow_rate,
82
+ end=end_location_text,
83
+ )
61
84
 
62
85
  return {
63
86
  "name": command_types.DISPENSE,
@@ -67,6 +90,7 @@ def dispense(
67
90
  "location": location,
68
91
  "rate": rate,
69
92
  "text": text,
93
+ "end_location": end_location,
70
94
  },
71
95
  }
72
96
 
@@ -208,6 +232,35 @@ def mix(
208
232
  }
209
233
 
210
234
 
235
+ def dynamic_mix(
236
+ instrument: InstrumentContext,
237
+ repetitions: int,
238
+ volume: float,
239
+ aspirate_start_location: Location,
240
+ aspirate_end_location: Union[Location, None],
241
+ dispense_start_location: Location,
242
+ dispense_end_location: Union[Location, None],
243
+ movement_delay: float,
244
+ ) -> command_types.DynamicMixCommand:
245
+ text = "Dynamically mixing {repetitions} times with a volume of {volume} ul".format(
246
+ repetitions=repetitions, volume=float(volume)
247
+ )
248
+ return {
249
+ "name": command_types.MIX,
250
+ "payload": {
251
+ "instrument": instrument,
252
+ "aspirate_start_location": aspirate_start_location,
253
+ "aspirate_end_location": aspirate_end_location,
254
+ "dispense_start_location": dispense_start_location,
255
+ "dispense_end_location": dispense_end_location,
256
+ "volume": volume,
257
+ "repetitions": repetitions,
258
+ "text": text,
259
+ "movement_delay": movement_delay,
260
+ },
261
+ }
262
+
263
+
211
264
  def blow_out(
212
265
  instrument: InstrumentContext, location: Location
213
266
  ) -> command_types.BlowOutCommand:
@@ -89,6 +89,24 @@ def thermocycler_set_block_temp(
89
89
  }
90
90
 
91
91
 
92
+ def thermocycler_start_set_block_temp(
93
+ temperature: float,
94
+ ) -> command_types.ThermocyclerStartSetBlockTempCommand:
95
+ temp = round(float(temperature), utils.TC_GCODE_ROUNDING_PRECISION)
96
+ text = f"Starting to set Thermocycler well block temperature to {temp} °C"
97
+ # TODO: BC 2019-09-05 this time resolving logic is partially duplicated
98
+ # in the thermocycler api class definition, with this command logger
99
+ # implementation, there isn't a great way to avoid this, but it should
100
+ # be consolidated as soon as an alternative to the publisher is settled on.
101
+ return {
102
+ "name": command_types.THERMOCYCLER_START_SET_BLOCK_TEMP,
103
+ "payload": {
104
+ "temperature": temperature,
105
+ "text": text,
106
+ },
107
+ }
108
+
109
+
92
110
  def thermocycler_execute_profile(
93
111
  steps: List[ThermocyclerStep], repetitions: int
94
112
  ) -> command_types.ThermocyclerExecuteProfileCommand:
@@ -102,6 +120,19 @@ def thermocycler_execute_profile(
102
120
  }
103
121
 
104
122
 
123
+ def thermocycler_start_execute_profile(
124
+ steps: List[ThermocyclerStep], repetitions: int
125
+ ) -> command_types.ThermocyclerStartExecuteProfileCommand:
126
+ text = (
127
+ f"In the background, thermocycler starting to run {repetitions} repetitions "
128
+ f" of cycle composed of the following steps: {steps}"
129
+ )
130
+ return {
131
+ "name": command_types.THERMOCYCLER_START_EXECUTE_PROFILE,
132
+ "payload": {"text": text, "steps": steps},
133
+ }
134
+
135
+
105
136
  def thermocycler_wait_for_hold() -> command_types.ThermocyclerWaitForHoldCommand:
106
137
  text = "Waiting for hold time duration"
107
138
  return {"name": command_types.THERMOCYCLER_WAIT_FOR_HOLD, "payload": {"text": text}}
@@ -120,6 +151,17 @@ def thermocycler_set_lid_temperature(
120
151
  return {"name": command_types.THERMOCYCLER_SET_LID_TEMP, "payload": {"text": text}}
121
152
 
122
153
 
154
+ def thermocycler_start_set_lid_temperature(
155
+ temperature: float,
156
+ ) -> command_types.ThermocyclerStartSetLidTempCommand:
157
+ temp = round(float(temperature), utils.TC_GCODE_ROUNDING_PRECISION)
158
+ text = f"Starting to set Thermocycler lid temperature to {temp} °C"
159
+ return {
160
+ "name": command_types.THERMOCYCLER_START_SET_LID_TEMP,
161
+ "payload": {"text": text},
162
+ }
163
+
164
+
123
165
  def thermocycler_deactivate_lid() -> command_types.ThermocyclerDeactivateLidCommand:
124
166
  text = "Deactivating Thermocycler lid heating"
125
167
  return {
@@ -183,6 +225,16 @@ def heater_shaker_set_and_wait_for_shake_speed(
183
225
  }
184
226
 
185
227
 
228
+ def heater_shaker_set_shake_speed(
229
+ rpm: int,
230
+ ) -> command_types.HeaterShakerSetShakeSpeedCommand:
231
+ text = f"Setting Heater-Shaker to Shake at {rpm} RPM"
232
+ return {
233
+ "name": command_types.HEATER_SHAKER_SET_SHAKE_SPEED,
234
+ "payload": {"text": text},
235
+ }
236
+
237
+
186
238
  def heater_shaker_open_labware_latch() -> command_types.HeaterShakerOpenLabwareLatchCommand:
187
239
  text = "Unlatching labware on Heater-Shaker"
188
240
  return {
@@ -1,6 +1,7 @@
1
1
  from datetime import timedelta
2
- from typing import Optional
2
+ from typing import Optional, Tuple
3
3
  from . import types as command_types
4
+ from opentrons.protocol_api.tasks import Task
4
5
 
5
6
 
6
7
  def comment(msg: str) -> command_types.CommentCommand:
@@ -52,3 +53,54 @@ def move_labware(text: str) -> command_types.MoveLabwareCommand:
52
53
  "name": command_types.MOVE_LABWARE,
53
54
  "payload": {"text": text},
54
55
  }
56
+
57
+
58
+ def capture_image(
59
+ resolution: Optional[Tuple[int, int]],
60
+ zoom: Optional[float],
61
+ contrast: Optional[float],
62
+ brightness: Optional[float],
63
+ saturation: Optional[float],
64
+ ) -> command_types.CaptureImageCommand:
65
+ text = "Capturing an image"
66
+ if resolution:
67
+ text += f" with resolution {resolution[0]}x{resolution[1]}"
68
+ if zoom:
69
+ text += f" zoom of {zoom}X"
70
+ if contrast:
71
+ text += f" contrast of {contrast}%"
72
+ if brightness:
73
+ text += f" brightness of {brightness}%"
74
+ if saturation:
75
+ text += f" saturation of {saturation}%"
76
+ text += "."
77
+ return {
78
+ "name": command_types.CAPTURE_IMAGE,
79
+ "payload": {
80
+ "text": text,
81
+ "resolution": resolution,
82
+ "zoom": zoom,
83
+ "contrast": contrast,
84
+ "brightness": brightness,
85
+ "saturation": saturation,
86
+ },
87
+ }
88
+
89
+
90
+ def wait_for_tasks(tasks: list[Task]) -> command_types.WaitForTasksCommand:
91
+ task_ids = [task.created_at.strftime("%Y-%m-%d %H:%M:%S") for task in tasks]
92
+ msg = f"Waiting for tasks that started at: {task_ids}."
93
+ return {
94
+ "name": command_types.WAIT_FOR_TASKS,
95
+ "payload": {"text": msg},
96
+ }
97
+
98
+
99
+ def create_timer(seconds: float) -> command_types.CreateTimerCommand:
100
+ return {
101
+ "name": command_types.CREATE_TIMER,
102
+ "payload": {
103
+ "text": f"Creating background timer for {seconds} seconds.",
104
+ "time": seconds,
105
+ },
106
+ }