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