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,1164 @@
|
|
|
1
|
+
"""Protocol engine commands sub-state."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import enum
|
|
6
|
+
from dataclasses import dataclass
|
|
7
|
+
from datetime import datetime
|
|
8
|
+
from typing import Dict, List, Optional, Union, Tuple
|
|
9
|
+
from typing_extensions import assert_never
|
|
10
|
+
|
|
11
|
+
from opentrons_shared_data.errors import EnumeratedError, ErrorCodes, PythonException
|
|
12
|
+
|
|
13
|
+
from opentrons.ordered_set import OrderedSet
|
|
14
|
+
|
|
15
|
+
from opentrons.hardware_control.types import DoorState
|
|
16
|
+
from opentrons.protocol_engine.actions.actions import (
|
|
17
|
+
ResumeFromRecoveryAction,
|
|
18
|
+
RunCommandAction,
|
|
19
|
+
SetErrorRecoveryPolicyAction,
|
|
20
|
+
)
|
|
21
|
+
from opentrons.protocol_engine.commands.unsafe.unsafe_ungrip_labware import (
|
|
22
|
+
UnsafeUngripLabwareCommandType,
|
|
23
|
+
)
|
|
24
|
+
from opentrons.protocol_engine.commands.unsafe.unsafe_stacker_close_latch import (
|
|
25
|
+
UnsafeFlexStackerCloseLatchCommandType,
|
|
26
|
+
)
|
|
27
|
+
from opentrons.protocol_engine.commands.unsafe.unsafe_stacker_open_latch import (
|
|
28
|
+
UnsafeFlexStackerOpenLatchCommandType,
|
|
29
|
+
)
|
|
30
|
+
from opentrons.protocol_engine.error_recovery_policy import (
|
|
31
|
+
ErrorRecoveryPolicy,
|
|
32
|
+
ErrorRecoveryType,
|
|
33
|
+
)
|
|
34
|
+
from opentrons.protocol_engine.notes.notes import CommandNote
|
|
35
|
+
from opentrons.protocol_engine.state import update_types
|
|
36
|
+
|
|
37
|
+
from ..actions import (
|
|
38
|
+
Action,
|
|
39
|
+
QueueCommandAction,
|
|
40
|
+
SucceedCommandAction,
|
|
41
|
+
FailCommandAction,
|
|
42
|
+
PlayAction,
|
|
43
|
+
PauseAction,
|
|
44
|
+
StopAction,
|
|
45
|
+
FinishAction,
|
|
46
|
+
HardwareStoppedAction,
|
|
47
|
+
DoorChangeAction,
|
|
48
|
+
)
|
|
49
|
+
|
|
50
|
+
from ..commands import Command, CommandStatus, CommandIntent, CommandCreate
|
|
51
|
+
from ..errors import (
|
|
52
|
+
RunStoppedError,
|
|
53
|
+
ErrorOccurrence,
|
|
54
|
+
RobotDoorOpenError,
|
|
55
|
+
SetupCommandNotAllowedError,
|
|
56
|
+
FixitCommandNotAllowedError,
|
|
57
|
+
ResumeFromRecoveryNotAllowedError,
|
|
58
|
+
PauseNotAllowedError,
|
|
59
|
+
UnexpectedProtocolError,
|
|
60
|
+
ProtocolCommandFailedError,
|
|
61
|
+
)
|
|
62
|
+
from ..types import EngineStatus
|
|
63
|
+
from ._abstract_store import HasState, HandlesActions
|
|
64
|
+
from .command_history import (
|
|
65
|
+
CommandEntry,
|
|
66
|
+
CommandHistory,
|
|
67
|
+
)
|
|
68
|
+
from .config import Config
|
|
69
|
+
|
|
70
|
+
|
|
71
|
+
class QueueStatus(enum.Enum):
|
|
72
|
+
"""Execution status of the command queue."""
|
|
73
|
+
|
|
74
|
+
SETUP = enum.auto()
|
|
75
|
+
"""The engine has been created, but the run has not yet started.
|
|
76
|
+
|
|
77
|
+
New protocol commands may be enqueued, but will wait to execute.
|
|
78
|
+
New setup commands may be enqueued and will execute immediately.
|
|
79
|
+
New fixup commands may not be enqueued.
|
|
80
|
+
"""
|
|
81
|
+
|
|
82
|
+
RUNNING = enum.auto()
|
|
83
|
+
"""The queue is running through protocol commands.
|
|
84
|
+
|
|
85
|
+
New protocol commands may be enqueued and will execute immediately.
|
|
86
|
+
New setup commands may not be enqueued.
|
|
87
|
+
New fixup commands may not be enqueued.
|
|
88
|
+
"""
|
|
89
|
+
|
|
90
|
+
PAUSED = enum.auto()
|
|
91
|
+
"""Execution of protocol commands has been paused.
|
|
92
|
+
|
|
93
|
+
New protocol commands may be enqueued, but will wait to execute.
|
|
94
|
+
New setup commands may not be enqueued.
|
|
95
|
+
New fixup commands may not be enqueued.
|
|
96
|
+
"""
|
|
97
|
+
|
|
98
|
+
AWAITING_RECOVERY = enum.auto()
|
|
99
|
+
"""A protocol command has encountered a recoverable error.
|
|
100
|
+
|
|
101
|
+
New protocol commands may be enqueued, but will wait to execute.
|
|
102
|
+
New setup commands may not be enqueued.
|
|
103
|
+
New fixup commands may be enqueued and will execute immediately.
|
|
104
|
+
"""
|
|
105
|
+
|
|
106
|
+
AWAITING_RECOVERY_PAUSED = enum.auto()
|
|
107
|
+
"""Execution of fixit commands has been paused.
|
|
108
|
+
|
|
109
|
+
New protocol and fixit commands may be enqueued, but will usually wait to execute.
|
|
110
|
+
There are certain exceptions where fixit commands will still run.
|
|
111
|
+
|
|
112
|
+
New setup commands may not be enqueued.
|
|
113
|
+
"""
|
|
114
|
+
|
|
115
|
+
|
|
116
|
+
class RunResult(enum.Enum):
|
|
117
|
+
"""Result of the run."""
|
|
118
|
+
|
|
119
|
+
SUCCEEDED = enum.auto()
|
|
120
|
+
FAILED = enum.auto()
|
|
121
|
+
STOPPED = enum.auto()
|
|
122
|
+
|
|
123
|
+
|
|
124
|
+
@dataclass(frozen=True)
|
|
125
|
+
class CommandSlice:
|
|
126
|
+
"""A subset of all commands in state."""
|
|
127
|
+
|
|
128
|
+
commands: List[Command]
|
|
129
|
+
cursor: int
|
|
130
|
+
total_length: int
|
|
131
|
+
|
|
132
|
+
|
|
133
|
+
@dataclass(frozen=True)
|
|
134
|
+
class CommandErrorSlice:
|
|
135
|
+
"""A subset of all commands errors in state."""
|
|
136
|
+
|
|
137
|
+
commands_errors: List[ErrorOccurrence]
|
|
138
|
+
cursor: int
|
|
139
|
+
total_length: int
|
|
140
|
+
|
|
141
|
+
|
|
142
|
+
@dataclass(frozen=True)
|
|
143
|
+
class CommandPointer:
|
|
144
|
+
"""Brief info about a command and where to find it."""
|
|
145
|
+
|
|
146
|
+
command_id: str
|
|
147
|
+
command_key: str
|
|
148
|
+
created_at: datetime
|
|
149
|
+
index: int
|
|
150
|
+
|
|
151
|
+
|
|
152
|
+
@dataclass(frozen=True)
|
|
153
|
+
class _RecoveryTargetInfo:
|
|
154
|
+
"""Info about the failed command that we're currently recovering from."""
|
|
155
|
+
|
|
156
|
+
command_id: str
|
|
157
|
+
|
|
158
|
+
state_update_if_false_positive: update_types.StateUpdate
|
|
159
|
+
"""See `CommandView.get_state_update_if_continued()`."""
|
|
160
|
+
|
|
161
|
+
|
|
162
|
+
@dataclass
|
|
163
|
+
class CommandState:
|
|
164
|
+
"""State of all protocol engine command resources."""
|
|
165
|
+
|
|
166
|
+
command_history: CommandHistory
|
|
167
|
+
|
|
168
|
+
queue_status: QueueStatus
|
|
169
|
+
"""Whether the engine is currently pulling new commands off the queue to execute.
|
|
170
|
+
|
|
171
|
+
A command may still be executing, and the robot may still be in motion,
|
|
172
|
+
even if PAUSED.
|
|
173
|
+
"""
|
|
174
|
+
|
|
175
|
+
run_started_at: Optional[datetime]
|
|
176
|
+
"""The time the run was started.
|
|
177
|
+
|
|
178
|
+
Set when the first `PlayAction` is dispatched.
|
|
179
|
+
"""
|
|
180
|
+
|
|
181
|
+
run_completed_at: Optional[datetime]
|
|
182
|
+
"""The time the run has completed.
|
|
183
|
+
|
|
184
|
+
Set when 'HardwareStoppedAction' is dispatched.
|
|
185
|
+
"""
|
|
186
|
+
|
|
187
|
+
is_door_blocking: bool
|
|
188
|
+
"""Whether the door is open when enable_door_safety_switch feature flag is ON."""
|
|
189
|
+
|
|
190
|
+
run_result: Optional[RunResult]
|
|
191
|
+
"""Whether the run is done and succeeded, failed, or stopped.
|
|
192
|
+
|
|
193
|
+
This doesn't include the post-run finish steps (homing and dropping tips).
|
|
194
|
+
|
|
195
|
+
Once set, this status is immutable.
|
|
196
|
+
"""
|
|
197
|
+
|
|
198
|
+
run_error: Optional[ErrorOccurrence]
|
|
199
|
+
"""The run's fatal error occurrence, if there was one.
|
|
200
|
+
|
|
201
|
+
Individual command errors, which may or may not be fatal,
|
|
202
|
+
are stored on the individual commands themselves.
|
|
203
|
+
"""
|
|
204
|
+
|
|
205
|
+
failed_command: Optional[CommandEntry]
|
|
206
|
+
"""The most recent command failure, if any."""
|
|
207
|
+
# TODO(mm, 2024-03-19): This attribute is currently only used to help robot-server
|
|
208
|
+
# with pagination, but "the failed command" is an increasingly nuanced idea, now
|
|
209
|
+
# that we're doing error recovery. See if we can implement robot-server pagination
|
|
210
|
+
# atop simpler concepts, like "the last command that ran" or "the next command that
|
|
211
|
+
# would run."
|
|
212
|
+
#
|
|
213
|
+
# TODO(mm, 2024-04-03): Can this be replaced by
|
|
214
|
+
# CommandHistory.get_terminal_command() now?
|
|
215
|
+
|
|
216
|
+
command_error_recovery_types: Dict[str, ErrorRecoveryType]
|
|
217
|
+
"""For each command that failed (indexed by ID), what its recovery type was.
|
|
218
|
+
|
|
219
|
+
This only includes commands that actually failed, not the ones that we mark as
|
|
220
|
+
failed but that are effectively "cancelled" because a command before them failed.
|
|
221
|
+
|
|
222
|
+
This separate attribute is a stopgap until error recovery concepts are a bit more
|
|
223
|
+
stable. Eventually, we might want this info to be stored directly on each command.
|
|
224
|
+
"""
|
|
225
|
+
|
|
226
|
+
recovery_target: Optional[_RecoveryTargetInfo]
|
|
227
|
+
"""If we're currently recovering from a command failure, info about that command."""
|
|
228
|
+
|
|
229
|
+
finish_error: Optional[ErrorOccurrence]
|
|
230
|
+
"""The error that happened during the post-run finish steps (homing & dropping tips), if any."""
|
|
231
|
+
|
|
232
|
+
latest_protocol_command_hash: Optional[str]
|
|
233
|
+
"""The latest PROTOCOL command hash value received in a QueueCommandAction.
|
|
234
|
+
|
|
235
|
+
This value can be used to generate future hashes.
|
|
236
|
+
"""
|
|
237
|
+
|
|
238
|
+
has_entered_error_recovery: bool
|
|
239
|
+
"""Whether the run has entered error recovery."""
|
|
240
|
+
|
|
241
|
+
stopped_by_estop: bool
|
|
242
|
+
"""If this is set to True, the engine was stopped by an estop event."""
|
|
243
|
+
|
|
244
|
+
error_recovery_policy: ErrorRecoveryPolicy
|
|
245
|
+
"""See `CommandView.get_error_recovery_policy()`."""
|
|
246
|
+
|
|
247
|
+
|
|
248
|
+
class CommandStore(HasState[CommandState], HandlesActions):
|
|
249
|
+
"""Command state container for run-level command concerns."""
|
|
250
|
+
|
|
251
|
+
_state: CommandState
|
|
252
|
+
|
|
253
|
+
def __init__(
|
|
254
|
+
self,
|
|
255
|
+
*,
|
|
256
|
+
config: Config,
|
|
257
|
+
is_door_open: bool,
|
|
258
|
+
error_recovery_policy: ErrorRecoveryPolicy,
|
|
259
|
+
) -> None:
|
|
260
|
+
"""Initialize a CommandStore and its state."""
|
|
261
|
+
self._config = config
|
|
262
|
+
self._state = CommandState(
|
|
263
|
+
command_history=CommandHistory(),
|
|
264
|
+
queue_status=QueueStatus.SETUP,
|
|
265
|
+
is_door_blocking=is_door_open and config.block_on_door_open,
|
|
266
|
+
run_result=None,
|
|
267
|
+
run_error=None,
|
|
268
|
+
finish_error=None,
|
|
269
|
+
failed_command=None,
|
|
270
|
+
command_error_recovery_types={},
|
|
271
|
+
recovery_target=None,
|
|
272
|
+
run_completed_at=None,
|
|
273
|
+
run_started_at=None,
|
|
274
|
+
latest_protocol_command_hash=None,
|
|
275
|
+
stopped_by_estop=False,
|
|
276
|
+
error_recovery_policy=error_recovery_policy,
|
|
277
|
+
has_entered_error_recovery=False,
|
|
278
|
+
)
|
|
279
|
+
|
|
280
|
+
def handle_action(self, action: Action) -> None:
|
|
281
|
+
"""Modify state in reaction to an action."""
|
|
282
|
+
match action:
|
|
283
|
+
case QueueCommandAction():
|
|
284
|
+
self._handle_queue_command_action(action)
|
|
285
|
+
case RunCommandAction():
|
|
286
|
+
self._handle_run_command_action(action)
|
|
287
|
+
case SucceedCommandAction():
|
|
288
|
+
self._handle_succeed_command_action(action)
|
|
289
|
+
case FailCommandAction():
|
|
290
|
+
self._handle_fail_command_action(action)
|
|
291
|
+
case PlayAction():
|
|
292
|
+
self._handle_play_action(action)
|
|
293
|
+
case PauseAction():
|
|
294
|
+
self._handle_pause_action(action)
|
|
295
|
+
case ResumeFromRecoveryAction():
|
|
296
|
+
self._handle_resume_from_recovery_action(action)
|
|
297
|
+
case StopAction():
|
|
298
|
+
self._handle_stop_action(action)
|
|
299
|
+
case FinishAction():
|
|
300
|
+
self._handle_finish_action(action)
|
|
301
|
+
case HardwareStoppedAction():
|
|
302
|
+
self._handle_hardware_stopped_action(action)
|
|
303
|
+
case DoorChangeAction():
|
|
304
|
+
self._handle_door_change_action(action)
|
|
305
|
+
case SetErrorRecoveryPolicyAction():
|
|
306
|
+
self._handle_set_error_recovery_policy_action(action)
|
|
307
|
+
case _:
|
|
308
|
+
pass
|
|
309
|
+
|
|
310
|
+
def clear_history(self) -> None:
|
|
311
|
+
"""Clears CommandHistory state."""
|
|
312
|
+
self._state.command_history.clear()
|
|
313
|
+
|
|
314
|
+
def _handle_queue_command_action(self, action: QueueCommandAction) -> None:
|
|
315
|
+
# TODO(mc, 2021-06-22): mypy has trouble with this automatic
|
|
316
|
+
# request > command mapping, figure out how to type precisely
|
|
317
|
+
# (or wait for a future mypy version that can figure it out).
|
|
318
|
+
queued_command = action.request._CommandCls.model_construct(
|
|
319
|
+
id=action.command_id,
|
|
320
|
+
key=(
|
|
321
|
+
action.request.key
|
|
322
|
+
if action.request.key is not None
|
|
323
|
+
else (action.request_hash or action.command_id)
|
|
324
|
+
),
|
|
325
|
+
createdAt=action.created_at,
|
|
326
|
+
params=action.request.params, # type: ignore[arg-type]
|
|
327
|
+
intent=action.request.intent,
|
|
328
|
+
status=CommandStatus.QUEUED,
|
|
329
|
+
failedCommandId=action.failed_command_id,
|
|
330
|
+
)
|
|
331
|
+
|
|
332
|
+
self._state.command_history.append_queued_command(queued_command)
|
|
333
|
+
|
|
334
|
+
if action.request_hash is not None:
|
|
335
|
+
self._state.latest_protocol_command_hash = action.request_hash
|
|
336
|
+
|
|
337
|
+
def _handle_run_command_action(self, action: RunCommandAction) -> None:
|
|
338
|
+
prev_entry = self._state.command_history.get(action.command_id)
|
|
339
|
+
|
|
340
|
+
running_command = prev_entry.command.model_copy(
|
|
341
|
+
update={
|
|
342
|
+
"status": CommandStatus.RUNNING,
|
|
343
|
+
"startedAt": action.started_at,
|
|
344
|
+
}
|
|
345
|
+
)
|
|
346
|
+
|
|
347
|
+
self._state.command_history.set_command_running(running_command)
|
|
348
|
+
|
|
349
|
+
def _handle_succeed_command_action(self, action: SucceedCommandAction) -> None:
|
|
350
|
+
succeeded_command = action.command
|
|
351
|
+
self._state.command_history.set_command_succeeded(succeeded_command)
|
|
352
|
+
|
|
353
|
+
def _handle_fail_command_action( # noqa: C901
|
|
354
|
+
self, action: FailCommandAction
|
|
355
|
+
) -> None:
|
|
356
|
+
prev_entry = self.state.command_history.get(action.command_id)
|
|
357
|
+
|
|
358
|
+
if isinstance(action.error, EnumeratedError): # The error was undefined.
|
|
359
|
+
public_error_occurrence = ErrorOccurrence.from_failed(
|
|
360
|
+
id=action.error_id,
|
|
361
|
+
createdAt=action.failed_at,
|
|
362
|
+
error=action.error,
|
|
363
|
+
)
|
|
364
|
+
# An empty state update, to no-op.
|
|
365
|
+
state_update_if_false_positive = update_types.StateUpdate()
|
|
366
|
+
else: # The error was defined.
|
|
367
|
+
public_error_occurrence = action.error.public
|
|
368
|
+
state_update_if_false_positive = action.error.state_update_if_false_positive
|
|
369
|
+
|
|
370
|
+
self._update_to_failed(
|
|
371
|
+
command_id=action.command_id,
|
|
372
|
+
failed_at=action.failed_at,
|
|
373
|
+
error_occurrence=public_error_occurrence,
|
|
374
|
+
error_recovery_type=action.type,
|
|
375
|
+
notes=action.notes,
|
|
376
|
+
)
|
|
377
|
+
self._state.failed_command = self._state.command_history.get(action.command_id)
|
|
378
|
+
|
|
379
|
+
if (
|
|
380
|
+
prev_entry.command.intent in (CommandIntent.PROTOCOL, None)
|
|
381
|
+
and action.type == ErrorRecoveryType.WAIT_FOR_RECOVERY
|
|
382
|
+
):
|
|
383
|
+
if (
|
|
384
|
+
self._state.queue_status == QueueStatus.PAUSED
|
|
385
|
+
or self._state.is_door_blocking
|
|
386
|
+
):
|
|
387
|
+
self._state.queue_status = QueueStatus.AWAITING_RECOVERY_PAUSED
|
|
388
|
+
else:
|
|
389
|
+
self._state.queue_status = QueueStatus.AWAITING_RECOVERY
|
|
390
|
+
self._state.recovery_target = _RecoveryTargetInfo(
|
|
391
|
+
command_id=action.command_id,
|
|
392
|
+
state_update_if_false_positive=state_update_if_false_positive,
|
|
393
|
+
)
|
|
394
|
+
self._state.has_entered_error_recovery = True
|
|
395
|
+
|
|
396
|
+
# When one command fails, we generally also cancel the commands that
|
|
397
|
+
# would have been queued after it.
|
|
398
|
+
other_command_ids_to_fail: List[str]
|
|
399
|
+
if prev_entry.command.intent == CommandIntent.SETUP:
|
|
400
|
+
other_command_ids_to_fail = list(
|
|
401
|
+
self._state.command_history.get_setup_queue_ids()
|
|
402
|
+
)
|
|
403
|
+
elif prev_entry.command.intent == CommandIntent.FIXIT:
|
|
404
|
+
if (
|
|
405
|
+
action.type == ErrorRecoveryType.CONTINUE_WITH_ERROR
|
|
406
|
+
or action.type == ErrorRecoveryType.ASSUME_FALSE_POSITIVE_AND_CONTINUE
|
|
407
|
+
):
|
|
408
|
+
other_command_ids_to_fail = []
|
|
409
|
+
else:
|
|
410
|
+
other_command_ids_to_fail = list(
|
|
411
|
+
self._state.command_history.get_fixit_queue_ids()
|
|
412
|
+
)
|
|
413
|
+
elif (
|
|
414
|
+
prev_entry.command.intent == CommandIntent.PROTOCOL
|
|
415
|
+
or prev_entry.command.intent is None
|
|
416
|
+
):
|
|
417
|
+
if action.type == ErrorRecoveryType.FAIL_RUN:
|
|
418
|
+
other_command_ids_to_fail = list(
|
|
419
|
+
self._state.command_history.get_queue_ids()
|
|
420
|
+
)
|
|
421
|
+
elif (
|
|
422
|
+
action.type == ErrorRecoveryType.WAIT_FOR_RECOVERY
|
|
423
|
+
or action.type == ErrorRecoveryType.CONTINUE_WITH_ERROR
|
|
424
|
+
or action.type == ErrorRecoveryType.ASSUME_FALSE_POSITIVE_AND_CONTINUE
|
|
425
|
+
):
|
|
426
|
+
other_command_ids_to_fail = []
|
|
427
|
+
else:
|
|
428
|
+
assert_never(action.type)
|
|
429
|
+
else:
|
|
430
|
+
assert_never(prev_entry.command.intent)
|
|
431
|
+
for command_id in other_command_ids_to_fail:
|
|
432
|
+
# TODO(mc, 2022-06-06): add new "cancelled" status or similar
|
|
433
|
+
self._update_to_failed(
|
|
434
|
+
command_id=command_id,
|
|
435
|
+
failed_at=action.failed_at,
|
|
436
|
+
error_occurrence=None,
|
|
437
|
+
error_recovery_type=None,
|
|
438
|
+
notes=None,
|
|
439
|
+
)
|
|
440
|
+
|
|
441
|
+
def _handle_play_action(self, action: PlayAction) -> None:
|
|
442
|
+
if not self._state.run_result:
|
|
443
|
+
self._state.run_started_at = (
|
|
444
|
+
self._state.run_started_at or action.requested_at
|
|
445
|
+
)
|
|
446
|
+
match self._state.queue_status:
|
|
447
|
+
case QueueStatus.SETUP:
|
|
448
|
+
self._state.queue_status = (
|
|
449
|
+
QueueStatus.PAUSED
|
|
450
|
+
if self._state.is_door_blocking
|
|
451
|
+
else QueueStatus.RUNNING
|
|
452
|
+
)
|
|
453
|
+
case QueueStatus.AWAITING_RECOVERY_PAUSED:
|
|
454
|
+
self._state.queue_status = QueueStatus.AWAITING_RECOVERY
|
|
455
|
+
case QueueStatus.PAUSED:
|
|
456
|
+
self._state.queue_status = QueueStatus.RUNNING
|
|
457
|
+
case QueueStatus.RUNNING | QueueStatus.AWAITING_RECOVERY:
|
|
458
|
+
# Nothing for the play action to do. No-op.
|
|
459
|
+
pass
|
|
460
|
+
|
|
461
|
+
def _handle_pause_action(self, action: PauseAction) -> None:
|
|
462
|
+
self._state.queue_status = QueueStatus.PAUSED
|
|
463
|
+
|
|
464
|
+
def _handle_resume_from_recovery_action(
|
|
465
|
+
self, action: ResumeFromRecoveryAction
|
|
466
|
+
) -> None:
|
|
467
|
+
self._state.queue_status = QueueStatus.RUNNING
|
|
468
|
+
self._state.recovery_target = None
|
|
469
|
+
|
|
470
|
+
def _handle_stop_action(self, action: StopAction) -> None:
|
|
471
|
+
if not self._state.run_result:
|
|
472
|
+
self._state.recovery_target = None
|
|
473
|
+
self._state.queue_status = QueueStatus.PAUSED
|
|
474
|
+
|
|
475
|
+
if action.from_estop:
|
|
476
|
+
self._state.stopped_by_estop = True
|
|
477
|
+
self._state.run_result = RunResult.FAILED
|
|
478
|
+
else:
|
|
479
|
+
self._state.run_result = RunResult.STOPPED
|
|
480
|
+
|
|
481
|
+
def _handle_finish_action(self, action: FinishAction) -> None:
|
|
482
|
+
if not self._state.run_result:
|
|
483
|
+
self._state.recovery_target = None
|
|
484
|
+
self._state.queue_status = QueueStatus.PAUSED
|
|
485
|
+
|
|
486
|
+
if action.set_run_status:
|
|
487
|
+
self._state.run_result = (
|
|
488
|
+
RunResult.SUCCEEDED
|
|
489
|
+
if not action.error_details
|
|
490
|
+
else RunResult.FAILED
|
|
491
|
+
)
|
|
492
|
+
else:
|
|
493
|
+
self._state.run_result = RunResult.STOPPED
|
|
494
|
+
|
|
495
|
+
if not self._state.run_error and action.error_details:
|
|
496
|
+
self._state.run_error = self._map_run_exception_to_error_occurrence(
|
|
497
|
+
action.error_details.error_id,
|
|
498
|
+
action.error_details.created_at,
|
|
499
|
+
action.error_details.error,
|
|
500
|
+
)
|
|
501
|
+
else:
|
|
502
|
+
# HACK(sf): There needs to be a better way to set
|
|
503
|
+
# an estop error than this else clause
|
|
504
|
+
if self._state.stopped_by_estop and action.error_details:
|
|
505
|
+
self._state.run_error = self._map_run_exception_to_error_occurrence(
|
|
506
|
+
action.error_details.error_id,
|
|
507
|
+
action.error_details.created_at,
|
|
508
|
+
action.error_details.error,
|
|
509
|
+
)
|
|
510
|
+
|
|
511
|
+
def _handle_hardware_stopped_action(self, action: HardwareStoppedAction) -> None:
|
|
512
|
+
self._state.queue_status = QueueStatus.PAUSED
|
|
513
|
+
self._state.run_result = self._state.run_result or RunResult.STOPPED
|
|
514
|
+
self._state.run_completed_at = (
|
|
515
|
+
self._state.run_completed_at or action.completed_at
|
|
516
|
+
)
|
|
517
|
+
|
|
518
|
+
if action.finish_error_details:
|
|
519
|
+
self._state.finish_error = self._map_finish_exception_to_error_occurrence(
|
|
520
|
+
action.finish_error_details.error_id,
|
|
521
|
+
action.finish_error_details.created_at,
|
|
522
|
+
action.finish_error_details.error,
|
|
523
|
+
)
|
|
524
|
+
|
|
525
|
+
def _handle_door_change_action(self, action: DoorChangeAction) -> None:
|
|
526
|
+
if self._config.block_on_door_open:
|
|
527
|
+
if action.door_state == DoorState.OPEN:
|
|
528
|
+
self._state.is_door_blocking = True
|
|
529
|
+
match self._state.queue_status:
|
|
530
|
+
case QueueStatus.SETUP:
|
|
531
|
+
pass
|
|
532
|
+
case QueueStatus.RUNNING | QueueStatus.PAUSED:
|
|
533
|
+
self._state.queue_status = QueueStatus.PAUSED
|
|
534
|
+
case (
|
|
535
|
+
QueueStatus.AWAITING_RECOVERY
|
|
536
|
+
| QueueStatus.AWAITING_RECOVERY_PAUSED
|
|
537
|
+
):
|
|
538
|
+
self._state.queue_status = QueueStatus.AWAITING_RECOVERY_PAUSED
|
|
539
|
+
elif action.door_state == DoorState.CLOSED:
|
|
540
|
+
self._state.is_door_blocking = False
|
|
541
|
+
|
|
542
|
+
def _handle_set_error_recovery_policy_action(
|
|
543
|
+
self, action: SetErrorRecoveryPolicyAction
|
|
544
|
+
) -> None:
|
|
545
|
+
self._state.error_recovery_policy = action.error_recovery_policy
|
|
546
|
+
|
|
547
|
+
def _update_to_failed(
|
|
548
|
+
self,
|
|
549
|
+
command_id: str,
|
|
550
|
+
failed_at: datetime,
|
|
551
|
+
error_occurrence: Optional[ErrorOccurrence],
|
|
552
|
+
error_recovery_type: Optional[ErrorRecoveryType],
|
|
553
|
+
notes: Optional[List[CommandNote]],
|
|
554
|
+
) -> None:
|
|
555
|
+
prev_entry = self._state.command_history.get(command_id)
|
|
556
|
+
failed_command = prev_entry.command.model_copy(
|
|
557
|
+
update={
|
|
558
|
+
"completedAt": failed_at,
|
|
559
|
+
"status": CommandStatus.FAILED,
|
|
560
|
+
**({"error": error_occurrence} if error_occurrence is not None else {}),
|
|
561
|
+
# Assume we're not overwriting any existing notes because they can
|
|
562
|
+
# only be added when a command completes, and if we're failing this
|
|
563
|
+
# command, it wouldn't have completed before now.
|
|
564
|
+
**({"notes": notes} if notes is not None else {}),
|
|
565
|
+
}
|
|
566
|
+
)
|
|
567
|
+
self._state.command_history.set_command_failed(failed_command)
|
|
568
|
+
if error_recovery_type is not None:
|
|
569
|
+
self._state.command_error_recovery_types[command_id] = error_recovery_type
|
|
570
|
+
|
|
571
|
+
@staticmethod
|
|
572
|
+
def _map_run_exception_to_error_occurrence(
|
|
573
|
+
error_id: str, created_at: datetime, exception: Exception
|
|
574
|
+
) -> ErrorOccurrence:
|
|
575
|
+
"""Map a fatal exception from the main part of the run to an ErrorOccurrence."""
|
|
576
|
+
if (
|
|
577
|
+
isinstance(exception, ProtocolCommandFailedError)
|
|
578
|
+
and exception.original_error is not None
|
|
579
|
+
):
|
|
580
|
+
return exception.original_error
|
|
581
|
+
elif isinstance(exception, EnumeratedError):
|
|
582
|
+
return ErrorOccurrence.from_failed(
|
|
583
|
+
id=error_id, createdAt=created_at, error=exception
|
|
584
|
+
)
|
|
585
|
+
else:
|
|
586
|
+
enumerated_wrapper = UnexpectedProtocolError(
|
|
587
|
+
message=str(exception),
|
|
588
|
+
wrapping=[exception],
|
|
589
|
+
)
|
|
590
|
+
return ErrorOccurrence.from_failed(
|
|
591
|
+
id=error_id, createdAt=created_at, error=enumerated_wrapper
|
|
592
|
+
)
|
|
593
|
+
|
|
594
|
+
@staticmethod
|
|
595
|
+
def _map_finish_exception_to_error_occurrence(
|
|
596
|
+
error_id: str, created_at: datetime, exception: Exception
|
|
597
|
+
) -> ErrorOccurrence:
|
|
598
|
+
"""Map a fatal exception from the finish phase (drop tip & home) to an ErrorOccurrence."""
|
|
599
|
+
if isinstance(exception, EnumeratedError):
|
|
600
|
+
return ErrorOccurrence.from_failed(
|
|
601
|
+
id=error_id, createdAt=created_at, error=exception
|
|
602
|
+
)
|
|
603
|
+
else:
|
|
604
|
+
enumerated_wrapper = PythonException(exc=exception)
|
|
605
|
+
return ErrorOccurrence.from_failed(
|
|
606
|
+
id=error_id, createdAt=created_at, error=enumerated_wrapper
|
|
607
|
+
)
|
|
608
|
+
|
|
609
|
+
|
|
610
|
+
class CommandView:
|
|
611
|
+
"""Read-only command state view."""
|
|
612
|
+
|
|
613
|
+
_state: CommandState
|
|
614
|
+
|
|
615
|
+
def __init__(self, state: CommandState) -> None:
|
|
616
|
+
"""Initialize the view of command state with its underlying data."""
|
|
617
|
+
self._state = state
|
|
618
|
+
|
|
619
|
+
def get(self, command_id: str) -> Command:
|
|
620
|
+
"""Get a command by its unique identifier."""
|
|
621
|
+
return self._state.command_history.get(command_id).command
|
|
622
|
+
|
|
623
|
+
def get_all(self) -> List[Command]:
|
|
624
|
+
"""Get a list of all commands in state.
|
|
625
|
+
|
|
626
|
+
Entries are returned in the order of first-added command to last-added command.
|
|
627
|
+
Replacing a command (to change its status, for example) keeps its place in the
|
|
628
|
+
ordering.
|
|
629
|
+
"""
|
|
630
|
+
return self._state.command_history.get_all_commands()
|
|
631
|
+
|
|
632
|
+
def get_slice(
|
|
633
|
+
self, cursor: Optional[int], length: int, include_fixit_commands: bool
|
|
634
|
+
) -> CommandSlice:
|
|
635
|
+
"""Get a subset of commands around a given cursor.
|
|
636
|
+
|
|
637
|
+
If the cursor is omitted, a cursor will be selected automatically
|
|
638
|
+
based on the currently running or most recently executed command,
|
|
639
|
+
and the slice of commands returned is the previous `length` commands
|
|
640
|
+
inclusive of the currently running or most recently executed command.
|
|
641
|
+
"""
|
|
642
|
+
command_ids = self._state.command_history.get_filtered_command_ids(
|
|
643
|
+
include_fixit_commands=include_fixit_commands
|
|
644
|
+
)
|
|
645
|
+
total_length = len(command_ids)
|
|
646
|
+
|
|
647
|
+
if cursor is None:
|
|
648
|
+
current_pointer = self.get_current()
|
|
649
|
+
|
|
650
|
+
if current_pointer is not None:
|
|
651
|
+
cursor = current_pointer.index
|
|
652
|
+
else:
|
|
653
|
+
cursor = total_length - 1
|
|
654
|
+
|
|
655
|
+
cursor = max(cursor - length + 1, 0)
|
|
656
|
+
|
|
657
|
+
# start is inclusive, stop is exclusive
|
|
658
|
+
start = max(0, min(cursor, total_length - 1))
|
|
659
|
+
stop = min(total_length, start + length)
|
|
660
|
+
commands = self._state.command_history.get_slice(
|
|
661
|
+
start=start, stop=stop, command_ids=command_ids
|
|
662
|
+
)
|
|
663
|
+
|
|
664
|
+
return CommandSlice(
|
|
665
|
+
commands=commands,
|
|
666
|
+
cursor=start,
|
|
667
|
+
total_length=total_length,
|
|
668
|
+
)
|
|
669
|
+
|
|
670
|
+
def get_errors_slice(
|
|
671
|
+
self,
|
|
672
|
+
cursor: int,
|
|
673
|
+
length: int,
|
|
674
|
+
) -> CommandErrorSlice:
|
|
675
|
+
"""Get a subset of commands error around a given cursor."""
|
|
676
|
+
# start is inclusive, stop is exclusive
|
|
677
|
+
all_errors = self.get_all_errors()
|
|
678
|
+
total_length = len(all_errors)
|
|
679
|
+
actual_cursor = max(0, min(cursor, total_length - 1))
|
|
680
|
+
stop = min(total_length, actual_cursor + length)
|
|
681
|
+
|
|
682
|
+
sliced_errors = all_errors[actual_cursor:stop]
|
|
683
|
+
|
|
684
|
+
return CommandErrorSlice(
|
|
685
|
+
commands_errors=sliced_errors,
|
|
686
|
+
cursor=actual_cursor,
|
|
687
|
+
total_length=total_length,
|
|
688
|
+
)
|
|
689
|
+
|
|
690
|
+
def get_error(self) -> Optional[ErrorOccurrence]:
|
|
691
|
+
"""Get the run's fatal error, if there was one."""
|
|
692
|
+
run_error = self._state.run_error
|
|
693
|
+
finish_error = self._state.finish_error
|
|
694
|
+
|
|
695
|
+
if run_error and finish_error:
|
|
696
|
+
combined_error = ErrorOccurrence(
|
|
697
|
+
id=finish_error.id,
|
|
698
|
+
createdAt=finish_error.createdAt,
|
|
699
|
+
errorType="RunAndFinishFailed",
|
|
700
|
+
detail=(
|
|
701
|
+
"The run had a fatal error,"
|
|
702
|
+
" and another error happened while doing post-run cleanup."
|
|
703
|
+
),
|
|
704
|
+
# TODO(mm, 2023-07-31): Consider adding a low-priority error code so clients can
|
|
705
|
+
# deemphasize this root node, in favor of its children in wrappedErrors.
|
|
706
|
+
errorCode=ErrorCodes.GENERAL_ERROR.value.code,
|
|
707
|
+
wrappedErrors=[
|
|
708
|
+
run_error,
|
|
709
|
+
finish_error,
|
|
710
|
+
],
|
|
711
|
+
)
|
|
712
|
+
return combined_error
|
|
713
|
+
else:
|
|
714
|
+
return run_error or finish_error
|
|
715
|
+
|
|
716
|
+
def get_all_errors(self) -> List[ErrorOccurrence]:
|
|
717
|
+
"""Get the run's full error list, if there was none, returns an empty list."""
|
|
718
|
+
failed_commands = self._state.command_history.get_all_failed_commands()
|
|
719
|
+
return [
|
|
720
|
+
command_error.error
|
|
721
|
+
for command_error in failed_commands
|
|
722
|
+
if command_error.error is not None
|
|
723
|
+
]
|
|
724
|
+
|
|
725
|
+
def get_has_entered_recovery_mode(self) -> bool:
|
|
726
|
+
"""Get whether the run has entered recovery mode."""
|
|
727
|
+
return self._state.has_entered_error_recovery
|
|
728
|
+
|
|
729
|
+
def get_running_command_id(self) -> Optional[str]:
|
|
730
|
+
"""Return the ID of the command that's currently running, if there is one."""
|
|
731
|
+
running_command = self._state.command_history.get_running_command()
|
|
732
|
+
if running_command is not None:
|
|
733
|
+
return running_command.command.id
|
|
734
|
+
else:
|
|
735
|
+
return None
|
|
736
|
+
|
|
737
|
+
def get_queue_ids(self) -> OrderedSet[str]:
|
|
738
|
+
"""Get the IDs of all queued protocol commands, in FIFO order."""
|
|
739
|
+
return self._state.command_history.get_queue_ids()
|
|
740
|
+
|
|
741
|
+
def get_current(self) -> Optional[CommandPointer]:
|
|
742
|
+
"""Return the "current" command, if any.
|
|
743
|
+
|
|
744
|
+
The "current" command is the command that is currently executing,
|
|
745
|
+
or the most recent command to have completed.
|
|
746
|
+
"""
|
|
747
|
+
running_command = self._state.command_history.get_running_command()
|
|
748
|
+
if running_command:
|
|
749
|
+
return CommandPointer(
|
|
750
|
+
command_id=running_command.command.id,
|
|
751
|
+
command_key=running_command.command.key,
|
|
752
|
+
created_at=running_command.command.createdAt,
|
|
753
|
+
index=running_command.index,
|
|
754
|
+
)
|
|
755
|
+
|
|
756
|
+
most_recently_finalized_command = self.get_most_recently_finalized_command()
|
|
757
|
+
if most_recently_finalized_command:
|
|
758
|
+
return CommandPointer(
|
|
759
|
+
command_id=most_recently_finalized_command.command.id,
|
|
760
|
+
command_key=most_recently_finalized_command.command.key,
|
|
761
|
+
created_at=most_recently_finalized_command.command.createdAt,
|
|
762
|
+
index=most_recently_finalized_command.index,
|
|
763
|
+
)
|
|
764
|
+
|
|
765
|
+
return None
|
|
766
|
+
|
|
767
|
+
def get_next_to_execute(self) -> Optional[str]:
|
|
768
|
+
"""Return the next command in line to be executed.
|
|
769
|
+
|
|
770
|
+
Returns:
|
|
771
|
+
The ID of the earliest queued command, if any.
|
|
772
|
+
|
|
773
|
+
Raises:
|
|
774
|
+
RunStoppedError: The engine is currently stopped or stopping,
|
|
775
|
+
so it will never run any more commands.
|
|
776
|
+
"""
|
|
777
|
+
if self._state.run_result:
|
|
778
|
+
raise RunStoppedError("Engine was stopped")
|
|
779
|
+
|
|
780
|
+
# if queue is in recovery mode, return the next fixit command
|
|
781
|
+
next_fixit_cmd = self._state.command_history.get_fixit_queue_ids().head(None)
|
|
782
|
+
if next_fixit_cmd and self._state.queue_status == QueueStatus.AWAITING_RECOVERY:
|
|
783
|
+
return next_fixit_cmd
|
|
784
|
+
if (
|
|
785
|
+
next_fixit_cmd
|
|
786
|
+
and self._state.queue_status == QueueStatus.AWAITING_RECOVERY_PAUSED
|
|
787
|
+
and self._may_run_with_door_open(fixit_command=self.get(next_fixit_cmd))
|
|
788
|
+
):
|
|
789
|
+
return next_fixit_cmd
|
|
790
|
+
|
|
791
|
+
# if there is a setup command queued, prioritize it
|
|
792
|
+
next_setup_cmd = self._state.command_history.get_setup_queue_ids().head(None)
|
|
793
|
+
if (
|
|
794
|
+
self._state.queue_status
|
|
795
|
+
not in [QueueStatus.PAUSED, QueueStatus.AWAITING_RECOVERY]
|
|
796
|
+
and next_setup_cmd
|
|
797
|
+
):
|
|
798
|
+
return next_setup_cmd
|
|
799
|
+
|
|
800
|
+
# if the queue is running, return the next protocol command
|
|
801
|
+
if self._state.queue_status == QueueStatus.RUNNING:
|
|
802
|
+
return self._state.command_history.get_queue_ids().head(None)
|
|
803
|
+
|
|
804
|
+
# otherwise we've got nothing to do
|
|
805
|
+
return None
|
|
806
|
+
|
|
807
|
+
def get_is_okay_to_clear(self) -> bool:
|
|
808
|
+
"""Get whether the engine is stopped or sitting idly so it could be removed."""
|
|
809
|
+
if self.get_is_stopped():
|
|
810
|
+
return True
|
|
811
|
+
elif (
|
|
812
|
+
self.get_status() == EngineStatus.IDLE
|
|
813
|
+
and self._state.command_history.get_running_command() is None
|
|
814
|
+
and len(self._state.command_history.get_setup_queue_ids()) == 0
|
|
815
|
+
):
|
|
816
|
+
return True
|
|
817
|
+
else:
|
|
818
|
+
return False
|
|
819
|
+
|
|
820
|
+
def get_is_door_blocking(self) -> bool:
|
|
821
|
+
"""Get whether the robot door is open when 'pause on door open' ff is True."""
|
|
822
|
+
return self._state.is_door_blocking
|
|
823
|
+
|
|
824
|
+
def get_is_running(self) -> bool:
|
|
825
|
+
"""Get whether the protocol is running & queued commands should be executed."""
|
|
826
|
+
return self._state.queue_status == QueueStatus.RUNNING
|
|
827
|
+
|
|
828
|
+
def get_most_recently_finalized_command(self) -> Optional[CommandEntry]:
|
|
829
|
+
"""Get the most recent command that has reached its final `status`. See get_command_is_final."""
|
|
830
|
+
run_requested_to_stop = self._state.run_result is not None
|
|
831
|
+
|
|
832
|
+
if run_requested_to_stop:
|
|
833
|
+
tail_command = self._state.command_history.get_tail_command()
|
|
834
|
+
if not tail_command:
|
|
835
|
+
return None
|
|
836
|
+
if tail_command.command.status != CommandStatus.RUNNING:
|
|
837
|
+
return tail_command
|
|
838
|
+
else:
|
|
839
|
+
return self._state.command_history.get_prev(tail_command.command.id)
|
|
840
|
+
else:
|
|
841
|
+
most_recently_finalized = (
|
|
842
|
+
self._state.command_history.get_most_recently_completed_command()
|
|
843
|
+
)
|
|
844
|
+
# This iteration is effectively O(1) as we'll only ever have to iterate one or two times at most.
|
|
845
|
+
while most_recently_finalized is not None:
|
|
846
|
+
next_command = self._state.command_history.get_next(
|
|
847
|
+
most_recently_finalized.command.id
|
|
848
|
+
)
|
|
849
|
+
if (
|
|
850
|
+
next_command is not None
|
|
851
|
+
and next_command.command.status != CommandStatus.QUEUED
|
|
852
|
+
and next_command.command.status != CommandStatus.RUNNING
|
|
853
|
+
):
|
|
854
|
+
most_recently_finalized = next_command
|
|
855
|
+
else:
|
|
856
|
+
break
|
|
857
|
+
|
|
858
|
+
return most_recently_finalized
|
|
859
|
+
|
|
860
|
+
def get_command_is_final(self, command_id: str) -> bool:
|
|
861
|
+
"""Get whether a given command has reached its final `status`.
|
|
862
|
+
|
|
863
|
+
This happens when one of the following is true:
|
|
864
|
+
|
|
865
|
+
- Its status is `CommandStatus.SUCCEEDED`.
|
|
866
|
+
- Its status is `CommandStatus.FAILED`.
|
|
867
|
+
- Its status is `CommandStatus.QUEUED` but the run has been requested to stop,
|
|
868
|
+
so the run will never reach it.
|
|
869
|
+
|
|
870
|
+
Arguments:
|
|
871
|
+
command_id: Command to check.
|
|
872
|
+
"""
|
|
873
|
+
status = self.get(command_id).status
|
|
874
|
+
|
|
875
|
+
run_requested_to_stop = self._state.run_result is not None
|
|
876
|
+
|
|
877
|
+
return (
|
|
878
|
+
status == CommandStatus.SUCCEEDED
|
|
879
|
+
or status == CommandStatus.FAILED
|
|
880
|
+
or (status == CommandStatus.QUEUED and run_requested_to_stop)
|
|
881
|
+
)
|
|
882
|
+
|
|
883
|
+
def get_all_commands_final(self) -> bool:
|
|
884
|
+
"""Get whether all commands added so far have reached their final `status`.
|
|
885
|
+
|
|
886
|
+
See `get_command_is_final()`.
|
|
887
|
+
|
|
888
|
+
Raises:
|
|
889
|
+
CommandExecutionFailedError: if any added command failed, and its `intent` wasn't
|
|
890
|
+
`setup`.
|
|
891
|
+
"""
|
|
892
|
+
no_command_running = self._state.command_history.get_running_command() is None
|
|
893
|
+
run_requested_to_stop = self._state.run_result is not None
|
|
894
|
+
no_command_to_execute = (
|
|
895
|
+
run_requested_to_stop
|
|
896
|
+
# TODO(mm, 2024-03-15): This ignores queued setup commands,
|
|
897
|
+
# which seems questionable?
|
|
898
|
+
or len(self._state.command_history.get_queue_ids()) == 0
|
|
899
|
+
)
|
|
900
|
+
|
|
901
|
+
return no_command_running and no_command_to_execute
|
|
902
|
+
|
|
903
|
+
def get_recovery_target(self) -> Optional[CommandPointer]:
|
|
904
|
+
"""Return the command currently undergoing error recovery, if any."""
|
|
905
|
+
recovery_target = self._state.recovery_target
|
|
906
|
+
if recovery_target is None:
|
|
907
|
+
return None
|
|
908
|
+
else:
|
|
909
|
+
entry = self._state.command_history.get(recovery_target.command_id)
|
|
910
|
+
return CommandPointer(
|
|
911
|
+
command_id=entry.command.id,
|
|
912
|
+
command_key=entry.command.key,
|
|
913
|
+
created_at=entry.command.createdAt,
|
|
914
|
+
index=entry.index,
|
|
915
|
+
)
|
|
916
|
+
|
|
917
|
+
def get_recovery_in_progress_for_command(self, command_id: str) -> bool:
|
|
918
|
+
"""Return whether the given command failed and its error recovery is in progress."""
|
|
919
|
+
pointer = self.get_recovery_target()
|
|
920
|
+
return pointer is not None and pointer.command_id == command_id
|
|
921
|
+
|
|
922
|
+
def raise_fatal_command_error(self) -> None:
|
|
923
|
+
"""Raise the run's fatal command error, if there was one, as an exception.
|
|
924
|
+
|
|
925
|
+
The "fatal command error" is the error from any non-setup command.
|
|
926
|
+
It's intended to be used as the fatal error of the overall run
|
|
927
|
+
(see `ProtocolEngine.finish()`) for JSON and live HTTP protocols.
|
|
928
|
+
|
|
929
|
+
This isn't useful for Python protocols, which have to account for the
|
|
930
|
+
fatal error of the overall run coming from anywhere in the Python script,
|
|
931
|
+
including in between commands.
|
|
932
|
+
"""
|
|
933
|
+
failed_command = self._state.failed_command
|
|
934
|
+
if (
|
|
935
|
+
failed_command
|
|
936
|
+
and failed_command.command.error
|
|
937
|
+
and failed_command.command.intent != CommandIntent.SETUP
|
|
938
|
+
):
|
|
939
|
+
raise ProtocolCommandFailedError(
|
|
940
|
+
original_error=failed_command.command.error,
|
|
941
|
+
message=failed_command.command.error.detail,
|
|
942
|
+
)
|
|
943
|
+
|
|
944
|
+
def get_error_recovery_type(self, command_id: str) -> ErrorRecoveryType:
|
|
945
|
+
"""Return the error recovery type with which the given command failed.
|
|
946
|
+
|
|
947
|
+
The command ID is assumed to point to a failed command.
|
|
948
|
+
"""
|
|
949
|
+
return self._state.command_error_recovery_types[command_id]
|
|
950
|
+
|
|
951
|
+
def get_is_stopped(self) -> bool:
|
|
952
|
+
"""Get whether an engine stop has completed."""
|
|
953
|
+
return self._state.run_completed_at is not None
|
|
954
|
+
|
|
955
|
+
def get_is_stopped_by_estop(self) -> bool:
|
|
956
|
+
"""Return whether the engine was stopped specifically by an E-stop."""
|
|
957
|
+
return self._state.stopped_by_estop
|
|
958
|
+
|
|
959
|
+
def has_been_played(self) -> bool:
|
|
960
|
+
"""Get whether engine has started."""
|
|
961
|
+
return self._state.run_started_at is not None
|
|
962
|
+
|
|
963
|
+
def get_is_terminal(self) -> bool:
|
|
964
|
+
"""Get whether engine is in a terminal state."""
|
|
965
|
+
return self._state.run_result is not None
|
|
966
|
+
|
|
967
|
+
def validate_action_allowed( # noqa: C901
|
|
968
|
+
self,
|
|
969
|
+
action: Union[
|
|
970
|
+
PlayAction,
|
|
971
|
+
PauseAction,
|
|
972
|
+
StopAction,
|
|
973
|
+
ResumeFromRecoveryAction,
|
|
974
|
+
QueueCommandAction,
|
|
975
|
+
],
|
|
976
|
+
) -> Union[
|
|
977
|
+
PlayAction,
|
|
978
|
+
PauseAction,
|
|
979
|
+
StopAction,
|
|
980
|
+
ResumeFromRecoveryAction,
|
|
981
|
+
QueueCommandAction,
|
|
982
|
+
]:
|
|
983
|
+
"""Validate whether a given control action is allowed.
|
|
984
|
+
|
|
985
|
+
Returns:
|
|
986
|
+
The action, if valid.
|
|
987
|
+
|
|
988
|
+
Raises:
|
|
989
|
+
RunStoppedError: The engine has been stopped.
|
|
990
|
+
RobotDoorOpenError: Cannot resume because the front door is open.
|
|
991
|
+
PauseNotAllowedError: The engine is not running, so cannot be paused.
|
|
992
|
+
SetupCommandNotAllowedError: The engine is running, so a setup command
|
|
993
|
+
may not be added.
|
|
994
|
+
"""
|
|
995
|
+
if self._state.run_result is not None:
|
|
996
|
+
raise RunStoppedError("The run has already stopped.")
|
|
997
|
+
|
|
998
|
+
elif isinstance(action, PlayAction):
|
|
999
|
+
if self.get_status() in (
|
|
1000
|
+
EngineStatus.BLOCKED_BY_OPEN_DOOR,
|
|
1001
|
+
EngineStatus.AWAITING_RECOVERY_BLOCKED_BY_OPEN_DOOR,
|
|
1002
|
+
):
|
|
1003
|
+
raise RobotDoorOpenError("Front door or top window is currently open.")
|
|
1004
|
+
else:
|
|
1005
|
+
return action
|
|
1006
|
+
|
|
1007
|
+
elif isinstance(action, PauseAction):
|
|
1008
|
+
if not self.get_is_running():
|
|
1009
|
+
raise PauseNotAllowedError("Cannot pause a run that is not running.")
|
|
1010
|
+
elif self.get_status() == EngineStatus.AWAITING_RECOVERY:
|
|
1011
|
+
raise PauseNotAllowedError("Cannot pause a run in recovery mode.")
|
|
1012
|
+
else:
|
|
1013
|
+
return action
|
|
1014
|
+
|
|
1015
|
+
elif isinstance(action, QueueCommandAction):
|
|
1016
|
+
if (
|
|
1017
|
+
action.request.intent == CommandIntent.SETUP
|
|
1018
|
+
and self._state.queue_status != QueueStatus.SETUP
|
|
1019
|
+
):
|
|
1020
|
+
raise SetupCommandNotAllowedError(
|
|
1021
|
+
"Setup commands are not allowed after run has started."
|
|
1022
|
+
)
|
|
1023
|
+
elif action.request.intent == CommandIntent.FIXIT:
|
|
1024
|
+
if self.get_status() == EngineStatus.AWAITING_RECOVERY:
|
|
1025
|
+
return action
|
|
1026
|
+
elif self.get_status() in (
|
|
1027
|
+
EngineStatus.AWAITING_RECOVERY_BLOCKED_BY_OPEN_DOOR,
|
|
1028
|
+
EngineStatus.AWAITING_RECOVERY_PAUSED,
|
|
1029
|
+
):
|
|
1030
|
+
if self._may_run_with_door_open(fixit_command=action.request):
|
|
1031
|
+
return action
|
|
1032
|
+
else:
|
|
1033
|
+
raise FixitCommandNotAllowedError(
|
|
1034
|
+
f"{action.request.commandType} fixit command may not run"
|
|
1035
|
+
" until the door is closed and the run is played again."
|
|
1036
|
+
)
|
|
1037
|
+
else:
|
|
1038
|
+
raise FixitCommandNotAllowedError(
|
|
1039
|
+
"Fixit commands are not allowed when the run is not in a recoverable state."
|
|
1040
|
+
)
|
|
1041
|
+
else:
|
|
1042
|
+
return action
|
|
1043
|
+
|
|
1044
|
+
elif isinstance(action, ResumeFromRecoveryAction):
|
|
1045
|
+
if self.get_status() != EngineStatus.AWAITING_RECOVERY:
|
|
1046
|
+
raise ResumeFromRecoveryNotAllowedError(
|
|
1047
|
+
"Cannot resume from recovery if the run is not in recovery mode."
|
|
1048
|
+
)
|
|
1049
|
+
elif (
|
|
1050
|
+
self.get_status() == EngineStatus.AWAITING_RECOVERY
|
|
1051
|
+
and len(self._state.command_history.get_fixit_queue_ids()) > 0
|
|
1052
|
+
):
|
|
1053
|
+
raise ResumeFromRecoveryNotAllowedError(
|
|
1054
|
+
"Cannot resume from recovery while there are fixit commands in the queue."
|
|
1055
|
+
)
|
|
1056
|
+
else:
|
|
1057
|
+
return action
|
|
1058
|
+
|
|
1059
|
+
elif isinstance(action, StopAction):
|
|
1060
|
+
return action
|
|
1061
|
+
|
|
1062
|
+
else:
|
|
1063
|
+
assert_never(action)
|
|
1064
|
+
|
|
1065
|
+
def get_status(self) -> EngineStatus: # noqa: C901
|
|
1066
|
+
"""Get the current execution status of the engine."""
|
|
1067
|
+
if self._state.run_result:
|
|
1068
|
+
# The main part of the run is over, or will be over soon.
|
|
1069
|
+
# Have we also completed the post-run finish steps (homing and dropping tips)?
|
|
1070
|
+
if self.get_is_stopped():
|
|
1071
|
+
# Post-run finish steps have completed. Calculate the engine's final status,
|
|
1072
|
+
# taking into account any failures in the run or the post-run finish steps.
|
|
1073
|
+
if (
|
|
1074
|
+
self._state.run_result == RunResult.FAILED
|
|
1075
|
+
or self._state.finish_error is not None
|
|
1076
|
+
):
|
|
1077
|
+
return EngineStatus.FAILED
|
|
1078
|
+
elif self._state.run_result == RunResult.SUCCEEDED:
|
|
1079
|
+
return EngineStatus.SUCCEEDED
|
|
1080
|
+
else:
|
|
1081
|
+
return EngineStatus.STOPPED
|
|
1082
|
+
else:
|
|
1083
|
+
# Post-run finish steps have not yet completed,
|
|
1084
|
+
# and we may even still be executing commands.
|
|
1085
|
+
return (
|
|
1086
|
+
EngineStatus.STOP_REQUESTED
|
|
1087
|
+
if self._state.run_result == RunResult.STOPPED
|
|
1088
|
+
else EngineStatus.FINISHING
|
|
1089
|
+
)
|
|
1090
|
+
|
|
1091
|
+
elif self._state.queue_status == QueueStatus.RUNNING:
|
|
1092
|
+
return EngineStatus.RUNNING
|
|
1093
|
+
|
|
1094
|
+
elif self._state.queue_status == QueueStatus.PAUSED:
|
|
1095
|
+
if self._state.is_door_blocking:
|
|
1096
|
+
return EngineStatus.BLOCKED_BY_OPEN_DOOR
|
|
1097
|
+
else:
|
|
1098
|
+
return EngineStatus.PAUSED
|
|
1099
|
+
|
|
1100
|
+
elif self._state.queue_status == QueueStatus.AWAITING_RECOVERY_PAUSED:
|
|
1101
|
+
if self._state.is_door_blocking:
|
|
1102
|
+
return EngineStatus.AWAITING_RECOVERY_BLOCKED_BY_OPEN_DOOR
|
|
1103
|
+
else:
|
|
1104
|
+
return EngineStatus.AWAITING_RECOVERY_PAUSED
|
|
1105
|
+
|
|
1106
|
+
elif self._state.queue_status == QueueStatus.AWAITING_RECOVERY:
|
|
1107
|
+
return EngineStatus.AWAITING_RECOVERY
|
|
1108
|
+
|
|
1109
|
+
# todo(mm, 2024-03-19): Does this intentionally return idle if QueueStatus is
|
|
1110
|
+
# SETUP and we're currently a setup command?
|
|
1111
|
+
return EngineStatus.IDLE
|
|
1112
|
+
|
|
1113
|
+
def get_latest_protocol_command_hash(self) -> Optional[str]:
|
|
1114
|
+
"""Get the command hash of the last queued command, if any."""
|
|
1115
|
+
return self._state.latest_protocol_command_hash
|
|
1116
|
+
|
|
1117
|
+
def get_error_recovery_policy(self) -> ErrorRecoveryPolicy:
|
|
1118
|
+
"""Return the run's current error recovery policy (see `ErrorRecoveryPolicy`).
|
|
1119
|
+
|
|
1120
|
+
This error recovery policy is not ever evaluated by
|
|
1121
|
+
`CommandStore`/`CommandView`. It's stored here for convenience, but evaluated by
|
|
1122
|
+
higher-level code.
|
|
1123
|
+
"""
|
|
1124
|
+
return self._state.error_recovery_policy
|
|
1125
|
+
|
|
1126
|
+
def get_state_update_for_false_positive(self) -> update_types.StateUpdate:
|
|
1127
|
+
"""Return the state update for if the current recovery target was a false positive.
|
|
1128
|
+
|
|
1129
|
+
If we're currently in error recovery mode, and you have decided that the
|
|
1130
|
+
underlying command error was a false positive, this returns a state update
|
|
1131
|
+
that will undo the error's effects on engine state.
|
|
1132
|
+
See `ProtocolEngine.resume_from_recovery(reconcile_false_positive=True)`.
|
|
1133
|
+
"""
|
|
1134
|
+
if self._state.recovery_target is None:
|
|
1135
|
+
return update_types.StateUpdate() # Empty/no-op.
|
|
1136
|
+
else:
|
|
1137
|
+
return self._state.recovery_target.state_update_if_false_positive
|
|
1138
|
+
|
|
1139
|
+
def _may_run_with_door_open(
|
|
1140
|
+
self, *, fixit_command: Command | CommandCreate
|
|
1141
|
+
) -> bool:
|
|
1142
|
+
"""Return whether the given fixit command is exempt from the usual open-door auto pause.
|
|
1143
|
+
|
|
1144
|
+
This is required for certain error recovery flows, where we want the robot to
|
|
1145
|
+
do stuff while the door is open.
|
|
1146
|
+
"""
|
|
1147
|
+
# CommandIntent.PROTOCOL and CommandIntent.SETUP have their own rules for whether
|
|
1148
|
+
# they run while the door is open. Passing one of those commands to this function
|
|
1149
|
+
# is probably a mistake in the caller's logic.
|
|
1150
|
+
assert fixit_command.intent == CommandIntent.FIXIT
|
|
1151
|
+
|
|
1152
|
+
# These type annotations are to make sure the string constants stay in sync and aren't typo'd.
|
|
1153
|
+
allowed_command_types: Tuple[
|
|
1154
|
+
UnsafeUngripLabwareCommandType,
|
|
1155
|
+
UnsafeFlexStackerCloseLatchCommandType,
|
|
1156
|
+
UnsafeFlexStackerOpenLatchCommandType,
|
|
1157
|
+
] = (
|
|
1158
|
+
"unsafe/ungripLabware",
|
|
1159
|
+
"unsafe/flexStacker/closeLatch",
|
|
1160
|
+
"unsafe/flexStacker/openLatch",
|
|
1161
|
+
)
|
|
1162
|
+
# todo(mm, 2024-10-04): Instead of allowlisting command types, maybe we should
|
|
1163
|
+
# add a `mayRunWithDoorOpen: bool` field to command requests.
|
|
1164
|
+
return fixit_command.commandType in allowed_command_types
|