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