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
@@ -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,10 @@ 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,
3063
3092
  ) -> None:
3064
3093
  """
3065
3094
  Aspirate a volume of liquid (in microliters/uL) while moving the z axis synchronously.
@@ -3067,20 +3096,33 @@ class OT3API(
3067
3096
  :param mount: A robot mount that the instrument is on.
3068
3097
  :param z_distance: The distance the z axis will move during apsiration.
3069
3098
  :param volume: The volume of liquid to be aspirated.
3070
- :param flow_rate: The flow rate to aspirate with.
3099
+ :param rate: The rate multiplier to aspirate with.
3100
+ :param movement_delay: Time to wait after the pipette starts aspirating before x/y/z movement.
3071
3101
  """
3072
3102
  realmount = OT3Mount.from_mount(mount)
3073
3103
  aspirate_spec = self._pipette_handler.plan_check_aspirate(
3074
- realmount, volume, flow_rate
3104
+ realmount, volume, rate
3075
3105
  )
3076
3106
  if not aspirate_spec:
3077
3107
  return
3108
+ end_position = target_position_from_absolute(
3109
+ realmount,
3110
+ end_point,
3111
+ self.critical_point_for,
3112
+ top_types.Point(*self._config.left_mount_offset),
3113
+ top_types.Point(*self._config.right_mount_offset),
3114
+ top_types.Point(*self._config.gripper_mount_offset),
3115
+ )
3116
+
3078
3117
  target_pos = target_positions_from_plunger_tracking(
3079
3118
  realmount,
3080
3119
  aspirate_spec.plunger_distance,
3081
- z_distance,
3082
- self._current_position,
3120
+ end_position,
3083
3121
  )
3122
+
3123
+ delay: Optional[Tuple[List[Axis], float]] = None
3124
+ if movement_delay is not None:
3125
+ delay = ([Axis.X, Axis.Y, Axis.Z_L, Axis.Z_R], movement_delay)
3084
3126
  try:
3085
3127
  await self._backend.set_active_current(
3086
3128
  {aspirate_spec.axis: aspirate_spec.current}
@@ -3093,7 +3135,13 @@ class OT3API(
3093
3135
  target_pos,
3094
3136
  speed=aspirate_spec.speed,
3095
3137
  home_flagged_axes=False,
3138
+ delay=delay,
3096
3139
  )
3140
+ except PipetteOverpressureError:
3141
+ self._log.exception("Aspirate failed with overpressure")
3142
+ # refresh positions during an over pressure here so we know where the gantry stopped.
3143
+ await self.refresh_positions()
3144
+ raise
3097
3145
  except Exception:
3098
3146
  self._log.exception("Aspirate failed")
3099
3147
  aspirate_spec.instr.set_current_volume(0)
@@ -3104,11 +3152,12 @@ class OT3API(
3104
3152
  async def dispense_while_tracking(
3105
3153
  self,
3106
3154
  mount: Union[top_types.Mount, OT3Mount],
3107
- z_distance: float,
3155
+ end_point: top_types.Point,
3108
3156
  volume: float,
3109
3157
  push_out: Optional[float],
3110
- flow_rate: float = 1.0,
3158
+ rate: float = 1.0,
3111
3159
  is_full_dispense: bool = False,
3160
+ movement_delay: Optional[float] = None,
3112
3161
  ) -> None:
3113
3162
  """
3114
3163
  Dispense a volume of liquid (in microliters/uL) while moving the z axis synchronously.
@@ -3116,21 +3165,34 @@ class OT3API(
3116
3165
  :param mount: A robot mount that the instrument is on.
3117
3166
  :param z_distance: The distance the z axis will move during dispensing.
3118
3167
  :param volume: The volume of liquid to be dispensed.
3119
- :param flow_rate: The flow rate to dispense with.
3168
+ :param rate: The rate multiplier to dispense with.
3169
+ :param movement_delay: Time to wait after the pipette starts dispensing before x/y/z movement.
3120
3170
  """
3121
3171
  realmount = OT3Mount.from_mount(mount)
3122
3172
  dispense_spec = self._pipette_handler.plan_check_dispense(
3123
- realmount, volume, flow_rate, push_out, is_full_dispense
3173
+ realmount, volume, rate, push_out, is_full_dispense
3124
3174
  )
3125
3175
  if not dispense_spec:
3126
3176
  return
3177
+ end_position = target_position_from_absolute(
3178
+ realmount,
3179
+ end_point,
3180
+ self.critical_point_for,
3181
+ top_types.Point(*self._config.left_mount_offset),
3182
+ top_types.Point(*self._config.right_mount_offset),
3183
+ top_types.Point(*self._config.gripper_mount_offset),
3184
+ )
3185
+
3127
3186
  target_pos = target_positions_from_plunger_tracking(
3128
3187
  realmount,
3129
3188
  dispense_spec.plunger_distance,
3130
- z_distance,
3131
- self._current_position,
3189
+ end_position,
3132
3190
  )
3133
3191
 
3192
+ delay: Optional[Tuple[List[Axis], float]] = None
3193
+ if movement_delay is not None:
3194
+ delay = ([Axis.X, Axis.Y, Axis.Z_L, Axis.Z_R], movement_delay)
3195
+
3134
3196
  try:
3135
3197
  await self._backend.set_active_current(
3136
3198
  {dispense_spec.axis: dispense_spec.current}
@@ -3143,7 +3205,13 @@ class OT3API(
3143
3205
  target_pos,
3144
3206
  speed=dispense_spec.speed,
3145
3207
  home_flagged_axes=False,
3208
+ delay=delay,
3146
3209
  )
3210
+ except PipetteOverpressureError:
3211
+ self._log.exception("Aspirate failed with overpressure")
3212
+ # refresh positions during an over pressure here so we know where the gantry stopped.
3213
+ await self.refresh_positions()
3214
+ raise
3147
3215
  except Exception:
3148
3216
  self._log.exception("dispense failed")
3149
3217
  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,9 +125,10 @@ 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,
131
132
  ) -> None:
132
133
  """
133
134
  Aspirate a volume of liquid (in microliters/uL) while moving the z axis synchronously.
@@ -136,6 +137,7 @@ class LiquidHandler(
136
137
  :param z_distance: The distance the z axis will move during apsiration.
137
138
  :param volume: The volume of liquid to be aspirated.
138
139
  :param flow_rate: The flow rate to aspirate with.
140
+ :param movement_delay: Time to wait after the pipette starts aspirating before x/y/z movement.
139
141
  """
140
142
  ...
141
143
 
@@ -164,11 +166,12 @@ class LiquidHandler(
164
166
  async def dispense_while_tracking(
165
167
  self,
166
168
  mount: MountArgType,
167
- z_distance: float,
169
+ end_point: Point,
168
170
  volume: float,
169
171
  push_out: Optional[float],
170
172
  flow_rate: float = 1.0,
171
173
  is_full_dispense: bool = False,
174
+ movement_delay: Optional[float] = None,
172
175
  ) -> None:
173
176
  """
174
177
  Dispense a volume of liquid (in microliters/uL) while moving the z axis synchronously.
@@ -177,6 +180,7 @@ class LiquidHandler(
177
180
  :param z_distance: The distance the z axis will move during dispensing.
178
181
  :param volume: The volume of liquid to be dispensed.
179
182
  :param flow_rate: The flow rate to dispense with.
183
+ :param movement_delay: Time to wait after the pipette starts dispensing before x/y/z movement.
180
184
  """
181
185
  ...
182
186
 
@@ -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
+ }