opentrons 8.1.0a0__py2.py3-none-any.whl → 8.2.0__py2.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 (238) hide show
  1. opentrons/cli/analyze.py +71 -7
  2. opentrons/config/__init__.py +9 -0
  3. opentrons/config/advanced_settings.py +22 -0
  4. opentrons/config/defaults_ot3.py +14 -36
  5. opentrons/config/feature_flags.py +4 -0
  6. opentrons/config/types.py +6 -17
  7. opentrons/drivers/absorbance_reader/abstract.py +27 -3
  8. opentrons/drivers/absorbance_reader/async_byonoy.py +208 -154
  9. opentrons/drivers/absorbance_reader/driver.py +24 -15
  10. opentrons/drivers/absorbance_reader/hid_protocol.py +79 -50
  11. opentrons/drivers/absorbance_reader/simulator.py +32 -6
  12. opentrons/drivers/types.py +23 -1
  13. opentrons/execute.py +2 -2
  14. opentrons/hardware_control/api.py +18 -10
  15. opentrons/hardware_control/backends/controller.py +3 -2
  16. opentrons/hardware_control/backends/flex_protocol.py +11 -5
  17. opentrons/hardware_control/backends/ot3controller.py +18 -50
  18. opentrons/hardware_control/backends/ot3simulator.py +7 -6
  19. opentrons/hardware_control/backends/ot3utils.py +1 -0
  20. opentrons/hardware_control/instruments/ot2/pipette_handler.py +22 -82
  21. opentrons/hardware_control/instruments/ot3/pipette_handler.py +10 -2
  22. opentrons/hardware_control/module_control.py +43 -2
  23. opentrons/hardware_control/modules/__init__.py +7 -1
  24. opentrons/hardware_control/modules/absorbance_reader.py +232 -83
  25. opentrons/hardware_control/modules/errors.py +7 -0
  26. opentrons/hardware_control/modules/heater_shaker.py +8 -3
  27. opentrons/hardware_control/modules/magdeck.py +12 -3
  28. opentrons/hardware_control/modules/mod_abc.py +27 -2
  29. opentrons/hardware_control/modules/tempdeck.py +15 -7
  30. opentrons/hardware_control/modules/thermocycler.py +69 -3
  31. opentrons/hardware_control/modules/types.py +11 -5
  32. opentrons/hardware_control/modules/update.py +11 -5
  33. opentrons/hardware_control/modules/utils.py +3 -1
  34. opentrons/hardware_control/ot3_calibration.py +6 -6
  35. opentrons/hardware_control/ot3api.py +131 -94
  36. opentrons/hardware_control/poller.py +15 -11
  37. opentrons/hardware_control/protocols/__init__.py +1 -7
  38. opentrons/hardware_control/protocols/instrument_configurer.py +14 -2
  39. opentrons/hardware_control/protocols/liquid_handler.py +5 -0
  40. opentrons/hardware_control/protocols/position_estimator.py +3 -1
  41. opentrons/hardware_control/types.py +2 -0
  42. opentrons/legacy_commands/helpers.py +8 -2
  43. opentrons/motion_planning/__init__.py +2 -0
  44. opentrons/motion_planning/waypoints.py +32 -0
  45. opentrons/protocol_api/__init__.py +2 -1
  46. opentrons/protocol_api/_liquid.py +87 -1
  47. opentrons/protocol_api/_parameter_context.py +10 -1
  48. opentrons/protocol_api/core/engine/deck_conflict.py +0 -297
  49. opentrons/protocol_api/core/engine/instrument.py +29 -25
  50. opentrons/protocol_api/core/engine/labware.py +20 -4
  51. opentrons/protocol_api/core/engine/module_core.py +166 -17
  52. opentrons/protocol_api/core/engine/pipette_movement_conflict.py +362 -0
  53. opentrons/protocol_api/core/engine/protocol.py +30 -2
  54. opentrons/protocol_api/core/instrument.py +2 -0
  55. opentrons/protocol_api/core/labware.py +4 -0
  56. opentrons/protocol_api/core/legacy/legacy_instrument_core.py +2 -0
  57. opentrons/protocol_api/core/legacy/legacy_labware_core.py +5 -0
  58. opentrons/protocol_api/core/legacy/legacy_protocol_core.py +6 -2
  59. opentrons/protocol_api/core/legacy_simulator/legacy_instrument_core.py +2 -0
  60. opentrons/protocol_api/core/module.py +22 -4
  61. opentrons/protocol_api/core/protocol.py +6 -2
  62. opentrons/protocol_api/instrument_context.py +52 -20
  63. opentrons/protocol_api/labware.py +13 -1
  64. opentrons/protocol_api/module_contexts.py +115 -17
  65. opentrons/protocol_api/protocol_context.py +49 -5
  66. opentrons/protocol_api/validation.py +5 -3
  67. opentrons/protocol_engine/__init__.py +10 -9
  68. opentrons/protocol_engine/actions/__init__.py +3 -0
  69. opentrons/protocol_engine/actions/actions.py +30 -25
  70. opentrons/protocol_engine/actions/get_state_update.py +38 -0
  71. opentrons/protocol_engine/clients/sync_client.py +1 -1
  72. opentrons/protocol_engine/clients/transports.py +1 -1
  73. opentrons/protocol_engine/commands/__init__.py +0 -4
  74. opentrons/protocol_engine/commands/absorbance_reader/__init__.py +41 -11
  75. opentrons/protocol_engine/commands/absorbance_reader/close_lid.py +148 -0
  76. opentrons/protocol_engine/commands/absorbance_reader/initialize.py +65 -9
  77. opentrons/protocol_engine/commands/absorbance_reader/open_lid.py +148 -0
  78. opentrons/protocol_engine/commands/absorbance_reader/read.py +200 -0
  79. opentrons/protocol_engine/commands/aspirate.py +29 -16
  80. opentrons/protocol_engine/commands/aspirate_in_place.py +33 -16
  81. opentrons/protocol_engine/commands/blow_out.py +63 -14
  82. opentrons/protocol_engine/commands/blow_out_in_place.py +55 -13
  83. opentrons/protocol_engine/commands/calibration/calibrate_gripper.py +2 -5
  84. opentrons/protocol_engine/commands/calibration/calibrate_module.py +3 -4
  85. opentrons/protocol_engine/commands/calibration/calibrate_pipette.py +2 -5
  86. opentrons/protocol_engine/commands/calibration/move_to_maintenance_position.py +6 -4
  87. opentrons/protocol_engine/commands/command.py +31 -18
  88. opentrons/protocol_engine/commands/command_unions.py +37 -24
  89. opentrons/protocol_engine/commands/comment.py +5 -3
  90. opentrons/protocol_engine/commands/configure_for_volume.py +11 -14
  91. opentrons/protocol_engine/commands/configure_nozzle_layout.py +9 -15
  92. opentrons/protocol_engine/commands/custom.py +5 -3
  93. opentrons/protocol_engine/commands/dispense.py +42 -20
  94. opentrons/protocol_engine/commands/dispense_in_place.py +32 -14
  95. opentrons/protocol_engine/commands/drop_tip.py +70 -16
  96. opentrons/protocol_engine/commands/drop_tip_in_place.py +59 -13
  97. opentrons/protocol_engine/commands/get_tip_presence.py +5 -3
  98. opentrons/protocol_engine/commands/heater_shaker/close_labware_latch.py +6 -6
  99. opentrons/protocol_engine/commands/heater_shaker/deactivate_heater.py +6 -6
  100. opentrons/protocol_engine/commands/heater_shaker/deactivate_shaker.py +6 -6
  101. opentrons/protocol_engine/commands/heater_shaker/open_labware_latch.py +8 -6
  102. opentrons/protocol_engine/commands/heater_shaker/set_and_wait_for_shake_speed.py +8 -4
  103. opentrons/protocol_engine/commands/heater_shaker/set_target_temperature.py +6 -4
  104. opentrons/protocol_engine/commands/heater_shaker/wait_for_temperature.py +6 -6
  105. opentrons/protocol_engine/commands/home.py +11 -5
  106. opentrons/protocol_engine/commands/liquid_probe.py +146 -88
  107. opentrons/protocol_engine/commands/load_labware.py +28 -5
  108. opentrons/protocol_engine/commands/load_liquid.py +18 -7
  109. opentrons/protocol_engine/commands/load_module.py +4 -6
  110. opentrons/protocol_engine/commands/load_pipette.py +18 -17
  111. opentrons/protocol_engine/commands/magnetic_module/disengage.py +6 -6
  112. opentrons/protocol_engine/commands/magnetic_module/engage.py +6 -4
  113. opentrons/protocol_engine/commands/move_labware.py +155 -23
  114. opentrons/protocol_engine/commands/move_relative.py +15 -3
  115. opentrons/protocol_engine/commands/move_to_addressable_area.py +29 -4
  116. opentrons/protocol_engine/commands/move_to_addressable_area_for_drop_tip.py +13 -4
  117. opentrons/protocol_engine/commands/move_to_coordinates.py +11 -5
  118. opentrons/protocol_engine/commands/move_to_well.py +37 -10
  119. opentrons/protocol_engine/commands/pick_up_tip.py +51 -30
  120. opentrons/protocol_engine/commands/pipetting_common.py +47 -16
  121. opentrons/protocol_engine/commands/prepare_to_aspirate.py +62 -15
  122. opentrons/protocol_engine/commands/reload_labware.py +13 -4
  123. opentrons/protocol_engine/commands/retract_axis.py +6 -3
  124. opentrons/protocol_engine/commands/save_position.py +2 -3
  125. opentrons/protocol_engine/commands/set_rail_lights.py +5 -3
  126. opentrons/protocol_engine/commands/set_status_bar.py +5 -3
  127. opentrons/protocol_engine/commands/temperature_module/deactivate.py +6 -4
  128. opentrons/protocol_engine/commands/temperature_module/set_target_temperature.py +3 -4
  129. opentrons/protocol_engine/commands/temperature_module/wait_for_temperature.py +6 -6
  130. opentrons/protocol_engine/commands/thermocycler/__init__.py +19 -0
  131. opentrons/protocol_engine/commands/thermocycler/close_lid.py +8 -8
  132. opentrons/protocol_engine/commands/thermocycler/deactivate_block.py +6 -4
  133. opentrons/protocol_engine/commands/thermocycler/deactivate_lid.py +6 -4
  134. opentrons/protocol_engine/commands/thermocycler/open_lid.py +8 -4
  135. opentrons/protocol_engine/commands/thermocycler/run_extended_profile.py +165 -0
  136. opentrons/protocol_engine/commands/thermocycler/run_profile.py +6 -6
  137. opentrons/protocol_engine/commands/thermocycler/set_target_block_temperature.py +3 -4
  138. opentrons/protocol_engine/commands/thermocycler/set_target_lid_temperature.py +3 -4
  139. opentrons/protocol_engine/commands/thermocycler/wait_for_block_temperature.py +6 -4
  140. opentrons/protocol_engine/commands/thermocycler/wait_for_lid_temperature.py +6 -4
  141. opentrons/protocol_engine/commands/touch_tip.py +19 -7
  142. opentrons/protocol_engine/commands/unsafe/__init__.py +30 -0
  143. opentrons/protocol_engine/commands/unsafe/unsafe_blow_out_in_place.py +6 -4
  144. opentrons/protocol_engine/commands/unsafe/unsafe_drop_tip_in_place.py +12 -4
  145. opentrons/protocol_engine/commands/unsafe/unsafe_engage_axes.py +5 -3
  146. opentrons/protocol_engine/commands/unsafe/unsafe_place_labware.py +208 -0
  147. opentrons/protocol_engine/commands/unsafe/unsafe_ungrip_labware.py +77 -0
  148. opentrons/protocol_engine/commands/unsafe/update_position_estimators.py +10 -4
  149. opentrons/protocol_engine/commands/verify_tip_presence.py +5 -5
  150. opentrons/protocol_engine/commands/wait_for_duration.py +5 -3
  151. opentrons/protocol_engine/commands/wait_for_resume.py +5 -3
  152. opentrons/protocol_engine/create_protocol_engine.py +60 -10
  153. opentrons/protocol_engine/engine_support.py +2 -1
  154. opentrons/protocol_engine/error_recovery_policy.py +14 -3
  155. opentrons/protocol_engine/errors/__init__.py +20 -0
  156. opentrons/protocol_engine/errors/error_occurrence.py +8 -3
  157. opentrons/protocol_engine/errors/exceptions.py +127 -2
  158. opentrons/protocol_engine/execution/__init__.py +2 -0
  159. opentrons/protocol_engine/execution/command_executor.py +22 -13
  160. opentrons/protocol_engine/execution/create_queue_worker.py +5 -1
  161. opentrons/protocol_engine/execution/door_watcher.py +1 -1
  162. opentrons/protocol_engine/execution/equipment.py +2 -1
  163. opentrons/protocol_engine/execution/error_recovery_hardware_state_synchronizer.py +101 -0
  164. opentrons/protocol_engine/execution/gantry_mover.py +4 -2
  165. opentrons/protocol_engine/execution/hardware_stopper.py +3 -3
  166. opentrons/protocol_engine/execution/heater_shaker_movement_flagger.py +1 -4
  167. opentrons/protocol_engine/execution/labware_movement.py +73 -22
  168. opentrons/protocol_engine/execution/movement.py +17 -7
  169. opentrons/protocol_engine/execution/pipetting.py +7 -4
  170. opentrons/protocol_engine/execution/queue_worker.py +6 -2
  171. opentrons/protocol_engine/execution/run_control.py +1 -1
  172. opentrons/protocol_engine/execution/thermocycler_movement_flagger.py +1 -1
  173. opentrons/protocol_engine/execution/thermocycler_plate_lifter.py +2 -1
  174. opentrons/protocol_engine/execution/tip_handler.py +77 -43
  175. opentrons/protocol_engine/notes/__init__.py +14 -2
  176. opentrons/protocol_engine/notes/notes.py +18 -1
  177. opentrons/protocol_engine/plugins.py +1 -1
  178. opentrons/protocol_engine/protocol_engine.py +47 -31
  179. opentrons/protocol_engine/resources/__init__.py +2 -0
  180. opentrons/protocol_engine/resources/deck_data_provider.py +19 -5
  181. opentrons/protocol_engine/resources/file_provider.py +161 -0
  182. opentrons/protocol_engine/resources/fixture_validation.py +11 -1
  183. opentrons/protocol_engine/resources/labware_validation.py +10 -0
  184. opentrons/protocol_engine/state/__init__.py +0 -70
  185. opentrons/protocol_engine/state/addressable_areas.py +1 -1
  186. opentrons/protocol_engine/state/command_history.py +21 -2
  187. opentrons/protocol_engine/state/commands.py +110 -31
  188. opentrons/protocol_engine/state/files.py +59 -0
  189. opentrons/protocol_engine/state/frustum_helpers.py +440 -0
  190. opentrons/protocol_engine/state/geometry.py +445 -59
  191. opentrons/protocol_engine/state/labware.py +264 -84
  192. opentrons/protocol_engine/state/liquids.py +1 -1
  193. opentrons/protocol_engine/state/module_substates/absorbance_reader_substate.py +21 -3
  194. opentrons/protocol_engine/state/modules.py +145 -90
  195. opentrons/protocol_engine/state/motion.py +33 -14
  196. opentrons/protocol_engine/state/pipettes.py +157 -317
  197. opentrons/protocol_engine/state/state.py +30 -1
  198. opentrons/protocol_engine/state/state_summary.py +3 -0
  199. opentrons/protocol_engine/state/tips.py +69 -114
  200. opentrons/protocol_engine/state/update_types.py +424 -0
  201. opentrons/protocol_engine/state/wells.py +236 -0
  202. opentrons/protocol_engine/types.py +90 -0
  203. opentrons/protocol_reader/file_format_validator.py +83 -15
  204. opentrons/protocol_runner/json_translator.py +21 -5
  205. opentrons/protocol_runner/legacy_command_mapper.py +27 -6
  206. opentrons/protocol_runner/legacy_context_plugin.py +27 -71
  207. opentrons/protocol_runner/protocol_runner.py +6 -3
  208. opentrons/protocol_runner/run_orchestrator.py +41 -6
  209. opentrons/protocols/advanced_control/mix.py +3 -5
  210. opentrons/protocols/advanced_control/transfers.py +125 -56
  211. opentrons/protocols/api_support/constants.py +1 -1
  212. opentrons/protocols/api_support/definitions.py +1 -1
  213. opentrons/protocols/api_support/labware_like.py +4 -4
  214. opentrons/protocols/api_support/tip_tracker.py +2 -2
  215. opentrons/protocols/api_support/types.py +15 -2
  216. opentrons/protocols/api_support/util.py +30 -42
  217. opentrons/protocols/duration/errors.py +1 -1
  218. opentrons/protocols/duration/estimator.py +50 -29
  219. opentrons/protocols/execution/dev_types.py +2 -2
  220. opentrons/protocols/execution/execute_json_v4.py +15 -10
  221. opentrons/protocols/execution/execute_python.py +8 -3
  222. opentrons/protocols/geometry/planning.py +12 -12
  223. opentrons/protocols/labware.py +17 -33
  224. opentrons/protocols/parameters/csv_parameter_interface.py +3 -1
  225. opentrons/simulate.py +3 -3
  226. opentrons/types.py +30 -3
  227. opentrons/util/logging_config.py +34 -0
  228. {opentrons-8.1.0a0.dist-info → opentrons-8.2.0.dist-info}/METADATA +5 -4
  229. {opentrons-8.1.0a0.dist-info → opentrons-8.2.0.dist-info}/RECORD +235 -223
  230. opentrons/protocol_engine/commands/absorbance_reader/measure.py +0 -94
  231. opentrons/protocol_engine/commands/configuring_common.py +0 -26
  232. opentrons/protocol_runner/thread_async_queue.py +0 -174
  233. /opentrons/protocol_engine/state/{abstract_store.py → _abstract_store.py} +0 -0
  234. /opentrons/protocol_engine/state/{move_types.py → _move_types.py} +0 -0
  235. {opentrons-8.1.0a0.dist-info → opentrons-8.2.0.dist-info}/LICENSE +0 -0
  236. {opentrons-8.1.0a0.dist-info → opentrons-8.2.0.dist-info}/WHEEL +0 -0
  237. {opentrons-8.1.0a0.dist-info → opentrons-8.2.0.dist-info}/entry_points.txt +0 -0
  238. {opentrons-8.1.0a0.dist-info → opentrons-8.2.0.dist-info}/top_level.txt +0 -0
