opentrons 8.6.0a1__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.

Potentially problematic release.


This version of opentrons might be problematic. Click here for more details.

Files changed (600) hide show
  1. opentrons/__init__.py +150 -0
  2. opentrons/_version.py +34 -0
  3. opentrons/calibration_storage/__init__.py +54 -0
  4. opentrons/calibration_storage/deck_configuration.py +62 -0
  5. opentrons/calibration_storage/encoder_decoder.py +31 -0
  6. opentrons/calibration_storage/file_operators.py +142 -0
  7. opentrons/calibration_storage/helpers.py +103 -0
  8. opentrons/calibration_storage/ot2/__init__.py +34 -0
  9. opentrons/calibration_storage/ot2/deck_attitude.py +85 -0
  10. opentrons/calibration_storage/ot2/mark_bad_calibration.py +27 -0
  11. opentrons/calibration_storage/ot2/models/__init__.py +0 -0
  12. opentrons/calibration_storage/ot2/models/v1.py +149 -0
  13. opentrons/calibration_storage/ot2/pipette_offset.py +129 -0
  14. opentrons/calibration_storage/ot2/tip_length.py +281 -0
  15. opentrons/calibration_storage/ot3/__init__.py +31 -0
  16. opentrons/calibration_storage/ot3/deck_attitude.py +83 -0
  17. opentrons/calibration_storage/ot3/gripper_offset.py +156 -0
  18. opentrons/calibration_storage/ot3/models/__init__.py +0 -0
  19. opentrons/calibration_storage/ot3/models/v1.py +122 -0
  20. opentrons/calibration_storage/ot3/module_offset.py +138 -0
  21. opentrons/calibration_storage/ot3/pipette_offset.py +95 -0
  22. opentrons/calibration_storage/types.py +45 -0
  23. opentrons/cli/__init__.py +21 -0
  24. opentrons/cli/__main__.py +5 -0
  25. opentrons/cli/analyze.py +501 -0
  26. opentrons/config/__init__.py +631 -0
  27. opentrons/config/advanced_settings.py +871 -0
  28. opentrons/config/defaults_ot2.py +214 -0
  29. opentrons/config/defaults_ot3.py +499 -0
  30. opentrons/config/feature_flags.py +86 -0
  31. opentrons/config/gripper_config.py +55 -0
  32. opentrons/config/reset.py +203 -0
  33. opentrons/config/robot_configs.py +187 -0
  34. opentrons/config/types.py +183 -0
  35. opentrons/drivers/__init__.py +0 -0
  36. opentrons/drivers/absorbance_reader/__init__.py +11 -0
  37. opentrons/drivers/absorbance_reader/abstract.py +72 -0
  38. opentrons/drivers/absorbance_reader/async_byonoy.py +352 -0
  39. opentrons/drivers/absorbance_reader/driver.py +81 -0
  40. opentrons/drivers/absorbance_reader/hid_protocol.py +161 -0
  41. opentrons/drivers/absorbance_reader/simulator.py +84 -0
  42. opentrons/drivers/asyncio/__init__.py +0 -0
  43. opentrons/drivers/asyncio/communication/__init__.py +22 -0
  44. opentrons/drivers/asyncio/communication/async_serial.py +183 -0
  45. opentrons/drivers/asyncio/communication/errors.py +88 -0
  46. opentrons/drivers/asyncio/communication/serial_connection.py +552 -0
  47. opentrons/drivers/command_builder.py +102 -0
  48. opentrons/drivers/flex_stacker/__init__.py +13 -0
  49. opentrons/drivers/flex_stacker/abstract.py +214 -0
  50. opentrons/drivers/flex_stacker/driver.py +768 -0
  51. opentrons/drivers/flex_stacker/errors.py +68 -0
  52. opentrons/drivers/flex_stacker/simulator.py +309 -0
  53. opentrons/drivers/flex_stacker/types.py +367 -0
  54. opentrons/drivers/flex_stacker/utils.py +19 -0
  55. opentrons/drivers/heater_shaker/__init__.py +5 -0
  56. opentrons/drivers/heater_shaker/abstract.py +76 -0
  57. opentrons/drivers/heater_shaker/driver.py +204 -0
  58. opentrons/drivers/heater_shaker/simulator.py +94 -0
  59. opentrons/drivers/mag_deck/__init__.py +6 -0
  60. opentrons/drivers/mag_deck/abstract.py +44 -0
  61. opentrons/drivers/mag_deck/driver.py +208 -0
  62. opentrons/drivers/mag_deck/simulator.py +63 -0
  63. opentrons/drivers/rpi_drivers/__init__.py +33 -0
  64. opentrons/drivers/rpi_drivers/dev_types.py +94 -0
  65. opentrons/drivers/rpi_drivers/gpio.py +282 -0
  66. opentrons/drivers/rpi_drivers/gpio_simulator.py +127 -0
  67. opentrons/drivers/rpi_drivers/interfaces.py +15 -0
  68. opentrons/drivers/rpi_drivers/types.py +364 -0
  69. opentrons/drivers/rpi_drivers/usb.py +102 -0
  70. opentrons/drivers/rpi_drivers/usb_simulator.py +22 -0
  71. opentrons/drivers/serial_communication.py +151 -0
  72. opentrons/drivers/smoothie_drivers/__init__.py +4 -0
  73. opentrons/drivers/smoothie_drivers/connection.py +51 -0
  74. opentrons/drivers/smoothie_drivers/constants.py +121 -0
  75. opentrons/drivers/smoothie_drivers/driver_3_0.py +1933 -0
  76. opentrons/drivers/smoothie_drivers/errors.py +49 -0
  77. opentrons/drivers/smoothie_drivers/parse_utils.py +143 -0
  78. opentrons/drivers/smoothie_drivers/simulator.py +99 -0
  79. opentrons/drivers/smoothie_drivers/types.py +16 -0
  80. opentrons/drivers/temp_deck/__init__.py +10 -0
  81. opentrons/drivers/temp_deck/abstract.py +54 -0
  82. opentrons/drivers/temp_deck/driver.py +197 -0
  83. opentrons/drivers/temp_deck/simulator.py +57 -0
  84. opentrons/drivers/thermocycler/__init__.py +12 -0
  85. opentrons/drivers/thermocycler/abstract.py +99 -0
  86. opentrons/drivers/thermocycler/driver.py +395 -0
  87. opentrons/drivers/thermocycler/simulator.py +126 -0
  88. opentrons/drivers/types.py +107 -0
  89. opentrons/drivers/utils.py +222 -0
  90. opentrons/execute.py +742 -0
  91. opentrons/hardware_control/__init__.py +65 -0
  92. opentrons/hardware_control/__main__.py +77 -0
  93. opentrons/hardware_control/adapters.py +98 -0
  94. opentrons/hardware_control/api.py +1347 -0
  95. opentrons/hardware_control/backends/__init__.py +7 -0
  96. opentrons/hardware_control/backends/controller.py +400 -0
  97. opentrons/hardware_control/backends/errors.py +9 -0
  98. opentrons/hardware_control/backends/estop_state.py +164 -0
  99. opentrons/hardware_control/backends/flex_protocol.py +497 -0
  100. opentrons/hardware_control/backends/ot3controller.py +1930 -0
  101. opentrons/hardware_control/backends/ot3simulator.py +900 -0
  102. opentrons/hardware_control/backends/ot3utils.py +664 -0
  103. opentrons/hardware_control/backends/simulator.py +442 -0
  104. opentrons/hardware_control/backends/status_bar_state.py +240 -0
  105. opentrons/hardware_control/backends/subsystem_manager.py +431 -0
  106. opentrons/hardware_control/backends/tip_presence_manager.py +173 -0
  107. opentrons/hardware_control/backends/types.py +14 -0
  108. opentrons/hardware_control/constants.py +6 -0
  109. opentrons/hardware_control/dev_types.py +125 -0
  110. opentrons/hardware_control/emulation/__init__.py +0 -0
  111. opentrons/hardware_control/emulation/abstract_emulator.py +21 -0
  112. opentrons/hardware_control/emulation/app.py +56 -0
  113. opentrons/hardware_control/emulation/connection_handler.py +38 -0
  114. opentrons/hardware_control/emulation/heater_shaker.py +150 -0
  115. opentrons/hardware_control/emulation/magdeck.py +60 -0
  116. opentrons/hardware_control/emulation/module_server/__init__.py +8 -0
  117. opentrons/hardware_control/emulation/module_server/client.py +78 -0
  118. opentrons/hardware_control/emulation/module_server/helpers.py +130 -0
  119. opentrons/hardware_control/emulation/module_server/models.py +31 -0
  120. opentrons/hardware_control/emulation/module_server/server.py +110 -0
  121. opentrons/hardware_control/emulation/parser.py +74 -0
  122. opentrons/hardware_control/emulation/proxy.py +241 -0
  123. opentrons/hardware_control/emulation/run_emulator.py +68 -0
  124. opentrons/hardware_control/emulation/scripts/__init__.py +0 -0
  125. opentrons/hardware_control/emulation/scripts/run_app.py +54 -0
  126. opentrons/hardware_control/emulation/scripts/run_module_emulator.py +72 -0
  127. opentrons/hardware_control/emulation/scripts/run_smoothie.py +37 -0
  128. opentrons/hardware_control/emulation/settings.py +119 -0
  129. opentrons/hardware_control/emulation/simulations.py +133 -0
  130. opentrons/hardware_control/emulation/smoothie.py +192 -0
  131. opentrons/hardware_control/emulation/tempdeck.py +69 -0
  132. opentrons/hardware_control/emulation/thermocycler.py +128 -0
  133. opentrons/hardware_control/emulation/types.py +10 -0
  134. opentrons/hardware_control/emulation/util.py +38 -0
  135. opentrons/hardware_control/errors.py +43 -0
  136. opentrons/hardware_control/execution_manager.py +164 -0
  137. opentrons/hardware_control/instruments/__init__.py +5 -0
  138. opentrons/hardware_control/instruments/instrument_abc.py +39 -0
  139. opentrons/hardware_control/instruments/ot2/__init__.py +0 -0
  140. opentrons/hardware_control/instruments/ot2/instrument_calibration.py +152 -0
  141. opentrons/hardware_control/instruments/ot2/pipette.py +777 -0
  142. opentrons/hardware_control/instruments/ot2/pipette_handler.py +995 -0
  143. opentrons/hardware_control/instruments/ot3/__init__.py +0 -0
  144. opentrons/hardware_control/instruments/ot3/gripper.py +420 -0
  145. opentrons/hardware_control/instruments/ot3/gripper_handler.py +173 -0
  146. opentrons/hardware_control/instruments/ot3/instrument_calibration.py +214 -0
  147. opentrons/hardware_control/instruments/ot3/pipette.py +858 -0
  148. opentrons/hardware_control/instruments/ot3/pipette_handler.py +1030 -0
  149. opentrons/hardware_control/module_control.py +332 -0
  150. opentrons/hardware_control/modules/__init__.py +69 -0
  151. opentrons/hardware_control/modules/absorbance_reader.py +373 -0
  152. opentrons/hardware_control/modules/errors.py +7 -0
  153. opentrons/hardware_control/modules/flex_stacker.py +948 -0
  154. opentrons/hardware_control/modules/heater_shaker.py +426 -0
  155. opentrons/hardware_control/modules/lid_temp_status.py +35 -0
  156. opentrons/hardware_control/modules/magdeck.py +233 -0
  157. opentrons/hardware_control/modules/mod_abc.py +245 -0
  158. opentrons/hardware_control/modules/module_calibration.py +93 -0
  159. opentrons/hardware_control/modules/plate_temp_status.py +61 -0
  160. opentrons/hardware_control/modules/tempdeck.py +299 -0
  161. opentrons/hardware_control/modules/thermocycler.py +731 -0
  162. opentrons/hardware_control/modules/types.py +417 -0
  163. opentrons/hardware_control/modules/update.py +255 -0
  164. opentrons/hardware_control/modules/utils.py +73 -0
  165. opentrons/hardware_control/motion_utilities.py +318 -0
  166. opentrons/hardware_control/nozzle_manager.py +422 -0
  167. opentrons/hardware_control/ot3_calibration.py +1171 -0
  168. opentrons/hardware_control/ot3api.py +3227 -0
  169. opentrons/hardware_control/pause_manager.py +31 -0
  170. opentrons/hardware_control/poller.py +112 -0
  171. opentrons/hardware_control/protocols/__init__.py +106 -0
  172. opentrons/hardware_control/protocols/asyncio_configurable.py +11 -0
  173. opentrons/hardware_control/protocols/calibratable.py +45 -0
  174. opentrons/hardware_control/protocols/chassis_accessory_manager.py +90 -0
  175. opentrons/hardware_control/protocols/configurable.py +48 -0
  176. opentrons/hardware_control/protocols/event_sourcer.py +18 -0
  177. opentrons/hardware_control/protocols/execution_controllable.py +33 -0
  178. opentrons/hardware_control/protocols/flex_calibratable.py +96 -0
  179. opentrons/hardware_control/protocols/flex_instrument_configurer.py +52 -0
  180. opentrons/hardware_control/protocols/gripper_controller.py +55 -0
  181. opentrons/hardware_control/protocols/hardware_manager.py +51 -0
  182. opentrons/hardware_control/protocols/identifiable.py +16 -0
  183. opentrons/hardware_control/protocols/instrument_configurer.py +206 -0
  184. opentrons/hardware_control/protocols/liquid_handler.py +266 -0
  185. opentrons/hardware_control/protocols/module_provider.py +16 -0
  186. opentrons/hardware_control/protocols/motion_controller.py +243 -0
  187. opentrons/hardware_control/protocols/position_estimator.py +45 -0
  188. opentrons/hardware_control/protocols/simulatable.py +10 -0
  189. opentrons/hardware_control/protocols/stoppable.py +9 -0
  190. opentrons/hardware_control/protocols/types.py +27 -0
  191. opentrons/hardware_control/robot_calibration.py +224 -0
  192. opentrons/hardware_control/scripts/README.md +28 -0
  193. opentrons/hardware_control/scripts/__init__.py +1 -0
  194. opentrons/hardware_control/scripts/gripper_control.py +208 -0
  195. opentrons/hardware_control/scripts/ot3gripper +7 -0
  196. opentrons/hardware_control/scripts/ot3repl +7 -0
  197. opentrons/hardware_control/scripts/repl.py +187 -0
  198. opentrons/hardware_control/scripts/tc_control.py +97 -0
  199. opentrons/hardware_control/simulator_setup.py +260 -0
  200. opentrons/hardware_control/thread_manager.py +431 -0
  201. opentrons/hardware_control/threaded_async_lock.py +97 -0
  202. opentrons/hardware_control/types.py +792 -0
  203. opentrons/hardware_control/util.py +234 -0
  204. opentrons/legacy_broker.py +53 -0
  205. opentrons/legacy_commands/__init__.py +1 -0
  206. opentrons/legacy_commands/commands.py +483 -0
  207. opentrons/legacy_commands/helpers.py +153 -0
  208. opentrons/legacy_commands/module_commands.py +215 -0
  209. opentrons/legacy_commands/protocol_commands.py +54 -0
  210. opentrons/legacy_commands/publisher.py +155 -0
  211. opentrons/legacy_commands/robot_commands.py +51 -0
  212. opentrons/legacy_commands/types.py +1115 -0
  213. opentrons/motion_planning/__init__.py +32 -0
  214. opentrons/motion_planning/adjacent_slots_getters.py +168 -0
  215. opentrons/motion_planning/deck_conflict.py +396 -0
  216. opentrons/motion_planning/errors.py +35 -0
  217. opentrons/motion_planning/types.py +42 -0
  218. opentrons/motion_planning/waypoints.py +218 -0
  219. opentrons/ordered_set.py +138 -0
  220. opentrons/protocol_api/__init__.py +105 -0
  221. opentrons/protocol_api/_liquid.py +157 -0
  222. opentrons/protocol_api/_liquid_properties.py +814 -0
  223. opentrons/protocol_api/_nozzle_layout.py +31 -0
  224. opentrons/protocol_api/_parameter_context.py +300 -0
  225. opentrons/protocol_api/_parameters.py +31 -0
  226. opentrons/protocol_api/_transfer_liquid_validation.py +108 -0
  227. opentrons/protocol_api/_types.py +43 -0
  228. opentrons/protocol_api/config.py +23 -0
  229. opentrons/protocol_api/core/__init__.py +23 -0
  230. opentrons/protocol_api/core/common.py +33 -0
  231. opentrons/protocol_api/core/core_map.py +74 -0
  232. opentrons/protocol_api/core/engine/__init__.py +22 -0
  233. opentrons/protocol_api/core/engine/_default_labware_versions.py +179 -0
  234. opentrons/protocol_api/core/engine/deck_conflict.py +348 -0
  235. opentrons/protocol_api/core/engine/exceptions.py +19 -0
  236. opentrons/protocol_api/core/engine/instrument.py +2391 -0
  237. opentrons/protocol_api/core/engine/labware.py +238 -0
  238. opentrons/protocol_api/core/engine/load_labware_params.py +73 -0
  239. opentrons/protocol_api/core/engine/module_core.py +1025 -0
  240. opentrons/protocol_api/core/engine/overlap_versions.py +20 -0
  241. opentrons/protocol_api/core/engine/pipette_movement_conflict.py +358 -0
  242. opentrons/protocol_api/core/engine/point_calculations.py +64 -0
  243. opentrons/protocol_api/core/engine/protocol.py +1153 -0
  244. opentrons/protocol_api/core/engine/robot.py +139 -0
  245. opentrons/protocol_api/core/engine/stringify.py +74 -0
  246. opentrons/protocol_api/core/engine/transfer_components_executor.py +990 -0
  247. opentrons/protocol_api/core/engine/well.py +241 -0
  248. opentrons/protocol_api/core/instrument.py +459 -0
  249. opentrons/protocol_api/core/labware.py +151 -0
  250. opentrons/protocol_api/core/legacy/__init__.py +11 -0
  251. opentrons/protocol_api/core/legacy/_labware_geometry.py +37 -0
  252. opentrons/protocol_api/core/legacy/deck.py +369 -0
  253. opentrons/protocol_api/core/legacy/labware_offset_provider.py +108 -0
  254. opentrons/protocol_api/core/legacy/legacy_instrument_core.py +709 -0
  255. opentrons/protocol_api/core/legacy/legacy_labware_core.py +235 -0
  256. opentrons/protocol_api/core/legacy/legacy_module_core.py +592 -0
  257. opentrons/protocol_api/core/legacy/legacy_protocol_core.py +612 -0
  258. opentrons/protocol_api/core/legacy/legacy_well_core.py +162 -0
  259. opentrons/protocol_api/core/legacy/load_info.py +67 -0
  260. opentrons/protocol_api/core/legacy/module_geometry.py +547 -0
  261. opentrons/protocol_api/core/legacy/well_geometry.py +148 -0
  262. opentrons/protocol_api/core/legacy_simulator/__init__.py +16 -0
  263. opentrons/protocol_api/core/legacy_simulator/legacy_instrument_core.py +624 -0
  264. opentrons/protocol_api/core/legacy_simulator/legacy_protocol_core.py +85 -0
  265. opentrons/protocol_api/core/module.py +484 -0
  266. opentrons/protocol_api/core/protocol.py +311 -0
  267. opentrons/protocol_api/core/robot.py +51 -0
  268. opentrons/protocol_api/core/well.py +116 -0
  269. opentrons/protocol_api/core/well_grid.py +45 -0
  270. opentrons/protocol_api/create_protocol_context.py +177 -0
  271. opentrons/protocol_api/deck.py +223 -0
  272. opentrons/protocol_api/disposal_locations.py +244 -0
  273. opentrons/protocol_api/instrument_context.py +3212 -0
  274. opentrons/protocol_api/labware.py +1579 -0
  275. opentrons/protocol_api/module_contexts.py +1425 -0
  276. opentrons/protocol_api/module_validation_and_errors.py +61 -0
  277. opentrons/protocol_api/protocol_context.py +1688 -0
  278. opentrons/protocol_api/robot_context.py +303 -0
  279. opentrons/protocol_api/validation.py +761 -0
  280. opentrons/protocol_engine/__init__.py +155 -0
  281. opentrons/protocol_engine/actions/__init__.py +65 -0
  282. opentrons/protocol_engine/actions/action_dispatcher.py +30 -0
  283. opentrons/protocol_engine/actions/action_handler.py +13 -0
  284. opentrons/protocol_engine/actions/actions.py +302 -0
  285. opentrons/protocol_engine/actions/get_state_update.py +38 -0
  286. opentrons/protocol_engine/clients/__init__.py +5 -0
  287. opentrons/protocol_engine/clients/sync_client.py +174 -0
  288. opentrons/protocol_engine/clients/transports.py +197 -0
  289. opentrons/protocol_engine/commands/__init__.py +757 -0
  290. opentrons/protocol_engine/commands/absorbance_reader/__init__.py +61 -0
  291. opentrons/protocol_engine/commands/absorbance_reader/close_lid.py +154 -0
  292. opentrons/protocol_engine/commands/absorbance_reader/common.py +6 -0
  293. opentrons/protocol_engine/commands/absorbance_reader/initialize.py +151 -0
  294. opentrons/protocol_engine/commands/absorbance_reader/open_lid.py +154 -0
  295. opentrons/protocol_engine/commands/absorbance_reader/read.py +226 -0
  296. opentrons/protocol_engine/commands/air_gap_in_place.py +162 -0
  297. opentrons/protocol_engine/commands/aspirate.py +244 -0
  298. opentrons/protocol_engine/commands/aspirate_in_place.py +184 -0
  299. opentrons/protocol_engine/commands/aspirate_while_tracking.py +211 -0
  300. opentrons/protocol_engine/commands/blow_out.py +146 -0
  301. opentrons/protocol_engine/commands/blow_out_in_place.py +119 -0
  302. opentrons/protocol_engine/commands/calibration/__init__.py +60 -0
  303. opentrons/protocol_engine/commands/calibration/calibrate_gripper.py +166 -0
  304. opentrons/protocol_engine/commands/calibration/calibrate_module.py +117 -0
  305. opentrons/protocol_engine/commands/calibration/calibrate_pipette.py +96 -0
  306. opentrons/protocol_engine/commands/calibration/move_to_maintenance_position.py +156 -0
  307. opentrons/protocol_engine/commands/command.py +308 -0
  308. opentrons/protocol_engine/commands/command_unions.py +974 -0
  309. opentrons/protocol_engine/commands/comment.py +57 -0
  310. opentrons/protocol_engine/commands/configure_for_volume.py +108 -0
  311. opentrons/protocol_engine/commands/configure_nozzle_layout.py +115 -0
  312. opentrons/protocol_engine/commands/custom.py +67 -0
  313. opentrons/protocol_engine/commands/dispense.py +194 -0
  314. opentrons/protocol_engine/commands/dispense_in_place.py +179 -0
  315. opentrons/protocol_engine/commands/dispense_while_tracking.py +204 -0
  316. opentrons/protocol_engine/commands/drop_tip.py +232 -0
  317. opentrons/protocol_engine/commands/drop_tip_in_place.py +205 -0
  318. opentrons/protocol_engine/commands/flex_stacker/__init__.py +64 -0
  319. opentrons/protocol_engine/commands/flex_stacker/common.py +900 -0
  320. opentrons/protocol_engine/commands/flex_stacker/empty.py +293 -0
  321. opentrons/protocol_engine/commands/flex_stacker/fill.py +281 -0
  322. opentrons/protocol_engine/commands/flex_stacker/retrieve.py +339 -0
  323. opentrons/protocol_engine/commands/flex_stacker/set_stored_labware.py +328 -0
  324. opentrons/protocol_engine/commands/flex_stacker/store.py +326 -0
  325. opentrons/protocol_engine/commands/generate_command_schema.py +61 -0
  326. opentrons/protocol_engine/commands/get_next_tip.py +134 -0
  327. opentrons/protocol_engine/commands/get_tip_presence.py +87 -0
  328. opentrons/protocol_engine/commands/hash_command_params.py +38 -0
  329. opentrons/protocol_engine/commands/heater_shaker/__init__.py +102 -0
  330. opentrons/protocol_engine/commands/heater_shaker/close_labware_latch.py +83 -0
  331. opentrons/protocol_engine/commands/heater_shaker/deactivate_heater.py +82 -0
  332. opentrons/protocol_engine/commands/heater_shaker/deactivate_shaker.py +84 -0
  333. opentrons/protocol_engine/commands/heater_shaker/open_labware_latch.py +110 -0
  334. opentrons/protocol_engine/commands/heater_shaker/set_and_wait_for_shake_speed.py +125 -0
  335. opentrons/protocol_engine/commands/heater_shaker/set_target_temperature.py +90 -0
  336. opentrons/protocol_engine/commands/heater_shaker/wait_for_temperature.py +102 -0
  337. opentrons/protocol_engine/commands/home.py +100 -0
  338. opentrons/protocol_engine/commands/identify_module.py +86 -0
  339. opentrons/protocol_engine/commands/labware_handling_common.py +29 -0
  340. opentrons/protocol_engine/commands/liquid_probe.py +464 -0
  341. opentrons/protocol_engine/commands/load_labware.py +210 -0
  342. opentrons/protocol_engine/commands/load_lid.py +154 -0
  343. opentrons/protocol_engine/commands/load_lid_stack.py +272 -0
  344. opentrons/protocol_engine/commands/load_liquid.py +95 -0
  345. opentrons/protocol_engine/commands/load_liquid_class.py +144 -0
  346. opentrons/protocol_engine/commands/load_module.py +223 -0
  347. opentrons/protocol_engine/commands/load_pipette.py +167 -0
  348. opentrons/protocol_engine/commands/magnetic_module/__init__.py +32 -0
  349. opentrons/protocol_engine/commands/magnetic_module/disengage.py +97 -0
  350. opentrons/protocol_engine/commands/magnetic_module/engage.py +119 -0
  351. opentrons/protocol_engine/commands/move_labware.py +546 -0
  352. opentrons/protocol_engine/commands/move_relative.py +102 -0
  353. opentrons/protocol_engine/commands/move_to_addressable_area.py +176 -0
  354. opentrons/protocol_engine/commands/move_to_addressable_area_for_drop_tip.py +198 -0
  355. opentrons/protocol_engine/commands/move_to_coordinates.py +107 -0
  356. opentrons/protocol_engine/commands/move_to_well.py +119 -0
  357. opentrons/protocol_engine/commands/movement_common.py +338 -0
  358. opentrons/protocol_engine/commands/pick_up_tip.py +241 -0
  359. opentrons/protocol_engine/commands/pipetting_common.py +443 -0
  360. opentrons/protocol_engine/commands/prepare_to_aspirate.py +121 -0
  361. opentrons/protocol_engine/commands/pressure_dispense.py +155 -0
  362. opentrons/protocol_engine/commands/reload_labware.py +90 -0
  363. opentrons/protocol_engine/commands/retract_axis.py +75 -0
  364. opentrons/protocol_engine/commands/robot/__init__.py +70 -0
  365. opentrons/protocol_engine/commands/robot/close_gripper_jaw.py +96 -0
  366. opentrons/protocol_engine/commands/robot/common.py +18 -0
  367. opentrons/protocol_engine/commands/robot/move_axes_relative.py +101 -0
  368. opentrons/protocol_engine/commands/robot/move_axes_to.py +100 -0
  369. opentrons/protocol_engine/commands/robot/move_to.py +94 -0
  370. opentrons/protocol_engine/commands/robot/open_gripper_jaw.py +86 -0
  371. opentrons/protocol_engine/commands/save_position.py +109 -0
  372. opentrons/protocol_engine/commands/seal_pipette_to_tip.py +353 -0
  373. opentrons/protocol_engine/commands/set_rail_lights.py +67 -0
  374. opentrons/protocol_engine/commands/set_status_bar.py +89 -0
  375. opentrons/protocol_engine/commands/temperature_module/__init__.py +46 -0
  376. opentrons/protocol_engine/commands/temperature_module/deactivate.py +86 -0
  377. opentrons/protocol_engine/commands/temperature_module/set_target_temperature.py +97 -0
  378. opentrons/protocol_engine/commands/temperature_module/wait_for_temperature.py +104 -0
  379. opentrons/protocol_engine/commands/thermocycler/__init__.py +152 -0
  380. opentrons/protocol_engine/commands/thermocycler/close_lid.py +87 -0
  381. opentrons/protocol_engine/commands/thermocycler/deactivate_block.py +80 -0
  382. opentrons/protocol_engine/commands/thermocycler/deactivate_lid.py +80 -0
  383. opentrons/protocol_engine/commands/thermocycler/open_lid.py +87 -0
  384. opentrons/protocol_engine/commands/thermocycler/run_extended_profile.py +171 -0
  385. opentrons/protocol_engine/commands/thermocycler/run_profile.py +124 -0
  386. opentrons/protocol_engine/commands/thermocycler/set_target_block_temperature.py +140 -0
  387. opentrons/protocol_engine/commands/thermocycler/set_target_lid_temperature.py +100 -0
  388. opentrons/protocol_engine/commands/thermocycler/wait_for_block_temperature.py +93 -0
  389. opentrons/protocol_engine/commands/thermocycler/wait_for_lid_temperature.py +89 -0
  390. opentrons/protocol_engine/commands/touch_tip.py +189 -0
  391. opentrons/protocol_engine/commands/unsafe/__init__.py +161 -0
  392. opentrons/protocol_engine/commands/unsafe/unsafe_blow_out_in_place.py +100 -0
  393. opentrons/protocol_engine/commands/unsafe/unsafe_drop_tip_in_place.py +121 -0
  394. opentrons/protocol_engine/commands/unsafe/unsafe_engage_axes.py +82 -0
  395. opentrons/protocol_engine/commands/unsafe/unsafe_place_labware.py +208 -0
  396. opentrons/protocol_engine/commands/unsafe/unsafe_stacker_close_latch.py +94 -0
  397. opentrons/protocol_engine/commands/unsafe/unsafe_stacker_manual_retrieve.py +295 -0
  398. opentrons/protocol_engine/commands/unsafe/unsafe_stacker_open_latch.py +91 -0
  399. opentrons/protocol_engine/commands/unsafe/unsafe_stacker_prepare_shuttle.py +136 -0
  400. opentrons/protocol_engine/commands/unsafe/unsafe_ungrip_labware.py +77 -0
  401. opentrons/protocol_engine/commands/unsafe/update_position_estimators.py +90 -0
  402. opentrons/protocol_engine/commands/unseal_pipette_from_tip.py +153 -0
  403. opentrons/protocol_engine/commands/verify_tip_presence.py +100 -0
  404. opentrons/protocol_engine/commands/wait_for_duration.py +76 -0
  405. opentrons/protocol_engine/commands/wait_for_resume.py +75 -0
  406. opentrons/protocol_engine/create_protocol_engine.py +193 -0
  407. opentrons/protocol_engine/engine_support.py +28 -0
  408. opentrons/protocol_engine/error_recovery_policy.py +81 -0
  409. opentrons/protocol_engine/errors/__init__.py +191 -0
  410. opentrons/protocol_engine/errors/error_occurrence.py +182 -0
  411. opentrons/protocol_engine/errors/exceptions.py +1308 -0
  412. opentrons/protocol_engine/execution/__init__.py +50 -0
  413. opentrons/protocol_engine/execution/command_executor.py +216 -0
  414. opentrons/protocol_engine/execution/create_queue_worker.py +102 -0
  415. opentrons/protocol_engine/execution/door_watcher.py +119 -0
  416. opentrons/protocol_engine/execution/equipment.py +819 -0
  417. opentrons/protocol_engine/execution/error_recovery_hardware_state_synchronizer.py +101 -0
  418. opentrons/protocol_engine/execution/gantry_mover.py +686 -0
  419. opentrons/protocol_engine/execution/hardware_stopper.py +147 -0
  420. opentrons/protocol_engine/execution/heater_shaker_movement_flagger.py +207 -0
  421. opentrons/protocol_engine/execution/labware_movement.py +297 -0
  422. opentrons/protocol_engine/execution/movement.py +349 -0
  423. opentrons/protocol_engine/execution/pipetting.py +607 -0
  424. opentrons/protocol_engine/execution/queue_worker.py +86 -0
  425. opentrons/protocol_engine/execution/rail_lights.py +25 -0
  426. opentrons/protocol_engine/execution/run_control.py +33 -0
  427. opentrons/protocol_engine/execution/status_bar.py +34 -0
  428. opentrons/protocol_engine/execution/thermocycler_movement_flagger.py +188 -0
  429. opentrons/protocol_engine/execution/thermocycler_plate_lifter.py +81 -0
  430. opentrons/protocol_engine/execution/tip_handler.py +550 -0
  431. opentrons/protocol_engine/labware_offset_standardization.py +194 -0
  432. opentrons/protocol_engine/notes/__init__.py +17 -0
  433. opentrons/protocol_engine/notes/notes.py +59 -0
  434. opentrons/protocol_engine/plugins.py +104 -0
  435. opentrons/protocol_engine/protocol_engine.py +683 -0
  436. opentrons/protocol_engine/resources/__init__.py +26 -0
  437. opentrons/protocol_engine/resources/deck_configuration_provider.py +232 -0
  438. opentrons/protocol_engine/resources/deck_data_provider.py +94 -0
  439. opentrons/protocol_engine/resources/file_provider.py +161 -0
  440. opentrons/protocol_engine/resources/fixture_validation.py +58 -0
  441. opentrons/protocol_engine/resources/labware_data_provider.py +106 -0
  442. opentrons/protocol_engine/resources/labware_validation.py +73 -0
  443. opentrons/protocol_engine/resources/model_utils.py +32 -0
  444. opentrons/protocol_engine/resources/module_data_provider.py +44 -0
  445. opentrons/protocol_engine/resources/ot3_validation.py +21 -0
  446. opentrons/protocol_engine/resources/pipette_data_provider.py +379 -0
  447. opentrons/protocol_engine/slot_standardization.py +128 -0
  448. opentrons/protocol_engine/state/__init__.py +1 -0
  449. opentrons/protocol_engine/state/_abstract_store.py +27 -0
  450. opentrons/protocol_engine/state/_axis_aligned_bounding_box.py +50 -0
  451. opentrons/protocol_engine/state/_labware_origin_math.py +636 -0
  452. opentrons/protocol_engine/state/_move_types.py +83 -0
  453. opentrons/protocol_engine/state/_well_math.py +193 -0
  454. opentrons/protocol_engine/state/addressable_areas.py +699 -0
  455. opentrons/protocol_engine/state/command_history.py +309 -0
  456. opentrons/protocol_engine/state/commands.py +1158 -0
  457. opentrons/protocol_engine/state/config.py +39 -0
  458. opentrons/protocol_engine/state/files.py +57 -0
  459. opentrons/protocol_engine/state/fluid_stack.py +138 -0
  460. opentrons/protocol_engine/state/geometry.py +2359 -0
  461. opentrons/protocol_engine/state/inner_well_math_utils.py +548 -0
  462. opentrons/protocol_engine/state/labware.py +1459 -0
  463. opentrons/protocol_engine/state/liquid_classes.py +82 -0
  464. opentrons/protocol_engine/state/liquids.py +73 -0
  465. opentrons/protocol_engine/state/module_substates/__init__.py +45 -0
  466. opentrons/protocol_engine/state/module_substates/absorbance_reader_substate.py +35 -0
  467. opentrons/protocol_engine/state/module_substates/flex_stacker_substate.py +112 -0
  468. opentrons/protocol_engine/state/module_substates/heater_shaker_module_substate.py +115 -0
  469. opentrons/protocol_engine/state/module_substates/magnetic_block_substate.py +17 -0
  470. opentrons/protocol_engine/state/module_substates/magnetic_module_substate.py +65 -0
  471. opentrons/protocol_engine/state/module_substates/temperature_module_substate.py +67 -0
  472. opentrons/protocol_engine/state/module_substates/thermocycler_module_substate.py +163 -0
  473. opentrons/protocol_engine/state/modules.py +1500 -0
  474. opentrons/protocol_engine/state/motion.py +373 -0
  475. opentrons/protocol_engine/state/pipettes.py +905 -0
  476. opentrons/protocol_engine/state/state.py +421 -0
  477. opentrons/protocol_engine/state/state_summary.py +36 -0
  478. opentrons/protocol_engine/state/tips.py +420 -0
  479. opentrons/protocol_engine/state/update_types.py +904 -0
  480. opentrons/protocol_engine/state/wells.py +290 -0
  481. opentrons/protocol_engine/types/__init__.py +308 -0
  482. opentrons/protocol_engine/types/automatic_tip_selection.py +39 -0
  483. opentrons/protocol_engine/types/command_annotations.py +53 -0
  484. opentrons/protocol_engine/types/deck_configuration.py +81 -0
  485. opentrons/protocol_engine/types/execution.py +96 -0
  486. opentrons/protocol_engine/types/hardware_passthrough.py +25 -0
  487. opentrons/protocol_engine/types/instrument.py +47 -0
  488. opentrons/protocol_engine/types/instrument_sensors.py +47 -0
  489. opentrons/protocol_engine/types/labware.py +131 -0
  490. opentrons/protocol_engine/types/labware_movement.py +22 -0
  491. opentrons/protocol_engine/types/labware_offset_location.py +111 -0
  492. opentrons/protocol_engine/types/labware_offset_vector.py +16 -0
  493. opentrons/protocol_engine/types/liquid.py +40 -0
  494. opentrons/protocol_engine/types/liquid_class.py +59 -0
  495. opentrons/protocol_engine/types/liquid_handling.py +13 -0
  496. opentrons/protocol_engine/types/liquid_level_detection.py +191 -0
  497. opentrons/protocol_engine/types/location.py +194 -0
  498. opentrons/protocol_engine/types/module.py +303 -0
  499. opentrons/protocol_engine/types/partial_tip_configuration.py +76 -0
  500. opentrons/protocol_engine/types/run_time_parameters.py +133 -0
  501. opentrons/protocol_engine/types/tip.py +18 -0
  502. opentrons/protocol_engine/types/util.py +21 -0
  503. opentrons/protocol_engine/types/well_position.py +124 -0
  504. opentrons/protocol_reader/__init__.py +37 -0
  505. opentrons/protocol_reader/extract_labware_definitions.py +66 -0
  506. opentrons/protocol_reader/file_format_validator.py +152 -0
  507. opentrons/protocol_reader/file_hasher.py +27 -0
  508. opentrons/protocol_reader/file_identifier.py +284 -0
  509. opentrons/protocol_reader/file_reader_writer.py +90 -0
  510. opentrons/protocol_reader/input_file.py +16 -0
  511. opentrons/protocol_reader/protocol_files_invalid_error.py +6 -0
  512. opentrons/protocol_reader/protocol_reader.py +188 -0
  513. opentrons/protocol_reader/protocol_source.py +124 -0
  514. opentrons/protocol_reader/role_analyzer.py +86 -0
  515. opentrons/protocol_runner/__init__.py +26 -0
  516. opentrons/protocol_runner/create_simulating_orchestrator.py +118 -0
  517. opentrons/protocol_runner/json_file_reader.py +55 -0
  518. opentrons/protocol_runner/json_translator.py +314 -0
  519. opentrons/protocol_runner/legacy_command_mapper.py +848 -0
  520. opentrons/protocol_runner/legacy_context_plugin.py +116 -0
  521. opentrons/protocol_runner/protocol_runner.py +530 -0
  522. opentrons/protocol_runner/python_protocol_wrappers.py +179 -0
  523. opentrons/protocol_runner/run_orchestrator.py +496 -0
  524. opentrons/protocol_runner/task_queue.py +95 -0
  525. opentrons/protocols/__init__.py +6 -0
  526. opentrons/protocols/advanced_control/__init__.py +0 -0
  527. opentrons/protocols/advanced_control/common.py +38 -0
  528. opentrons/protocols/advanced_control/mix.py +60 -0
  529. opentrons/protocols/advanced_control/transfers/__init__.py +0 -0
  530. opentrons/protocols/advanced_control/transfers/common.py +180 -0
  531. opentrons/protocols/advanced_control/transfers/transfer.py +972 -0
  532. opentrons/protocols/advanced_control/transfers/transfer_liquid_utils.py +231 -0
  533. opentrons/protocols/api_support/__init__.py +0 -0
  534. opentrons/protocols/api_support/constants.py +8 -0
  535. opentrons/protocols/api_support/deck_type.py +110 -0
  536. opentrons/protocols/api_support/definitions.py +18 -0
  537. opentrons/protocols/api_support/instrument.py +151 -0
  538. opentrons/protocols/api_support/labware_like.py +233 -0
  539. opentrons/protocols/api_support/tip_tracker.py +175 -0
  540. opentrons/protocols/api_support/types.py +32 -0
  541. opentrons/protocols/api_support/util.py +403 -0
  542. opentrons/protocols/bundle.py +89 -0
  543. opentrons/protocols/duration/__init__.py +4 -0
  544. opentrons/protocols/duration/errors.py +5 -0
  545. opentrons/protocols/duration/estimator.py +628 -0
  546. opentrons/protocols/execution/__init__.py +0 -0
  547. opentrons/protocols/execution/dev_types.py +181 -0
  548. opentrons/protocols/execution/errors.py +40 -0
  549. opentrons/protocols/execution/execute.py +84 -0
  550. opentrons/protocols/execution/execute_json_v3.py +275 -0
  551. opentrons/protocols/execution/execute_json_v4.py +359 -0
  552. opentrons/protocols/execution/execute_json_v5.py +28 -0
  553. opentrons/protocols/execution/execute_python.py +169 -0
  554. opentrons/protocols/execution/json_dispatchers.py +87 -0
  555. opentrons/protocols/execution/types.py +7 -0
  556. opentrons/protocols/geometry/__init__.py +0 -0
  557. opentrons/protocols/geometry/planning.py +297 -0
  558. opentrons/protocols/labware.py +312 -0
  559. opentrons/protocols/models/__init__.py +0 -0
  560. opentrons/protocols/models/json_protocol.py +679 -0
  561. opentrons/protocols/parameters/__init__.py +0 -0
  562. opentrons/protocols/parameters/csv_parameter_definition.py +77 -0
  563. opentrons/protocols/parameters/csv_parameter_interface.py +96 -0
  564. opentrons/protocols/parameters/exceptions.py +34 -0
  565. opentrons/protocols/parameters/parameter_definition.py +272 -0
  566. opentrons/protocols/parameters/types.py +17 -0
  567. opentrons/protocols/parameters/validation.py +267 -0
  568. opentrons/protocols/parse.py +671 -0
  569. opentrons/protocols/types.py +159 -0
  570. opentrons/py.typed +0 -0
  571. opentrons/resources/scripts/lpc21isp +0 -0
  572. opentrons/resources/smoothie-edge-8414642.hex +23010 -0
  573. opentrons/simulate.py +1065 -0
  574. opentrons/system/__init__.py +6 -0
  575. opentrons/system/camera.py +51 -0
  576. opentrons/system/log_control.py +59 -0
  577. opentrons/system/nmcli.py +856 -0
  578. opentrons/system/resin.py +24 -0
  579. opentrons/system/smoothie_update.py +15 -0
  580. opentrons/system/wifi.py +204 -0
  581. opentrons/tools/__init__.py +0 -0
  582. opentrons/tools/args_handler.py +22 -0
  583. opentrons/tools/write_pipette_memory.py +157 -0
  584. opentrons/types.py +618 -0
  585. opentrons/util/__init__.py +1 -0
  586. opentrons/util/async_helpers.py +166 -0
  587. opentrons/util/broker.py +84 -0
  588. opentrons/util/change_notifier.py +47 -0
  589. opentrons/util/entrypoint_util.py +278 -0
  590. opentrons/util/get_union_elements.py +26 -0
  591. opentrons/util/helpers.py +6 -0
  592. opentrons/util/linal.py +178 -0
  593. opentrons/util/logging_config.py +265 -0
  594. opentrons/util/logging_queue_handler.py +61 -0
  595. opentrons/util/performance_helpers.py +157 -0
  596. opentrons-8.6.0a1.dist-info/METADATA +37 -0
  597. opentrons-8.6.0a1.dist-info/RECORD +600 -0
  598. opentrons-8.6.0a1.dist-info/WHEEL +4 -0
  599. opentrons-8.6.0a1.dist-info/entry_points.txt +3 -0
  600. opentrons-8.6.0a1.dist-info/licenses/LICENSE +202 -0
