opentrons 8.6.0__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.
- opentrons/__init__.py +150 -0
- opentrons/_version.py +34 -0
- opentrons/calibration_storage/__init__.py +54 -0
- opentrons/calibration_storage/deck_configuration.py +62 -0
- opentrons/calibration_storage/encoder_decoder.py +31 -0
- opentrons/calibration_storage/file_operators.py +142 -0
- opentrons/calibration_storage/helpers.py +103 -0
- opentrons/calibration_storage/ot2/__init__.py +34 -0
- opentrons/calibration_storage/ot2/deck_attitude.py +85 -0
- opentrons/calibration_storage/ot2/mark_bad_calibration.py +27 -0
- opentrons/calibration_storage/ot2/models/__init__.py +0 -0
- opentrons/calibration_storage/ot2/models/v1.py +149 -0
- opentrons/calibration_storage/ot2/pipette_offset.py +129 -0
- opentrons/calibration_storage/ot2/tip_length.py +281 -0
- opentrons/calibration_storage/ot3/__init__.py +31 -0
- opentrons/calibration_storage/ot3/deck_attitude.py +83 -0
- opentrons/calibration_storage/ot3/gripper_offset.py +156 -0
- opentrons/calibration_storage/ot3/models/__init__.py +0 -0
- opentrons/calibration_storage/ot3/models/v1.py +122 -0
- opentrons/calibration_storage/ot3/module_offset.py +138 -0
- opentrons/calibration_storage/ot3/pipette_offset.py +95 -0
- opentrons/calibration_storage/types.py +45 -0
- opentrons/cli/__init__.py +21 -0
- opentrons/cli/__main__.py +5 -0
- opentrons/cli/analyze.py +557 -0
- opentrons/config/__init__.py +631 -0
- opentrons/config/advanced_settings.py +871 -0
- opentrons/config/defaults_ot2.py +214 -0
- opentrons/config/defaults_ot3.py +499 -0
- opentrons/config/feature_flags.py +86 -0
- opentrons/config/gripper_config.py +55 -0
- opentrons/config/reset.py +203 -0
- opentrons/config/robot_configs.py +187 -0
- opentrons/config/types.py +183 -0
- opentrons/drivers/__init__.py +0 -0
- opentrons/drivers/absorbance_reader/__init__.py +11 -0
- opentrons/drivers/absorbance_reader/abstract.py +72 -0
- opentrons/drivers/absorbance_reader/async_byonoy.py +352 -0
- opentrons/drivers/absorbance_reader/driver.py +81 -0
- opentrons/drivers/absorbance_reader/hid_protocol.py +161 -0
- opentrons/drivers/absorbance_reader/simulator.py +84 -0
- opentrons/drivers/asyncio/__init__.py +0 -0
- opentrons/drivers/asyncio/communication/__init__.py +22 -0
- opentrons/drivers/asyncio/communication/async_serial.py +187 -0
- opentrons/drivers/asyncio/communication/errors.py +88 -0
- opentrons/drivers/asyncio/communication/serial_connection.py +557 -0
- opentrons/drivers/command_builder.py +102 -0
- opentrons/drivers/flex_stacker/__init__.py +13 -0
- opentrons/drivers/flex_stacker/abstract.py +214 -0
- opentrons/drivers/flex_stacker/driver.py +768 -0
- opentrons/drivers/flex_stacker/errors.py +68 -0
- opentrons/drivers/flex_stacker/simulator.py +309 -0
- opentrons/drivers/flex_stacker/types.py +367 -0
- opentrons/drivers/flex_stacker/utils.py +19 -0
- opentrons/drivers/heater_shaker/__init__.py +5 -0
- opentrons/drivers/heater_shaker/abstract.py +76 -0
- opentrons/drivers/heater_shaker/driver.py +204 -0
- opentrons/drivers/heater_shaker/simulator.py +94 -0
- opentrons/drivers/mag_deck/__init__.py +6 -0
- opentrons/drivers/mag_deck/abstract.py +44 -0
- opentrons/drivers/mag_deck/driver.py +208 -0
- opentrons/drivers/mag_deck/simulator.py +63 -0
- opentrons/drivers/rpi_drivers/__init__.py +33 -0
- opentrons/drivers/rpi_drivers/dev_types.py +94 -0
- opentrons/drivers/rpi_drivers/gpio.py +282 -0
- opentrons/drivers/rpi_drivers/gpio_simulator.py +127 -0
- opentrons/drivers/rpi_drivers/interfaces.py +15 -0
- opentrons/drivers/rpi_drivers/types.py +364 -0
- opentrons/drivers/rpi_drivers/usb.py +102 -0
- opentrons/drivers/rpi_drivers/usb_simulator.py +22 -0
- opentrons/drivers/serial_communication.py +151 -0
- opentrons/drivers/smoothie_drivers/__init__.py +4 -0
- opentrons/drivers/smoothie_drivers/connection.py +51 -0
- opentrons/drivers/smoothie_drivers/constants.py +121 -0
- opentrons/drivers/smoothie_drivers/driver_3_0.py +1933 -0
- opentrons/drivers/smoothie_drivers/errors.py +49 -0
- opentrons/drivers/smoothie_drivers/parse_utils.py +143 -0
- opentrons/drivers/smoothie_drivers/simulator.py +99 -0
- opentrons/drivers/smoothie_drivers/types.py +16 -0
- opentrons/drivers/temp_deck/__init__.py +10 -0
- opentrons/drivers/temp_deck/abstract.py +54 -0
- opentrons/drivers/temp_deck/driver.py +197 -0
- opentrons/drivers/temp_deck/simulator.py +57 -0
- opentrons/drivers/thermocycler/__init__.py +12 -0
- opentrons/drivers/thermocycler/abstract.py +99 -0
- opentrons/drivers/thermocycler/driver.py +395 -0
- opentrons/drivers/thermocycler/simulator.py +126 -0
- opentrons/drivers/types.py +107 -0
- opentrons/drivers/utils.py +222 -0
- opentrons/execute.py +742 -0
- opentrons/hardware_control/__init__.py +65 -0
- opentrons/hardware_control/__main__.py +77 -0
- opentrons/hardware_control/adapters.py +98 -0
- opentrons/hardware_control/api.py +1347 -0
- opentrons/hardware_control/backends/__init__.py +7 -0
- opentrons/hardware_control/backends/controller.py +400 -0
- opentrons/hardware_control/backends/errors.py +9 -0
- opentrons/hardware_control/backends/estop_state.py +164 -0
- opentrons/hardware_control/backends/flex_protocol.py +497 -0
- opentrons/hardware_control/backends/ot3controller.py +1930 -0
- opentrons/hardware_control/backends/ot3simulator.py +900 -0
- opentrons/hardware_control/backends/ot3utils.py +664 -0
- opentrons/hardware_control/backends/simulator.py +442 -0
- opentrons/hardware_control/backends/status_bar_state.py +240 -0
- opentrons/hardware_control/backends/subsystem_manager.py +431 -0
- opentrons/hardware_control/backends/tip_presence_manager.py +173 -0
- opentrons/hardware_control/backends/types.py +14 -0
- opentrons/hardware_control/constants.py +6 -0
- opentrons/hardware_control/dev_types.py +125 -0
- opentrons/hardware_control/emulation/__init__.py +0 -0
- opentrons/hardware_control/emulation/abstract_emulator.py +21 -0
- opentrons/hardware_control/emulation/app.py +56 -0
- opentrons/hardware_control/emulation/connection_handler.py +38 -0
- opentrons/hardware_control/emulation/heater_shaker.py +150 -0
- opentrons/hardware_control/emulation/magdeck.py +60 -0
- opentrons/hardware_control/emulation/module_server/__init__.py +8 -0
- opentrons/hardware_control/emulation/module_server/client.py +78 -0
- opentrons/hardware_control/emulation/module_server/helpers.py +130 -0
- opentrons/hardware_control/emulation/module_server/models.py +31 -0
- opentrons/hardware_control/emulation/module_server/server.py +110 -0
- opentrons/hardware_control/emulation/parser.py +74 -0
- opentrons/hardware_control/emulation/proxy.py +241 -0
- opentrons/hardware_control/emulation/run_emulator.py +68 -0
- opentrons/hardware_control/emulation/scripts/__init__.py +0 -0
- opentrons/hardware_control/emulation/scripts/run_app.py +54 -0
- opentrons/hardware_control/emulation/scripts/run_module_emulator.py +72 -0
- opentrons/hardware_control/emulation/scripts/run_smoothie.py +37 -0
- opentrons/hardware_control/emulation/settings.py +119 -0
- opentrons/hardware_control/emulation/simulations.py +133 -0
- opentrons/hardware_control/emulation/smoothie.py +192 -0
- opentrons/hardware_control/emulation/tempdeck.py +69 -0
- opentrons/hardware_control/emulation/thermocycler.py +128 -0
- opentrons/hardware_control/emulation/types.py +10 -0
- opentrons/hardware_control/emulation/util.py +38 -0
- opentrons/hardware_control/errors.py +43 -0
- opentrons/hardware_control/execution_manager.py +164 -0
- opentrons/hardware_control/instruments/__init__.py +5 -0
- opentrons/hardware_control/instruments/instrument_abc.py +39 -0
- opentrons/hardware_control/instruments/ot2/__init__.py +0 -0
- opentrons/hardware_control/instruments/ot2/instrument_calibration.py +152 -0
- opentrons/hardware_control/instruments/ot2/pipette.py +777 -0
- opentrons/hardware_control/instruments/ot2/pipette_handler.py +995 -0
- opentrons/hardware_control/instruments/ot3/__init__.py +0 -0
- opentrons/hardware_control/instruments/ot3/gripper.py +420 -0
- opentrons/hardware_control/instruments/ot3/gripper_handler.py +173 -0
- opentrons/hardware_control/instruments/ot3/instrument_calibration.py +214 -0
- opentrons/hardware_control/instruments/ot3/pipette.py +858 -0
- opentrons/hardware_control/instruments/ot3/pipette_handler.py +1030 -0
- opentrons/hardware_control/module_control.py +332 -0
- opentrons/hardware_control/modules/__init__.py +69 -0
- opentrons/hardware_control/modules/absorbance_reader.py +373 -0
- opentrons/hardware_control/modules/errors.py +7 -0
- opentrons/hardware_control/modules/flex_stacker.py +948 -0
- opentrons/hardware_control/modules/heater_shaker.py +426 -0
- opentrons/hardware_control/modules/lid_temp_status.py +35 -0
- opentrons/hardware_control/modules/magdeck.py +233 -0
- opentrons/hardware_control/modules/mod_abc.py +245 -0
- opentrons/hardware_control/modules/module_calibration.py +93 -0
- opentrons/hardware_control/modules/plate_temp_status.py +61 -0
- opentrons/hardware_control/modules/tempdeck.py +299 -0
- opentrons/hardware_control/modules/thermocycler.py +731 -0
- opentrons/hardware_control/modules/types.py +417 -0
- opentrons/hardware_control/modules/update.py +255 -0
- opentrons/hardware_control/modules/utils.py +73 -0
- opentrons/hardware_control/motion_utilities.py +318 -0
- opentrons/hardware_control/nozzle_manager.py +422 -0
- opentrons/hardware_control/ot3_calibration.py +1171 -0
- opentrons/hardware_control/ot3api.py +3227 -0
- opentrons/hardware_control/pause_manager.py +31 -0
- opentrons/hardware_control/poller.py +112 -0
- opentrons/hardware_control/protocols/__init__.py +106 -0
- opentrons/hardware_control/protocols/asyncio_configurable.py +11 -0
- opentrons/hardware_control/protocols/calibratable.py +45 -0
- opentrons/hardware_control/protocols/chassis_accessory_manager.py +90 -0
- opentrons/hardware_control/protocols/configurable.py +48 -0
- opentrons/hardware_control/protocols/event_sourcer.py +18 -0
- opentrons/hardware_control/protocols/execution_controllable.py +33 -0
- opentrons/hardware_control/protocols/flex_calibratable.py +96 -0
- opentrons/hardware_control/protocols/flex_instrument_configurer.py +52 -0
- opentrons/hardware_control/protocols/gripper_controller.py +55 -0
- opentrons/hardware_control/protocols/hardware_manager.py +51 -0
- opentrons/hardware_control/protocols/identifiable.py +16 -0
- opentrons/hardware_control/protocols/instrument_configurer.py +206 -0
- opentrons/hardware_control/protocols/liquid_handler.py +266 -0
- opentrons/hardware_control/protocols/module_provider.py +16 -0
- opentrons/hardware_control/protocols/motion_controller.py +243 -0
- opentrons/hardware_control/protocols/position_estimator.py +45 -0
- opentrons/hardware_control/protocols/simulatable.py +10 -0
- opentrons/hardware_control/protocols/stoppable.py +9 -0
- opentrons/hardware_control/protocols/types.py +27 -0
- opentrons/hardware_control/robot_calibration.py +224 -0
- opentrons/hardware_control/scripts/README.md +28 -0
- opentrons/hardware_control/scripts/__init__.py +1 -0
- opentrons/hardware_control/scripts/gripper_control.py +208 -0
- opentrons/hardware_control/scripts/ot3gripper +7 -0
- opentrons/hardware_control/scripts/ot3repl +7 -0
- opentrons/hardware_control/scripts/repl.py +187 -0
- opentrons/hardware_control/scripts/tc_control.py +97 -0
- opentrons/hardware_control/scripts/update_module_fw.py +274 -0
- opentrons/hardware_control/simulator_setup.py +260 -0
- opentrons/hardware_control/thread_manager.py +431 -0
- opentrons/hardware_control/threaded_async_lock.py +97 -0
- opentrons/hardware_control/types.py +792 -0
- opentrons/hardware_control/util.py +234 -0
- opentrons/legacy_broker.py +53 -0
- opentrons/legacy_commands/__init__.py +1 -0
- opentrons/legacy_commands/commands.py +483 -0
- opentrons/legacy_commands/helpers.py +153 -0
- opentrons/legacy_commands/module_commands.py +276 -0
- opentrons/legacy_commands/protocol_commands.py +54 -0
- opentrons/legacy_commands/publisher.py +155 -0
- opentrons/legacy_commands/robot_commands.py +51 -0
- opentrons/legacy_commands/types.py +1186 -0
- opentrons/motion_planning/__init__.py +32 -0
- opentrons/motion_planning/adjacent_slots_getters.py +168 -0
- opentrons/motion_planning/deck_conflict.py +501 -0
- opentrons/motion_planning/errors.py +35 -0
- opentrons/motion_planning/types.py +42 -0
- opentrons/motion_planning/waypoints.py +218 -0
- opentrons/ordered_set.py +138 -0
- opentrons/protocol_api/__init__.py +105 -0
- opentrons/protocol_api/_liquid.py +157 -0
- opentrons/protocol_api/_liquid_properties.py +814 -0
- opentrons/protocol_api/_nozzle_layout.py +31 -0
- opentrons/protocol_api/_parameter_context.py +300 -0
- opentrons/protocol_api/_parameters.py +31 -0
- opentrons/protocol_api/_transfer_liquid_validation.py +108 -0
- opentrons/protocol_api/_types.py +43 -0
- opentrons/protocol_api/config.py +23 -0
- opentrons/protocol_api/core/__init__.py +23 -0
- opentrons/protocol_api/core/common.py +33 -0
- opentrons/protocol_api/core/core_map.py +74 -0
- opentrons/protocol_api/core/engine/__init__.py +22 -0
- opentrons/protocol_api/core/engine/_default_labware_versions.py +179 -0
- opentrons/protocol_api/core/engine/deck_conflict.py +400 -0
- opentrons/protocol_api/core/engine/exceptions.py +19 -0
- opentrons/protocol_api/core/engine/instrument.py +2391 -0
- opentrons/protocol_api/core/engine/labware.py +238 -0
- opentrons/protocol_api/core/engine/load_labware_params.py +73 -0
- opentrons/protocol_api/core/engine/module_core.py +1027 -0
- opentrons/protocol_api/core/engine/overlap_versions.py +20 -0
- opentrons/protocol_api/core/engine/pipette_movement_conflict.py +358 -0
- opentrons/protocol_api/core/engine/point_calculations.py +64 -0
- opentrons/protocol_api/core/engine/protocol.py +1153 -0
- opentrons/protocol_api/core/engine/robot.py +139 -0
- opentrons/protocol_api/core/engine/stringify.py +74 -0
- opentrons/protocol_api/core/engine/transfer_components_executor.py +1006 -0
- opentrons/protocol_api/core/engine/well.py +241 -0
- opentrons/protocol_api/core/instrument.py +459 -0
- opentrons/protocol_api/core/labware.py +151 -0
- opentrons/protocol_api/core/legacy/__init__.py +11 -0
- opentrons/protocol_api/core/legacy/_labware_geometry.py +37 -0
- opentrons/protocol_api/core/legacy/deck.py +369 -0
- opentrons/protocol_api/core/legacy/labware_offset_provider.py +108 -0
- opentrons/protocol_api/core/legacy/legacy_instrument_core.py +709 -0
- opentrons/protocol_api/core/legacy/legacy_labware_core.py +235 -0
- opentrons/protocol_api/core/legacy/legacy_module_core.py +592 -0
- opentrons/protocol_api/core/legacy/legacy_protocol_core.py +612 -0
- opentrons/protocol_api/core/legacy/legacy_well_core.py +162 -0
- opentrons/protocol_api/core/legacy/load_info.py +67 -0
- opentrons/protocol_api/core/legacy/module_geometry.py +547 -0
- opentrons/protocol_api/core/legacy/well_geometry.py +148 -0
- opentrons/protocol_api/core/legacy_simulator/__init__.py +16 -0
- opentrons/protocol_api/core/legacy_simulator/legacy_instrument_core.py +624 -0
- opentrons/protocol_api/core/legacy_simulator/legacy_protocol_core.py +85 -0
- opentrons/protocol_api/core/module.py +484 -0
- opentrons/protocol_api/core/protocol.py +311 -0
- opentrons/protocol_api/core/robot.py +51 -0
- opentrons/protocol_api/core/well.py +116 -0
- opentrons/protocol_api/core/well_grid.py +45 -0
- opentrons/protocol_api/create_protocol_context.py +177 -0
- opentrons/protocol_api/deck.py +223 -0
- opentrons/protocol_api/disposal_locations.py +244 -0
- opentrons/protocol_api/instrument_context.py +3272 -0
- opentrons/protocol_api/labware.py +1579 -0
- opentrons/protocol_api/module_contexts.py +1447 -0
- opentrons/protocol_api/module_validation_and_errors.py +61 -0
- opentrons/protocol_api/protocol_context.py +1688 -0
- opentrons/protocol_api/robot_context.py +303 -0
- opentrons/protocol_api/validation.py +761 -0
- opentrons/protocol_engine/__init__.py +155 -0
- opentrons/protocol_engine/actions/__init__.py +65 -0
- opentrons/protocol_engine/actions/action_dispatcher.py +30 -0
- opentrons/protocol_engine/actions/action_handler.py +13 -0
- opentrons/protocol_engine/actions/actions.py +302 -0
- opentrons/protocol_engine/actions/get_state_update.py +38 -0
- opentrons/protocol_engine/clients/__init__.py +5 -0
- opentrons/protocol_engine/clients/sync_client.py +174 -0
- opentrons/protocol_engine/clients/transports.py +197 -0
- opentrons/protocol_engine/commands/__init__.py +757 -0
- opentrons/protocol_engine/commands/absorbance_reader/__init__.py +61 -0
- opentrons/protocol_engine/commands/absorbance_reader/close_lid.py +154 -0
- opentrons/protocol_engine/commands/absorbance_reader/common.py +6 -0
- opentrons/protocol_engine/commands/absorbance_reader/initialize.py +151 -0
- opentrons/protocol_engine/commands/absorbance_reader/open_lid.py +154 -0
- opentrons/protocol_engine/commands/absorbance_reader/read.py +226 -0
- opentrons/protocol_engine/commands/air_gap_in_place.py +162 -0
- opentrons/protocol_engine/commands/aspirate.py +244 -0
- opentrons/protocol_engine/commands/aspirate_in_place.py +184 -0
- opentrons/protocol_engine/commands/aspirate_while_tracking.py +211 -0
- opentrons/protocol_engine/commands/blow_out.py +146 -0
- opentrons/protocol_engine/commands/blow_out_in_place.py +119 -0
- opentrons/protocol_engine/commands/calibration/__init__.py +60 -0
- opentrons/protocol_engine/commands/calibration/calibrate_gripper.py +166 -0
- opentrons/protocol_engine/commands/calibration/calibrate_module.py +117 -0
- opentrons/protocol_engine/commands/calibration/calibrate_pipette.py +96 -0
- opentrons/protocol_engine/commands/calibration/move_to_maintenance_position.py +156 -0
- opentrons/protocol_engine/commands/command.py +308 -0
- opentrons/protocol_engine/commands/command_unions.py +974 -0
- opentrons/protocol_engine/commands/comment.py +57 -0
- opentrons/protocol_engine/commands/configure_for_volume.py +108 -0
- opentrons/protocol_engine/commands/configure_nozzle_layout.py +115 -0
- opentrons/protocol_engine/commands/custom.py +67 -0
- opentrons/protocol_engine/commands/dispense.py +194 -0
- opentrons/protocol_engine/commands/dispense_in_place.py +179 -0
- opentrons/protocol_engine/commands/dispense_while_tracking.py +204 -0
- opentrons/protocol_engine/commands/drop_tip.py +232 -0
- opentrons/protocol_engine/commands/drop_tip_in_place.py +205 -0
- opentrons/protocol_engine/commands/flex_stacker/__init__.py +64 -0
- opentrons/protocol_engine/commands/flex_stacker/common.py +900 -0
- opentrons/protocol_engine/commands/flex_stacker/empty.py +293 -0
- opentrons/protocol_engine/commands/flex_stacker/fill.py +281 -0
- opentrons/protocol_engine/commands/flex_stacker/retrieve.py +339 -0
- opentrons/protocol_engine/commands/flex_stacker/set_stored_labware.py +328 -0
- opentrons/protocol_engine/commands/flex_stacker/store.py +339 -0
- opentrons/protocol_engine/commands/generate_command_schema.py +61 -0
- opentrons/protocol_engine/commands/get_next_tip.py +134 -0
- opentrons/protocol_engine/commands/get_tip_presence.py +87 -0
- opentrons/protocol_engine/commands/hash_command_params.py +38 -0
- opentrons/protocol_engine/commands/heater_shaker/__init__.py +102 -0
- opentrons/protocol_engine/commands/heater_shaker/close_labware_latch.py +83 -0
- opentrons/protocol_engine/commands/heater_shaker/deactivate_heater.py +82 -0
- opentrons/protocol_engine/commands/heater_shaker/deactivate_shaker.py +84 -0
- opentrons/protocol_engine/commands/heater_shaker/open_labware_latch.py +110 -0
- opentrons/protocol_engine/commands/heater_shaker/set_and_wait_for_shake_speed.py +125 -0
- opentrons/protocol_engine/commands/heater_shaker/set_target_temperature.py +90 -0
- opentrons/protocol_engine/commands/heater_shaker/wait_for_temperature.py +102 -0
- opentrons/protocol_engine/commands/home.py +100 -0
- opentrons/protocol_engine/commands/identify_module.py +86 -0
- opentrons/protocol_engine/commands/labware_handling_common.py +29 -0
- opentrons/protocol_engine/commands/liquid_probe.py +464 -0
- opentrons/protocol_engine/commands/load_labware.py +210 -0
- opentrons/protocol_engine/commands/load_lid.py +154 -0
- opentrons/protocol_engine/commands/load_lid_stack.py +272 -0
- opentrons/protocol_engine/commands/load_liquid.py +95 -0
- opentrons/protocol_engine/commands/load_liquid_class.py +144 -0
- opentrons/protocol_engine/commands/load_module.py +223 -0
- opentrons/protocol_engine/commands/load_pipette.py +167 -0
- opentrons/protocol_engine/commands/magnetic_module/__init__.py +32 -0
- opentrons/protocol_engine/commands/magnetic_module/disengage.py +97 -0
- opentrons/protocol_engine/commands/magnetic_module/engage.py +119 -0
- opentrons/protocol_engine/commands/move_labware.py +546 -0
- opentrons/protocol_engine/commands/move_relative.py +102 -0
- opentrons/protocol_engine/commands/move_to_addressable_area.py +176 -0
- opentrons/protocol_engine/commands/move_to_addressable_area_for_drop_tip.py +198 -0
- opentrons/protocol_engine/commands/move_to_coordinates.py +107 -0
- opentrons/protocol_engine/commands/move_to_well.py +119 -0
- opentrons/protocol_engine/commands/movement_common.py +338 -0
- opentrons/protocol_engine/commands/pick_up_tip.py +241 -0
- opentrons/protocol_engine/commands/pipetting_common.py +443 -0
- opentrons/protocol_engine/commands/prepare_to_aspirate.py +121 -0
- opentrons/protocol_engine/commands/pressure_dispense.py +155 -0
- opentrons/protocol_engine/commands/reload_labware.py +90 -0
- opentrons/protocol_engine/commands/retract_axis.py +75 -0
- opentrons/protocol_engine/commands/robot/__init__.py +70 -0
- opentrons/protocol_engine/commands/robot/close_gripper_jaw.py +96 -0
- opentrons/protocol_engine/commands/robot/common.py +18 -0
- opentrons/protocol_engine/commands/robot/move_axes_relative.py +101 -0
- opentrons/protocol_engine/commands/robot/move_axes_to.py +100 -0
- opentrons/protocol_engine/commands/robot/move_to.py +94 -0
- opentrons/protocol_engine/commands/robot/open_gripper_jaw.py +86 -0
- opentrons/protocol_engine/commands/save_position.py +109 -0
- opentrons/protocol_engine/commands/seal_pipette_to_tip.py +353 -0
- opentrons/protocol_engine/commands/set_rail_lights.py +67 -0
- opentrons/protocol_engine/commands/set_status_bar.py +89 -0
- opentrons/protocol_engine/commands/temperature_module/__init__.py +46 -0
- opentrons/protocol_engine/commands/temperature_module/deactivate.py +86 -0
- opentrons/protocol_engine/commands/temperature_module/set_target_temperature.py +97 -0
- opentrons/protocol_engine/commands/temperature_module/wait_for_temperature.py +104 -0
- opentrons/protocol_engine/commands/thermocycler/__init__.py +152 -0
- opentrons/protocol_engine/commands/thermocycler/close_lid.py +87 -0
- opentrons/protocol_engine/commands/thermocycler/deactivate_block.py +80 -0
- opentrons/protocol_engine/commands/thermocycler/deactivate_lid.py +80 -0
- opentrons/protocol_engine/commands/thermocycler/open_lid.py +87 -0
- opentrons/protocol_engine/commands/thermocycler/run_extended_profile.py +171 -0
- opentrons/protocol_engine/commands/thermocycler/run_profile.py +124 -0
- opentrons/protocol_engine/commands/thermocycler/set_target_block_temperature.py +140 -0
- opentrons/protocol_engine/commands/thermocycler/set_target_lid_temperature.py +100 -0
- opentrons/protocol_engine/commands/thermocycler/wait_for_block_temperature.py +93 -0
- opentrons/protocol_engine/commands/thermocycler/wait_for_lid_temperature.py +89 -0
- opentrons/protocol_engine/commands/touch_tip.py +189 -0
- opentrons/protocol_engine/commands/unsafe/__init__.py +161 -0
- opentrons/protocol_engine/commands/unsafe/unsafe_blow_out_in_place.py +100 -0
- opentrons/protocol_engine/commands/unsafe/unsafe_drop_tip_in_place.py +121 -0
- opentrons/protocol_engine/commands/unsafe/unsafe_engage_axes.py +82 -0
- opentrons/protocol_engine/commands/unsafe/unsafe_place_labware.py +208 -0
- opentrons/protocol_engine/commands/unsafe/unsafe_stacker_close_latch.py +94 -0
- opentrons/protocol_engine/commands/unsafe/unsafe_stacker_manual_retrieve.py +295 -0
- opentrons/protocol_engine/commands/unsafe/unsafe_stacker_open_latch.py +91 -0
- opentrons/protocol_engine/commands/unsafe/unsafe_stacker_prepare_shuttle.py +136 -0
- opentrons/protocol_engine/commands/unsafe/unsafe_ungrip_labware.py +77 -0
- opentrons/protocol_engine/commands/unsafe/update_position_estimators.py +90 -0
- opentrons/protocol_engine/commands/unseal_pipette_from_tip.py +153 -0
- opentrons/protocol_engine/commands/verify_tip_presence.py +100 -0
- opentrons/protocol_engine/commands/wait_for_duration.py +76 -0
- opentrons/protocol_engine/commands/wait_for_resume.py +75 -0
- opentrons/protocol_engine/create_protocol_engine.py +193 -0
- opentrons/protocol_engine/engine_support.py +28 -0
- opentrons/protocol_engine/error_recovery_policy.py +81 -0
- opentrons/protocol_engine/errors/__init__.py +191 -0
- opentrons/protocol_engine/errors/error_occurrence.py +182 -0
- opentrons/protocol_engine/errors/exceptions.py +1308 -0
- opentrons/protocol_engine/execution/__init__.py +50 -0
- opentrons/protocol_engine/execution/command_executor.py +216 -0
- opentrons/protocol_engine/execution/create_queue_worker.py +102 -0
- opentrons/protocol_engine/execution/door_watcher.py +119 -0
- opentrons/protocol_engine/execution/equipment.py +819 -0
- opentrons/protocol_engine/execution/error_recovery_hardware_state_synchronizer.py +101 -0
- opentrons/protocol_engine/execution/gantry_mover.py +686 -0
- opentrons/protocol_engine/execution/hardware_stopper.py +147 -0
- opentrons/protocol_engine/execution/heater_shaker_movement_flagger.py +207 -0
- opentrons/protocol_engine/execution/labware_movement.py +297 -0
- opentrons/protocol_engine/execution/movement.py +350 -0
- opentrons/protocol_engine/execution/pipetting.py +607 -0
- opentrons/protocol_engine/execution/queue_worker.py +86 -0
- opentrons/protocol_engine/execution/rail_lights.py +25 -0
- opentrons/protocol_engine/execution/run_control.py +33 -0
- opentrons/protocol_engine/execution/status_bar.py +34 -0
- opentrons/protocol_engine/execution/thermocycler_movement_flagger.py +188 -0
- opentrons/protocol_engine/execution/thermocycler_plate_lifter.py +81 -0
- opentrons/protocol_engine/execution/tip_handler.py +550 -0
- opentrons/protocol_engine/labware_offset_standardization.py +194 -0
- opentrons/protocol_engine/notes/__init__.py +17 -0
- opentrons/protocol_engine/notes/notes.py +59 -0
- opentrons/protocol_engine/plugins.py +104 -0
- opentrons/protocol_engine/protocol_engine.py +683 -0
- opentrons/protocol_engine/resources/__init__.py +26 -0
- opentrons/protocol_engine/resources/deck_configuration_provider.py +232 -0
- opentrons/protocol_engine/resources/deck_data_provider.py +94 -0
- opentrons/protocol_engine/resources/file_provider.py +161 -0
- opentrons/protocol_engine/resources/fixture_validation.py +68 -0
- opentrons/protocol_engine/resources/labware_data_provider.py +106 -0
- opentrons/protocol_engine/resources/labware_validation.py +73 -0
- opentrons/protocol_engine/resources/model_utils.py +32 -0
- opentrons/protocol_engine/resources/module_data_provider.py +44 -0
- opentrons/protocol_engine/resources/ot3_validation.py +21 -0
- opentrons/protocol_engine/resources/pipette_data_provider.py +379 -0
- opentrons/protocol_engine/slot_standardization.py +128 -0
- opentrons/protocol_engine/state/__init__.py +1 -0
- opentrons/protocol_engine/state/_abstract_store.py +27 -0
- opentrons/protocol_engine/state/_axis_aligned_bounding_box.py +50 -0
- opentrons/protocol_engine/state/_labware_origin_math.py +636 -0
- opentrons/protocol_engine/state/_move_types.py +83 -0
- opentrons/protocol_engine/state/_well_math.py +193 -0
- opentrons/protocol_engine/state/addressable_areas.py +699 -0
- opentrons/protocol_engine/state/command_history.py +309 -0
- opentrons/protocol_engine/state/commands.py +1164 -0
- opentrons/protocol_engine/state/config.py +39 -0
- opentrons/protocol_engine/state/files.py +57 -0
- opentrons/protocol_engine/state/fluid_stack.py +138 -0
- opentrons/protocol_engine/state/geometry.py +2408 -0
- opentrons/protocol_engine/state/inner_well_math_utils.py +548 -0
- opentrons/protocol_engine/state/labware.py +1432 -0
- opentrons/protocol_engine/state/liquid_classes.py +82 -0
- opentrons/protocol_engine/state/liquids.py +73 -0
- opentrons/protocol_engine/state/module_substates/__init__.py +45 -0
- opentrons/protocol_engine/state/module_substates/absorbance_reader_substate.py +35 -0
- opentrons/protocol_engine/state/module_substates/flex_stacker_substate.py +112 -0
- opentrons/protocol_engine/state/module_substates/heater_shaker_module_substate.py +115 -0
- opentrons/protocol_engine/state/module_substates/magnetic_block_substate.py +17 -0
- opentrons/protocol_engine/state/module_substates/magnetic_module_substate.py +65 -0
- opentrons/protocol_engine/state/module_substates/temperature_module_substate.py +67 -0
- opentrons/protocol_engine/state/module_substates/thermocycler_module_substate.py +163 -0
- opentrons/protocol_engine/state/modules.py +1515 -0
- opentrons/protocol_engine/state/motion.py +373 -0
- opentrons/protocol_engine/state/pipettes.py +905 -0
- opentrons/protocol_engine/state/state.py +421 -0
- opentrons/protocol_engine/state/state_summary.py +36 -0
- opentrons/protocol_engine/state/tips.py +420 -0
- opentrons/protocol_engine/state/update_types.py +904 -0
- opentrons/protocol_engine/state/wells.py +290 -0
- opentrons/protocol_engine/types/__init__.py +310 -0
- opentrons/protocol_engine/types/automatic_tip_selection.py +39 -0
- opentrons/protocol_engine/types/command_annotations.py +53 -0
- opentrons/protocol_engine/types/deck_configuration.py +81 -0
- opentrons/protocol_engine/types/execution.py +96 -0
- opentrons/protocol_engine/types/hardware_passthrough.py +25 -0
- opentrons/protocol_engine/types/instrument.py +47 -0
- opentrons/protocol_engine/types/instrument_sensors.py +47 -0
- opentrons/protocol_engine/types/labware.py +131 -0
- opentrons/protocol_engine/types/labware_movement.py +22 -0
- opentrons/protocol_engine/types/labware_offset_location.py +111 -0
- opentrons/protocol_engine/types/labware_offset_vector.py +16 -0
- opentrons/protocol_engine/types/liquid.py +40 -0
- opentrons/protocol_engine/types/liquid_class.py +59 -0
- opentrons/protocol_engine/types/liquid_handling.py +13 -0
- opentrons/protocol_engine/types/liquid_level_detection.py +191 -0
- opentrons/protocol_engine/types/location.py +194 -0
- opentrons/protocol_engine/types/module.py +310 -0
- opentrons/protocol_engine/types/partial_tip_configuration.py +76 -0
- opentrons/protocol_engine/types/run_time_parameters.py +133 -0
- opentrons/protocol_engine/types/tip.py +18 -0
- opentrons/protocol_engine/types/util.py +21 -0
- opentrons/protocol_engine/types/well_position.py +124 -0
- opentrons/protocol_reader/__init__.py +37 -0
- opentrons/protocol_reader/extract_labware_definitions.py +66 -0
- opentrons/protocol_reader/file_format_validator.py +152 -0
- opentrons/protocol_reader/file_hasher.py +27 -0
- opentrons/protocol_reader/file_identifier.py +284 -0
- opentrons/protocol_reader/file_reader_writer.py +90 -0
- opentrons/protocol_reader/input_file.py +16 -0
- opentrons/protocol_reader/protocol_files_invalid_error.py +6 -0
- opentrons/protocol_reader/protocol_reader.py +188 -0
- opentrons/protocol_reader/protocol_source.py +124 -0
- opentrons/protocol_reader/role_analyzer.py +86 -0
- opentrons/protocol_runner/__init__.py +26 -0
- opentrons/protocol_runner/create_simulating_orchestrator.py +118 -0
- opentrons/protocol_runner/json_file_reader.py +55 -0
- opentrons/protocol_runner/json_translator.py +314 -0
- opentrons/protocol_runner/legacy_command_mapper.py +852 -0
- opentrons/protocol_runner/legacy_context_plugin.py +116 -0
- opentrons/protocol_runner/protocol_runner.py +530 -0
- opentrons/protocol_runner/python_protocol_wrappers.py +179 -0
- opentrons/protocol_runner/run_orchestrator.py +496 -0
- opentrons/protocol_runner/task_queue.py +95 -0
- opentrons/protocols/__init__.py +6 -0
- opentrons/protocols/advanced_control/__init__.py +0 -0
- opentrons/protocols/advanced_control/common.py +38 -0
- opentrons/protocols/advanced_control/mix.py +60 -0
- opentrons/protocols/advanced_control/transfers/__init__.py +0 -0
- opentrons/protocols/advanced_control/transfers/common.py +180 -0
- opentrons/protocols/advanced_control/transfers/transfer.py +972 -0
- opentrons/protocols/advanced_control/transfers/transfer_liquid_utils.py +231 -0
- opentrons/protocols/api_support/__init__.py +0 -0
- opentrons/protocols/api_support/constants.py +8 -0
- opentrons/protocols/api_support/deck_type.py +110 -0
- opentrons/protocols/api_support/definitions.py +18 -0
- opentrons/protocols/api_support/instrument.py +151 -0
- opentrons/protocols/api_support/labware_like.py +233 -0
- opentrons/protocols/api_support/tip_tracker.py +175 -0
- opentrons/protocols/api_support/types.py +32 -0
- opentrons/protocols/api_support/util.py +403 -0
- opentrons/protocols/bundle.py +89 -0
- opentrons/protocols/duration/__init__.py +4 -0
- opentrons/protocols/duration/errors.py +5 -0
- opentrons/protocols/duration/estimator.py +628 -0
- opentrons/protocols/execution/__init__.py +0 -0
- opentrons/protocols/execution/dev_types.py +181 -0
- opentrons/protocols/execution/errors.py +40 -0
- opentrons/protocols/execution/execute.py +84 -0
- opentrons/protocols/execution/execute_json_v3.py +275 -0
- opentrons/protocols/execution/execute_json_v4.py +359 -0
- opentrons/protocols/execution/execute_json_v5.py +28 -0
- opentrons/protocols/execution/execute_python.py +169 -0
- opentrons/protocols/execution/json_dispatchers.py +87 -0
- opentrons/protocols/execution/types.py +7 -0
- opentrons/protocols/geometry/__init__.py +0 -0
- opentrons/protocols/geometry/planning.py +297 -0
- opentrons/protocols/labware.py +312 -0
- opentrons/protocols/models/__init__.py +0 -0
- opentrons/protocols/models/json_protocol.py +679 -0
- opentrons/protocols/parameters/__init__.py +0 -0
- opentrons/protocols/parameters/csv_parameter_definition.py +77 -0
- opentrons/protocols/parameters/csv_parameter_interface.py +96 -0
- opentrons/protocols/parameters/exceptions.py +34 -0
- opentrons/protocols/parameters/parameter_definition.py +272 -0
- opentrons/protocols/parameters/types.py +17 -0
- opentrons/protocols/parameters/validation.py +267 -0
- opentrons/protocols/parse.py +671 -0
- opentrons/protocols/types.py +159 -0
- opentrons/py.typed +0 -0
- opentrons/resources/scripts/lpc21isp +0 -0
- opentrons/resources/smoothie-edge-8414642.hex +23010 -0
- opentrons/simulate.py +1065 -0
- opentrons/system/__init__.py +6 -0
- opentrons/system/camera.py +51 -0
- opentrons/system/log_control.py +59 -0
- opentrons/system/nmcli.py +856 -0
- opentrons/system/resin.py +24 -0
- opentrons/system/smoothie_update.py +15 -0
- opentrons/system/wifi.py +204 -0
- opentrons/tools/__init__.py +0 -0
- opentrons/tools/args_handler.py +22 -0
- opentrons/tools/write_pipette_memory.py +157 -0
- opentrons/types.py +618 -0
- opentrons/util/__init__.py +1 -0
- opentrons/util/async_helpers.py +166 -0
- opentrons/util/broker.py +84 -0
- opentrons/util/change_notifier.py +47 -0
- opentrons/util/entrypoint_util.py +278 -0
- opentrons/util/get_union_elements.py +26 -0
- opentrons/util/helpers.py +6 -0
- opentrons/util/linal.py +178 -0
- opentrons/util/logging_config.py +265 -0
- opentrons/util/logging_queue_handler.py +61 -0
- opentrons/util/performance_helpers.py +157 -0
- opentrons-8.6.0.dist-info/METADATA +37 -0
- opentrons-8.6.0.dist-info/RECORD +601 -0
- opentrons-8.6.0.dist-info/WHEEL +4 -0
- opentrons-8.6.0.dist-info/entry_points.txt +3 -0
- opentrons-8.6.0.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 ------------ #
|