@@ -143,7 +143,8 @@ from .backends.types import HWStopCondition
143
143
  from .backends.flex_protocol import FlexBackend
144
144
  from .backends.ot3simulator import OT3Simulator
145
145
  from .backends.errors import SubsystemUpdating
146
-
146
+ from opentrons_hardware.firmware_bindings.constants import SensorId
147
+ from opentrons_hardware.sensors.types import SensorDataType
147
148
 
148
149
  mod_log = logging.getLogger(__name__)
149
150
 
@@ -164,6 +165,7 @@ def _adjust_high_throughput_z_current(func: Wrapped) -> Wrapped:
164
165
  A decorator that temproarily and conditionally changes the active current (based on the axis input)
165
166
  before a function is executed and the cleans up afterwards
166
167
  """
168
+
167
169
  # only home and retract should be wrappeed by this decorator
168
170
  @wraps(func)
169
171
  async def wrapper(self: Any, axis: Axis, *args: Any, **kwargs: Any) -> Any:
@@ -306,6 +308,12 @@ class OT3API(
306
308
  async with self._backend.restore_system_constraints():
307
309
  yield
308
310
 
311
+ @contextlib.asynccontextmanager
312
+ async def grab_pressure(self, mount: OT3Mount) -> AsyncIterator[None]:
313
+ instrument = self._pipette_handler.get_pipette(mount)
314
+ async with self._backend.grab_pressure(instrument.channels, mount):
315
+ yield
316
+
309
317
  def _update_door_state(self, door_state: DoorState) -> None:
310
318
  mod_log.info(f"Updating the window switch status: {door_state}")
311
319
  self.door_state = door_state
@@ -767,12 +775,12 @@ class OT3API(
767
775
  """