@@ -0,0 +1,3227 @@
1
+ import asyncio
2
+ from concurrent.futures import Future
3
+ import contextlib
4
+ from copy import deepcopy
5
+ from functools import partial, lru_cache, wraps
6
+ from dataclasses import replace
7
+ import logging
8
+ from collections import OrderedDict
9
+ from typing import (
10
+ AsyncIterator,
11
+ cast,
12
+ Callable,
13
+ Dict,
14
+ Union,
15
+ List,
16
+ Optional,
17
+ Sequence,
18
+ Set,
19
+ Any,
20
+ TypeVar,
21
+ Tuple,
22
+ Mapping,
23
+ Awaitable,
24
+ )
25
+ from opentrons.hardware_control.modules.module_calibration import (
26
+ ModuleCalibrationOffset,
27
+ )
28
+
29
+
30
+ from opentrons_shared_data.pipette.types import PipetteName, PipetteModelType
31
+ from opentrons_shared_data.pipette import (
32
+ pipette_load_name_conversions as pipette_load_name,
33
+ pipette_definition,
34
+ )
35
+ from opentrons_shared_data.robot.types import RobotType
36
+
37
+ from opentrons import types as top_types
38
+ from opentrons.config import robot_configs
39
+ from opentrons.config.types import (
40
+ RobotConfig,
41
+ OT3Config,
42
+ GantryLoad,
43
+ CapacitivePassSettings,
44
+ LiquidProbeSettings,
45
+ )
46
+ from opentrons.drivers.rpi_drivers.types import USBPort, PortGroup
47
+ from opentrons_shared_data.errors.exceptions import (
48
+ EnumeratedError,
49
+ PythonException,
50
+ PositionUnknownError,
51
+ GripperNotPresentError,
52
+ InvalidActuator,
53
+ FirmwareUpdateFailedError,
54
+ PipetteLiquidNotFoundError,
55
+ )
56
+
57
+ from .util import use_or_initialize_loop, check_motion_bounds
58
+
59
+ from .instruments.ot3.pipette import (
60
+ load_from_config_and_check_skip,
61
+ )
62
+ from .instruments.ot3.gripper import compare_gripper_config_and_check_skip, Gripper
63
+ from .instruments.ot3.instrument_calibration import (
64
+ GripperCalibrationOffset,
65
+ PipetteOffsetSummary,
66
+ )
67
+
68
+ from .execution_manager import ExecutionManagerProvider
69
+ from .pause_manager import PauseManager
70
+ from .module_control import AttachedModulesControl
71
+ from .types import (
72
+ CriticalPoint,
73
+ DoorState,
74
+ DoorStateNotification,
75
+ ErrorMessageNotification,
76
+ HardwareEvent,
77
+ HardwareEventHandler,
78
+ HardwareAction,
79
+ HepaFanState,
80
+ HepaUVState,
81
+ MotionChecks,
82
+ SubSystem,
83
+ PauseType,
84
+ Axis,
85
+ OT3AxisKind,
86
+ OT3Mount,
87
+ OT3AxisMap,
88
+ InstrumentProbeType,
89
+ GripperProbe,
90
+ UpdateStatus,
91
+ StatusBarState,
92
+ StatusBarUpdateListener,
93
+ StatusBarUpdateUnsubscriber,
94
+ SubSystemState,
95
+ TipStateType,
96
+ EstopOverallStatus,
97
+ EstopStateNotification,
98
+ EstopState,
99
+ HardwareFeatureFlags,
100
+ FailedTipStateCheck,
101
+ PipetteSensorResponseQueue,
102
+ TipScrapeType,
103
+ )
104
+ from .errors import (
105
+ UpdateOngoingError,
106
+ )
107
+ from . import modules
108
+ from .ot3_calibration import OT3Transforms, OT3RobotCalibrationProvider
109
+
110
+ from .protocols import FlexHardwareControlInterface
111
+
112
+ # TODO (lc 09/15/2022) We should update our pipette handler to reflect OT-3 properties
113
+ # in a follow-up PR.
114
+ from .instruments.ot3.pipette_handler import (
115
+ OT3PipetteHandler,
116
+ InstrumentsByMount,
117
+ TipActionSpec,
118
+ TipActionMoveSpec,
119
+ )
120
+ from .instruments.ot3.instrument_calibration import load_pipette_offset
121
+ from .instruments.ot3.gripper_handler import GripperHandler
122
+ from .instruments.ot3.instrument_calibration import (
123
+ load_gripper_calibration_offset,
124
+ )
125
+
126
+ from .motion_utilities import (
127
+ target_position_from_absolute,
128
+ target_position_from_relative,
129
+ target_position_from_plunger,
130
+ target_positions_from_plunger_tracking,
131
+ offset_for_mount,
132
+ deck_from_machine,
133
+ machine_from_deck,
134
+ machine_vector_from_deck_vector,
135
+ )
136
+
137
+ from .dev_types import (
138
+ AttachedGripper,
139
+ AttachedPipette,
140
+ PipetteDict,
141
+ PipetteStateDict,
142
+ InstrumentDict,
143
+ GripperDict,
144
+ )
145
+ from .backends.types import HWStopCondition
146
+ from .backends.flex_protocol import FlexBackend
147
+ from .backends.ot3simulator import OT3Simulator
148
+ from .backends.errors import SubsystemUpdating
149
+
150
+ mod_log = logging.getLogger(__name__)
151
+
152
+ AXES_IN_HOMING_ORDER: Tuple[Axis, Axis, Axis, Axis, Axis, Axis, Axis, Axis, Axis] = (
153
+ *Axis.ot3_mount_axes(),
154
+ Axis.X,
155
+ Axis.Y,
156
+ *Axis.pipette_axes(),
157
+ Axis.G,
158
+ Axis.Q,
159
+ )
160
+
161
+ Wrapped = TypeVar("Wrapped", bound=Callable[..., Awaitable[Any]])
162
+
163
+
164
+ def _adjust_high_throughput_z_current(func: Wrapped) -> Wrapped:
165
+ """
166
+ A decorator that temproarily and conditionally changes the active current (based on the axis input)
167
+ before a function is executed and the cleans up afterwards
168
+ """
169
+
170
+ # only home and retract should be wrappeed by this decorator
171
+ @wraps(func)
172
+ async def wrapper(self: Any, axis: Axis, *args: Any, **kwargs: Any) -> Any:
173
+ async with contextlib.AsyncExitStack() as stack:
174
+ if axis == Axis.Z_R and self.gantry_load in [
175
+ GantryLoad.HIGH_THROUGHPUT_1000,
176
+ GantryLoad.HIGH_THROUGHPUT_200,
177
+ ]:
178
+ await stack.enter_async_context(self._backend.restore_z_r_run_current())
179
+ return await func(self, axis, *args, **kwargs)
180
+
181
+ return cast(Wrapped, wrapper)
182
+
183
+
184
+ class OT3API(
185
+ ExecutionManagerProvider,
186
+ OT3RobotCalibrationProvider,
187
+ # This MUST be kept last in the inheritance list so that it is
188
+ # deprioritized in the method resolution order; otherwise, invocations
189
+ # of methods that are present in the protocol will call the (empty,
190
+ # do-nothing) methods in the protocol. This will happily make all the
191
+ # tests fail.
192
+ FlexHardwareControlInterface[
193
+ OT3Transforms, Union[top_types.Mount, OT3Mount], OT3Config
194
+ ],
195
+ ):
196
+ """This API is the primary interface to the hardware controller.
197
+
198
+ Because the hardware manager controls access to the system's hardware
199
+ as a whole, it is designed as a class of which only one should be
200
+ instantiated at a time. This class's methods should be the only method
201
+ of external access to the hardware. Each method may be minimal - it may
202
+ only delegate the call to another submodule of the hardware manager -
203
+ but its purpose is to be gathered here to provide a single interface.
204
+
205
+ This implements the protocols in opentrons.hardware_control.protocols,
206
+ and longer method docstrings may be found there. Docstrings for the
207
+ methods in this class only note where their behavior is different or
208
+ extended from that described in the protocol.
209
+ """
210
+
211
+ CLS_LOG = mod_log.getChild("OT3API")
212
+
213
+ def __init__(
214
+ self,
215
+ backend: FlexBackend,
216
+ loop: asyncio.AbstractEventLoop,
217
+ config: OT3Config,
218
+ feature_flags: HardwareFeatureFlags,
219
+ ) -> None:
220
+ """Initialize an API instance.
221
+
222
+ This should rarely be explicitly invoked by an external user; instead,
223
+ one of the factory methods build_hardware_controller or
224
+ build_hardware_simulator should be used.
225
+ """
226
+ self._log = self.CLS_LOG.getChild(str(id(self)))
227
+ self._config = config
228
+ self._backend = backend
229
+ self._loop = loop
230
+
231
+ def estop_cb(event: HardwareEvent) -> None:
232
+ self._update_estop_state(event)
233
+
234
+ self._feature_flags = feature_flags
235
+ backend.add_estop_callback(estop_cb)
236
+
237
+ self._callbacks: Set[HardwareEventHandler] = set()
238
+ # {'X': 0.0, 'Y': 0.0, 'Z': 0.0, 'A': 0.0, 'B': 0.0, 'C': 0.0}
239
+ self._current_position: OT3AxisMap[float] = {}
240
+ self._encoder_position: OT3AxisMap[float] = {}
241
+
242
+ self._last_moved_mount: Optional[OT3Mount] = None
243
+ # The motion lock synchronizes calls to long-running physical tasks
244
+ # involved in motion. This fixes issue where for instance a move()
245
+ # or home() call is in flight and something else calls
246
+ # current_position(), which will not be updated until the move() or
247
+ # home() call succeeds or fails.
248
+ self._motion_lock = asyncio.Lock()
249
+ self._door_state = DoorState.CLOSED
250
+ self._module_door_serial: str | None = None
251
+ self._pause_manager = PauseManager()
252
+ self._pipette_handler = OT3PipetteHandler({m: None for m in OT3Mount})
253
+ self._gripper_handler = GripperHandler(gripper=None)
254
+ self._gantry_load = GantryLoad.LOW_THROUGHPUT
255
+ self._configured_since_update = True
256
+ OT3RobotCalibrationProvider.__init__(self, self._config)
257
+ ExecutionManagerProvider.__init__(self, isinstance(backend, OT3Simulator))
258
+
259
+ def is_idle_mount(self, mount: Union[top_types.Mount, OT3Mount]) -> bool:
260
+ """Only the gripper mount or the 96-channel pipette mount would be idle
261
+ (disengaged).
262
+
263
+ If gripper mount is NOT the last moved mount, it's idle.
264
+ If a 96-channel pipette is attached, the mount is idle if it's not
265
+ the last moved mount.
266
+ """
267
+ realmount = OT3Mount.from_mount(mount)
268
+ if realmount == OT3Mount.GRIPPER or (
269
+ realmount == OT3Mount.LEFT
270
+ and self._gantry_load
271
+ in [GantryLoad.HIGH_THROUGHPUT_1000, GantryLoad.HIGH_THROUGHPUT_200]
272
+ ):
273
+ ax = Axis.by_mount(realmount)
274
+ if ax in self.engaged_axes.keys():
275
+ return not self.engaged_axes[ax]
276
+
277
+ return False
278
+
279
+ @property
280
+ def door_state(self) -> DoorState:
281
+ return self._door_state
282
+
283
+ @door_state.setter
284
+ def door_state(self, door_state: DoorState) -> None:
285
+ self._door_state = door_state
286
+
287
+ @property
288
+ def module_door_serial(self) -> str | None:
289
+ return self._module_door_serial
290
+
291
+ @module_door_serial.setter
292
+ def module_door_serial(self, module_serial: str | None = None) -> None:
293
+ self._module_door_serial = module_serial
294
+
295
+ @property
296
+ def gantry_load(self) -> GantryLoad:
297
+ return self._gantry_load
298
+
299
+ async def set_gantry_load(self, gantry_load: GantryLoad) -> None:
300
+ mod_log.info(f"Setting gantry load to {gantry_load}")
301
+ self._gantry_load = gantry_load
302
+ self._backend.update_constraints_for_gantry_load(gantry_load)
303
+ await self._backend.update_to_default_current_settings(gantry_load)
304
+
305
+ async def get_serial_number(self) -> Optional[str]:
306
+ return await self._backend.get_serial_number()
307
+
308
+ async def set_system_constraints_for_plunger_acceleration(
309
+ self, mount: OT3Mount, acceleration: float
310
+ ) -> None:
311
+ high_speed_pipette = self._pipette_handler.get_pipette(
312
+ mount
313
+ ).is_high_speed_pipette()
314
+ self._backend.update_constraints_for_plunger_acceleration(
315
+ mount, acceleration, self._gantry_load, high_speed_pipette
316
+ )
317
+
318
+ @contextlib.asynccontextmanager
319
+ async def restore_system_constrants(self) -> AsyncIterator[None]:
320
+ async with self._backend.restore_system_constraints():
321
+ yield
322
+
323
+ @contextlib.asynccontextmanager
324
+ async def grab_pressure(self, mount: OT3Mount) -> AsyncIterator[None]:
325
+ instrument = self._pipette_handler.get_pipette(mount)
326
+ async with self._backend.grab_pressure(instrument.channels, mount):
327
+ yield
328
+
329
+ def _update_door_state(
330
+ self, door_state: DoorState, module_serial: str | None = None
331
+ ) -> None:
332
+ mod_log.info(f"Updating the window or module switch status: {door_state}")
333
+ self.door_state = door_state
334
+ self.module_door_serial = module_serial
335
+ for cb in self._callbacks:
336
+ hw_event = DoorStateNotification(
337
+ new_state=door_state, module_serial=module_serial
338
+ )
339
+ try:
340
+ cb(hw_event)
341
+ except Exception:
342
+ mod_log.exception("Errored during door state event callback")
343
+
344
+ def _update_estop_state(self, event: HardwareEvent) -> "List[Future[None]]":
345
+ if not isinstance(event, EstopStateNotification):
346
+ return []
347
+ mod_log.info(
348
+ f"Updating the estop status from {event.old_state} to {event.new_state}"
349
+ )
350
+ futures: "List[Future[None]]" = []
351
+ if (
352
+ event.new_state == EstopState.PHYSICALLY_ENGAGED
353
+ and event.old_state != EstopState.PHYSICALLY_ENGAGED
354
+ ):
355
+ # If the estop was just pressed, turn off every module.
356
+ for mod in self._backend.module_controls.available_modules:
357
+ futures.append(
358
+ asyncio.run_coroutine_threadsafe(
359
+ modules.utils.disable_module(mod), self._loop
360
+ )
361
+ )
362
+ for cb in self._callbacks:
363
+ try:
364
+ cb(event)
365
+ except Exception:
366
+ mod_log.exception("Errored during estop state event callback")
367
+
368
+ return futures
369
+
370
+ def _reset_last_mount(self) -> None:
371
+ self._last_moved_mount = None
372
+
373
+ def get_deck_from_machine(
374
+ self, machine_pos: Dict[Axis, float]
375
+ ) -> Dict[Axis, float]:
376
+ return deck_from_machine(
377
+ machine_pos=machine_pos,
378
+ attitude=self._robot_calibration.deck_calibration.attitude,
379
+ offset=self._robot_calibration.carriage_offset,
380
+ robot_type=cast(RobotType, "OT-3 Standard"),
381
+ )
382
+
383
+ @classmethod
384
+ async def build_hardware_controller(
385
+ cls,
386
+ attached_instruments: Optional[
387
+ Dict[Union[top_types.Mount, OT3Mount], Dict[str, Optional[str]]]
388
+ ] = None,
389
+ attached_modules: Optional[List[str]] = None,
390
+ config: Union[OT3Config, RobotConfig, None] = None,
391
+ loop: Optional[asyncio.AbstractEventLoop] = None,
392
+ strict_attached_instruments: bool = True,
393
+ use_usb_bus: bool = False,
394
+ update_firmware: bool = True,
395
+ status_bar_enabled: bool = True,
396
+ feature_flags: Optional[HardwareFeatureFlags] = None,
397
+ ) -> "OT3API":
398
+ """Build an ot3 hardware controller."""
399
+ checked_loop = use_or_initialize_loop(loop)
400
+ if feature_flags is None:
401
+ # If no feature flag set is defined, we will use the default values
402
+ feature_flags = HardwareFeatureFlags()
403
+ if not isinstance(config, OT3Config):
404
+ checked_config = robot_configs.load_ot3()
405
+ else:
406
+ checked_config = config
407
+ from .backends.ot3controller import OT3Controller
408
+
409
+ backend = await OT3Controller.build(
410
+ checked_config,
411
+ use_usb_bus,
412
+ check_updates=update_firmware,
413
+ feature_flags=feature_flags,
414
+ )
415
+
416
+ api_instance = cls(
417
+ backend,
418
+ loop=checked_loop,
419
+ config=checked_config,
420
+ feature_flags=feature_flags,
421
+ )
422
+
423
+ await api_instance.set_status_bar_enabled(status_bar_enabled)
424
+ module_controls = await AttachedModulesControl.build(
425
+ api_instance, board_revision=backend.board_revision
426
+ )
427
+ backend.module_controls = module_controls
428
+ await backend.build_estop_detector()
429
+ door_state = await backend.door_state()
430
+ api_instance._update_door_state(door_state)
431
+ backend.add_door_state_listener(api_instance._update_door_state)
432
+ checked_loop.create_task(backend.watch(loop=checked_loop))
433
+ backend.initialized = True
434
+ await api_instance.refresh_positions()
435
+ return api_instance
436
+
437
+ @classmethod
438
+ async def build_hardware_simulator(
439
+ cls,
440
+ attached_instruments: Union[
441
+ None,
442
+ Dict[OT3Mount, Dict[str, Optional[str]]],
443
+ Dict[top_types.Mount, Dict[str, Optional[str]]],
444
+ ] = None,
445
+ attached_modules: Optional[Dict[str, List[modules.SimulatingModule]]] = None,
446
+ config: Union[RobotConfig, OT3Config, None] = None,
447
+ loop: Optional[asyncio.AbstractEventLoop] = None,
448
+ strict_attached_instruments: bool = True,
449
+ feature_flags: Optional[HardwareFeatureFlags] = None,
450
+ ) -> "OT3API":
451
+ """Build a simulating hardware controller.
452
+
453
+ This method may be used both on a real robot and on dev machines.
454
+ Multiple simulating hardware controllers may be active at one time.
455
+ """
456
+ if feature_flags is None:
457
+ feature_flags = HardwareFeatureFlags()
458
+
459
+ checked_modules = attached_modules or {}
460
+
461
+ checked_loop = use_or_initialize_loop(loop)
462
+ if not isinstance(config, OT3Config):
463
+ checked_config = robot_configs.load_ot3()
464
+ else:
465
+ checked_config = config
466
+
467
+ backend = await OT3Simulator.build(
468
+ (
469
+ {OT3Mount.from_mount(k): v for k, v in attached_instruments.items()}
470
+ if attached_instruments
471
+ else {}
472
+ ),
473
+ checked_modules,
474
+ checked_config,
475
+ checked_loop,
476
+ strict_attached_instruments,
477
+ feature_flags,
478
+ )
479
+ api_instance = cls(
480
+ backend,
481
+ loop=checked_loop,
482
+ config=checked_config,
483
+ feature_flags=feature_flags,
484
+ )
485
+ await api_instance.cache_instruments()
486
+ module_controls = await AttachedModulesControl.build(
487
+ api_instance, board_revision=backend.board_revision
488
+ )
489
+ backend.module_controls = module_controls
490
+ await backend.watch(api_instance.loop)
491
+ await api_instance.refresh_positions()
492
+ return api_instance
493
+
494
+ def __repr__(self) -> str:
495
+ return "<{} using backend {}>".format(type(self), type(self._backend))
496
+
497
+ @property
498
+ def loop(self) -> asyncio.AbstractEventLoop:
499
+ """The event loop used by this instance."""
500
+ return self._loop
501
+
502
+ @property
503
+ def is_simulator(self) -> bool:
504
+ """`True` if this is a simulator; `False` otherwise."""
505
+ return isinstance(self._backend, OT3Simulator)
506
+
507
+ def register_callback(self, cb: HardwareEventHandler) -> Callable[[], None]:
508
+ """Allows the caller to register a callback, and returns a closure
509
+ that can be used to unregister the provided callback
510
+ """
511
+ self._callbacks.add(cb)
512
+
513
+ def unregister() -> None:
514
+ self._callbacks.remove(cb)
515
+
516
+ return unregister
517
+
518
+ def get_fw_version(self) -> str:
519
+ """
520
+ Return the firmware version of the connected hardware.
521
+ """
522
+ from_backend = self._backend.fw_version
523
+ uniques = set(version for version in from_backend.values())
524
+ if not from_backend:
525
+ return "unknown"
526
+ else:
527
+ return ", ".join(str(version) for version in uniques)
528
+
529
+ @property
530
+ def fw_version(self) -> str:
531
+ return self.get_fw_version()
532
+
533
+ @property
534
+ def board_revision(self) -> str:
535
+ return str(self._backend.board_revision)
536
+
537
+ async def update_firmware(
538
+ self, subsystems: Optional[Set[SubSystem]] = None, force: bool = False
539
+ ) -> AsyncIterator[UpdateStatus]:
540
+ """Start the firmware update for one or more subsystems and return update progress iterator."""
541
+ subsystems = subsystems or set()
542
+ if SubSystem.head in subsystems:
543
+ await self.disengage_axes([Axis.Z_L, Axis.Z_R])
544
+ if SubSystem.gripper in subsystems:
545
+ await self.disengage_axes([Axis.Z_G])
546
+ # start the updates and yield the progress
547
+ async with self._motion_lock:
548
+ try:
549
+ async for update_status in self._backend.update_firmware(
550
+ subsystems, force
551
+ ):
552
+ yield update_status
553
+ except SubsystemUpdating as e:
554
+ raise UpdateOngoingError(e.msg) from e
555
+ except EnumeratedError:
556
+ raise
557
+ except BaseException as e:
558
+ mod_log.exception("Firmware update failed")
559
+ raise FirmwareUpdateFailedError(
560
+ message="Update failed because of uncaught error",
561
+ wrapping=[PythonException(e)],
562
+ ) from e
563
+ finally:
564
+ self._configured_since_update = False
565
+
566
+ # Incidentals (i.e. not motion) API
567
+
568
+ async def set_lights(
569
+ self, button: Optional[bool] = None, rails: Optional[bool] = None
570
+ ) -> None:
571
+ """Control the robot lights."""
572
+ await self._backend.set_lights(button, rails)
573
+
574
+ async def get_lights(self) -> Dict[str, bool]:
575
+ """Return the current status of the robot lights."""
576
+ return await self._backend.get_lights()
577
+
578
+ async def identify(self, duration_s: int = 5) -> None:
579
+ """Blink the button light to identify the robot."""
580
+ count = duration_s * 4
581
+ on = False
582
+ for sec in range(count):
583
+ then = self._loop.time()
584
+ await self.set_lights(button=on)
585
+ on = not on
586
+ now = self._loop.time()
587
+ await asyncio.sleep(max(0, 0.25 - (now - then)))
588
+ await self.set_lights(button=True)
589
+
590
+ async def set_status_bar_state(self, state: StatusBarState) -> None:
591
+ self._log.info(f"Setting status bar state to {state}")
592
+ await self._backend.set_status_bar_state(state)
593
+
594
+ async def set_status_bar_enabled(self, enabled: bool) -> None:
595
+ await self._backend.set_status_bar_enabled(enabled)
596
+
597
+ def get_status_bar_enabled(self) -> bool:
598
+ return self._backend.get_status_bar_enabled()
599
+
600
+ def get_status_bar_state(self) -> StatusBarState:
601
+ return self._backend.get_status_bar_state()
602
+
603
+ def add_status_bar_listener(
604
+ self, listener: StatusBarUpdateListener
605
+ ) -> StatusBarUpdateUnsubscriber:
606
+ return self._backend.add_status_bar_listener(listener)
607
+
608
+ @ExecutionManagerProvider.wait_for_running
609
+ async def delay(self, duration_s: float) -> None:
610
+ """Delay execution by pausing and sleeping."""
611
+ self.pause(PauseType.DELAY)
612
+ try:
613
+ await self.do_delay(duration_s)
614
+ finally:
615
+ self.resume(PauseType.DELAY)
616
+
617
+ @property
618
+ def attached_modules(self) -> List[modules.AbstractModule]:
619
+ return self._backend.module_controls.available_modules
620
+
621
+ async def create_simulating_module(
622
+ self,
623
+ model: modules.types.ModuleModel,
624
+ ) -> modules.AbstractModule:
625
+ """Create a simulating module hardware interface."""
626
+ assert (
627
+ self.is_simulator
628
+ ), "Cannot build simulating module from non-simulating hardware control API"
629
+
630
+ return await self._backend.module_controls.build_module(
631
+ port="",
632
+ usb_port=USBPort(name="", port_number=1, port_group=PortGroup.LEFT),
633
+ type=modules.ModuleType.from_model(model),
634
+ sim_model=model.value,
635
+ )
636
+
637
+ def _gantry_load_from_instruments(self) -> GantryLoad:
638
+ """Compute the gantry load based on attached instruments."""
639
+ left = self._pipette_handler.has_pipette(OT3Mount.LEFT)
640
+ if left:
641
+ pip = self._pipette_handler.get_pipette(OT3Mount.LEFT)
642
+ if pip.config.channels == 96:
643
+ if pip.config.pipette_type == PipetteModelType.p1000:
644
+ return GantryLoad.HIGH_THROUGHPUT_1000
645
+ else:
646
+ return GantryLoad.HIGH_THROUGHPUT_200
647
+ return GantryLoad.LOW_THROUGHPUT
648
+
649
+ async def cache_pipette(
650
+ self,
651
+ mount: OT3Mount,
652
+ instrument_data: AttachedPipette,
653
+ req_instr: Optional[PipetteName],
654
+ ) -> bool:
655
+ """Set up pipette based on scanned information."""
656
+ config = instrument_data.get("config")
657
+ pip_id = instrument_data.get("id")
658
+ pip_offset_cal = load_pipette_offset(pip_id, mount)
659
+
660
+ p, skipped = load_from_config_and_check_skip(
661
+ config,
662
+ self._pipette_handler.hardware_instruments[mount],
663
+ req_instr,
664
+ pip_id,
665
+ pip_offset_cal,
666
+ self._feature_flags.use_old_aspiration_functions,
667
+ )
668
+ self._pipette_handler.hardware_instruments[mount] = p
669
+
670
+ if config is not None:
671
+ self._set_pressure_sensor_available(mount, instrument_config=config)
672
+
673
+ # TODO (lc 12-5-2022) Properly support backwards compatibility
674
+ # when applicable
675
+ return skipped
676
+
677
+ def get_pressure_sensor_available(self, mount: OT3Mount) -> bool:
678
+ pip_axis = Axis.of_main_tool_actuator(mount)
679
+ return self._backend.get_pressure_sensor_available(pip_axis)
680
+
681
+ def _set_pressure_sensor_available(
682
+ self,
683
+ mount: OT3Mount,
684
+ instrument_config: pipette_definition.PipetteConfigurations,
685
+ ) -> None:
686
+ pressure_sensor_available = (
687
+ "pressure" in instrument_config.available_sensors.sensors
688
+ )
689
+ pip_axis = Axis.of_main_tool_actuator(mount)
690
+ self._backend.set_pressure_sensor_available(
691
+ pipette_axis=pip_axis, available=pressure_sensor_available
692
+ )
693
+
694
+ async def cache_gripper(self, instrument_data: AttachedGripper) -> bool:
695
+ """Set up gripper based on scanned information."""
696
+ grip_cal = load_gripper_calibration_offset(instrument_data.get("id"))
697
+ g, skipped = compare_gripper_config_and_check_skip(
698
+ instrument_data,
699
+ self._gripper_handler._gripper,
700
+ grip_cal,
701
+ )
702
+ self._gripper_handler.gripper = g
703
+ return skipped
704
+
705
+ def get_all_attached_instr(self) -> Dict[OT3Mount, Optional[InstrumentDict]]:
706
+ # NOTE (spp, 2023-03-07): The return type of this method indicates that
707
+ # if a particular mount has no attached instrument then it will provide a
708
+ # None value for that mount. But in reality, we get an empty dict.
709
+ # We should either not call the value Optional, or have `_attached_...` return
710
+ # a None for empty mounts.
711
+ return {
712
+ OT3Mount.LEFT: self.attached_pipettes[top_types.Mount.LEFT],
713
+ OT3Mount.RIGHT: self.attached_pipettes[top_types.Mount.RIGHT],
714
+ OT3Mount.GRIPPER: self.attached_gripper,
715
+ }
716
+
717
+ # TODO (spp, 2023-01-31): add unit tests
718
+ async def cache_instruments(
719
+ self,
720
+ require: Optional[Dict[top_types.Mount, PipetteName]] = None,
721
+ skip_if_would_block: bool = False,
722
+ ) -> None:
723
+ """
724
+ Scan the attached instruments, take necessary configuration actions,
725
+ and set up hardware controller internal state if necessary.
726
+ """
727
+ if skip_if_would_block and self._motion_lock.locked():
728
+ return
729
+ async with self._motion_lock:
730
+ skip_configure = await self._cache_instruments(require)
731
+ if not skip_configure or not self._configured_since_update:
732
+ self._log.info("Reconfiguring instrument cache")
733
+ await self._configure_instruments()
734
+
735
+ async def _cache_instruments( # noqa: C901
736
+ self, require: Optional[Dict[top_types.Mount, PipetteName]] = None
737
+ ) -> bool:
738
+ """Actually cache instruments and scan network.
739
+
740
+ Returns True if nothing changed since the last call and can skip any follow-up
741
+ configuration; False if we need to reconfigure.
742
+ """
743
+ checked_require = {
744
+ OT3Mount.from_mount(m): v for m, v in (require or {}).items()
745
+ }
746
+ skip_configure = True
747
+ for mount, name in checked_require.items():
748
+ # TODO (lc 12-5-2022) cache instruments should be receiving
749
+ # a pipette type / channels rather than the named config.
750
+ # We should also check version here once we're comfortable.
751
+ if not pipette_load_name.supported_pipette(name):
752
+ raise RuntimeError(f"{name} is not a valid pipette name")
753
+
754
+ # we're not actually checking the required instrument except in the context
755
+ # of simulation and it feels like a lot of work for this function
756
+ # actually be doing.
757
+ found = await self._backend.get_attached_instruments(checked_require)
758
+
759
+ if OT3Mount.GRIPPER in found.keys():
760
+ # Is now a gripper, ask if it's ok to skip
761
+ gripper_skip = await self.cache_gripper(
762
+ cast(AttachedGripper, found.get(OT3Mount.GRIPPER))
763
+ )
764
+ skip_configure &= gripper_skip
765
+ if not gripper_skip:
766
+ self._log.info(
767
+ "cache_instruments: must configure because gripper now attached or changed config"
768
+ )
769
+ elif self._gripper_handler.gripper:
770
+ # Is no gripper, have a cached gripper, definitely need to reconfig
771
+ await self._gripper_handler.reset()
772
+ skip_configure = False
773
+ self._log.info("cache_instruments: must configure because gripper now gone")
774
+
775
+ for pipette_mount in [OT3Mount.LEFT, OT3Mount.RIGHT]:
776
+ if pipette_mount in found.keys():
777
+ # is now a pipette, ask if we need to reconfig
778
+ req_instr_name = checked_require.get(pipette_mount, None)
779
+ pipette_skip = await self.cache_pipette(
780
+ pipette_mount,
781
+ cast(AttachedPipette, found.get(pipette_mount)),
782
+ req_instr_name,
783
+ )
784
+ skip_configure &= pipette_skip
785
+ if not pipette_skip:
786
+ self._log.info(
787
+ f"cache_instruments: must configure because {pipette_mount.name} now attached or changed"
788
+ )
789
+
790
+ elif self._pipette_handler.hardware_instruments[pipette_mount]:
791
+ # Is no pipette, have a cached pipette, need to reconfig
792
+ skip_configure = False
793
+ self._pipette_handler.hardware_instruments[pipette_mount] = None
794
+ self._log.info(
795
+ f"cache_instruments: must configure because {pipette_mount.name} now empty"
796
+ )
797
+
798
+ return skip_configure
799
+
800
+ async def _configure_instruments(self) -> None:
801
+ """Configure instruments"""
802
+ await self.set_gantry_load(self._gantry_load_from_instruments())
803
+ await self.refresh_positions(acquire_lock=False)
804
+ await self.reset_tip_detectors(False)
805
+ self._configured_since_update = True
806
+
807
+ async def reset_tip_detectors(
808
+ self,
809
+ refresh_state: bool = True,
810
+ ) -> None:
811
+ """Reset tip detector whenever we configure instruments."""
812
+ for mount in [OT3Mount.LEFT, OT3Mount.RIGHT]:
813
+ # rebuild tip detector using the attached instrument
814
+ self._log.info(f"resetting tip detector for mount {mount}")
815
+ if self._pipette_handler.has_pipette(mount):
816
+ await self._backend.update_tip_detector(
817
+ mount, self._pipette_handler.get_tip_sensor_count(mount)
818
+ )
819
+ else:
820
+ await self._backend.teardown_tip_detector(mount)
821
+
822
+ if refresh_state and self._pipette_handler.has_pipette(mount):
823
+ await self.get_tip_presence_status(mount)
824
+
825
+ @ExecutionManagerProvider.wait_for_running
826
+ async def _update_position_estimation(
827
+ self, axes: Optional[Sequence[Axis]] = None
828
+ ) -> None:
829
+ """
830
+ Function to update motor estimation for a set of axes
831
+ """
832
+ await self._backend.update_motor_status()
833
+
834
+ if axes is None:
835
+ axes = [ax for ax in Axis]
836
+
837
+ axes = [ax for ax in axes if self._backend.axis_is_present(ax)]
838
+
839
+ await self._backend.update_motor_estimation(axes)
840
+
841
+ # Global actions API
842
+ def pause(self, pause_type: PauseType) -> None:
843
+ """
844
+ Pause motion of the robot after a current motion concludes."""
845
+ self._pause_manager.pause(pause_type)
846
+
847
+ async def _chained_calls() -> None:
848
+ await self._execution_manager.pause()
849
+ self._backend.pause()
850
+
851
+ asyncio.run_coroutine_threadsafe(_chained_calls(), self._loop)
852
+
853
+ def pause_with_message(self, message: str) -> None:
854
+ self._log.warning(f"Pause with message: {message}")
855
+ notification = ErrorMessageNotification(message=message)
856
+ for cb in self._callbacks:
857
+ cb(notification)
858
+ self.pause(PauseType.PAUSE)
859
+
860
+ def resume(self, pause_type: PauseType) -> None:
861
+ """
862
+ Resume motion after a call to :py:meth:`pause`.
863
+ """
864
+ self._pause_manager.resume(pause_type)
865
+
866
+ if self._pause_manager.should_pause:
867
+ return
868
+
869
+ # Resume must be called immediately to awaken thread running hardware
870
+ # methods (ThreadManager)
871
+ self._backend.resume()
872
+
873
+ async def _chained_calls() -> None:
874
+ # mirror what happens API.pause.
875
+ await self._execution_manager.resume()
876
+ self._backend.resume()
877
+
878
+ asyncio.run_coroutine_threadsafe(_chained_calls(), self._loop)
879
+
880
+ def is_movement_execution_taskified(self) -> bool:
881
+ return self.taskify_movement_execution
882
+
883
+ def should_taskify_movement_execution(self, taskify: bool) -> None:
884
+ self.taskify_movement_execution = taskify
885
+
886
+ async def _stop_motors(self) -> None:
887
+ """Immediately stop motors."""
888
+ await self._backend.halt()
889
+
890
+ async def cancel_execution_and_running_tasks(self) -> None:
891
+ await self._execution_manager.cancel()
892
+
893
+ async def halt(self, disengage_before_stopping: bool = False) -> None:
894
+ """Immediately disengage all present motors and clear motor and module tasks."""
895
+ if disengage_before_stopping:
896
+ await self.disengage_axes(
897
+ [ax for ax in Axis if self._backend.axis_is_present(ax) if ax != Axis.G]
898
+ )
899
+ await self._stop_motors()
900
+
901
+ async def stop(self, home_after: bool = True) -> None:
902
+ """Stop motion as soon as possible, reset, and optionally home."""
903
+ await self._stop_motors()
904
+ await self.cancel_execution_and_running_tasks()
905
+ self._log.info("Resetting OT3API")
906
+ await self.reset()
907
+ if home_after:
908
+ skip = []
909
+ if (
910
+ self._gripper_handler.has_gripper()
911
+ and not self._gripper_handler.is_ready_for_jaw_home()
912
+ ):
913
+ skip.append(Axis.G)
914
+ await self.home(skip=skip)
915
+
916
+ async def reset(self) -> None:
917
+ """Reset the stored state of the system."""
918
+ self._pause_manager.reset()
919
+ await self._execution_manager.reset()
920
+ await self._pipette_handler.reset()
921
+ await self._gripper_handler.reset()
922
+ await self.cache_instruments()
923
+
924
+ # Gantry/frame (i.e. not pipette) action API
925
+ # TODO(mc, 2022-07-25): add "home both if necessary" functionality
926
+ # https://github.com/Opentrons/opentrons/pull/11072
927
+ async def home_z(
928
+ self,
929
+ mount: Optional[Union[top_types.Mount, OT3Mount]] = None,
930
+ allow_home_other: bool = True,
931
+ ) -> None:
932
+ """Home all of the z-axes."""
933
+ self._reset_last_mount()
934
+ if isinstance(mount, (top_types.Mount, OT3Mount)):
935
+ axes = [Axis.by_mount(mount)]
936
+ else:
937
+ axes = list(Axis.ot3_mount_axes())
938
+ await self.home(axes)
939
+
940
+ async def _do_home_and_maybe_calibrate_gripper_jaw(
941
+ self,
942
+ recalibrate_jaw_width: bool = False,
943
+ ) -> None:
944
+ gripper = self._gripper_handler.get_gripper()
945
+ self._log.info("Homing gripper jaw.")
946
+ dc = self._gripper_handler.get_duty_cycle_by_grip_force(
947
+ gripper.default_home_force
948
+ )
949
+ await self._ungrip(duty_cycle=dc)
950
+ if recalibrate_jaw_width or not gripper.has_jaw_width_calibration:
951
+ self._log.info("Calibrating gripper jaw.")
952
+ await self._grip(
953
+ duty_cycle=dc, expected_displacement=gripper.max_jaw_displacement()
954
+ )
955
+ jaw_at_closed = (await self._cache_encoder_position())[Axis.G]
956
+ gripper.update_jaw_open_position_from_closed_position(jaw_at_closed)
957
+ await self._ungrip(duty_cycle=dc)
958
+
959
+ async def home_gripper_jaw(
960
+ self,
961
+ recalibrate_jaw_width: bool = False,
962
+ ) -> None:
963
+ """Home the jaw of the gripper."""
964
+ try:
965
+ await self._do_home_and_maybe_calibrate_gripper_jaw(recalibrate_jaw_width)
966
+ except GripperNotPresentError:
967
+ pass
968
+
969
+ async def home_plunger(self, mount: Union[top_types.Mount, OT3Mount]) -> None:
970
+ """
971
+ Home the plunger motor for a mount, and then return it to the 'bottom'
972
+ position.
973
+ """
974
+
975
+ checked_mount = OT3Mount.from_mount(mount)
976
+ await self.home([Axis.of_main_tool_actuator(checked_mount)])
977
+ instr = self._pipette_handler.hardware_instruments[checked_mount]
978
+ if instr:
979
+ self._log.info("Attempting to move the plunger to bottom.")
980
+ await self._move_to_plunger_bottom(
981
+ checked_mount, rate=1.0, acquire_lock=False
982
+ )
983
+
984
+ async def home_gear_motors(self) -> None:
985
+ homing_velocity = self._config.motion_settings.max_speed_discontinuity[
986
+ self._gantry_load
987
+ ][OT3AxisKind.Q]
988
+
989
+ max_distance = self._backend.axis_bounds[Axis.Q][1]
990
+ # if position is not known, move toward limit switch at a constant velocity
991
+ if self._backend.gear_motor_position is None:
992
+ await self._backend.home_tip_motors(
993
+ distance=max_distance,
994
+ velocity=homing_velocity,
995
+ )
996
+ return
997
+
998
+ current_pos_float = self._backend.gear_motor_position or 0.0
999
+
1000
+ # We filter out a distance more than `max_distance` because, if the tip motor was stopped during
1001
+ # a slow-home motion, the position may be stuck at an enormous large value.
1002
+ if (
1003
+ current_pos_float > self._config.safe_home_distance
1004
+ and current_pos_float < max_distance
1005
+ ):
1006
+ # move toward home until a safe distance
1007
+ await self._backend.tip_action(
1008
+ origin=current_pos_float,
1009
+ targets=[(self._config.safe_home_distance, 400)],
1010
+ )
1011
+
1012
+ # update current position
1013
+ current_pos_float = self._backend.gear_motor_position or 0.0
1014
+
1015
+ # move until the limit switch is triggered, with no acceleration
1016
+ await self._backend.home_tip_motors(
1017
+ distance=min(
1018
+ current_pos_float + self._config.safe_home_distance, max_distance
1019
+ ),
1020
+ velocity=homing_velocity,
1021
+ )
1022
+
1023
+ @lru_cache(1)
1024
+ def _carriage_offset(self) -> top_types.Point:
1025
+ return top_types.Point(*self._config.carriage_offset)
1026
+
1027
+ async def current_position(
1028
+ self,
1029
+ mount: Union[top_types.Mount, OT3Mount],
1030
+ critical_point: Optional[CriticalPoint] = None,
1031
+ refresh: bool = False,
1032
+ fail_on_not_homed: bool = False,
1033
+ ) -> Dict[Axis, float]:
1034
+ realmount = OT3Mount.from_mount(mount)
1035
+ ot3_pos = await self.current_position_ot3(realmount, critical_point, refresh)
1036
+ return ot3_pos
1037
+
1038
+ async def current_position_ot3(
1039
+ self,
1040
+ mount: OT3Mount,
1041
+ critical_point: Optional[CriticalPoint] = None,
1042
+ refresh: bool = False,
1043
+ ) -> Dict[Axis, float]:
1044
+ """Return the postion (in deck coords) of the critical point of the
1045
+ specified mount.
1046
+ """
1047
+ if mount == OT3Mount.GRIPPER and not self._gripper_handler.has_gripper():
1048
+ raise GripperNotPresentError(
1049
+ message=f"Cannot return position for {mount} if no gripper is attached",
1050
+ detail={"mount": str(mount)},
1051
+ )
1052
+ mount_axes = [Axis.X, Axis.Y, Axis.by_mount(mount)]
1053
+ if refresh:
1054
+ await self.refresh_positions()
1055
+ elif not self._current_position:
1056
+ raise PositionUnknownError(
1057
+ message=f"Motor positions for {str(mount)} mount are missing ("
1058
+ f"{mount_axes}); must first home motors.",
1059
+ detail={"mount": str(mount), "missing_axes": str(mount_axes)},
1060
+ )
1061
+ self._assert_motor_ok(mount_axes)
1062
+
1063
+ return self._effector_pos_from_carriage_pos(
1064
+ OT3Mount.from_mount(mount), self._current_position, critical_point
1065
+ )
1066
+
1067
+ async def refresh_positions(self, acquire_lock: bool = True) -> None:
1068
+ """Request and update both the motor and encoder positions from backend."""
1069
+ async with contextlib.AsyncExitStack() as stack:
1070
+ if acquire_lock:
1071
+ await stack.enter_async_context(self._motion_lock)
1072
+ await self._backend.update_motor_status()
1073
+ await self._cache_current_position()
1074
+ await self._cache_encoder_position()
1075
+ await self._refresh_jaw_state()
1076
+
1077
+ async def _refresh_jaw_state(self) -> None:
1078
+ try:
1079
+ gripper = self._gripper_handler.get_gripper()
1080
+ gripper.state = await self._backend.get_jaw_state()
1081
+ except GripperNotPresentError:
1082
+ pass
1083
+
1084
+ async def _cache_current_position(self) -> Dict[Axis, float]:
1085
+ """Cache current position from backend and return in absolute deck coords."""
1086
+ self._current_position = self.get_deck_from_machine(
1087
+ await self._backend.update_position()
1088
+ )
1089
+ return self._current_position
1090
+
1091
+ async def _cache_encoder_position(self) -> Dict[Axis, float]:
1092
+ """Cache encoder position from backend and return in absolute deck coords."""
1093
+ self._encoder_position = self.get_deck_from_machine(
1094
+ await self._backend.update_encoder_position()
1095
+ )
1096
+ if self.has_gripper():
1097
+ self._gripper_handler.set_jaw_displacement(self._encoder_position[Axis.G])
1098
+ return self._encoder_position
1099
+
1100
+ def _assert_motor_ok(self, axes: Sequence[Axis]) -> None:
1101
+ invalid_axes = self._backend.get_invalid_motor_axes(axes)
1102
+ if invalid_axes:
1103
+ axes_str = ",".join([ax.name for ax in invalid_axes])
1104
+ raise PositionUnknownError(
1105
+ message=f"Motor position of axes ({axes_str}) is invalid; please home motors.",
1106
+ detail={"axes": axes_str},
1107
+ )
1108
+
1109
+ def _assert_encoder_ok(self, axes: Sequence[Axis]) -> None:
1110
+ invalid_axes = self._backend.get_invalid_motor_axes(axes)
1111
+ if invalid_axes:
1112
+ axes_str = ",".join([ax.name for ax in invalid_axes])
1113
+ raise PositionUnknownError(
1114
+ message=f"Encoder position of axes ({axes_str}) is invalid; please home motors.",
1115
+ detail={"axes": axes_str},
1116
+ )
1117
+
1118
+ def motor_status_ok(self, axis: Axis) -> bool:
1119
+ return self._backend.check_motor_status([axis])
1120
+
1121
+ def encoder_status_ok(self, axis: Axis) -> bool:
1122
+ return self._backend.check_encoder_status([axis])
1123
+
1124
+ async def encoder_current_position(
1125
+ self,
1126
+ mount: Union[top_types.Mount, OT3Mount],
1127
+ critical_point: Optional[CriticalPoint] = None,
1128
+ refresh: bool = False,
1129
+ ) -> Dict[Axis, float]:
1130
+ """
1131
+ Return the encoder position in absolute deck coords specified mount.
1132
+ """
1133
+ return await self.encoder_current_position_ot3(mount, critical_point, refresh)
1134
+
1135
+ async def encoder_current_position_ot3(
1136
+ self,
1137
+ mount: Union[top_types.Mount, OT3Mount],
1138
+ critical_point: Optional[CriticalPoint] = None,
1139
+ refresh: bool = False,
1140
+ ) -> Dict[Axis, float]:
1141
+ """
1142
+ Return the encoder position in absolute deck coords specified mount.
1143
+ """
1144
+ if refresh:
1145
+ await self.refresh_positions()
1146
+ elif not self._encoder_position:
1147
+ raise PositionUnknownError(
1148
+ message=f"Encoder positions for {str(mount)} are missing; must first home motors.",
1149
+ detail={"mount": str(mount)},
1150
+ )
1151
+
1152
+ if mount == OT3Mount.GRIPPER and not self._gripper_handler.has_gripper():
1153
+ raise GripperNotPresentError(
1154
+ message=f"Cannot return encoder position for {mount} if no gripper is attached",
1155
+ detail={"mount": str(mount)},
1156
+ )
1157
+
1158
+ self._assert_encoder_ok([Axis.X, Axis.Y, Axis.by_mount(mount)])
1159
+
1160
+ ot3pos = self._effector_pos_from_carriage_pos(
1161
+ OT3Mount.from_mount(mount),
1162
+ self._encoder_position,
1163
+ critical_point,
1164
+ )
1165
+ return ot3pos
1166
+
1167
+ def _effector_pos_from_carriage_pos(
1168
+ self,
1169
+ mount: OT3Mount,
1170
+ carriage_position: OT3AxisMap[float],
1171
+ critical_point: Optional[CriticalPoint],
1172
+ ) -> OT3AxisMap[float]:
1173
+ offset = offset_for_mount(
1174
+ mount,
1175
+ top_types.Point(*self._config.left_mount_offset),
1176
+ top_types.Point(*self._config.right_mount_offset),
1177
+ top_types.Point(*self._config.gripper_mount_offset),
1178
+ )
1179
+ cp = self.critical_point_for(mount, critical_point)
1180
+ z_ax = Axis.by_mount(mount)
1181
+ plunger_ax = Axis.of_main_tool_actuator(mount)
1182
+
1183
+ effector_pos = {
1184
+ Axis.X: carriage_position[Axis.X] + offset[0] + cp.x,
1185
+ Axis.Y: carriage_position[Axis.Y] + offset[1] + cp.y,
1186
+ z_ax: carriage_position[z_ax] + offset[2] + cp.z,
1187
+ plunger_ax: carriage_position[plunger_ax],
1188
+ }
1189
+ if self._gantry_load in [
1190
+ GantryLoad.HIGH_THROUGHPUT_1000,
1191
+ GantryLoad.HIGH_THROUGHPUT_200,
1192
+ ]:
1193
+ effector_pos[Axis.Q] = self._backend.gear_motor_position or 0.0
1194
+
1195
+ return effector_pos
1196
+
1197
+ async def gantry_position(
1198
+ self,
1199
+ mount: Union[top_types.Mount, OT3Mount],
1200
+ critical_point: Optional[CriticalPoint] = None,
1201
+ refresh: bool = False,
1202
+ fail_on_not_homed: bool = False,
1203
+ ) -> top_types.Point:
1204
+ """Return the position of the critical point as pertains to the gantry."""
1205
+ realmount = OT3Mount.from_mount(mount)
1206
+ cur_pos = await self.current_position_ot3(
1207
+ realmount,
1208
+ critical_point,
1209
+ refresh,
1210
+ )
1211
+ return top_types.Point(
1212
+ x=cur_pos[Axis.X],
1213
+ y=cur_pos[Axis.Y],
1214
+ z=cur_pos[Axis.by_mount(realmount)],
1215
+ )
1216
+
1217
+ async def update_axis_position_estimations(self, axes: Sequence[Axis]) -> None:
1218
+ """Update specified axes position estimators from their encoders."""
1219
+ await self._update_position_estimation(axes)
1220
+ await self._cache_current_position()
1221
+ await self._cache_encoder_position()
1222
+
1223
+ async def move_to(
1224
+ self,
1225
+ mount: Union[top_types.Mount, OT3Mount],
1226
+ abs_position: top_types.Point,
1227
+ speed: Optional[float] = None,
1228
+ critical_point: Optional[CriticalPoint] = None,
1229
+ max_speeds: Union[None, Dict[Axis, float], OT3AxisMap[float]] = None,
1230
+ expect_stalls: bool = False,
1231
+ ) -> None:
1232
+ """Move the critical point of the specified mount to a location
1233
+ relative to the deck, at the specified speed."""
1234
+ realmount = OT3Mount.from_mount(mount)
1235
+ axes_moving = [Axis.X, Axis.Y, Axis.by_mount(mount)]
1236
+
1237
+ if (
1238
+ self.gantry_load
1239
+ in [GantryLoad.HIGH_THROUGHPUT_1000, GantryLoad.HIGH_THROUGHPUT_200]
1240
+ and realmount == OT3Mount.RIGHT
1241
+ ):
1242
+ raise RuntimeError(
1243
+ f"unable to move {realmount.name} "
1244
+ f"with {self.gantry_load.name} gantry load"
1245
+ )
1246
+
1247
+ # Cache current position from backend
1248
+ if not self._current_position:
1249
+ await self.refresh_positions()
1250
+
1251
+ if not self._backend.check_encoder_status(axes_moving):
1252
+ # a moving axis has not been homed before, homing robot now
1253
+ await self.home()
1254
+ else:
1255
+ self._assert_motor_ok(axes_moving)
1256
+
1257
+ target_position = target_position_from_absolute(
1258
+ realmount,
1259
+ abs_position,
1260
+ partial(self.critical_point_for, cp_override=critical_point),
1261
+ top_types.Point(*self._config.left_mount_offset),
1262
+ top_types.Point(*self._config.right_mount_offset),
1263
+ top_types.Point(*self._config.gripper_mount_offset),
1264
+ )
1265
+ if max_speeds:
1266
+ checked_max: Optional[OT3AxisMap[float]] = max_speeds
1267
+ else:
1268
+ checked_max = None
1269
+
1270
+ await self.prepare_for_mount_movement(realmount)
1271
+ await self._move(
1272
+ target_position,
1273
+ speed=speed,
1274
+ max_speeds=checked_max,
1275
+ expect_stalls=expect_stalls,
1276
+ )
1277
+
1278
+ async def move_axes( # noqa: C901
1279
+ self,
1280
+ position: Mapping[Axis, float],
1281
+ speed: Optional[float] = None,
1282
+ max_speeds: Optional[Dict[Axis, float]] = None,
1283
+ expect_stalls: bool = False,
1284
+ ) -> None:
1285
+ """Moves the effectors of the specified axis to the specified position.
1286
+ The effector of the x,y axis is the center of the carriage.
1287
+ The effector of the pipette mount axis are the mount critical points but only in z.
1288
+ """
1289
+ if not self._current_position:
1290
+ await self.refresh_positions()
1291
+
1292
+ for axis in position.keys():
1293
+ if not self._backend.axis_is_present(axis):
1294
+ raise InvalidActuator(
1295
+ message=f"{axis} is not present", detail={"axis": str(axis)}
1296
+ )
1297
+
1298
+ self._log.info(f"Attempting to move {position} with speed {speed}.")
1299
+ if not self._backend.check_encoder_status(list(position.keys())):
1300
+ self._log.info("Calling home in move_axes")
1301
+ await self.home()
1302
+ self._assert_motor_ok(list(position.keys()))
1303
+
1304
+ absolute_positions: "OrderedDict[Axis, float]" = OrderedDict()
1305
+ current_position = self._current_position
1306
+ if Axis.X in position:
1307
+ absolute_positions[Axis.X] = position[Axis.X]
1308
+ else:
1309
+ absolute_positions[Axis.X] = current_position[Axis.X]
1310
+ if Axis.Y in position:
1311
+ absolute_positions[Axis.Y] = position[Axis.Y]
1312
+ else:
1313
+ absolute_positions[Axis.Y] = current_position[Axis.Y]
1314
+
1315
+ have_z = False
1316
+ for axis in [Axis.Z_L, Axis.Z_R, Axis.Z_G]:
1317
+ if axis in position:
1318
+ have_z = True
1319
+ if Axis.Z_L:
1320
+ carriage_effectors_offset = (
1321
+ self._robot_calibration.left_mount_offset
1322
+ )
1323
+ elif Axis.Z_R:
1324
+ carriage_effectors_offset = (
1325
+ self._robot_calibration.right_mount_offset
1326
+ )
1327
+ else:
1328
+ carriage_effectors_offset = (
1329
+ self._robot_calibration.gripper_mount_offset
1330
+ )
1331
+ absolute_positions[axis] = position[axis] - carriage_effectors_offset.z
1332
+
1333
+ if not have_z:
1334
+ absolute_positions[Axis.Z_L] = current_position[Axis.Z_L]
1335
+ for axis, position_value in position.items():
1336
+ if axis not in absolute_positions:
1337
+ absolute_positions[axis] = position_value
1338
+
1339
+ await self._move(
1340
+ target_position=absolute_positions,
1341
+ speed=speed,
1342
+ expect_stalls=expect_stalls,
1343
+ )
1344
+
1345
+ async def move_rel(
1346
+ self,
1347
+ mount: Union[top_types.Mount, OT3Mount],
1348
+ delta: top_types.Point,
1349
+ speed: Optional[float] = None,
1350
+ max_speeds: Union[None, Dict[Axis, float], OT3AxisMap[float]] = None,
1351
+ check_bounds: MotionChecks = MotionChecks.NONE,
1352
+ fail_on_not_homed: bool = False,
1353
+ expect_stalls: bool = False,
1354
+ ) -> None:
1355
+ """Move the critical point of the specified mount by a specified
1356
+ displacement in a specified direction, at the specified speed."""
1357
+ if not self._current_position:
1358
+ await self.refresh_positions()
1359
+
1360
+ realmount = OT3Mount.from_mount(mount)
1361
+ axes_moving = [Axis.X, Axis.Y, Axis.by_mount(mount)]
1362
+
1363
+ if (
1364
+ self.gantry_load
1365
+ in [GantryLoad.HIGH_THROUGHPUT_1000, GantryLoad.HIGH_THROUGHPUT_200]
1366
+ and realmount == OT3Mount.RIGHT
1367
+ ):
1368
+ raise RuntimeError(
1369
+ f"unable to move {realmount.name} "
1370
+ f"with {self.gantry_load.name} gantry load"
1371
+ )
1372
+
1373
+ if not self._backend.check_encoder_status(axes_moving):
1374
+ await self.home()
1375
+
1376
+ # Cache current position from backend
1377
+ await self._cache_current_position()
1378
+ await self._cache_encoder_position()
1379
+
1380
+ self._assert_motor_ok([axis for axis in axes_moving])
1381
+
1382
+ target_position = target_position_from_relative(
1383
+ realmount, delta, self._current_position
1384
+ )
1385
+ if max_speeds:
1386
+ checked_max: Optional[OT3AxisMap[float]] = max_speeds
1387
+ else:
1388
+ checked_max = None
1389
+
1390
+ await self.prepare_for_mount_movement(realmount)
1391
+ await self._move(
1392
+ target_position,
1393
+ speed=speed,
1394
+ max_speeds=checked_max,
1395
+ check_bounds=check_bounds,
1396
+ expect_stalls=expect_stalls,
1397
+ )
1398
+
1399
+ async def _cache_and_maybe_retract_mount(self, mount: OT3Mount) -> None:
1400
+ """Retract the 'other' mount if necessary.
1401
+
1402
+ If `mount` does not match the value in :py:attr:`_last_moved_mount`
1403
+ (and :py:attr:`_last_moved_mount` exists) then retract the mount
1404
+ in :py:attr:`_last_moved_mount`. Also unconditionally update
1405
+ :py:attr:`_last_moved_mount` to contain `mount`.
1406
+
1407
+ Disengage the 96-channel and gripper mount if retracted. Re-engage
1408
+ the 96-channel or gripper mount if it is about to move.
1409
+ """
1410
+ last_moved = self._last_moved_mount
1411
+ # if gripper exists and it's not the moving mount, it should retract
1412
+ if (
1413
+ self.has_gripper()
1414
+ and mount != OT3Mount.GRIPPER
1415
+ and not self.is_idle_mount(OT3Mount.GRIPPER)
1416
+ ):
1417
+ await self.retract(OT3Mount.GRIPPER, 10)
1418
+ await self.disengage_axes([Axis.Z_G])
1419
+ await self.idle_gripper()
1420
+
1421
+ # if 96-channel pipette is attached and not being moved, it should retract
1422
+ if (
1423
+ mount != OT3Mount.LEFT
1424
+ and self._gantry_load
1425
+ in [GantryLoad.HIGH_THROUGHPUT_1000, GantryLoad.HIGH_THROUGHPUT_200]
1426
+ and not self.is_idle_mount(OT3Mount.LEFT)
1427
+ ):
1428
+ await self.retract(OT3Mount.LEFT, 10)
1429
+ await self.disengage_axes([Axis.Z_L])
1430
+
1431
+ # if the last moved mount is not covered in neither of the above scenario,
1432
+ # simply retract the last moved mount
1433
+ if last_moved and not self.is_idle_mount(last_moved) and mount != last_moved:
1434
+ await self.retract(last_moved, 10)
1435
+
1436
+ # finally, home the current left/gripper mount to prepare for movement
1437
+ if self.is_idle_mount(mount):
1438
+ await self.home_z(mount)
1439
+ self._last_moved_mount = mount
1440
+
1441
+ async def prepare_for_mount_movement(
1442
+ self, mount: Union[top_types.Mount, OT3Mount]
1443
+ ) -> None:
1444
+ """Retract the idle mount if necessary."""
1445
+ realmount = OT3Mount.from_mount(mount)
1446
+ await self._cache_and_maybe_retract_mount(realmount)
1447
+
1448
+ async def idle_gripper(self) -> None:
1449
+ """Move gripper to its idle, gripped position."""
1450
+ try:
1451
+ gripper = self._gripper_handler.get_gripper()
1452
+ if self._gripper_handler.is_ready_for_idle():
1453
+ await self.grip(
1454
+ force_newtons=gripper.default_idle_force,
1455
+ stay_engaged=False,
1456
+ )
1457
+ except GripperNotPresentError:
1458
+ pass
1459
+
1460
+ def raise_error_if_gripper_pickup_failed(
1461
+ self,
1462
+ expected_grip_width: float,
1463
+ grip_width_uncertainty_wider: float,
1464
+ grip_width_uncertainty_narrower: float,
1465
+ ) -> None:
1466
+ """Ensure that a gripper pickup succeeded.
1467
+
1468
+ The labware width is the width of the labware at the point of the grip, as closely as it is known.
1469
+ The uncertainty values should be specified to handle the case where the labware definition does not
1470
+ provide that information.
1471
+
1472
+ Both values should be positive; their direcitonal sense is determined by which argument they are.
1473
+ """
1474
+ # check if the gripper is at an acceptable position after attempting to
1475
+ # pick up labware
1476
+ gripper = self._gripper_handler.get_gripper()
1477
+ self._backend.check_gripper_position_within_bounds(
1478
+ expected_grip_width,
1479
+ grip_width_uncertainty_wider,
1480
+ grip_width_uncertainty_narrower,
1481
+ gripper.jaw_width,
1482
+ gripper.max_allowed_grip_error,
1483
+ gripper.max_jaw_width,
1484
+ gripper.min_jaw_width,
1485
+ )
1486
+
1487
+ def gripper_jaw_can_home(self) -> bool:
1488
+ return self._gripper_handler.is_ready_for_jaw_home()
1489
+
1490
+ @ExecutionManagerProvider.wait_for_running
1491
+ async def _move(
1492
+ self,
1493
+ target_position: "OrderedDict[Axis, float]",
1494
+ speed: Optional[float] = None,
1495
+ home_flagged_axes: bool = True,
1496
+ max_speeds: Optional[OT3AxisMap[float]] = None,
1497
+ acquire_lock: bool = True,
1498
+ check_bounds: MotionChecks = MotionChecks.NONE,
1499
+ expect_stalls: bool = False,
1500
+ ) -> None:
1501
+ """Worker function to apply robot motion."""
1502
+ machine_pos = machine_from_deck(
1503
+ deck_pos=target_position,
1504
+ attitude=self._robot_calibration.deck_calibration.attitude,
1505
+ offset=self._robot_calibration.carriage_offset,
1506
+ robot_type=cast(RobotType, "OT-3 Standard"),
1507
+ )
1508
+ bounds = self._backend.axis_bounds
1509
+ to_check = {
1510
+ ax: machine_pos[ax]
1511
+ for ax in target_position.keys()
1512
+ if ax in Axis.gantry_axes()
1513
+ }
1514
+ check_motion_bounds(to_check, target_position, bounds, check_bounds)
1515
+ self._log.info(f"Move: deck {target_position} becomes machine {machine_pos}")
1516
+ origin = await self._backend.update_position()
1517
+
1518
+ if self._gantry_load in [
1519
+ GantryLoad.HIGH_THROUGHPUT_1000,
1520
+ GantryLoad.HIGH_THROUGHPUT_200,
1521
+ ]:
1522
+ origin[Axis.Q] = self._backend.gear_motor_position or 0.0
1523
+
1524
+ async with contextlib.AsyncExitStack() as stack:
1525
+ if acquire_lock:
1526
+ await stack.enter_async_context(self._motion_lock)
1527
+ try:
1528
+ await self._backend.move(
1529
+ origin,
1530
+ machine_pos,
1531
+ speed or 400.0,
1532
+ HWStopCondition.stall if expect_stalls else HWStopCondition.none,
1533
+ )
1534
+ except Exception:
1535
+ self._log.exception("Move failed")
1536
+ self._current_position.clear()
1537
+ raise
1538
+ else:
1539
+ await self._cache_current_position()
1540
+ await self._cache_encoder_position()
1541
+
1542
+ async def _set_plunger_current_and_home(
1543
+ self,
1544
+ axis: Axis,
1545
+ motor_ok: bool,
1546
+ encoder_ok: bool,
1547
+ ) -> None:
1548
+ mount = Axis.to_ot3_mount(axis)
1549
+ instr = self._pipette_handler.hardware_instruments[mount]
1550
+ if instr is None:
1551
+ self._log.warning("no pipette found")
1552
+ return
1553
+
1554
+ origin, target_pos = await self._retrieve_home_position(axis)
1555
+
1556
+ if encoder_ok and motor_ok:
1557
+ if origin[axis] - target_pos[axis] > self._config.safe_home_distance:
1558
+ target_pos[axis] += self._config.safe_home_distance
1559
+ async with self._backend.motor_current(
1560
+ run_currents={
1561
+ axis: instr.config.plunger_homing_configurations.current
1562
+ }
1563
+ ):
1564
+ await self._backend.move(
1565
+ origin,
1566
+ target_pos,
1567
+ instr.config.plunger_homing_configurations.speed,
1568
+ HWStopCondition.none,
1569
+ )
1570
+ await self._backend.home([axis], self.gantry_load)
1571
+ else:
1572
+ async with self._backend.motor_current(
1573
+ run_currents={axis: instr.config.plunger_homing_configurations.current}
1574
+ ):
1575
+ await self._backend.home([axis], self.gantry_load)
1576
+
1577
+ async def _retrieve_home_position(
1578
+ self, axis: Axis
1579
+ ) -> Tuple[OT3AxisMap[float], OT3AxisMap[float]]:
1580
+ origin = await self._backend.update_position()
1581
+ origin_pos = {axis: origin[axis]}
1582
+ target_pos = {axis: self._backend.home_position()[axis]}
1583
+ return origin_pos, target_pos
1584
+
1585
+ async def _enable_before_update_estimation(self, axis: Axis) -> None:
1586
+ enabled = await self._backend.is_motor_engaged(axis)
1587
+
1588
+ if not enabled:
1589
+ if axis == Axis.Z_L and self.gantry_load in [
1590
+ GantryLoad.HIGH_THROUGHPUT_1000,
1591
+ GantryLoad.HIGH_THROUGHPUT_200,
1592
+ ]:
1593
+ # we're here if the left mount has been idle and the brake is engaged
1594
+ # we want to temporarily increase its hold current to prevent the z
1595
+ # stage from dropping when switching off the ebrake
1596
+ async with self._backend.increase_z_l_hold_current():
1597
+ await self.engage_axes([axis])
1598
+ else:
1599
+ await self.engage_axes([axis])
1600
+
1601
+ # now that motor is enabled, we can update position estimation
1602
+ await self._update_position_estimation([axis])
1603
+
1604
+ @_adjust_high_throughput_z_current
1605
+ async def _home_axis(self, axis: Axis) -> None:
1606
+ """
1607
+ Perform home; base on axis motor/encoder statuses, shorten homing time
1608
+ if possible.
1609
+
1610
+ 1. If stepper position status is valid, move directly to the home position.
1611
+ 2. If encoder position status is valid, update position estimation.
1612
+ If axis encoder is accurate (Zs & Ps ONLY), move directly to home position.
1613
+ Or, if axis encoder is not accurate, move to 20mm away from home position,
1614
+ then home.
1615
+ 3. If both stepper and encoder statuses are invalid, home full axis.
1616
+
1617
+ Note that when an axis is move directly to the home position, the axis limit
1618
+ switch will not be triggered.
1619
+ """
1620
+
1621
+ # G, Q should be handled in the backend through `self._home()`
1622
+ assert axis not in [Axis.G, Axis.Q]
1623
+
1624
+ encoder_ok = self._backend.check_encoder_status([axis])
1625
+ if encoder_ok:
1626
+ # enable motor (if needed) and update estimation
1627
+ await self._enable_before_update_estimation(axis)
1628
+
1629
+ # refresh motor status after position estimation update
1630
+ motor_ok = self._backend.check_motor_status([axis])
1631
+
1632
+ if Axis.to_kind(axis) == OT3AxisKind.P:
1633
+ await self._set_plunger_current_and_home(axis, motor_ok, encoder_ok)
1634
+ return
1635
+
1636
+ # TODO: (ba, 2024-04-19): We need to explictly engage the axis and enable
1637
+ # the motor when we are attempting to move. This should be already
1638
+ # happening but something on the firmware is either not enabling the motor or
1639
+ # disabling the motor.
1640
+ await self.engage_axes([axis])
1641
+
1642
+ # we can move to safe home distance!
1643
+ if encoder_ok and motor_ok:
1644
+ origin, target_pos = await self._retrieve_home_position(axis)
1645
+ if Axis.to_kind(axis) == OT3AxisKind.Z:
1646
+ axis_home_dist = self._config.safe_home_distance
1647
+ else:
1648
+ # FIXME: (AA 2/15/23) This is a temporary workaround because of
1649
+ # XY encoder inaccuracy. Otherwise, we should be able to use
1650
+ # 5.0 mm for all axes.
1651
+ # Move to 20 mm away from the home position and then home
1652
+ axis_home_dist = 20.0
1653
+ if origin[axis] - target_pos[axis] > axis_home_dist:
1654
+ target_pos[axis] += axis_home_dist
1655
+ await self._backend.move(
1656
+ origin,
1657
+ target_pos,
1658
+ speed=400,
1659
+ stop_condition=HWStopCondition.none,
1660
+ )
1661
+ await self._backend.home([axis], self.gantry_load)
1662
+ else:
1663
+ # both stepper and encoder positions are invalid, must home
1664
+ await self._backend.home([axis], self.gantry_load)
1665
+
1666
+ async def _home(self, axes: Sequence[Axis]) -> None:
1667
+ """Home one axis at a time."""
1668
+ for axis in axes:
1669
+ try:
1670
+ if axis == Axis.G:
1671
+ await self.home_gripper_jaw()
1672
+ elif axis == Axis.Q:
1673
+ await self._backend.home([axis], self.gantry_load)
1674
+ else:
1675
+ await self._home_axis(axis)
1676
+ except BaseException as e:
1677
+ self._log.exception(f"Homing failed: {e}")
1678
+ self._current_position.clear()
1679
+ raise
1680
+ else:
1681
+ await self._cache_current_position()
1682
+ await self._cache_encoder_position()
1683
+
1684
+ @ExecutionManagerProvider.wait_for_running
1685
+ async def home(
1686
+ self,
1687
+ axes: Optional[List[Axis]] = None,
1688
+ skip: Optional[List[Axis]] = None,
1689
+ ) -> None:
1690
+ """
1691
+ Worker function to home the robot by axis or list of
1692
+ desired axes.
1693
+ """
1694
+ # make sure current position is up-to-date
1695
+ await self.refresh_positions()
1696
+
1697
+ if axes:
1698
+ checked_axes = axes
1699
+ else:
1700
+ checked_axes = [ax for ax in Axis if ax != Axis.Q]
1701
+ if self.gantry_load in [
1702
+ GantryLoad.HIGH_THROUGHPUT_1000,
1703
+ GantryLoad.HIGH_THROUGHPUT_200,
1704
+ ]:
1705
+ checked_axes.append(Axis.Q)
1706
+ if skip:
1707
+ checked_axes = [ax for ax in checked_axes if ax not in skip]
1708
+ self._log.info(f"Homing {axes}")
1709
+
1710
+ home_seq = [
1711
+ ax
1712
+ for ax in AXES_IN_HOMING_ORDER
1713
+ if (ax in checked_axes and self._backend.axis_is_present(ax))
1714
+ ]
1715
+ self._log.info(f"home was called with {axes} generating sequence {home_seq}")
1716
+ async with self._motion_lock:
1717
+ await self._home(home_seq)
1718
+
1719
+ def get_engaged_axes(self) -> Dict[Axis, bool]:
1720
+ """Which axes are engaged and holding."""
1721
+ return self._backend.engaged_axes()
1722
+
1723
+ @property
1724
+ def engaged_axes(self) -> Dict[Axis, bool]:
1725
+ return self.get_engaged_axes()
1726
+
1727
+ async def disengage_axes(self, which: List[Axis]) -> None:
1728
+ await self._backend.disengage_axes(which)
1729
+
1730
+ async def engage_axes(self, which: List[Axis]) -> None:
1731
+ await self._backend.engage_axes(
1732
+ [axis for axis in which if self._backend.axis_is_present(axis)]
1733
+ )
1734
+
1735
+ def axis_is_present(self, axis: Axis) -> bool:
1736
+ return self._backend.axis_is_present(axis)
1737
+
1738
+ async def get_limit_switches(self) -> Dict[Axis, bool]:
1739
+ res = await self._backend.get_limit_switches()
1740
+ return {ax: val for ax, val in res.items()}
1741
+
1742
+ @ExecutionManagerProvider.wait_for_running
1743
+ async def retract(
1744
+ self, mount: Union[top_types.Mount, OT3Mount], margin: float = 10
1745
+ ) -> None:
1746
+ """Pull the specified mount up to its home position.
1747
+
1748
+ Works regardless of critical point or home status.
1749
+ """
1750
+ await self.retract_axis(Axis.by_mount(mount))
1751
+
1752
+ @ExecutionManagerProvider.wait_for_running
1753
+ @_adjust_high_throughput_z_current
1754
+ async def retract_axis(self, axis: Axis) -> None:
1755
+ """
1756
+ Move an axis to its home position, without engaging the limit switch,
1757
+ whenever we can.
1758
+
1759
+ OT-2 uses this function to recover from a stall. In order to keep
1760
+ the behaviors between the two robots similar, retract_axis on the FLEX
1761
+ will call home if the stepper position is inaccurate.
1762
+ """
1763
+ motor_ok = self._backend.check_motor_status([axis])
1764
+ encoder_ok = self._backend.check_encoder_status([axis])
1765
+
1766
+ async with self._motion_lock:
1767
+ if motor_ok and encoder_ok:
1768
+ # TODO: (ba, 2024-04-19): We need to explictly engage the axis and enable
1769
+ # the motor when we are attempting to move. This should be already
1770
+ # happening but something on the firmware is either not enabling the motor or
1771
+ # disabling the motor.
1772
+ await self.engage_axes([axis])
1773
+
1774
+ # we can move to the home position without checking the limit switch
1775
+ origin = await self._backend.update_position()
1776
+ target_pos = {axis: self._backend.home_position()[axis]}
1777
+ await self._backend.move(origin, target_pos, 400, HWStopCondition.none)
1778
+ else:
1779
+ # home the axis
1780
+ await self._home_axis(axis)
1781
+
1782
+ await self._cache_current_position()
1783
+ await self._cache_encoder_position()
1784
+
1785
+ # Gantry/frame (i.e. not pipette) config API
1786
+ @property
1787
+ def config(self) -> OT3Config:
1788
+ """Get the robot's configuration object.
1789
+
1790
+ :returns .RobotConfig: The object.
1791
+ """
1792
+ return self._config
1793
+
1794
+ @config.setter
1795
+ def config(self, config: Union[OT3Config, RobotConfig]) -> None:
1796
+ """Replace the currently-loaded config"""
1797
+ if isinstance(config, OT3Config):
1798
+ self._config = config
1799
+ else:
1800
+ self._log.error("Tried to specify an OT2 config object")
1801
+
1802
+ def get_config(self) -> OT3Config:
1803
+ """
1804
+ Get the robot's configuration object.
1805
+
1806
+ :returns .RobotConfig: The object.
1807
+ """
1808
+ return self.config
1809
+
1810
+ def set_config(self, config: Union[OT3Config, RobotConfig]) -> None:
1811
+ """Replace the currently-loaded config"""
1812
+ if isinstance(config, OT3Config):
1813
+ self.config = config
1814
+ else:
1815
+ self._log.error("Tried to specify an OT2 config object")
1816
+
1817
+ async def update_config(self, **kwargs: Any) -> None:
1818
+ """Update values of the robot's configuration."""
1819
+ self._config = replace(self._config, **kwargs)
1820
+
1821
+ @property
1822
+ def hardware_feature_flags(self) -> HardwareFeatureFlags:
1823
+ return self._feature_flags
1824
+
1825
+ @hardware_feature_flags.setter
1826
+ def hardware_feature_flags(self, feature_flags: HardwareFeatureFlags) -> None:
1827
+ self._feature_flags = feature_flags
1828
+ self._backend.update_feature_flags(self._feature_flags)
1829
+
1830
+ @ExecutionManagerProvider.wait_for_running
1831
+ async def _grip(
1832
+ self, duty_cycle: float, expected_displacement: float, stay_engaged: bool = True
1833
+ ) -> None:
1834
+ """Move the gripper jaw inward to close."""
1835
+ try:
1836
+ await self._backend.gripper_grip_jaw(
1837
+ duty_cycle=duty_cycle,
1838
+ expected_displacement=self._gripper_handler.get_gripper().max_jaw_displacement(),
1839
+ stay_engaged=stay_engaged,
1840
+ )
1841
+ await self._cache_encoder_position()
1842
+ self._gripper_handler.set_jaw_state(await self._backend.get_jaw_state())
1843
+ except Exception:
1844
+ self._log.exception(
1845
+ f"Gripper grip failed, encoder pos: {self._encoder_position[Axis.G]}"
1846
+ )
1847
+ raise
1848
+
1849
+ @ExecutionManagerProvider.wait_for_running
1850
+ async def _ungrip(self, duty_cycle: float) -> None:
1851
+ """Move the gripper jaw outward to reach the homing switch."""
1852
+ try:
1853
+ await self._backend.gripper_home_jaw(duty_cycle=duty_cycle)
1854
+ await self._cache_encoder_position()
1855
+ self._gripper_handler.set_jaw_state(await self._backend.get_jaw_state())
1856
+ except Exception:
1857
+ self._log.exception("Gripper home failed")
1858
+ raise
1859
+
1860
+ @ExecutionManagerProvider.wait_for_running
1861
+ async def _hold_jaw_width(self, jaw_width_mm: float) -> None:
1862
+ """Move the gripper jaw to a specific width."""
1863
+ try:
1864
+ if not self._gripper_handler.is_valid_jaw_width(jaw_width_mm):
1865
+ raise ValueError("Setting gripper jaw width out of bounds")
1866
+ gripper = self._gripper_handler.get_gripper()
1867
+ width_max = gripper.config.geometry.jaw_width["max"]
1868
+ jaw_displacement_mm = (width_max - jaw_width_mm) / 2.0
1869
+ await self._backend.gripper_hold_jaw(int(1000 * jaw_displacement_mm))
1870
+ await self._cache_encoder_position()
1871
+ self._gripper_handler.set_jaw_state(await self._backend.get_jaw_state())
1872
+ except Exception:
1873
+ self._log.exception("Gripper set width failed")
1874
+ raise
1875
+
1876
+ async def grip(
1877
+ self, force_newtons: Optional[float] = None, stay_engaged: bool = True
1878
+ ) -> None:
1879
+ self._gripper_handler.check_ready_for_jaw_move("grip")
1880
+ dc = self._gripper_handler.get_duty_cycle_by_grip_force(
1881
+ force_newtons or self._gripper_handler.get_gripper().default_grip_force
1882
+ )
1883
+ await self._grip(
1884
+ duty_cycle=dc,
1885
+ expected_displacement=self._gripper_handler.get_gripper().max_jaw_displacement(),
1886
+ stay_engaged=stay_engaged,
1887
+ )
1888
+
1889
+ async def ungrip(self, force_newtons: Optional[float] = None) -> None:
1890
+ """
1891
+ Release gripped object.
1892
+
1893
+ To simply open the jaw, use `home_gripper_jaw` instead.
1894
+ """
1895
+ # get default grip force for release if not provided
1896
+ self._gripper_handler.check_ready_for_jaw_move("ungrip")
1897
+ # TODO: check jaw width to make sure it is actually gripping something
1898
+ dc = self._gripper_handler.get_duty_cycle_by_grip_force(
1899
+ force_newtons or self._gripper_handler.get_gripper().default_home_force
1900
+ )
1901
+ await self._ungrip(duty_cycle=dc)
1902
+
1903
+ async def hold_jaw_width(self, jaw_width_mm: int) -> None:
1904
+ self._gripper_handler.check_ready_for_jaw_move("hold_jaw_width")
1905
+ await self._hold_jaw_width(jaw_width_mm)
1906
+
1907
+ async def tip_pickup_moves(
1908
+ self,
1909
+ mount: Union[top_types.Mount, OT3Mount],
1910
+ presses: Optional[int] = None,
1911
+ increment: Optional[float] = None,
1912
+ ) -> None:
1913
+ """This is a slightly more barebones variation of pick_up_tip. This is only the motor routine
1914
+ directly involved in tip pickup, and leaves any state updates and plunger moves to the caller.
1915
+ """
1916
+ realmount = OT3Mount.from_mount(mount)
1917
+ instrument = self._pipette_handler.get_pipette(realmount)
1918
+
1919
+ if (
1920
+ self.gantry_load
1921
+ in [GantryLoad.HIGH_THROUGHPUT_1000, GantryLoad.HIGH_THROUGHPUT_200]
1922
+ and instrument.nozzle_manager.current_configuration.configuration
1923
+ == top_types.NozzleConfigurationType.FULL
1924
+ ):
1925
+ spec = self._pipette_handler.plan_ht_pick_up_tip(
1926
+ instrument.nozzle_manager.current_configuration.tip_count
1927
+ )
1928
+ if spec.z_distance_to_tiprack:
1929
+ await self.move_rel(
1930
+ realmount, top_types.Point(z=spec.z_distance_to_tiprack)
1931
+ )
1932
+ await self._tip_motor_action(realmount, spec.tip_action_moves)
1933
+ else:
1934
+ spec = self._pipette_handler.plan_lt_pick_up_tip(
1935
+ realmount,
1936
+ instrument.nozzle_manager.current_configuration.tip_count,
1937
+ presses,
1938
+ increment,
1939
+ )
1940
+ await self._force_pick_up_tip(realmount, spec)
1941
+
1942
+ # neighboring tips tend to get stuck in the space between
1943
+ # the volume chamber and the drop-tip sleeve on p1000.
1944
+ # This extra shake ensures those tips are removed
1945
+ for rel_point, speed in spec.shake_off_moves:
1946
+ await self.move_rel(realmount, rel_point, speed=speed)
1947
+
1948
+ if isinstance(self._backend, OT3Simulator):
1949
+ self._backend._update_tip_state(realmount, True)
1950
+
1951
+ # fixme: really only need this during labware position check so user
1952
+ # can verify if a tip is properly attached
1953
+ if spec.ending_z_retract_distance:
1954
+ await self.move_rel(
1955
+ realmount, top_types.Point(z=spec.ending_z_retract_distance)
1956
+ )
1957
+
1958
+ async def _move_to_plunger_bottom(
1959
+ self,
1960
+ mount: OT3Mount,
1961
+ rate: float,
1962
+ acquire_lock: bool = True,
1963
+ check_current_vol: bool = True,
1964
+ ) -> None:
1965
+ """
1966
+ Move an instrument's plunger to its bottom position, while no liquids
1967
+ are held by said instrument.
1968
+
1969
+ Possible events where this occurs:
1970
+
1971
+ 1. After homing the plunger
1972
+ 2. After picking up a new tip
1973
+ 3. Between a blow-out and an aspiration (eg: re-using tips)
1974
+
1975
+ Three possible physical tip states when this happens:
1976
+
1977
+ 1. no tip on pipette
1978
+ 2. empty and dry (unused) tip on pipette
1979
+ 3. empty and wet (used) tip on pipette
1980
+
1981
+ With wet tips, the primary concern is leftover droplets inside the tip.
1982
+ These droplets ideally only move down and out of the tip, not up into the tip.
1983
+ Therefore, it is preferable to use the slower "aspirate" speed when
1984
+ moving the plunger up after a blow-out.
1985
+
1986
+ All other situations, moving at the max speed is preferable, to save time.
1987
+ """
1988
+ checked_mount = OT3Mount.from_mount(mount)
1989
+ instrument = self._pipette_handler.get_pipette(checked_mount)
1990
+ if check_current_vol and instrument.current_volume > 0:
1991
+ raise RuntimeError("cannot position plunger while holding liquid")
1992
+ # target position is plunger BOTTOM
1993
+ target_pos = target_position_from_plunger(
1994
+ OT3Mount.from_mount(mount),
1995
+ instrument.plunger_positions.bottom,
1996
+ self._current_position,
1997
+ )
1998
+ pip_ax = Axis.of_main_tool_actuator(checked_mount)
1999
+ # save time while moving down by using max speed
2000
+ max_speeds = self.config.motion_settings.default_max_speed
2001
+ speed_down = max_speeds[self.gantry_load][OT3AxisKind.P]
2002
+ # upward moves can be max speed, or aspirate speed
2003
+ # use the (slower) aspirate if there is a tip and we're following a blow-out
2004
+ plunger_is_below_bottom_pos = (
2005
+ self._current_position[pip_ax] > instrument.plunger_positions.bottom
2006
+ )
2007
+ if instrument.has_tip_length and plunger_is_below_bottom_pos:
2008
+ # using slower aspirate flow-rate, to avoid pulling droplets up
2009
+ speed_up = self._pipette_handler.plunger_speed(
2010
+ instrument, instrument.aspirate_flow_rate, "aspirate"
2011
+ )
2012
+ else:
2013
+ # either no tip, or plunger just homed, so tip is dry
2014
+ speed_up = max_speeds[self.gantry_load][OT3AxisKind.P]
2015
+ # IMPORTANT: Here is our backlash compensation.
2016
+ # The plunger is pre-loaded in the "aspirate" direction
2017
+ backlash_pos = target_pos.copy()
2018
+ backlash_pos[pip_ax] += instrument.backlash_distance
2019
+ # NOTE: plunger position (mm) decreases up towards homing switch
2020
+ # NOTE: if already at BOTTOM, we still need to run backlash-compensation movement,
2021
+ # because we do not know if we arrived at BOTTOM from above or below.
2022
+ async with self._backend.motor_current(
2023
+ run_currents={
2024
+ pip_ax: instrument.config.plunger_homing_configurations.current
2025
+ }
2026
+ ):
2027
+ if self._current_position[pip_ax] < backlash_pos[pip_ax]:
2028
+ await self._move(
2029
+ backlash_pos,
2030
+ speed=(speed_down * rate),
2031
+ acquire_lock=acquire_lock,
2032
+ )
2033
+ # NOTE: This should ALWAYS be moving UP.
2034
+ # There should never be a time that this function is called and
2035
+ # the plunger doesn't physically move UP into it's BOTTOM position.
2036
+ # This is to make sure we are always engaged at the beginning of aspirate.
2037
+ await self._move(
2038
+ target_pos,
2039
+ speed=(speed_up * rate),
2040
+ acquire_lock=acquire_lock,
2041
+ )
2042
+
2043
+ async def _move_to_plunger_top_for_liquid_probe(
2044
+ self,
2045
+ mount: OT3Mount,
2046
+ rate: float,
2047
+ acquire_lock: bool = True,
2048
+ ) -> None:
2049
+ """
2050
+ Move an instrument's plunger to the top, to prepare for a following
2051
+ liquid probe action.
2052
+
2053
+ The plunger backlash distance (mm) is used to ensure the plunger is pre-loaded
2054
+ in the downward direction. This means that the final position will not be
2055
+ the plunger's configured "top" position, but "top" plus the "backlashDistance".
2056
+ """
2057
+ max_speeds = self.config.motion_settings.default_max_speed
2058
+ speed = max_speeds[self.gantry_load][OT3AxisKind.P]
2059
+ instrument = self._pipette_handler.get_pipette(mount)
2060
+ top_plunger_pos = target_position_from_plunger(
2061
+ OT3Mount.from_mount(mount),
2062
+ instrument.plunger_positions.top,
2063
+ self._current_position,
2064
+ )
2065
+ target_pos = top_plunger_pos.copy()
2066
+ target_pos[Axis.of_main_tool_actuator(mount)] += instrument.backlash_distance
2067
+ await self._move(top_plunger_pos, speed=speed * rate, acquire_lock=acquire_lock)
2068
+ # NOTE: This should ALWAYS be moving DOWN.
2069
+ # There should never be a time that this function is called and
2070
+ # the plunger doesn't physically move DOWN.
2071
+ # This is to make sure we are always engaged at the beginning of liquid-probe.
2072
+ await self._move(target_pos, speed=speed * rate, acquire_lock=acquire_lock)
2073
+
2074
+ async def configure_for_volume(
2075
+ self, mount: Union[top_types.Mount, OT3Mount], volume: float
2076
+ ) -> None:
2077
+ checked_mount = OT3Mount.from_mount(mount)
2078
+ await self._pipette_handler.configure_for_volume(checked_mount, volume)
2079
+
2080
+ async def set_liquid_class(
2081
+ self, mount: Union[top_types.Mount, OT3Mount], liquid_class: str
2082
+ ) -> None:
2083
+ checked_mount = OT3Mount.from_mount(mount)
2084
+ await self._pipette_handler.set_liquid_class(checked_mount, liquid_class)
2085
+
2086
+ # Pipette action API
2087
+ async def prepare_for_aspirate(
2088
+ self, mount: Union[top_types.Mount, OT3Mount], rate: float = 1.0
2089
+ ) -> None:
2090
+ """Prepare the pipette for aspiration."""
2091
+ checked_mount = OT3Mount.from_mount(mount)
2092
+ instrument = self._pipette_handler.get_pipette(checked_mount)
2093
+ self._pipette_handler.ready_for_tip_action(
2094
+ instrument, HardwareAction.PREPARE_ASPIRATE, checked_mount
2095
+ )
2096
+ if instrument.current_volume == 0:
2097
+ await self._move_to_plunger_bottom(checked_mount, rate)
2098
+ instrument.ready_to_aspirate = True
2099
+
2100
+ async def aspirate(
2101
+ self,
2102
+ mount: Union[top_types.Mount, OT3Mount],
2103
+ volume: Optional[float] = None,
2104
+ rate: float = 1.0,
2105
+ correction_volume: float = 0.0,
2106
+ ) -> None:
2107
+ """
2108
+ Aspirate a volume of liquid (in microliters/uL) using this pipette."""
2109
+ realmount = OT3Mount.from_mount(mount)
2110
+ aspirate_spec = self._pipette_handler.plan_check_aspirate(
2111
+ mount=realmount,
2112
+ volume=volume,
2113
+ rate=rate,
2114
+ correction_volume=correction_volume,
2115
+ )
2116
+ if not aspirate_spec:
2117
+ return
2118
+
2119
+ target_pos = target_position_from_plunger(
2120
+ realmount,
2121
+ aspirate_spec.plunger_distance,
2122
+ self._current_position,
2123
+ )
2124
+
2125
+ try:
2126
+ await self._backend.set_active_current(
2127
+ {aspirate_spec.axis: aspirate_spec.current}
2128
+ )
2129
+ async with self.restore_system_constrants():
2130
+ await self.set_system_constraints_for_plunger_acceleration(
2131
+ realmount, aspirate_spec.acceleration
2132
+ )
2133
+ await self._move(
2134
+ target_pos,
2135
+ speed=aspirate_spec.speed,
2136
+ home_flagged_axes=False,
2137
+ )
2138
+ except Exception:
2139
+ self._log.exception("Aspirate failed")
2140
+ aspirate_spec.instr.set_current_volume(0)
2141
+ raise
2142
+ else:
2143
+ aspirate_spec.instr.add_current_volume(aspirate_spec.volume)
2144
+
2145
+ async def dispense(
2146
+ self,
2147
+ mount: Union[top_types.Mount, OT3Mount],
2148
+ volume: Optional[float] = None,
2149
+ rate: float = 1.0,
2150
+ push_out: Optional[float] = None,
2151
+ correction_volume: float = 0.0,
2152
+ is_full_dispense: bool = False,
2153
+ ) -> None:
2154
+ """
2155
+ Dispense a volume of liquid in microliters(uL) using this pipette."""
2156
+ realmount = OT3Mount.from_mount(mount)
2157
+ dispense_spec = self._pipette_handler.plan_check_dispense(
2158
+ mount=realmount,
2159
+ volume=volume,
2160
+ rate=rate,
2161
+ push_out=push_out,
2162
+ is_full_dispense=is_full_dispense,
2163
+ correction_volume=correction_volume,
2164
+ )
2165
+ if not dispense_spec:
2166
+ return
2167
+ target_pos = target_position_from_plunger(
2168
+ realmount,
2169
+ dispense_spec.plunger_distance,
2170
+ self._current_position,
2171
+ )
2172
+
2173
+ try:
2174
+ await self._backend.set_active_current(
2175
+ {dispense_spec.axis: dispense_spec.current}
2176
+ )
2177
+ async with self.restore_system_constrants():
2178
+ await self.set_system_constraints_for_plunger_acceleration(
2179
+ realmount, dispense_spec.acceleration
2180
+ )
2181
+ await self._move(
2182
+ target_pos,
2183
+ speed=dispense_spec.speed,
2184
+ home_flagged_axes=False,
2185
+ )
2186
+ except Exception:
2187
+ self._log.exception("Dispense failed")
2188
+ dispense_spec.instr.set_current_volume(0)
2189
+ raise
2190
+ else:
2191
+ dispense_spec.instr.remove_current_volume(dispense_spec.volume)
2192
+ bottom = dispense_spec.instr.plunger_positions.bottom
2193
+ plunger_target_pos = target_pos[Axis.of_main_tool_actuator(realmount)]
2194
+ if plunger_target_pos > bottom:
2195
+ dispense_spec.instr.ready_to_aspirate = False
2196
+
2197
+ async def blow_out(
2198
+ self,
2199
+ mount: Union[top_types.Mount, OT3Mount],
2200
+ volume: Optional[float] = None,
2201
+ ) -> None:
2202
+ """
2203
+ Force any remaining liquid to dispense. The liquid will be dispensed at
2204
+ the current location of pipette
2205
+ """
2206
+ realmount = OT3Mount.from_mount(mount)
2207
+ instrument = self._pipette_handler.get_pipette(realmount)
2208
+ blowout_spec = self._pipette_handler.plan_check_blow_out(realmount, volume)
2209
+
2210
+ max_blowout_pos = instrument.plunger_positions.blow_out
2211
+ # start at the bottom position and move additional distance
2212
+ # determined by plan_check_blow_out
2213
+ blowout_distance = (
2214
+ instrument.plunger_positions.bottom + blowout_spec.plunger_distance
2215
+ )
2216
+ if blowout_distance > max_blowout_pos:
2217
+ raise ValueError(
2218
+ f"Blow out distance exceeds plunger position limit: blowout dist = {blowout_distance}, "
2219
+ f"max blowout distance = {max_blowout_pos}"
2220
+ )
2221
+
2222
+ await self._backend.set_active_current(
2223
+ {blowout_spec.axis: blowout_spec.current}
2224
+ )
2225
+
2226
+ target_pos = target_position_from_plunger(
2227
+ realmount,
2228
+ blowout_distance,
2229
+ self._current_position,
2230
+ )
2231
+
2232
+ try:
2233
+ async with self.restore_system_constrants():
2234
+ await self.set_system_constraints_for_plunger_acceleration(
2235
+ realmount, blowout_spec.acceleration
2236
+ )
2237
+ await self._move(
2238
+ target_pos,
2239
+ speed=blowout_spec.speed,
2240
+ home_flagged_axes=False,
2241
+ )
2242
+ except Exception:
2243
+ self._log.exception("Blow out failed")
2244
+ raise
2245
+ finally:
2246
+ blowout_spec.instr.set_current_volume(0)
2247
+ blowout_spec.instr.ready_to_aspirate = False
2248
+
2249
+ @contextlib.asynccontextmanager
2250
+ async def _high_throughput_check_tip(self) -> AsyncIterator[None]:
2251
+ """Tip action required for high throughput pipettes to get tip status."""
2252
+ instrument = self._pipette_handler.get_pipette(OT3Mount.LEFT)
2253
+ tip_presence_check_target = instrument.tip_presence_check_dist_mm
2254
+
2255
+ # if position is not known, home gear motors before any potential movement
2256
+ if self._backend.gear_motor_position is None:
2257
+ await self.home_gear_motors()
2258
+
2259
+ tip_motor_pos_float = self._backend.gear_motor_position or 0.0
2260
+
2261
+ # only move tip motors if they are not already below the sensor
2262
+ if tip_motor_pos_float < tip_presence_check_target:
2263
+ await self._backend.tip_action(
2264
+ origin=tip_motor_pos_float,
2265
+ targets=[(tip_presence_check_target, 400)],
2266
+ )
2267
+ try:
2268
+ yield
2269
+ finally:
2270
+ await self.home_gear_motors()
2271
+
2272
+ async def get_tip_presence_status(
2273
+ self,
2274
+ mount: Union[top_types.Mount, OT3Mount],
2275
+ follow_singular_sensor: Optional[InstrumentProbeType] = None,
2276
+ ) -> TipStateType:
2277
+ """
2278
+ Check tip presence status. If a high throughput pipette is present,
2279
+ move the tip motors down before checking the sensor status.
2280
+ """
2281
+ async with self._motion_lock:
2282
+ real_mount = OT3Mount.from_mount(mount)
2283
+ async with contextlib.AsyncExitStack() as stack:
2284
+ if real_mount == OT3Mount.LEFT and self._gantry_load in [
2285
+ GantryLoad.HIGH_THROUGHPUT_1000,
2286
+ GantryLoad.HIGH_THROUGHPUT_200,
2287
+ ]:
2288
+ await stack.enter_async_context(self._high_throughput_check_tip())
2289
+ result = await self._backend.get_tip_status(
2290
+ real_mount, follow_singular_sensor
2291
+ )
2292
+ return result
2293
+
2294
+ async def verify_tip_presence(
2295
+ self,
2296
+ mount: Union[top_types.Mount, OT3Mount],
2297
+ expected: TipStateType,
2298
+ follow_singular_sensor: Optional[InstrumentProbeType] = None,
2299
+ ) -> None:
2300
+ real_mount = OT3Mount.from_mount(mount)
2301
+ status = await self.get_tip_presence_status(real_mount, follow_singular_sensor)
2302
+ if status != expected:
2303
+ raise FailedTipStateCheck(expected, status)
2304
+
2305
+ async def _force_pick_up_tip(
2306
+ self, mount: OT3Mount, pipette_spec: TipActionSpec
2307
+ ) -> None:
2308
+ for press in pipette_spec.tip_action_moves:
2309
+ async with self._backend.motor_current(run_currents=press.currents):
2310
+ target = target_position_from_relative(
2311
+ mount, top_types.Point(z=press.distance), self._current_position
2312
+ )
2313
+ if press.distance < 0:
2314
+ # we expect a stall has happened during a downward movement into the tiprack, so
2315
+ # we want to update the motor estimation
2316
+ await self._move(target, speed=press.speed, expect_stalls=True)
2317
+ await self._update_position_estimation([Axis.by_mount(mount)])
2318
+ else:
2319
+ # we should not ignore stalls that happen during the retract part of the routine
2320
+ await self._move(target, speed=press.speed, expect_stalls=False)
2321
+
2322
+ async def _tip_motor_action(
2323
+ self, mount: OT3Mount, pipette_spec: List[TipActionMoveSpec]
2324
+ ) -> None:
2325
+ # currents should be the same for each move in tip motor pickup
2326
+ assert [move.currents == pipette_spec[0].currents for move in pipette_spec]
2327
+ currents = pipette_spec[0].currents
2328
+ # Move to pickup position
2329
+ async with self._backend.motor_current(run_currents=currents):
2330
+ if self._backend.gear_motor_position is None:
2331
+ # home gear motor if position not known
2332
+ await self.home_gear_motors()
2333
+ gear_origin_float = self._backend.gear_motor_position or 0.0
2334
+
2335
+ move_targets = [
2336
+ (move_segment.distance, move_segment.speed or 400)
2337
+ for move_segment in pipette_spec
2338
+ ]
2339
+ await self._backend.tip_action(
2340
+ origin=gear_origin_float, targets=move_targets
2341
+ )
2342
+ await self.home_gear_motors()
2343
+
2344
+ async def pick_up_tip(
2345
+ self,
2346
+ mount: Union[top_types.Mount, OT3Mount],
2347
+ tip_length: float,
2348
+ presses: Optional[int] = None,
2349
+ increment: Optional[float] = None,
2350
+ prep_after: bool = True,
2351
+ ) -> None:
2352
+ """Pick up tip from current location."""
2353
+ realmount = OT3Mount.from_mount(mount)
2354
+ instrument = self._pipette_handler.get_pipette(realmount)
2355
+
2356
+ def add_tip_to_instr() -> None:
2357
+ instrument.add_tip(tip_length=tip_length)
2358
+ instrument.set_current_volume(0)
2359
+
2360
+ await self._move_to_plunger_bottom(realmount, rate=1.0)
2361
+
2362
+ await self.tip_pickup_moves(mount, presses, increment)
2363
+
2364
+ add_tip_to_instr()
2365
+
2366
+ if prep_after:
2367
+ await self.prepare_for_aspirate(realmount)
2368
+
2369
+ def set_current_tiprack_diameter(
2370
+ self, mount: Union[top_types.Mount, OT3Mount], tiprack_diameter: float
2371
+ ) -> None:
2372
+ instrument = self._pipette_handler.get_pipette(OT3Mount.from_mount(mount))
2373
+ self._log.info(
2374
+ "Updating tip rack diameter on pipette mount: "
2375
+ f"{mount.name}, tip diameter: {tiprack_diameter} mm"
2376
+ )
2377
+ instrument.current_tiprack_diameter = tiprack_diameter
2378
+
2379
+ def set_working_volume(
2380
+ self, mount: Union[top_types.Mount, OT3Mount], tip_volume: float
2381
+ ) -> None:
2382
+ instrument = self._pipette_handler.get_pipette(OT3Mount.from_mount(mount))
2383
+ self._log.info(
2384
+ "Updating working volume on pipette mount:"
2385
+ f"{mount.name}, tip volume: {tip_volume} ul"
2386
+ )
2387
+ instrument.working_volume = tip_volume
2388
+
2389
+ async def tip_drop_moves(
2390
+ self,
2391
+ mount: Union[top_types.Mount, OT3Mount],
2392
+ home_after: bool = False,
2393
+ ignore_plunger: bool = False,
2394
+ scrape_type: TipScrapeType = TipScrapeType.NONE,
2395
+ ) -> None:
2396
+ realmount = OT3Mount.from_mount(mount)
2397
+ if ignore_plunger is False:
2398
+ await self._move_to_plunger_bottom(
2399
+ realmount, rate=1.0, check_current_vol=False
2400
+ )
2401
+
2402
+ if self.gantry_load in [
2403
+ GantryLoad.HIGH_THROUGHPUT_1000,
2404
+ GantryLoad.HIGH_THROUGHPUT_200,
2405
+ ]:
2406
+ spec = self._pipette_handler.plan_ht_drop_tip()
2407
+ await self._tip_motor_action(realmount, spec.tip_action_moves)
2408
+ else:
2409
+ spec = self._pipette_handler.plan_lt_drop_tip(realmount, scrape_type)
2410
+ for move in spec.tip_action_moves:
2411
+ async with self._backend.motor_current(move.currents):
2412
+ if not move.scrape_axis:
2413
+ target_pos = target_position_from_plunger(
2414
+ realmount, move.distance, self._current_position
2415
+ )
2416
+ await self._move(
2417
+ target_pos,
2418
+ speed=move.speed,
2419
+ home_flagged_axes=False,
2420
+ )
2421
+ else:
2422
+ target_pos = OrderedDict(self._current_position)
2423
+ target_pos[move.scrape_axis] += move.distance
2424
+ self._log.info(f"Moving to target Pos: {target_pos}")
2425
+ await self._move(
2426
+ target_pos,
2427
+ speed=move.speed,
2428
+ home_flagged_axes=False,
2429
+ )
2430
+ for shake in spec.shake_off_moves:
2431
+ await self.move_rel(mount, shake[0], speed=shake[1])
2432
+
2433
+ # home mount axis
2434
+ if home_after:
2435
+ await self._home([Axis.by_mount(mount)])
2436
+
2437
+ # call this in case we're simulating:
2438
+ if isinstance(self._backend, OT3Simulator):
2439
+ self._backend._update_tip_state(realmount, False)
2440
+
2441
+ async def drop_tip(
2442
+ self, mount: Union[top_types.Mount, OT3Mount], home_after: bool = False
2443
+ ) -> None:
2444
+ """Drop tip at the current location."""
2445
+ await self.tip_drop_moves(mount=mount, home_after=home_after)
2446
+
2447
+ # todo(mm, 2024-10-17): Ideally, callers would be able to replicate the behavior
2448
+ # of this method via self.drop_tip_moves() plus other public methods. This
2449
+ # currently prevents that: there is no public equivalent for
2450
+ # instrument.set_current_volume().
2451
+ realmount = OT3Mount.from_mount(mount)
2452
+ instrument = self._pipette_handler.get_pipette(realmount)
2453
+ instrument.set_current_volume(0)
2454
+
2455
+ self.set_current_tiprack_diameter(mount, 0.0)
2456
+ self.remove_tip(mount)
2457
+
2458
+ async def clean_up(self) -> None:
2459
+ """Get the API ready to stop cleanly."""
2460
+ await self._backend.clean_up()
2461
+
2462
+ def critical_point_for(
2463
+ self,
2464
+ mount: Union[top_types.Mount, OT3Mount],
2465
+ cp_override: Optional[CriticalPoint] = None,
2466
+ ) -> top_types.Point:
2467
+ if mount == OT3Mount.GRIPPER:
2468
+ return self._gripper_handler.get_critical_point(cp_override)
2469
+ else:
2470
+ return self._pipette_handler.critical_point_for(
2471
+ OT3Mount.from_mount(mount), cp_override
2472
+ )
2473
+
2474
+ @property
2475
+ def hardware_pipettes(self) -> InstrumentsByMount[top_types.Mount]:
2476
+ # TODO (lc 12-5-2022) We should have ONE entry point into knowing
2477
+ # what pipettes are attached from the hardware controller.
2478
+ return {
2479
+ m.to_mount(): i
2480
+ for m, i in self._pipette_handler.hardware_instruments.items()
2481
+ if m != OT3Mount.GRIPPER
2482
+ }
2483
+
2484
+ @property
2485
+ def hardware_gripper(self) -> Optional[Gripper]:
2486
+ if not self.has_gripper():
2487
+ return None
2488
+ return self._gripper_handler.get_gripper()
2489
+
2490
+ @property
2491
+ def hardware_instruments(self) -> InstrumentsByMount[top_types.Mount]: # type: ignore
2492
+ # see comment in `protocols.instrument_configurer`
2493
+ # override required for type matching
2494
+ # Warning: don't use this in new code, used `hardware_pipettes` instead
2495
+ return self.hardware_pipettes
2496
+
2497
+ def get_attached_pipettes(self) -> Dict[top_types.Mount, PipetteDict]:
2498
+ return {
2499
+ m.to_mount(): pd
2500
+ for m, pd in self._pipette_handler.get_attached_instruments().items()
2501
+ if m != OT3Mount.GRIPPER
2502
+ }
2503
+
2504
+ def get_attached_instruments(self) -> Dict[top_types.Mount, PipetteDict]:
2505
+ # Warning: don't use this in new code, used `get_attached_pipettes` instead
2506
+ return self.get_attached_pipettes()
2507
+
2508
+ async def get_instrument_state(
2509
+ self,
2510
+ mount: Union[top_types.Mount, OT3Mount],
2511
+ ) -> PipetteStateDict:
2512
+ # TODO we should have a PipetteState that can be returned from
2513
+ # this function with additional state (such as critical points)
2514
+ realmount = OT3Mount.from_mount(mount)
2515
+ tip_attached = self._backend.current_tip_state(realmount)
2516
+ pipette_state_for_mount: PipetteStateDict = {
2517
+ "tip_detected": tip_attached if tip_attached is not None else False
2518
+ }
2519
+ return pipette_state_for_mount
2520
+
2521
+ def reset_instrument(
2522
+ self, mount: Union[top_types.Mount, OT3Mount, None] = None
2523
+ ) -> None:
2524
+ if mount:
2525
+ checked_mount: Optional[OT3Mount] = OT3Mount.from_mount(mount)
2526
+ else:
2527
+ checked_mount = None
2528
+ if checked_mount == OT3Mount.GRIPPER:
2529
+ self._gripper_handler.reset_gripper()
2530
+ else:
2531
+ self._pipette_handler.reset_instrument(checked_mount)
2532
+
2533
+ def get_instrument_offset(
2534
+ self, mount: Union[top_types.Mount, OT3Mount]
2535
+ ) -> Union[GripperCalibrationOffset, PipetteOffsetSummary, None]:
2536
+ """Get instrument calibration data."""
2537
+ # TODO (spp, 2023-04-19): We haven't introduced a 'calibration_offset' key in
2538
+ # PipetteDict because the dict is shared with OT2 pipettes which have
2539
+ # different offset type. Once we figure out if we want the calibration data
2540
+ # to be a part of the dict, this getter can be updated to fetch pipette offset
2541
+ # from the dict, or just remove this getter entirely.
2542
+
2543
+ ot3_mount = OT3Mount.from_mount(mount)
2544
+
2545
+ if ot3_mount == OT3Mount.GRIPPER:
2546
+ gripper_dict = self._gripper_handler.get_gripper_dict()
2547
+ return gripper_dict["calibration_offset"] if gripper_dict else None
2548
+ else:
2549
+ return self._pipette_handler.get_instrument_offset(mount=ot3_mount)
2550
+
2551
+ async def reset_instrument_offset(
2552
+ self, mount: Union[top_types.Mount, OT3Mount], to_default: bool = True
2553
+ ) -> None:
2554
+ """Reset the given instrument to system offsets."""
2555
+ checked_mount = OT3Mount.from_mount(mount)
2556
+ if checked_mount == OT3Mount.GRIPPER:
2557
+ self._gripper_handler.reset_instrument_offset(to_default)
2558
+ else:
2559
+ self._pipette_handler.reset_instrument_offset(checked_mount, to_default)
2560
+
2561
+ async def save_instrument_offset(
2562
+ self, mount: Union[top_types.Mount, OT3Mount], delta: top_types.Point
2563
+ ) -> Union[GripperCalibrationOffset, PipetteOffsetSummary]:
2564
+ """Save a new offset for a given instrument."""
2565
+ checked_mount = OT3Mount.from_mount(mount)
2566
+ if checked_mount == OT3Mount.GRIPPER:
2567
+ self._log.info(f"Saving instrument offset: {delta} for gripper")
2568
+ return self._gripper_handler.save_instrument_offset(delta)
2569
+ else:
2570
+ return self._pipette_handler.save_instrument_offset(checked_mount, delta)
2571
+
2572
+ async def save_module_offset(
2573
+ self, module_id: str, mount: OT3Mount, slot: str, offset: top_types.Point
2574
+ ) -> Optional[ModuleCalibrationOffset]:
2575
+ """Save a new offset for a given module."""
2576
+ module = self._backend.module_controls.get_module_by_module_id(module_id)
2577
+ if not module:
2578
+ self._log.warning(f"Could not save calibration: unknown module {module_id}")
2579
+ return None
2580
+ # TODO (ba, 2023-03-22): gripper_id and pipette_id should probably be combined to instrument_id
2581
+ if self._pipette_handler.has_pipette(mount):
2582
+ instrument_id = self._pipette_handler.get_pipette(mount).pipette_id
2583
+ elif mount == OT3Mount.GRIPPER and self._gripper_handler.has_gripper():
2584
+ instrument_id = self._gripper_handler.get_gripper().gripper_id
2585
+ else:
2586
+ self._log.warning(
2587
+ f"Could not save calibration: no instrument found for {mount}"
2588
+ )
2589
+ return None
2590
+ module_type = module.MODULE_TYPE
2591
+ self._log.info(
2592
+ f"Saving module offset: {offset} for module {module_type.name} {module_id}."
2593
+ )
2594
+ return self._backend.module_controls.save_module_offset(
2595
+ module_type, module_id, mount, slot, offset, instrument_id
2596
+ )
2597
+
2598
+ def get_module_calibration_offset(
2599
+ self, serial_number: str
2600
+ ) -> Optional[ModuleCalibrationOffset]:
2601
+ """Get the module calibration offset of a module."""
2602
+ module = self._backend.module_controls.get_module_by_module_id(serial_number)
2603
+ if not module:
2604
+ self._log.warning(
2605
+ f"Could not load calibration: unknown module {serial_number}"
2606
+ )
2607
+ return None
2608
+ module_type = module.MODULE_TYPE
2609
+ return self._backend.module_controls.load_module_offset(
2610
+ module_type, serial_number
2611
+ )
2612
+
2613
+ def get_attached_pipette(
2614
+ self, mount: Union[top_types.Mount, OT3Mount]
2615
+ ) -> PipetteDict:
2616
+ return self._pipette_handler.get_attached_instrument(OT3Mount.from_mount(mount))
2617
+
2618
+ def get_attached_instrument(
2619
+ self, mount: Union[top_types.Mount, OT3Mount]
2620
+ ) -> PipetteDict:
2621
+ # Warning: don't use this in new code, used `get_attached_pipette` instead
2622
+ return self.get_attached_pipette(mount)
2623
+
2624
+ @property
2625
+ def attached_instruments(self) -> Any:
2626
+ # Warning: don't use this in new code, used `attached_pipettes` instead
2627
+ return self.attached_pipettes
2628
+
2629
+ @property
2630
+ def attached_pipettes(self) -> Dict[top_types.Mount, PipetteDict]:
2631
+ return {
2632
+ m.to_mount(): d
2633
+ for m, d in self._pipette_handler.attached_instruments.items()
2634
+ if m != OT3Mount.GRIPPER
2635
+ }
2636
+
2637
+ @property
2638
+ def attached_gripper(self) -> Optional[GripperDict]:
2639
+ return self._gripper_handler.get_gripper_dict()
2640
+
2641
+ def has_gripper(self) -> bool:
2642
+ return self._gripper_handler.has_gripper()
2643
+
2644
+ def calibrate_plunger(
2645
+ self,
2646
+ mount: Union[top_types.Mount, OT3Mount],
2647
+ top: Optional[float] = None,
2648
+ bottom: Optional[float] = None,
2649
+ blow_out: Optional[float] = None,
2650
+ drop_tip: Optional[float] = None,
2651
+ ) -> None:
2652
+ self._pipette_handler.calibrate_plunger(
2653
+ OT3Mount.from_mount(mount), top, bottom, blow_out, drop_tip
2654
+ )
2655
+
2656
+ def set_flow_rate(
2657
+ self,
2658
+ mount: Union[top_types.Mount, OT3Mount],
2659
+ aspirate: Optional[float] = None,
2660
+ dispense: Optional[float] = None,
2661
+ blow_out: Optional[float] = None,
2662
+ ) -> None:
2663
+ return self._pipette_handler.set_flow_rate(
2664
+ OT3Mount.from_mount(mount), aspirate, dispense, blow_out
2665
+ )
2666
+
2667
+ def set_pipette_speed(
2668
+ self,
2669
+ mount: Union[top_types.Mount, OT3Mount],
2670
+ aspirate: Optional[float] = None,
2671
+ dispense: Optional[float] = None,
2672
+ blow_out: Optional[float] = None,
2673
+ ) -> None:
2674
+ self._pipette_handler.set_pipette_speed(
2675
+ OT3Mount.from_mount(mount), aspirate, dispense, blow_out
2676
+ )
2677
+
2678
+ def get_instrument_max_height(
2679
+ self,
2680
+ mount: Union[top_types.Mount, OT3Mount],
2681
+ critical_point: Optional[CriticalPoint] = None,
2682
+ ) -> float:
2683
+ carriage_pos = self.get_deck_from_machine(self._backend.home_position())
2684
+ pos_at_home = self._effector_pos_from_carriage_pos(
2685
+ OT3Mount.from_mount(mount), carriage_pos, critical_point
2686
+ )
2687
+
2688
+ return pos_at_home[Axis.by_mount(mount)]
2689
+
2690
+ async def update_nozzle_configuration_for_mount(
2691
+ self,
2692
+ mount: Union[top_types.Mount, OT3Mount],
2693
+ back_left_nozzle: Optional[str] = None,
2694
+ front_right_nozzle: Optional[str] = None,
2695
+ starting_nozzle: Optional[str] = None,
2696
+ ) -> None:
2697
+ """
2698
+ The expectation of this function is that the back_left_nozzle/front_right_nozzle are the two corners
2699
+ of a rectangle of nozzles. A call to this function that does not follow that schema will result
2700
+ in an error.
2701
+
2702
+ :param mount: A robot mount that the instrument is on.
2703
+ :param back_left_nozzle: A string representing a nozzle name of the form <LETTER><NUMBER> such as 'A1'.
2704
+ :param front_right_nozzle: A string representing a nozzle name of the form <LETTER><NUMBER> such as 'A1'.
2705
+ :param starting_nozzle: A string representing the starting nozzle which will be used as the critical point
2706
+ of the pipette nozzle configuration. By default, the back left nozzle will be the starting nozzle if
2707
+ none is provided.
2708
+ :return: None.
2709
+
2710
+ If none of the nozzle parameters are provided, the nozzle configuration will be reset to default.
2711
+ """
2712
+ if not back_left_nozzle and not front_right_nozzle and not starting_nozzle:
2713
+ await self._pipette_handler.reset_nozzle_configuration(
2714
+ OT3Mount.from_mount(mount)
2715
+ )
2716
+ else:
2717
+ assert back_left_nozzle and front_right_nozzle
2718
+ await self._pipette_handler.update_nozzle_configuration(
2719
+ OT3Mount.from_mount(mount),
2720
+ back_left_nozzle,
2721
+ front_right_nozzle,
2722
+ starting_nozzle,
2723
+ )
2724
+
2725
+ def add_tip(
2726
+ self, mount: Union[top_types.Mount, OT3Mount], tip_length: float
2727
+ ) -> None:
2728
+ self._pipette_handler.add_tip(OT3Mount.from_mount(mount), tip_length)
2729
+
2730
+ def cache_tip(
2731
+ self, mount: Union[top_types.Mount, OT3Mount], tip_length: float
2732
+ ) -> None:
2733
+ self._pipette_handler.cache_tip(OT3Mount.from_mount(mount), tip_length)
2734
+
2735
+ def remove_tip(self, mount: Union[top_types.Mount, OT3Mount]) -> None:
2736
+ self._pipette_handler.remove_tip(OT3Mount.from_mount(mount))
2737
+
2738
+ def add_gripper_probe(self, probe: GripperProbe) -> None:
2739
+ self._gripper_handler.add_probe(probe)
2740
+
2741
+ def remove_gripper_probe(self) -> None:
2742
+ self._gripper_handler.remove_probe()
2743
+
2744
+ @staticmethod
2745
+ def liquid_probe_non_responsive_z_distance(
2746
+ z_speed: float, samples_for_baselining: int, sample_time_sec: float
2747
+ ) -> float:
2748
+ """Calculate the Z distance travelled where the LLD pass will be unresponsive."""
2749
+ # NOTE: (sigler) Here lye some magic numbers.
2750
+ # The Z axis probing motion uses the first 20 samples to calculate
2751
+ # a baseline for all following samples, making the very beginning of
2752
+ # that Z motion unable to detect liquid. The sensor is configured for
2753
+ # 4ms sample readings, and so we then assume it takes ~80ms to complete.
2754
+ # If the Z is moving at 5mm/sec, then ~80ms equates to ~0.4mm
2755
+ baseline_duration_sec = samples_for_baselining * sample_time_sec
2756
+ non_responsive_z_mm = baseline_duration_sec * z_speed
2757
+ return non_responsive_z_mm
2758
+
2759
+ async def _liquid_probe_pass(
2760
+ self,
2761
+ mount: OT3Mount,
2762
+ probe_settings: LiquidProbeSettings,
2763
+ probe: InstrumentProbeType,
2764
+ p_travel: float,
2765
+ z_offset_for_plunger_prep: float,
2766
+ force_both_sensors: bool = False,
2767
+ response_queue: Optional[PipetteSensorResponseQueue] = None,
2768
+ ) -> float:
2769
+ plunger_direction = -1 if probe_settings.aspirate_while_sensing else 1
2770
+ end_z = await self._backend.liquid_probe(
2771
+ mount,
2772
+ p_travel,
2773
+ probe_settings.mount_speed,
2774
+ (probe_settings.plunger_speed * plunger_direction),
2775
+ probe_settings.sensor_threshold_pascals,
2776
+ probe_settings.plunger_impulse_time,
2777
+ probe_settings.samples_for_baselining,
2778
+ z_offset_for_plunger_prep,
2779
+ probe=probe,
2780
+ force_both_sensors=force_both_sensors,
2781
+ response_queue=response_queue,
2782
+ )
2783
+ machine_pos = await self._backend.update_position()
2784
+ machine_pos[Axis.by_mount(mount)] = end_z
2785
+ deck_end_z = self.get_deck_from_machine(machine_pos)[Axis.by_mount(mount)]
2786
+ offset = offset_for_mount(
2787
+ mount,
2788
+ top_types.Point(*self._config.left_mount_offset),
2789
+ top_types.Point(*self._config.right_mount_offset),
2790
+ top_types.Point(*self._config.gripper_mount_offset),
2791
+ )
2792
+ cp = self.critical_point_for(mount, None)
2793
+ return deck_end_z + offset.z + cp.z
2794
+
2795
+ async def liquid_probe( # noqa: C901
2796
+ self,
2797
+ mount: Union[top_types.Mount, OT3Mount],
2798
+ max_z_dist: float,
2799
+ probe_settings: Optional[LiquidProbeSettings] = None,
2800
+ probe: Optional[InstrumentProbeType] = None,
2801
+ force_both_sensors: bool = False,
2802
+ response_queue: Optional[PipetteSensorResponseQueue] = None,
2803
+ ) -> float:
2804
+ """Search for and return liquid level height.
2805
+
2806
+ This function begins by moving the mount 2 mm upward to protect against a case where the tip starts right at a
2807
+ liquid meniscus.
2808
+ After this, the mount and plunger motors will move simultaneously while
2809
+ reading from the pressure sensor.
2810
+
2811
+ If the move is completed without the specified threshold being triggered, a
2812
+ PipetteLiquidNotFoundError error will be thrown.
2813
+
2814
+ Otherwise, the function will stop moving once the threshold is triggered,
2815
+ and return the position of the
2816
+ z axis in deck coordinates, as well as the encoder position, where
2817
+ the liquid was found.
2818
+ """
2819
+
2820
+ checked_mount = OT3Mount.from_mount(mount)
2821
+ instrument = self._pipette_handler.get_pipette(checked_mount)
2822
+ self._pipette_handler.ready_for_tip_action(
2823
+ instrument, HardwareAction.LIQUID_PROBE, checked_mount
2824
+ )
2825
+ # default to using all available sensors
2826
+ if probe:
2827
+ checked_probe = probe
2828
+ else:
2829
+ checked_probe = (
2830
+ InstrumentProbeType.BOTH
2831
+ if instrument.channels > 1
2832
+ else InstrumentProbeType.PRIMARY
2833
+ )
2834
+
2835
+ if not probe_settings:
2836
+ probe_settings = deepcopy(self.config.liquid_sense)
2837
+
2838
+ # We need to significantly slow down the 96 channel liquid probe
2839
+ if self.gantry_load in [
2840
+ GantryLoad.HIGH_THROUGHPUT_1000,
2841
+ GantryLoad.HIGH_THROUGHPUT_200,
2842
+ ]:
2843
+ max_plunger_speed = self.config.motion_settings.max_speed_discontinuity[
2844
+ self.gantry_load
2845
+ ][OT3AxisKind.P]
2846
+ probe_settings.plunger_speed = min(
2847
+ max_plunger_speed, probe_settings.plunger_speed
2848
+ )
2849
+
2850
+ starting_position = await self.gantry_position(checked_mount, refresh=True)
2851
+
2852
+ sensor_baseline_plunger_move_mm = (
2853
+ probe_settings.plunger_impulse_time * probe_settings.plunger_speed
2854
+ )
2855
+ total_plunger_axis_mm = (
2856
+ instrument.plunger_positions.bottom - instrument.plunger_positions.top
2857
+ )
2858
+ max_allowed_plunger_distance_mm = total_plunger_axis_mm - (
2859
+ instrument.backlash_distance + sensor_baseline_plunger_move_mm
2860
+ )
2861
+ # height where probe action will begin
2862
+ sensor_baseline_z_move_mm = OT3API.liquid_probe_non_responsive_z_distance(
2863
+ probe_settings.mount_speed,
2864
+ probe_settings.samples_for_baselining,
2865
+ probe_settings.sample_time_sec,
2866
+ )
2867
+ z_offset_per_pass = (
2868
+ sensor_baseline_z_move_mm + probe_settings.z_overlap_between_passes_mm
2869
+ )
2870
+
2871
+ # height that is considered safe to reset the plunger without disturbing liquid
2872
+ # this usually needs to at least 1-2mm from liquid, to avoid splashes from air
2873
+ z_offset_for_plunger_prep = max(
2874
+ probe_settings.plunger_reset_offset, z_offset_per_pass
2875
+ )
2876
+
2877
+ async def prep_plunger_for_probe_move(
2878
+ position: top_types.Point, aspirate_while_sensing: bool
2879
+ ) -> None:
2880
+ # safe distance so we don't accidentally aspirate liquid if we're already close to liquid
2881
+ mount_pos_for_plunger_prep = top_types.Point(
2882
+ position.x,
2883
+ position.y,
2884
+ position.z + z_offset_for_plunger_prep,
2885
+ )
2886
+ # Prep the plunger
2887
+ await self.move_to(checked_mount, mount_pos_for_plunger_prep)
2888
+ if aspirate_while_sensing:
2889
+ await self._move_to_plunger_bottom(checked_mount, rate=1)
2890
+ else:
2891
+ await self._move_to_plunger_top_for_liquid_probe(checked_mount, rate=1)
2892
+
2893
+ error: Optional[PipetteLiquidNotFoundError] = None
2894
+ current_position = await self.gantry_position(checked_mount, refresh=True)
2895
+ # starting_position.z + z_distance of pass - pos.z should be < max_z_dist
2896
+ # due to rounding errors this can get caught in an infinite loop when the distance is almost equal
2897
+ # so we check to see if they're within 0.01 which is 1/5th the minimum movement distance from move_utils.py
2898
+ while (starting_position.z - current_position.z) < (max_z_dist - 0.01):
2899
+ await prep_plunger_for_probe_move(
2900
+ position=current_position,
2901
+ aspirate_while_sensing=probe_settings.aspirate_while_sensing,
2902
+ )
2903
+
2904
+ # overlap amount we want to use between passes
2905
+ pass_start_pos = top_types.Point(
2906
+ current_position.x,
2907
+ current_position.y,
2908
+ current_position.z + z_offset_per_pass,
2909
+ )
2910
+
2911
+ total_remaining_z_dist = pass_start_pos.z - (
2912
+ starting_position.z - max_z_dist
2913
+ )
2914
+ finish_probe_move_duration = (
2915
+ total_remaining_z_dist / probe_settings.mount_speed
2916
+ )
2917
+ finish_probe_plunger_distance_mm = (
2918
+ finish_probe_move_duration * probe_settings.plunger_speed
2919
+ )
2920
+ plunger_travel_mm = min(
2921
+ finish_probe_plunger_distance_mm, max_allowed_plunger_distance_mm
2922
+ )
2923
+ try:
2924
+ # move to where we want to start a pass and run a pass
2925
+ await self.move_to(checked_mount, pass_start_pos)
2926
+ height = await self._liquid_probe_pass(
2927
+ checked_mount,
2928
+ probe_settings,
2929
+ checked_probe,
2930
+ plunger_travel_mm + sensor_baseline_plunger_move_mm,
2931
+ z_offset_for_plunger_prep,
2932
+ force_both_sensors,
2933
+ response_queue,
2934
+ )
2935
+ # if we made it here without an error we found the liquid
2936
+ error = None
2937
+ break
2938
+ except PipetteLiquidNotFoundError as lnfe:
2939
+ error = lnfe
2940
+ current_position = await self.gantry_position(checked_mount, refresh=True)
2941
+ await self.move_to(checked_mount, starting_position + top_types.Point(z=2))
2942
+ await self.prepare_for_aspirate(checked_mount)
2943
+ await self.move_to(checked_mount, starting_position)
2944
+ if error is not None:
2945
+ # if we never found liquid raise an error
2946
+ raise error
2947
+ return height
2948
+
2949
+ async def capacitive_probe(
2950
+ self,
2951
+ mount: OT3Mount,
2952
+ moving_axis: Axis,
2953
+ target_pos: float,
2954
+ pass_settings: CapacitivePassSettings,
2955
+ retract_after: bool = True,
2956
+ probe: Optional[InstrumentProbeType] = None,
2957
+ ) -> Tuple[float, bool]:
2958
+ if moving_axis not in [
2959
+ Axis.X,
2960
+ Axis.Y,
2961
+ ] and moving_axis != Axis.by_mount(mount):
2962
+ raise RuntimeError(
2963
+ "Probing must be done with a gantry axis or the mount of the sensing"
2964
+ " tool"
2965
+ )
2966
+
2967
+ here = await self.gantry_position(mount, refresh=True)
2968
+ origin_pos = moving_axis.of_point(here)
2969
+ if origin_pos < target_pos:
2970
+ pass_start = target_pos - pass_settings.prep_distance_mm
2971
+ pass_distance = (
2972
+ pass_settings.prep_distance_mm + pass_settings.max_overrun_distance_mm
2973
+ )
2974
+ else:
2975
+ pass_start = target_pos + pass_settings.prep_distance_mm
2976
+ pass_distance = -1.0 * (
2977
+ pass_settings.prep_distance_mm + pass_settings.max_overrun_distance_mm
2978
+ )
2979
+ machine_pass_distance = moving_axis.of_point(
2980
+ machine_vector_from_deck_vector(
2981
+ moving_axis.set_in_point(top_types.Point(0, 0, 0), pass_distance),
2982
+ self._robot_calibration.deck_calibration.attitude,
2983
+ )
2984
+ )
2985
+ pass_start_pos = moving_axis.set_in_point(here, pass_start)
2986
+ await self.move_to(mount, pass_start_pos)
2987
+ if probe is None:
2988
+ if mount == OT3Mount.GRIPPER:
2989
+ gripper_probe = self._gripper_handler.get_attached_probe()
2990
+ assert gripper_probe
2991
+ probe = GripperProbe.to_type(gripper_probe)
2992
+ else:
2993
+ # default to primary (rear) probe
2994
+ probe = InstrumentProbeType.PRIMARY
2995
+ contact = await self._backend.capacitive_probe(
2996
+ mount,
2997
+ moving_axis,
2998
+ machine_pass_distance,
2999
+ pass_settings.speed_mm_per_s,
3000
+ pass_settings.sensor_threshold_pf,
3001
+ probe,
3002
+ )
3003
+ end_pos = await self.gantry_position(mount, refresh=True)
3004
+ if retract_after:
3005
+ await self.move_to(mount, pass_start_pos)
3006
+ return moving_axis.of_point(end_pos), contact
3007
+
3008
+ async def capacitive_sweep(
3009
+ self,
3010
+ mount: OT3Mount,
3011
+ moving_axis: Axis,
3012
+ begin: top_types.Point,
3013
+ end: top_types.Point,
3014
+ speed_mm_s: float,
3015
+ ) -> List[float]:
3016
+ if moving_axis not in [
3017
+ Axis.X,
3018
+ Axis.Y,
3019
+ ] and moving_axis != Axis.by_mount(mount):
3020
+ raise RuntimeError(
3021
+ "Probing must be done with a gantry axis or the mount of the sensing"
3022
+ " tool"
3023
+ )
3024
+ sweep_distance = moving_axis.of_point(
3025
+ machine_vector_from_deck_vector(
3026
+ end - begin,
3027
+ self._robot_calibration.deck_calibration.attitude,
3028
+ )
3029
+ )
3030
+
3031
+ await self.move_to(mount, begin)
3032
+ if mount == OT3Mount.GRIPPER:
3033
+ probe = self._gripper_handler.get_attached_probe()
3034
+ assert probe
3035
+ values = await self._backend.capacitive_pass(
3036
+ mount,
3037
+ moving_axis,
3038
+ sweep_distance,
3039
+ speed_mm_s,
3040
+ GripperProbe.to_type(probe),
3041
+ )
3042
+ else:
3043
+ values = await self._backend.capacitive_pass(
3044
+ mount,
3045
+ moving_axis,
3046
+ sweep_distance,
3047
+ speed_mm_s,
3048
+ probe=InstrumentProbeType.PRIMARY,
3049
+ )
3050
+ await self.move_to(mount, begin)
3051
+ return values
3052
+
3053
+ AMKey = TypeVar("AMKey")
3054
+
3055
+ async def aspirate_while_tracking(
3056
+ self,
3057
+ mount: Union[top_types.Mount, OT3Mount],
3058
+ z_distance: float,
3059
+ volume: float,
3060
+ flow_rate: float = 1.0,
3061
+ ) -> None:
3062
+ """
3063
+ Aspirate a volume of liquid (in microliters/uL) while moving the z axis synchronously.
3064
+
3065
+ :param mount: A robot mount that the instrument is on.
3066
+ :param z_distance: The distance the z axis will move during apsiration.
3067
+ :param volume: The volume of liquid to be aspirated.
3068
+ :param flow_rate: The flow rate to aspirate with.
3069
+ """
3070
+ realmount = OT3Mount.from_mount(mount)
3071
+ aspirate_spec = self._pipette_handler.plan_check_aspirate(
3072
+ realmount, volume, flow_rate
3073
+ )
3074
+ if not aspirate_spec:
3075
+ return
3076
+ target_pos = target_positions_from_plunger_tracking(
3077
+ realmount,
3078
+ aspirate_spec.plunger_distance,
3079
+ z_distance,
3080
+ self._current_position,
3081
+ )
3082
+ try:
3083
+ await self._backend.set_active_current(
3084
+ {aspirate_spec.axis: aspirate_spec.current}
3085
+ )
3086
+ async with self.restore_system_constrants():
3087
+ await self.set_system_constraints_for_plunger_acceleration(
3088
+ realmount, aspirate_spec.acceleration
3089
+ )
3090
+ await self._move(
3091
+ target_pos,
3092
+ speed=aspirate_spec.speed,
3093
+ home_flagged_axes=False,
3094
+ )
3095
+ except Exception:
3096
+ self._log.exception("Aspirate failed")
3097
+ aspirate_spec.instr.set_current_volume(0)
3098
+ raise
3099
+ else:
3100
+ aspirate_spec.instr.add_current_volume(aspirate_spec.volume)
3101
+
3102
+ async def dispense_while_tracking(
3103
+ self,
3104
+ mount: Union[top_types.Mount, OT3Mount],
3105
+ z_distance: float,
3106
+ volume: float,
3107
+ push_out: Optional[float],
3108
+ flow_rate: float = 1.0,
3109
+ is_full_dispense: bool = False,
3110
+ ) -> None:
3111
+ """
3112
+ Dispense a volume of liquid (in microliters/uL) while moving the z axis synchronously.
3113
+
3114
+ :param mount: A robot mount that the instrument is on.
3115
+ :param z_distance: The distance the z axis will move during dispensing.
3116
+ :param volume: The volume of liquid to be dispensed.
3117
+ :param flow_rate: The flow rate to dispense with.
3118
+ """
3119
+ realmount = OT3Mount.from_mount(mount)
3120
+ dispense_spec = self._pipette_handler.plan_check_dispense(
3121
+ realmount, volume, flow_rate, push_out, is_full_dispense
3122
+ )
3123
+ if not dispense_spec:
3124
+ return
3125
+ target_pos = target_positions_from_plunger_tracking(
3126
+ realmount,
3127
+ dispense_spec.plunger_distance,
3128
+ z_distance,
3129
+ self._current_position,
3130
+ )
3131
+
3132
+ try:
3133
+ await self._backend.set_active_current(
3134
+ {dispense_spec.axis: dispense_spec.current}
3135
+ )
3136
+ async with self.restore_system_constrants():
3137
+ await self.set_system_constraints_for_plunger_acceleration(
3138
+ realmount, dispense_spec.acceleration
3139
+ )
3140
+ await self._move(
3141
+ target_pos,
3142
+ speed=dispense_spec.speed,
3143
+ home_flagged_axes=False,
3144
+ )
3145
+ except Exception:
3146
+ self._log.exception("dispense failed")
3147
+ dispense_spec.instr.set_current_volume(0)
3148
+ raise
3149
+ else:
3150
+ dispense_spec.instr.remove_current_volume(dispense_spec.volume)
3151
+
3152
+ @property
3153
+ def attached_subsystems(self) -> Dict[SubSystem, SubSystemState]:
3154
+ """Get a view of the state of the currently-attached subsystems."""
3155
+ return self._backend.subsystems
3156
+
3157
+ @property
3158
+ def estop_status(self) -> EstopOverallStatus:
3159
+ return self._backend.estop_status
3160
+
3161
+ def estop_acknowledge_and_clear(self) -> EstopOverallStatus:
3162
+ """Attempt to acknowledge an Estop event and clear the status.
3163
+
3164
+ Returns the estop status after clearing the status."""
3165
+ self._backend.estop_acknowledge_and_clear()
3166
+ return self.estop_status
3167
+
3168
+ def get_estop_state(self) -> EstopState:
3169
+ return self._backend.get_estop_state()
3170
+
3171
+ async def set_hepa_fan_state(
3172
+ self, turn_on: bool = False, duty_cycle: int = 75
3173
+ ) -> bool:
3174
+ """Sets the state and duty cycle of the Hepa/UV module."""
3175
+ return await self._backend.set_hepa_fan_state(turn_on, duty_cycle)
3176
+
3177
+ async def get_hepa_fan_state(self) -> Optional[HepaFanState]:
3178
+ return await self._backend.get_hepa_fan_state()
3179
+
3180
+ async def set_hepa_uv_state(
3181
+ self, turn_on: bool = False, uv_duration_s: int = 900
3182
+ ) -> bool:
3183
+ """Sets the state and duration (seconds) of the UV light for the Hepa/UV module."""
3184
+ return await self._backend.set_hepa_uv_state(turn_on, uv_duration_s)
3185
+
3186
+ async def get_hepa_uv_state(self) -> Optional[HepaUVState]:
3187
+ return await self._backend.get_hepa_uv_state()
3188
+
3189
+ async def increase_evo_disp_count(
3190
+ self,
3191
+ mount: Union[top_types.Mount, OT3Mount],
3192
+ ) -> None:
3193
+ """Tell a pipette to increase its evo-tip-dispense-count in eeprom."""
3194
+ realmount = OT3Mount.from_mount(mount)
3195
+ await self._backend.increase_evo_disp_count(realmount)
3196
+
3197
+ async def read_stem_temperature(
3198
+ self, mount: Union[top_types.Mount, OT3Mount], primary: bool = True
3199
+ ) -> float:
3200
+ """Read and return the current stem temperature."""
3201
+ realmount = OT3Mount.from_mount(mount)
3202
+ s_data = await self._backend.read_env_temp_sensor(realmount, primary)
3203
+ return s_data if s_data else 0.0
3204
+
3205
+ async def read_stem_humidity(
3206
+ self, mount: Union[top_types.Mount, OT3Mount], primary: bool = True
3207
+ ) -> float:
3208
+ """Read and return the current primary stem humidity."""
3209
+ realmount = OT3Mount.from_mount(mount)
3210
+ s_data = await self._backend.read_env_hum_sensor(realmount, primary)
3211
+ return s_data if s_data else 0.0
3212
+
3213
+ async def read_stem_pressure(
3214
+ self, mount: Union[top_types.Mount, OT3Mount], primary: bool = True
3215
+ ) -> float:
3216
+ """Read and return the current primary stem pressure."""
3217
+ realmount = OT3Mount.from_mount(mount)
3218
+ s_data = await self._backend.read_pressure_sensor(realmount, primary)
3219
+ return s_data if s_data else 0.0
3220
+
3221
+ async def read_stem_capacitance(
3222
+ self, mount: Union[top_types.Mount, OT3Mount], primary: bool = True
3223
+ ) -> float:
3224
+ """Read and return the current primary stem capacitance."""
3225
+ realmount = OT3Mount.from_mount(mount)
3226
+ s_data = await self._backend.read_capacitive_sensor(realmount, primary)
3227
+ return s_data if s_data else 0.0