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,1930 @@
1
+ """OT3 Hardware Controller Backend."""
2
+
3
+ from __future__ import annotations
4
+ import asyncio
5
+ from contextlib import asynccontextmanager
6
+ from functools import wraps
7
+ import logging
8
+ from copy import deepcopy
9
+ from numpy import isclose
10
+ from typing import (
11
+ Any,
12
+ Awaitable,
13
+ Callable,
14
+ Dict,
15
+ List,
16
+ Optional,
17
+ Tuple,
18
+ Sequence,
19
+ AsyncIterator,
20
+ cast,
21
+ Set,
22
+ TypeVar,
23
+ Iterator,
24
+ KeysView,
25
+ Union,
26
+ Mapping,
27
+ )
28
+ from opentrons.config.types import OT3Config, GantryLoad
29
+ from opentrons.config import gripper_config
30
+ from .ot3utils import (
31
+ axis_convert,
32
+ create_move_group,
33
+ axis_to_node,
34
+ get_current_settings,
35
+ create_home_groups,
36
+ node_to_axis,
37
+ sensor_node_for_mount,
38
+ sensor_node_for_pipette,
39
+ sensor_id_for_instrument,
40
+ create_gripper_jaw_grip_group,
41
+ create_gripper_jaw_home_group,
42
+ create_gripper_jaw_hold_group,
43
+ create_tip_action_group,
44
+ create_tip_motor_home_group,
45
+ motor_nodes,
46
+ LIMIT_SWITCH_OVERTRAVEL_DISTANCE,
47
+ map_pipette_type_to_sensor_id,
48
+ moving_pipettes_in_move_group,
49
+ gripper_jaw_state_from_fw,
50
+ get_system_constraints,
51
+ get_system_constraints_for_plunger_acceleration,
52
+ )
53
+ from .tip_presence_manager import TipPresenceManager
54
+
55
+ try:
56
+ import aionotify # type: ignore[import-untyped]
57
+ except (OSError, ModuleNotFoundError):
58
+ aionotify = None
59
+
60
+
61
+ from opentrons_hardware.drivers import SystemDrivers
62
+ from opentrons_hardware.drivers.can_bus import CanMessenger, DriverSettings
63
+ from opentrons_hardware.drivers.can_bus.abstract_driver import AbstractCanDriver
64
+ from opentrons_hardware.drivers.can_bus.build import build_driver
65
+ from opentrons_hardware.drivers.binary_usb import (
66
+ BinaryMessenger,
67
+ SerialUsbDriver,
68
+ build_rear_panel_driver,
69
+ )
70
+ from opentrons_hardware.drivers.eeprom import EEPROMDriver, EEPROMData
71
+ from opentrons_hardware.hardware_control.move_group_runner import MoveGroupRunner
72
+ from opentrons_hardware.hardware_control.motion_planning import (
73
+ MoveManager,
74
+ MoveTarget,
75
+ ZeroLengthMoveError,
76
+ )
77
+ from opentrons_hardware.hardware_control.estop.detector import (
78
+ EstopDetector,
79
+ )
80
+
81
+ from opentrons.hardware_control.backends.estop_state import EstopStateMachine
82
+
83
+ from opentrons_hardware.hardware_control.motor_enable_disable import (
84
+ set_enable_motor,
85
+ set_disable_motor,
86
+ set_enable_tip_motor,
87
+ set_disable_tip_motor,
88
+ get_motor_enabled,
89
+ )
90
+ from opentrons_hardware.hardware_control.motor_position_status import (
91
+ get_motor_position,
92
+ update_motor_position_estimation,
93
+ )
94
+ from opentrons_hardware.hardware_control.limit_switches import get_limit_switches
95
+ from opentrons_hardware.hardware_control.current_settings import (
96
+ set_run_current,
97
+ set_hold_current,
98
+ set_currents,
99
+ )
100
+ from opentrons_hardware.firmware_bindings.constants import (
101
+ NodeId,
102
+ PipetteName as FirmwarePipetteName,
103
+ ErrorCode,
104
+ SensorId,
105
+ )
106
+ from opentrons_hardware.firmware_bindings.messages.message_definitions import (
107
+ StopRequest,
108
+ )
109
+ from opentrons_hardware.firmware_bindings.messages.payloads import EmptyPayload
110
+ from opentrons_hardware.hardware_control import status_bar
111
+
112
+ from opentrons_hardware.firmware_bindings.binary_constants import BinaryMessageId
113
+ from opentrons_hardware.firmware_bindings.messages.binary_message_definitions import (
114
+ BinaryMessageDefinition,
115
+ DoorSwitchStateInfo,
116
+ )
117
+ from opentrons_hardware.firmware_update import FirmwareUpdate
118
+ from opentrons_hardware.hardware_control import network, tools
119
+
120
+ from opentrons.hardware_control.module_control import AttachedModulesControl
121
+ from opentrons.hardware_control.types import (
122
+ BoardRevision,
123
+ Axis,
124
+ AionotifyEvent,
125
+ OT3Mount,
126
+ OT3AxisMap,
127
+ OT3AxisKind,
128
+ CurrentConfig,
129
+ MotorStatus,
130
+ InstrumentProbeType,
131
+ UpdateStatus,
132
+ DoorState,
133
+ SubSystemState,
134
+ SubSystem,
135
+ TipStateType,
136
+ GripperJawState,
137
+ HardwareFeatureFlags,
138
+ EstopOverallStatus,
139
+ EstopAttachLocation,
140
+ EstopState,
141
+ HardwareEventHandler,
142
+ HardwareEventUnsubscriber,
143
+ PipetteSensorId,
144
+ PipetteSensorType,
145
+ PipetteSensorData,
146
+ PipetteSensorResponseQueue,
147
+ StatusBarState,
148
+ StatusBarUpdateListener,
149
+ StatusBarUpdateUnsubscriber,
150
+ HepaFanState,
151
+ HepaUVState,
152
+ )
153
+ from opentrons.hardware_control.errors import (
154
+ InvalidPipetteName,
155
+ InvalidPipetteModel,
156
+ )
157
+ from opentrons_hardware.hardware_control.motion import (
158
+ MoveStopCondition,
159
+ MoveGroup,
160
+ )
161
+ from opentrons_hardware.hardware_control.types import (
162
+ NodeMap,
163
+ MotorPositionStatus,
164
+ MoveCompleteAck,
165
+ )
166
+ from opentrons_hardware.hardware_control.tools import types as ohc_tool_types
167
+
168
+ from opentrons_hardware.hardware_control.tool_sensors import (
169
+ capacitive_probe,
170
+ capacitive_pass,
171
+ liquid_probe,
172
+ check_overpressure,
173
+ grab_pressure,
174
+ )
175
+ from opentrons_hardware.hardware_control.rear_panel_settings import (
176
+ get_door_state,
177
+ set_deck_light,
178
+ get_deck_light_state,
179
+ )
180
+ from opentrons_hardware.hardware_control.gripper_settings import (
181
+ get_gripper_jaw_state,
182
+ )
183
+ from opentrons_hardware.hardware_control.hepa_uv_settings import (
184
+ set_hepa_fan_state as set_hepa_fan_state_fw,
185
+ get_hepa_fan_state as get_hepa_fan_state_fw,
186
+ set_hepa_uv_state as set_hepa_uv_state_fw,
187
+ get_hepa_uv_state as get_hepa_uv_state_fw,
188
+ )
189
+
190
+ from opentrons_hardware.drivers.gpio import OT3GPIO, RemoteOT3GPIO
191
+ from opentrons_shared_data.pipette.types import PipetteName
192
+ from opentrons_shared_data.pipette import (
193
+ pipette_load_name_conversions as pipette_load_name,
194
+ load_data as load_pipette_data,
195
+ )
196
+ from opentrons_shared_data.gripper.gripper_definition import GripForceProfile
197
+
198
+ from opentrons_shared_data.errors.exceptions import (
199
+ EStopActivatedError,
200
+ EStopNotPresentError,
201
+ PipetteOverpressureError,
202
+ FirmwareUpdateRequiredError,
203
+ FailedGripperPickupError,
204
+ PipetteLiquidNotFoundError,
205
+ CommunicationError,
206
+ PythonException,
207
+ UnsupportedHardwareCommand,
208
+ )
209
+
210
+ from .subsystem_manager import SubsystemManager
211
+
212
+ from ..dev_types import (
213
+ AttachedPipette,
214
+ AttachedGripper,
215
+ OT3AttachedInstruments,
216
+ )
217
+
218
+ from .types import HWStopCondition
219
+ from .flex_protocol import FlexBackend
220
+ from .status_bar_state import StatusBarStateController
221
+ from opentrons_hardware.sensors.sensor_types import (
222
+ EnvironmentSensor,
223
+ CapacitiveSensor,
224
+ PressureSensor,
225
+ )
226
+ from opentrons_hardware.sensors.types import SensorDataType, EnvironmentSensorDataType
227
+ from opentrons_hardware.sensors.sensor_driver import SensorDriver
228
+ from opentrons_hardware.sensors.utils import send_evo_dispense_count_increase
229
+
230
+ from .. import modules
231
+
232
+ log = logging.getLogger(__name__)
233
+
234
+ MapPayload = TypeVar("MapPayload")
235
+ Wrapped = TypeVar("Wrapped", bound=Callable[..., Awaitable[Any]])
236
+
237
+
238
+ def requires_update(func: Wrapped) -> Wrapped:
239
+ """Decorator that raises FirmwareUpdateRequiredError if the update_required flag is set."""
240
+
241
+ @wraps(func)
242
+ async def wrapper(self: Any, *args: Any, **kwargs: Any) -> Any:
243
+ if self.update_required and self.initialized:
244
+ raise FirmwareUpdateRequiredError(
245
+ func.__name__,
246
+ self.subsystems_to_update,
247
+ )
248
+ return await func(self, *args, **kwargs)
249
+
250
+ return cast(Wrapped, wrapper)
251
+
252
+
253
+ def requires_estop(func: Wrapped) -> Wrapped:
254
+ """Decorator that raises an exception if the Estop is engaged."""
255
+
256
+ @wraps(func)
257
+ async def wrapper(self: OT3Controller, *args: Any, **kwargs: Any) -> Any:
258
+ state = self._estop_state_machine.state
259
+ if state == EstopState.NOT_PRESENT and self._feature_flags.require_estop:
260
+ raise EStopNotPresentError(
261
+ message="An Estop must be plugged in to move the robot."
262
+ )
263
+ if state == EstopState.LOGICALLY_ENGAGED:
264
+ raise EStopActivatedError(
265
+ message="Estop must be acknowledged and cleared to move the robot."
266
+ )
267
+ if state == EstopState.PHYSICALLY_ENGAGED:
268
+ raise EStopActivatedError(
269
+ message="Estop is currently engaged, robot cannot move."
270
+ )
271
+ return await func(self, *args, **kwargs)
272
+
273
+ return cast(Wrapped, wrapper)
274
+
275
+
276
+ class OT3Controller(FlexBackend):
277
+ """OT3 Hardware Controller Backend."""
278
+
279
+ _initialized: bool
280
+ _messenger: CanMessenger
281
+ _usb_messenger: Optional[BinaryMessenger]
282
+ _position: Dict[NodeId, float]
283
+ _encoder_position: Dict[NodeId, float]
284
+ _motor_status: Dict[NodeId, MotorStatus]
285
+ _subsystem_manager: SubsystemManager
286
+ _engaged_axes: OT3AxisMap[bool]
287
+
288
+ @classmethod
289
+ async def build(
290
+ cls,
291
+ config: OT3Config,
292
+ use_usb_bus: bool = False,
293
+ check_updates: bool = True,
294
+ feature_flags: Optional[HardwareFeatureFlags] = None,
295
+ ) -> OT3Controller:
296
+ """Create the OT3Controller instance.
297
+
298
+ Args:
299
+ config: Robot configuration
300
+
301
+ Returns:
302
+ Instance.
303
+ """
304
+ driver = await build_driver(DriverSettings())
305
+ usb_driver = None
306
+ if use_usb_bus:
307
+ try:
308
+ usb_driver = await build_rear_panel_driver()
309
+ except IOError as e:
310
+ log.error(
311
+ "No rear panel device found, probably an EVT bot, disable rearPanelIntegration feature flag if it is"
312
+ )
313
+ raise e
314
+ inst = cls(
315
+ config,
316
+ driver=driver,
317
+ usb_driver=usb_driver,
318
+ check_updates=check_updates,
319
+ feature_flags=feature_flags,
320
+ )
321
+ await inst._subsystem_manager.start()
322
+ return inst
323
+
324
+ def __init__(
325
+ self,
326
+ config: OT3Config,
327
+ driver: AbstractCanDriver,
328
+ usb_driver: Optional[SerialUsbDriver] = None,
329
+ eeprom_driver: Optional[EEPROMDriver] = None,
330
+ check_updates: bool = True,
331
+ feature_flags: Optional[HardwareFeatureFlags] = None,
332
+ ) -> None:
333
+ """Construct.
334
+
335
+ Args:
336
+ config: Robot configuration
337
+ driver: The Can Driver
338
+ """
339
+ self._configuration = config
340
+ self._module_controls: Optional[AttachedModulesControl] = None
341
+ self._messenger = CanMessenger(driver=driver)
342
+ self._messenger.start()
343
+ self._drivers = self._build_system_hardware(
344
+ self._messenger, usb_driver, eeprom_driver
345
+ )
346
+ self._feature_flags = feature_flags or HardwareFeatureFlags()
347
+ self._usb_messenger = self._drivers.usb_messenger
348
+ self._gpio_dev = self._drivers.gpio_dev
349
+ self._subsystem_manager = SubsystemManager(
350
+ self._messenger,
351
+ self._usb_messenger,
352
+ tools.detector.ToolDetector(self._messenger),
353
+ network.NetworkInfo(self._messenger, self._usb_messenger),
354
+ FirmwareUpdate(),
355
+ )
356
+ self._estop_detector: Optional[EstopDetector] = None
357
+ self._estop_state_machine = EstopStateMachine(detector=None)
358
+ self._position = self._get_home_position()
359
+ self._gear_motor_position: Dict[NodeId, float] = {}
360
+ self._encoder_position = self._get_home_position()
361
+ self._motor_status = {}
362
+ self._engaged_axes = {}
363
+ self._check_updates = check_updates
364
+ self._initialized = False
365
+ self._status_bar = status_bar.StatusBar(messenger=self._usb_messenger)
366
+ self._status_bar_controller = StatusBarStateController(self._status_bar)
367
+
368
+ try:
369
+ self._event_watcher = self._build_event_watcher()
370
+ except AttributeError:
371
+ log.warning(
372
+ "Failed to initiate aionotify, cannot watch modules "
373
+ "or door, likely because not running on linux"
374
+ )
375
+ self._current_settings: Optional[OT3AxisMap[CurrentConfig]] = None
376
+ self._tip_presence_manager = TipPresenceManager(self._messenger)
377
+ self._move_manager = MoveManager(
378
+ constraints=get_system_constraints(
379
+ self._configuration.motion_settings, GantryLoad.LOW_THROUGHPUT
380
+ )
381
+ )
382
+ self._pressure_sensor_available: Dict[NodeId, bool] = {}
383
+
384
+ @asynccontextmanager
385
+ async def restore_system_constraints(self) -> AsyncIterator[None]:
386
+ old_system_constraints = deepcopy(self._move_manager.get_constraints())
387
+ try:
388
+ yield
389
+ finally:
390
+ self._move_manager.update_constraints(old_system_constraints)
391
+ log.debug(f"Restore previous system constraints: {old_system_constraints}")
392
+
393
+ @asynccontextmanager
394
+ async def grab_pressure(
395
+ self, channels: int, mount: OT3Mount
396
+ ) -> AsyncIterator[None]:
397
+ tool = axis_to_node(Axis.of_main_tool_actuator(mount))
398
+ async with grab_pressure(channels, tool, self._messenger):
399
+ yield
400
+
401
+ def set_pressure_sensor_available(
402
+ self, pipette_axis: Axis, available: bool
403
+ ) -> None:
404
+ pip_node = axis_to_node(pipette_axis)
405
+ self._pressure_sensor_available[pip_node] = available
406
+
407
+ def get_pressure_sensor_available(self, pipette_axis: Axis) -> bool:
408
+ pip_node = axis_to_node(pipette_axis)
409
+ return self._pressure_sensor_available[pip_node]
410
+
411
+ def update_constraints_for_gantry_load(self, gantry_load: GantryLoad) -> None:
412
+ self._move_manager.update_constraints(
413
+ get_system_constraints(self._configuration.motion_settings, gantry_load)
414
+ )
415
+
416
+ def update_constraints_for_plunger_acceleration(
417
+ self,
418
+ mount: OT3Mount,
419
+ acceleration: float,
420
+ gantry_load: GantryLoad,
421
+ high_speed_pipette: bool = False,
422
+ ) -> None:
423
+ new_constraints = get_system_constraints_for_plunger_acceleration(
424
+ self._configuration.motion_settings,
425
+ gantry_load,
426
+ mount,
427
+ acceleration,
428
+ high_speed_pipette,
429
+ )
430
+ self._move_manager.update_constraints(new_constraints)
431
+
432
+ async def get_serial_number(self) -> Optional[str]:
433
+ if not self.initialized:
434
+ return None
435
+ return self.eeprom_data.serial_number
436
+
437
+ @property
438
+ def initialized(self) -> bool:
439
+ """True when the hardware controller has initialized and is ready."""
440
+ return self._initialized
441
+
442
+ @initialized.setter
443
+ def initialized(self, value: bool) -> None:
444
+ self._initialized = value
445
+
446
+ @property
447
+ def subsystems(self) -> Dict[SubSystem, SubSystemState]:
448
+ return self._subsystem_manager.subsystems
449
+
450
+ @property
451
+ def fw_version(self) -> Dict[SubSystem, int]:
452
+ """Get the firmware version."""
453
+ return {
454
+ subsystem: info.current_fw_version
455
+ for subsystem, info in self.subsystems.items()
456
+ }
457
+
458
+ @property
459
+ def eeprom_driver(self) -> EEPROMDriver:
460
+ """The eeprom driver interface."""
461
+ return self._drivers.eeprom
462
+
463
+ @property
464
+ def eeprom_data(self) -> EEPROMData:
465
+ """Get the data on the eeprom."""
466
+ return self._drivers.eeprom.data
467
+
468
+ @property
469
+ def update_required(self) -> bool:
470
+ return self._subsystem_manager.update_required and self._check_updates
471
+
472
+ @property
473
+ def subsystems_to_update(self) -> List[SubSystem]:
474
+ return self._subsystem_manager.subsystems_to_update
475
+
476
+ @staticmethod
477
+ def _build_system_hardware(
478
+ can_messenger: CanMessenger,
479
+ usb_driver: Optional[SerialUsbDriver],
480
+ eeprom_driver: Optional[EEPROMDriver],
481
+ ) -> SystemDrivers:
482
+ gpio = OT3GPIO("hardware_control")
483
+ eeprom_driver = eeprom_driver or EEPROMDriver(gpio)
484
+ eeprom_driver.setup()
485
+ gpio_dev: Union[OT3GPIO, RemoteOT3GPIO] = gpio
486
+ usb_messenger: Optional[BinaryMessenger] = None
487
+ if usb_driver:
488
+ usb_messenger = BinaryMessenger(usb_driver)
489
+ usb_messenger.start()
490
+ gpio_dev = RemoteOT3GPIO(usb_messenger)
491
+ return SystemDrivers(
492
+ can_messenger,
493
+ gpio_dev,
494
+ eeprom_driver,
495
+ usb_messenger=usb_messenger,
496
+ )
497
+
498
+ @property
499
+ def gear_motor_position(self) -> Optional[float]:
500
+ return self._gear_motor_position.get(NodeId.pipette_left, None)
501
+
502
+ def _motor_nodes(self) -> Set[NodeId]:
503
+ """Get a list of the motor controller nodes of all attached and ok devices."""
504
+ return motor_nodes(self._subsystem_manager.targets)
505
+
506
+ async def update_firmware(
507
+ self,
508
+ subsystems: Set[SubSystem],
509
+ force: bool = False,
510
+ ) -> AsyncIterator[UpdateStatus]:
511
+ """Updates the firmware on the OT3."""
512
+ async for update in self._subsystem_manager.update_firmware(subsystems, force):
513
+ yield update
514
+
515
+ def get_current_settings(
516
+ self, gantry_load: GantryLoad
517
+ ) -> OT3AxisMap[CurrentConfig]:
518
+ return get_current_settings(self._configuration.current_settings, gantry_load)
519
+
520
+ async def update_to_default_current_settings(self, gantry_load: GantryLoad) -> None:
521
+ self._current_settings = self.get_current_settings(gantry_load)
522
+ await self.set_default_currents()
523
+
524
+ def update_feature_flags(self, feature_flags: HardwareFeatureFlags) -> None:
525
+ """Update the hardware feature flags used by the hardware controller."""
526
+ self._feature_flags = feature_flags
527
+
528
+ async def update_motor_status(self) -> None:
529
+ """Retreieve motor and encoder status and position from all present nodes"""
530
+ motor_nodes = self._motor_nodes()
531
+ assert len(motor_nodes)
532
+ response = await get_motor_position(self._messenger, motor_nodes)
533
+ self._handle_motor_status_response(response)
534
+
535
+ async def update_motor_estimation(self, axes: Sequence[Axis]) -> None:
536
+ """Update motor position estimation for commanded nodes, and update cache of data."""
537
+ nodes = set([axis_to_node(a) for a in axes])
538
+ response = await update_motor_position_estimation(self._messenger, nodes)
539
+ self._handle_motor_status_response(response)
540
+
541
+ @property
542
+ def grip_force_profile(self) -> Optional[GripForceProfile]:
543
+ return self._gripper_force_settings
544
+
545
+ @grip_force_profile.setter
546
+ def grip_force_profile(self, profile: Optional[GripForceProfile]) -> None:
547
+ self._gripper_force_settings = profile
548
+
549
+ @property
550
+ def motor_run_currents(self) -> OT3AxisMap[float]:
551
+ assert self._current_settings
552
+ run_currents: OT3AxisMap[float] = {}
553
+ for axis, settings in self._current_settings.items():
554
+ run_currents[axis] = settings.run_current
555
+ return run_currents
556
+
557
+ @property
558
+ def motor_hold_currents(self) -> OT3AxisMap[float]:
559
+ assert self._current_settings
560
+ hold_currents: OT3AxisMap[float] = {}
561
+ for axis, settings in self._current_settings.items():
562
+ hold_currents[axis] = settings.hold_current
563
+ return hold_currents
564
+
565
+ @property
566
+ def gpio_chardev(self) -> Union[OT3GPIO, RemoteOT3GPIO]:
567
+ """Get the GPIO device."""
568
+ return self._gpio_dev
569
+
570
+ @property
571
+ def board_revision(self) -> BoardRevision:
572
+ """Get the board revision"""
573
+ return BoardRevision.FLEX_B2
574
+
575
+ @property
576
+ def module_controls(self) -> AttachedModulesControl:
577
+ """Get the module controls."""
578
+ if self._module_controls is None:
579
+ raise AttributeError("Module controls not found.")
580
+ return self._module_controls
581
+
582
+ @module_controls.setter
583
+ def module_controls(self, module_controls: AttachedModulesControl) -> None:
584
+ """Set the module controls"""
585
+ self._module_controls = module_controls
586
+
587
+ def _get_motor_status(
588
+ self, axes: Sequence[Axis]
589
+ ) -> Dict[Axis, Optional[MotorStatus]]:
590
+ return {ax: self._motor_status.get(axis_to_node(ax)) for ax in axes}
591
+
592
+ def get_invalid_motor_axes(self, axes: Sequence[Axis]) -> List[Axis]:
593
+ """Get axes that currently do not have the motor-ok flag."""
594
+ return [
595
+ ax
596
+ for ax, status in self._get_motor_status(axes).items()
597
+ if not status or not status.motor_ok
598
+ ]
599
+
600
+ def get_invalid_encoder_axes(self, axes: Sequence[Axis]) -> List[Axis]:
601
+ """Get axes that currently do not have the encoder-ok flag."""
602
+ return [
603
+ ax
604
+ for ax, status in self._get_motor_status(axes).items()
605
+ if not status or not status.encoder_ok
606
+ ]
607
+
608
+ def check_motor_status(self, axes: Sequence[Axis]) -> bool:
609
+ return len(self.get_invalid_motor_axes(axes)) == 0
610
+
611
+ def check_encoder_status(self, axes: Sequence[Axis]) -> bool:
612
+ return len(self.get_invalid_encoder_axes(axes)) == 0
613
+
614
+ async def update_position(self) -> OT3AxisMap[float]:
615
+ """Get the current position."""
616
+ return axis_convert(self._position, 0.0)
617
+
618
+ async def update_encoder_position(self) -> OT3AxisMap[float]:
619
+ """Get the encoder current position."""
620
+ return axis_convert(self._encoder_position, 0.0)
621
+
622
+ def _handle_motor_status_response(
623
+ self, response: NodeMap[MotorPositionStatus], handle_gear_move: bool = False
624
+ ) -> None:
625
+ for axis, pos in response.items():
626
+ if handle_gear_move and axis == NodeId.pipette_left:
627
+ self._gear_motor_position = {axis: pos.motor_position}
628
+ else:
629
+ self._position.update({axis: pos.motor_position})
630
+ self._encoder_position.update({axis: pos.encoder_position})
631
+ # TODO (FPS 6-01-2023): Remove this once the Feature Flag to ignore stall detection is removed.
632
+ # This check will latch the motor status for an axis at "true" if it was ever set to true.
633
+ # To account for the case where a motor axis has its power reset, we also depend on the
634
+ # "encoder_ok" flag staying set (it will only be False if the motor axis has not been
635
+ # homed since a power cycle)
636
+ motor_ok_latch = (
637
+ (not self._feature_flags.stall_detection_enabled)
638
+ and (
639
+ (axis in self._motor_status)
640
+ and self._motor_status[axis].motor_ok
641
+ )
642
+ and self._motor_status[axis].encoder_ok
643
+ )
644
+ self._motor_status.update(
645
+ {
646
+ axis: MotorStatus(
647
+ motor_ok=(pos.motor_ok or motor_ok_latch),
648
+ encoder_ok=pos.encoder_ok,
649
+ )
650
+ }
651
+ )
652
+
653
+ def _build_move_node_axis_runner(
654
+ self,
655
+ origin: Dict[Axis, float],
656
+ target: Dict[Axis, float],
657
+ speed: float,
658
+ stop_condition: HWStopCondition,
659
+ nodes_in_moves_only: bool,
660
+ ) -> Tuple[Optional[MoveGroupRunner], bool]:
661
+ if not target:
662
+ return None, False
663
+ move_target = MoveTarget.build(position=target, max_speed=speed)
664
+ try:
665
+ _, movelist = self._move_manager.plan_motion(
666
+ origin=origin, target_list=[move_target]
667
+ )
668
+ except ZeroLengthMoveError as zme:
669
+ log.debug(f"Not moving because move was zero length {str(zme)}")
670
+ return None, False
671
+ moves = movelist[0]
672
+ log.debug(
673
+ f"move: machine coordinates {target} from origin: machine coordinates {origin} at speed: {speed} requires {moves}"
674
+ )
675
+
676
+ ordered_nodes = self._motor_nodes()
677
+ if nodes_in_moves_only:
678
+ moving_axes = {
679
+ axis_to_node(ax) for move in moves for ax in move.unit_vector.keys()
680
+ }
681
+ ordered_nodes = ordered_nodes.intersection(moving_axes)
682
+
683
+ move_group, _ = create_move_group(
684
+ origin, moves, ordered_nodes, MoveStopCondition[stop_condition.name]
685
+ )
686
+ return (
687
+ MoveGroupRunner(
688
+ move_groups=[move_group],
689
+ ignore_stalls=True
690
+ if not self._feature_flags.stall_detection_enabled
691
+ else False,
692
+ ),
693
+ False,
694
+ )
695
+
696
+ def _build_move_gear_axis_runner(
697
+ self,
698
+ possible_q_axis_origin: Optional[float],
699
+ possible_q_axis_target: Optional[float],
700
+ speed: float,
701
+ nodes_in_moves_only: bool,
702
+ ) -> Tuple[Optional[MoveGroupRunner], bool]:
703
+ if possible_q_axis_origin is None or possible_q_axis_target is None:
704
+ return None, True
705
+ tip_motor_move_group = self._build_tip_action_group(
706
+ possible_q_axis_origin, [(possible_q_axis_target, speed)]
707
+ )
708
+ if nodes_in_moves_only:
709
+ ordered_nodes = self._motor_nodes()
710
+
711
+ ordered_nodes.intersection({axis_to_node(Axis.Q)})
712
+ return (
713
+ MoveGroupRunner(
714
+ move_groups=[tip_motor_move_group],
715
+ ignore_stalls=True
716
+ if not self._feature_flags.stall_detection_enabled
717
+ else False,
718
+ ),
719
+ True,
720
+ )
721
+
722
+ @requires_update
723
+ @requires_estop
724
+ async def move(
725
+ self,
726
+ origin: Dict[Axis, float],
727
+ target: Dict[Axis, float],
728
+ speed: float,
729
+ stop_condition: HWStopCondition = HWStopCondition.none,
730
+ nodes_in_moves_only: bool = True,
731
+ ) -> None:
732
+ """Move to a position.
733
+
734
+ Args:
735
+ origin: The starting point of the move
736
+ moves: List of moves.
737
+ stop_condition: The stop condition.
738
+ nodes_in_moves_only: Default is True. If False, also send empty moves to
739
+ nodes that are present but not defined in moves.
740
+
741
+ .. caution::
742
+ Setting `nodes_in_moves_only` to False will enable *all* present motors in
743
+ the system. DO NOT USE when you want to keep one of the axes disabled.
744
+
745
+ Returns:
746
+ None
747
+ """
748
+ possible_q_axis_origin = origin.pop(Axis.Q, None)
749
+ possible_q_axis_target = target.pop(Axis.Q, None)
750
+
751
+ maybe_runners = (
752
+ self._build_move_node_axis_runner(
753
+ origin, target, speed, stop_condition, nodes_in_moves_only
754
+ ),
755
+ self._build_move_gear_axis_runner(
756
+ possible_q_axis_origin,
757
+ possible_q_axis_target,
758
+ speed,
759
+ nodes_in_moves_only,
760
+ ),
761
+ )
762
+ log.debug(f"The move groups are {maybe_runners}.")
763
+
764
+ gather_moving_nodes = set()
765
+ all_moving_nodes = set()
766
+ for runner, _ in maybe_runners:
767
+ if runner:
768
+ for n in runner.all_nodes():
769
+ gather_moving_nodes.add(n)
770
+ for n in runner.all_moving_nodes():
771
+ all_moving_nodes.add(n)
772
+
773
+ pipettes_moving = moving_pipettes_in_move_group(
774
+ gather_moving_nodes, all_moving_nodes
775
+ )
776
+
777
+ async def _runner_coroutine(
778
+ runner: MoveGroupRunner, is_gear_move: bool
779
+ ) -> Tuple[Dict[NodeId, MotorPositionStatus], bool]:
780
+ positions = await runner.run(can_messenger=self._messenger)
781
+ return positions, is_gear_move
782
+
783
+ coros = [
784
+ _runner_coroutine(runner, is_gear_move)
785
+ for runner, is_gear_move in maybe_runners
786
+ if runner
787
+ ]
788
+ checked_moving_pipettes = self._pipettes_to_monitor_pressure(pipettes_moving)
789
+ async with self._monitor_overpressure(checked_moving_pipettes):
790
+ all_positions = await asyncio.gather(*coros)
791
+
792
+ for positions, handle_gear_move in all_positions:
793
+ self._handle_motor_status_response(positions, handle_gear_move)
794
+
795
+ def _get_axis_home_distance(self, axis: Axis) -> float:
796
+ if self.check_motor_status([axis]):
797
+ return -1 * (
798
+ self._position[axis_to_node(axis)] + LIMIT_SWITCH_OVERTRAVEL_DISTANCE
799
+ )
800
+ else:
801
+ return -1 * self.axis_bounds[axis][1] - self.axis_bounds[axis][0]
802
+
803
+ def _build_axes_home_groups(
804
+ self, axes: Sequence[Axis], speed_settings: Dict[OT3AxisKind, float]
805
+ ) -> List[MoveGroup]:
806
+ present_axes = [ax for ax in axes if self.axis_is_present(ax)]
807
+ if not present_axes:
808
+ return []
809
+ else:
810
+ distances = {ax: self._get_axis_home_distance(ax) for ax in present_axes}
811
+ velocities = {
812
+ ax: -1 * speed_settings[Axis.to_kind(ax)] for ax in present_axes
813
+ }
814
+ return create_home_groups(distances, velocities)
815
+
816
+ def _build_home_pipettes_runner(
817
+ self,
818
+ axes: Sequence[Axis],
819
+ gantry_load: GantryLoad,
820
+ ) -> Optional[MoveGroupRunner]:
821
+ pipette_axes = [ax for ax in axes if ax in Axis.pipette_axes()]
822
+ if not pipette_axes:
823
+ return None
824
+
825
+ speed_settings = self._configuration.motion_settings.max_speed_discontinuity[
826
+ gantry_load
827
+ ]
828
+ move_groups: List[MoveGroup] = self._build_axes_home_groups(
829
+ pipette_axes, speed_settings
830
+ )
831
+ return MoveGroupRunner(move_groups=move_groups)
832
+
833
+ def _build_home_gantry_z_runner(
834
+ self,
835
+ axes: Sequence[Axis],
836
+ gantry_load: GantryLoad,
837
+ ) -> Optional[MoveGroupRunner]:
838
+ gantry_axes = [ax for ax in axes if ax in Axis.gantry_axes()]
839
+ if not gantry_axes:
840
+ return None
841
+
842
+ speed_settings = self._configuration.motion_settings.max_speed_discontinuity[
843
+ gantry_load
844
+ ]
845
+
846
+ # first home all the present mount axes
847
+ z_axes = list(filter(lambda ax: ax in Axis.ot3_mount_axes(), gantry_axes))
848
+ z_groups = self._build_axes_home_groups(z_axes, speed_settings)
849
+
850
+ # home X axis before Y axis, to avoid collision with thermo-cycler lid
851
+ # that could be in the back-left corner
852
+ x_groups = (
853
+ self._build_axes_home_groups([Axis.X], speed_settings)
854
+ if Axis.X in gantry_axes
855
+ else []
856
+ )
857
+ y_groups = (
858
+ self._build_axes_home_groups([Axis.Y], speed_settings)
859
+ if Axis.Y in gantry_axes
860
+ else []
861
+ )
862
+
863
+ move_groups = [*z_groups, *x_groups, *y_groups]
864
+ if move_groups:
865
+ return MoveGroupRunner(move_groups=move_groups)
866
+ return None
867
+
868
+ @requires_update
869
+ @requires_estop
870
+ async def home(
871
+ self, axes: Sequence[Axis], gantry_load: GantryLoad
872
+ ) -> OT3AxisMap[float]:
873
+ """Home each axis passed in, and reset the positions to 0.
874
+
875
+ Args:
876
+ axes: List[Axis]
877
+
878
+ Returns:
879
+ A dictionary containing the new positions of each axis
880
+ """
881
+ checked_axes = [axis for axis in axes if self.axis_is_present(axis)]
882
+ assert Axis.G not in checked_axes, "Please home G axis using gripper_home_jaw()"
883
+ if not checked_axes:
884
+ return {}
885
+
886
+ maybe_runners = (
887
+ self._build_home_gantry_z_runner(checked_axes, gantry_load),
888
+ self._build_home_pipettes_runner(checked_axes, gantry_load),
889
+ )
890
+ coros = [
891
+ runner.run(can_messenger=self._messenger)
892
+ for runner in maybe_runners
893
+ if runner
894
+ ]
895
+ moving_pipettes = [
896
+ axis_to_node(ax) for ax in checked_axes if ax in Axis.pipette_axes()
897
+ ]
898
+ checked_moving_pipettes = self._pipettes_to_monitor_pressure(moving_pipettes)
899
+ async with self._monitor_overpressure(checked_moving_pipettes):
900
+ positions = await asyncio.gather(*coros)
901
+ # TODO(CM): default gear motor homing routine to have some acceleration
902
+ if gantry_load in [
903
+ GantryLoad.HIGH_THROUGHPUT_1000,
904
+ GantryLoad.HIGH_THROUGHPUT_200,
905
+ ]:
906
+ await self.home_tip_motors(
907
+ distance=self.axis_bounds[Axis.Q][1] - self.axis_bounds[Axis.Q][0],
908
+ velocity=self._configuration.motion_settings.max_speed_discontinuity[
909
+ gantry_load
910
+ ][Axis.to_kind(Axis.Q)],
911
+ )
912
+
913
+ for position in positions:
914
+ self._handle_motor_status_response(position)
915
+ return axis_convert(self._position, 0.0)
916
+
917
+ def _pipettes_to_monitor_pressure(self, pipettes: List[NodeId]) -> List[NodeId]:
918
+ return [pip for pip in pipettes if self._pressure_sensor_available[pip]]
919
+
920
+ def _filter_move_group(self, move_group: MoveGroup) -> MoveGroup:
921
+ new_group: MoveGroup = []
922
+ for step in move_group:
923
+ new_group.append(
924
+ {
925
+ node: axis_step
926
+ for node, axis_step in step.items()
927
+ if node in self._motor_nodes()
928
+ }
929
+ )
930
+ return new_group
931
+
932
+ async def home_tip_motors(
933
+ self,
934
+ distance: float,
935
+ velocity: float,
936
+ back_off: bool = True,
937
+ ) -> None:
938
+ move_group = create_tip_motor_home_group(distance, velocity, back_off)
939
+
940
+ runner = MoveGroupRunner(
941
+ move_groups=[move_group],
942
+ ignore_stalls=True
943
+ if not self._feature_flags.stall_detection_enabled
944
+ else False,
945
+ )
946
+ try:
947
+ positions = await runner.run(can_messenger=self._messenger)
948
+ if NodeId.pipette_left in positions:
949
+ self._gear_motor_position = {
950
+ NodeId.pipette_left: positions[NodeId.pipette_left].motor_position
951
+ }
952
+ else:
953
+ log.debug("no position returned from NodeId.pipette_left")
954
+ self._gear_motor_position = {}
955
+ except Exception as e:
956
+ log.error("Clearing tip motor position due to failed movement")
957
+ self._gear_motor_position = {}
958
+ raise e
959
+
960
+ def _build_tip_action_group(
961
+ self, origin: float, targets: List[Tuple[float, float]]
962
+ ) -> MoveGroup:
963
+ move_targets = [
964
+ MoveTarget.build({Axis.Q: target_pos}, speed)
965
+ for target_pos, speed in targets
966
+ ]
967
+ _, moves = self._move_manager.plan_motion(
968
+ origin={Axis.Q: origin}, target_list=move_targets
969
+ )
970
+
971
+ return create_tip_action_group(moves[0], [NodeId.pipette_left], "clamp")
972
+
973
+ async def tip_action(
974
+ self, origin: float, targets: List[Tuple[float, float]]
975
+ ) -> None:
976
+ move_group = self._build_tip_action_group(origin, targets)
977
+ runner = MoveGroupRunner(
978
+ move_groups=[move_group],
979
+ ignore_stalls=True
980
+ if not self._feature_flags.stall_detection_enabled
981
+ else False,
982
+ )
983
+ try:
984
+ positions = await runner.run(can_messenger=self._messenger)
985
+ if NodeId.pipette_left in positions:
986
+ self._gear_motor_position = {
987
+ NodeId.pipette_left: positions[NodeId.pipette_left].motor_position
988
+ }
989
+ else:
990
+ log.debug("no position returned from NodeId.pipette_left")
991
+ self._gear_motor_position = {}
992
+ except Exception as e:
993
+ log.error("Clearing tip motor position due to failed movement")
994
+ self._gear_motor_position = {}
995
+ raise e
996
+
997
+ @requires_update
998
+ @requires_estop
999
+ async def gripper_grip_jaw(
1000
+ self,
1001
+ duty_cycle: float,
1002
+ expected_displacement: float, # not used on real hardware
1003
+ stop_condition: HWStopCondition = HWStopCondition.none,
1004
+ stay_engaged: bool = True,
1005
+ ) -> None:
1006
+ move_group = create_gripper_jaw_grip_group(
1007
+ duty_cycle, MoveStopCondition[stop_condition.name], stay_engaged
1008
+ )
1009
+ runner = MoveGroupRunner(move_groups=[move_group])
1010
+ positions = await runner.run(can_messenger=self._messenger)
1011
+ self._handle_motor_status_response(positions)
1012
+
1013
+ @requires_update
1014
+ @requires_estop
1015
+ async def gripper_hold_jaw(
1016
+ self,
1017
+ encoder_position_um: int,
1018
+ ) -> None:
1019
+ move_group = create_gripper_jaw_hold_group(encoder_position_um)
1020
+ runner = MoveGroupRunner(move_groups=[move_group])
1021
+ positions = await runner.run(can_messenger=self._messenger)
1022
+ self._handle_motor_status_response(positions)
1023
+
1024
+ @requires_update
1025
+ @requires_estop
1026
+ async def gripper_home_jaw(self, duty_cycle: float) -> None:
1027
+ move_group = create_gripper_jaw_home_group(duty_cycle)
1028
+ runner = MoveGroupRunner(move_groups=[move_group])
1029
+ positions = await runner.run(can_messenger=self._messenger)
1030
+ self._handle_motor_status_response(positions)
1031
+
1032
+ async def get_jaw_state(self) -> GripperJawState:
1033
+ res = await get_gripper_jaw_state(self._messenger)
1034
+ return gripper_jaw_state_from_fw(res)
1035
+
1036
+ @staticmethod
1037
+ def _lookup_serial_key(pipette_name: FirmwarePipetteName) -> str:
1038
+ lookup_name = {
1039
+ FirmwarePipetteName.p1000_single: "P1KS",
1040
+ FirmwarePipetteName.p1000_multi: "P1KM",
1041
+ FirmwarePipetteName.p1000_multi_em: "P1KP",
1042
+ FirmwarePipetteName.p50_single: "P50S",
1043
+ FirmwarePipetteName.p50_multi: "P50M",
1044
+ FirmwarePipetteName.p1000_96: "P1KH",
1045
+ FirmwarePipetteName.p50_96: "P50H",
1046
+ FirmwarePipetteName.p200_96: "P2HH",
1047
+ }
1048
+ return lookup_name[pipette_name]
1049
+
1050
+ @staticmethod
1051
+ def _combine_serial_number(pipette_info: ohc_tool_types.PipetteInformation) -> str:
1052
+ serialized_name = OT3Controller._lookup_serial_key(pipette_info.name)
1053
+ version = pipette_load_name.version_from_string(pipette_info.model)
1054
+ return f"{serialized_name}V{version.major}{version.minor}{pipette_info.serial}"
1055
+
1056
+ @staticmethod
1057
+ def _build_attached_pip(
1058
+ attached: ohc_tool_types.PipetteInformation, mount: OT3Mount
1059
+ ) -> AttachedPipette:
1060
+ if attached.name == FirmwarePipetteName.unknown:
1061
+ raise InvalidPipetteName(name=attached.name_int, mount=mount.name)
1062
+ try:
1063
+ # TODO (lc 12-8-2022) We should return model as an int rather than
1064
+ # a string.
1065
+ # TODO (lc 12-6-2022) We should also provide the full serial number
1066
+ # for PipetteInformation.serial so we don't have to use
1067
+ # helper methods to convert the serial back to what was flashed
1068
+ # on the eeprom.
1069
+ converted_name = pipette_load_name.convert_pipette_name(
1070
+ cast(PipetteName, attached.name.name), attached.model
1071
+ )
1072
+ return {
1073
+ "config": load_pipette_data.load_definition(
1074
+ converted_name.pipette_type,
1075
+ converted_name.pipette_channels,
1076
+ converted_name.pipette_version,
1077
+ converted_name.oem_type,
1078
+ ),
1079
+ "id": OT3Controller._combine_serial_number(attached),
1080
+ }
1081
+ except KeyError:
1082
+ raise InvalidPipetteModel(
1083
+ name=attached.name.name, model=attached.model, mount=mount.name
1084
+ )
1085
+
1086
+ @staticmethod
1087
+ def _build_attached_gripper(
1088
+ attached: ohc_tool_types.GripperInformation,
1089
+ ) -> AttachedGripper:
1090
+ model = gripper_config.info_num_to_model(attached.model)
1091
+ serial = attached.serial
1092
+ return {
1093
+ "config": gripper_config.load(model),
1094
+ "id": f"GRPV{attached.model.replace('.', '')}{serial}",
1095
+ }
1096
+
1097
+ @staticmethod
1098
+ def _generate_attached_instrs(
1099
+ attached: ohc_tool_types.ToolSummary,
1100
+ ) -> Iterator[Tuple[OT3Mount, OT3AttachedInstruments]]:
1101
+ if attached.left:
1102
+ yield (
1103
+ OT3Mount.LEFT,
1104
+ OT3Controller._build_attached_pip(attached.left, OT3Mount.LEFT),
1105
+ )
1106
+ if attached.right:
1107
+ yield (
1108
+ OT3Mount.RIGHT,
1109
+ OT3Controller._build_attached_pip(attached.right, OT3Mount.RIGHT),
1110
+ )
1111
+ if attached.gripper:
1112
+ yield (
1113
+ OT3Mount.GRIPPER,
1114
+ OT3Controller._build_attached_gripper(attached.gripper),
1115
+ )
1116
+
1117
+ async def get_attached_instruments(
1118
+ self, expected: Mapping[OT3Mount, PipetteName]
1119
+ ) -> Dict[OT3Mount, OT3AttachedInstruments]:
1120
+ """Get attached instruments.
1121
+
1122
+ Args:
1123
+ expected: Which mounts are expected.
1124
+
1125
+ Returns:
1126
+ A map of mount to instrument name.
1127
+ """
1128
+ return dict(
1129
+ OT3Controller._generate_attached_instrs(self._subsystem_manager.tools)
1130
+ )
1131
+
1132
+ async def get_limit_switches(self) -> OT3AxisMap[bool]:
1133
+ """Get the state of the gantry's limit switches on each axis."""
1134
+ motor_nodes = self._motor_nodes()
1135
+ assert motor_nodes, "No nodes available to read limit switch status from"
1136
+ res = await get_limit_switches(self._messenger, motor_nodes)
1137
+ return {node_to_axis(node): bool(val) for node, val in res.items()}
1138
+
1139
+ @staticmethod
1140
+ def _tip_motor_nodes(axis_current_keys: KeysView[Axis]) -> List[NodeId]:
1141
+ return [axis_to_node(Axis.Q)] if Axis.Q in axis_current_keys else []
1142
+
1143
+ async def set_default_currents(self) -> None:
1144
+ """Set both run and hold currents from robot config to each node."""
1145
+ assert self._current_settings, "Invalid current settings"
1146
+ await set_currents(
1147
+ self._messenger,
1148
+ self._axis_map_to_present_nodes(
1149
+ {k: v.as_tuple() for k, v in self._current_settings.items()}
1150
+ ),
1151
+ use_tip_motor_message_for=self._tip_motor_nodes(
1152
+ self._current_settings.keys()
1153
+ ),
1154
+ )
1155
+
1156
+ @requires_update
1157
+ async def set_active_current(self, axis_currents: OT3AxisMap[float]) -> None:
1158
+ """Set the active current.
1159
+
1160
+ Args:
1161
+ axis_currents: Axes' currents
1162
+
1163
+ Returns:
1164
+ None
1165
+ """
1166
+ assert self._current_settings, "Invalid current settings"
1167
+ await set_run_current(
1168
+ self._messenger,
1169
+ self._axis_map_to_present_nodes(axis_currents),
1170
+ use_tip_motor_message_for=self._tip_motor_nodes(axis_currents.keys()),
1171
+ )
1172
+ for axis, current in axis_currents.items():
1173
+ self._current_settings[axis].run_current = current
1174
+
1175
+ @requires_update
1176
+ async def set_hold_current(self, axis_currents: OT3AxisMap[float]) -> None:
1177
+ """Set the hold current for motor.
1178
+
1179
+ Args:
1180
+ axis_currents: Axes' currents
1181
+
1182
+ Returns:
1183
+ None
1184
+ """
1185
+ assert self._current_settings, "Invalid current settings"
1186
+ await set_hold_current(
1187
+ self._messenger,
1188
+ self._axis_map_to_present_nodes(axis_currents),
1189
+ use_tip_motor_message_for=self._tip_motor_nodes(axis_currents.keys()),
1190
+ )
1191
+ for axis, current in axis_currents.items():
1192
+ self._current_settings[axis].hold_current = current
1193
+
1194
+ @asynccontextmanager
1195
+ async def motor_current(
1196
+ self,
1197
+ run_currents: Optional[OT3AxisMap[float]] = None,
1198
+ hold_currents: Optional[OT3AxisMap[float]] = None,
1199
+ ) -> AsyncIterator[None]:
1200
+ """Update and restore current."""
1201
+ assert self._current_settings
1202
+ old_settings = deepcopy(self._current_settings)
1203
+ if run_currents:
1204
+ await self.set_active_current(run_currents)
1205
+ if hold_currents:
1206
+ await self.set_hold_current(hold_currents)
1207
+ try:
1208
+ yield
1209
+ finally:
1210
+ if run_currents:
1211
+ await self.set_active_current(
1212
+ {ax: old_settings[ax].run_current for ax in run_currents.keys()}
1213
+ )
1214
+ if hold_currents:
1215
+ await self.set_hold_current(
1216
+ {ax: old_settings[ax].hold_current for ax in hold_currents.keys()}
1217
+ )
1218
+ if not run_currents and not hold_currents:
1219
+ self._current_settings = old_settings
1220
+ await self.set_default_currents()
1221
+
1222
+ @asynccontextmanager
1223
+ async def restore_z_r_run_current(self) -> AsyncIterator[None]:
1224
+ """
1225
+ Temporarily restore the active current ONLY when homing or
1226
+ retracting the Z_R axis while the 96-channel is attached.
1227
+ """
1228
+ assert self._current_settings
1229
+ high_throughput_settings = deepcopy(self._current_settings)
1230
+ conf = self.get_current_settings(GantryLoad.LOW_THROUGHPUT)[Axis.Z_R]
1231
+ # outside of homing and retracting, Z_R run current should
1232
+ # be reduced to its hold current
1233
+ await self.set_active_current({Axis.Z_R: conf.run_current})
1234
+ try:
1235
+ yield
1236
+ finally:
1237
+ await self.set_active_current(
1238
+ {Axis.Z_R: high_throughput_settings[Axis.Z_R].run_current}
1239
+ )
1240
+
1241
+ @asynccontextmanager
1242
+ async def increase_z_l_hold_current(self) -> AsyncIterator[None]:
1243
+ """
1244
+ Temporarily increase the hold current when engaging the Z_L axis
1245
+ while the 96-channel is attached
1246
+ """
1247
+ assert self._current_settings
1248
+ high_throughput_settings = deepcopy(self._current_settings)
1249
+ await self.set_hold_current(
1250
+ {Axis.Z_L: high_throughput_settings[Axis.Z_L].run_current}
1251
+ )
1252
+ try:
1253
+ yield
1254
+ finally:
1255
+ await self.set_hold_current(
1256
+ {Axis.Z_L: high_throughput_settings[Axis.Z_L].hold_current}
1257
+ )
1258
+
1259
+ @staticmethod
1260
+ def _build_event_watcher() -> aionotify.Watcher:
1261
+ watcher = aionotify.Watcher()
1262
+ watcher.watch(
1263
+ alias="modules",
1264
+ path="/dev",
1265
+ flags=(
1266
+ aionotify.Flags.CREATE
1267
+ | aionotify.Flags.DELETE
1268
+ | aionotify.Flags.MOVED_FROM
1269
+ | aionotify.Flags.MOVED_TO
1270
+ ),
1271
+ )
1272
+ return watcher
1273
+
1274
+ async def _handle_watch_event(self) -> None:
1275
+ try:
1276
+ event = await self._event_watcher.get_event()
1277
+ except asyncio.IncompleteReadError:
1278
+ log.debug("incomplete read error when quitting watcher")
1279
+ return
1280
+ if event is not None:
1281
+ flags = aionotify.Flags.parse(event.flags)
1282
+ log.debug(f"aionotify: {flags} {event.name}")
1283
+ if "ot_module" in event.name:
1284
+ event_name = event.name
1285
+ event_description = AionotifyEvent.build(event_name, flags)
1286
+ await self.module_controls.handle_module_appearance(event_description)
1287
+
1288
+ async def watch(self, loop: asyncio.AbstractEventLoop) -> None:
1289
+ can_watch = aionotify is not None
1290
+ if can_watch:
1291
+ await self._event_watcher.setup(loop)
1292
+
1293
+ while can_watch and (not self._event_watcher.closed):
1294
+ await self._handle_watch_event()
1295
+
1296
+ @property
1297
+ def axis_bounds(self) -> OT3AxisMap[Tuple[float, float]]:
1298
+ """Get the axis bounds."""
1299
+ # TODO (AL, 2021-11-18): The bounds need to be defined
1300
+ return {
1301
+ Axis.Z_L: (0, 300),
1302
+ Axis.Z_R: (0, 300),
1303
+ Axis.P_L: (0, 200),
1304
+ Axis.P_R: (0, 200),
1305
+ Axis.X: (0, 550),
1306
+ Axis.Y: (0, 550),
1307
+ Axis.Z_G: (0, 300),
1308
+ Axis.Q: (0, 200),
1309
+ }
1310
+
1311
+ def engaged_axes(self) -> OT3AxisMap[bool]:
1312
+ """Get engaged axes."""
1313
+ return self._engaged_axes
1314
+
1315
+ async def update_engaged_axes(self) -> None:
1316
+ """Update engaged axes."""
1317
+ motor_nodes = self._motor_nodes()
1318
+ results = await get_motor_enabled(self._messenger, motor_nodes)
1319
+ for node, status in results.items():
1320
+ self._engaged_axes[node_to_axis(node)] = status
1321
+
1322
+ async def is_motor_engaged(self, axis: Axis) -> bool:
1323
+ node = axis_to_node(axis)
1324
+ result = await get_motor_enabled(self._messenger, {node})
1325
+ try:
1326
+ engaged = result[node]
1327
+ except KeyError as ke:
1328
+ raise CommunicationError(
1329
+ message=f"No response from {node.name} for motor engagement query",
1330
+ detail={"node": node.name},
1331
+ wrapping=[PythonException(ke)],
1332
+ ) from ke
1333
+ self._engaged_axes.update({axis: engaged})
1334
+ return engaged
1335
+
1336
+ async def disengage_axes(self, axes: List[Axis]) -> None:
1337
+ """Disengage axes."""
1338
+ if Axis.Q in axes:
1339
+ await set_disable_tip_motor(self._messenger, {axis_to_node(Axis.Q)})
1340
+ self._engaged_axes[Axis.Q] = False
1341
+ axes = [ax for ax in axes if ax is not Axis.Q]
1342
+
1343
+ if len(axes) > 0:
1344
+ await set_disable_motor(self._messenger, {axis_to_node(ax) for ax in axes})
1345
+ for ax in axes:
1346
+ self._engaged_axes[ax] = False
1347
+
1348
+ async def engage_axes(self, axes: List[Axis]) -> None:
1349
+ """Engage axes."""
1350
+ if Axis.Q in axes:
1351
+ await set_enable_tip_motor(self._messenger, {axis_to_node(Axis.Q)})
1352
+ self._engaged_axes[Axis.Q] = True
1353
+ axes = [ax for ax in axes if ax is not Axis.Q]
1354
+
1355
+ if len(axes) > 0:
1356
+ await set_enable_motor(self._messenger, {axis_to_node(ax) for ax in axes})
1357
+ for ax in axes:
1358
+ self._engaged_axes[ax] = True
1359
+
1360
+ @requires_update
1361
+ async def set_lights(self, button: Optional[bool], rails: Optional[bool]) -> None:
1362
+ """Set the light states."""
1363
+ if rails is not None:
1364
+ await set_deck_light(1 if rails else 0, self._usb_messenger)
1365
+
1366
+ @requires_update
1367
+ async def get_lights(self) -> Dict[str, bool]:
1368
+ """Get the light state."""
1369
+ return {
1370
+ "rails": await get_deck_light_state(self._usb_messenger),
1371
+ "button": False,
1372
+ }
1373
+
1374
+ def pause(self) -> None:
1375
+ """Pause the controller activity."""
1376
+ return None
1377
+
1378
+ def resume(self) -> None:
1379
+ """Resume the controller activity."""
1380
+ return None
1381
+
1382
+ async def halt(self) -> None:
1383
+ """Halt the motors."""
1384
+ error = await self._messenger.ensure_send(
1385
+ NodeId.broadcast, StopRequest(payload=EmptyPayload())
1386
+ )
1387
+ if error != ErrorCode.ok:
1388
+ log.warning(f"Halt stop request failed: {error}")
1389
+
1390
+ async def probe(self, axis: Axis, distance: float) -> OT3AxisMap[float]:
1391
+ """Probe."""
1392
+ return {}
1393
+
1394
+ async def clean_up(self) -> None:
1395
+ """Clean up."""
1396
+ try:
1397
+ loop = asyncio.get_event_loop()
1398
+ except RuntimeError:
1399
+ return
1400
+
1401
+ if hasattr(self, "_event_watcher"):
1402
+ if (
1403
+ loop.is_running()
1404
+ and self._event_watcher
1405
+ and not self._event_watcher.closed
1406
+ ):
1407
+ self._event_watcher.close()
1408
+
1409
+ messenger = getattr(self, "_messenger", None)
1410
+ if messenger:
1411
+ await messenger.stop()
1412
+
1413
+ usb_messenger = getattr(self, "_usb_messenger", None)
1414
+ if usb_messenger:
1415
+ await usb_messenger.stop()
1416
+
1417
+ return None
1418
+
1419
+ @staticmethod
1420
+ def _get_home_position() -> Dict[NodeId, float]:
1421
+ return {
1422
+ NodeId.head_l: 0,
1423
+ NodeId.head_r: 0,
1424
+ NodeId.gantry_x: 0,
1425
+ NodeId.gantry_y: 0,
1426
+ NodeId.pipette_left: 0,
1427
+ NodeId.pipette_right: 0,
1428
+ NodeId.gripper_z: 0,
1429
+ NodeId.gripper_g: 0,
1430
+ }
1431
+
1432
+ @staticmethod
1433
+ def home_position() -> OT3AxisMap[float]:
1434
+ return {
1435
+ node_to_axis(k): v for k, v in OT3Controller._get_home_position().items()
1436
+ }
1437
+
1438
+ async def probe_network(self, timeout: float = 5.0) -> None:
1439
+ """Update the list of nodes present on the network.
1440
+
1441
+ The stored result is used to make sure that move commands include entries
1442
+ for all present axes, so none incorrectly move before the others are ready.
1443
+ """
1444
+ await self._subsystem_manager.refresh()
1445
+
1446
+ def axis_is_present(self, axis: Axis) -> bool:
1447
+ try:
1448
+ return axis_to_node(axis) in self._motor_nodes()
1449
+ except KeyError:
1450
+ # Currently unhandled axis
1451
+ return False
1452
+
1453
+ def _axis_map_to_present_nodes(
1454
+ self, to_xform: OT3AxisMap[MapPayload]
1455
+ ) -> NodeMap[MapPayload]:
1456
+ by_node = {axis_to_node(k): v for k, v in to_xform.items()}
1457
+ return {k: v for k, v in by_node.items() if k in self._motor_nodes()}
1458
+
1459
+ @asynccontextmanager
1460
+ async def _monitor_overpressure(self, mounts: List[NodeId]) -> AsyncIterator[None]:
1461
+ msg = "The pressure sensor on the {} mount has exceeded operational limits."
1462
+ if self._feature_flags.overpressure_detection_enabled and mounts:
1463
+ tools_with_id = map_pipette_type_to_sensor_id(
1464
+ mounts, self._subsystem_manager.device_info
1465
+ )
1466
+ # FIXME we should switch the sensor type based on the channel
1467
+ # used when partial tip pick up is implemented.
1468
+ provided_context_manager = await check_overpressure(
1469
+ self._messenger, tools_with_id
1470
+ )
1471
+ errors: asyncio.Queue[Tuple[NodeId, ErrorCode]] = asyncio.Queue()
1472
+
1473
+ async with provided_context_manager() as errors:
1474
+ try:
1475
+ yield
1476
+ finally:
1477
+
1478
+ def _pop_queue() -> Optional[Tuple[NodeId, ErrorCode]]:
1479
+ try:
1480
+ return errors.get_nowait()
1481
+ except asyncio.QueueEmpty:
1482
+ return None
1483
+
1484
+ q_msg = _pop_queue()
1485
+ if q_msg:
1486
+ mount = Axis.to_ot3_mount(node_to_axis(q_msg[0]))
1487
+ raise PipetteOverpressureError(
1488
+ message=msg.format(str(mount)),
1489
+ detail={"mount": str(mount)},
1490
+ )
1491
+ else:
1492
+ yield
1493
+
1494
+ async def liquid_probe(
1495
+ self,
1496
+ mount: OT3Mount,
1497
+ max_p_distance: float,
1498
+ mount_speed: float,
1499
+ plunger_speed: float,
1500
+ threshold_pascals: float,
1501
+ plunger_impulse_time: float,
1502
+ num_baseline_reads: int,
1503
+ z_offset_for_plunger_prep: float,
1504
+ probe: InstrumentProbeType = InstrumentProbeType.PRIMARY,
1505
+ force_both_sensors: bool = False,
1506
+ response_queue: Optional[PipetteSensorResponseQueue] = None,
1507
+ ) -> float:
1508
+ head_node = axis_to_node(Axis.by_mount(mount))
1509
+ tool = sensor_node_for_pipette(OT3Mount(mount.value))
1510
+ if tool not in self._pipettes_to_monitor_pressure([tool]):
1511
+ raise UnsupportedHardwareCommand(
1512
+ "Liquid Presence Detection not available on this pipette."
1513
+ )
1514
+
1515
+ if response_queue is None:
1516
+ response_capture: Optional[
1517
+ Callable[[Dict[SensorId, List[SensorDataType]]], None]
1518
+ ] = None
1519
+ else:
1520
+
1521
+ def response_capture(data: Dict[SensorId, List[SensorDataType]]) -> None:
1522
+ response_queue.put_nowait(
1523
+ {
1524
+ PipetteSensorId(sensor_id.value): [
1525
+ PipetteSensorData(
1526
+ sensor_type=PipetteSensorType(packet.sensor_type.value),
1527
+ _as_int=packet.to_int,
1528
+ _as_float=packet.to_float(),
1529
+ )
1530
+ for packet in packets
1531
+ ]
1532
+ for sensor_id, packets in data.items()
1533
+ }
1534
+ )
1535
+
1536
+ positions = await liquid_probe(
1537
+ messenger=self._messenger,
1538
+ tool=tool,
1539
+ head_node=head_node,
1540
+ max_p_distance=max_p_distance,
1541
+ plunger_speed=plunger_speed,
1542
+ mount_speed=mount_speed,
1543
+ threshold_pascals=threshold_pascals,
1544
+ plunger_impulse_time=plunger_impulse_time,
1545
+ num_baseline_reads=num_baseline_reads,
1546
+ z_offset_for_plunger_prep=z_offset_for_plunger_prep,
1547
+ sensor_id=sensor_id_for_instrument(probe),
1548
+ force_both_sensors=force_both_sensors,
1549
+ emplace_data=response_capture,
1550
+ )
1551
+ for node, point in positions.items():
1552
+ self._position.update({node: point.motor_position})
1553
+ self._encoder_position.update({node: point.encoder_position})
1554
+ if (
1555
+ head_node not in positions
1556
+ or positions[head_node].move_ack
1557
+ == MoveCompleteAck.complete_without_condition
1558
+ ):
1559
+ raise PipetteLiquidNotFoundError(
1560
+ "Liquid not found during probe.",
1561
+ {
1562
+ str(node_to_axis(node)): str(point.motor_position)
1563
+ for node, point in positions.items()
1564
+ },
1565
+ )
1566
+ return self._position[axis_to_node(Axis.by_mount(mount))]
1567
+
1568
+ async def capacitive_probe(
1569
+ self,
1570
+ mount: OT3Mount,
1571
+ moving: Axis,
1572
+ distance_mm: float,
1573
+ speed_mm_per_s: float,
1574
+ sensor_threshold_pf: float,
1575
+ probe: InstrumentProbeType = InstrumentProbeType.PRIMARY,
1576
+ ) -> bool:
1577
+ status = await capacitive_probe(
1578
+ messenger=self._messenger,
1579
+ tool=sensor_node_for_mount(mount),
1580
+ mover=axis_to_node(moving),
1581
+ distance=distance_mm,
1582
+ mount_speed=speed_mm_per_s,
1583
+ sensor_id=sensor_id_for_instrument(probe),
1584
+ relative_threshold_pf=sensor_threshold_pf,
1585
+ )
1586
+
1587
+ self._position[axis_to_node(moving)] = status.motor_position
1588
+ return status.move_ack == MoveCompleteAck.stopped_by_condition
1589
+
1590
+ async def capacitive_pass(
1591
+ self,
1592
+ mount: OT3Mount,
1593
+ moving: Axis,
1594
+ distance_mm: float,
1595
+ speed_mm_per_s: float,
1596
+ probe: InstrumentProbeType,
1597
+ ) -> List[float]:
1598
+ data = await capacitive_pass(
1599
+ self._messenger,
1600
+ sensor_node_for_mount(mount),
1601
+ axis_to_node(moving),
1602
+ distance_mm,
1603
+ speed_mm_per_s,
1604
+ sensor_id_for_instrument(probe),
1605
+ )
1606
+ self._position[axis_to_node(moving)] += distance_mm
1607
+ return data
1608
+
1609
+ async def release_estop(self) -> None:
1610
+ if self._gpio_dev is None:
1611
+ log.error("no gpio control available")
1612
+ raise IOError("no gpio control")
1613
+ elif isinstance(self._gpio_dev, RemoteOT3GPIO):
1614
+ await self._gpio_dev.deactivate_estop()
1615
+ else:
1616
+ self._gpio_dev.deactivate_estop()
1617
+
1618
+ async def engage_estop(self) -> None:
1619
+ if self._gpio_dev is None:
1620
+ log.error("no gpio control available")
1621
+ raise IOError("no gpio control")
1622
+ elif isinstance(self._gpio_dev, RemoteOT3GPIO):
1623
+ await self._gpio_dev.activate_estop()
1624
+ else:
1625
+ self._gpio_dev.activate_estop()
1626
+
1627
+ async def release_sync(self) -> None:
1628
+ if self._gpio_dev is None:
1629
+ log.error("no gpio control available")
1630
+ raise IOError("no gpio control")
1631
+ elif isinstance(self._gpio_dev, RemoteOT3GPIO):
1632
+ await self._gpio_dev.deactivate_nsync_out()
1633
+ else:
1634
+ self._gpio_dev.deactivate_nsync_out()
1635
+
1636
+ async def engage_sync(self) -> None:
1637
+ if self._gpio_dev is None:
1638
+ log.error("no gpio control available")
1639
+ raise IOError("no gpio control")
1640
+ elif isinstance(self._gpio_dev, RemoteOT3GPIO):
1641
+ await self._gpio_dev.activate_nsync_out()
1642
+ else:
1643
+ self._gpio_dev.activate_nsync_out()
1644
+
1645
+ async def door_state(self) -> DoorState:
1646
+ door_open = await get_door_state(self._usb_messenger)
1647
+ return DoorState.OPEN if door_open else DoorState.CLOSED
1648
+
1649
+ def add_door_state_listener(
1650
+ self, callback: Callable[[DoorState, str | None], None]
1651
+ ) -> None:
1652
+ def _module_door_listener(door_state: DoorState) -> None:
1653
+ module_serial: str | None = None
1654
+ for module in self.module_controls.available_modules:
1655
+ # Systematically handle doored modules
1656
+ if (
1657
+ module.MODULE_TYPE == modules.types.ModuleType.FLEX_STACKER
1658
+ and module.hopper_door_state == modules.types.HopperDoorState.OPENED
1659
+ ):
1660
+ module_serial = module.serial_number
1661
+ break
1662
+ callback(door_state, module_serial)
1663
+
1664
+ def _door_listener(msg: BinaryMessageDefinition) -> None:
1665
+ door_state = (
1666
+ DoorState.OPEN
1667
+ if cast(DoorSwitchStateInfo, msg).door_open.value
1668
+ else DoorState.CLOSED
1669
+ )
1670
+ _module_door_listener(door_state)
1671
+
1672
+ if self._usb_messenger is not None:
1673
+ self._usb_messenger.add_listener(
1674
+ _door_listener,
1675
+ lambda message_id: bool(
1676
+ message_id == BinaryMessageId.door_switch_state_info
1677
+ ),
1678
+ )
1679
+
1680
+ async def build_estop_detector(self) -> bool:
1681
+ """Must be called to set up the estop detector & state machine."""
1682
+ if self._drivers.usb_messenger is None:
1683
+ return False
1684
+ self._estop_detector = await EstopDetector.build(
1685
+ usb_messenger=self._drivers.usb_messenger
1686
+ )
1687
+ self._estop_state_machine.subscribe_to_detector(self._estop_detector)
1688
+ return True
1689
+
1690
+ @property
1691
+ def tip_presence_manager(self) -> TipPresenceManager:
1692
+ return self._tip_presence_manager
1693
+
1694
+ async def update_tip_detector(self, mount: OT3Mount, sensor_count: int) -> None:
1695
+ """Build indiviudal tip detector for a mount."""
1696
+ await self.teardown_tip_detector(mount)
1697
+ await self._tip_presence_manager.build_detector(mount, sensor_count)
1698
+
1699
+ async def teardown_tip_detector(self, mount: OT3Mount) -> None:
1700
+ await self._tip_presence_manager.clear_detector(mount)
1701
+
1702
+ async def get_tip_status(
1703
+ self,
1704
+ mount: OT3Mount,
1705
+ follow_singular_sensor: Optional[InstrumentProbeType] = None,
1706
+ ) -> TipStateType:
1707
+ return await self.tip_presence_manager.get_tip_status(
1708
+ mount, follow_singular_sensor
1709
+ )
1710
+
1711
+ def current_tip_state(self, mount: OT3Mount) -> Optional[bool]:
1712
+ return self.tip_presence_manager.current_tip_state(mount)
1713
+
1714
+ async def set_status_bar_state(self, state: StatusBarState) -> None:
1715
+ await self._status_bar_controller.set_status_bar_state(state)
1716
+
1717
+ async def set_status_bar_enabled(self, enabled: bool) -> None:
1718
+ await self._status_bar_controller.set_enabled(enabled)
1719
+
1720
+ def get_status_bar_enabled(self) -> bool:
1721
+ return self._status_bar_controller.get_enabled()
1722
+
1723
+ def get_status_bar_state(self) -> StatusBarState:
1724
+ return self._status_bar_controller.get_current_state()
1725
+
1726
+ def add_status_bar_listener(
1727
+ self, listener: StatusBarUpdateListener
1728
+ ) -> StatusBarUpdateUnsubscriber:
1729
+ remove_cb = self._status_bar_controller.add_listener(listener)
1730
+ return remove_cb
1731
+
1732
+ @property
1733
+ def estop_status(self) -> EstopOverallStatus:
1734
+ return EstopOverallStatus(
1735
+ state=self._estop_state_machine.state,
1736
+ left_physical_state=self._estop_state_machine.get_physical_status(
1737
+ EstopAttachLocation.LEFT
1738
+ ),
1739
+ right_physical_state=self._estop_state_machine.get_physical_status(
1740
+ EstopAttachLocation.RIGHT
1741
+ ),
1742
+ )
1743
+
1744
+ def estop_acknowledge_and_clear(self) -> EstopOverallStatus:
1745
+ """Attempt to acknowledge an Estop event and clear the status.
1746
+
1747
+ Returns the estop status after clearing the status."""
1748
+ self._estop_state_machine.acknowledge_and_clear()
1749
+ return self.estop_status
1750
+
1751
+ def get_estop_state(self) -> EstopState:
1752
+ return self._estop_state_machine.state
1753
+
1754
+ def add_estop_callback(self, cb: HardwareEventHandler) -> HardwareEventUnsubscriber:
1755
+ return self._estop_state_machine.add_listener(cb)
1756
+
1757
+ def check_gripper_position_within_bounds(
1758
+ self,
1759
+ expected_grip_width: float,
1760
+ grip_width_uncertainty_wider: float,
1761
+ grip_width_uncertainty_narrower: float,
1762
+ jaw_width: float,
1763
+ max_allowed_grip_error: float,
1764
+ hard_limit_lower: float,
1765
+ hard_limit_upper: float,
1766
+ ) -> None:
1767
+ """
1768
+ Check if the gripper is at the expected location.
1769
+
1770
+ While this doesn't seem like it belongs here, it needs to act differently
1771
+ when we're simulating, so it does.
1772
+ """
1773
+ expected_gripper_position_min = (
1774
+ expected_grip_width - grip_width_uncertainty_narrower
1775
+ )
1776
+ expected_gripper_position_max = (
1777
+ expected_grip_width + grip_width_uncertainty_wider
1778
+ )
1779
+ current_gripper_position = jaw_width
1780
+ if isclose(current_gripper_position, hard_limit_lower):
1781
+ raise FailedGripperPickupError(
1782
+ message="Failed to grip: jaws all the way closed",
1783
+ details={
1784
+ "failure-type": "jaws-all-the-way-closed",
1785
+ "actual-jaw-width": current_gripper_position,
1786
+ },
1787
+ )
1788
+ if isclose(current_gripper_position, hard_limit_upper):
1789
+ raise FailedGripperPickupError(
1790
+ message="Failed to grip: jaws all the way open",
1791
+ details={
1792
+ "failure-type": "jaws-all-the-way-open",
1793
+ "actual-jaw-width": current_gripper_position,
1794
+ },
1795
+ )
1796
+ if (
1797
+ current_gripper_position - expected_gripper_position_min
1798
+ < -max_allowed_grip_error
1799
+ ):
1800
+ raise FailedGripperPickupError(
1801
+ message="Failed to grip: jaws closed too far",
1802
+ details={
1803
+ "failure-type": "jaws-more-closed-than-expected",
1804
+ "lower-bound-labware-width": expected_grip_width
1805
+ - grip_width_uncertainty_narrower,
1806
+ "actual-jaw-width": current_gripper_position,
1807
+ },
1808
+ )
1809
+ if (
1810
+ current_gripper_position - expected_gripper_position_max
1811
+ > max_allowed_grip_error
1812
+ ):
1813
+ raise FailedGripperPickupError(
1814
+ message="Failed to grip: jaws could not close far enough",
1815
+ details={
1816
+ "failure-type": "jaws-more-open-than-expected",
1817
+ "upper-bound-labware-width": expected_grip_width
1818
+ - grip_width_uncertainty_narrower,
1819
+ "actual-jaw-width": current_gripper_position,
1820
+ },
1821
+ )
1822
+
1823
+ async def set_hepa_fan_state(self, fan_on: bool, duty_cycle: int) -> bool:
1824
+ return await set_hepa_fan_state_fw(self._messenger, fan_on, duty_cycle)
1825
+
1826
+ async def get_hepa_fan_state(self) -> Optional[HepaFanState]:
1827
+ res = await get_hepa_fan_state_fw(self._messenger)
1828
+ return (
1829
+ HepaFanState(
1830
+ fan_on=res.fan_on,
1831
+ duty_cycle=res.duty_cycle,
1832
+ )
1833
+ if res
1834
+ else None
1835
+ )
1836
+
1837
+ async def set_hepa_uv_state(self, light_on: bool, uv_duration_s: int) -> bool:
1838
+ return await set_hepa_uv_state_fw(self._messenger, light_on, uv_duration_s)
1839
+
1840
+ async def get_hepa_uv_state(self) -> Optional[HepaUVState]:
1841
+ res = await get_hepa_uv_state_fw(self._messenger)
1842
+ return (
1843
+ HepaUVState(
1844
+ light_on=res.uv_light_on,
1845
+ uv_duration_s=res.uv_duration_s,
1846
+ remaining_time_s=res.remaining_time_s,
1847
+ )
1848
+ if res
1849
+ else None
1850
+ )
1851
+
1852
+ def _update_tip_state(self, mount: OT3Mount, status: bool) -> None:
1853
+ """This is something we only use in the simulator.
1854
+ It is required so that PE simulations using ot3api don't break."""
1855
+ pass
1856
+
1857
+ async def increase_evo_disp_count(self, mount: OT3Mount) -> None:
1858
+ """Tell a pipette to increase it's evo-tip-dispense-count in eeprom."""
1859
+ await send_evo_dispense_count_increase(
1860
+ self._messenger, sensor_node_for_pipette(OT3Mount(mount.value))
1861
+ )
1862
+
1863
+ async def _read_env_sensor(
1864
+ self, mount: OT3Mount, primary: bool
1865
+ ) -> Optional[EnvironmentSensorDataType]:
1866
+ """Read and return the current sensor information."""
1867
+ sensor = EnvironmentSensor.build(
1868
+ sensor_id=SensorId.S0 if primary else SensorId.S1,
1869
+ node_id=sensor_node_for_mount(mount),
1870
+ )
1871
+ s_driver = SensorDriver()
1872
+ sensor_data = await s_driver.read(
1873
+ can_messenger=self._messenger,
1874
+ sensor=sensor,
1875
+ offset=False,
1876
+ )
1877
+ assert sensor_data is None or isinstance(sensor_data, EnvironmentSensorDataType)
1878
+ return sensor_data
1879
+
1880
+ async def read_env_temp_sensor(
1881
+ self, mount: OT3Mount, primary: bool
1882
+ ) -> Optional[float]:
1883
+ """Read and return the current sensor information."""
1884
+ s_data = await self._read_env_sensor(mount, primary)
1885
+ if s_data is None or s_data.temperature is None:
1886
+ return None
1887
+ return s_data.temperature.to_float()
1888
+
1889
+ async def read_env_hum_sensor(
1890
+ self, mount: OT3Mount, primary: bool
1891
+ ) -> Optional[float]:
1892
+ """Read and return the current sensor information."""
1893
+ s_data = await self._read_env_sensor(mount, primary)
1894
+ if s_data is None or s_data.humidity is None:
1895
+ return None
1896
+ return s_data.humidity.to_float()
1897
+
1898
+ async def read_pressure_sensor(
1899
+ self, mount: OT3Mount, primary: bool
1900
+ ) -> Optional[float]:
1901
+ """Read and return the current sensor information."""
1902
+ sensor = PressureSensor.build(
1903
+ sensor_id=SensorId.S0 if primary else SensorId.S1,
1904
+ node_id=sensor_node_for_mount(mount),
1905
+ )
1906
+ s_driver = SensorDriver()
1907
+ sensor_data = await s_driver.read(
1908
+ can_messenger=self._messenger,
1909
+ sensor=sensor,
1910
+ offset=False,
1911
+ )
1912
+ assert sensor_data is None or isinstance(sensor_data, SensorDataType)
1913
+ return sensor_data.to_float() if sensor_data else None
1914
+
1915
+ async def read_capacitive_sensor(
1916
+ self, mount: OT3Mount, primary: bool
1917
+ ) -> Optional[float]:
1918
+ """Read and return the current sensor information."""
1919
+ sensor = CapacitiveSensor.build(
1920
+ sensor_id=SensorId.S0 if primary else SensorId.S1,
1921
+ node_id=sensor_node_for_mount(mount),
1922
+ )
1923
+ s_driver = SensorDriver()
1924
+ sensor_data = await s_driver.read(
1925
+ can_messenger=self._messenger,
1926
+ sensor=sensor,
1927
+ offset=False,
1928
+ )
1929
+ assert sensor_data is None or isinstance(sensor_data, SensorDataType)
1930
+ return sensor_data.to_float() if sensor_data else None