768
776
  Function to update motor estimation for a set of axes
769
777
  """
778
+ if axes is None:
779
+ axes = [ax for ax in Axis]
770
780
 
771
- if axes:
772
- checked_axes = [ax for ax in axes if ax in Axis]
773
- else:
774
- checked_axes = [ax for ax in Axis]
775
- await self._backend.update_motor_estimation(checked_axes)
781
+ axes = [ax for ax in axes if self._backend.axis_is_present(ax)]
782
+
783
+ await self._backend.update_motor_estimation(axes)
776
784
 
777
785
  # Global actions API
778
786
  def pause(self, pause_type: PauseType) -> None:
@@ -933,7 +941,6 @@ class OT3API(
933
941
  current_pos_float > self._config.safe_home_distance
934
942
  and current_pos_float < max_distance
935
943
  ):
936
-
937
944
  # move toward home until a safe distance
938
945
  await self._backend.tip_action(
939
946
  origin={Axis.Q: current_pos_float},
@@ -1811,7 +1818,8 @@ class OT3API(
1811
1818
  increment: Optional[float] = None,
1812
1819
  ) -> None:
1813
1820
  """This is a slightly more barebones variation of pick_up_tip. This is only the motor routine
1814
- directly involved in tip pickup, and leaves any state updates and plunger moves to the caller."""
1821
+ directly involved in tip pickup, and leaves any state updates and plunger moves to the caller.
1822
+ """
1815
1823
  realmount = OT3Mount.from_mount(mount)
