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,1933 @@
1
+ """
2
+ - Driver is responsible for providing an interface for motion control
3
+ - Driver is the only system component that knows about GCODES or how smoothie
4
+ communications
5
+
6
+ - Driver is NOT responsible interpreting the motions in any way
7
+ or knowing anything about what the axes are used for
8
+ """
9
+
10
+ from __future__ import annotations
11
+ import asyncio
12
+ import contextlib
13
+ import logging
14
+ from os import environ
15
+ from time import monotonic
16
+ from typing import Any, Dict, Optional, Union, List, Tuple, cast, AsyncIterator
17
+
18
+ from math import isclose
19
+
20
+ from opentrons.drivers.serial_communication import get_ports_by_name
21
+ from serial.serialutil import SerialException # type: ignore[import-untyped]
22
+
23
+ from opentrons.drivers.smoothie_drivers.connection import SmoothieConnection
24
+ from opentrons.drivers.smoothie_drivers.constants import (
25
+ GCODE,
26
+ HOMED_POSITION,
27
+ Y_BOUND_OVERRIDE,
28
+ SMOOTHIE_COMMAND_TERMINATOR,
29
+ SMOOTHIE_ACK,
30
+ PLUNGER_BACKLASH_MM,
31
+ CURRENT_CHANGE_DELAY,
32
+ PIPETTE_READ_DELAY,
33
+ Y_SWITCH_BACK_OFF_MM,
34
+ Y_SWITCH_REVERSE_BACK_OFF_MM,
35
+ Y_BACKOFF_LOW_CURRENT,
36
+ Y_BACKOFF_SLOW_SPEED,
37
+ Y_RETRACT_SPEED,
38
+ Y_RETRACT_DISTANCE,
39
+ UNSTICK_DISTANCE,
40
+ UNSTICK_SPEED,
41
+ DEFAULT_AXES_SPEED,
42
+ XY_HOMING_SPEED,
43
+ HOME_SEQUENCE,
44
+ AXES,
45
+ DISABLE_AXES,
46
+ SEC_PER_MIN,
47
+ DEFAULT_ACK_TIMEOUT,
48
+ DEFAULT_EXECUTE_TIMEOUT,
49
+ DEFAULT_MOVEMENT_TIMEOUT,
50
+ SMOOTHIE_BOOT_TIMEOUT,
51
+ DEFAULT_STABILIZE_DELAY,
52
+ DEFAULT_COMMAND_RETRIES,
53
+ MICROSTEPPING_GCODES,
54
+ GCODE_ROUNDING_PRECISION,
55
+ )
56
+ from opentrons.drivers.smoothie_drivers.errors import (
57
+ SmoothieError,
58
+ SmoothieAlarm,
59
+ TipProbeError,
60
+ )
61
+ from opentrons.drivers.smoothie_drivers import parse_utils
62
+ from opentrons.drivers.command_builder import CommandBuilder
63
+
64
+ from opentrons.config.types import RobotConfig
65
+ from opentrons.config.robot_configs import current_for_revision
66
+ from opentrons.drivers.asyncio.communication import (
67
+ SerialConnection,
68
+ NoResponse,
69
+ AlarmResponse,
70
+ ErrorResponse,
71
+ )
72
+ from opentrons.drivers.types import MoveSplits
73
+ from opentrons.drivers.utils import AxisMoveTimestamp, ParseError, string_to_hex
74
+ from opentrons.drivers.rpi_drivers.gpio_simulator import SimulatingGPIOCharDev
75
+ from opentrons.drivers.rpi_drivers.dev_types import GPIODriverLike
76
+ from opentrons.system import smoothie_update
77
+ from .types import AxisCurrentSettings
78
+
79
+
80
+ log = logging.getLogger(__name__)
81
+
82
+
83
+ def _command_builder() -> CommandBuilder:
84
+ """Create a CommandBuilder"""
85
+ return CommandBuilder(terminator=SMOOTHIE_COMMAND_TERMINATOR)
86
+
87
+
88
+ class SmoothieDriver:
89
+ @classmethod
90
+ async def build(
91
+ cls,
92
+ port: str,
93
+ config: RobotConfig,
94
+ gpio_chardev: Optional[GPIODriverLike] = None,
95
+ ) -> SmoothieDriver:
96
+ """
97
+ Build a smoothie driver
98
+
99
+ Args:
100
+ port: The port
101
+ config: Robot configuration
102
+ gpio_chardev: Optional GPIO driver
103
+
104
+ Returns:
105
+ A SmoothieDriver instance.
106
+ """
107
+ connection = await SmoothieConnection.create(
108
+ port=port,
109
+ baud_rate=config.serial_speed,
110
+ name="smoothie",
111
+ timeout=DEFAULT_EXECUTE_TIMEOUT,
112
+ ack=SMOOTHIE_ACK,
113
+ reset_buffer_before_write=True,
114
+ )
115
+ gpio_chardev = gpio_chardev or SimulatingGPIOCharDev("simulated")
116
+
117
+ instance = cls(config=config, connection=connection, gpio_chardev=gpio_chardev)
118
+ await instance._setup()
119
+ return instance
120
+
121
+ def __init__(
122
+ self,
123
+ config: RobotConfig,
124
+ gpio_chardev: GPIODriverLike,
125
+ connection: Optional[SerialConnection] = None,
126
+ ):
127
+ """
128
+ Constructor
129
+
130
+ Args:
131
+ config: The robot configuration
132
+ gpio_chardev: GPIO device.
133
+ connection: The serial connection.
134
+ """
135
+ self.run_flag = asyncio.Event()
136
+ self.run_flag.set()
137
+
138
+ self._position = HOMED_POSITION.copy()
139
+
140
+ # why do we do this after copying the HOMED_POSITION?
141
+ self._update_position({axis: 0 for axis in AXES})
142
+
143
+ self.simulating = connection is None
144
+ self._connection = connection
145
+ self._config = config
146
+
147
+ self._gpio_chardev = gpio_chardev
148
+
149
+ # Current settings:
150
+ # The amperage of each axis, has been organized into three states:
151
+ # Current-Settings is the amperage each axis was last set to
152
+ # Active-Current-Settings is set when an axis is moving/homing
153
+ # Dwelling-Current-Settings is set when an axis is NOT moving/homing
154
+ self._current_settings = AxisCurrentSettings(
155
+ val=current_for_revision(config.low_current, self._gpio_chardev.board_rev)
156
+ )
157
+ self._active_current_settings = AxisCurrentSettings(
158
+ val=current_for_revision(config.high_current, self._gpio_chardev.board_rev)
159
+ )
160
+ self._dwelling_current_settings = AxisCurrentSettings(
161
+ val=current_for_revision(config.low_current, self._gpio_chardev.board_rev)
162
+ )
163
+
164
+ # Active axes are axes that are in use. An axis might be disabled if
165
+ # a motor has had a failure and the robot is operating without that
166
+ # axis until it can be repaired. This will be an unusual circumstance.
167
+ self._active_axes = {ax: False for ax in AXES}
168
+
169
+ # Engaged axes are axes that have not been disengaged (GCode M18) since
170
+ # their last "move" or "home" operations. Disengaging an axis stops the
171
+ # power output to the associated motor, primarily for the purpose of
172
+ # reducing heat. When a "disengage" command is sent for an axis, this
173
+ # dict should be updated to False for that axis, and when a "move" or
174
+ # "home" command is sent for an axis, that axis should be updated to
175
+ # True.
176
+ self.engaged_axes = {ax: True for ax in AXES}
177
+
178
+ # motor speed settings
179
+ self._max_speed_settings = cast(
180
+ Dict[str, float], config.default_max_speed.copy()
181
+ )
182
+ self._saved_max_speed_settings = self._max_speed_settings.copy()
183
+ self._combined_speed = float(DEFAULT_AXES_SPEED)
184
+ self._saved_axes_speed = float(self._combined_speed)
185
+ self._steps_per_mm: Dict[str, float] = {}
186
+ self._acceleration = config.acceleration.copy()
187
+ self._saved_acceleration = config.acceleration.copy()
188
+
189
+ # position after homing
190
+ self._homed_position = HOMED_POSITION.copy()
191
+ self.homed_flags: Dict[str, bool] = {
192
+ "X": False,
193
+ "Y": False,
194
+ "Z": False,
195
+ "A": False,
196
+ "B": False,
197
+ "C": False,
198
+ }
199
+
200
+ self._is_hard_halting = asyncio.Event()
201
+ self._move_split_config: MoveSplits = {}
202
+ #: Cache of currently configured splits from callers
203
+ self._axes_moved_at = AxisMoveTimestamp(AXES)
204
+
205
+ @property
206
+ def gpio_chardev(self) -> GPIODriverLike:
207
+ return self._gpio_chardev
208
+
209
+ @gpio_chardev.setter
210
+ def gpio_chardev(self, gpio_chardev: GPIODriverLike) -> None:
211
+ self._gpio_chardev = gpio_chardev
212
+
213
+ @property
214
+ def homed_position(self) -> Dict[str, float]:
215
+ return self._homed_position.copy()
216
+
217
+ @property
218
+ def axis_bounds(self) -> Dict[str, float]:
219
+ bounds = {k: v for k, v in self._homed_position.items()}
220
+ bounds["Y"] = Y_BOUND_OVERRIDE
221
+ return bounds
222
+
223
+ def _update_position(self, target: Dict[str, float]) -> None:
224
+ """Update the cached position."""
225
+ self._position.update(
226
+ {axis: value for axis, value in target.items() if value is not None}
227
+ )
228
+
229
+ async def update_position(self, default: Optional[Dict[str, float]] = None) -> None:
230
+ """Get the current position from the smoothie and cache it."""
231
+ if default is None:
232
+ default = self._position
233
+
234
+ if self.simulating:
235
+ updated_position = self._position.copy()
236
+ updated_position.update(**default)
237
+ else:
238
+
239
+ async def _recursive_update_position(retries: int) -> Dict[str, float]:
240
+ try:
241
+ position_response = await self._send_command(
242
+ _command_builder().add_gcode(gcode=GCODE.CURRENT_POSITION)
243
+ )
244
+ return parse_utils.parse_position_response(position_response)
245
+ except ParseError as e:
246
+ retries -= 1
247
+ if retries <= 0:
248
+ raise e
249
+ await asyncio.sleep(DEFAULT_STABILIZE_DELAY)
250
+ return await _recursive_update_position(retries)
251
+
252
+ updated_position = await _recursive_update_position(DEFAULT_COMMAND_RETRIES)
253
+
254
+ self._update_position(updated_position)
255
+
256
+ def configure_splits_for(self, config: MoveSplits) -> None:
257
+ """Configure the driver to automatically split moves on a given
258
+ axis that execute (including pauses) after a specified amount of
259
+ time. The move created will adhere to the split config.
260
+
261
+ To remove the setting, set None for the specified axis.
262
+
263
+ Only pipette axes may be specified for splitting
264
+ """
265
+ assert all(
266
+ (ax.lower() in "bc" for ax in config.keys())
267
+ ), "splits may only be configured for plunger axes"
268
+ self._move_split_config.update(config)
269
+ log.info(f"Updated move split config with {config}")
270
+ self._axes_moved_at.reset_moved(config.keys())
271
+
272
+ async def read_pipette_id(self, mount: str) -> Optional[str]:
273
+ """
274
+ Reads in an attached pipette's ID
275
+ The ID is unique to this pipette, and is a string of unknown length
276
+
277
+ :param mount: string with value 'left' or 'right'
278
+ :return id string, or None
279
+ """
280
+ res: Optional[str] = None
281
+ if self.simulating:
282
+ res = "1234567890"
283
+ else:
284
+ try:
285
+ res = await self._read_from_pipette(GCODE.READ_INSTRUMENT_ID, mount)
286
+ except UnicodeDecodeError:
287
+ log.exception("Failed to decode pipette ID string:")
288
+ res = None
289
+ return res
290
+
291
+ async def read_pipette_model(self, mount: str) -> Optional[str]:
292
+ """
293
+ Reads an attached pipette's MODEL
294
+ The MODEL is a unique string for this model of pipette
295
+
296
+ :param mount: string with value 'left' or 'right'
297
+ :return model string, or None
298
+ """
299
+ if self.simulating:
300
+ res = None
301
+ else:
302
+ res = await self._read_from_pipette(GCODE.READ_INSTRUMENT_MODEL, mount)
303
+ if res and "_v" not in res:
304
+ # Backward compatibility for pipettes programmed with model
305
+ # strings that did not include the _v# designation
306
+ res = res + "_v1"
307
+ elif res and "_v13" in res:
308
+ # Backward compatibility for pipettes programmed with model
309
+ # strings that did not include the "." to seperate version
310
+ # major and minor values
311
+ res = res.replace("_v13", "_v1.3")
312
+ return res
313
+
314
+ async def write_pipette_id(self, mount: str, data_string: str) -> None:
315
+ """
316
+ Writes to an attached pipette's ID memory location
317
+ The ID is unique to this pipette, and is a string of unknown length
318
+
319
+ NOTE: To enable write-access to the pipette, it's button must be held
320
+
321
+ mount:
322
+ String (str) with value 'left' or 'right'
323
+ data_string:
324
+ String (str) that is of unknown length, and should be unique to
325
+ this one pipette
326
+ """
327
+ await self._write_to_pipette(GCODE.WRITE_INSTRUMENT_ID, mount, data_string)
328
+
329
+ async def write_pipette_model(self, mount: str, data_string: str) -> None:
330
+ """
331
+ Writes to an attached pipette's MODEL memory location
332
+ The MODEL is a unique string for this model of pipette
333
+
334
+ NOTE: To enable write-access to the pipette, it's button must be held
335
+
336
+ mount:
337
+ String (str) with value 'left' or 'right'
338
+ data_string:
339
+ String (str) that is unique to this model of pipette
340
+ """
341
+ await self._write_to_pipette(GCODE.WRITE_INSTRUMENT_MODEL, mount, data_string)
342
+
343
+ async def update_pipette_config(
344
+ self, axis: str, data: Dict[str, float]
345
+ ) -> Dict[str, Dict[str, float]]:
346
+ """
347
+ Updates the following configs for a given pipette mount based on
348
+ the detected pipette type:
349
+ - homing positions M365.0
350
+ - Max Travel M365.1
351
+ - endstop debounce M365.2 (NOT for zprobe debounce)
352
+ - retract from endstop distance M365.3
353
+
354
+ Returns the data as the value of a dict with the axis as a key.
355
+ For instance, calling update_pipette_config('B', {'retract': 2})
356
+ would return (if successful) {'B': {'retract': 2}}
357
+ """
358
+ if self.simulating:
359
+ return {axis: data}
360
+
361
+ gcodes = {
362
+ "retract": GCODE.PIPETTE_RETRACT,
363
+ "debounce": GCODE.PIPETTE_DEBOUNCE,
364
+ "max_travel": GCODE.PIPETTE_MAX_TRAVEL,
365
+ "home": GCODE.PIPETTE_HOME,
366
+ }
367
+
368
+ res_msg: Dict[str, Dict[str, float]] = {axis: {}}
369
+
370
+ for key, value in data.items():
371
+ cmd = _command_builder().add_gcode(gcode=gcodes[key])
372
+ if key == "debounce":
373
+ # debounce variable for all axes, so do not specify an axis
374
+ cmd.add_float(prefix="O", value=value, precision=None)
375
+ else:
376
+ cmd.add_float(prefix=axis, value=value, precision=None)
377
+ res = await self._send_command(cmd)
378
+ if res is None:
379
+ raise ValueError(f"{key} was not updated to {value} on {axis} axis")
380
+ res_msg[axis][key] = value
381
+
382
+ return res_msg
383
+
384
+ # FIXME (JG 9/28/17): Should have a more thought out
385
+ # way of simulating vs really running
386
+ async def connect(self, port: Optional[str] = None) -> None:
387
+ if environ.get("ENABLE_VIRTUAL_SMOOTHIE", "").lower() == "true":
388
+ self.simulating = True
389
+ return
390
+ await self.disconnect()
391
+ await self._connect_to_port(port)
392
+ await self._setup()
393
+
394
+ async def disconnect(self) -> None:
395
+ if self._connection and await self.is_connected():
396
+ await self._connection.close()
397
+ self._connection = None
398
+ self.simulating = True
399
+
400
+ async def is_connected(self) -> bool:
401
+ if not self._connection:
402
+ return False
403
+ return await self._connection.is_open()
404
+
405
+ @staticmethod
406
+ def get_port() -> str:
407
+ """Determine the port to connect to."""
408
+ # Check if smoothie emulator is to be used
409
+ port = environ.get("OT_SMOOTHIE_EMULATOR_URI")
410
+ if port:
411
+ return port
412
+ smoothie_id = environ.get("OT_SMOOTHIE_ID", "AMA")
413
+ # Let this raise an exception.
414
+ return get_ports_by_name(device_name=smoothie_id)[0]
415
+
416
+ async def _connect_to_port(self, port: Optional[str] = None) -> None:
417
+ try:
418
+ port = self.get_port() if port is None else port
419
+
420
+ log.info(f"Connecting to smoothie at port {port}")
421
+
422
+ self._connection = await SmoothieConnection.create(
423
+ port=port,
424
+ baud_rate=self._config.serial_speed,
425
+ name="smoothie",
426
+ timeout=DEFAULT_EXECUTE_TIMEOUT,
427
+ ack=SMOOTHIE_ACK,
428
+ reset_buffer_before_write=True,
429
+ )
430
+ self.simulating = False
431
+ except SerialException:
432
+ # if another process is using the port, pyserial raises an
433
+ # exception that describes a "readiness to read" which is confusing
434
+ error_msg = "Unable to access UART port to Smoothie. This is "
435
+ error_msg += "because another process is currently using it, or "
436
+ error_msg += "the UART port is disabled on this device (OS)"
437
+ raise SerialException(error_msg)
438
+
439
+ @property
440
+ def port(self) -> Optional[str]:
441
+ if not self._connection:
442
+ return None
443
+ return self._connection.port
444
+
445
+ async def get_fw_version(self) -> str:
446
+ """
447
+ Queries Smoothieware for it's build version, and returns
448
+ the parsed response.
449
+
450
+ returns: str
451
+ Current version of attached Smoothi-driver. Versions are derived
452
+ from git branch-hash (eg: edge-66ec883NOMSD)
453
+
454
+ Example Smoothieware response:
455
+
456
+ Build version: edge-66ec883NOMSD, Build date: Jan 28 2018 15:26:57, MCU: LPC1769, System Clock: 120MHz # NOQA
457
+ CNC Build NOMSD Build
458
+ 6 axis
459
+ """
460
+ if self.simulating:
461
+ version = "Virtual Smoothie"
462
+ else:
463
+ version = await self._send_command(
464
+ _command_builder().add_gcode(gcode=GCODE.VERSION)
465
+ )
466
+ version = version.split(",")[0].split(":")[-1].strip()
467
+ version = version.replace("NOMSD", "")
468
+ return version
469
+
470
+ @property
471
+ def position(self) -> Dict[str, float]:
472
+ """
473
+ Instead of sending M114.2 we are storing target values in
474
+ self._position since movement and home commands are blocking and
475
+ assumed to go the correct place.
476
+
477
+ Cases where Smoothie would not be in the correct place (such as if a
478
+ belt slips) would not be corrected by getting position with M114.2
479
+ because Smoothie would also not be aware of slippage.
480
+ """
481
+ return {k.upper(): v for k, v in self._position.items()}
482
+
483
+ async def switch_state(self) -> Dict[str, bool]:
484
+ """Returns the state of all SmoothieBoard limit switches"""
485
+ res = await self._send_command(
486
+ _command_builder().add_gcode(gcode=GCODE.LIMIT_SWITCH_STATUS)
487
+ )
488
+ return parse_utils.parse_switch_values(res)
489
+
490
+ async def update_homed_flags(self, flags: Optional[Dict[str, bool]] = None) -> None:
491
+ """
492
+ Returns Smoothieware's current homing-status, which is a dictionary
493
+ of boolean values for each axis (XYZABC). If an axis is False, then it
494
+ still needs to be homed, and it's coordinate cannot be trusted.
495
+ Smoothieware sets it's internal homing flags for all axes to False when
496
+ it has yet to home since booting/restarting, or an endstop/homing error
497
+ """
498
+ if flags and isinstance(flags, dict):
499
+ self.homed_flags.update(flags)
500
+ elif self.simulating:
501
+ self.homed_flags.update({ax: False for ax in AXES})
502
+ elif await self.is_connected():
503
+
504
+ async def _recursive_update_homed_flags(retries: int) -> None:
505
+ try:
506
+ res = await self._send_command(
507
+ _command_builder().add_gcode(gcode=GCODE.HOMING_STATUS)
508
+ )
509
+ flags = parse_utils.parse_homing_status_values(res)
510
+ self.homed_flags.update(flags)
511
+ except ParseError as e:
512
+ retries -= 1
513
+ if retries <= 0:
514
+ raise e
515
+ await asyncio.sleep(DEFAULT_STABILIZE_DELAY)
516
+ return await _recursive_update_homed_flags(retries)
517
+
518
+ await _recursive_update_homed_flags(DEFAULT_COMMAND_RETRIES)
519
+
520
+ @property
521
+ def current(self) -> Dict[str, float]:
522
+ return self._current_settings.now
523
+
524
+ @property
525
+ def speed(self) -> None:
526
+ pass
527
+
528
+ @property
529
+ def steps_per_mm(self) -> Dict[str, float]:
530
+ return self._steps_per_mm
531
+
532
+ @contextlib.asynccontextmanager
533
+ async def restore_speed(self, value: Union[float, str]) -> AsyncIterator[None]:
534
+ await self.set_speed(value, update=False)
535
+ try:
536
+ yield
537
+ finally:
538
+ await self.set_speed(self._combined_speed)
539
+
540
+ @staticmethod
541
+ def _build_speed_command(speed: float) -> CommandBuilder:
542
+ return (
543
+ _command_builder()
544
+ .add_gcode(gcode=GCODE.SET_SPEED)
545
+ .add_int(prefix="F", value=int(float(speed) * SEC_PER_MIN))
546
+ )
547
+
548
+ async def set_speed(self, value: Union[float, str], update: bool = True) -> None:
549
+ """set total axes movement speed in mm/second"""
550
+ if update:
551
+ self._combined_speed = float(value)
552
+ command = self._build_speed_command(float(value))
553
+ log.debug(f"set_speed: {command}")
554
+ await self._send_command(command)
555
+
556
+ def push_speed(self) -> None:
557
+ self._saved_axes_speed = float(self._combined_speed)
558
+
559
+ async def pop_speed(self) -> None:
560
+ await self.set_speed(self._saved_axes_speed)
561
+
562
+ @contextlib.asynccontextmanager
563
+ async def restore_axis_max_speed(
564
+ self, new_max_speeds: Dict[str, float]
565
+ ) -> AsyncIterator[None]:
566
+ await self.set_axis_max_speed(new_max_speeds, update=False)
567
+ try:
568
+ yield
569
+ finally:
570
+ await self.set_axis_max_speed(self._max_speed_settings)
571
+
572
+ async def set_axis_max_speed(
573
+ self, settings: Dict[str, float], update: bool = True
574
+ ) -> None:
575
+ """
576
+ Sets the maximum speed (mm/sec) that a given axis will move
577
+
578
+ settings
579
+ Dict with axes as valies (e.g.: 'X', 'Y', 'Z', 'A', 'B', or 'C')
580
+ and floating point number for millimeters per second (mm/sec)
581
+ update
582
+ bool, True to save the settings for future use
583
+ """
584
+ if update:
585
+ self._max_speed_settings.update(settings)
586
+
587
+ command = _command_builder().add_gcode(gcode=GCODE.SET_MAX_SPEED)
588
+ for axis, value in sorted(settings.items()):
589
+ command = command.add_float(prefix=axis, value=value, precision=None)
590
+
591
+ log.debug(f"set_axis_max_speed: {command}")
592
+ await self._send_command(command)
593
+
594
+ def push_axis_max_speed(self) -> None:
595
+ self._saved_max_speed_settings = self._max_speed_settings.copy()
596
+
597
+ async def pop_axis_max_speed(self) -> None:
598
+ await self.set_axis_max_speed(self._saved_max_speed_settings)
599
+
600
+ async def set_acceleration(self, settings: Dict[str, float]) -> None:
601
+ """
602
+ Sets the acceleration (mm/sec^2) that a given axis will move
603
+
604
+ settings
605
+ Dict with axes as valies (e.g.: 'X', 'Y', 'Z', 'A', 'B', or 'C')
606
+ and floating point number for mm-per-second-squared (mm/sec^2)
607
+ """
608
+ self._acceleration.update(settings)
609
+
610
+ command = (
611
+ _command_builder()
612
+ .add_gcode(gcode=GCODE.ACCELERATION)
613
+ .add_int(prefix="S", value=10000)
614
+ )
615
+ for axis, value in sorted(settings.items()):
616
+ command.add_float(prefix=axis, value=value, precision=None)
617
+
618
+ log.debug(f"set_acceleration: {command}")
619
+ await self._send_command(command)
620
+
621
+ def push_acceleration(self) -> None:
622
+ self._saved_acceleration = self._acceleration.copy()
623
+
624
+ async def pop_acceleration(self) -> None:
625
+ await self.set_acceleration(self._saved_acceleration)
626
+
627
+ def set_active_current(self, settings: Dict[str, float]) -> None:
628
+ """
629
+ Sets the amperage of each motor for when it is activated by driver.
630
+ Values are initialized from the `robot_config.high_current` values,
631
+ and can then be changed through this method by other parts of the API.
632
+
633
+ For example, `Pipette` setting the active-current of it's pipette,
634
+ depending on what model pipette it is, and what action it is performing
635
+
636
+ settings
637
+ Dict with axes as valies (e.g.: 'X', 'Y', 'Z', 'A', 'B', or 'C')
638
+ and floating point number for current (generally between 0.1 and 2)
639
+ """
640
+ self._active_current_settings.now.update(settings)
641
+
642
+ # if an axis specified in the `settings` is currently active,
643
+ # reset it's current to the new active-current value
644
+ active_axes_to_update = {
645
+ axis: amperage
646
+ for axis, amperage in self._active_current_settings.now.items()
647
+ if self._active_axes.get(axis) is True
648
+ if self.current[axis] != amperage
649
+ }
650
+ if active_axes_to_update:
651
+ self._save_current(active_axes_to_update, axes_active=True)
652
+
653
+ def push_active_current(self) -> None:
654
+ self._active_current_settings.saved.update(self._active_current_settings.now)
655
+
656
+ def pop_active_current(self) -> None:
657
+ self.set_active_current(self._active_current_settings.saved)
658
+
659
+ def set_dwelling_current(self, settings: Dict[str, float]) -> None:
660
+ """
661
+ Sets the amperage of each motor for when it is dwelling.
662
+ Values are initialized from the `robot_config.log_current` values,
663
+ and can then be changed through this method by other parts of the API.
664
+
665
+ For example, `Pipette` setting the dwelling-current of it's pipette,
666
+ depending on what model pipette it is.
667
+
668
+ settings
669
+ Dict with axes as valies (e.g.: 'X', 'Y', 'Z', 'A', 'B', or 'C')
670
+ and floating point number for current (generally between 0.1 and 2)
671
+ """
672
+ self._dwelling_current_settings.now.update(settings)
673
+
674
+ # if an axis specified in the `settings` is currently dwelling,
675
+ # reset it's current to the new dwelling-current value
676
+ dwelling_axes_to_update = {
677
+ axis: amps
678
+ for axis, amps in self._dwelling_current_settings.now.items()
679
+ if self._active_axes.get(axis) is False
680
+ if self.current[axis] != amps
681
+ }
682
+ if dwelling_axes_to_update:
683
+ self._save_current(dwelling_axes_to_update, axes_active=False)
684
+
685
+ def push_dwelling_current(self) -> None:
686
+ self._dwelling_current_settings.saved.update(
687
+ self._dwelling_current_settings.now
688
+ )
689
+
690
+ def pop_dwelling_current(self) -> None:
691
+ self.set_dwelling_current(self._dwelling_current_settings.saved)
692
+
693
+ def _save_current(
694
+ self, settings: Dict[str, float], axes_active: bool = True
695
+ ) -> None:
696
+ """
697
+ Sets the current in Amperes (A) by axis. Currents are limited to be
698
+ between 0.0-2.0 amps per axis motor.
699
+
700
+ Note: this method does not send gcode commands, but instead stores the
701
+ desired current setting. A seperate call to _generate_current_command()
702
+ will return a gcode command that can be used to set Smoothie's current
703
+
704
+ settings
705
+ Dict with axes as valies (e.g.: 'X', 'Y', 'Z', 'A', 'B', or 'C')
706
+ and floating point number for current (generally between 0.1 and 2)
707
+ """
708
+ self._active_axes.update({ax: axes_active for ax in settings.keys()})
709
+ self._current_settings.now.update(settings)
710
+ log.debug(f"_save_current: {self.current}")
711
+
712
+ async def _set_saved_current(self) -> None:
713
+ """
714
+ Sends the driver's current settings to the serial port as gcode. Call
715
+ this method to set the axis-current state on the actual Smoothie
716
+ motor-driver.
717
+ """
718
+ await self._send_command(self._generate_current_command())
719
+
720
+ def _generate_current_command(self) -> CommandBuilder:
721
+ """
722
+ Returns a constructed GCode string that contains this driver's
723
+ axis-current settings, plus a small delay to wait for those settings
724
+ to take effect.
725
+ """
726
+ command = _command_builder().add_gcode(gcode=GCODE.SET_CURRENT)
727
+ for axis, value in sorted(self.current.items()):
728
+ command.add_float(prefix=axis, value=value, precision=None)
729
+
730
+ command.add_gcode(gcode=GCODE.DWELL).add_float(
731
+ prefix="P", value=CURRENT_CHANGE_DELAY, precision=None
732
+ )
733
+ log.debug(f"_generate_current_command: {command}")
734
+ return command
735
+
736
+ async def disengage_axis(self, axes: str) -> None:
737
+ """
738
+ Disable the stepper-motor-driver's 36v output to motor
739
+ This is a safe GCODE to send to Smoothieware, as it will automatically
740
+ re-engage the motor if it receives a home or move command
741
+
742
+ axes
743
+ String containing the axes to be disengaged
744
+ (e.g.: 'XY' or 'ZA' or 'XYZABC')
745
+ """
746
+ available_axes = set(AXES)
747
+ axes = "".join(a for a in axes.upper() if a in available_axes)
748
+ if axes:
749
+ log.debug(f"disengage_axis: {axes}")
750
+ await self._send_command(
751
+ _command_builder()
752
+ .add_gcode(gcode=GCODE.DISENGAGE_MOTOR)
753
+ .add_element(element=axes)
754
+ )
755
+ for axis in axes:
756
+ self.engaged_axes[axis] = False
757
+
758
+ def dwell_axes(self, axes: str) -> None:
759
+ """
760
+ Sets motors to low current, for when they are not moving.
761
+
762
+ Dwell for XYZA axes is only called after HOMING
763
+ Dwell for BC axes is called after both HOMING and MOVING
764
+
765
+ axes:
766
+ String containing the axes to set to low current (eg: 'XYZABC')
767
+ """
768
+ axes = "".join(set(axes) & set(AXES) - set(DISABLE_AXES))
769
+ dwelling_currents = {
770
+ ax: self._dwelling_current_settings.now[ax]
771
+ for ax in axes
772
+ if self._active_axes[ax] is True
773
+ }
774
+ if dwelling_currents:
775
+ self._save_current(dwelling_currents, axes_active=False)
776
+
777
+ def activate_axes(self, axes: str) -> None:
778
+ """
779
+ Sets motors to a high current, for when they are moving
780
+ and/or must hold position
781
+
782
+ Activating XYZABC axes before both HOMING and MOVING
783
+
784
+ axes:
785
+ String containing the axes to set to high current (eg: 'XYZABC')
786
+ """
787
+ axes = "".join(set(axes) & set(AXES) - set(DISABLE_AXES))
788
+ active_currents = {
789
+ ax: self._active_current_settings.now[ax]
790
+ for ax in axes
791
+ if self._active_axes[ax] is False
792
+ }
793
+ if active_currents:
794
+ self._save_current(active_currents, axes_active=True)
795
+
796
+ # ----------- Private functions --------------- #
797
+
798
+ async def _wait_for_ack(self) -> None:
799
+ """
800
+ In the case where smoothieware has just been reset, we want to
801
+ ignore all the garbage it spits out
802
+
803
+ This methods writes a sequence of newline characters, which will
804
+ guarantee Smoothieware responds with 'ok\r\nok\r\n' within 3 seconds
805
+ """
806
+ if self._connection:
807
+ await self._connection.flush_input()
808
+ await self._send_command(_command_builder(), timeout=SMOOTHIE_BOOT_TIMEOUT)
809
+
810
+ async def _reset_from_error(self) -> None:
811
+ # smoothieware will ignore new messages for a short time
812
+ # after it has entered an error state, so sleep for some milliseconds
813
+ await asyncio.sleep(DEFAULT_STABILIZE_DELAY)
814
+ log.debug("reset_from_error")
815
+ self._is_hard_halting.clear()
816
+ await self._send_command(
817
+ _command_builder().add_gcode(gcode=GCODE.RESET_FROM_ERROR)
818
+ )
819
+ await self.update_homed_flags()
820
+
821
+ # Potential place for command optimization (buffering, flushing, etc)
822
+ async def _send_command(
823
+ self,
824
+ command: CommandBuilder,
825
+ timeout: float = DEFAULT_EXECUTE_TIMEOUT,
826
+ suppress_error_msg: bool = False,
827
+ ack_timeout: float = DEFAULT_ACK_TIMEOUT,
828
+ suppress_home_after_error: bool = False,
829
+ ) -> str:
830
+ """
831
+ Submit a GCODE command to the robot, followed by M400 to block until
832
+ done. This method also ensures that any command on the B or C axis
833
+ (the axis for plunger control) do current ramp-up and ramp-down, so
834
+ that plunger motors rest at a low current to prevent burn-out.
835
+
836
+ In the case of a limit-switch alarm during any command other than home,
837
+ the robot should home the axis from the alarm and then raise a
838
+ SmoothieError. The robot should *not* recover and continue to run the
839
+ protocol, as this could result in unpredictable handling of liquids.
840
+ When a SmoothieError is raised, the user should inspect the physical
841
+ configuration of the robot and the protocol and determine why the limit
842
+ switch was hit unexpectedly. This is usually due to an undetected
843
+ collision in a previous move command.
844
+
845
+ SmoothieErrors are also raised when a command is sent to a pipette that
846
+ is not present, such as when identifying which pipettes are on a robot.
847
+ In this case, the message should not be logged, so the caller of this
848
+ function should specify `supress_error_msg=True`.
849
+
850
+ :param command: the GCODE to submit to the robot
851
+ :param timeout: the time to wait for the smoothie to execute the
852
+ command (after an m400). this should be long enough to allow the
853
+ command to execute. If this is None, the timeout will be infinite.
854
+ This is almost certainly not what you want.
855
+ :param suppress_error_msg: flag for indicating that smoothie errors
856
+ should not be logged
857
+ :param ack_timeout: The time to wait for the smoothie to ack a
858
+ command. For commands that queue (like move) or are short (like
859
+ pipette interrogation) this should be a small number, and is the
860
+ default. For commands the smoothie only acks after execution,
861
+ like home, it should be long enough to allow the command to
862
+ complete in the worst case. If this is None, the timeout will
863
+ be infinite. This is almost certainly not what you want.
864
+ """
865
+ if self.simulating:
866
+ return ""
867
+ try:
868
+ return await self._send_command_unsynchronized(
869
+ command, ack_timeout, timeout
870
+ )
871
+ except SmoothieError as se:
872
+ # XXX: This is a reentrancy error because another command could
873
+ # swoop in here. We're already resetting though and errors (should
874
+ # be) rare so it's probably fine, but the actual solution to this
875
+ # is locking at a higher level like in APIv2.
876
+ await self._reset_from_error()
877
+ error_axis = se.ret_code.strip()[-1]
878
+ if not suppress_error_msg:
879
+ log.warning(f"alarm/error: command={command}, resp={se.ret_code}")
880
+ if (
881
+ GCODE.MOVE in command or GCODE.PROBE in command
882
+ ) and not suppress_home_after_error:
883
+ if error_axis not in "XYZABC":
884
+ error_axis = AXES
885
+ log.info("Homing after alarm/error")
886
+ await self.home(error_axis)
887
+ raise SmoothieError(se.ret_code, str(command))
888
+
889
+ async def _send_command_unsynchronized(
890
+ self, command: CommandBuilder, ack_timeout: float, execute_timeout: float
891
+ ) -> str:
892
+ assert self._connection, "There is no connection."
893
+ command_result = ""
894
+ try:
895
+ command_result = await self._connection.send_command(
896
+ command=command, retries=DEFAULT_COMMAND_RETRIES, timeout=ack_timeout
897
+ )
898
+ wait_command = CommandBuilder(
899
+ terminator=SMOOTHIE_COMMAND_TERMINATOR
900
+ ).add_gcode(gcode=GCODE.WAIT)
901
+ await self._connection.send_command(
902
+ command=wait_command, retries=0, timeout=execute_timeout
903
+ )
904
+ except AlarmResponse as e:
905
+ self._handle_return(ret_code=e.response, is_alarm=True)
906
+ except ErrorResponse as e:
907
+ self._handle_return(ret_code=e.response, is_error=True)
908
+ return command_result
909
+
910
+ def _handle_return(
911
+ self, ret_code: str, is_alarm: bool = False, is_error: bool = False
912
+ ) -> None:
913
+ """Check the return string from smoothie for an error condition.
914
+
915
+ Usually raises a SmoothieError, which can be handled by the error
916
+ handling in write_with_retries. However, if the hard halt line has
917
+ been set, we need to catch that halt and _not_ handle it, since it
918
+ is used for things like cancelling protocols and needs to be
919
+ handled elsewhere. In that case, we raise SmoothieAlarm, which isn't
920
+ (and shouldn't be) handled by the normal error handling.
921
+ """
922
+ if self._is_hard_halting.is_set():
923
+ # This is the alarm from setting the hard halt
924
+ if is_alarm:
925
+ self._is_hard_halting.clear()
926
+ raise SmoothieAlarm(ret_code)
927
+ elif is_error:
928
+ # this would be a race condition
929
+ raise SmoothieError(ret_code)
930
+ else:
931
+ if is_alarm or is_error:
932
+ # info-level logging for errors of form "no L instrument found"
933
+ if "instrument found" in ret_code.lower():
934
+ log.info(f"smoothie: {ret_code}")
935
+ raise SmoothieError(ret_code)
936
+
937
+ # the two errors below happen when we're recovering from a hard
938
+ # halt. in that case, some try/finallys above us may send
939
+ # further commands. smoothie responds to those commands with
940
+ # errors like these. if we raise exceptions here, they
941
+ # overwrite the original exception and we don't properly
942
+ # handle it. This hack to get around this is really bad!
943
+ if (
944
+ "alarm lock" not in ret_code.lower()
945
+ and "after halt you should home" not in ret_code.lower()
946
+ ):
947
+ log.error(f"alarm/error outside hard halt: {ret_code}")
948
+ raise SmoothieError(ret_code)
949
+
950
+ async def _home_x(self) -> None:
951
+ log.debug("_home_x")
952
+ # move the gantry forward on Y axis with low power
953
+ self._save_current({"Y": Y_BACKOFF_LOW_CURRENT})
954
+ self.push_axis_max_speed()
955
+ await self.set_axis_max_speed({"Y": Y_BACKOFF_SLOW_SPEED})
956
+
957
+ # move away from the Y endstop switch, then backward half that distance
958
+ relative_retract_command = (
959
+ _command_builder()
960
+ .add_gcode(
961
+ # set to relative coordinate system
962
+ gcode=GCODE.RELATIVE_COORDS
963
+ )
964
+ .add_gcode(gcode=GCODE.MOVE)
965
+ .add_int(
966
+ # move towards front of machine
967
+ prefix="Y",
968
+ value=int(-Y_SWITCH_BACK_OFF_MM),
969
+ )
970
+ .add_gcode(gcode=GCODE.MOVE)
971
+ .add_int(
972
+ # move towards back of machine
973
+ prefix="Y",
974
+ value=int(Y_SWITCH_REVERSE_BACK_OFF_MM),
975
+ )
976
+ .add_gcode(
977
+ # set back to abs coordinate system
978
+ gcode=GCODE.ABSOLUTE_COORDS
979
+ )
980
+ )
981
+
982
+ command = self._generate_current_command().add_builder(
983
+ builder=relative_retract_command
984
+ )
985
+ await self._send_command(command)
986
+ self.dwell_axes("Y")
987
+
988
+ # time it is safe to home the X axis
989
+ try:
990
+ # override firmware's default XY homing speed, to avoid resonance
991
+ await self.set_axis_max_speed({"X": XY_HOMING_SPEED})
992
+ self.activate_axes("X")
993
+ command = (
994
+ self._generate_current_command()
995
+ .add_gcode(gcode=GCODE.HOME)
996
+ .add_element("X")
997
+ )
998
+ # home commands are acked after execution rather than queueing, so
999
+ # we want a long ack timeout and a short execution timeout
1000
+ home_timeout = (HOMED_POSITION["X"] / XY_HOMING_SPEED) * 2
1001
+ await self._send_command(command, ack_timeout=home_timeout, timeout=5)
1002
+ await self.update_homed_flags(flags={"X": True})
1003
+ finally:
1004
+ await self.pop_axis_max_speed()
1005
+ self.dwell_axes("X")
1006
+ await self._set_saved_current()
1007
+
1008
+ async def _home_y(self) -> None:
1009
+ log.debug("_home_y")
1010
+ # override firmware's default XY homing speed, to avoid resonance
1011
+ self.push_axis_max_speed()
1012
+ await self.set_axis_max_speed({"Y": XY_HOMING_SPEED})
1013
+
1014
+ self.activate_axes("Y")
1015
+ # home the Y at normal speed (fast)
1016
+ command = (
1017
+ self._generate_current_command()
1018
+ .add_gcode(gcode=GCODE.HOME)
1019
+ .add_element("Y")
1020
+ )
1021
+ fast_home_timeout = (HOMED_POSITION["Y"] / XY_HOMING_SPEED) * 2
1022
+ # home commands are executed before ack, set a long ack timeout
1023
+ await self._send_command(command, ack_timeout=fast_home_timeout, timeout=5)
1024
+
1025
+ # slow the maximum allowed speed on Y axis
1026
+ await self.set_axis_max_speed({"Y": Y_RETRACT_SPEED})
1027
+
1028
+ # retract, then home, then retract again
1029
+ relative_retract_command = (
1030
+ _command_builder()
1031
+ .add_gcode(
1032
+ # set to relative coordinate system
1033
+ gcode=GCODE.RELATIVE_COORDS
1034
+ )
1035
+ .add_gcode(gcode=GCODE.MOVE)
1036
+ .add_int(
1037
+ # move 3 millimeters away from switch
1038
+ prefix="Y",
1039
+ value=-Y_RETRACT_DISTANCE,
1040
+ )
1041
+ .add_gcode(
1042
+ # set back to abs coordinate system
1043
+ gcode=GCODE.ABSOLUTE_COORDS
1044
+ )
1045
+ )
1046
+ try:
1047
+ await self._send_command(relative_retract_command)
1048
+ # home commands are executed before ack, use a long ack timeout
1049
+ slow_timeout = (Y_RETRACT_DISTANCE / Y_RETRACT_SPEED) * 2
1050
+ await self._send_command(
1051
+ _command_builder().add_gcode(gcode=GCODE.HOME).add_element("Y"),
1052
+ ack_timeout=slow_timeout,
1053
+ timeout=5,
1054
+ )
1055
+ await self.update_homed_flags(flags={"Y": True})
1056
+ await self._send_command(relative_retract_command)
1057
+ finally:
1058
+ await self.pop_axis_max_speed() # bring max speeds back to normal
1059
+ self.dwell_axes("Y")
1060
+ await self._set_saved_current()
1061
+
1062
+ async def _setup(self) -> None:
1063
+ log.debug("_setup")
1064
+ try:
1065
+ await self._wait_for_ack()
1066
+ except NoResponse:
1067
+ # in case motor-driver is stuck in bootloader and unresponsive,
1068
+ # use gpio to reset into a known state
1069
+ log.debug("wait for ack failed, resetting")
1070
+ await self._smoothie_reset()
1071
+ log.debug("wait for ack done")
1072
+ await self._reset_from_error()
1073
+ log.debug("_reset")
1074
+ await self.update_steps_per_mm(self._config.gantry_steps_per_mm)
1075
+ await self.update_steps_per_mm(
1076
+ {ax: self._config.default_pipette_configs["stepsPerMM"] for ax in "BC"}
1077
+ )
1078
+ log.debug("sent steps")
1079
+ await self._send_command(
1080
+ _command_builder().add_gcode(gcode=GCODE.ABSOLUTE_COORDS)
1081
+ )
1082
+ log.debug("sent abs")
1083
+ self._save_current(self.current, axes_active=False)
1084
+ log.debug("sent current")
1085
+ await self.update_position(default=self.homed_position)
1086
+ await self.pop_axis_max_speed()
1087
+ await self.pop_speed()
1088
+ await self.pop_acceleration()
1089
+ log.debug("setup done")
1090
+
1091
+ def _build_steps_per_mm(self, data: Dict[str, float]) -> CommandBuilder:
1092
+ """Build the set steps/mm command string without sending"""
1093
+ command = _command_builder()
1094
+
1095
+ if not data:
1096
+ return command
1097
+
1098
+ command.add_gcode(gcode=GCODE.STEPS_PER_MM)
1099
+ for axis, value in data.items():
1100
+ command.add_float(prefix=axis, value=value, precision=None)
1101
+ return command
1102
+
1103
+ async def update_steps_per_mm(self, data: Union[Dict[str, float], str]) -> None:
1104
+ # Using M92, update steps per mm for a given axis
1105
+ if self.simulating:
1106
+ if isinstance(data, dict):
1107
+ self.steps_per_mm.update(data)
1108
+ return
1109
+
1110
+ if isinstance(data, str):
1111
+ # Unfortunately update server calls driver._setup() before the
1112
+ # update can correctly load the robot_config change on disk.
1113
+ # Need to account for old command format to avoid this issue.
1114
+ await self._send_command(_command_builder().add_gcode(data))
1115
+ else:
1116
+ self.steps_per_mm.update(data)
1117
+ cmd = self._build_steps_per_mm(data)
1118
+ await self._send_command(cmd)
1119
+
1120
+ async def _read_from_pipette(self, gcode: str, mount: str) -> Optional[str]:
1121
+ """
1122
+ Read from an attached pipette's internal memory. The gcode used
1123
+ determines which portion of memory is read and returned.
1124
+
1125
+ All motors must be disengaged to consistently read over I2C lines
1126
+
1127
+ gcode:
1128
+ String (str) containing a GCode
1129
+ either 'READ_INSTRUMENT_ID' or 'READ_INSTRUMENT_MODEL'
1130
+ mount:
1131
+ String (str) with value 'left' or 'right'
1132
+ """
1133
+ allowed_mounts = {"left": "L", "right": "R"}
1134
+ allowed_mount = allowed_mounts.get(mount)
1135
+ if not allowed_mount:
1136
+ raise ValueError(f"Unexpected mount: {mount}")
1137
+ try:
1138
+ # EMI interference from both plunger motors has been found to
1139
+ # prevent the I2C lines from communicating between Smoothieware and
1140
+ # pipette's onboard EEPROM. To avoid, turn off both plunger motors
1141
+ await self.disengage_axis("ZABC")
1142
+ await self.delay(PIPETTE_READ_DELAY)
1143
+ # request from Smoothieware the information from that pipette
1144
+ res = await self._send_command(
1145
+ _command_builder().add_gcode(gcode=gcode).add_element(allowed_mount),
1146
+ suppress_error_msg=True,
1147
+ )
1148
+ if res:
1149
+ parsed_res = parse_utils.parse_instrument_data(res)
1150
+ assert allowed_mount in parsed_res
1151
+ # data is read/written as strings of HEX characters
1152
+ # to avoid firmware weirdness in how it parses GCode arguments
1153
+ return parse_utils.byte_array_to_ascii_string(parsed_res[allowed_mount])
1154
+ except (ParseError, AssertionError, SmoothieError):
1155
+ pass
1156
+ return None
1157
+
1158
+ async def _write_to_pipette(self, gcode: str, mount: str, data_string: str) -> None:
1159
+ """
1160
+ Write to an attached pipette's internal memory. The gcode used
1161
+ determines which portion of memory is written to.
1162
+
1163
+ NOTE: To enable write-access to the pipette, it's button must be held
1164
+
1165
+ gcode:
1166
+ String (str) containing a GCode
1167
+ either 'WRITE_INSTRUMENT_ID' or 'WRITE_INSTRUMENT_MODEL'
1168
+ mount:
1169
+ String (str) with value 'left' or 'right'
1170
+ data_string:
1171
+ String (str) that is of unkown length
1172
+ """
1173
+ allowed_mounts = {"left": "L", "right": "R"}
1174
+ allowed_mount = allowed_mounts.get(mount)
1175
+ if not allowed_mount:
1176
+ raise ValueError(f"Unexpected mount: {mount}")
1177
+ if not isinstance(data_string, str):
1178
+ raise ValueError("Expected {0}, not {1}".format(str, type(data_string)))
1179
+ # EMI interference from both plunger motors has been found to
1180
+ # prevent the I2C lines from communicating between Smoothieware and
1181
+ # pipette's onboard EEPROM. To avoid, turn off both plunger motors
1182
+ await self.disengage_axis("BC")
1183
+ await self.delay(CURRENT_CHANGE_DELAY)
1184
+ # data is read/written as strings of HEX characters
1185
+ # to avoid firmware weirdness in how it parses GCode arguments
1186
+ byte_string = string_to_hex(val=data_string)
1187
+ command = (
1188
+ _command_builder()
1189
+ .add_gcode(gcode=gcode)
1190
+ .add_element(element=allowed_mount)
1191
+ .add_element(element=byte_string)
1192
+ )
1193
+ log.debug(f"_write_to_pipette: {command}")
1194
+ await self._send_command(command)
1195
+
1196
+ # ----------- END Private functions ----------- #
1197
+
1198
+ # ----------- Public interface ---------------- #
1199
+ async def move( # noqa: C901
1200
+ self,
1201
+ target: Dict[str, float],
1202
+ home_flagged_axes: bool = False,
1203
+ speed: Optional[float] = None,
1204
+ ) -> None:
1205
+ """
1206
+ Move to the `target` Smoothieware coordinate, along any of the size
1207
+ axes, XYZABC.
1208
+
1209
+ :param target: dict setting the coordinate that Smoothieware will be
1210
+ at when `move()` returns. `target` keys are the axis in
1211
+ upper-case, and the values are the coordinate in mm (float)
1212
+ :param home_flagged_axes: boolean (default=False)
1213
+ If set to `True`, each axis included within the target coordinate
1214
+ may be homed before moving, determined by Smoothieware's internal
1215
+ homing-status flags (`True` means it has already homed). All axes'
1216
+ flags are set to `False` by Smoothieware under three conditions:
1217
+ 1) Smoothieware boots or resets, 2) if a HALT gcode or signal
1218
+ is sent, or 3) a homing/limitswitch error occured.
1219
+ :param speed: Optional speed for the move. If not specified, set to the
1220
+ current cached _combined_speed. To avoid conflict with callers that
1221
+ expect the smoothie's speed setting to always be combined_speed,
1222
+ the smoothie is set back to this state after every move
1223
+
1224
+
1225
+ If the current move split config indicates that the move should be
1226
+ broken up, the driver will do so. If the new position requires a
1227
+ change in position of an axis with a split configuration, it may be
1228
+ split into multiple moves such that the axis will move a maximum of the
1229
+ specified split distance at the specified current and speed. If the
1230
+ axis would move less than the split distance, it will move the
1231
+ entire distance at the specified current and speed.
1232
+
1233
+ This command respects the run flag and will wait until it is set.
1234
+
1235
+ The function may issue up to 3 moves:
1236
+ - if move splitting is required, the split move
1237
+ - the actual move, plus a bit extra to give room to preload backlash
1238
+ - if we preload backlash we then issue a third move to preload backlash
1239
+ """
1240
+ await self.run_flag.wait()
1241
+
1242
+ def valid_movement(axis: str, coord: float) -> bool:
1243
+ """True if the axis is not disabled and the coord is different
1244
+ from the current position cache
1245
+ """
1246
+ return not (
1247
+ (axis in DISABLE_AXES)
1248
+ or isclose(coord, self.position[axis], rel_tol=1e-05, abs_tol=1e-08)
1249
+ )
1250
+
1251
+ def only_moving(move_target: Dict[str, float]) -> Dict[str, float]:
1252
+ """Filter a target dict to have only those axes which have valid
1253
+ movements"""
1254
+ return {
1255
+ ax: coord
1256
+ for ax, coord in move_target.items()
1257
+ if valid_movement(ax, coord)
1258
+ }
1259
+
1260
+ def create_coords_list(coords_dict: Dict[str, float]) -> CommandBuilder:
1261
+ """Build the gcode string for a move"""
1262
+ cmd = _command_builder()
1263
+ for axis, coords in sorted(coords_dict.items()):
1264
+ if valid_movement(axis, coords):
1265
+ cmd.add_float(
1266
+ prefix=axis, value=coords, precision=GCODE_ROUNDING_PRECISION
1267
+ )
1268
+ return cmd
1269
+
1270
+ moving_target = only_moving(target)
1271
+ if not moving_target:
1272
+ log.info(f"No axes move in {target} from position {self.position}")
1273
+ return
1274
+
1275
+ # Multi-axis movements should include the added backlash.
1276
+ # After all axes arrive at target, finally then apply
1277
+ # a backlash correction to just the plunger axes
1278
+ plunger_backlash_axes = [
1279
+ axis
1280
+ for axis, value in target.items()
1281
+ if axis in "BC" and self.position[axis] < value
1282
+ ]
1283
+ backlash_target = {ax: moving_target[ax] for ax in plunger_backlash_axes}
1284
+ moving_target.update(
1285
+ {
1286
+ ax: moving_target[ax] + PLUNGER_BACKLASH_MM
1287
+ for ax in plunger_backlash_axes
1288
+ }
1289
+ )
1290
+
1291
+ # whatever else we do to our motion target, if nothing moves in the
1292
+ # input we will not command it to move
1293
+ non_moving_axes = [ax for ax in AXES if ax not in moving_target.keys()]
1294
+
1295
+ # cache which axes move because we might take them out of moving target
1296
+ moving_axes = list(moving_target.keys())
1297
+
1298
+ def build_split(here: float, dest: float, split_distance: float) -> float:
1299
+ """Return the destination for the split move"""
1300
+ if dest < here:
1301
+ return max(dest, here - split_distance)
1302
+ else:
1303
+ return min(dest, here + split_distance)
1304
+
1305
+ since_moved = self._axes_moved_at.time_since_moved()
1306
+ # generate the split moves if necessary
1307
+ split_target = {
1308
+ ax: build_split(
1309
+ self.position[ax],
1310
+ moving_target[ax],
1311
+ split.split_distance,
1312
+ )
1313
+ for ax, split in self._move_split_config.items()
1314
+ # a split is only necessary if:
1315
+ # - the axis is moving
1316
+ if (ax in moving_target)
1317
+ # - we have a split configuration
1318
+ and split
1319
+ # - it's been long enough since the last time it moved
1320
+ and ((since_moved[ax] is None) or (split.after_time < since_moved[ax])) # type: ignore[operator]
1321
+ }
1322
+
1323
+ split_command_string = create_coords_list(split_target)
1324
+ primary_command_string = create_coords_list(moving_target)
1325
+ backlash_command_string = create_coords_list(backlash_target)
1326
+
1327
+ self.dwell_axes("".join(non_moving_axes))
1328
+ self.activate_axes("".join(moving_axes))
1329
+
1330
+ checked_speed = speed or self._combined_speed
1331
+
1332
+ if split_command_string:
1333
+ # set fullstepping if necessary
1334
+ split_prefix, split_postfix = self._build_fullstep_configurations(
1335
+ "".join(
1336
+ (
1337
+ ax
1338
+ for ax in split_target.keys()
1339
+ if self._move_split_config[ax].fullstep
1340
+ )
1341
+ )
1342
+ )
1343
+
1344
+ # move at the slowest required speed
1345
+ split_speed = min(
1346
+ split.split_speed
1347
+ for ax, split in self._move_split_config.items()
1348
+ if ax in split_target
1349
+ )
1350
+
1351
+ # use the higher current from the split config without changing
1352
+ # our global cache
1353
+ split_prefix.add_builder(builder=self._build_speed_command(split_speed))
1354
+ cached = {}
1355
+ for ax in split_target.keys():
1356
+ cached[ax] = self.current[ax]
1357
+ self.current[ax] = self._move_split_config[ax].split_current
1358
+ split_prefix.add_builder(builder=self._generate_current_command())
1359
+ for ax in split_target.keys():
1360
+ self.current[ax] = cached[ax]
1361
+
1362
+ split_command = (
1363
+ _command_builder()
1364
+ .add_gcode(gcode=GCODE.MOVE)
1365
+ .add_builder(builder=split_command_string)
1366
+ )
1367
+ else:
1368
+ split_prefix = _command_builder()
1369
+ split_command = _command_builder()
1370
+ split_postfix = _command_builder()
1371
+
1372
+ command = _command_builder()
1373
+
1374
+ if split_command_string or (checked_speed != self._combined_speed):
1375
+ command.add_builder(builder=self._build_speed_command(checked_speed))
1376
+
1377
+ # introduce the standard currents
1378
+ command.add_builder(builder=self._generate_current_command())
1379
+
1380
+ # move to target position, including any added backlash to B/C axes
1381
+ command.add_gcode(GCODE.MOVE).add_builder(builder=primary_command_string)
1382
+ if backlash_command_string:
1383
+ # correct the B/C positions
1384
+ command.add_gcode(gcode=GCODE.MOVE).add_builder(
1385
+ builder=backlash_command_string
1386
+ )
1387
+
1388
+ if checked_speed != self._combined_speed:
1389
+ command.add_builder(builder=self._build_speed_command(self._combined_speed))
1390
+
1391
+ for axis in target.keys():
1392
+ self.engaged_axes[axis] = True
1393
+ if home_flagged_axes:
1394
+ await self.home_flagged_axes("".join(list(target.keys())))
1395
+
1396
+ async def _do_split() -> None:
1397
+ try:
1398
+ for sc in (c for c in (split_prefix, split_command) if c):
1399
+ await self._send_command(sc)
1400
+ finally:
1401
+ if split_postfix:
1402
+ await self._send_command(split_postfix)
1403
+
1404
+ try:
1405
+ log.debug(f"move: {command}")
1406
+ # TODO (hmg) a movement's timeout should be calculated by
1407
+ # how long the movement is expected to take.
1408
+ await _do_split()
1409
+ await self._send_command(command, timeout=DEFAULT_EXECUTE_TIMEOUT)
1410
+ finally:
1411
+ # dwell pipette motors because they get hot
1412
+ plunger_axis_moved = "".join(set("BC") & set(target.keys()))
1413
+ if plunger_axis_moved:
1414
+ self.dwell_axes(plunger_axis_moved)
1415
+ await self._set_saved_current()
1416
+ self._axes_moved_at.mark_moved(moving_axes)
1417
+
1418
+ self._update_position(target)
1419
+
1420
+ async def home(
1421
+ self, axis: str = AXES, disabled: str = DISABLE_AXES
1422
+ ) -> Dict[str, float]:
1423
+
1424
+ await self.run_flag.wait()
1425
+
1426
+ axis = axis.upper()
1427
+
1428
+ # If Y is requested make sure we home X first
1429
+ if "Y" in axis:
1430
+ axis += "X"
1431
+ # If horizontal movement is requested, ensure we raise the instruments
1432
+ if "X" in axis:
1433
+ axis += "ZA"
1434
+ # These two additions are safe even if they duplicate requested axes
1435
+ # because of the use of set operations below, which will de-duplicate
1436
+ # characters from the resulting string
1437
+
1438
+ # HOME_SEQUENCE defines a pattern for homing, specifically that the
1439
+ # ZABC axes should be homed first so that horizontal movement doesn't
1440
+ # happen with the pipette down (which could bump into things). Then
1441
+ # the X axis is homed, which has to happen before Y. Finally Y can be
1442
+ # homed. This variable will contain the sequence just explained, but
1443
+ # filters out unrequested axes using set intersection (&) and then
1444
+ # filters out disabled axes using set difference (-)
1445
+ home_sequence = list(
1446
+ filter(
1447
+ None,
1448
+ [
1449
+ "".join(set(group) & set(axis) - set(disabled))
1450
+ for group in HOME_SEQUENCE
1451
+ ],
1452
+ )
1453
+ )
1454
+
1455
+ non_moving_axes = "".join(ax for ax in AXES if ax not in home_sequence)
1456
+ self.dwell_axes(non_moving_axes)
1457
+ log.info(f"Homing axes {axis} in sequence {home_sequence}")
1458
+ for axes in home_sequence:
1459
+ if "X" in axes:
1460
+ await self._home_x()
1461
+ elif "Y" in axes:
1462
+ await self._home_y()
1463
+ else:
1464
+ # if we are homing neither the X nor Y axes, simple home
1465
+ self.activate_axes(axes)
1466
+ await self._do_relative_splits_during_home_for(
1467
+ "".join(ax for ax in axes if ax in "BC")
1468
+ )
1469
+
1470
+ command = self._generate_current_command()
1471
+ command.add_gcode(gcode=GCODE.HOME).add_element(
1472
+ element="".join(sorted(axes))
1473
+ )
1474
+ try:
1475
+ # home commands are executed before ack, use a long ack
1476
+ # timeout and short execute timeout
1477
+ await self._send_command(
1478
+ command,
1479
+ ack_timeout=DEFAULT_EXECUTE_TIMEOUT,
1480
+ timeout=DEFAULT_ACK_TIMEOUT,
1481
+ )
1482
+ await self.update_homed_flags(flags={ax: True for ax in axes})
1483
+ finally:
1484
+ # always dwell an axis after it has been homed
1485
+ self.dwell_axes(axes)
1486
+ await self._set_saved_current()
1487
+
1488
+ # Only update axes that have been selected for homing
1489
+ homed_axes = "".join(home_sequence)
1490
+ axis_position = ((ax, self.homed_position.get(ax)) for ax in homed_axes)
1491
+ homed = {k: v for (k, v) in axis_position if v is not None}
1492
+ await self.update_position(default=homed)
1493
+
1494
+ for ax in homed_axes:
1495
+ self.engaged_axes[ax] = True
1496
+
1497
+ # coordinate after homing might not sync with default in API
1498
+ # so update this driver's homed position using current coordinates
1499
+ new = {ax: self.position[ax] for ax in homed_axes}
1500
+ self._homed_position.update(new)
1501
+ self._axes_moved_at.mark_moved(homed_axes)
1502
+ return self.position
1503
+
1504
+ def _build_fullstep_configurations(
1505
+ self, axes: str
1506
+ ) -> Tuple[CommandBuilder, CommandBuilder]:
1507
+ """For one or more specified pipette axes,
1508
+ build a prefix and postfix command string that will configure
1509
+ the step mode and steps/mm value to
1510
+ - in the prefix: set full stepping with an appropriate steps/mm
1511
+ - in the postfix: set 1/32 microstepping with the correct steps/mm
1512
+
1513
+ Prefix will always be empty or end with a space, and postfix will
1514
+ always be empty or start with a space, so they can be added to
1515
+ command strings easily
1516
+ """
1517
+ prefix = _command_builder()
1518
+ postfix = _command_builder()
1519
+ if not axes:
1520
+ return prefix, postfix
1521
+ assert all(
1522
+ (ax in "BC" for ax in axes)
1523
+ ), "only plunger axes have controllable microstepping"
1524
+ for ax in axes:
1525
+ prefix.add_gcode(gcode=MICROSTEPPING_GCODES[ax]["DISABLE"])
1526
+ for ax in axes:
1527
+ postfix.add_gcode(gcode=MICROSTEPPING_GCODES[ax]["ENABLE"])
1528
+
1529
+ prefix.add_builder(
1530
+ builder=self._build_steps_per_mm(
1531
+ {ax: self.steps_per_mm[ax] / 32 for ax in axes}
1532
+ )
1533
+ ).add_gcode(gcode=GCODE.DWELL).add_float(prefix="P", value=0.01, precision=None)
1534
+
1535
+ postfix.add_builder(
1536
+ builder=self._build_steps_per_mm({ax: self.steps_per_mm[ax] for ax in axes})
1537
+ ).add_gcode(gcode=GCODE.DWELL).add_float(prefix="P", value=0.01, precision=None)
1538
+ return prefix, postfix
1539
+
1540
+ async def _do_relative_splits_during_home_for(self, axes: str) -> None:
1541
+ """Handle split moves for unsticking axes before home.
1542
+
1543
+ This is particularly ugly bit of code that flips the motor controller
1544
+ into relative mode since we don't necessarily know where we are.
1545
+
1546
+ It will induce a movement. It should really only be called before a
1547
+ home because it doesn't update the position cache.
1548
+
1549
+ :param axes: A string that is a sequence of plunger axis names.
1550
+ """
1551
+ assert all(
1552
+ ax.lower() in "bc" for ax in axes
1553
+ ), "only plunger axes may be unstuck"
1554
+ since_moved = self._axes_moved_at.time_since_moved()
1555
+ split_currents = _command_builder().add_gcode(gcode=GCODE.SET_CURRENT)
1556
+ split_moves = _command_builder().add_gcode(gcode=GCODE.MOVE)
1557
+ applicable_speeds: List[float] = []
1558
+ log.debug(f"Finding splits for {axes} with since moved {since_moved}")
1559
+ to_unstick = [
1560
+ ax
1561
+ for ax in axes
1562
+ if (
1563
+ since_moved.get(ax) is None
1564
+ or (
1565
+ self._move_split_config.get(ax)
1566
+ and since_moved[ax] # type: ignore[operator]
1567
+ > self._move_split_config[ax].after_time
1568
+ )
1569
+ )
1570
+ ]
1571
+ for axis in axes:
1572
+ msc = self._move_split_config.get(axis)
1573
+ log.debug(f"axis {axis}: msc {msc}")
1574
+ if not msc:
1575
+ continue
1576
+ if axis in to_unstick:
1577
+ log.debug(f"adding unstick for {axis}")
1578
+ split_currents.add_float(
1579
+ prefix=axis, value=msc.split_current, precision=None
1580
+ )
1581
+ split_moves.add_float(
1582
+ prefix=axis, value=-msc.split_distance, precision=None
1583
+ )
1584
+ applicable_speeds.append(msc.split_speed)
1585
+ if not applicable_speeds:
1586
+ log.debug("no unstick needed")
1587
+ # nothing to do
1588
+ return
1589
+
1590
+ fullstep_prefix, fullstep_postfix = self._build_fullstep_configurations(
1591
+ "".join(to_unstick)
1592
+ )
1593
+
1594
+ command_sequence = [
1595
+ fullstep_prefix.add_builder(builder=split_currents)
1596
+ .add_gcode(gcode=GCODE.DWELL)
1597
+ .add_float(prefix="P", value=CURRENT_CHANGE_DELAY, precision=None)
1598
+ .add_builder(builder=self._build_speed_command(min(applicable_speeds)))
1599
+ .add_gcode(gcode=GCODE.RELATIVE_COORDS),
1600
+ split_moves,
1601
+ ]
1602
+ try:
1603
+ for command_string in command_sequence:
1604
+ await self._send_command(
1605
+ command_string,
1606
+ timeout=DEFAULT_EXECUTE_TIMEOUT,
1607
+ suppress_home_after_error=True,
1608
+ )
1609
+ except SmoothieError:
1610
+ pass
1611
+ finally:
1612
+ await self._send_command(
1613
+ _command_builder()
1614
+ .add_gcode(gcode=GCODE.ABSOLUTE_COORDS)
1615
+ .add_builder(builder=fullstep_postfix)
1616
+ .add_builder(builder=self._build_speed_command(self._combined_speed))
1617
+ )
1618
+
1619
+ async def fast_home(self, axis: str, safety_margin: float) -> Dict[str, float]:
1620
+ """home after a controlled motor stall
1621
+
1622
+ Given a known distance we have just stalled along an axis, move
1623
+ that distance away from the homing switch. Then finish with home.
1624
+ """
1625
+ # move some mm distance away from the target axes endstop switch(es)
1626
+ axis_values = ((ax, self.homed_position.get(ax)) for ax in axis.upper())
1627
+ destination = {
1628
+ ax: val - abs(safety_margin) for (ax, val) in axis_values if val is not None
1629
+ }
1630
+
1631
+ # there is a chance the axis will hit it's home switch too soon
1632
+ # if this happens, catch the error and continue with homing afterwards
1633
+ try:
1634
+ await self.move(destination)
1635
+ except SmoothieError:
1636
+ pass
1637
+
1638
+ # then home once we're closer to the endstop(s)
1639
+ disabled = "".join(ax for ax in AXES if ax not in axis.upper())
1640
+ return await self.home(axis=axis, disabled=disabled)
1641
+
1642
+ async def unstick_axes(
1643
+ self, axes: str, distance: Optional[float] = None, speed: Optional[float] = None
1644
+ ) -> None:
1645
+ """
1646
+ The plunger axes on OT2 can build up static friction over time and
1647
+ when it's cold. To get over this, the robot can move that plunger at
1648
+ normal current and a very slow speed to increase the torque, removing
1649
+ the static friction
1650
+
1651
+ axes:
1652
+ String containing each axis to be moved. Ex: 'BC' or 'ZABC'
1653
+
1654
+ distance:
1655
+ Distance to travel in millimeters (default is 1mm)
1656
+
1657
+ speed:
1658
+ Millimeters-per-second to travel to travel at (default is 1mm/sec)
1659
+ """
1660
+ for ax in axes:
1661
+ if ax not in AXES:
1662
+ raise ValueError(f"Unknown axes: {axes}")
1663
+
1664
+ if distance is None:
1665
+ distance = UNSTICK_DISTANCE
1666
+ if speed is None:
1667
+ speed = UNSTICK_SPEED
1668
+
1669
+ self.push_active_current()
1670
+ self.set_active_current(
1671
+ {
1672
+ ax: self._config.high_current["default"][ax] # type: ignore[literal-required]
1673
+ for ax in axes
1674
+ }
1675
+ )
1676
+ self.push_axis_max_speed()
1677
+ await self.set_axis_max_speed({ax: speed for ax in axes})
1678
+
1679
+ # only need to request switch state once
1680
+ state_of_switches = await self.switch_state()
1681
+
1682
+ # incase axes is pressing endstop, home it slowly instead of moving
1683
+ homing_axes = "".join(ax for ax in axes if state_of_switches[ax])
1684
+ moving_axes = {
1685
+ ax: self.position[ax] - distance # retract
1686
+ for ax in axes
1687
+ if (not state_of_switches[ax]) and (ax not in homing_axes)
1688
+ }
1689
+
1690
+ try:
1691
+ if moving_axes:
1692
+ await self.move(moving_axes)
1693
+ if homing_axes:
1694
+ await self.home(homing_axes)
1695
+ finally:
1696
+ self.pop_active_current()
1697
+ await self.pop_axis_max_speed()
1698
+
1699
+ def pause(self) -> None:
1700
+ if not self.simulating:
1701
+ self.run_flag.clear()
1702
+
1703
+ def resume(self) -> None:
1704
+ if not self.simulating:
1705
+ self.run_flag.set()
1706
+
1707
+ async def delay(self, seconds: float) -> None:
1708
+ # per http://smoothieware.org/supported-g-codes:
1709
+ # In grbl mode P is float seconds to comply with gcode standards
1710
+ command = (
1711
+ _command_builder()
1712
+ .add_gcode(gcode=GCODE.DWELL)
1713
+ .add_float(prefix="P", value=seconds, precision=None)
1714
+ )
1715
+
1716
+ log.debug(f"delay: {command}")
1717
+ await self._send_command(command, timeout=int(seconds) + 1)
1718
+
1719
+ async def probe_axis(self, axis: str, probing_distance: float) -> Dict[str, float]:
1720
+ if axis.upper() in AXES:
1721
+ self.engaged_axes[axis] = True
1722
+ command = (
1723
+ _command_builder()
1724
+ .add_gcode(gcode=GCODE.PROBE)
1725
+ .add_int(
1726
+ prefix="F", value=420 # 420 mm/min (7 mm/sec) to avoid resonance
1727
+ )
1728
+ .add_float(prefix=axis.upper(), value=probing_distance, precision=None)
1729
+ )
1730
+ log.debug(f"probe_axis: {command}")
1731
+ try:
1732
+ await self._send_command(
1733
+ command=command,
1734
+ ack_timeout=DEFAULT_MOVEMENT_TIMEOUT,
1735
+ suppress_home_after_error=True,
1736
+ )
1737
+ except SmoothieError as se:
1738
+ log.exception("Tip probe failure")
1739
+ await self.home(axis)
1740
+ if "probe" in str(se).lower():
1741
+ raise TipProbeError(se.ret_code, se.command)
1742
+ else:
1743
+ raise
1744
+ await self.update_position(self.position)
1745
+ return self.position
1746
+ else:
1747
+ raise RuntimeError(f"Cant probe axis {axis}")
1748
+
1749
+ def turn_on_blue_button_light(self) -> None:
1750
+ self._gpio_chardev.set_button_light(blue=True)
1751
+
1752
+ def turn_on_green_button_light(self) -> None:
1753
+ self._gpio_chardev.set_button_light(green=True)
1754
+
1755
+ def turn_on_red_button_light(self) -> None:
1756
+ self._gpio_chardev.set_button_light(red=True)
1757
+
1758
+ def turn_off_button_light(self) -> None:
1759
+ self._gpio_chardev.set_button_light(red=False, green=False, blue=False)
1760
+
1761
+ def turn_on_rail_lights(self) -> None:
1762
+ self._gpio_chardev.set_rail_lights(True)
1763
+
1764
+ def turn_off_rail_lights(self) -> None:
1765
+ self._gpio_chardev.set_rail_lights(False)
1766
+
1767
+ def get_rail_lights_on(self) -> bool:
1768
+ return self._gpio_chardev.get_rail_lights()
1769
+
1770
+ def read_button(self) -> bool:
1771
+ return self._gpio_chardev.read_button()
1772
+
1773
+ def read_window_switches(self) -> bool:
1774
+ return self._gpio_chardev.read_window_switches()
1775
+
1776
+ def set_lights(
1777
+ self, button: Optional[bool] = None, rails: Optional[bool] = None
1778
+ ) -> None:
1779
+ if button is not None:
1780
+ self._gpio_chardev.set_button_light(blue=button)
1781
+ if rails is not None:
1782
+ self._gpio_chardev.set_rail_lights(rails)
1783
+
1784
+ def get_lights(self) -> Dict[str, bool]:
1785
+ return {
1786
+ "button": self._gpio_chardev.get_button_light()[2],
1787
+ "rails": self._gpio_chardev.get_rail_lights(),
1788
+ }
1789
+
1790
+ async def kill(self) -> None:
1791
+ """
1792
+ In order to terminate Smoothie motion immediately (including
1793
+ interrupting a command in progress, we set the reset pin low and then
1794
+ back to high, then call `_setup` method to send the RESET_FROM_ERROR
1795
+ Smoothie code to return Smoothie to a normal waiting state and reset
1796
+ any other state needed for the driver.
1797
+ """
1798
+ log.debug("kill")
1799
+ await self.hard_halt()
1800
+ await self._reset_from_error()
1801
+ await self._setup()
1802
+
1803
+ async def home_flagged_axes(self, axes_string: str) -> None:
1804
+ """
1805
+ Given a list of axes to check, this method will home each axis if
1806
+ Smoothieware's internal flag sets it as needing to be homed
1807
+ """
1808
+ axes_that_need_to_home = [
1809
+ axis for axis in axes_string if not self.homed_flags.get(axis)
1810
+ ]
1811
+ if axes_that_need_to_home:
1812
+ axes_string = "".join(axes_that_need_to_home)
1813
+ await self.home(axes_string)
1814
+
1815
+ async def _smoothie_reset(self) -> None:
1816
+ log.debug(f"Resetting Smoothie (simulating: {self.simulating})")
1817
+ if self.simulating:
1818
+ pass
1819
+ else:
1820
+ self._gpio_chardev.set_reset_pin(False)
1821
+ self._gpio_chardev.set_isp_pin(True)
1822
+ await asyncio.sleep(0.25)
1823
+ self._gpio_chardev.set_reset_pin(True)
1824
+ await asyncio.sleep(0.25)
1825
+ await self._wait_for_ack()
1826
+ await self._reset_from_error()
1827
+
1828
+ async def _smoothie_programming_mode(self) -> None:
1829
+ log.debug(f"Setting Smoothie to ISP mode (simulating: {self.simulating})")
1830
+ if self.simulating:
1831
+ pass
1832
+ else:
1833
+ self._gpio_chardev.set_reset_pin(False)
1834
+ self._gpio_chardev.set_isp_pin(False)
1835
+ await asyncio.sleep(0.25)
1836
+ self._gpio_chardev.set_reset_pin(True)
1837
+ await asyncio.sleep(0.25)
1838
+ self._gpio_chardev.set_isp_pin(True)
1839
+ await asyncio.sleep(0.25)
1840
+
1841
+ async def hard_halt(self) -> None:
1842
+ log.debug(f"Halting Smoothie (simulating: {self.simulating}")
1843
+ self._is_hard_halting.set()
1844
+ if self.simulating:
1845
+ pass
1846
+ else:
1847
+ self._gpio_chardev.set_halt_pin(False)
1848
+ await asyncio.sleep(0.25)
1849
+ self._gpio_chardev.set_halt_pin(True)
1850
+ await asyncio.sleep(0.25)
1851
+ self.run_flag.set()
1852
+
1853
+ async def update_firmware(
1854
+ self,
1855
+ filename: str,
1856
+ loop: Optional[asyncio.AbstractEventLoop] = None,
1857
+ explicit_modeset: bool = True,
1858
+ ) -> str:
1859
+ """
1860
+ Program the smoothie board with a given hex file.
1861
+
1862
+ If explicit_modeset is True (default), explicitly place the smoothie in
1863
+ programming mode.
1864
+
1865
+ If explicit_modeset is False, assume the smoothie is already in
1866
+ programming mode.
1867
+ """
1868
+ try:
1869
+ smoothie_update._ensure_programmer_executable()
1870
+ except OSError as ose:
1871
+ if ose.errno == 30:
1872
+ # This is "read only filesystem" and happens on buildroot
1873
+ pass
1874
+ else:
1875
+ raise
1876
+
1877
+ if not await self.is_connected():
1878
+ log.info("Getting port to connect")
1879
+ await self._connect_to_port()
1880
+
1881
+ assert self._connection, "driver must have been initialized with a port"
1882
+ # get port name
1883
+ port = self._connection.port
1884
+
1885
+ if explicit_modeset:
1886
+ log.info("Setting programming mode")
1887
+ # set smoothieware into programming mode
1888
+ await self._smoothie_programming_mode()
1889
+ # close the port so other application can access it
1890
+ await self._connection.close()
1891
+
1892
+ # run lpc21isp, THIS WILL TAKE AROUND 1 MINUTE TO COMPLETE
1893
+ update_cmd = (
1894
+ f"lpc21isp -wipe -donotstart {filename} "
1895
+ f"{port} {self._config.serial_speed} 12000"
1896
+ )
1897
+ kwargs: Dict[str, Any] = {
1898
+ "stdout": asyncio.subprocess.PIPE,
1899
+ "stderr": asyncio.subprocess.PIPE,
1900
+ }
1901
+ # if loop:
1902
+ # kwargs["loop"] = loop
1903
+ log.info(update_cmd)
1904
+ before = monotonic()
1905
+ proc = await asyncio.create_subprocess_shell(update_cmd, **kwargs)
1906
+ created = monotonic()
1907
+ log.info(f"created lpc21isp subproc in {created-before}")
1908
+ out_b, err_b = await proc.communicate()
1909
+ done = monotonic()
1910
+ log.info(f"ran lpc21isp subproc in {done-created}")
1911
+ if proc.returncode != 0:
1912
+ log.error(
1913
+ f"Smoothie update failed: {proc.returncode}" f" {out_b!r} {err_b!r}"
1914
+ )
1915
+ raise RuntimeError(
1916
+ f"Failed to program smoothie: {proc.returncode}: {err_b!r}"
1917
+ )
1918
+ else:
1919
+ log.info("Smoothie update complete")
1920
+ try:
1921
+ await self._connection.close()
1922
+ except Exception:
1923
+ log.exception("Failed to close smoothie connection.")
1924
+ # re-open the port
1925
+ await self._connection.open()
1926
+ # reset smoothieware
1927
+ await self._smoothie_reset()
1928
+ # run setup gcodes
1929
+ await self._setup()
1930
+
1931
+ return out_b.decode().strip()
1932
+
1933
+ # ----------- END Public interface ------------ #