1816
1824
  instrument = self._pipette_handler.get_pipette(realmount)
1817
1825
 
@@ -1878,12 +1886,10 @@ class OT3API(
1878
1886
 
1879
1887
  With wet tips, the primary concern is leftover droplets inside the tip.
1880
1888
  These droplets ideally only move down and out of the tip, not up into the tip.
1881
- Therefore, it is preferable to use the "blow-out" speed when moving the
1882
- plunger down, and the slower "aspirate" speed when moving the plunger up.
1889
+ Therefore, it is preferable to use the slower "aspirate" speed when
1890
+ moving the plunger up after a blow-out.
1883
1891
 
1884
- Assume all tips are wet, because we do not differentiate between wet/dry tips.
1885
-
1886
- When no tip is attached, moving at the max speed is preferable, to save time.
1892
+ All other situations, moving at the max speed is preferable, to save time.
1887
1893
  """
1888
1894
  checked_mount = OT3Mount.from_mount(mount)
1889
1895
  instrument = self._pipette_handler.get_pipette(checked_mount)
@@ -1896,21 +1902,22 @@ class OT3API(
1896
1902
  self._current_position,
1897
1903
  )
1898
1904
  pip_ax = Axis.of_main_tool_actuator(checked_mount)
1899
- # speed depends on if there is a tip, and which direction to move
1900
- if instrument.has_tip_length:
1905
+ # save time while moving down by using max speed
1906
+ max_speeds = self.config.motion_settings.default_max_speed
1907
+ speed_down = max_speeds[self.gantry_load][OT3AxisKind.P]
1908
+ # upward moves can be max speed, or aspirate speed
1909
+ # use the (slower) aspirate if there is a tip and we're following a blow-out
1910
+ plunger_is_below_bottom_pos = (
1911
+ self._current_position[pip_ax] > instrument.plunger_positions.bottom
1912
+ )
1913
+ if instrument.has_tip_length and plunger_is_below_bottom_pos:
1901
1914
  # using slower aspirate flow-rate, to avoid pulling droplets up
1902
1915
  speed_up = self._pipette_handler.plunger_speed(
1903
1916
  instrument, instrument.aspirate_flow_rate, "aspirate"
1904
1917
  )
1905
- # use blow-out flow-rate, so we can push droplets out
1906
- speed_down = self._pipette_handler.plunger_speed(
1907
- instrument, instrument.blow_out_flow_rate, "dispense"
1908
- )
1909
1918
  else:
1910
- # save time by using max speed
1911
- max_speeds = self.config.motion_settings.default_max_speed
1919
+ # either no tip, or plunger just homed, so tip is dry
1912
1920
  speed_up = max_speeds[self.gantry_load][OT3AxisKind.P]
1913
- speed_down = speed_up
1914
1921
  # IMPORTANT: Here is our backlash compensation.
1915
1922
  # The plunger is pre-loaded in the "aspirate" direction
1916
1923
  backlash_pos = target_pos.copy()
@@ -2229,15 +2236,6 @@ class OT3API(
2229
2236
  )
2230
2237
  await self.home_gear_motors()
2231
2238
 
2232
- def cache_tip(
2233
- self, mount: Union[top_types.Mount, OT3Mount], tip_length: float
2234
- ) -> None:
2235
- realmount = OT3Mount.from_mount(mount)
2236
- instrument = self._pipette_handler.get_pipette(realmount)
2237
-
2238
- instrument.add_tip(tip_length=tip_length)
2239
- instrument.set_current_volume(0)
2240
-
2241
2239
  async def pick_up_tip(
2242
2240
  self,
2243
2241
  mount: Union[top_types.Mount, OT3Mount],
@@ -2283,17 +2281,10 @@ class OT3API(
2283
2281
  )
2284
2282
  instrument.working_volume = tip_volume
2285
2283
 
2286
- async def drop_tip(
2284
+ async def tip_drop_moves(
2287
2285
  self, mount: Union[top_types.Mount, OT3Mount], home_after: bool = False
2288
2286
  ) -> None:
2289
- """Drop tip at the current location."""
2290
2287
  realmount = OT3Mount.from_mount(mount)
2291
- instrument = self._pipette_handler.get_pipette(realmount)
2292
-
2293
- def _remove_tips() -> None:
2294
- instrument.set_current_volume(0)
2295
- instrument.current_tiprack_diameter = 0.0
2296
- instrument.remove_tip()
2297
2288
 
2298
2289
  await self._move_to_plunger_bottom(realmount, rate=1.0, check_current_vol=False)
2299
2290
 
@@ -2320,11 +2311,27 @@ class OT3API(
2320
2311
  if home_after:
2321
2312
  await self._home([Axis.by_mount(mount)])
2322
2313
 
2323
- _remove_tips()
2324
- # call this in case we're simulating
2314
+ # call this in case we're simulating:
2325
2315
  if isinstance(self._backend, OT3Simulator):
2326
2316
  self._backend._update_tip_state(realmount, False)
2327
2317
 
2318
+ async def drop_tip(
2319
+ self, mount: Union[top_types.Mount, OT3Mount], home_after: bool = False
2320
+ ) -> None:
2321
+ """Drop tip at the current location."""
2322
+ await self.tip_drop_moves(mount=mount, home_after=home_after)
2323
+
2324
+ # todo(mm, 2024-10-17): Ideally, callers would be able to replicate the behavior
2325
+ # of this method via self.drop_tip_moves() plus other public methods. This
2326
+ # currently prevents that: there is no public equivalent for
2327
+ # instrument.set_current_volume().
2328
+ realmount = OT3Mount.from_mount(mount)
2329
+ instrument = self._pipette_handler.get_pipette(realmount)
2330
+ instrument.set_current_volume(0)
2331
+
2332
+ self.set_current_tiprack_diameter(mount, 0.0)
2333
+ self.remove_tip(mount)
2334
+
2328
2335
  async def clean_up(self) -> None:
2329
2336
  """Get the API ready to stop cleanly."""
2330
2337
  await self._backend.clean_up()
@@ -2592,13 +2599,18 @@ class OT3API(
2592
2599
  starting_nozzle,
2593
2600
  )
2594
2601
 
2595
- async def add_tip(
2602
+ def add_tip(
2596
2603
  self, mount: Union[top_types.Mount, OT3Mount], tip_length: float
2597
2604
  ) -> None:
2598
- await self._pipette_handler.add_tip(OT3Mount.from_mount(mount), tip_length)
2605
+ self._pipette_handler.add_tip(OT3Mount.from_mount(mount), tip_length)
2599
2606
 
2600
- async def remove_tip(self, mount: Union[top_types.Mount, OT3Mount]) -> None:
2601
- await self._pipette_handler.remove_tip(OT3Mount.from_mount(mount))
2607
+ def cache_tip(
2608
+ self, mount: Union[top_types.Mount, OT3Mount], tip_length: float
2609
+ ) -> None:
2610
+ self._pipette_handler.cache_tip(OT3Mount.from_mount(mount), tip_length)
2611
+
2612
+ def remove_tip(self, mount: Union[top_types.Mount, OT3Mount]) -> None:
2613
+ self._pipette_handler.remove_tip(OT3Mount.from_mount(mount))
2602
2614
 
2603
2615
  def add_gripper_probe(self, probe: GripperProbe) -> None:
2604
2616
  self._gripper_handler.add_probe(probe)
@@ -2607,17 +2619,17 @@ class OT3API(
2607
2619
  self._gripper_handler.remove_probe()
2608
2620
 
2609
2621
  @staticmethod
2610
- def liquid_probe_non_responsive_z_distance(z_speed: float) -> float:
2622
+ def liquid_probe_non_responsive_z_distance(
2623
+ z_speed: float, samples_for_baselining: int, sample_time_sec: float
2624
+ ) -> float:
2611
2625
  """Calculate the Z distance travelled where the LLD pass will be unresponsive."""
2612
2626
  # NOTE: (sigler) Here lye some magic numbers.
2613
2627
  # The Z axis probing motion uses the first 20 samples to calculate
2614
2628
  # a baseline for all following samples, making the very beginning of
2615
2629
  # that Z motion unable to detect liquid. The sensor is configured for
2616
2630
  # 4ms sample readings, and so we then assume it takes ~80ms to complete.
2617
- # If the Z is moving at 5mm/sec, then ~80ms equates to ~0.4
2618
- baseline_during_z_sample_num = 20 # FIXME: (sigler) shouldn't be defined here?
2619
- sample_time_sec = 0.004 # FIXME: (sigler) shouldn't be defined here?
2620
- baseline_duration_sec = baseline_during_z_sample_num * sample_time_sec
2631
+ # If the Z is moving at 5mm/sec, then ~80ms equates to ~0.4mm
2632
+ baseline_duration_sec = samples_for_baselining * sample_time_sec
2621
2633
  non_responsive_z_mm = baseline_duration_sec * z_speed
2622
2634
  return non_responsive_z_mm
2623
2635
 
@@ -2628,6 +2640,9 @@ class OT3API(
2628
2640
  probe: InstrumentProbeType,
2629
2641
  p_travel: float,
2630
2642
  force_both_sensors: bool = False,
2643
+ response_queue: Optional[
2644
+ asyncio.Queue[Dict[SensorId, List[SensorDataType]]]
2645
+ ] = None,
2631
2646
  ) -> float:
2632
2647
  plunger_direction = -1 if probe_settings.aspirate_while_sensing else 1
2633
2648
  end_z = await self._backend.liquid_probe(
@@ -2637,10 +2652,10 @@ class OT3API(
2637
2652
  (probe_settings.plunger_speed * plunger_direction),
2638
2653
  probe_settings.sensor_threshold_pascals,
2639
2654
  probe_settings.plunger_impulse_time,
2640
- probe_settings.output_option,
2641
- probe_settings.data_files,
2655
+ probe_settings.samples_for_baselining,
2642
2656
  probe=probe,
2643
2657
  force_both_sensors=force_both_sensors,
2658
+ response_queue=response_queue,
2644
2659
  )
2645
2660
  machine_pos = await self._backend.update_position()
2646
2661
  machine_pos[Axis.by_mount(mount)] = end_z
@@ -2654,13 +2669,16 @@ class OT3API(
2654
2669
  cp = self.critical_point_for(mount, None)
2655
2670
  return deck_end_z + offset.z + cp.z
2656
2671
 
2657
- async def liquid_probe(
2672
+ async def liquid_probe( # noqa: C901
2658
2673
  self,
2659
2674
  mount: Union[top_types.Mount, OT3Mount],
2660
2675
  max_z_dist: float,
2661
2676
  probe_settings: Optional[LiquidProbeSettings] = None,
2662
2677
  probe: Optional[InstrumentProbeType] = None,
2663
2678
  force_both_sensors: bool = False,
2679
+ response_queue: Optional[
2680
+ asyncio.Queue[Dict[SensorId, List[SensorDataType]]]
2681
+ ] = None,
2664
2682
  ) -> float:
2665
2683
  """Search for and return liquid level height.
2666
2684
 
@@ -2697,7 +2715,6 @@ class OT3API(
2697
2715
  probe_settings = deepcopy(self.config.liquid_sense)
2698
2716
 
2699
2717
  # We need to significatly slow down the 96 channel liquid probe
2700
- # TODO: (sigler) add LLD plunger-speed to pipette definitions
2701
2718
  if self.gantry_load == GantryLoad.HIGH_THROUGHPUT:
2702
2719
  max_plunger_speed = self.config.motion_settings.max_speed_discontinuity[
2703
2720
  GantryLoad.HIGH_THROUGHPUT
@@ -2706,59 +2723,79 @@ class OT3API(
2706
2723
  max_plunger_speed, probe_settings.plunger_speed
2707
2724
  )
2708
2725
 
2709
- probe_start_pos = await self.gantry_position(checked_mount, refresh=True)
2726
+ starting_position = await self.gantry_position(checked_mount, refresh=True)
2710
2727
 
2711
- # plunger travel distance is from TOP->BOTTOM (minus the backlash distance + impulse)
2712
- # FIXME: logic for how plunger moves is divided between here and tool_sensors.py
2713
- p_impulse_mm = (
2728
+ sensor_baseline_plunger_move_mm = (
2714
2729
  probe_settings.plunger_impulse_time * probe_settings.plunger_speed
2715
2730
  )
2716
- p_total_mm = (
2731
+ total_plunger_axis_mm = (
2717
2732
  instrument.plunger_positions.bottom - instrument.plunger_positions.top
2718
2733
  )
2719
-
2720
- p_working_mm = p_total_mm - (instrument.backlash_distance + p_impulse_mm)
2721
-
2734
+ max_allowed_plunger_distance_mm = total_plunger_axis_mm - (
2735
+ instrument.backlash_distance + sensor_baseline_plunger_move_mm
2736
+ )
2722
2737
  # height where probe action will begin
2723
- # TODO: (sigler) add this to pipette's liquid def (per tip)
2724
- probe_pass_overlap_mm = 0.1
2725
- non_responsive_z_mm = OT3API.liquid_probe_non_responsive_z_distance(
2726
- probe_settings.mount_speed
2738
+ sensor_baseline_z_move_mm = OT3API.liquid_probe_non_responsive_z_distance(
2739
+ probe_settings.mount_speed,
2740
+ probe_settings.samples_for_baselining,
2741
+ probe_settings.sample_time_sec,
2742
+ )
2743
+ z_offset_per_pass = (
2744
+ sensor_baseline_z_move_mm + probe_settings.z_overlap_between_passes_mm
2727
2745
  )
2728
- probe_pass_z_offset_mm = non_responsive_z_mm + probe_pass_overlap_mm
2729
2746
 
2730
2747
  # height that is considered safe to reset the plunger without disturbing liquid
2731
2748
  # this usually needs to at least 1-2mm from liquid, to avoid splashes from air
2732
- # TODO: (sigler) add this to pipette's liquid def (per tip)
2733
- probe_safe_reset_mm = max(2.0, probe_pass_z_offset_mm)
2749
+ z_offset_for_plunger_prep = max(
2750
+ probe_settings.plunger_reset_offset, z_offset_per_pass
2751
+ )
2752
+
2753
+ async def prep_plunger_for_probe_move(
2754
+ position: top_types.Point, aspirate_while_sensing: bool
2755
+ ) -> None:
2756
+ # safe distance so we don't accidentally aspirate liquid if we're already close to liquid
2757
+ mount_pos_for_plunger_prep = top_types.Point(
2758
+ position.x,
2759
+ position.y,
2760
+ position.z + z_offset_for_plunger_prep,
2761
+ )
2762
+ # Prep the plunger
2763
+ await self.move_to(checked_mount, mount_pos_for_plunger_prep)
2764
+ if aspirate_while_sensing:
2765
+ await self._move_to_plunger_bottom(checked_mount, rate=1)
2766
+ else:
2767
+ await self._move_to_plunger_top_for_liquid_probe(checked_mount, rate=1)
2734
2768
 
2735
2769
  error: Optional[PipetteLiquidNotFoundError] = None
2736
- pos = await self.gantry_position(checked_mount, refresh=True)
2737
- # probe_start_pos.z + z_distance of pass - pos.z should be < max_z_dist
2770
+ current_position = await self.gantry_position(checked_mount, refresh=True)
2771
+ # starting_position.z + z_distance of pass - pos.z should be < max_z_dist
2738
2772
  # due to rounding errors this can get caught in an infinite loop when the distance is almost equal
2739
2773
  # so we check to see if they're within 0.01 which is 1/5th the minimum movement distance from move_utils.py
2740
- while (probe_start_pos.z - pos.z) < (max_z_dist - 0.01):
2741
- # safe distance so we don't accidentally aspirate liquid if we're already close to liquid
2742
- safe_plunger_pos = top_types.Point(
2743
- pos.x, pos.y, pos.z + probe_safe_reset_mm
2774
+ while (starting_position.z - current_position.z) < (max_z_dist - 0.01):
2775
+ await prep_plunger_for_probe_move(
2776
+ position=current_position,
2777
+ aspirate_while_sensing=probe_settings.aspirate_while_sensing,
2744
2778
  )
2779
+
2745
2780
  # overlap amount we want to use between passes
2746
2781
  pass_start_pos = top_types.Point(
2747
- pos.x, pos.y, pos.z + probe_pass_z_offset_mm
2782
+ current_position.x,
2783
+ current_position.y,
2784
+ current_position.z + z_offset_per_pass,
2748
2785
  )
2749
- max_z_time = (
2750
- max_z_dist - probe_start_pos.z + pass_start_pos.z
2751
- ) / probe_settings.mount_speed
2752
- p_travel_required_for_z = max_z_time * probe_settings.plunger_speed
2753
- p_pass_travel = min(p_travel_required_for_z, p_working_mm)
2754
- # Prep the plunger
2755
- await self.move_to(checked_mount, safe_plunger_pos)
2756
- if probe_settings.aspirate_while_sensing:
2757
- # TODO(cm, 7/8/24): remove p_prep_speed from the rate at some point
2758
- await self._move_to_plunger_bottom(checked_mount, rate=1)
2759
- else:
2760
- await self._move_to_plunger_top_for_liquid_probe(checked_mount, rate=1)
2761
2786
 
2787
+ total_remaining_z_dist = pass_start_pos.z - (
2788
+ starting_position.z - max_z_dist
2789
+ )
2790
+ finish_probe_move_duration = (
2791
+ total_remaining_z_dist / probe_settings.mount_speed
2792
+ )
2793
+ finish_probe_plunger_distance_mm = (
2794
+ finish_probe_move_duration * probe_settings.plunger_speed
2795
+ )
2796
+ plunger_travel_mm = min(
2797
+ finish_probe_plunger_distance_mm, max_allowed_plunger_distance_mm
2798
+ )
2762
2799
  try:
2763
2800
  # move to where we want to start a pass and run a pass
2764
2801
  await self.move_to(checked_mount, pass_start_pos)
@@ -2766,17 +2803,19 @@ class OT3API(
2766
2803
  checked_mount,
2767
2804
  probe_settings,
2768
2805
  checked_probe,
2769
- p_pass_travel + p_impulse_mm,
2806
+ plunger_travel_mm + sensor_baseline_plunger_move_mm,
2807
+ force_both_sensors,
2808
+ response_queue,
2770
2809
  )
2771
2810
  # if we made it here without an error we found the liquid
2772
2811
  error = None
2773
2812
  break
2774
2813
  except PipetteLiquidNotFoundError as lnfe:
2775
2814
  error = lnfe
2776
- pos = await self.gantry_position(checked_mount, refresh=True)
2777
- await self.move_to(checked_mount, probe_start_pos + top_types.Point(z=2))
2815
+ current_position = await self.gantry_position(checked_mount, refresh=True)
2816
+ await self.move_to(checked_mount, starting_position + top_types.Point(z=2))
2778
2817
  await self.prepare_for_aspirate(checked_mount)
2779
- await self.move_to(checked_mount, probe_start_pos)
2818
+ await self.move_to(checked_mount, starting_position)
2780
2819
  if error is not None:
2781
2820
  # if we never found liquid raise an error
2782
2821
  raise error
@@ -2835,8 +2874,6 @@ class OT3API(
2835
2874
  pass_settings.speed_mm_per_s,
2836
2875
  pass_settings.sensor_threshold_pf,
2837
2876
  probe,
2838
- pass_settings.output_option,
2839
- pass_settings.data_files,
2840
2877
  )
2841
2878
  end_pos = await self.gantry_position(mount, refresh=True)
2842
2879
  if retract_after:
@@ -3,6 +3,7 @@ import contextlib
3
3
  import logging
4
4
  from abc import ABC, abstractmethod
5
5
  from typing import AsyncGenerator, List, Optional
6
+ from opentrons.hardware_control.modules.errors import AbsorbanceReaderDisconnectedError
6
7
  from opentrons_shared_data.errors.exceptions import ModuleCommunicationError
7
8
 
8
9
 
@@ -36,21 +37,20 @@ class Poller:
36
37
  self._poll_forever_task: Optional["asyncio.Task[None]"] = None
37
38
 
38
39
  async def start(self) -> None:
39
- assert self._poll_forever_task is None, "Poller already started"
40
- self._poll_forever_task = asyncio.create_task(self._poll_forever())
41
- await self.wait_next_poll()
40
+ if self._poll_forever_task is None:
41
+ self._poll_forever_task = asyncio.create_task(self._poll_forever())
42
+ await self.wait_next_poll()
42
43
 
43
44
  async def stop(self) -> None:
44
45
  """Stop polling."""
45
46
  task = self._poll_forever_task
46
-
47
- assert task is not None, "Poller never started"
48
-
49
- async with self._use_read_lock():
50
- task.cancel()
51
- await asyncio.gather(task, return_exceptions=True)
52
- for waiter in self._poll_waiters:
53
- waiter.cancel(msg="Module was removed")
47
+ if task is not None:
48
+ async with self._use_read_lock():
49
+ task.cancel()
50
+ await asyncio.gather(task, return_exceptions=True)
51
+ for waiter in self._poll_waiters:
52
+ waiter.cancel(msg="Module was removed")
53
+ self._poll_forever_task = None
54
54
 
55
55
  async def wait_next_poll(self) -> None:
56
56
  """Wait for the next poll to complete.
@@ -98,6 +98,10 @@ class Poller:
98
98
  await self._reader.read()
99
99
  except asyncio.CancelledError:
100
100
  raise
101
+ except AbsorbanceReaderDisconnectedError as e:
102
+ for waiter in previous_waiters:
103
+ Poller._set_waiter_complete(waiter, None)
104
+ self._reader.on_error(e)
101
105
  except Exception as e:
102
106
  log.exception("Polling exception")
103
107
  self._reader.on_error(e)
@@ -58,9 +58,6 @@ class HardwareControlInterface(
58
58
  def get_robot_type(self) -> Type[OT2RobotType]:
59
59
  return OT2RobotType
60
60
 
61
- def cache_tip(self, mount: MountArgType, tip_length: float) -> None:
62
- ...
63
-
64
61
 
65
62
  class FlexHardwareControlInterface(
66
63
  PositionEstimator,
@@ -87,12 +84,9 @@ class FlexHardwareControlInterface(
87
84
  def get_robot_type(self) -> Type[FlexRobotType]:
88
85
  return FlexRobotType
89
86
 
90
- def cache_tip(self, mount: MountArgType, tip_length: float) -> None:
91
- ...
92
-
93
87
 
94
88
  __all__ = [
95
- "HardwareControlAPI",
89
+ "HardwareControlInterface",
96
90
  "FlexHardwareControlInterface",
97
91
  "Simulatable",
98
92
  "Stoppable",
@@ -142,15 +142,27 @@ class InstrumentConfigurer(Protocol[MountArgType]):
142
142
  """
143
143
  ...
144
144
 
145
- async def add_tip(self, mount: MountArgType, tip_length: float) -> None:
145
+ # todo(mm, 2024-10-17): Consider deleting this in favor of cache_tip()
146
+ # if we can do so without breaking anything.
147
+ def add_tip(self, mount: MountArgType, tip_length: float) -> None:
146
148
  """Inform the hardware that a tip is now attached to a pipette.
147
149
 
150
+ If a tip is already attached, this no-ops.
151
+
148
152
  This changes the critical point of the pipette to make sure that
149
153
  the end of the tip is what moves around, and allows liquid handling.
150
154
  """
151
155
  ...
152
156
 
153
- async def remove_tip(self, mount: MountArgType) -> None:
157
+ def cache_tip(self, mount: MountArgType, tip_length: float) -> None:
158
+ """Inform the hardware that a tip is now attached to a pipette.
159
+
160
+ This is like `add_tip()`, except that if a tip is already attached,
161
+ this replaces it instead of no-opping.
162
+ """
163
+ ...
164
+
165
+ def remove_tip(self, mount: MountArgType) -> None:
154
166
  """Inform the hardware that a tip is no longer attached to a pipette.
155
167
 
156
168
  This changes the critical point of the system to the end of the
@@ -164,6 +164,11 @@ class LiquidHandler(
164
164
  """
165
165
  ...
166
166
 
167
+ async def tip_drop_moves(
168
+ self, mount: MountArgType, home_after: bool = True
169
+ ) -> None:
170
+ ...
171
+
167
172
  async def drop_tip(
168
173
  self,
169
174
  mount: MountArgType,
@@ -10,7 +10,7 @@ class PositionEstimator(Protocol):
10
10
  """Update the specified axes' position estimators from their encoders.
11
11
 
12
12
  This will allow these axes to make a non-home move even if they do not currently have
13
- a position estimation (unless there is no tracked poition from the encoders, as would be
13
+ a position estimation (unless there is no tracked position from the encoders, as would be
14
14
  true immediately after boot).
15
15
 
16
16
  Axis encoders have less precision than their position estimators. Calling this function will
@@ -19,6 +19,8 @@ class PositionEstimator(Protocol):
19
19
 
20
20
  This function updates only the requested axes. If other axes have bad position estimation,
21
21
  moves that require those axes or attempts to get the position of those axes will still fail.
22
+ Axes that are not currently available (like a plunger for a pipette that is not connected)
23
+ will be ignored.
22
24
  """
23
25
  ...
24
26
 
@@ -625,6 +625,8 @@ class GripperJawState(enum.Enum):
625
625
  #: the gripper is actively force-control gripping something
626
626
  HOLDING = enum.auto()
627
627
  #: the gripper is in position-control mode
628
+ STOPPED = enum.auto()
629
+ #: the gripper has been homed before but is stopped now
628
630
 
629
631
 
630
632
  class InstrumentProbeType(enum.Enum):
@@ -49,7 +49,9 @@ def stringify_disposal_location(location: Union[TrashBin, WasteChute]) -> str:
49
49
 
50
50
 
51
51
  def _stringify_labware_movement_location(
52
- location: Union[DeckLocation, OffDeckType, Labware, ModuleContext, WasteChute]
52
+ location: Union[
53
+ DeckLocation, OffDeckType, Labware, ModuleContext, WasteChute, TrashBin
54
+ ]
53
55
  ) -> str:
54
56
  if isinstance(location, (int, str)):
55
57
  return f"slot {location}"
@@ -61,11 +63,15 @@ def _stringify_labware_movement_location(
61
63
  return str(location)
62
64
  elif isinstance(location, WasteChute):
63
65
  return "Waste Chute"
66
+ elif isinstance(location, TrashBin):
67
+ return "Trash Bin " + location.location.name
64
68
 
65
69
 
66
70
  def stringify_labware_movement_command(
67
71
  source_labware: Labware,
68
- destination: Union[DeckLocation, OffDeckType, Labware, ModuleContext, WasteChute],
72
+ destination: Union[
73
+ DeckLocation, OffDeckType, Labware, ModuleContext, WasteChute, TrashBin
74
+ ],
69
75
  use_gripper: bool,
70
76
  ) -> str:
71
77
  source_labware_text = _stringify_labware_movement_location(source_labware)
@@ -6,6 +6,7 @@ from .waypoints import (
6
6
  MINIMUM_Z_MARGIN,
7
7
  get_waypoints,
8
8
  get_gripper_labware_movement_waypoints,
9
+ get_gripper_labware_placement_waypoints,
9
10
  )
10
11
 
11
12
  from .types import Waypoint, MoveType
@@ -27,4 +28,5 @@ __all__ = [
27
28
  "ArcOutOfBoundsError",
28
29
  "get_waypoints",
29
30
  "get_gripper_labware_movement_waypoints",
31
+ "get_gripper_labware_placement_waypoints",
30
32
  ]
@@ -181,3 +181,35 @@ def get_gripper_labware_movement_waypoints(
181
181
  )
182
182
  )
183
183
  return waypoints_with_jaw_status
184
+
185
+
186
+ def get_gripper_labware_placement_waypoints(
187
+ to_labware_center: Point,
188
+ gripper_home_z: float,
189
+ drop_offset: Optional[Point],
190
+ ) -> List[GripperMovementWaypointsWithJawStatus]:
191
+ """Get waypoints for placing labware using a gripper."""
192
+ drop_offset = drop_offset or Point()
193
+
194
+ drop_location = to_labware_center + Point(
195
+ drop_offset.x, drop_offset.y, drop_offset.z
196
+ )
197
+
198
+ post_drop_home_pos = Point(drop_location.x, drop_location.y, gripper_home_z)
199
+
200
+ return [
201
+ GripperMovementWaypointsWithJawStatus(
202
+ position=Point(drop_location.x, drop_location.y, gripper_home_z),
203
+ jaw_open=False,
204
+ dropping=False,
205
+ ),
206
+ GripperMovementWaypointsWithJawStatus(
207
+ position=drop_location, jaw_open=False, dropping=False
208
+ ),
209
+ # Gripper ungrips here
210
+ GripperMovementWaypointsWithJawStatus(
211
+ position=post_drop_home_pos,
212
+ jaw_open=True,
213
+ dropping=True,
214
+ ),
215
+ ]
@@ -29,7 +29,7 @@ from .module_contexts import (
29
29
  AbsorbanceReaderContext,
30
30
  )
31
31
  from .disposal_locations import TrashBin, WasteChute
32
- from ._liquid import Liquid
32
+ from ._liquid import Liquid, LiquidClass
33
33
  from ._types import OFF_DECK
34
34
  from ._nozzle_layout import (
35
35
  COLUMN,
@@ -67,6 +67,7 @@ __all__ = [
67
67
  "WasteChute",
68
68
  "Well",
69
69
  "Liquid",
70
+ "LiquidClass",
70
71
  "Parameters",
71
72
  "COLUMN",
72
73
  "PARTIAL_COLUMN",